diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d59f5..7b5d603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ ##### Enhancements +* Extend CLLocationCoordinate2D with isValid computed property +[Andrew Winn](https://github.com/andrew-winn-br) +[#102](https://github.com/BottleRocketStudios/iOS-UtiliKit/pull/102) + * Improve support for registering supplementary and decoration views with UICollectionView. [Will McGinty](https://github.com/willmcginty) [#110](https://github.com/BottleRocketStudios/iOS-UtiliKit/pull/110) diff --git a/Sources/UtiliKit/General/CLLocationCoordinate2D+Extensions.swift b/Sources/UtiliKit/General/CLLocationCoordinate2D+Extensions.swift new file mode 100644 index 0000000..47dc019 --- /dev/null +++ b/Sources/UtiliKit/General/CLLocationCoordinate2D+Extensions.swift @@ -0,0 +1,32 @@ +// +// CLLocationCoordinate2D+Extensions.swift +// UtiliKit-iOS +// +// Created by Andrew Winn on 8/24/21. +// Copyright © 2021 Bottle Rocket Studios. All rights reserved. +// + +import CoreLocation + +public extension CLLocationCoordinate2D { + + /// Checks if a coordinate is valid. Attempting to use an invalid coordinate (e.g. by setting a map region to center on it) will crash the app. + /// + /// A coordinate is considered **invalid** if it meets at least one of the following criteria: + /// - Its latitude is greater than 90 degrees or less than -90 degrees. + /// - Its longitude is greater than 180 degrees or less than -180 degrees. + /// + /// An invalid coordinate can be generated from: + /// - Invalid data from an API + /// - `mkMapView.userLocation.coordinate` is not an optional property and will provide an invalid coordinate when the user is not sharing their location. + /// - `locationManager.location.coordinate` is an optional property but may still provide an invalid coordinate (e.g. when the user has revoked location permissions while the app is running after previously granting) + var isValid: Bool { + if CLLocationCoordinate2DIsValid(self) { + return true + } + debugPrint("================================================") + debugPrint("Invalid CLLocationCoordinate2D Detected: \(self)") + debugPrint("================================================") + return false + } +} diff --git a/Tests/UtiliKitTests.swift b/Tests/UtiliKitTests.swift index eff3d38..a471aa6 100644 --- a/Tests/UtiliKitTests.swift +++ b/Tests/UtiliKitTests.swift @@ -6,6 +6,7 @@ // import XCTest +import CoreLocation @testable import UtiliKit class OpenSourceUtilitiesTests: XCTestCase { @@ -77,4 +78,45 @@ class OpenSourceUtilitiesTests: XCTestCase { XCTAssertEqual(superview.bounds.inset(by: insets), view.frame) } + + //MARK: - CLLocationCoordinate2D Tests + func test_CLLocationCoordinate2D_Valid() { + let coordinates = [CLLocationCoordinate2D(latitude: CLLocationDegrees(0), + longitude: CLLocationDegrees(0)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(-0), + longitude: CLLocationDegrees(-0)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(90), + longitude: CLLocationDegrees(180)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(-90), + longitude: CLLocationDegrees(180)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(90), + longitude: CLLocationDegrees(-180)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(-90), + longitude: CLLocationDegrees(-180)) + ] + + XCTAssertTrue(coordinates.allSatisfy { $0.isValid }, "All test coordinates should be valid") + } + + func test_CLLocationCoordinate2d_Invalid() { + let coordinates = [CLLocationCoordinate2D(latitude: CLLocationDegrees(95), + longitude: CLLocationDegrees(0)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(-95), + longitude: CLLocationDegrees(0)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(0), + longitude: CLLocationDegrees(185)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(0), + longitude: CLLocationDegrees(-185)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(95), + longitude: CLLocationDegrees(185)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(-95), + longitude: CLLocationDegrees(185)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(95), + longitude: CLLocationDegrees(-185)), + CLLocationCoordinate2D(latitude: CLLocationDegrees(-95), + longitude: CLLocationDegrees(-185)) + ] + + XCTAssertTrue(coordinates.allSatisfy { !$0.isValid }, "All test coordinates should be invalid") + } } diff --git a/UtiliKit.xcodeproj/project.pbxproj b/UtiliKit.xcodeproj/project.pbxproj index 441db9f..e4f409f 100644 --- a/UtiliKit.xcodeproj/project.pbxproj +++ b/UtiliKit.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ 8698D0272061A3930065AE20 /* ViewControllerA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D0072061A3930065AE20 /* ViewControllerA.swift */; }; 8698D0282061A3930065AE20 /* ViewControllerB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D0082061A3930065AE20 /* ViewControllerB.swift */; }; 8698D0292061A3930065AE20 /* WipeTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D0092061A3930065AE20 /* WipeTransitionAnimator.swift */; }; + A3C49DC226D56CFD00A4FCBB /* CLLocationCoordinate2D+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C49DC126D56CFD00A4FCBB /* CLLocationCoordinate2D+Extensions.swift */; }; B4A5231A22E6925B00AB1424 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4A5231922E6925B00AB1424 /* URL+Extensions.swift */; }; B4CCCAEF22D57284001A7A4F /* DefaultContainerTransitionAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698CFA620619EAD0065AE20 /* DefaultContainerTransitionAnimator.swift */; }; /* End PBXBuildFile section */ @@ -191,6 +192,7 @@ 8698D0072061A3930065AE20 /* ViewControllerA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerA.swift; sourceTree = ""; }; 8698D0082061A3930065AE20 /* ViewControllerB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerB.swift; sourceTree = ""; }; 8698D0092061A3930065AE20 /* WipeTransitionAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WipeTransitionAnimator.swift; sourceTree = ""; }; + A3C49DC126D56CFD00A4FCBB /* CLLocationCoordinate2D+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLLocationCoordinate2D+Extensions.swift"; sourceTree = ""; }; B4A5231922E6925B00AB1424 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -436,6 +438,7 @@ 8698CFA820619EAD0065AE20 /* FileManager+Extensions.swift */, 8698CFA920619EAD0065AE20 /* UIView+Extensions.swift */, B4A5231922E6925B00AB1424 /* URL+Extensions.swift */, + A3C49DC126D56CFD00A4FCBB /* CLLocationCoordinate2D+Extensions.swift */, ); path = General; sourceTree = ""; @@ -705,6 +708,7 @@ 5936A29324632D9B006E3FA6 /* ScrollingPageControl.swift in Sources */, 8698CFCA20619EAD0065AE20 /* VersionConfig.swift in Sources */, B4A5231A22E6925B00AB1424 /* URL+Extensions.swift in Sources */, + A3C49DC226D56CFD00A4FCBB /* CLLocationCoordinate2D+Extensions.swift in Sources */, 8698CFC420619EAD0065AE20 /* UICollectionView+Extensions.swift in Sources */, 0EC8025C209CE9C90051F732 /* Configurable.swift in Sources */, 8698CFC820619EAD0065AE20 /* TimelessDate.swift in Sources */,