diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml new file mode 100644 index 0000000..033cb72 --- /dev/null +++ b/.github/workflows/continuous_integration.yml @@ -0,0 +1,34 @@ +name: Continuous Integration + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + sonarcloud: + name: Unit-Tests + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Link SwiftLint or install it + run: brew link --overwrite swiftlint || brew install swiftlint + + - name: Set up XCode + run: sudo xcode-select --switch /Applications/Xcode_15.0.app + + - name: Bundle Install + run: bundle install + + - name: Unit tests + run: bundle exec fastlane unit_tests + + - name: Code Coverage + run: bundle exec fastlane coverage + + - name: Lint + run: bundle exec fastlane lint \ No newline at end of file diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 0000000..20e4d4c --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,81 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + versionBumpLevel: + description: 'Version bump level (patch, minor, major)' + required: true + type: choice + default: 'patch' + options: + - patch + - minor + - major + +jobs: + build-and-release: + if: github.ref == 'refs/heads/main' + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Link SwiftLint or install it + run: brew link --overwrite swiftlint || brew install swiftlint + + - name: Set up XCode + run: sudo xcode-select --switch /Applications/Xcode_15.0.app + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + + - name: Bump version + run: ruby ./scripts/bump_versions.rb ${{ github.event.inputs.versionBumpLevel }} + + - name: Build XCFramework + run: ./scripts/build_framework.sh + + - name: Get new version + id: version + run: echo "VERSION=$(ruby -e 'puts File.read("./OSGeolocationLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV + + - name: Create new branch + run: | + git switch --create "prepare-new-release-${{ env.VERSION }}" + + - name: Move zip file to root and push changes + run: | + if [ -f OSGeolocationLib.zip ]; then + rm OSGeolocationLib.zip + else + echo "File does not exist." + fi + mv build/OSGeolocationLib.zip . + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "chore: Bump version to ${{ env.VERSION }}" + git push origin HEAD:prepare-new-release-${{ env.VERSION }} + + - name: Create pull request + id: create_pr + run: | + gh pr create -B main -H prepare-new-release-${{ env.VERSION }} --title 'Prepare `main` to Release `${{ env.VERSION }}`' --body 'Bumps version to `${{ env.VERSION }}`.
Creates an updated and ready-to-be-released `OSGeolocationLib.zip`.' + PR_NUMBER=$(gh pr view --json number --jq '.number') + echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Add label to the pull request + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/issues/${{ env.PR_NUMBER }}/labels \ + -f "labels[]=release" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release_and_publish.yml b/.github/workflows/release_and_publish.yml new file mode 100644 index 0000000..4735ea5 --- /dev/null +++ b/.github/workflows/release_and_publish.yml @@ -0,0 +1,67 @@ +name: Release and Publish + +on: + pull_request: + types: [closed] + branches: + - 'main' + +jobs: + post-merge: + if: contains(github.event.pull_request.labels.*.name, 'release') && github.event.pull_request.merged == true + runs-on: macos-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Cocoapods + run: gem install cocoapods + + - name: Get new version + id: version + run: echo "VERSION=$(ruby -e 'puts File.read("./OSGeolocationLib.podspec").match(/spec.version.*=.*''(\d+\.\d+\.\d+)''/)[1]')" >> $GITHUB_ENV + + - name: Extract release notes + run: sh scripts/extract_release_notes.sh "${{ env.VERSION }}" >> release_notes.md + + - name: Create Tag + id: create_tag + run: | + # Define the tag name and message + TAG_NAME="${{ env.VERSION }}" + TAG_MESSAGE="Tag for version ${{ env.VERSION }}" + + # Create the tag + git tag -a "$TAG_NAME" -m "$TAG_MESSAGE" + git push origin "$TAG_NAME" + + echo "Tag created: $TAG_NAME" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Release + run: | + # Extract the tag name + TAG_NAME="${{ env.VERSION }}" + RELEASE_NOTES="$(cat release_notes.md)" + + # Create the release using GitHub CLI + gh release create "$TAG_NAME" \ + --title "$TAG_NAME" \ + --notes "$RELEASE_NOTES" \ + "OSGeolocationLib.zip" + + echo "Release created for tag: $TAG_NAME" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to Cocoapods + run: pod trunk push ./OSGeolocationLib.podspec --allow-warnings + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + + - name: Delete Release Branch + run: git push origin --delete prepare-new-release-${{ env.VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..66bf042 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,34 @@ +disabled_rules: +- trailing_whitespace +- switch_case_alignment +opt_in_rules: +- empty_count +- empty_string +excluded: +- Carthage +- Pods +- vendor +- SwiftLint/Common/3rdPartyLib +line_length: + warning: 150 + error: 200 + ignores_function_declarations: true + ignores_comments: true + ignores_urls: true +function_body_length: + warning: 300 + error: 500 +function_parameter_count: + warning: 6 + error: 8 +type_body_length: + warning: 300 + error: 500 +file_length: + warning: 1000 + error: 1500 + ignore_comment_only_lines: true +cyclomatic_complexity: + warning: 15 + error: 25 +reporter: "xcode" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2d102ae --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Features +- Add complete implementation, including `getCurrentPosition`, `watchPosition`, and `clearWatch`. +- Create repository. \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..484334d --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "fastlane" +gem "slather" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..a8e6b78 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,273 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.2.2.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.1034.0) + aws-sdk-core (3.214.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.177.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) + claide (1.1.0) + clamp (1.3.2) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.3.4) + connection_pool (2.5.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + drb (2.2.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.0) + fastlane (2.226.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.9.1) + jwt (2.10.1) + base64 + logger (1.6.5) + mini_magick (4.13.2) + mini_mime (1.1.5) + mini_portile2 (2.8.8) + minitest (5.25.4) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.4.0) + naturally (2.2.1) + nkf (0.2.0) + nokogiri (1.18.1) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.1-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.1-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.1-x86_64-linux-gnu) + racc (~> 1.4) + optparse (0.6.0) + os (1.1.4) + plist (3.7.2) + public_suffix (6.0.1) + racc (1.8.1) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.4.0) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + securerandom (0.4.1) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + slather (2.8.5) + CFPropertyList (>= 2.2, < 4) + activesupport + clamp (~> 1.3) + nokogiri (>= 1.14.3) + xcodeproj (~> 1.27) + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.0) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + aarch64-linux + arm-linux + arm64-darwin + x86-linux + x86_64-darwin + x86_64-linux + +DEPENDENCIES + fastlane + slather + +BUNDLED WITH + 2.5.10 diff --git a/OSGeolocationLib.podspec b/OSGeolocationLib.podspec new file mode 100644 index 0000000..d91220b --- /dev/null +++ b/OSGeolocationLib.podspec @@ -0,0 +1,17 @@ +Pod::Spec.new do |spec| + spec.name = 'OSGeolocationLib' + spec.version = '0.0.1' + + spec.summary = 'A native iOS library for Geolocation authorisation and monitoring.' + spec.description = 'A Swift library for iOS that provides simple, reliable access to device GPS capabilities. Get location data, monitor position changes, and manage location services with a clean, modern API.' + + spec.homepage = 'https://github.com/ionic-team/OSGeolocationLib-iOS' + spec.license = { :type => 'MIT', :file => 'LICENSE' } + spec.author = { 'OutSystems Mobile Ecosystem' => 'rd.mobileecosystem.team@outsystems.com' } + + spec.source = { :http => "https://github.com/ionic-team/OSGeolocationLib-iOS/releases/download/#{spec.version}/OSGeolocationLib.zip", :type => "zip" } + spec.vendored_frameworks = "OSGeolocationLib.xcframework" + + spec.ios.deployment_target = '14.0' + spec.swift_versions = ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7', '5.8', '5.9'] +end \ No newline at end of file diff --git a/OSGeolocationLib.xcodeproj/project.pbxproj b/OSGeolocationLib.xcodeproj/project.pbxproj new file mode 100644 index 0000000..632bbd4 --- /dev/null +++ b/OSGeolocationLib.xcodeproj/project.pbxproj @@ -0,0 +1,558 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 752B49212D11B262002EA65D /* OSGLOCManagerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752B49202D11B262002EA65D /* OSGLOCManagerWrapper.swift */; }; + 752B49232D11D421002EA65D /* OSGLOCAuthorisationRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752B49222D11D421002EA65D /* OSGLOCAuthorisationRequestType.swift */; }; + 752B49262D11D440002EA65D /* OSGLOCAuthorisation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752B49252D11D440002EA65D /* OSGLOCAuthorisation.swift */; }; + 752B49282D11D46D002EA65D /* OSGLOCPositionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752B49272D11D46D002EA65D /* OSGLOCPositionModel.swift */; }; + 752B492B2D11DCC6002EA65D /* OSGLOCManagerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752B492A2D11DCC6002EA65D /* OSGLOCManagerProtocols.swift */; }; + 7575CF6A2BFCEE6F008F3FD0 /* OSGeolocationLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7575CF612BFCEE6F008F3FD0 /* OSGeolocationLib.framework */; }; + 7575CF802BFCEEEA008F3FD0 /* OSGLOCManagerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7575CF7D2BFCEEEA008F3FD0 /* OSGLOCManagerWrapperTests.swift */; }; + 75E8BAA12D12D5AB00ED4467 /* MockCLLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75E8BAA02D12D5AB00ED4467 /* MockCLLocationManager.swift */; }; + 75E8BAA32D12F87D00ED4467 /* MockServicesChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75E8BAA22D12F87300ED4467 /* MockServicesChecker.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7575CF6B2BFCEE6F008F3FD0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7575CF582BFCEE6F008F3FD0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7575CF602BFCEE6F008F3FD0; + remoteInfo = GeolocationLib; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 752B49202D11B262002EA65D /* OSGLOCManagerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSGLOCManagerWrapper.swift; sourceTree = ""; }; + 752B49222D11D421002EA65D /* OSGLOCAuthorisationRequestType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSGLOCAuthorisationRequestType.swift; sourceTree = ""; }; + 752B49252D11D440002EA65D /* OSGLOCAuthorisation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSGLOCAuthorisation.swift; sourceTree = ""; }; + 752B49272D11D46D002EA65D /* OSGLOCPositionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSGLOCPositionModel.swift; sourceTree = ""; }; + 752B492A2D11DCC6002EA65D /* OSGLOCManagerProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSGLOCManagerProtocols.swift; sourceTree = ""; }; + 7575CF612BFCEE6F008F3FD0 /* OSGeolocationLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OSGeolocationLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7575CF692BFCEE6F008F3FD0 /* OSGeolocationLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OSGeolocationLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7575CF7D2BFCEEEA008F3FD0 /* OSGLOCManagerWrapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSGLOCManagerWrapperTests.swift; sourceTree = ""; }; + 75E8BAA02D12D5AB00ED4467 /* MockCLLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCLLocationManager.swift; sourceTree = ""; }; + 75E8BAA22D12F87300ED4467 /* MockServicesChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServicesChecker.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7575CF5E2BFCEE6F008F3FD0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7575CF662BFCEE6F008F3FD0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7575CF6A2BFCEE6F008F3FD0 /* OSGeolocationLib.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 752B492C2D11DFDD002EA65D /* Publishers */ = { + isa = PBXGroup; + children = ( + 752B49202D11B262002EA65D /* OSGLOCManagerWrapper.swift */, + 752B492A2D11DCC6002EA65D /* OSGLOCManagerProtocols.swift */, + ); + path = Publishers; + sourceTree = ""; + }; + 7575CF572BFCEE6F008F3FD0 = { + isa = PBXGroup; + children = ( + 7575CF632BFCEE6F008F3FD0 /* OSGeolocationLib */, + 7575CF6D2BFCEE6F008F3FD0 /* OSGeolocationLibTests */, + 7575CF622BFCEE6F008F3FD0 /* Products */, + ); + sourceTree = ""; + }; + 7575CF622BFCEE6F008F3FD0 /* Products */ = { + isa = PBXGroup; + children = ( + 7575CF612BFCEE6F008F3FD0 /* OSGeolocationLib.framework */, + 7575CF692BFCEE6F008F3FD0 /* OSGeolocationLibTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7575CF632BFCEE6F008F3FD0 /* OSGeolocationLib */ = { + isa = PBXGroup; + children = ( + 752B492C2D11DFDD002EA65D /* Publishers */, + 752B49252D11D440002EA65D /* OSGLOCAuthorisation.swift */, + 752B49222D11D421002EA65D /* OSGLOCAuthorisationRequestType.swift */, + 752B49272D11D46D002EA65D /* OSGLOCPositionModel.swift */, + ); + path = OSGeolocationLib; + sourceTree = ""; + }; + 7575CF6D2BFCEE6F008F3FD0 /* OSGeolocationLibTests */ = { + isa = PBXGroup; + children = ( + 75E8BAA02D12D5AB00ED4467 /* MockCLLocationManager.swift */, + 75E8BAA22D12F87300ED4467 /* MockServicesChecker.swift */, + 7575CF7D2BFCEEEA008F3FD0 /* OSGLOCManagerWrapperTests.swift */, + ); + path = OSGeolocationLibTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 7575CF5C2BFCEE6F008F3FD0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 7575CF602BFCEE6F008F3FD0 /* OSGeolocationLib */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSGeolocationLib" */; + buildPhases = ( + 7575CF5C2BFCEE6F008F3FD0 /* Headers */, + 7575CF5D2BFCEE6F008F3FD0 /* Sources */, + 7575CF5E2BFCEE6F008F3FD0 /* Frameworks */, + 7575CF5F2BFCEE6F008F3FD0 /* Resources */, + 7575CF822BFCEEF2008F3FD0 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OSGeolocationLib; + productName = GeolocationLib; + productReference = 7575CF612BFCEE6F008F3FD0 /* OSGeolocationLib.framework */; + productType = "com.apple.product-type.framework"; + }; + 7575CF682BFCEE6F008F3FD0 /* OSGeolocationLibTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7575CF762BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSGeolocationLibTests" */; + buildPhases = ( + 7575CF652BFCEE6F008F3FD0 /* Sources */, + 7575CF662BFCEE6F008F3FD0 /* Frameworks */, + 7575CF672BFCEE6F008F3FD0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7575CF6C2BFCEE6F008F3FD0 /* PBXTargetDependency */, + ); + name = OSGeolocationLibTests; + productName = GeolocationLibTests; + productReference = 7575CF692BFCEE6F008F3FD0 /* OSGeolocationLibTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7575CF582BFCEE6F008F3FD0 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1600; + TargetAttributes = { + 7575CF602BFCEE6F008F3FD0 = { + CreatedOnToolsVersion = 15.1; + LastSwiftMigration = 1510; + }; + 7575CF682BFCEE6F008F3FD0 = { + CreatedOnToolsVersion = 15.1; + LastSwiftMigration = 1510; + }; + }; + }; + buildConfigurationList = 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "OSGeolocationLib" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7575CF572BFCEE6F008F3FD0; + packageReferences = ( + 752B491F2D11B151002EA65D /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */, + ); + productRefGroup = 7575CF622BFCEE6F008F3FD0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7575CF602BFCEE6F008F3FD0 /* OSGeolocationLib */, + 7575CF682BFCEE6F008F3FD0 /* OSGeolocationLibTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7575CF5F2BFCEE6F008F3FD0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7575CF672BFCEE6F008F3FD0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 7575CF822BFCEEF2008F3FD0 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: Swiftlint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7575CF5D2BFCEE6F008F3FD0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 752B49262D11D440002EA65D /* OSGLOCAuthorisation.swift in Sources */, + 752B49282D11D46D002EA65D /* OSGLOCPositionModel.swift in Sources */, + 752B49232D11D421002EA65D /* OSGLOCAuthorisationRequestType.swift in Sources */, + 752B49212D11B262002EA65D /* OSGLOCManagerWrapper.swift in Sources */, + 752B492B2D11DCC6002EA65D /* OSGLOCManagerProtocols.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7575CF652BFCEE6F008F3FD0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7575CF802BFCEEEA008F3FD0 /* OSGLOCManagerWrapperTests.swift in Sources */, + 75E8BAA12D12D5AB00ED4467 /* MockCLLocationManager.swift in Sources */, + 75E8BAA32D12F87D00ED4467 /* MockServicesChecker.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7575CF6C2BFCEE6F008F3FD0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7575CF602BFCEE6F008F3FD0 /* OSGeolocationLib */; + targetProxy = 7575CF6B2BFCEE6F008F3FD0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7575CF712BFCEE6F008F3FD0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 0; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.0.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7575CF722BFCEE6F008F3FD0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 0; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.0.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 7575CF742BFCEE6F008F3FD0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 0; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 0.0.1; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.geolocation.GeolocationLib; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7575CF752BFCEE6F008F3FD0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 0; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 0.0.1; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.geolocation.GeolocationLib; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7575CF772BFCEE6F008F3FD0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 0; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 0.0.1; + PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.geolocation.GeolocationLibTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7575CF782BFCEE6F008F3FD0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 0; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MARKETING_VERSION = 0.0.1; + PRODUCT_BUNDLE_IDENTIFIER = com.outsystems.rd.geolocation.GeolocationLibTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7575CF5B2BFCEE6F008F3FD0 /* Build configuration list for PBXProject "OSGeolocationLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7575CF712BFCEE6F008F3FD0 /* Debug */, + 7575CF722BFCEE6F008F3FD0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7575CF732BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSGeolocationLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7575CF742BFCEE6F008F3FD0 /* Debug */, + 7575CF752BFCEE6F008F3FD0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7575CF762BFCEE6F008F3FD0 /* Build configuration list for PBXNativeTarget "OSGeolocationLibTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7575CF772BFCEE6F008F3FD0 /* Debug */, + 7575CF782BFCEE6F008F3FD0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 752B491F2D11B151002EA65D /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.57.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + }; + rootObject = 7575CF582BFCEE6F008F3FD0 /* Project object */; +} diff --git a/OSGeolocationLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/OSGeolocationLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/OSGeolocationLib.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..d3aadb6 --- /dev/null +++ b/OSGeolocationLib.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "1fa961aa1dc717cea452f3389668f0f99a254f07e4eb11d190768a53798f744f", + "pins" : [ + { + "identity" : "swiftlintplugins", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "f9731bef175c3eea3a0ca960f1be78fcc2bc7853", + "version" : "0.57.1" + } + } + ], + "version" : 2 +} diff --git a/OSGeolocationLib.xcodeproj/xcshareddata/xcschemes/OSGeolocationLib.xcscheme b/OSGeolocationLib.xcodeproj/xcshareddata/xcschemes/OSGeolocationLib.xcscheme new file mode 100644 index 0000000..cf10041 --- /dev/null +++ b/OSGeolocationLib.xcodeproj/xcshareddata/xcschemes/OSGeolocationLib.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OSGeolocationLib/OSGLOCAuthorisation.swift b/OSGeolocationLib/OSGLOCAuthorisation.swift new file mode 100644 index 0000000..57d87a5 --- /dev/null +++ b/OSGeolocationLib/OSGLOCAuthorisation.swift @@ -0,0 +1,26 @@ +import CoreLocation + +public enum OSGLOCAuthorisation { + case notDetermined + case restricted + case denied + case authorisedAlways + case authorisedWhenInUse + + init(from status: CLAuthorizationStatus) { + self = switch status { + case .notDetermined: .notDetermined + case .restricted: .restricted + case .denied: .denied + case .authorizedAlways: .authorisedAlways + case .authorizedWhenInUse: .authorisedWhenInUse + @unknown default: .notDetermined + } + } +} + +extension CLLocationManager { + var currentAuthorisationValue: OSGLOCAuthorisation { + .init(from: authorizationStatus) + } +} diff --git a/OSGeolocationLib/OSGLOCAuthorisationRequestType.swift b/OSGeolocationLib/OSGLOCAuthorisationRequestType.swift new file mode 100644 index 0000000..d44815e --- /dev/null +++ b/OSGeolocationLib/OSGLOCAuthorisationRequestType.swift @@ -0,0 +1,16 @@ +import CoreLocation + +public enum OSGLOCAuthorisationRequestType { + case whenInUse + case always + + func requestAuthorization(using locationManager: CLLocationManager) { + let requestAuthorisation = switch self { + case .whenInUse: + locationManager.requestWhenInUseAuthorization + case .always: + locationManager.requestAlwaysAuthorization + } + requestAuthorisation() + } +} diff --git a/OSGeolocationLib/OSGLOCPositionModel.swift b/OSGeolocationLib/OSGLOCPositionModel.swift new file mode 100644 index 0000000..22dfeae --- /dev/null +++ b/OSGeolocationLib/OSGLOCPositionModel.swift @@ -0,0 +1,44 @@ +import CoreLocation + +public struct OSGLOCPositionModel: Equatable { + private(set) public var altitude: Double + private(set) public var course: Double + private(set) public var horizontalAccuracy: Double + private(set) public var latitude: Double + private(set) public var longitude: Double + private(set) public var speed: Double + private(set) public var timestamp: Double + private(set) public var verticalAccuracy: Double + + private init(altitude: Double, course: Double, horizontalAccuracy: Double, latitude: Double, longitude: Double, speed: Double, timestamp: Double, verticalAccuracy: Double) { + self.altitude = altitude + self.course = course + self.horizontalAccuracy = horizontalAccuracy + self.latitude = latitude + self.longitude = longitude + self.speed = speed + self.timestamp = timestamp + self.verticalAccuracy = verticalAccuracy + } +} + +public extension OSGLOCPositionModel { + static func create(from location: CLLocation) -> OSGLOCPositionModel { + .init( + altitude: location.altitude, + course: location.course, + horizontalAccuracy: location.horizontalAccuracy, + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude, + speed: location.speed, + timestamp: location.timestamp.millisecondsSinceUnixEpoch, + verticalAccuracy: location.verticalAccuracy + ) + } +} + +private extension Date { + var millisecondsSinceUnixEpoch: Double { + timeIntervalSince1970 * 1000 + } +} diff --git a/OSGeolocationLib/Publishers/OSGLOCManagerProtocols.swift b/OSGeolocationLib/Publishers/OSGLOCManagerProtocols.swift new file mode 100644 index 0000000..3ba0da4 --- /dev/null +++ b/OSGeolocationLib/Publishers/OSGLOCManagerProtocols.swift @@ -0,0 +1,43 @@ +import Combine + +public protocol OSGLOCServicesChecker { + func areLocationServicesEnabled() -> Bool +} + +public protocol OSGLOCAuthorisationHandler { + var authorisationStatus: OSGLOCAuthorisation { get } + var authorisationStatusPublisher: Published.Publisher { get } + + func requestAuthorisation(withType authorisationType: OSGLOCAuthorisationRequestType) +} + +public enum OSGLOCLocationError: Error { + case locationUnavailable + case other(_ error: Error) +} + +public protocol OSGLOCLocationHandler { + var currentLocation: OSGLOCPositionModel? { get } + var currentLocationPublisher: AnyPublisher { get } + + func updateConfiguration(_ configuration: OSGLOCConfigurationModel) +} + +public protocol OSGLOCSingleLocationHandler: OSGLOCLocationHandler { + func requestSingleLocation() +} + +public protocol OSGLOCMonitorLocationHandler: OSGLOCLocationHandler { + func startMonitoringLocation() + func stopMonitoringLocation() +} + +public struct OSGLOCConfigurationModel { + private(set) var enableHighAccuracy: Bool + private(set) var minimumUpdateDistanceInMeters: Double? + + public init(enableHighAccuracy: Bool, minimumUpdateDistanceInMeters: Double? = nil) { + self.enableHighAccuracy = enableHighAccuracy + self.minimumUpdateDistanceInMeters = minimumUpdateDistanceInMeters + } +} diff --git a/OSGeolocationLib/Publishers/OSGLOCManagerWrapper.swift b/OSGeolocationLib/Publishers/OSGLOCManagerWrapper.swift new file mode 100644 index 0000000..7c7347f --- /dev/null +++ b/OSGeolocationLib/Publishers/OSGLOCManagerWrapper.swift @@ -0,0 +1,86 @@ +import Combine +import CoreLocation + +public typealias OSGLOCService = OSGLOCServicesChecker & OSGLOCAuthorisationHandler & OSGLOCSingleLocationHandler & OSGLOCMonitorLocationHandler + +public struct OSGLOCServicesValidator: OSGLOCServicesChecker { + public init() {} + + public func areLocationServicesEnabled() -> Bool { + CLLocationManager.locationServicesEnabled() + } +} + +public class OSGLOCManagerWrapper: NSObject, OSGLOCService { + @Published public var authorisationStatus: OSGLOCAuthorisation + public var authorisationStatusPublisher: Published.Publisher { $authorisationStatus } + + @Published public var currentLocation: OSGLOCPositionModel? + public var currentLocationPublisher: AnyPublisher { + $currentLocation + .dropFirst() // ignore the first value as it's the one set on the constructor. + .tryMap { location in + guard let location else { throw OSGLOCLocationError.locationUnavailable } + return location + } + .mapError { $0 as? OSGLOCLocationError ?? .other($0) } + .eraseToAnyPublisher() + } + + private let locationManager: CLLocationManager + private let servicesChecker: OSGLOCServicesChecker + + public init(locationManager: CLLocationManager = .init(), servicesChecker: OSGLOCServicesChecker = OSGLOCServicesValidator()) { + self.locationManager = locationManager + self.servicesChecker = servicesChecker + self.authorisationStatus = locationManager.currentAuthorisationValue + + super.init() + locationManager.delegate = self + } + + public func requestAuthorisation(withType authorisationType: OSGLOCAuthorisationRequestType) { + authorisationType.requestAuthorization(using: locationManager) + } + + public func startMonitoringLocation() { + locationManager.startUpdatingLocation() + } + + public func stopMonitoringLocation() { + locationManager.stopUpdatingLocation() + } + + public func requestSingleLocation() { + locationManager.requestLocation() + } + + public func updateConfiguration(_ configuration: OSGLOCConfigurationModel) { + locationManager.desiredAccuracy = configuration.enableHighAccuracy ? kCLLocationAccuracyBest : kCLLocationAccuracyThreeKilometers + configuration.minimumUpdateDistanceInMeters.map { + locationManager.distanceFilter = $0 + } + } + + public func areLocationServicesEnabled() -> Bool { + servicesChecker.areLocationServicesEnabled() + } +} + +extension OSGLOCManagerWrapper: CLLocationManagerDelegate { + public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + authorisationStatus = manager.currentAuthorisationValue + } + + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let latestLocation = locations.last else { + currentLocation = nil + return + } + currentLocation = OSGLOCPositionModel.create(from: latestLocation) + } + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { + currentLocation = nil + } +} diff --git a/OSGeolocationLibTests/MockCLLocationManager.swift b/OSGeolocationLibTests/MockCLLocationManager.swift new file mode 100644 index 0000000..7cbf259 --- /dev/null +++ b/OSGeolocationLibTests/MockCLLocationManager.swift @@ -0,0 +1,46 @@ +import CoreLocation + +class MockCLLocationManager: CLLocationManager { + private(set) var didCallRequestAlwaysAuthorization = false + private(set) var didCallRequestLocation = false + private(set) var didCallRequestWhenInUseAuthorization = false + private(set) var didStartUpdatingLocation = false + private(set) var mockAuthorizationStatus: CLAuthorizationStatus = .notDetermined + + override var authorizationStatus: CLAuthorizationStatus { + mockAuthorizationStatus + } + + override func startUpdatingLocation() { + didStartUpdatingLocation = true + } + + override func stopUpdatingLocation() { + didStartUpdatingLocation = false + } + + override func requestLocation() { + didCallRequestLocation = true + } + + override func requestAlwaysAuthorization() { + didCallRequestAlwaysAuthorization = true + } + + override func requestWhenInUseAuthorization() { + didCallRequestWhenInUseAuthorization = true + } + + func changeAuthorisation(to status: CLAuthorizationStatus) { + self.mockAuthorizationStatus = status + delegate?.locationManagerDidChangeAuthorization?(self) + } + + func updateLocation(to locations: [CLLocation]) { + delegate?.locationManager?(self, didUpdateLocations: locations) + } + + func failWhileUpdatingLocation(_ error: Error) { + delegate?.locationManager?(self, didFailWithError: error) + } +} diff --git a/OSGeolocationLibTests/MockServicesChecker.swift b/OSGeolocationLibTests/MockServicesChecker.swift new file mode 100644 index 0000000..19c03e9 --- /dev/null +++ b/OSGeolocationLibTests/MockServicesChecker.swift @@ -0,0 +1,17 @@ +import OSGeolocationLib + +class MockServicesChecker: OSGLOCServicesChecker { + private var didEnableLocationServices = false + + func areLocationServicesEnabled() -> Bool { + didEnableLocationServices + } + + func enableLocationServices() { + didEnableLocationServices = true + } + + func disableLocationServices() { + didEnableLocationServices = false + } +} diff --git a/OSGeolocationLibTests/OSGLOCManagerWrapperTests.swift b/OSGeolocationLibTests/OSGLOCManagerWrapperTests.swift new file mode 100644 index 0000000..6e9e7b6 --- /dev/null +++ b/OSGeolocationLibTests/OSGLOCManagerWrapperTests.swift @@ -0,0 +1,323 @@ +import OSGeolocationLib +import XCTest + +import Combine +import CoreLocation + +final class OSGLOCManagerWrapperTests: XCTestCase { + private var sut: OSGLOCManagerWrapper! + + private var locationManager: MockCLLocationManager! + private var servicesChecker: MockServicesChecker! + private var cancellables: Set! + + override func setUp() { + super.setUp() + locationManager = MockCLLocationManager() + servicesChecker = MockServicesChecker() + cancellables = .init() + sut = .init(locationManager: locationManager, servicesChecker: servicesChecker) + } + + override func tearDown() { + sut = nil + cancellables = nil + servicesChecker = nil + locationManager = nil + super.tearDown() + } + + // MARK: - 'requestAuthorisation' tests + + func test_requestWhenInUseAuthorisation_triggersALocationManagerWhenInUseAuthorizationRequest() { + // Given + XCTAssertFalse(locationManager.didCallRequestWhenInUseAuthorization) + + // When + sut.requestAuthorisation(withType: .whenInUse) + + // Then + XCTAssertTrue(locationManager.didCallRequestWhenInUseAuthorization) + } + + func test_requestAlwaysAuthorisation_triggersALocationManagerAlwaysAuthorizationRequest() { + // Given + XCTAssertFalse(locationManager.didCallRequestAlwaysAuthorization) + + // When + sut.requestAuthorisation(withType: .always) + + // Then + XCTAssertTrue(locationManager.didCallRequestAlwaysAuthorization) + } + + func test_locationManagerAuthorisationChangesToWhenInUse_authorisationStatusUpdatesToWhenInUse() { + // Given + let expectedStatus = OSGLOCAuthorisation.authorisedWhenInUse + let expectation = expectation(description: "Authorisation status updated to 'authorisedWhenInUse'.") + + validateAuthorisationStatusPublisher(expectation, expectedStatus) + + // When + locationManager.changeAuthorisation(to: .authorizedWhenInUse) + + // Then + waitForExpectations(timeout: 1.0) + } + + func test_locationManagerAuthorisationChangesToAlways_authorisationStatusUpdatesToAlways() { + // Given + let expectedStatus = OSGLOCAuthorisation.authorisedAlways + let expectation = expectation(description: "Authorisation status updated to 'authorisedAlways'.") + + validateAuthorisationStatusPublisher(expectation, expectedStatus) + + // When + locationManager.changeAuthorisation(to: .authorizedAlways) + + // Then + waitForExpectations(timeout: 1.0) + } + + func test_locationManagerAuthorisationChangesToWhenInUse_andThenToAlways_authorisationStatusUpdatesToAlways() { + // Given + locationManager.changeAuthorisation(to: .authorizedWhenInUse) + + let expectedStatus = OSGLOCAuthorisation.authorisedAlways + let expectationAlways = expectation(description: "Authorisation status updated to 'authorisedAlways'.") + validateAuthorisationStatusPublisher(expectationAlways, expectedStatus) + + // When + locationManager.changeAuthorisation(to: .authorizedAlways) + + // Then + waitForExpectations(timeout: 1.0) + } + + // MARK: - 'startMonitoringLocation' tests + + func test_startMonitoringLocation_setsUpLocationManager() { + // Given + XCTAssertFalse(locationManager.didStartUpdatingLocation) + + // When + sut.startMonitoringLocation() + + // Then + XCTAssertTrue(locationManager.didStartUpdatingLocation) + } + + // MARK: - 'stopMonitoringLocation' tests + + func test_startMonitoringLocation_thenStop_locationManagerStopsMonitoring() { + // Given + XCTAssertFalse(locationManager.didStartUpdatingLocation) + + // When + sut.startMonitoringLocation() + + XCTAssertTrue(locationManager.didStartUpdatingLocation) + + sut.stopMonitoringLocation() + + // Then + XCTAssertFalse(locationManager.didStartUpdatingLocation) + } + + // MARK: - 'requestSingleLocation' tests + + func test_requestSingleLocation_returnsIt() { + // Given + XCTAssertFalse(locationManager.didCallRequestLocation) + + // When + sut.requestSingleLocation() + + // Then + XCTAssertTrue(locationManager.didCallRequestLocation) + } + + // MARK: - 'updateConfiguration' tests + + func test_enableHighAccuracy_thenLocationManagerUpdatesIt() { + // Given + XCTAssertEqual(locationManager.desiredAccuracy, CLLocationManager.defaultDesiredAccuracy) + XCTAssertEqual(locationManager.distanceFilter, CLLocationManager.defaultDistanceFilter) + + // When + let configuration = OSGLOCConfigurationModel(enableHighAccuracy: true) + sut.updateConfiguration(configuration) + + // Then + XCTAssertEqual(locationManager.desiredAccuracy, kCLLocationAccuracyBest) + XCTAssertEqual(locationManager.distanceFilter, CLLocationManager.defaultDistanceFilter) + } + + func test_disableHighAccuracy_thenLocationManagerUpdatesIt() { + // Given + XCTAssertEqual(locationManager.desiredAccuracy, CLLocationManager.defaultDesiredAccuracy) + XCTAssertEqual(locationManager.distanceFilter, CLLocationManager.defaultDistanceFilter) + + // When + let configuration = OSGLOCConfigurationModel(enableHighAccuracy: false) + sut.updateConfiguration(configuration) + + // Then + XCTAssertEqual(locationManager.desiredAccuracy, kCLLocationAccuracyThreeKilometers) + XCTAssertEqual(locationManager.distanceFilter, CLLocationManager.defaultDistanceFilter) + } + + func test_setMinimumUpdateDistanceInMeters_thenLocationManagerUpdatesIt() { + // Given + XCTAssertEqual(locationManager.desiredAccuracy, CLLocationManager.defaultDesiredAccuracy) + XCTAssertEqual(locationManager.distanceFilter, CLLocationManager.defaultDistanceFilter) + + // When + let configuration = OSGLOCConfigurationModel(enableHighAccuracy: true, minimumUpdateDistanceInMeters: 10) + sut.updateConfiguration(configuration) + + // Then + XCTAssertEqual(locationManager.desiredAccuracy, kCLLocationAccuracyBest) + XCTAssertEqual(locationManager.distanceFilter, 10) + } + + // MARK: - 'areLocationServicesEnabled' tests + + func test_enableLocationServices_updatesLocationManager() { + // Given + XCTAssertFalse(sut.areLocationServicesEnabled()) + + // When + servicesChecker.enableLocationServices() + + // Then + XCTAssertTrue(sut.areLocationServicesEnabled()) + } + + func test_disableLocationServices_updatesLocationManager() { + // Given + XCTAssertFalse(sut.areLocationServicesEnabled()) + + // When + servicesChecker.enableLocationServices() + + XCTAssertTrue(sut.areLocationServicesEnabled()) + + servicesChecker.disableLocationServices() + + // Then + XCTAssertFalse(sut.areLocationServicesEnabled()) + } + + // MARK: - Location Monitoring Tests + + func test_locationIsUpdated_locationManagerTriggersNewPosition() { + // Given + let expectedLocation = CLLocation(latitude: 37.7749, longitude: -122.4194) + let expectedPosition = OSGLOCPositionModel.create(from: expectedLocation) + let expectation = expectation(description: "Location updated.") + + validateCurrentLocationPublisher(expectation, expectedPosition) + + // When + locationManager.updateLocation(to: [expectedLocation]) + + // Then + waitForExpectations(timeout: 1.0) + } + + func test_locationIsUpdatedTwice_locationManagerTriggersLatestPosition() { + // Given + let firstLocation = CLLocation(latitude: 37.7749, longitude: -122.4194) + let expectedLocation = CLLocation(latitude: 48.8859, longitude: -111.3083) + let expectedPosition = OSGLOCPositionModel.create(from: expectedLocation) + let expectation = expectation(description: "Location updated.") + + validateCurrentLocationPublisher(expectation, expectedPosition) + + // When + locationManager.updateLocation(to: [firstLocation, expectedLocation]) + + // Then + waitForExpectations(timeout: 1.0) + } + + func test_locationIsUpdated_andThenAgain_locationManagerTriggersLatestPosition() { + // Given + let firstLocation = CLLocation(latitude: 37.7749, longitude: -122.4194) + locationManager.updateLocation(to: [firstLocation]) + + let expectedLocation = CLLocation(latitude: 48.8859, longitude: -111.3083) + let expectedPosition = OSGLOCPositionModel.create(from: expectedLocation) + let expectation = expectation(description: "Location updated.") + validateCurrentLocationPublisher(expectation, expectedPosition) + + // When + locationManager.updateLocation(to: [expectedLocation]) + + // Then + waitForExpectations(timeout: 1.0) + } + + func test_locationIsMissing_locationManagerTriggersError() { + // Given + let noLocationData = [CLLocation]() + let expectation = expectation(description: "Location missing data.") + + validateCurrentLocationPublisher(expectation) + + // When + locationManager.updateLocation(to: noLocationData) + + // Then + waitForExpectations(timeout: 1.0) + } + + func test_locationUpdateFailes_locationManagerTriggersError() { + // Given + let mockError = MockLocationUpdateError.locationUpdateFailed + let expectation = expectation(description: "Location update failed.") + + validateCurrentLocationPublisher(expectation) + + // When + locationManager.failWhileUpdatingLocation(mockError) + + // Then + waitForExpectations(timeout: 1.0) + } +} + +private extension OSGLOCManagerWrapperTests { + func validateCurrentLocationPublisher(_ expectation: XCTestExpectation, _ expectedPosition: OSGLOCPositionModel? = nil) { + sut.currentLocationPublisher + .sink(receiveCompletion: { completion in + if expectedPosition == nil, case .failure = completion { + expectation.fulfill() + } + }, receiveValue: { newPosition in + XCTAssertEqual(newPosition, expectedPosition) + expectation.fulfill() + }) + .store(in: &cancellables) + } + + func validateAuthorisationStatusPublisher(_ expectation: XCTestExpectation, _ expectedStatus: OSGLOCAuthorisation) { + sut.authorisationStatusPublisher + .dropFirst() // ignore the first value as it's the one set on the constructor. + .sink { status in + XCTAssertEqual(status, expectedStatus) + expectation.fulfill() + } + .store(in: &cancellables) + } +} + +private extension CLLocationManager { + static var defaultDesiredAccuracy = kCLLocationAccuracyBest + static var defaultDistanceFilter = kCLDistanceFilterNone +} + +private enum MockLocationUpdateError: Error { + case locationUpdateFailed +} diff --git a/README.md b/README.md index d4dfb34..42ae8c1 100644 --- a/README.md +++ b/README.md @@ -1 +1,252 @@ -# OSGeolocationLib-iOS \ No newline at end of file +# OSGeolocationLib + +A Swift library for iOS that provides simple, reliable access to device GPS capabilities. Get location data, monitor position changes, and manage location services with a clean, modern API. + +[![License](https://img.shields.io/cocoapods/l/OSGeolocationLib.svg)](https://cocoapods.org/pods/OSGeolocationLib) +[![Version](https://img.shields.io/cocoapods/v/OSGeolocationLib.svg)](https://cocoapods.org/pods/OSGeolocationLib) +[![Platform](https://img.shields.io/cocoapods/p/OSGeolocationLib.svg)](https://cocoapods.org/pods/OSGeolocationLib) + +## Requirements + +- iOS 14.0+ +- Swift 5.0+ +- Xcode 15.0+ + +## Installation + +### CocoaPods + +`OSGeolocationLib` is available through [CocoaPods](https://cocoapods.org). Add this to your Podfile: + +```ruby +pod 'OSGeolocationLib', '~> 0.0.1' # Use the latest 0.0.x version +``` + +## Quick Start + +This library is currently used by the Geolocation Plugin for OutSystems' [Cordova](https://github.com/ionic-team/cordova-outsystems-geolocation) and [Capacitor](https://github.com/ionic-team/outsystems-geolocation) Plugins. Please check the library usage there for real use-case scenarios. + +## Features + +All the library's features are split in 4 different protocols. Each are detailed in the following subsections: +- `OSGLOCServicesChecker` +- `OSGLOCAuthorisationHandler` +- `OSGLOCSingleLocationHandler` +- `OSGLOCMonitorLocationHandler` + +There's also the typealias `OSGLOCService` that merges all protocols together. Its concrete implementation is achieved by the `OSGLOCManagerWrapper` class. + +### `OSGLOCServicesChecker` + +The sole goal of `OSGLOCServicesChecker` is to verify if the location services have been enabled on the device. + +#### Check if Location Services are Enabled + +```swift +func areLocationServicesEnabled() -> Bool +``` + +Returns a Boolean value indicating whether location services are enabled on the device. + + +### `OSGLOCAuthorisationHandler` + +Manages all authorisation status logic related with location. It's composed by the following: +- a property that indicates the app's at-the-moment authorisation status to use location services; +- a publisher that delivers all authorisation status updates to its subscribers; +- a method that requests the user's permission to use location services. + +Authorisation is vital to receive location-related information. The user needs to be prompted to grant permission to the app to use location services. + +#### Location Services' Authorisation Status Property + +```swift +var authorisationStatus: OSGLOCAuthorisation +``` + +It returns the at-the-moment authorisation status to use the device's location services. The following are the possible values: +- `notDetermined`: User hasn't chosen whether the app can use location services. This is the property's default value; +- `restricted`: App is not authorized to use location services; +- `denied`: User denied the use of location services for the app or globally; +- `authorisedAlways`: User authorized the app to start location services at any time; +- `authorisedWhenInUse`: User authorized the app to start location services while it is in use. + +#### Location Services' Authorisation Status Publisher + +```swift +var authorisationStatusPublisher: Published.Publisher +``` + +It returns a publisher that delivers all authorisation status updates to whoever subscribes to it. The `authorisationStatus` values are the elements that can be emitted by `authorisationStatusPublisher`. + +#### Request User's Permission to Use Location Services + +``` +func requestAuthorisation(withType authorisationType: OSGLOCAuthorisationRequestType) +``` + +Requests the user’s permission to use location services. There are two types of authorisation that can be requested: +- `always`: Requests the user’s permission to use location services regardless of whether the app is in use; +- `whenInUse`: Requests the user’s permission to use location services while the app is in use. + +### `OSGLOCLocationHandler` + +Manages all location-related information. It's composed by the following: +- a property that retrieves the device's at-the-moment location position. It can be `nil` if there hasn't been a request or in case of some issue occurring while fetching it; +- a publisher that delivers all location updates to its subscribers. This includes successful updates or the error it occurred while updating. +- a method that updates two conditions that influence how the location updates are performed: + - the location data accuracy the app wants to receive; + - the minimum distance the device must move horizontally before an update event is generated. The distance is measured in meters (m). + +`OSGLOCLocationHandler` serves has the base for both `OSGLOCSingleLocationHandler` and `OSGLOCMonitorLocationHandler`. More on both later. + +#### Current Location Property + +```swift +var currentLocation: OSGLOCPositionModel? +``` + +It returns the device's latest fetched location position. It can be `nil` if there hasn't been a request or in case of some issue occuring while fetching it. +`OSGLOCPositionModel` is composed by the following properties: +- `altitude`: Altitude above mean sea level, measured in meters (m); +- `course`: Direction in which the device is travelling, measured in degrees (º) and relative to due north; +- `horizontalAccuracy`: Radius of uncertainty, measured in meters (m); +- `latitude`: Latitude of the geographical coordinate, measured in degrees (º) and relative to due north; +- `longitude`: Longitude of the geographical coordinate, measured in degrees (º) and relative to the zero meridian; +- `speed`: Instantaneous speed of the device, measured in meters per second (m/s); +- `timestamp`: Time at which this location was determined, measured in milliseconds (ms) elapsed since the UNIX epoch (Jan 1, 1970); +- `verticalAccuracy`: Validity of the altitude values and their estimated uncertainty, measured in meters (m). + +#### Current Location Publisher + +```swift +var currentLocationPublisher: AnyPublisher +``` + +It returns a publisher that delivers all location updates to whoever subscribes to it. The `currentLocation` values are the elements that can be emitted by `currentLocationPublisher`. + +#### Update the Location Manager's Configuration + +```swift +func updateConfiguration(_ configuration: OSGLOCConfigurationModel) +``` + +Updates two properties that condition how location update events are generated: +- `enableHighAccuracy`: Boolean value that indicates if the app wants location data accuracy to be at its best or not. It needs to be explicitly mentioned by the method callers +- `minimumUpdateDistanceInMeters`: Minimum distance the device must move horizontally before an update event is generated, measured in meters (m). As it's optional, it can be omitted by the method callers. + +### `OSGLOCSingleLocationHandler` + +It's responsible to trigger one-time deliveries of the device's current location. It's composed by the following: +- a method that requests the user's current location position. + +#### Request Device's Current Location + +```swift +func requestSingleLocation() +``` + +The method returns immediately. By calling it, it triggers an update to `currentLocation` and a new element delivery by `currentLocationPublisher`. + + +### `OSGLOCMonitorLocationHandler` + +It's responsible for the continuous generation of updates that report the device's current location position. It's composed by the following: +- a method that starts the generation of updates; +- a method that ends the generation of updates. + +#### Start Monitoring the Device's Position + +```swift +func startMonitoringLocation() +``` + +The method returns immediately. By calling it, it triggers an update to `currentLocation` and signals `currentLocationPublisher` to continuously emit relevant location updates. + +#### Stop Monitoring the Device's Position + +```swift +func stopMonitoringLocation() +``` + +The method should be called whenever you no longer need to received location-related events. + +## Error Handling + +The library uses `OSGLOCLocationError` for error handling regarding location position updates. Possible errors include: + +```swift +enum OSGLOCLocationError: Error { + case locationUnavailable + case other(_ error: Error) +} +``` + +## Location Data Format + +Location updates are delivered as `OSGLOCPositionModel` objects: + +```json +{ + "latitude": 37.7749, + "longitude": -122.4194, + "altitude": 0.0, + "horizontalAccuracy": 5.0, + "verticalAccuracy": 10.0, + "course": 180.0, + "speed": 0.0, + "timestamp": 1641034800000 +} +``` + +## Battery Impact Considerations + +- High accuracy mode (`enableHighAccuracy: true`) uses GPS and significantly impacts battery life +- Consider using a larger `minimumUpdateDistanceInMeters` for battery optimization +- Call `stopMonitoringLocation()` when updates are no longer needed + +## Background Location + +To enable background location updates: + +1. Add required background modes to `Info.plist`: +```xml +UIBackgroundModes + Location updates +``` + +2. Request "always" authorization: + +```swift +locationService.requestAuthorisation(withType: .always) +``` + +## Troubleshooting + +Common issues and solutions: + +1. Location updates not received + - Check authorization status + - Verify location services are enabled + - Ensure proper `Info.plist` permissions + +2. Poor accuracy + - Enable high accuracy mode + - Ensure clear sky view + - Wait for better GPS signal + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +`OSGeolocationLib` is available under the MIT license. See the [LICENSE](LICENSE) file for more info. + +## Support + +- Report issues on our [Issue Tracker](https://github.com/ionic-team/OSGeolocationLib-iOS/issues) \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..4282947 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,6 @@ +# app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app +# apple_id("[[APPLE_ID]]") # Your Apple Developer Portal username + + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..84597b0 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,44 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Lane to run the unit tests" + lane :unit_tests do + run_tests(scheme: "OSGeolocationLib") + end + + desc "Code coverage" + lane :coverage do + slather( + scheme: "OSGeolocationLib", + proj: "OSGeolocationLib.xcodeproj", + output_directory: "sonar-reports", + sonarqube_xml: "true" + ) + end + + lane :lint do + swiftlint( + output_file: "sonar-reports/OSGeolocationLib-swiftlint.txt", + ignore_exit_status: true + ) + end + + lane :sonarqube do + sonar + end +end diff --git a/scripts/build_framework.sh b/scripts/build_framework.sh new file mode 100755 index 0000000..3a79b0f --- /dev/null +++ b/scripts/build_framework.sh @@ -0,0 +1,37 @@ +BUILD_FOLDER="build" +BUILD_SCHEME="OSGeolocationLib" +FRAMEWORK_NAME="OSGeolocationLib" +SIMULATOR_ARCHIVE_PATH="${BUILD_FOLDER}/iphonesimulator.xcarchive" +IOS_DEVICE_ARCHIVE_PATH="${BUILD_FOLDER}/iphoneos.xcarchive" + +rm -rf "${FRAMEWORK_NAME}.zip" +rm -rf ${BUILD_FOLDER} + +xcodebuild archive \ + -scheme ${BUILD_SCHEME} \ + -configuration Release \ + -destination 'generic/platform=iOS Simulator' \ + -archivePath "./${SIMULATOR_ARCHIVE_PATH}/" \ + SKIP_INSTALL=NO \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES + +xcodebuild archive \ + -scheme ${BUILD_SCHEME} \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -archivePath "./${IOS_DEVICE_ARCHIVE_PATH}/" \ + SKIP_INSTALL=NO \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES + +xcodebuild -create-xcframework \ + -framework "./${SIMULATOR_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -debug-symbols "${PWD}/${SIMULATOR_ARCHIVE_PATH}/dSYMs/${FRAMEWORK_NAME}.framework.dSYM" \ + -framework "./${IOS_DEVICE_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -debug-symbols "${PWD}/${IOS_DEVICE_ARCHIVE_PATH}/dSYMs/${FRAMEWORK_NAME}.framework.dSYM" \ + -output "./${BUILD_FOLDER}/${FRAMEWORK_NAME}.xcframework" + +cp LICENSE ${BUILD_FOLDER} + +cd "./${BUILD_FOLDER}" + +zip -r "${FRAMEWORK_NAME}.zip" "${FRAMEWORK_NAME}.xcframework" LICENSE \ No newline at end of file diff --git a/scripts/bump_versions.rb b/scripts/bump_versions.rb new file mode 100644 index 0000000..6d5a1db --- /dev/null +++ b/scripts/bump_versions.rb @@ -0,0 +1,78 @@ +# bump_version.rb + +require 'bundler' + +# Read version level from ARGV +level = ARGV[0] + +# Define the path to your .podspec file +podspec_path = "./OSGeolocationLib.podspec" + +# Read the .podspec file +podspec_content = File.read(podspec_path) + +# Extract current version +current_version_number = podspec_content.match(/spec.version\s*=\s*["'](\d+\.\d+\.\d+)["']/)[1] + +# Parse the version into major, minor, and patch components +major, minor, patch = current_version_number.split('.').map(&:to_i) + +# Increment the version based on the specified level +case level +when "major" + major += 1 + # Reset minor and patch to 0 when major version is incremented + minor = 0 + patch = 0 +when "minor" + minor += 1 + # Reset patch to 0 when minor version is incremented + patch = 0 +when "patch" + patch += 1 +else + raise ArgumentError, "Invalid version bump level: #{level}. Must be one of: major, minor, patch." +end + +# Combine the new version components +new_version_number = [major, minor, patch].join('.') + +# Replace 'Unreleased' in the CHANGELOG.md with the new version +changelog_path = "./CHANGELOG.md" +changelog_content = File.read(changelog_path) +new_changelog_content = changelog_content.gsub("[Unreleased]", new_version_number) +File.write(changelog_path, new_changelog_content) + +# Replace the old version with the new version in the .podspec content +new_podspec_content = podspec_content.gsub(/(spec.version\s*=\s*["'])\d+\.\d+\.\d+(["'])/, "\\1#{new_version_number}\\2") +File.write(podspec_path, new_podspec_content) + +# Set the application name +LIBRARY_NAME = "OSGeolocationLib" + +# Set the Xcode project file path +project_file = "#{LIBRARY_NAME}.xcodeproj/project.pbxproj" + +# Read the project file content +file_content = File.read(project_file) + +# Fetch the current MARKETING_VERSION and CURRENT_PROJECT_VERSION values +current_build_number = Integer(file_content[/CURRENT_PROJECT_VERSION = ([^;]+)/, 1]) + +# Set the new build numbers +new_build_number = current_build_number + 1 + +# Update the MARKETING_VERSION and CURRENT_PROJECT_VERSION values in the project file +updated_content = file_content.gsub(/MARKETING_VERSION = [^;]+;/, "MARKETING_VERSION = #{new_version_number};") + .gsub(/CURRENT_PROJECT_VERSION = [^;]+;/, "CURRENT_PROJECT_VERSION = #{new_build_number};") + +# Write the updated content back to the project file +File.open(project_file, "w") { |file| file.puts updated_content } + +readme_path = "./README.md" +readme_content = File.read(readme_path) +new_readme_content = readme_content.gsub(/(pod 'OSGeolocationLib', '~> )\d+\.\d+\.\d+/, "\\1#{new_version_number}\\2") + .gsub(/(# Use the latest )\d+\.\d+/, "\\1#{[major, minor].join('.')}\\2") +File.write(readme_path, new_readme_content) + +puts "Version updated to #{new_version_number} (Build Number ##{new_build_number})" \ No newline at end of file diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh new file mode 100644 index 0000000..35e03d6 --- /dev/null +++ b/scripts/extract_release_notes.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Check if a section header is provided +if [ -z "$1" ]; then + echo "No section header provided. Usage: ./extract_release_notes.sh section_header" + exit 1 +fi + +SECTION_HEADER=$1 + +# Escape the section header for use in awk +ESCAPED_HEADER=$(echo "$SECTION_HEADER" | sed 's/[]\/$*.^|[]/\\&/g') + +# Extract the specified section from CHANGELOG.md, remove the empty line after the header, and convert ### to # +awk -v header="$ESCAPED_HEADER" ' + $0 ~ "## " header {flag=1; next} + flag && /^$/ {next} + /^## / && !($0 ~ "## " header) {flag=0} + flag {gsub(/^### /, "# "); print} +' CHANGELOG.md