GEOSwift was started in May 2015 by Andrea Cremaschi. At this time Swift 1.2 had been out for just over a month. Over the years, the library and Swift both grew and evolved, and new features in Swift like the error handling model (Swift 2) and Codable (Swift 4) started to make GEOSwift feel a bit less Swifty than it did originally. Even early on, there was a desire for GEOSwift to use structs instead of classes, but the design did not allow it.
Additionally, other desires were difficult to address within the parameters of the original design:
- Thread safety
- Increased test coverage (version 4 was at 71%)
- Support for general-purpose 2D geometry
- Useful errors when GeoJSON decoding fails
Version 5 brings a completely re-imagined, yet familiar, design to GEOSwift. The changes can be grouped into 3 major areas:
- Confine the use of GEOS
- Move map-based features into a separate library
- Adopt modern Swift features
Prior to version 5, each GEOSwift object was backed by a GEOS object. This meant that GEOSwift objects needed to carefully manage C pointers and could not be safely shared across threads. This was also the reason why it used classes instead of structs. Furthermore, the library used a single, shared context for all interaction with GEOS. Even though it was using the thread-safe GEOS API, that API only achieves thread-safety by allowing you to create separate contexts per thread.
In version 5, GEOSwift represents all geometry types as Swift structs. GEOS is still used, but only as an implementation detail for certain methods. In fact, GEOS contexts never last beyond a single method call. This means that you can pass GEOSwift values between threads and threads will never share GEOS contexts, which, combined with the value semantics of struct, results in a thread-safe API.
Additionally, this approach of confining the use of GEOS within the library, opens up interesting future possibilities like splitting GEOS-dependent features into a separate library for people who would prefer to avoid the GEOS dependency or replacing GEOS-based implementations (which require careful error handling) with Swift-native implementations.
One of the appeals of GEOSwift 4 and earlier was its tight-knit integration with MapKit and QuickLook. These features made it fun for experimentation in playgrounds and quick to get something working in iOS projects.
Even so, there were other use cases that were made more difficult by the inclusion of these features. Some developers were interested in using GEOSwift for general-purpose geometry, but the QuickLook integration could crash if your geometry was outside of the typical bounds of latitude and longitude. Others wanted to use the library on Linux where UIKit and MapKit are not available.
To address these needs, version 5 moves the MapKit and UIKit-dependent features, including QuickLook and the demo playground, into a separate library called GEOSwiftMapKit. This allows us to broaden the usefulness of the library to include all kinds of general-purpose 2D geometry needs (not just geography). This is possible because GEOS is already a general-purpose geometry engine.
This change suggested that we ought to re-brand the library from "The Swift Geographic Engine" to "The Swift Geometry Engine".
By adopting the Swift error handling model, version 5 reduces the use of optionals, and can now bubble up errors from GEOS in a more programmer-friendly way.
In adopting Codable, GEOSwift is now easy to combine with your app's own API request and response objects that need to handle GeoJSON. You also get more helpful error messages when decoding fails.
In keeping with the GeoJSON spec, the top-level object in GeoJSON data can
either be a feature collection, a feature, or a geometry. GEOSwift's GeoJSON
enum models this directly using enums with associated values, so when
you decode to that type, you then need to see which top-level type you received:
let myGeoJSON = try decoder.decode(GeoJSON.self, from: data)
switch myGeoJSON {
case let .featureCollection(myFeatureCollection):
// the variable myFeatureCollection is now bound to the associated value and is of type FeatureCollection
case let .feature(myFeature):
// the variable myFeature is now bound to the associated value and is of type Feature
case let .geometry(myGeometry):
// the variable myGeometry is now bound to the associated value and is of type Geometry
}
FeatureCollection
and Feature
are structs, but Geometry
follows the same
pattern as GeoJSON
, so when you need to, you can access its associated values
too using the same technique.
One nice thing about pattern matching is that you can compose patterns, so if, for example, you only cared about the case where the GeoJSON was a line string, you could write:
switch myGeoJSON {
case let .geometry(.lineString(myLineString)):
// the variable myLineString is now bound to the associated value and is of type LineString
default:
// your data was of some other type
}
In the above situation you could also use guard
:
guard let .geometry(.lineString(myLineString)) = myGeoJSON else {
// your data was of some other type
return / throw / etc
}
// the variable myLineString is now bound to the associated value and is of type LineString
if
works in a similar way.
One final gotcha about this is that if myGeoJSON
is optional (type GeoJSON?
instead of GeoJSON
), you'll need to either unwrap it before you
switch
/guard
/if
or incorporate that into the pattern matching. Here's what
that would look like in the previous example:
guard let .some(.geometry(.lineString(myLineString))) = mySomething else {
// your data was of some other type
return / throw / etc
}
// the variable myLineString is now bound to the associated data and is of type LineString
Version 4 API | Version 5 Equivalent | Moved to GEOSwiftMapKit? |
---|---|---|
CLLocationCoordinate2D.init(_ coord: Coordinate) | CLLocationCoordinate2D.init(_ point: Point) | Yes |
Coordinate | Point | |
Coordinate.init(_ coord: CLLocationCoordinate2D) | Point.init(_ coordinate: CLLocationCoordinate2D) | Yes |
CoordinateDegrees | Double | |
CoordinatesCollection | [Point] | |
Envelope | Envelope | |
Envelope.bottomLeft: Coordinate | Envelope.minXMinY: Point | |
Envelope.bottomRight: Coordinate | Envelope.maxXMinY: Point | |
Envelope.byExpanding(_ base: Envelope, toInclude geom: Geometry) -> Envelope? | GeometryCollection.init(geometries: [GeometryConvertible]).envelope() throws -> Envelope | |
Envelope.byExpanding(_ base: Envelope, toIncludeCoordinate coord: Coordinate) -> Envelope? | GeometryCollection.init(geometries: [GeometryConvertible]).envelope() throws -> Envelope | |
Envelope.init?(p1: Coordinate, p2: Coordinate) | GeometryCollection.init(geometries: [GeometryConvertible]).envelope() throws -> Envelope | |
Envelope.maxX: Double | Envelope.maxX: Double | |
Envelope.maxY: Double | Envelope.maxY: Double | |
Envelope.minX: Double | Envelope.minX: Double | |
Envelope.minY: Double | Envelope.minY: Double | |
Envelope.topLeft: Coordinate | Envelope.minXMaxY: Point | |
Envelope.topRight: Coordinate | Envelope.maxXMaxY: Point | |
Feature.geometries: [GEOSwift.Geometry]? | Feature.geometry: Geometry? | |
Feature.id: Any? | Feature.id: Feature.FeatureId? | |
Feature.properties: NSDictionary? | Feature.properties: [String : JSON]? | |
Features | FeatureCollection | |
Features.fromGeoJSON(_ data: Data) throws -> Features? | FeatureCollection: Decodable | |
Features.fromGeoJSON(_ string: String) throws -> Features? | FeatureCollection: Decodable | |
Features.fromGeoJSON(_ URL: URL) throws -> Features? | FeatureCollection: Decodable | |
Features.fromGeoJSONDictionary(_ dictionary: [String : AnyObject]) -> Features? | FeatureCollection: Decodable | |
GeometriesCollection | [GeometryConvertible] | |
Geometry.area() -> Double? | GeometryConvertible.area() throws -> Double | |
Geometry.boundary() -> Geometry? | Boundable.boundary() throws -> Geometry | |
Geometry.buffer(width: Double) -> Geometry? | GeometryConvertible.buffer(by width: Double) throws -> Geometry | |
Geometry.centroid() -> Waypoint? | GeometryConvertible.centroid() throws -> Point | |
Geometry.contains(_ geometry: Geometry) -> Bool | GeometryConvertible.contains(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.convexHull() -> Polygon? | GeometryConvertible.convexHull() throws -> Geometry | |
Geometry.covers(_ geometry: Geometry) -> Bool | GeometryConvertible.covers(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.create(_ WKB: UnsafePointer, size: Int) -> Geometry? | WKBInitializable.init(wkb: Data) throws | |
Geometry.create(_ WKT: String) -> Geometry? | WKTInitializable.init(wkt: String) throws | |
Geometry.crosses(_ geometry: Geometry) -> Bool | GeometryConvertible.crosses(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.difference(_ geometry: Geometry) -> Geometry? | GeometryConvertible.difference(with geometry: GeometryConvertible) throws -> Geometry | |
Geometry.disjoint(_ geometry: Geometry) -> Bool | GeometryConvertible.isDisjoint(with geometry: GeometryConvertible) throws -> Bool | |
Geometry.distance(geometry: Geometry) -> Double | GeometryConvertible.distance(to geometry: GeometryConvertible) throws -> Double | |
Geometry.envelope() -> Envelope? | GeometryConvertible.envelope() throws -> Envelope | |
Geometry.equals(_ geometry: Geometry) -> Bool | GeometryCollection.isTopologicallyEquivalent(to geometry: GeometryConvertible) throws -> Bool | |
Geometry.geometryTypeId() -> Int32 | Removed | |
Geometry.init?(data: Data) | WKBInitializable.init(wkb: Data) throws | |
Geometry.init?(WKB: [UInt8]) | WKBInitializable.init(wkb: Data) throws | |
Geometry.init?(WKT: String) | WKTInitializable.init(wkt: String) throws | |
Geometry.intersection(_ geometry: Geometry) -> Geometry? | GeometryConvertible.intersection(with geometry: GeometryConvertible) throws -> Geometry | |
Geometry.intersects(_ geometry: Geometry) -> Bool | GeometryConvertible.intersects(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.mapShape() -> MKShape? | MKPointAnnotation.init(point: Point) MKPlacemark.init(point: Point) MKPolyline.init(lineString: LineString) MKPolygon.init(linearRing: Polygon.LinearRing) MKPolygon.init(polygon: Polygon) GeometryMapShape.init(geometry: GeometryConvertible) throws |
Yes |
Geometry.nearestPoint(_ geometry: Geometry) -> Coordinate | GeometryConvertible.nearestPoints(with geometry: GeometryConvertible)[0] | |
Geometry.nearestPoints(_ geometry: Geometry) -> [Coordinate] | GeometryConvertible.nearestPoints(with geometry: GeometryConvertible) throws -> [Point] | |
Geometry.overlaps(_ geometry: Geometry) -> Bool | GeometryConvertible.overlaps(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.pointOnSurface() -> Waypoint? | GeometryConvertible.pointOnSurface() throws -> Point | |
Geometry.relate(_ geometry: Geometry, pattern: String) -> Bool | GeometryConvertible.relate(_ geometry: GeometryConvertible, mask: String) throws -> Bool | |
Geometry.relationship(_ geometry: Geometry) -> String | GeometryConvertible.relate(_ geometry: GeometryConvertible) throws -> String | |
Geometry.touches(_ geometry: Geometry) -> Bool | GeometryConvertible.touches(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.unaryUnion() -> Geometry? | GeometryConvertible.unaryUnion() throws -> Geometry | |
Geometry.union(_ geometry: Geometry) -> Geometry? | GeometryConvertible.union(with geometry: GeometryConvertible) throws -> Geometry | |
Geometry.within(_ geometry: Geometry) -> Bool | GeometryConvertible.isWithin(_ geometry: GeometryConvertible) throws -> Bool | |
Geometry.WKB: [UInt8]? | WKBConvertible.wkb() throws -> Data | |
Geometry.WKT: String? | WKTConvertible.wkt() throws -> String | |
GeometryCollection.init?(geometries: [T]) where T : Geometry | GeometryCollection.init(geometries: [GeometryConvertible]) | |
HumboldtVersionNumber | GEOSwiftVersionNumber | |
LineString.distanceFromOriginToProjectionOfPoint(point: Waypoint) -> Double | LineStringConvertible.distanceFromStart(toProjectionOf point: Point) throws -> Double | |
LineString.init?(points: [Coordinate]) | LineString.init(points: [Point]) throws | |
LineString.interpolatePoint(distance: Double) -> Waypoint? | LineStringConvertible.interpolatedPoint(withDistance distance: Double) throws -> Point | |
LineString.interpolatePoint(fraction: Double) -> Waypoint? | LineStringConvertible.interpolatedPoint(withFraction fraction: Double) throws -> Point | |
LineString.middlePoint() -> Waypoint? | LineStringConvertible.interpolatedPoint(withFraction: 0.5) throws -> Point | |
LineString.normalizedDistanceFromOriginToProjectionOfPoint(point: Waypoint) -> Double | LineStringConvertible.normalizedDistanceFromStart(toProjectionOf point: Point) throws -> Double | |
MKShapesCollection | GeometryMapShape | Yes |
Polygon.exteriorRing | Polygon.exterior | |
Polygon.init?(shell: LinearRing, holes: [LinearRing]?) | Polygon.init(exterior: Polygon.LinearRing, holes: [Polygon.LinearRing] = []) | |
Polygon.interiorRings | Polygon.holes | |
Waypoint | Point | |
Waypoint.coordinate: Coordinate | Point | |
Waypoint.init?(latitude: CoordinateDegrees, longitude: CoordinateDegrees) | Point.init(x: Double, y: Double) | |
Waypoint.init?(latitude: CoordinateDegrees, longitude: CoordinateDegrees) | Point.init(longitude: Double, latitude: Double) | Yes |
The new GEOSwift is all struct based, so to add ObjC support, we'd either need to switch back to using classes or create a set of wrapper classes. Neither of these options make much sense right now though: Switching to classes would lose many of the advantages of the new design, and it's not clear how we could make the MapKit and QuickLook add-ons work with both libraries. Initially it seemed like ObjC support was a must-have to match the features of the previous version, but after reviewing the existing ObjC interface, it turns out that there was never much ObjC support to begin with. If the community still wants this badly enough, we should consider spinning up a sister project (GEOObjC?) so that we can focus on making GEOSwift a truly first-rate experience for Swift projects.