diff --git a/CHANGELOG.md b/CHANGELOG.md index d3af29be8..96f1b6ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,20 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.2...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 5.7.2 +[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.1...5.7.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.7.2/documentation/parseswift) + +__Fixes__ +* ParsePolygon encoding during a save and decoding resulted in (longitude, latitude) when it should be + (latitude, longitude). If a developer used ParseSwift <= 5.7.1 + to save/update ParsePolygon's, they will need to update the respective ParseObjects by swapping the latitude + and longitude manually. Deprecated withinPolygon() query constraint for geoPoint() and polygonContains() for + polygon() ([#118](https://github.com/netreconlab/Parse-Swift/pull/118)), thanks to +[Corey Baker](https://github.com/cbaker6). + ### 5.7.1 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.7.0...5.7.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.7.1/documentation/parseswift) diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index 72994795e..a5276d5d8 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -81,7 +81,7 @@ struct GameData: ParseObject { var originalData: Data? //: Your own properties. - var polygon: ParsePolygon? + var fence: ParsePolygon? //: `ParseBytes` needs to be a part of the original schema //: or else you will need your primaryKey to force an upgrade. var bytes: ParseBytes? @@ -92,9 +92,9 @@ struct GameData: ParseObject { */ func merge(with object: Self) throws -> Self { var updated = try mergeParse(with: object) - if shouldRestoreKey(\.polygon, + if shouldRestoreKey(\.fence, original: object) { - updated.polygon = object.polygon + updated.fence = object.fence } if shouldRestoreKey(\.bytes, original: object) { @@ -108,9 +108,9 @@ struct GameData: ParseObject { //: to preserve the memberwise initializer. extension GameData { - init (bytes: ParseBytes?, polygon: ParsePolygon) { + init (bytes: ParseBytes?, fence: ParsePolygon) { self.bytes = bytes - self.polygon = polygon + self.fence = fence } } @@ -396,18 +396,18 @@ Task { Task { //: How to add `ParseBytes` and `ParsePolygon` to objects. - let points = [ - try ParseGeoPoint(latitude: 0, longitude: 0), - try ParseGeoPoint(latitude: 0, longitude: 1), - try ParseGeoPoint(latitude: 1, longitude: 1), - try ParseGeoPoint(latitude: 1, longitude: 0), - try ParseGeoPoint(latitude: 0, longitude: 0) + let detroitPoints = [ + try ParseGeoPoint(latitude: 42.631655189280224, longitude: -83.78406753121705), + try ParseGeoPoint(latitude: 42.633047793854814, longitude: -83.75333640366955), + try ParseGeoPoint(latitude: 42.61625254348911, longitude: -83.75149921669944), + try ParseGeoPoint(latitude: 42.61526926650296, longitude: -83.78161794858735), + try ParseGeoPoint(latitude: 42.631655189280224, longitude: -83.78406753121705) ] do { - let polygon = try ParsePolygon(points) + let detroit = try ParsePolygon(detroitPoints) let bytes = ParseBytes(data: "hello world".data(using: .utf8)!) - var gameData = GameData(bytes: bytes, polygon: polygon) + var gameData = GameData(bytes: bytes, fence: detroit) gameData = try await gameData.save() print("Successfully saved: \(gameData)") } catch { diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index e893c7fd0..92fe228b8 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -32,7 +32,7 @@ struct GameScore: ParseObject { //: Your own properties var points: Int? var location: ParseGeoPoint? - var polygon: ParsePolygon? + var fence: ParsePolygon? /*: Optional - implement your own version of merge @@ -48,9 +48,9 @@ struct GameScore: ParseObject { original: object) { updated.location = object.location } - if updated.shouldRestoreKey(\.polygon, + if updated.shouldRestoreKey(\.fence, original: object) { - updated.polygon = object.polygon + updated.fence = object.fence } return updated } @@ -74,7 +74,7 @@ do { try .init(latitude: 42.0, longitude: -35.0), try .init(latitude: 42.0, longitude: -20.0) ] - score.polygon = try ParsePolygon(points) + score.fence = try ParsePolygon(points) } /*: @@ -90,7 +90,7 @@ score.save { result in assert(savedScore.updatedAt != nil) assert(savedScore.points == 10) assert(savedScore.location != nil) - assert(savedScore.polygon != nil) + assert(savedScore.fence != nil) guard let location = savedScore.location else { print("Something went wrong") @@ -99,13 +99,13 @@ score.save { result in print("Saved location: \(location)") - guard let polygon = savedScore.polygon else { + guard let fence = savedScore.fence else { print("Something went wrong") return } - print("Saved polygon: \(polygon)") - print("Saved polygon geopoints: \(polygon.coordinates)") + print("Saved polygon: \(fence)") + print("Saved polygon geopoints: \(fence.coordinates)") case .failure(let error): assertionFailure("Error saving: \(error)") @@ -293,7 +293,7 @@ do { try .init(latitude: 42.0, longitude: -35.0), try .init(latitude: 42.0, longitude: -20.0) ] - let query9 = GameScore.query(withinPolygon(key: "location", points: points)) + let query9 = GameScore.query(geoPoint("location", within: points)) query9.find { results in switch results { case .success(let scores): @@ -320,7 +320,7 @@ do { try .init(latitude: 42.0, longitude: -20.0) ] let polygon = try ParsePolygon(points) - let query10 = GameScore.query(withinPolygon(key: "location", polygon: polygon)) + let query10 = GameScore.query(geoPoint("location", within: polygon)) query10.find { results in switch results { case .success(let scores): @@ -339,6 +339,27 @@ do { print("Could not create geopoints: \(error)") } +do { + let location = try ParseGeoPoint(latitude: 40.0, longitude: -30.0) + let query = GameScore.query(polygon("fence", contains: location)) + query.find { results in + switch results { + case .success(let scores): + scores.forEach { (score) in + print(""" + Someone has a points value of \"\(String(describing: score.points))\" + with a geolocation \(location) within the + polygon: \(String(describing: score.location)) + """) + } + case .failure(let error): + assertionFailure("Error querying: \(error)") + } + } +} catch { + print("Could not create geopoints: \(error)") +} + //: Hint of the previous query (asynchronous) query2 = query2.hint("_id_") query2.find { result in diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index c4917e148..daa8420f0 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -10,7 +10,7 @@ import Foundation enum ParseConstants { static let sdk = "swift" - static let version = "5.7.1" + static let version = "5.7.2" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" diff --git a/Sources/ParseSwift/Types/ParsePolygon.swift b/Sources/ParseSwift/Types/ParsePolygon.swift index d910e2e28..0ee84c719 100644 --- a/Sources/ParseSwift/Types/ParsePolygon.swift +++ b/Sources/ParseSwift/Types/ParsePolygon.swift @@ -14,6 +14,7 @@ public struct ParsePolygon: ParseTypeable, Hashable { private let __type: String = "Polygon" // swiftlint:disable:this identifier_name public let coordinates: [ParseGeoPoint] + var isSwappingCoordinates = false enum CodingKeys: String, CodingKey { case __type // swiftlint:disable:this identifier_name @@ -108,6 +109,10 @@ extension ParsePolygon { try container.encode(__type, forKey: .__type) var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: .coordinates) try coordinates.forEach { + guard isSwappingCoordinates else { + try nestedUnkeyedContainer.encode([$0.latitude, $0.longitude]) + return + } try nestedUnkeyedContainer.encode([$0.longitude, $0.latitude]) } } @@ -124,8 +129,8 @@ extension ParsePolygon { let points = try values.decode([[Double]].self, forKey: .coordinates) try points.forEach { if $0.count == 2 { - guard let latitude = $0.last, - let longitude = $0.first else { + guard let latitude = $0.first, + let longitude = $0.last else { throw ParseError(code: .otherCause, message: "Could not decode ParsePolygon: \(points)") } decodedCoordinates.append(try ParseGeoPoint(latitude: latitude, diff --git a/Sources/ParseSwift/Types/QueryConstraint.swift b/Sources/ParseSwift/Types/QueryConstraint.swift index 84374138d..11bd94010 100644 --- a/Sources/ParseSwift/Types/QueryConstraint.swift +++ b/Sources/ParseSwift/Types/QueryConstraint.swift @@ -587,11 +587,28 @@ public func withinGeoBox(key: String, fromSouthWest southwest: ParseGeoPoint, - warning: Requires Parse Server 2.5.0+. - returns: The same instance of `QueryConstraint` as the receiver. */ -public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstraint { +public func geoPoint(_ key: String, within points: [ParseGeoPoint]) -> QueryConstraint { let dictionary = [QueryConstraint.Comparator.polygon.rawValue: points] return .init(key: key, value: dictionary, comparator: .geoWithin) } +/** + Add a constraint to the query that requires a particular key's + coordinates be contained within and on the bounds of a given polygon + Supports closed and open (last point is connected to first) paths. + + Polygon must have at least 3 points. + + - parameter key: The key to be constrained. + - parameter points: The polygon points as an Array of `ParseGeoPoint`'s. + - warning: Requires Parse Server 2.5.0+. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +@available(*, deprecated, renamed: "geoPoint") +public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstraint { + geoPoint(key, within: points) +} + /** Add a constraint to the query that requires a particular key's coordinates be contained within and on the bounds of a given polygon @@ -602,11 +619,28 @@ public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstrai - warning: Requires Parse Server 2.5.0+. - returns: The same instance of `QueryConstraint` as the receiver. */ -public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint { +public func geoPoint(_ key: String, within polygon: ParsePolygon) -> QueryConstraint { + var polygon = polygon + polygon.isSwappingCoordinates = true let dictionary = [QueryConstraint.Comparator.polygon.rawValue: polygon] return .init(key: key, value: dictionary, comparator: .geoWithin) } +/** + Add a constraint to the query that requires a particular key's + coordinates be contained within and on the bounds of a given polygon + Supports closed and open (last point is connected to first) paths. + + - parameter key: The key to be constrained. + - parameter polygon: The `ParsePolygon`. + - warning: Requires Parse Server 2.5.0+. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +@available(*, deprecated, renamed: "geoPoint") +public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint { + geoPoint(key, within: polygon) +} + /** Add a constraint to the query that requires a particular key's coordinates contains a `ParseGeoPoint`. @@ -616,11 +650,25 @@ public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint - warning: Requires Parse Server 2.6.0+. - returns: The same instance of `QueryConstraint` as the receiver. */ -public func polygonContains(key: String, point: ParseGeoPoint) -> QueryConstraint { +public func polygon(_ key: String, contains point: ParseGeoPoint) -> QueryConstraint { let dictionary = [QueryConstraint.Comparator.point.rawValue: point] return .init(key: key, value: dictionary, comparator: .geoIntersects) } +/** + Add a constraint to the query that requires a particular key's + coordinates contains a `ParseGeoPoint`. + + - parameter key: The key of the `ParsePolygon`. + - parameter point: The `ParseGeoPoint` to check for containment. + - warning: Requires Parse Server 2.6.0+. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +@available(*, deprecated, renamed: "polygon") +public func polygonContains(key: String, point: ParseGeoPoint) -> QueryConstraint { + polygon(key, contains: point) +} + /** Add a constraint for finding string values that contain a provided string using Full Text Search. diff --git a/Tests/ParseSwiftTests/ParsePolygonTests.swift b/Tests/ParseSwiftTests/ParsePolygonTests.swift index 547362ec5..d599f5a03 100644 --- a/Tests/ParseSwiftTests/ParsePolygonTests.swift +++ b/Tests/ParseSwiftTests/ParsePolygonTests.swift @@ -76,7 +76,7 @@ class ParsePolygonTests: XCTestCase { func testEncode() throws { let polygon = try ParsePolygon(points) - let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[1,0],[1,1],[0,1],[0,0]]}" + let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]}" XCTAssertEqual(polygon.debugDescription, expected) guard polygon.coordinates.count == points.count else { XCTAssertEqual(polygon.coordinates.count, points.count) @@ -88,7 +88,8 @@ class ParsePolygonTests: XCTestCase { } func testDecode() throws { - let polygon = try ParsePolygon(points) + var polygon = try ParsePolygon(points) + polygon.isSwappingCoordinates = false let encoded = try ParseCoding.jsonEncoder().encode(polygon) let decoded = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded) XCTAssertEqual(decoded, polygon) @@ -141,7 +142,7 @@ class ParsePolygonTests: XCTestCase { func testDescription() throws { let polygon = try ParsePolygon(points) - let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[1,0],[1,1],[0,1],[0,0]]}" + let expected = "{\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]}" XCTAssertEqual(polygon.description, expected) } }