├── .gitignore ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── Waypoints │ ├── Extensions │ ├── CLGeocoder+Combine.swift │ └── CLLocationManager+Combine.swift │ ├── Location.swift │ └── LocationTracker.swift └── Tests └── WaypointsTests ├── LocationTrackerTests.swift ├── Mocks.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */*.xcodeproj/xcuserdata/* 3 | */*.xcodeproj/project.xcworkspace/ 4 | build/* 5 | 6 | .swiftpm 7 | Package.resolved 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2015-2020 Andrew Shepard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Waypoints", 8 | platforms: [ 9 | .macOS(.v11), .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "Waypoints", 15 | targets: ["Waypoints"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "Waypoints", 26 | dependencies: []), 27 | .testTarget( 28 | name: "WaypointsTests", 29 | dependencies: ["Waypoints"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Waypoints 2 | 3 | ![Swift 5.0](https://img.shields.io/badge/swift-5.0-orange.svg) 4 | ![Platform](https://img.shields.io/badge/platform-ios%20%7C%20macos-lightgrey.svg) 5 | 6 | Easy location tracking in Swift. 7 | 8 | * Add multiple observers for a single location change. 9 | * Configure the minimum distance before a new location is published. 10 | * Supports reverse geocoding. 11 | * Target iOS or macOS. 12 | 13 | ## Usage 14 | 15 | Create a `LocationTracker` instance: 16 | 17 | let locationTracker = LocationTracker() 18 | 19 | Subscribe to location changes: 20 | 21 | ``` 22 | locationTracker 23 | .locationUpdatePublisher 24 | .sink { [weak self] (result) in 25 | switch result { 26 | case .success(let location): 27 | // handle new location 28 | case .failure(let error): 29 | // handle error 30 | } 31 | } 32 | .store(in: &cancellables) 33 | ``` 34 | 35 | The location is returned as a `Result` type, representing either the `Location` or an `Error` about why the location could not be obtained. 36 | 37 | The `Location` type combines a `CLLocation` with metadata for the associated city and state. Address information is obtained using `CLGeocoder`. 38 | 39 | ``` 40 | public struct Location { 41 | let physical: CLLocation 42 | let city: String 43 | let state: String 44 | } 45 | ``` 46 | 47 | ## Requirements 48 | 49 | * Xcode 12 50 | * Swift 5 51 | * iOS 13, macOS 11 52 | 53 | ## Installation 54 | 55 | Clone the repo directly and copy `LocationTracker.swift` into your Xcode project. 56 | 57 | ## Configuration 58 | 59 | Starting in iOS 8, the privacy settings require you to [specify *why* the location is needed](http://stackoverflow.com/a/24063578) before the app is authorized to use it. Add an entry into the `Info.plist` for `NSLocationAlwaysUsageDescription` or `NSLocationWhenInUseUsageDescription`. 60 | 61 | If location is only required when app is active. 62 | 63 | NSLocationWhenInUseUsageDescription 64 | Location required because... 65 | 66 | If the location is always required. 67 | 68 | NSLocationAlwaysUsageDescription 69 | Location always required because... 70 | 71 | ## License 72 | 73 | The MIT License 74 | -------------------------------------------------------------------------------- /Sources/Waypoints/Extensions/CLGeocoder+Combine.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Combine 3 | 4 | enum GeocoderError: Error { 5 | case notFound 6 | case other(Error) 7 | } 8 | 9 | class ReverseGeocoderSubscription: Subscription where SubscriberType.Input == Result<[CLPlacemark], Error> { 10 | 11 | private var subscriber: SubscriberType? 12 | private let geocoder: CLGeocoder 13 | 14 | init(subscriber: SubscriberType, location: CLLocation, geocoder: CLGeocoder) { 15 | self.subscriber = subscriber 16 | self.geocoder = geocoder 17 | 18 | geocoder.reverseGeocodeLocation(location) { (placemarks, error) in 19 | if let error = error { 20 | _ = subscriber.receive(.failure(GeocoderError.other(error))) 21 | } else if let placemarks = placemarks { 22 | _ = subscriber.receive(.success(placemarks)) 23 | } else { 24 | _ = subscriber.receive(.failure(GeocoderError.notFound)) 25 | } 26 | } 27 | } 28 | 29 | func request(_ demand: Subscribers.Demand) { 30 | // No need to handle `demand` because events are sent when they occur 31 | } 32 | 33 | func cancel() { 34 | subscriber = nil 35 | } 36 | } 37 | 38 | public struct ReverseGeocoderPublisher: Publisher { 39 | public typealias Output = Result<[CLPlacemark], Error> 40 | public typealias Failure = Never 41 | 42 | private let location: CLLocation 43 | private let geocoder: CLGeocoder 44 | 45 | init(location: CLLocation, geocoder: CLGeocoder = CLGeocoder()) { 46 | self.location = location 47 | self.geocoder = geocoder 48 | } 49 | 50 | public func receive(subscriber: S) where S : Subscriber, ReverseGeocoderPublisher.Failure == S.Failure, ReverseGeocoderPublisher.Output == S.Input { 51 | let subscription = ReverseGeocoderSubscription( 52 | subscriber: subscriber, 53 | location: location, 54 | geocoder: geocoder 55 | ) 56 | subscriber.receive(subscription: subscription) 57 | } 58 | } 59 | 60 | public extension CLGeocoder { 61 | func reverseGeocodingPublisher(for location: CLLocation) -> AnyPublisher, Never> { 62 | return ReverseGeocoderPublisher(location: location, geocoder: self) 63 | .eraseToAnyPublisher() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Waypoints/Extensions/CLLocationManager+Combine.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Combine 3 | 4 | class LocationManagerSubscription: NSObject, CLLocationManagerDelegate, Subscription where SubscriberType.Input == Result<[CLLocation], Error> { 5 | 6 | private var subscriber: SubscriberType? 7 | private let locationManager: CLLocationManager 8 | 9 | init(subscriber: SubscriberType, locationManager: CLLocationManager) { 10 | self.subscriber = subscriber 11 | self.locationManager = locationManager 12 | super.init() 13 | 14 | locationManager.delegate = self 15 | locationManager.startUpdatingLocation() 16 | } 17 | 18 | func request(_ demand: Subscribers.Demand) { 19 | // No need to handle `demand` because events are sent when they occur 20 | } 21 | 22 | func cancel() { 23 | subscriber = nil 24 | } 25 | 26 | // MARK: 27 | 28 | @objc func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 29 | _ = subscriber?.receive(.failure(error)) 30 | } 31 | 32 | @objc func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 33 | _ = subscriber?.receive(.success(locations)) 34 | } 35 | } 36 | 37 | public struct LocationPublisher: Publisher { 38 | public typealias Output = Result<[CLLocation], Error> 39 | public typealias Failure = Never 40 | 41 | private let locationManager: CLLocationManager 42 | 43 | init(locationManager: CLLocationManager = CLLocationManager()) { 44 | self.locationManager = locationManager 45 | } 46 | 47 | public func receive(subscriber: S) where S : Subscriber, LocationPublisher.Failure == S.Failure, LocationPublisher.Output == S.Input { 48 | let subscription = LocationManagerSubscription(subscriber: subscriber, locationManager: locationManager) 49 | subscriber.receive(subscription: subscription) 50 | } 51 | } 52 | 53 | public extension CLLocationManager { 54 | func publisher() -> AnyPublisher, Never> { 55 | return LocationPublisher(locationManager: self) 56 | .eraseToAnyPublisher() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Waypoints/Location.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | /// Location value representing a `CLLocation` and some local metadata. 4 | public struct Location: Equatable, Identifiable { 5 | public let id: UUID = UUID() 6 | 7 | /// A CLLocation object for the current location. 8 | public let physical: CLLocation 9 | 10 | /// The city the location is in. 11 | public let city: String 12 | 13 | /// The state the location is in. 14 | public let state: String 15 | 16 | init(location physical: CLLocation, city: String, state: String) { 17 | self.physical = physical 18 | self.city = city 19 | self.state = state 20 | } 21 | } 22 | 23 | public func ==(lhs: Location, rhs: Location) -> Bool { 24 | return lhs.physical == rhs.physical 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Waypoints/LocationTracker.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Combine 3 | 4 | #if os(iOS) 5 | import UIKit 6 | #endif 7 | 8 | enum LocationError: Error { 9 | case unknown 10 | } 11 | 12 | public protocol LocationTracking { 13 | 14 | /// Emits when the location changes, either through an error or a location update. 15 | var locationUpdatePublisher: AnyPublisher, Never> { get } 16 | } 17 | 18 | /// The LocationTracker class defines a mechanism for determining the current location and observing location changes. 19 | public final class LocationTracker: LocationTracking { 20 | 21 | // MARK: Dependencies 22 | 23 | private let locationManager: LocationManaging 24 | private let geocoder: GeocoderProviding 25 | private let notificationCenter: NotificationCentering 26 | 27 | // MARK: 28 | 29 | public var locationUpdatePublisher: AnyPublisher, Never> { 30 | return locationSubject 31 | .flatMap { (result) -> AnyPublisher, Never> in 32 | switch result { 33 | case .success(let location): 34 | return self.geocoder 35 | .reverseGeocodingPublisher(for: location) 36 | .map { (result) in 37 | switch result { 38 | case .success(let placemarks): 39 | return placemarks.location(physical: location) 40 | case .failure(let error): 41 | return .failure(error) 42 | } 43 | } 44 | .eraseToAnyPublisher() 45 | case .failure(let error): 46 | return Just(Result.failure(error)) 47 | .eraseToAnyPublisher() 48 | } 49 | } 50 | .eraseToAnyPublisher() 51 | } 52 | private let locationSubject = CurrentValueSubject, Never>(.failure(LocationError.unknown)) 53 | 54 | private var cancelables: [AnyCancellable] = [] 55 | 56 | /// Creates a new instance of a `LocationTracker` 57 | /// - Parameters: 58 | /// - makeLocationManager: Factory function that returns a `CLLocationManager` 59 | /// - geocoder: A geocoder object; defaults to `CLGeocoder`. 60 | /// - notificationCenter: A notification center; defaults to `NotificationCenter.default`. 61 | /// - queue: A queue for observing notication changes. Defaults to `DispatchQueue.main` 62 | /// - interval: An interval to wait before receiving location updates. Defaults to 60 seconds. 63 | public init(locationManager makeLocationManager: LocationManagerMaking = { CLLocationManager() }, 64 | geocoder: GeocoderProviding = CLGeocoder(), 65 | notificationCenter: NotificationCentering = NotificationCenter.default, 66 | queue: DispatchQueue = DispatchQueue.main, 67 | updateInterval interval : TimeInterval = 60.0) { 68 | self.locationManager = makeLocationManager() 69 | self.geocoder = geocoder 70 | self.notificationCenter = notificationCenter 71 | 72 | watchForApplicationLifecycleChanges() 73 | watchForLocationChanges(interval: interval, scheduler: queue) 74 | 75 | locationManager.requestWhenInUseAuthorization() 76 | } 77 | 78 | deinit { 79 | cancelables.forEach { $0.cancel() } 80 | locationManager.stopUpdatingLocation() 81 | } 82 | 83 | // MARK: Private 84 | 85 | private func watchForApplicationLifecycleChanges() { 86 | #if os(iOS) 87 | notificationCenter 88 | .publisher(for: UIApplication.willResignActiveNotification) 89 | .map { _ in () } 90 | .sink(receiveValue: { [weak self] _ in 91 | self?.locationManager.stopUpdatingLocation() 92 | }) 93 | .store(in: &cancelables) 94 | 95 | notificationCenter 96 | .publisher(for: UIApplication.didBecomeActiveNotification) 97 | .map { _ in () } 98 | .sink(receiveValue: { [weak self] _ in 99 | self?.locationManager.startUpdatingLocation() 100 | }) 101 | .store(in: &cancelables) 102 | #endif 103 | } 104 | 105 | private func watchForLocationChanges(interval: TimeInterval, scheduler: DispatchQueue) { 106 | let locationPublisher = locationManager 107 | .publisher() 108 | .share() 109 | 110 | // grab the first location and respond to it 111 | locationPublisher 112 | .prefix(1) 113 | .sink { [weak self] (result) in 114 | self?.updateLocationSubject(with: result) 115 | } 116 | .store(in: &cancelables) 117 | 118 | // then grab a new location every `interval` seconds 119 | locationPublisher 120 | .dropFirst() 121 | .throttle( 122 | for: DispatchQueue.SchedulerTimeType.Stride.init(floatLiteral: interval), 123 | scheduler: scheduler, 124 | latest: true 125 | ) 126 | .sink { [weak self] (result) in 127 | self?.updateLocationSubject(with: result) 128 | } 129 | .store(in: &cancelables) 130 | } 131 | 132 | private func updateLocationSubject(with result: Result<[CLLocation], Error>) { 133 | switch result { 134 | case .success(let locations): 135 | if let location = locations.first { 136 | locationSubject.send(.success(location)) 137 | } else { 138 | locationSubject.send(.failure(LocationError.unknown)) 139 | } 140 | case .failure(let error): 141 | locationSubject.send(.failure(error)) 142 | } 143 | } 144 | } 145 | 146 | private extension Array where Element: CLPlacemark { 147 | func location(physical: CLLocation) -> Result { 148 | guard 149 | let placemark = self.first, 150 | let city = placemark.locality, 151 | let state = placemark.administrativeArea 152 | else { return .failure(LocationError.unknown) } 153 | 154 | let location = Location(location: physical, city: city, state: state) 155 | return .success(location) 156 | } 157 | } 158 | 159 | /// Factory function for creating a `LocationManaging` object 160 | public typealias LocationManagerMaking = () -> LocationManaging 161 | 162 | /// Wraps `CLGeocoder` for dependency injection 163 | public protocol GeocoderProviding { 164 | func reverseGeocodingPublisher(for location: CLLocation) -> AnyPublisher, Never> 165 | } 166 | 167 | extension CLGeocoder: GeocoderProviding { } 168 | 169 | /// Wraps `CLLocationManager` for dependency injection 170 | public protocol LocationManaging { 171 | func stopUpdatingLocation() 172 | func startUpdatingLocation() 173 | func requestWhenInUseAuthorization() 174 | 175 | func publisher() -> AnyPublisher, Never> 176 | } 177 | 178 | extension CLLocationManager : LocationManaging { } 179 | 180 | /// Wraps `NotificationCenter` for dependency injection 181 | public protocol NotificationCentering { 182 | func publisher(for name: Notification.Name) -> AnyPublisher 183 | } 184 | 185 | extension NotificationCenter: NotificationCentering { 186 | 187 | /// Returns a publisher that emits when the notification `name` is received 188 | /// - Parameter name: Notification name to watch for 189 | /// - Returns: Publisher that emits with the notification triggered by `name`. 190 | public func publisher(for name: Notification.Name) -> AnyPublisher { 191 | return publisher(for: name, object: nil) 192 | .eraseToAnyPublisher() 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Tests/WaypointsTests/LocationTrackerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | 4 | #if os(iOS) 5 | import UIKit 6 | #endif 7 | 8 | @testable import Waypoints 9 | 10 | class LocationTrackerTests: XCTestCase { 11 | 12 | var sut: LocationTracker! 13 | 14 | var locationManager: MockLocationManager! 15 | var geocoder: MockGeocoder! 16 | var notificationCenter: MockNotificationCenter! 17 | 18 | var cancellables = Set() 19 | 20 | override func setUp() { 21 | locationManager = MockLocationManager() 22 | geocoder = MockGeocoder() 23 | notificationCenter = MockNotificationCenter() 24 | 25 | sut = LocationTracker( 26 | locationManager: { locationManager }, 27 | geocoder: geocoder, 28 | notificationCenter: notificationCenter 29 | ) 30 | } 31 | 32 | func testLocationDoesUpdate() { 33 | let expected = expectation(description: "Should publish location change") 34 | 35 | sut.locationUpdatePublisher 36 | .dropFirst() 37 | .sink { (result) in 38 | switch result { 39 | case .success(let location): 40 | XCTAssertEqual(location.city, "Miami") 41 | 42 | expected.fulfill() 43 | case .failure: 44 | XCTFail("Should update location") 45 | } 46 | } 47 | .store(in: &cancellables) 48 | 49 | locationManager.steer(MockFactory.makeLocation()) 50 | geocoder.steer(MockFactory.makePlacemark()) 51 | 52 | waitForExpectations(timeout: 1, handler: nil) 53 | } 54 | 55 | func testErrorsWhenLocationIsEmpty() { 56 | let expected = expectation(description: "Should publish location change") 57 | 58 | sut.locationUpdatePublisher 59 | .dropFirst() 60 | .sink { (result) in 61 | switch result { 62 | case .success: 63 | XCTFail("Should error when empty locations") 64 | case .failure(let error): 65 | if case LocationError.unknown = error { 66 | expected.fulfill() 67 | } else { 68 | XCTFail("Should error with `noData`") 69 | } 70 | } 71 | } 72 | .store(in: &cancellables) 73 | 74 | locationManager.steerEmpty() 75 | 76 | waitForExpectations(timeout: 1, handler: nil) 77 | } 78 | 79 | func testErrorsWhenReverseGeocodingFails() { 80 | let expected = expectation(description: "Should publish location change") 81 | 82 | sut.locationUpdatePublisher 83 | .dropFirst() 84 | .sink { (result) in 85 | switch result { 86 | case .success: 87 | XCTFail("Should error when empty locations") 88 | case .failure(let error): 89 | if case LocationError.unknown = error { 90 | expected.fulfill() 91 | } else { 92 | XCTFail("Should error with `noData`") 93 | } 94 | } 95 | } 96 | .store(in: &cancellables) 97 | 98 | locationManager.steer(MockFactory.makeLocation()) 99 | geocoder.steer(MockFactory.makeBadPlacemark()) 100 | 101 | waitForExpectations(timeout: 1, handler: nil) 102 | } 103 | 104 | #if os(iOS) 105 | func testStopsLocationUpdatesOnResignActive() { 106 | sut.locationUpdatePublisher 107 | .sink { _ in } 108 | .store(in: &cancellables) 109 | 110 | let notification = Notification(name: UIApplication.willResignActiveNotification) 111 | notificationCenter.steer(notification) 112 | 113 | XCTAssertEqual(locationManager.stopUpdatingLocationCount, 1) 114 | } 115 | 116 | func testStartsLocationUpdatesOnBecomeActive() { 117 | sut.locationUpdatePublisher 118 | .sink { _ in } 119 | .store(in: &cancellables) 120 | 121 | let notification = Notification(name: UIApplication.didBecomeActiveNotification) 122 | notificationCenter.steer(notification) 123 | 124 | XCTAssertEqual(locationManager.startUpdatingLocationCount, 1) 125 | } 126 | #endif 127 | } 128 | -------------------------------------------------------------------------------- /Tests/WaypointsTests/Mocks.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Combine 3 | import Contacts 4 | import Intents 5 | 6 | @testable import Waypoints 7 | 8 | final class MockLocationManager: LocationManaging { 9 | private let locationPublisher = PassthroughSubject, Never>() 10 | 11 | private(set) var stopUpdatingLocationCount = 0 12 | private(set) var startUpdatingLocationCount = 0 13 | 14 | func steer(_ location: CLLocation) { 15 | locationPublisher.send(.success([location])) 16 | } 17 | 18 | func steerEmpty() { 19 | locationPublisher.send(.success([])) 20 | } 21 | 22 | func steer(_ error: Error) { 23 | locationPublisher.send(.failure(error)) 24 | } 25 | 26 | // MARK: 27 | 28 | func stopUpdatingLocation() { 29 | stopUpdatingLocationCount += 1 30 | } 31 | 32 | func startUpdatingLocation() { 33 | startUpdatingLocationCount += 1 34 | } 35 | 36 | func requestWhenInUseAuthorization() { 37 | // 38 | } 39 | 40 | func publisher() -> AnyPublisher, Never> { 41 | return locationPublisher.eraseToAnyPublisher() 42 | } 43 | } 44 | 45 | final class MockGeocoder: GeocoderProviding { 46 | private let geocoderPublisher = PassthroughSubject, Never>() 47 | 48 | func steer(_ placemark: CLPlacemark) { 49 | geocoderPublisher.send(.success([placemark])) 50 | } 51 | 52 | func steer(_ error: Error) { 53 | geocoderPublisher.send(.failure(error)) 54 | } 55 | 56 | // MARK: 57 | 58 | func reverseGeocodingPublisher(for location: CLLocation) -> AnyPublisher, Never> { 59 | return geocoderPublisher.eraseToAnyPublisher() 60 | } 61 | } 62 | 63 | final class MockNotificationCenter: NotificationCentering { 64 | let notificationPublisher = PassthroughSubject() 65 | 66 | func steer(_ notification: Notification) { 67 | notificationPublisher.send(notification) 68 | } 69 | 70 | // MARK: 71 | 72 | func publisher(for name: Notification.Name) -> AnyPublisher { 73 | return notificationPublisher.eraseToAnyPublisher() 74 | } 75 | } 76 | 77 | enum MockFactory { 78 | static func makePlacemark() -> CLPlacemark { 79 | let location = makeLocation() 80 | let postmark = CNMutablePostalAddress() 81 | postmark.city = "Miami" 82 | postmark.state = "FL" 83 | postmark.postalCode = "33122" 84 | 85 | return CLPlacemark(location: location, name: "Miami, FL", postalAddress: postmark) 86 | } 87 | 88 | static func makeLocation() -> CLLocation { 89 | return CLLocation(latitude: 25.7617, longitude: 80.1918) 90 | } 91 | 92 | static func makeBadPlacemark() -> CLPlacemark { 93 | let location = makeLocation() 94 | return CLPlacemark(location: location, name: nil, postalAddress: nil) 95 | } 96 | 97 | static func makeError() -> Swift.Error { 98 | return Error.mock 99 | } 100 | 101 | private enum Error: Swift.Error { 102 | case mock 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/WaypointsTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(WaypointsTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------