OGC GeoPackage (.gpkg) read and write support for Swift, built on top of gis-tools.
- Feature tables — read/write vector features with full geometry and attribute support
- Spatial index (rtree) — accelerated bounding-box queries via
gpkg_rtree_index - Tile pyramids — read/write raster tile blobs (PNG/JPEG/WebP) at multiple zoom levels
- Attribute tables — non-spatial data with
data_type = "attributes" - Media tables — blob storage linked to features (Related Tables Extension)
- Relationships —
gpkgext_relationsfor linking feature, attribute, and media tables - Schema validation —
GeoPackage.validate(url:)checks spec compliance - Async streaming —
FeatureStreamyields features one at a time viaAsyncSequence - Thread-safe connection —
GeoPackageConnectionactor with paginated reads and incremental writes
Swift 6.1 or higher. Compiles on iOS (≥ iOS 15), macOS (≥ macOS 15), tvOS (≥ tvOS 15), watchOS (≥ watchOS 7), Linux, Android and Wasm. Requires libsqlite3 (available by default on all supported platforms).
dependencies: [
.package(url: "https://github.com/Outdooractive/gis-tools-geopackage", from: "1.0.0"),
.package(url: "https://github.com/Outdooractive/gis-tools", from: "2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "GISToolsGeoPackage", package: "gis-tools-geopackage"),
.product(name: "GISTools", package: "gis-tools"),
]),
]import GISTools
import GISToolsGeoPackage
// Read all features:
let fc = try await FeatureCollection(geopackage: url, table: "features")
// Write features:
try await fc.writeGeopackage(to: url, table: "features")let gpkg = try await GeoPackageConnection(url: url)
defer { await gpkg.close() }
// Paginated read:
let page = try await gpkg.readFeatures(table: "countries", limit: 100, offset: 0)
// Async stream:
for try await feature in gpkg.features(table: "countries") {
// process one at a time
}
// Incremental write:
try await gpkg.createMetadata()
try await gpkg.write(features: batch1, to: "places")
try await gpkg.write(features: batch2, to: "places")let bbox = BoundingBox(
southWest: Coordinate3D(latitude: 35.0, longitude: -10.0),
northEast: Coordinate3D(latitude: 60.0, longitude: 40.0))
let filtered = try await FeatureCollection(
geopackage: url, table: "countries",
boundingBox: bbox)// Read a single tile:
let data = try await gpkg.readTile(
for: TileKey(zoom: 5, column: 12, row: 8),
from: "tiles")
// Write a tile pyramid:
try await gpkg.writeTilePyramid(
tiles: tileData, to: "tiles",
matrixSetBounds: bounds, matrices: matrices)// Read a non-spatial attribute table:
let attrs = try await gpkg.readAttributeTable(table: "station_info", rowId: 42)
// Read media rows:
let media = try await gpkg.readMediaRows(table: "photos", rowId: 1)let result = try GeoPackage.validate(url: url)
if result.isValid { /* conforms to spec */ }When features are loaded from a GeoPackage, the row ID and table name are stored in foreignMembers:
let feature = fc.features[0]
print(feature.gpkgRowId) // Int?
print(feature.gpkgTableName) // String?
// Look up related data:
let related = try await feature.relatedAttributes(in: gpkg)
let media = try await feature.relatedMedia(in: gpkg)GeoPackage feature tables distinguish between two kinds of identifier. This library handles both:
The OGC GeoPackage spec requires every feature table to have an INTEGER PRIMARY KEY column that acts as a rowid alias. GeoJSON Feature.id values, however, can be strings, integers, or doubles — not necessarily numeric. This library therefore writes two columns:
-
fid—INTEGER PRIMARY KEY AUTOINCREMENT. This is the spec-mandated primary key; it is auto-assigned by SQLite and never derived fromFeature.id. -
id— stores the GeoJSONFeature.id. Its SQLite type is inferred from the identifiers in the collection so that uniform collections round-trip with type fidelity:Feature.idvalues in the collectionidcolumn typeRead-back Feature.idall .int/.uintINTEGER.intall .doubleREAL.doubleall .stringTEXT.stringmixed types (or none) TEXT.string(fallback)Mixed-type collections fall back to
TEXTbecause no single SQLite type can hold every value; integer ids in such a collection come back as.string(e.g..int(42)→.string("42")).
feature.gpkgRowId reflects the fid INTEGER primary key (the SQLite rowid), not the GeoJSON Feature.id. It is stored in foreignMembers (key _gpkg_rowid) and is set automatically by the reader. It is the value used to resolve related attribute/media rows:
let feature = fc.features[0]
feature.id // Feature.Identifier? — the GeoJSON id (from the `id` column)
feature.gpkgRowId // Int? — the SQLite rowid (from the `fid` PK)On read-back, the id/fid column value is promoted to Feature.id based on the runtime SQLite cell type (Int → .int, Double → .double, String → .string, NULL → nil), so typed columns round-trip with type fidelity. The fid/id columns are never leaked into feature.properties.
let tables = try await gpkg.readContents()
let featureTables = try await gpkg.featureTables()
let tileTables = try await gpkg.tileTables()Feature properties (attribute columns other than fid, id, and the geometry column) are mapped as follows:
| GeoPackage type | Swift type |
|---|---|
INTEGER |
Int |
REAL |
Double |
TEXT |
String |
BLOB |
String (Base64) |
BOOLEAN |
Bool |
Please create an issue or open a pull request with a fix or enhancement.
MIT
Thomas Rasch, Outdooractive
Built on top of gis-tools.