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.
+
+[](https://cocoapods.org/pods/OSGeolocationLib)
+[](https://cocoapods.org/pods/OSGeolocationLib)
+[](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