diff --git a/.github/workflows/all.yml b/.github/workflows/all.yml deleted file mode 100644 index 743a06c..0000000 --- a/.github/workflows/all.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: "SwiftLocation CI" - -on: - push: - branches: - - master - pull_request: - branches: - - '*' - -concurrency: - group: ci - cancel-in-progress: true - -jobs: - macos-run-tests: - name: Unit Tests (Xcode ${{ matrix.xcode }}) - strategy: - fail-fast: false - matrix: - include: - - macos: macOS-latest - runs-on: ${{ matrix.macos }} - env: - DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer - steps: - - uses: actions/checkout@v2 - - name: Run Tests - run: - swift package generate-xcodeproj - xcodebuild test -project SwiftLocation.xcodeproj -scheme SwiftLocation -sdk iphonesimulator17.0 -destination "OS=17.0,name=iPhone 15" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4903057 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: "SwiftLocation CI" + +on: + push: + branches: + - master + pull_request: + branches: + - '*' + +concurrency: + group: ci + cancel-in-progress: true + +jobs: + ios-latest: + name: Unit Tests (iOS 16.4, Xcode 14.3.1) + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Run Tests + run: | + Scripts/test.sh -s "SwiftLocation" -d "OS=16.4,name=iPhone 14 Pro" + macos-latest: + name: Unit Tests (macOS, Xcode 14.3.1) + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Run Tests + run: Scripts/test.sh -d "platform=macOS" + tvos-latest: + name: Unit Tests (tvOS 16.4, Xcode 14.3.1) + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Run Tests + run: | + Scripts/test.sh -s "SwiftLocation" -d "OS=16.4,name=Apple TV" + watchos-latest: + name: Run Build (watchOS 9.2, Xcode 14.2) + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Run Tests + run: | + Scripts/build.sh -s "SwiftLocation" -d "OS=9.2,name=Apple Watch Series 8 (45mm)" + swift-build: + name: Swift Build (SPM) + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer + steps: + - uses: actions/checkout@v2 + - name: Build + run: swift build \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..44e40b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# 💻 Contributing to SwiftLocation + +First of all, thanks for your interest in SwiftLocation. + +There are several ways to contribute to this project. We welcome contributions in all ways. +We have made some contribution guidelines to smoothly incorporate your opinions and code into this project. + +## 📝 Open Issue + +When you found a bug or having a feature request, search for the issue from the [existing](https://github.com/malcommac/SwiftLocation/issues) and feel free to open the issue after making sure it isn't already reported. + +In order to we understand your issue accurately, please include as much information as possible in the issue template. +The screenshot are also big clue to understand the issue. + +If you know exactly how to fix the bug you report or implement the feature you propose, please pull request instead of an issue. + +## 🚀 Pull Request + +We are waiting for a pull request to make this project more better with us. +If you want to add a new feature, let's discuss about it first on issue. + +```bash +$ git clone https://github.com/malcommac/SwiftLocation.git +$ cd SwiftLocation/ +$ open SwiftLocation +``` + +### Test + +The test will tells us the validity of your code. +All codes entering the master must pass the all tests. +If you change the code or add new features, you should add tests. + +### Documentation + +Please write the document using [Xcode markup](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/) to the code you added. +Documentation template is inserted automatically by using Xcode shortcut **⌥⌘/**. +Our document style is slightly different from the template. The example is below. +```swift +/// The example class for documentation. +final class Foo { + /// A property value. + let prop: Int + + /// Create a new foo with a param. + /// + /// - Parameters: + /// - param: An Int value for prop. + init(param: Int) { + prop = param + } + + /// Returns a string value concatenating `param1` and `param2`. + /// + /// - Parameters: + /// - param1: An Int value for prefix. + /// - param2: A String value for suffix. + /// + /// - Returns: A string concatenating given params. + func bar(param1: Int, param2: String) -> String { + return "\(param1)" + param2 + } +} +``` + +## [Developer's Certificate of Origin 1.1](https://elinux.org/Developer_Certificate_Of_Origin) +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0812f30 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ + +Copyright (c) 2023 Daniele Margutti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Package.swift b/Package.swift index 291c360..866e841 100644 --- a/Package.swift +++ b/Package.swift @@ -5,16 +5,13 @@ import PackageDescription let package = Package( name: "SwiftLocation", - platforms: [.iOS(.v14)], + platforms: [.iOS(.v14), .macOS(.v11), .watchOS(.v7), .tvOS(.v14)], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "SwiftLocation", targets: ["SwiftLocation"]), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .target( name: "SwiftLocation"), .testTarget( diff --git a/README.md b/README.md index 7089c36..c305f1f 100644 --- a/README.md +++ b/README.md @@ -5,45 +5,61 @@

-# What's SwiftLocation +[![Platform](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS%20-4E4E4E.svg?colorA=28a745)](#installation) +[![Swift](https://img.shields.io/badge/Swift-5.3_5.4_5.5_5.6-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.5_5.6_5.7_5.8_5.9-Orange?style=flat-square) +[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square) **SwiftLocation is a lightweight wrapper around Apple's CoreLocation framework that supports the new Swift Concurrency model.** -*This means no more delegate pattern to deal with, nor completion blocks. + +*This means **no more delegate pattern to deal with, nor completion blocks**. You can manage location requests, region, and beacon monitoring directly using the new async/await syntax.* -## What's new in 6.0 -The new 6.0 milestone is a completely rewritten version designed to support async/await optimally. We are also focused on supporting all CoreLocation features without creating an overwhelmed package. -All the features are supported by a complete unit tests suite. +Would you, for example, get the current user location? +*It's just 2 lines code away:* -This new version is also distributed only via Swift Package Manager and requires Swift 5.5+, iOS 14+. +```swift +try await location.requestPermission(.whenInUse) // obtain the permissions +let userLocation = try await location.requestLocation() // get the location +``` -The features from version 5.x will be included as separate downloadable modules later in the development process. Support for other Apple platforms (macOS, watchOS, visionOS) is currently underway. +# How it works + +SwiftLocation is quite straightforward to use. +Simply create your own `Location` instance and use one of the available methods. + +> [!IMPORTANT] +> Some APIs may not available under some of the supported platforms due to specific hardware constraints. + +- [How it works](#how-it-works) + - [What's new in 6.0](#whats-new-in-60) + - [Service Location Status](#service-location-status) + - [Authorization Status](#authorization-status) + - [Accuracy Authorization Level](#accuracy-authorization-level) + - [Request Location Permission](#request-location-permission) + - [Provide descriptions of how you use location services](#provide-descriptions-of-how-you-use-location-services) + - [Request Temporary Precision Permission](#request-temporary-precision-permission) + - [Continous Location Monitoring](#continous-location-monitoring) + - [Request One-Shot User Location](#request-one-shot-user-location) + - [Visits Monitoring](#visits-monitoring) + - [Significant Location Changes Monitoring](#significant-location-changes-monitoring) + - [Device Heading Monitoring](#device-heading-monitoring) + - [Beacon Ranging](#beacon-ranging) +- [Testing Suite \& Mocked CLLocationManager](#testing-suite---mocked-cllocationmanager) +- [Installation via SPM](#installation-via-spm) +- [Support This Work ❤️](#support-this-work-️) +- [License](#license) +- [Contributing](#contributing) -## How to use it +## What's new in 6.0 -SwiftLocation is quite straightforward to use; simply create your own Location instance and utilize the necessary methods. +The new 6.0 milestone is a completely rewritten version designed to support async/await optimally. We are also focused on supporting all CoreLocation features without creating an overwhelmed package. +All the features are supported by a complete unit tests suite. + +This new version is also distributed only via Swift Package Manager (5.5+) and it's compatible with all the Apple Platforms: iOS 14+, macOS 11+, watchOS 7+, tvOS 14+. -- [What's SwiftLocation](#whats-swiftlocation) - - [What's new in 6.0](#whats-new-in-60) - - [How to use it](#how-to-use-it) - - [Service Location Status](#service-location-status) - - [Authorization Status](#authorization-status) - - [Accuracy Authorization Level](#accuracy-authorization-level) - - [Request Location Permission](#request-location-permission) - - [Provide descriptions of how you use location services](#provide-descriptions-of-how-you-use-location-services) - - [Request Temporary Precision Permission](#request-temporary-precision-permission) - - [Continous Location Monitoring](#continous-location-monitoring) - - [Request One-Shot User Location](#request-one-shot-user-location) - - [Visits Monitoring](#visits-monitoring) - - [Significant Location Changes Monitoring](#significant-location-changes-monitoring) - - [Device Heading Monitoring](#device-heading-monitoring) - - [Beacon Ranging](#beacon-ranging) - - [Testing Suite \& Mocked CLLocationManager](#testing-suite---mocked-cllocationmanager) - - [Installation](#installation) - - [Support And License](#support-and-license) - - [Contributing](#contributing) +*The features from version 5.x - geocoding, ip resolve, autocomplete - will be included as separate downloadable modules later in the development process.* -### Service Location Status +## Service Location Status Use the `location.locationServicesEnabled` to get the current status of the location services. In order to monitor changes you can use the `AsyncStream`'s startMonitoringLocationServices()` method: @@ -57,7 +73,7 @@ for await event in await location.startMonitoringLocationServices() { You can stop the stream at any moment using `break`; it will call the `stopMonitoringLocationServices()` automatically on used `Location`` instance. -### Authorization Status +## Authorization Status You can obtain the current status of the authorization status by using the `location.authorizationStatus` property. If you need to monitor changes to this value you can use the `AsyncStream` offered by `startMonitoringAuthorization()` method: @@ -69,7 +85,7 @@ for await event in await location.startMonitoringAuthorization() { } ``` -### Accuracy Authorization Level +## Accuracy Authorization Level The `location.accuracyAuthorization` offers a one shot value of the current precision level offered by your application. When you need to monitor changes you can use the `AsyncStream` offered by `startMonitoringAccuracyAuthorization()`: @@ -81,7 +97,7 @@ for await event in await location.startMonitoringAccuracyAuthorization() { } ``` -### Request Location Permission +## Request Location Permission Also the request location permission is managed via async await. You can use the `requestPermission()` method once you have properly configured your `Info.plist` file: @@ -90,7 +106,7 @@ Also the request location permission is managed via async await. You can use the let obtaninedStatus = try await location.requestPermission(.whenInUse) ``` -#### Provide descriptions of how you use location services +### Provide descriptions of how you use location services The first time you make an authorization request, the system displays an alert asking the person to grant or deny the request. The alert includes a usage description string that explains why you want access to location data. @@ -105,7 +121,7 @@ Core Location supports different usage strings for each access level. You must i | `NSLocationTemporaryUsageDescriptionDictionary` | Used when you want to temporary extend the precision of your authorization level | | | | -### Request Temporary Precision Permission +## Request Temporary Precision Permission If the App does not require an exact location for all of its features, but it is required to have accurate one only for specific features (i.e during checkout, booking service, etc) — then App may ask for temporary accuracy level for that session only using the `requestTemporaryPrecisionAuthorization(purpose:)` method: @@ -114,7 +130,7 @@ If the App does not require an exact location for all of its features, but it is let status = try await location.requestTemporaryPrecisionAuthorization(purpose: "booking") ``` -### Continous Location Monitoring +## Continous Location Monitoring If you need to continous monitoring new locations from user's device you can use the `AsyncStream` offered by `startMonitoringLocations()`: @@ -134,7 +150,7 @@ for await event in try await location.startMonitoringLocations() { } ``` -### Request One-Shot User Location +## Request One-Shot User Location Sometimes you may need to get the user location as single value. The async's `requestLocation(accuracy:timeout:)` method was created to return an optionally filtered location within a valid time interval: @@ -150,7 +166,7 @@ let location = try await location.requestLocation(accuracy: [ Filters include horizontal/vertical, speed, course accuracy and it offer the opportunity to set a custom filter functions as callback. -### Visits Monitoring +## Visits Monitoring Visits monitoring allows you to observe places that the user has been. Visit objects are created by the system and delivered by the CLLocationManager. @@ -169,7 +185,7 @@ for await event in await location.startMonitoringVisits() { } ``` -### Significant Location Changes Monitoring +## Significant Location Changes Monitoring The `AsyncStream`'s `startMonitoringSignificantLocationChanges()` method starts the generation of updates based on significant location changes. @@ -189,7 +205,7 @@ for await event in await self.location.startMonitoringSignificantLocationChanges } ``` -### Device Heading Monitoring +## Device Heading Monitoring To get updates about the current device's heading use the `AsyncStream` offered by `startUpdatingHeading()` method: @@ -199,7 +215,7 @@ for await event in await self.location.startUpdatingHeading() { } ``` -### Beacon Ranging +## Beacon Ranging Beacon ranging is offered by the `AsyncStream`'s `startRangingBeacons()` method: @@ -210,13 +226,13 @@ for await event in await location.startRangingBeacons(satisfying: constraint) { } ``` -### Testing Suite & Mocked CLLocationManager +# Testing Suite & Mocked CLLocationManager SwiftLocation is distribuited with an extensive unit testing suite you can found into the `SwiftLocationTests` folder. Inside the suite you will also found the `MockedLocationManager.swift` file which is a `CLLocationManager` mock class you can use to provide the testing suite for your application. By configuring and extending this file you will be able to mock results of location requests and monitoring directly in your host app. -## Installation +# Installation via SPM SwiftLocation is offered via Swift Package Manager. Add it as a dependency in a Swift Package, and add it to your `Package.swift`: @@ -226,17 +242,25 @@ dependencies: [ .package(url: "https://github.com/malcommac/SwiftLocation.git", from: "6.0") ] ``` +# Support This Work ❤️ -## Support And License +If you love this library and wanna encourage further development **consider becoming a sponsor of my work** via [Github Sponsorship](https://github.com/sponsors/malcommac). -This package was created and maintaned by [Daniele Margutti](https://www.linkedin.com/in/danielemargutti/). -It was distribuited via [MIT License](https://github.com/malcommac/SwiftLocation/blob/master/LICENSE.md). +# License -If you love this library and wanna encourage further development **consider becoming a sponsor of my work** via [Github Sponsorship](https://github.com/sponsors/malcommac). +This package was created and maintaned by [Daniele Margutti](https://github.com/malcommac). -## Contributing +- [LinkedIn Profile](https://www.linkedin.com/in/danielemargutti/) +- [X/Twitter](http://twitter.com/danielemargutti) +- [Website](https://www.danielemargutti.com) + +It was distribuited using [MIT License](https://github.com/malcommac/SwiftLocation/blob/master/LICENSE.md). + +# Contributing - If you need help or you'd like to ask a general question, open an issue. - If you found a bug, open an issue. - If you have a feature request, open an issue. - If you want to contribute, submit a pull request. + +Read the [CONTRIBUTING](CONTRIBUTING.md) file for more informations. diff --git a/Scripts/test.sh b/Scripts/test.sh new file mode 100644 index 0000000..1e19b9c --- /dev/null +++ b/Scripts/test.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -eo pipefail + +scheme="SwiftLocation" + +while getopts "s:d:" opt; do + case $opt in + s) scheme=${OPTARG};; + d) destinations+=("$OPTARG");; + #... + esac +done +shift $((OPTIND -1)) + +echo "scheme = ${scheme}" +echo "destinations = ${destinations[@]}" + +xcodebuild -version + +xcodebuild build-for-testing -scheme "$scheme" -destination "${destinations[0]}" | xcpretty + +for destination in "${destinations[@]}"; +do + echo "\nRunning tests for destination: $destination" + xcodebuild test-without-building -scheme "$scheme" -destination "$destination" | xcpretty --test +done diff --git a/Sources/SwiftLocation/Async Tasks/BeaconMonitoring.swift b/Sources/SwiftLocation/Async Tasks/BeaconMonitoring.swift index a3d8dc6..43f2a78 100644 --- a/Sources/SwiftLocation/Async Tasks/BeaconMonitoring.swift +++ b/Sources/SwiftLocation/Async Tasks/BeaconMonitoring.swift @@ -26,6 +26,7 @@ import Foundation import CoreLocation +#if !os(watchOS) && !os(tvOS) extension Tasks { public final class BeaconMonitoring: AnyTask { @@ -93,3 +94,4 @@ extension Tasks { } } +#endif diff --git a/Sources/SwiftLocation/Async Tasks/ContinuousUpdateLocation.swift b/Sources/SwiftLocation/Async Tasks/ContinuousUpdateLocation.swift index cd27267..7062be9 100644 --- a/Sources/SwiftLocation/Async Tasks/ContinuousUpdateLocation.swift +++ b/Sources/SwiftLocation/Async Tasks/ContinuousUpdateLocation.swift @@ -38,18 +38,20 @@ extension Tasks { /// The event produced by the stream. public enum StreamEvent: CustomStringConvertible, Equatable { - /// Location updates did pause. - case didPaused - - /// Location updates did resume. - case didResume - /// A new array of locations has been received. case didUpdateLocations(_ locations: [CLLocation]) /// Something went wrong while reading new locations. case didFailed(_ error: Error) + #if os(iOS) + /// Location updates did resume. + case didResume + + /// Location updates did pause. + case didPaused + #endif + /// Return the location received by the event if it's a location event. /// In case of multiple events it will return the most recent one. public var location: CLLocation? { @@ -74,8 +76,10 @@ extension Tasks { public var description: String { switch self { + #if os(iOS) case .didPaused: "paused" case .didResume: "resume" + #endif case let .didFailed(e): "error \(e.localizedDescription)" case let .didUpdateLocations(l): "\(l.count) locations" } @@ -85,10 +89,12 @@ extension Tasks { switch (lhs, rhs) { case (.didFailed(let e1), .didFailed(let e2)): return e1.localizedDescription == e2.localizedDescription + #if os(iOS) case (.didPaused, .didPaused): return true case (.didResume, .didResume): return true + #endif case (.didUpdateLocations(let l1), .didUpdateLocations(let l2)): return l1 == l2 default: @@ -117,11 +123,13 @@ extension Tasks { public func receivedLocationManagerEvent(_ event: LocationManagerBridgeEvent) { switch event { + #if os(iOS) case .locationUpdatesPaused: stream?.yield(.didPaused) case .locationUpdatesResumed: stream?.yield(.didResume) + #endif case let .didFailWithError(error): stream?.yield(.didFailed(error)) diff --git a/Sources/SwiftLocation/Async Tasks/HeadingMonitoring.swift b/Sources/SwiftLocation/Async Tasks/HeadingMonitoring.swift index 9345337..6720b7d 100644 --- a/Sources/SwiftLocation/Async Tasks/HeadingMonitoring.swift +++ b/Sources/SwiftLocation/Async Tasks/HeadingMonitoring.swift @@ -26,6 +26,7 @@ import Foundation import CoreLocation +#if os(iOS) extension Tasks { public final class HeadingMonitoring: AnyTask { @@ -92,3 +93,4 @@ extension Tasks { } } +#endif diff --git a/Sources/SwiftLocation/Async Tasks/LocatePermission.swift b/Sources/SwiftLocation/Async Tasks/LocatePermission.swift index a1c0d81..dcd0657 100644 --- a/Sources/SwiftLocation/Async Tasks/LocatePermission.swift +++ b/Sources/SwiftLocation/Async Tasks/LocatePermission.swift @@ -84,11 +84,16 @@ extension Tasks { } } + #if !os(tvOS) func requestAlwaysPermission() async throws -> CLAuthorizationStatus { try await withCheckedThrowingContinuation { continuation in guard let instance = self.instance else { return } + #if os(macOS) + let isAuthorized = instance.authorizationStatus != .notDetermined + #else let isAuthorized = instance.authorizationStatus != .notDetermined && instance.authorizationStatus != .authorizedWhenInUse + #endif guard !isAuthorized else { continuation.resume(with: .success(instance.authorizationStatus)) return @@ -99,6 +104,7 @@ extension Tasks { instance.locationManager.requestAlwaysAuthorization() } } + #endif } diff --git a/Sources/SwiftLocation/Async Tasks/VisitsMonitoring.swift b/Sources/SwiftLocation/Async Tasks/VisitsMonitoring.swift index cdd1624..3a7d862 100644 --- a/Sources/SwiftLocation/Async Tasks/VisitsMonitoring.swift +++ b/Sources/SwiftLocation/Async Tasks/VisitsMonitoring.swift @@ -26,6 +26,7 @@ import Foundation import CoreLocation +#if !os(watchOS) && !os(tvOS) extension Tasks { public final class VisitsMonitoring: AnyTask { @@ -86,3 +87,4 @@ extension Tasks { } } +#endif diff --git a/Sources/SwiftLocation/Location Managers/LocationManagerBridgeEvent.swift b/Sources/SwiftLocation/Location Managers/LocationManagerBridgeEvent.swift index 55ecb09..dd4d968 100644 --- a/Sources/SwiftLocation/Location Managers/LocationManagerBridgeEvent.swift +++ b/Sources/SwiftLocation/Location Managers/LocationManagerBridgeEvent.swift @@ -54,15 +54,20 @@ public enum LocationManagerBridgeEvent { // MARK: - Visits Monitoring + #if !os(watchOS) && !os(tvOS) case didVisit(visit: CLVisit) + #endif // MARK: - Headings + #if os(iOS) case didUpdateHeading(_ heading: CLHeading) + #endif // MARK: - Beacons + #if !os(watchOS) && !os(tvOS) case didRange(beacons: [CLBeacon], constraint: CLBeaconIdentityConstraint) case didFailRanginFor(constraint: CLBeaconIdentityConstraint, error: Error) - + #endif } diff --git a/Sources/SwiftLocation/Location Managers/LocationManagerProtocol.swift b/Sources/SwiftLocation/Location Managers/LocationManagerProtocol.swift index dcefbdf..450cc53 100644 --- a/Sources/SwiftLocation/Location Managers/LocationManagerProtocol.swift +++ b/Sources/SwiftLocation/Location Managers/LocationManagerProtocol.swift @@ -38,10 +38,18 @@ public protocol LocationManagerProtocol { var authorizationStatus: CLAuthorizationStatus { get } var accuracyAuthorization: CLAccuracyAuthorization { get } + + #if !os(tvOS) var activityType: CLActivityType { get set } + #endif + var distanceFilter: CLLocationDistance { get set } var desiredAccuracy: CLLocationAccuracy { get set } + + #if !os(tvOS) var allowsBackgroundLocationUpdates: Bool { get set } + #endif + func locationServicesEnabled() -> Bool // MARK: - Location Permissions @@ -49,38 +57,52 @@ public protocol LocationManagerProtocol { func validatePlistConfigurationOrThrow(permission: LocationPermission) throws func validatePlistConfigurationForTemporaryAccuracy(purposeKey: String) throws func requestWhenInUseAuthorization() + + #if !os(tvOS) func requestAlwaysAuthorization() + #endif func requestTemporaryFullAccuracyAuthorization(withPurposeKey purposeKey: String, completion: ((Error?) -> Void)?) // MARK: - Getting Locations + #if !os(tvOS) func startUpdatingLocation() func stopUpdatingLocation() + #endif func requestLocation() + #if !os(watchOS) && !os(tvOS) // MARK: - Monitoring Regions func startMonitoring(for region: CLRegion) func stopMonitoring(for region: CLRegion) + #endif // MARK: - Monitoring Visits + #if !os(watchOS) && !os(tvOS) func startMonitoringVisits() func stopMonitoringVisits() - + #endif + + #if !os(watchOS) && !os(tvOS) // MARK: - Monitoring Significant Location Changes func startMonitoringSignificantLocationChanges() func stopMonitoringSignificantLocationChanges() + #endif + #if os(iOS) // MARK: - Getting Heading func startUpdatingHeading() func stopUpdatingHeading() + #endif // MARK: - Beacon Ranging + #if !os(watchOS) && !os(tvOS) func startRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) func stopRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) - + #endif } diff --git a/Sources/SwiftLocation/Location.swift b/Sources/SwiftLocation/Location.swift index c3711c8..f722b41 100644 --- a/Sources/SwiftLocation/Location.swift +++ b/Sources/SwiftLocation/Location.swift @@ -88,12 +88,14 @@ public final class Location { set { locationManager.desiredAccuracy = newValue.level } } + #if !os(tvOS) /// The type of activity the app expects the user to typically perform while in the app’s location session. /// By default is set to `CLActivityType.other`. public var activityType: CLActivityType { get { locationManager.activityType } set { locationManager.activityType = newValue } } + #endif /// The minimum distance in meters the device must move horizontally before an update event is generated. /// By defualt is set to `kCLDistanceFilterNone`. @@ -115,13 +117,16 @@ public final class Location { /// Core Location configures the system to keep the app running to receive continuous background location updates, /// and arranges to show the background location indicator (blue bar or pill) if needed. /// Updates continue even if the app subsequently enters the background. + #if !os(tvOS) public var allowsBackgroundLocationUpdates: Bool { get { locationManager.allowsBackgroundLocationUpdates } set { locationManager.allowsBackgroundLocationUpdates = newValue } } + #endif // MARK: - Initialization + #if !os(tvOS) /// Initialize a new SwiftLocation instance to work with the Core Location service. /// /// - Parameter locationManager: underlying service. By default the device's CLLocationManager instance is used @@ -136,6 +141,18 @@ public final class Location { self.locationManager.allowsBackgroundLocationUpdates = allowsBackgroundLocationUpdates self.asyncBridge.location = self } + #else + /// Initialize a new SwiftLocation instance to work with the Core Location service. + /// + /// - Parameter locationManager: underlying service. By default the device's CLLocationManager instance is used + /// but you can provide your own. + public init(locationManager: LocationManagerProtocol = CLLocationManager()) { + self.locationDelegate = LocationDelegate(asyncBridge: self.asyncBridge) + self.locationManager = locationManager + self.locationManager.delegate = locationDelegate + self.asyncBridge.location = self + } + #endif // MARK: - Monitor Location Services Enabled @@ -212,12 +229,14 @@ public final class Location { switch permission { case .whenInUse: return try await requestWhenInUsePermission() + #if !os(tvOS) case .always: #if APPCLIP return try await requestWhenInUsePermission() #else return try await requestAlwaysPermission() #endif + #endif } } @@ -233,6 +252,7 @@ public final class Location { // MARK: - Monitor Location Updates + #if !os(tvOS) /// Start receiving changes of the locations with a stream. /// /// - Returns: events received from the location manager. @@ -262,6 +282,7 @@ public final class Location { locationManager.stopUpdatingLocation() asyncBridge.cancel(tasksTypes: Tasks.ContinuousUpdateLocation.self) } + #endif // MARK: - Get Location @@ -284,6 +305,7 @@ public final class Location { } } + #if !os(watchOS) && !os(tvOS) // MARK: - Monitor Regions /// Starts the monitoring a region and receive stream of events from it. @@ -310,9 +332,11 @@ public final class Location { ($0 as! Tasks.RegionMonitoring).region == region } } + #endif // MARK: - Monitor Visits Updates + #if !os(watchOS) && !os(tvOS) /// Starts monitoring visits to locations. /// /// - Returns: stream of events for visits. @@ -333,7 +357,9 @@ public final class Location { asyncBridge.cancel(tasksTypes: Tasks.VisitsMonitoring.self) locationManager.stopMonitoringVisits() } + #endif + #if !os(watchOS) && !os(tvOS) // MARK: - Monitor Significant Locations /// Starts monitoring significant location changes. @@ -356,7 +382,9 @@ public final class Location { locationManager.stopMonitoringSignificantLocationChanges() asyncBridge.cancel(tasksTypes: Tasks.SignificantLocationMonitoring.self) } + #endif + #if os(iOS) // MARK: - Monitor Device Heading Updates /// Starts monitoring heading changes. @@ -378,7 +406,8 @@ public final class Location { public func stopUpdatingHeading() { locationManager.stopUpdatingHeading() asyncBridge.cancel(tasksTypes: Tasks.HeadingMonitoring.self) - } + } + #endif // MARK: - Monitor Beacons Ranging @@ -386,6 +415,7 @@ public final class Location { /// /// - Parameter satisfying: A `CLBeaconIdentityConstraint` constraint. /// - Returns: stream of events related to passed constraint. + #if !os(watchOS) && !os(tvOS) public func startRangingBeacons(satisfying: CLBeaconIdentityConstraint) async -> Tasks.BeaconMonitoring.Stream { let task = Tasks.BeaconMonitoring(satisfying: satisfying) return Tasks.BeaconMonitoring.Stream { stream in @@ -407,6 +437,7 @@ public final class Location { } locationManager.stopRangingBeacons(satisfying: satisfying) } + #endif // MARK: - Private Functions @@ -435,6 +466,7 @@ public final class Location { } } + #if !os(tvOS) /// Request authorization to get location both in foreground and background. /// /// - Returns: authorization obtained. @@ -446,5 +478,6 @@ public final class Location { asyncBridge.cancel(task: task) } } + #endif } diff --git a/Sources/SwiftLocation/Support/Extensions.swift b/Sources/SwiftLocation/Support/Extensions.swift index 4b08254..d16fd3e 100644 --- a/Sources/SwiftLocation/Support/Extensions.swift +++ b/Sources/SwiftLocation/Support/Extensions.swift @@ -39,10 +39,12 @@ extension CLLocationManager: LocationManagerProtocol { /// - Parameter permission: permission you would to obtain. public func validatePlistConfigurationOrThrow(permission: LocationPermission) throws { switch permission { + #if !os(tvOS) case .always: if !Bundle.hasAlwaysPermission() { throw LocationErrors.plistNotConfigured } + #endif case .whenInUse: if !Bundle.hasWhenInUsePermission() { throw LocationErrors.plistNotConfigured diff --git a/Sources/SwiftLocation/Support/LocationDelegate.swift b/Sources/SwiftLocation/Support/LocationDelegate.swift index a22b372..c61e1dd 100644 --- a/Sources/SwiftLocation/Support/LocationDelegate.swift +++ b/Sources/SwiftLocation/Support/LocationDelegate.swift @@ -59,10 +59,13 @@ final class LocationDelegate: NSObject, CLLocationManagerDelegate { // MARK: - Heading Updates + #if os(iOS) func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { asyncBridge?.dispatchEvent(.didUpdateHeading(newHeading)) } + #endif + #if os(iOS) // MARK: - Pause/Resume func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { @@ -72,9 +75,11 @@ final class LocationDelegate: NSObject, CLLocationManagerDelegate { func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) { asyncBridge?.dispatchEvent(.locationUpdatesResumed) } + #endif // MARK: - Region Monitoring + #if os(iOS) func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) { asyncBridge?.dispatchEvent(.monitoringDidFailFor(region: region, error: error)) } @@ -90,13 +95,17 @@ final class LocationDelegate: NSObject, CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) { asyncBridge?.dispatchEvent(.didStartMonitoringFor(region)) } + #endif // MARK: - Visits Monitoring + #if os(iOS) func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) { asyncBridge?.dispatchEvent(.didVisit(visit: visit)) } - + #endif + + #if os(iOS) // MARK: - Beacons Ranging func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) { @@ -106,5 +115,6 @@ final class LocationDelegate: NSObject, CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didFailRangingFor beaconConstraint: CLBeaconIdentityConstraint, error: Error) { asyncBridge?.dispatchEvent(.didFailRanginFor(constraint: beaconConstraint, error: error)) } + #endif } diff --git a/Sources/SwiftLocation/Support/SupportModels.swift b/Sources/SwiftLocation/Support/SupportModels.swift index 88d368d..a730a47 100644 --- a/Sources/SwiftLocation/Support/SupportModels.swift +++ b/Sources/SwiftLocation/Support/SupportModels.swift @@ -167,7 +167,9 @@ public enum LocationAccuracy { public enum LocationPermission { /// Always authorization, both background and when in use. + #if !os(tvOS) case always + #endif /// Only when in use authorization. case whenInUse } diff --git a/Tests/SwiftLocationTests/MockedLocationManager.swift b/Tests/SwiftLocationTests/MockedLocationManager.swift index 1209e22..3e6fba3 100644 --- a/Tests/SwiftLocationTests/MockedLocationManager.swift +++ b/Tests/SwiftLocationTests/MockedLocationManager.swift @@ -66,19 +66,23 @@ public class MockedLocationManager: LocationManagerProtocol { public var onRequestAlwaysAuthorization: (() -> CLAuthorizationStatus) = { .notDetermined } public var onRequestValidationForTemporaryAccuracy: ((String) -> Error?) = { _ in return nil } + public func updateLocations(event: Tasks.ContinuousUpdateLocation.StreamEvent) { switch event { case let .didUpdateLocations(locations): delegate?.locationManager?(fakeInstance, didUpdateLocations: locations) + #if os(iOS) case .didResume: delegate?.locationManagerDidResumeLocationUpdates?(fakeInstance) case .didPaused: delegate?.locationManagerDidPauseLocationUpdates?(fakeInstance) + #endif case let .didFailed(error): delegate?.locationManager?(fakeInstance, didFailWithError: error) } } - + + #if !os(watchOS) && !os(tvOS) public func updateSignificantLocation(event: Tasks.SignificantLocationMonitoring.StreamEvent) { switch event { case let .didFailWithError(error): @@ -91,7 +95,9 @@ public class MockedLocationManager: LocationManagerProtocol { delegate?.locationManager?(fakeInstance, didUpdateLocations: locations) } } + #endif + #if !os(watchOS) && !os(tvOS) public func updateVisits(event: Tasks.VisitsMonitoring.StreamEvent) { switch event { case let .didVisit(visit): @@ -117,6 +123,7 @@ public class MockedLocationManager: LocationManagerProtocol { } } + #endif public func validatePlistConfigurationForTemporaryAccuracy(purposeKey: String) throws { if let error = onRequestValidationForTemporaryAccuracy(purposeKey) { @@ -168,7 +175,7 @@ public class MockedLocationManager: LocationManagerProtocol { } public func startMonitoringVisits() { - + } public func stopMonitoringVisits() { @@ -191,6 +198,7 @@ public class MockedLocationManager: LocationManagerProtocol { } + #if !os(watchOS) && !os(tvOS) public func startRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) { } @@ -198,6 +206,7 @@ public class MockedLocationManager: LocationManagerProtocol { public func stopRangingBeacons(satisfying constraint: CLBeaconIdentityConstraint) { } + #endif public init() { diff --git a/Tests/SwiftLocationTests/SwiftLocationTests.swift b/Tests/SwiftLocationTests/SwiftLocationTests.swift index a7a07cd..92e1a4e 100644 --- a/Tests/SwiftLocationTests/SwiftLocationTests.swift +++ b/Tests/SwiftLocationTests/SwiftLocationTests.swift @@ -83,6 +83,7 @@ final class SwiftLocationTests: XCTestCase { } } + #if !os(tvOS) /// Test request for permission with failure in plist configuration func testRequestPermissionsFailureWithPlistConfiguration() async throws { mockLocationManager.onValidatePlistConfiguration = { permission in @@ -99,6 +100,7 @@ final class SwiftLocationTests: XCTestCase { XCTFail("Permission should fail due to missing plist while it returned \(newStatus)") } catch { } } + #endif func testRequestPermissionWhenInUseSuccess() async throws { do { @@ -113,6 +115,7 @@ final class SwiftLocationTests: XCTestCase { } } + #if !os(tvOS) func testRequestAlwaysSuccess() async throws { do { let expectedStatus = CLAuthorizationStatus.authorizedAlways @@ -126,6 +129,7 @@ final class SwiftLocationTests: XCTestCase { XCTFail("Request should not fail: \(error.localizedDescription)") } } + #endif /// Test the request location permission while observing authorization status change. func testMonitorAuthorizationWithPermissionRequest() async throws { @@ -143,9 +147,15 @@ final class SwiftLocationTests: XCTestCase { } sleep(1) + #if os(macOS) + mockLocationManager.onRequestAlwaysAuthorization = { .authorizedAlways } + let newStatus = try await location.requestPermission(.always) + XCTAssertEqual(newStatus, .authorizedAlways) + #else mockLocationManager.onRequestWhenInUseAuthorization = { .authorizedWhenInUse } let newStatus = try await location.requestPermission(.whenInUse) XCTAssertEqual(newStatus, .authorizedWhenInUse) + #endif await fulfillment(of: [exp]) } @@ -155,9 +165,15 @@ final class SwiftLocationTests: XCTestCase { mockLocationManager.authorizationStatus = .notDetermined XCTAssertEqual(mockLocationManager.accuracyAuthorization, .reducedAccuracy) + #if os(macOS) + mockLocationManager.onRequestAlwaysAuthorization = { .authorizedAlways } + let newStatus = try await location.requestPermission(.always) + XCTAssertEqual(newStatus, .authorizedAlways) + #else mockLocationManager.onRequestWhenInUseAuthorization = { .authorizedWhenInUse } let newStatus = try await location.requestPermission(.whenInUse) XCTAssertEqual(newStatus, .authorizedWhenInUse) + #endif // Test misconfigured Info.plist file do { @@ -182,12 +198,17 @@ final class SwiftLocationTests: XCTestCase { } } + #if !os(tvOS) /// Test stream of updates for locations. func testUpdatingLocations() async throws { // Request authorization + #if os(macOS) + mockLocationManager.onRequestAlwaysAuthorization = { .authorizedAlways } + try await location.requestPermission(.always) + #else mockLocationManager.onRequestWhenInUseAuthorization = { .authorizedWhenInUse } try await location.requestPermission(.whenInUse) - + #endif let expectedValues = simulateLocationUpdates() var idx = 0 for await event in try await self.location.startMonitoringLocations() { @@ -199,13 +220,18 @@ final class SwiftLocationTests: XCTestCase { } } } + #endif /// Test one shot request method. func testRequestLocation() async throws { // Request authorization + #if os(macOS) + mockLocationManager.onRequestAlwaysAuthorization = { .authorizedAlways} + try await location.requestPermission(.always) + #else mockLocationManager.onRequestWhenInUseAuthorization = { .authorizedWhenInUse } try await location.requestPermission(.whenInUse) - + #endif // Check the return of an error simulateRequestLocationDelayedResponse(event: .didFailed(LocationErrors.notAuthorized)) let e1 = try await self.location.requestLocation() @@ -285,6 +311,7 @@ final class SwiftLocationTests: XCTestCase { } + #if !os(watchOS) && !os(tvOS) func testMonitorCLRegion() async throws { let (expectedValues, region) = simulateRegions() var idx = 0 @@ -297,7 +324,9 @@ final class SwiftLocationTests: XCTestCase { } } } + #endif + #if !os(watchOS) && !os(tvOS) func testMonitoringVisits() async throws { let expectedValues = simulateVisits() var idx = 0 @@ -310,7 +339,9 @@ final class SwiftLocationTests: XCTestCase { } } } + #endif + #if !os(watchOS) && !os(tvOS) func testMonitoringSignificantLocationChanges() async throws { let expectedValues = simulateSignificantLocations() var idx = 0 @@ -323,14 +354,18 @@ final class SwiftLocationTests: XCTestCase { } } } + #endif + #if !os(tvOS) func testAllowsBackgroundLocationUpdates() async throws { location.allowsBackgroundLocationUpdates = true XCTAssertEqual(location.allowsBackgroundLocationUpdates, location.locationManager.allowsBackgroundLocationUpdates) } + #endif // MARK: - Private Functions + #if !os(watchOS) && !os(tvOS) private func simulateSignificantLocations() -> [Tasks.SignificantLocationMonitoring.StreamEvent] { let sequence: [Tasks.SignificantLocationMonitoring.StreamEvent] = [ .didFailWithError(LocationErrors.timeout), @@ -354,7 +389,9 @@ final class SwiftLocationTests: XCTestCase { }) return sequence } + #endif + #if !os(watchOS) && !os(tvOS) private func simulateVisits() -> [Tasks.VisitsMonitoring.StreamEvent] { let sequence: [Tasks.VisitsMonitoring.StreamEvent] = [ .didVisit(CLVisit()), @@ -368,7 +405,9 @@ final class SwiftLocationTests: XCTestCase { }) return sequence } + #endif + #if !os(watchOS) && !os(tvOS) private func simulateRegions() -> (sequence: [Tasks.RegionMonitoring.StreamEvent], region: CLRegion) { let region = CLBeaconRegion(uuid: UUID(), identifier: "beacon_1") let sequence: [Tasks.RegionMonitoring.StreamEvent] = [ @@ -384,6 +423,7 @@ final class SwiftLocationTests: XCTestCase { }) return (sequence, region) } + #endif private func simulateRequestLocationDelayedResponse(event: Tasks.ContinuousUpdateLocation.StreamEvent) { DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { @@ -392,6 +432,7 @@ final class SwiftLocationTests: XCTestCase { } private func simulateLocationUpdates() -> [Tasks.ContinuousUpdateLocation.StreamEvent] { + #if os(iOS) let sequence: [Tasks.ContinuousUpdateLocation.StreamEvent] = [ .didUpdateLocations([ CLLocation( @@ -423,6 +464,37 @@ final class SwiftLocationTests: XCTestCase { ) ]) ] + #else + let sequence: [Tasks.ContinuousUpdateLocation.StreamEvent] = [ + .didUpdateLocations([ + CLLocation( + coordinate: CLLocationCoordinate2D(latitude: 41.915001, longitude: 12.577772), + altitude: 100, horizontalAccuracy: 50, verticalAccuracy: 20, timestamp: Date() + ), + CLLocation( + coordinate: CLLocationCoordinate2D(latitude: 41.8, longitude: 12.7), + altitude: 97, horizontalAccuracy: 30, verticalAccuracy: 10, timestamp: Date() + ) + ]), + .didFailed(LocationErrors.notAuthorized), + .didUpdateLocations([ + CLLocation( + coordinate: CLLocationCoordinate2D(latitude: 40, longitude: 13), + altitude: 4, horizontalAccuracy: 1, verticalAccuracy: 2, timestamp: Date() + ), + CLLocation( + coordinate: CLLocationCoordinate2D(latitude: 39, longitude: 15), + altitude: 1300, horizontalAccuracy: 300, verticalAccuracy: 1, timestamp: Date() + ) + ]), + .didUpdateLocations([ + CLLocation( + coordinate: CLLocationCoordinate2D(latitude: 10, longitude: 20), + altitude: 10, horizontalAccuracy: 30, verticalAccuracy: 20, timestamp: Date() + ) + ]) + ] + #endif DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { for event in sequence { @@ -447,14 +519,23 @@ final class SwiftLocationTests: XCTestCase { } private func simulateAuthorizationStatusChanges() -> [CLAuthorizationStatus] { + #if os(macOS) + let sequence : [CLAuthorizationStatus] = [.notDetermined, .restricted, .denied, .denied, .authorizedAlways] + #else let sequence : [CLAuthorizationStatus] = [.notDetermined, .restricted, .denied, .denied, .authorizedWhenInUse, .authorizedAlways] + #endif DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: { for value in sequence { self.mockLocationManager.authorizationStatus = value usleep(10) // 0.1s } }) + + #if os(macOS) + return [.restricted, .denied, .authorizedAlways] + #else return [.restricted, .denied, .authorizedWhenInUse, .authorizedAlways] + #endif } private func simulateLocationServicesChanges() -> [Bool] {