├── Images └── INTULocationManager.png ├── LocationManager ├── LocationManagerExample │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── Images.xcassets │ │ ├── LaunchImage.launchimage │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── INTUAppDelegate.h │ ├── main.m │ ├── INTUViewController.h │ ├── Base.lproj │ │ ├── Launch Screen.storyboard │ │ └── Main.storyboard │ ├── LocationManagerExample-Info.plist │ ├── INTUAppDelegate.m │ └── INTUViewController.m ├── LocationManagerTests │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── LocationManagerTests-Info.plist │ ├── INTUHeadingRequestTests.m │ ├── INTURequestIDGeneratorTests.m │ ├── INTULocationRequestTests.m │ └── INTULocationManagerTests.m ├── LocationManager.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ ├── LocationManagerExample.xcscheme │ │ └── INTULocationManager.xcscheme ├── Podfile ├── LocationManager.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile.lock ├── LocationManagerFramework │ └── Info.plist └── INTULocationManager │ ├── INTULocationManager+Internal.h │ ├── INTURequestIDGenerator.m │ ├── INTURequestIDGenerator.h │ ├── INTUHeadingRequest.h │ ├── INTUHeadingRequest.m │ ├── INTULocationRequest.h │ ├── INTULocationRequest.m │ ├── INTULocationRequestDefines.h │ └── INTULocationManager.h ├── .slather.yml ├── .gitignore ├── .travis.yml ├── os-project-logo.svg ├── LICENSE ├── INTULocationManager.podspec └── README.md /Images/INTULocationManager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/LocationManager/HEAD/Images/INTULocationManager.png -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | ci_service: travis_ci 2 | coverage_service: coveralls 3 | xcodeproj: LocationManager/LocationManager.xcodeproj 4 | source_directory: LocationManager/INTULocationManager 5 | -------------------------------------------------------------------------------- /LocationManager/LocationManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LocationManager/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | target 'LocationManagerExample' do 5 | 6 | end 7 | 8 | target 'LocationManagerTests' do 9 | pod 'Specta' 10 | pod 'Expecta' 11 | pod 'OCMock' 12 | end 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | -------------------------------------------------------------------------------- /LocationManager/LocationManager.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LocationManager/LocationManager.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LocationManager/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Expecta (1.0.6) 3 | - OCMock (3.4) 4 | - Specta (1.0.6) 5 | 6 | DEPENDENCIES: 7 | - Expecta 8 | - OCMock 9 | - Specta 10 | 11 | SPEC REPOS: 12 | https://github.com/cocoapods/specs.git: 13 | - Expecta 14 | - OCMock 15 | - Specta 16 | 17 | SPEC CHECKSUMS: 18 | Expecta: 3b6bd90a64b9a1dcb0b70aa0e10a7f8f631667d5 19 | OCMock: 35ae71d6a8fcc1b59434d561d1520b9dd4f15765 20 | Specta: f506f3a8361de16bc0dcf3b17b75e269072ba465 21 | 22 | PODFILE CHECKSUM: d344db7d2ea9b8ce07105bbe9f21f10f670336e2 23 | 24 | COCOAPODS: 1.5.3 25 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: objective-c 3 | os: osx 4 | cache: cocoapods 5 | xcode_workspace: LocationManager/LocationManager.xcworkspace 6 | xcode_scheme: LocationManagerExample 7 | xcode_sdk: iphonesimulator12.0 8 | osx_image: xcode10 9 | podfile: LocationManager/Podfile 10 | 11 | before_install: 12 | # - gem install cocoapods --no-ri --no-rdoc 13 | - gem install slather --no-document 14 | - gem install cocoapods 15 | - pod repo update 16 | 17 | script: set -o pipefail && xcodebuild test -workspace LocationManager/LocationManager.xcworkspace -scheme LocationManagerExample -destination 'platform=iOS Simulator,name=iPhone 8 Plus,OS=12.0' 18 | 19 | after_success: 20 | slather 21 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerTests/LocationManagerTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /os-project-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerFramework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.3.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Intuit Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/INTUAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTUAppDelegate.h 3 | // LocationManagerExample 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | @interface INTUAppDelegate : UIResponder 30 | 31 | @property (strong, nonatomic) UIWindow *window; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTULocationManager+Internal.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationManager+Internal.h 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTULocationManager.h" 27 | 28 | /** 29 | A category that exposes the internal (private) methods of INTULocationManager. 30 | */ 31 | @interface INTULocationManager (Internal) 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // LocationManagerExample 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | #import "INTUAppDelegate.h" 30 | 31 | int main(int argc, char * argv[]) 32 | { 33 | @autoreleasepool { 34 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([INTUAppDelegate class])); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/INTUViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTUViewController.h 3 | // LocationManagerExample 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | @interface INTUViewController : UIViewController 30 | 31 | /** 32 | Starts a new subscription for significant location changes. 33 | */ 34 | - (void)startMonitoringSignificantLocationChanges; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTURequestIDGenerator.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTURequestIDGenerator.m 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | #import "INTURequestIDGenerator.h" 26 | 27 | @implementation INTURequestIDGenerator 28 | 29 | static INTULocationRequestID _nextRequestID = 0; 30 | 31 | +(INTULocationRequestID)getUniqueRequestID 32 | { 33 | _nextRequestID++; 34 | return _nextRequestID; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTURequestIDGenerator.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTURequestIDGenerator.h 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTULocationRequestDefines.h" 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | @interface INTURequestIDGenerator : NSObject 31 | 32 | /** 33 | Returns a unique request ID (within the lifetime of the application). 34 | */ 35 | +(INTULocationRequestID)getUniqueRequestID; 36 | 37 | @end 38 | 39 | NS_ASSUME_NONNULL_END 40 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTUHeadingRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTUHeadingRequest.h 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTULocationRequestDefines.h" 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | @interface INTUHeadingRequest : NSObject 31 | 32 | /** The request ID for this heading request (set during initialization). */ 33 | @property (nonatomic, readonly) INTUHeadingRequestID requestID; 34 | /** Whether this is a recurring heading request (all heading requests are assumed to be for now). */ 35 | @property (nonatomic, readonly) BOOL isRecurring; 36 | /** The block to execute when the heading request completes. */ 37 | @property (nonatomic, copy, nullable) INTUHeadingRequestBlock block; 38 | 39 | @end 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/Base.lproj/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerTests/INTUHeadingRequestTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTUHaedingRequestTests.m 3 | // LocationManagerTests 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | #import 29 | #import 30 | 31 | #import "INTUHeadingRequest.h" 32 | 33 | SpecBegin(HeadingRequest) 34 | 35 | describe(@"INTUHeadingRequest", ^{ 36 | __block INTUHeadingRequest *request; 37 | 38 | before(^{ 39 | request = [[INTUHeadingRequest alloc] init]; 40 | }); 41 | 42 | it(@"generates a unique request id for each request", ^{ 43 | INTUHeadingRequest *request1 = [[INTUHeadingRequest alloc] init]; 44 | INTUHeadingRequest *request2 = [[INTUHeadingRequest alloc] init]; 45 | expect(request1.requestID).notTo.equal(request2.requestID); 46 | }); 47 | 48 | describe(@"is a subscription", ^{ 49 | it(@"should be recurring", ^{ 50 | expect(request.isRecurring).to.beTruthy(); 51 | }); 52 | }); 53 | 54 | }); 55 | 56 | 57 | SpecEnd 58 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTUHeadingRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTUHeadingRequest.m 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTUHeadingRequest.h" 27 | #import "INTURequestIDGenerator.h" 28 | 29 | @implementation INTUHeadingRequest 30 | 31 | /** 32 | Designated initializer. Initializes and returns a newly allocated heading request. 33 | */ 34 | - (instancetype)init 35 | { 36 | if (self = [super init]) { 37 | _requestID = [INTURequestIDGenerator getUniqueRequestID]; 38 | _isRecurring = YES; 39 | } 40 | return self; 41 | } 42 | 43 | /** 44 | Two heading requests are considered equal if their request IDs match. 45 | */ 46 | - (BOOL)isEqual:(id)object 47 | { 48 | if (object == self) { 49 | return YES; 50 | } 51 | if (!object || ![object isKindOfClass:[self class]]) { 52 | return NO; 53 | } 54 | if (((INTUHeadingRequest *)object).requestID == self.requestID) { 55 | return YES; 56 | } 57 | return NO; 58 | } 59 | 60 | /** 61 | Return a hash based on the string representation of the request ID. 62 | */ 63 | - (NSUInteger)hash 64 | { 65 | return [[NSString stringWithFormat:@"%ld", (long)self.requestID] hash]; 66 | } 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/LocationManagerExample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 2.0 25 | LSRequiresIPhoneOS 26 | 27 | NSLocationAlwaysUsageDescription 28 | This string is required to gain permission to access location services on iOS 8+ when the app in the background and should describe how your app uses location services. Set this string in the Info.plist 29 | NSLocationUsageDescription 30 | This string is optional but recommended on iOS 6 & 7 and should describe how your app uses location services. Set this string in the Info.plist 31 | NSLocationWhenInUseUsageDescription 32 | This string is required to gain permission to access location services on iOS 8+ and should describe how your app uses location services. Set this string in the Info.plist 33 | NSLocationAlwaysAndWhenInUseUsageDescription 34 | This string is required to gain permission to access location services on iOS 11+ and should describe how your app uses location services. Set this string in the Info.plist 35 | UIBackgroundModes 36 | 37 | location 38 | 39 | UILaunchStoryboardName 40 | Launch Screen 41 | UIMainStoryboardFile 42 | Main 43 | UIRequiredDeviceCapabilities 44 | 45 | armv7 46 | 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerTests/INTURequestIDGeneratorTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTURequestIDGeneratorTests.m 3 | // LocationManagerTests 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | #import 29 | #import 30 | 31 | #import "INTUHeadingRequest.h" 32 | #import "INTULocationRequest.h" 33 | 34 | SpecBegin(RequestIDGenerator) 35 | 36 | describe(@"RequestIDGenerator", ^{ 37 | __block INTUHeadingRequest *headingRequest; 38 | __block INTULocationRequest *locationRequest; 39 | 40 | before(^{ 41 | headingRequest = [[INTUHeadingRequest alloc] init]; 42 | locationRequest = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSingle]; 43 | }); 44 | 45 | it(@"generates a unique request id for each request regardless of type (no dupes)", ^{ 46 | expect(headingRequest.requestID).notTo.equal(locationRequest.requestID); 47 | }); 48 | 49 | describe(@"heading request is a subscription", ^{ 50 | it(@"should be recurring", ^{ 51 | expect(headingRequest.isRecurring).to.beTruthy(); 52 | }); 53 | }); 54 | 55 | describe(@"location request is a subscription", ^{ 56 | context(@"when the type is INTULocationRequestTypeSingle", ^{ 57 | it(@"should not be recurring", ^{ 58 | expect(locationRequest.isRecurring).to.beFalsy(); 59 | }); 60 | }); 61 | }); 62 | }); 63 | 64 | SpecEnd 65 | -------------------------------------------------------------------------------- /INTULocationManager.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "INTULocationManager" 3 | s.version = "4.4.0" 4 | s.homepage = "https://github.com/intuit/LocationManager" 5 | s.license = 'MIT' 6 | s.author = { "Lucien Dupont" => "lucien@chromedomesoftware.com" } 7 | s.source = { :git => "https://github.com/intuit/LocationManager.git", :tag => "v4.4.0" } 8 | s.source_files = 'LocationManager/INTULocationManager' 9 | s.platform = :ios 10 | s.ios.deployment_target = '12.0' 11 | s.requires_arc = true 12 | s.summary = "Easily get the device's current location on iOS." 13 | s.description = <<-DESC 14 | # INTULocationManager 15 | INTULocationManager makes it easy to get the device's current location on iOS. It is an Objective-C library that also works great in Swift. 16 | 17 | INTULocationManager provides a block-based asynchronous API to request the current location, either once or continuously. It internally manages multiple simultaneous location requests, and each one-time request can specify its own desired accuracy level and timeout duration. INTULocationManager automatically starts location services when the first request comes in and stops location services as soon as all requests have been completed, all the while dynamically managing the power consumed by location services to reduce impact on battery life. 18 | 19 | ## What's wrong with CLLocationManager? 20 | CLLocationManager requires you to manually detect and handle things like permissions, stale/inaccurate locations, errors, and more. CLLocationManager uses a more traditional delegate pattern instead of the modern block-based callback pattern. And while it works fine to track changes in the user's location over time (such as for turn-by-turn navigation), it is extremely cumbersome to correctly request a single location update (such as to determine the user's current city to get a weather forecast, or to autofill an address from the current location). 21 | 22 | INTULocationManager makes it easy to request the device's current location, either once or continuously. The API is extremely simple for both one-time location requests and recurring subscriptions to location updates. For one-time location requests, you can specify how accurate of a location you need, and how long you're willing to wait to get it. Significant location change monitoring is also supported. INTULocationManager is power efficient and conserves the device's battery by automatically determining and using the most efficient Core Location accuracy settings, and by automatically powering down location services (e.g. GPS) as soon as they are no longer needed. 23 | DESC 24 | end 25 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/INTUAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTUAppDelegate.m 3 | // LocationManagerExample 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import "INTUAppDelegate.h" 28 | #import "INTUViewController.h" 29 | 30 | @implementation INTUAppDelegate 31 | 32 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 33 | { 34 | // If you start monitoring significant location changes and your app is subsequently terminated, the system automatically relaunches the app into the background if a new event arrives. 35 | // Upon relaunch, you must still subscribe to significant location changes to continue receiving location events. 36 | if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) { 37 | INTUViewController *rootViewController = (INTUViewController *)self.window.rootViewController; 38 | [rootViewController startMonitoringSignificantLocationChanges]; 39 | } 40 | return YES; 41 | } 42 | 43 | - (void)applicationWillResignActive:(UIApplication *)application 44 | { 45 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 46 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 47 | } 48 | 49 | - (void)applicationDidEnterBackground:(UIApplication *)application 50 | { 51 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 52 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 53 | } 54 | 55 | - (void)applicationWillEnterForeground:(UIApplication *)application 56 | { 57 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 58 | } 59 | 60 | - (void)applicationDidBecomeActive:(UIApplication *)application 61 | { 62 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 63 | } 64 | 65 | - (void)applicationWillTerminate:(UIApplication *)application 66 | { 67 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /LocationManager/LocationManager.xcodeproj/xcshareddata/xcschemes/LocationManagerExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTULocationRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationRequest.h 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTULocationRequestDefines.h" 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | /** The available types of location requests. */ 31 | typedef NS_ENUM(NSInteger, INTULocationRequestType) { 32 | /** A one-time location request with a specific desired accuracy and optional timeout. */ 33 | INTULocationRequestTypeSingle, 34 | /** A subscription to location updates. */ 35 | INTULocationRequestTypeSubscription, 36 | /** A subscription to significant location changes. */ 37 | INTULocationRequestTypeSignificantChanges 38 | }; 39 | 40 | @class INTULocationRequest; 41 | 42 | /** 43 | Protocol for the INTULocationRequest to notify the its delegate that a request has timed out. 44 | */ 45 | @protocol INTULocationRequestDelegate 46 | 47 | /** 48 | Notification that a location request has timed out. 49 | 50 | @param locationRequest The location request that timed out. 51 | */ 52 | - (void)locationRequestDidTimeout:(INTULocationRequest *)locationRequest; 53 | 54 | @end 55 | 56 | 57 | /** 58 | Represents a geolocation request that is created and managed by INTULocationManager. 59 | */ 60 | @interface INTULocationRequest : NSObject 61 | 62 | /** The delegate for this location request. */ 63 | @property (nonatomic, weak, nullable) id delegate; 64 | /** The request ID for this location request (set during initialization). */ 65 | @property (nonatomic, readonly) INTULocationRequestID requestID; 66 | /** The type of this location request (set during initialization). */ 67 | @property (nonatomic, readonly) INTULocationRequestType type; 68 | /** Whether this is a recurring location request (type is either Subscription or SignificantChanges). */ 69 | @property (nonatomic, readonly) BOOL isRecurring; 70 | /** The desired accuracy for this location request. */ 71 | @property (nonatomic, assign) INTULocationAccuracy desiredAccuracy; 72 | /** The desired activity type for this location request. */ 73 | @property (nonatomic, assign) CLActivityType desiredActivityType; 74 | /** The maximum amount of time the location request should be allowed to live before completing. 75 | If this value is exactly 0.0, it will be ignored (the request will never timeout by itself). */ 76 | @property (nonatomic, assign) NSTimeInterval timeout; 77 | /** How long the location request has been alive since the timeout value was last set. */ 78 | @property (nonatomic, readonly) NSTimeInterval timeAlive; 79 | /** Whether this location request has timed out (will also be YES if it has been completed). Subcriptions can never time out. */ 80 | @property (nonatomic, readonly) BOOL hasTimedOut; 81 | /** The block to execute when the location request completes. */ 82 | @property (nonatomic, copy, nullable) INTULocationRequestBlock block; 83 | 84 | /** Designated initializer. Initializes and returns a newly allocated location request object with the specified type. */ 85 | - (instancetype)initWithType:(INTULocationRequestType)type __INTU_DESIGNATED_INITIALIZER; 86 | 87 | /** Completes the location request. */ 88 | - (void)complete; 89 | /** Forces the location request to consider itself timed out. */ 90 | - (void)forceTimeout; 91 | /** Cancels the location request. */ 92 | - (void)cancel; 93 | 94 | /** Starts the location request's timeout timer if a nonzero timeout value is set, and the timer has not already been started. */ 95 | - (void)startTimeoutTimerIfNeeded; 96 | 97 | /** Returns the associated recency threshold (in seconds) for the location request's desired accuracy level. */ 98 | - (NSTimeInterval)updateTimeStaleThreshold; 99 | 100 | /** Returns the associated horizontal accuracy threshold (in meters) for the location request's desired accuracy level. */ 101 | - (CLLocationAccuracy)horizontalAccuracyThreshold; 102 | 103 | @end 104 | 105 | NS_ASSUME_NONNULL_END 106 | -------------------------------------------------------------------------------- /LocationManager/LocationManager.xcodeproj/xcshareddata/xcschemes/INTULocationManager.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 87 | 88 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 109 | 110 | 116 | 117 | 118 | 119 | 121 | 122 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTULocationRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationRequest.m 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTULocationRequest.h" 27 | #import "INTURequestIDGenerator.h" 28 | 29 | @interface INTULocationRequest () 30 | 31 | // Redeclare this property as readwrite for internal use. 32 | @property (nonatomic, assign, readwrite) BOOL hasTimedOut; 33 | 34 | /** The NSDate representing the time when the request started. Set when the |timeout| property is set. */ 35 | @property (nonatomic, strong) NSDate *requestStartTime; 36 | /** The timer that will fire to notify this request that it has timed out. Started when the |timeout| property is set. */ 37 | @property (nonatomic, strong) NSTimer *timeoutTimer; 38 | 39 | @end 40 | 41 | 42 | @implementation INTULocationRequest 43 | 44 | /** 45 | Throws an exeption when you try to create a location request using a non-designated initializer. 46 | */ 47 | - (instancetype)init 48 | { 49 | @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithType: instead." userInfo:nil]; 50 | return [self initWithType:INTULocationRequestTypeSingle]; 51 | } 52 | 53 | /** 54 | Designated initializer. Initializes and returns a newly allocated location request object with the specified type. 55 | 56 | @param type The type of the location request. 57 | */ 58 | - (instancetype)initWithType:(INTULocationRequestType)type 59 | { 60 | self = [super init]; 61 | if (self) { 62 | _requestID = [INTURequestIDGenerator getUniqueRequestID]; 63 | _type = type; 64 | _hasTimedOut = NO; 65 | } 66 | return self; 67 | } 68 | 69 | /** 70 | Returns the associated recency threshold (in seconds) for the location request's desired accuracy level. 71 | */ 72 | - (NSTimeInterval)updateTimeStaleThreshold 73 | { 74 | switch (self.desiredAccuracy) { 75 | case INTULocationAccuracyRoom: 76 | return kINTUUpdateTimeStaleThresholdRoom; 77 | break; 78 | case INTULocationAccuracyHouse: 79 | return kINTUUpdateTimeStaleThresholdHouse; 80 | break; 81 | case INTULocationAccuracyBlock: 82 | return kINTUUpdateTimeStaleThresholdBlock; 83 | break; 84 | case INTULocationAccuracyNeighborhood: 85 | return kINTUUpdateTimeStaleThresholdNeighborhood; 86 | break; 87 | case INTULocationAccuracyCity: 88 | return kINTUUpdateTimeStaleThresholdCity; 89 | break; 90 | default: 91 | NSAssert(NO, @"Unknown desired accuracy."); 92 | return 0.0; 93 | break; 94 | } 95 | } 96 | 97 | /** 98 | Returns the associated horizontal accuracy threshold (in meters) for the location request's desired accuracy level. 99 | */ 100 | - (CLLocationAccuracy)horizontalAccuracyThreshold 101 | { 102 | switch (self.desiredAccuracy) { 103 | case INTULocationAccuracyRoom: 104 | return kINTUHorizontalAccuracyThresholdRoom; 105 | break; 106 | case INTULocationAccuracyHouse: 107 | return kINTUHorizontalAccuracyThresholdHouse; 108 | break; 109 | case INTULocationAccuracyBlock: 110 | return kINTUHorizontalAccuracyThresholdBlock; 111 | break; 112 | case INTULocationAccuracyNeighborhood: 113 | return kINTUHorizontalAccuracyThresholdNeighborhood; 114 | break; 115 | case INTULocationAccuracyCity: 116 | return kINTUHorizontalAccuracyThresholdCity; 117 | break; 118 | default: 119 | NSAssert(NO, @"Unknown desired accuracy."); 120 | return 0.0; 121 | break; 122 | } 123 | } 124 | 125 | /** 126 | Completes the location request. 127 | */ 128 | - (void)complete 129 | { 130 | [self.timeoutTimer invalidate]; 131 | self.timeoutTimer = nil; 132 | self.requestStartTime = nil; 133 | } 134 | 135 | /** 136 | Forces the location request to consider itself timed out. 137 | */ 138 | - (void)forceTimeout 139 | { 140 | if (self.isRecurring == NO) { 141 | self.hasTimedOut = YES; 142 | } else { 143 | NSAssert(self.isRecurring == NO, @"Only single location requests (not recurring requests) should ever be considered timed out."); 144 | } 145 | } 146 | 147 | /** 148 | Cancels the location request. 149 | */ 150 | - (void)cancel 151 | { 152 | [self.timeoutTimer invalidate]; 153 | self.timeoutTimer = nil; 154 | self.requestStartTime = nil; 155 | } 156 | 157 | /** 158 | Starts the location request's timeout timer if a nonzero timeout value is set, and the timer has not already been started. 159 | */ 160 | - (void)startTimeoutTimerIfNeeded 161 | { 162 | if (self.timeout > 0 && !self.timeoutTimer) { 163 | self.requestStartTime = [NSDate date]; 164 | self.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeout target:self selector:@selector(timeoutTimerFired:) userInfo:nil repeats:NO]; 165 | } 166 | } 167 | 168 | /** 169 | Computed property that returns whether this is a subscription request. 170 | */ 171 | - (BOOL)isRecurring 172 | { 173 | return (self.type == INTULocationRequestTypeSubscription) || (self.type == INTULocationRequestTypeSignificantChanges); 174 | } 175 | 176 | /** 177 | Computed property that returns how long the request has been alive (since the timeout value was set). 178 | */ 179 | - (NSTimeInterval)timeAlive 180 | { 181 | if (self.requestStartTime == nil) { 182 | return 0.0; 183 | } 184 | return fabs([self.requestStartTime timeIntervalSinceNow]); 185 | } 186 | 187 | /** 188 | Returns whether the location request has timed out or not. 189 | Once this becomes YES, it will not automatically reset to NO even if a new timeout value is set. 190 | */ 191 | - (BOOL)hasTimedOut 192 | { 193 | if (self.timeout > 0.0 && self.timeAlive > self.timeout) { 194 | _hasTimedOut = YES; 195 | } 196 | return _hasTimedOut; 197 | } 198 | 199 | /** 200 | Callback when the timeout timer fires. Notifies the delegate that this event has occurred. 201 | */ 202 | - (void)timeoutTimerFired:(NSTimer *)timer 203 | { 204 | self.hasTimedOut = YES; 205 | [self.delegate locationRequestDidTimeout:self]; 206 | } 207 | 208 | /** 209 | Two location requests are considered equal if their request IDs match. 210 | */ 211 | - (BOOL)isEqual:(id)object 212 | { 213 | if (object == self) { 214 | return YES; 215 | } 216 | if (!object || ![object isKindOfClass:[self class]]) { 217 | return NO; 218 | } 219 | if (((INTULocationRequest *)object).requestID == self.requestID) { 220 | return YES; 221 | } 222 | return NO; 223 | } 224 | 225 | /** 226 | Return a hash based on the string representation of the request ID. 227 | */ 228 | - (NSUInteger)hash 229 | { 230 | return [[NSString stringWithFormat:@"%ld", (long)self.requestID] hash]; 231 | } 232 | 233 | - (void)dealloc 234 | { 235 | [_timeoutTimer invalidate]; 236 | } 237 | 238 | @end 239 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerTests/INTULocationRequestTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationRequestTests.m 3 | // LocationManagerTests 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | #import 29 | #import 30 | 31 | #import "INTULocationRequest.h" 32 | 33 | SpecBegin(LocationRequest) 34 | 35 | describe(@"INTULocationRequest", ^{ 36 | __block INTULocationRequest *request; 37 | 38 | before(^{ 39 | request = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSingle]; 40 | }); 41 | 42 | it(@"generates a unique request id for each request", ^{ 43 | INTULocationRequest *request1 = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSingle]; 44 | INTULocationRequest *request2 = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSingle]; 45 | expect(request1.requestID).notTo.equal(request2.requestID); 46 | 47 | INTULocationRequest *request3 = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSubscription]; 48 | expect(request2.requestID).notTo.equal(request3.requestID); 49 | 50 | INTULocationRequest *request4 = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSignificantChanges]; 51 | expect(request3.requestID).notTo.equal(request4.requestID); 52 | }); 53 | 54 | describe(@"is a subscription", ^{ 55 | context(@"when the type is INTULocationRequestTypeSingle", ^{ 56 | it(@"should not be recurring", ^{ 57 | expect(request.isRecurring).to.beFalsy(); 58 | }); 59 | }); 60 | 61 | context(@"when the type is INTULocationRequestTypeSubscription or INTULocationRequestTypeSignificantChanges", ^{ 62 | it(@"should be recurring", ^{ 63 | INTULocationRequest *locationRequestWithSubscriptionForAllChangesType = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSubscription]; 64 | expect(locationRequestWithSubscriptionForAllChangesType.isRecurring).to.beTruthy(); 65 | 66 | INTULocationRequest *locationRequestWithSubscriptionForSignificantChangesType = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSignificantChanges]; 67 | expect(locationRequestWithSubscriptionForSignificantChangesType.isRecurring).to.beTruthy(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe(@"setting accuracy", ^{ 73 | it(@"should return the appropriate horizontal accuracy", ^{ 74 | request.desiredAccuracy = INTULocationAccuracyCity; 75 | expect(request.horizontalAccuracyThreshold).to.equal(kINTUHorizontalAccuracyThresholdCity); 76 | request.desiredAccuracy = INTULocationAccuracyNeighborhood; 77 | expect(request.horizontalAccuracyThreshold).to.equal(kINTUHorizontalAccuracyThresholdNeighborhood); 78 | request.desiredAccuracy = INTULocationAccuracyBlock; 79 | expect(request.horizontalAccuracyThreshold).to.equal(kINTUHorizontalAccuracyThresholdBlock); 80 | request.desiredAccuracy = INTULocationAccuracyHouse; 81 | expect(request.horizontalAccuracyThreshold).to.equal(kINTUHorizontalAccuracyThresholdHouse); 82 | request.desiredAccuracy = INTULocationAccuracyRoom; 83 | expect(request.horizontalAccuracyThreshold).to.equal(kINTUHorizontalAccuracyThresholdRoom); 84 | }); 85 | 86 | it(@"should return the appropriate recency threshold", ^{ 87 | request.desiredAccuracy = INTULocationAccuracyCity; 88 | expect(request.updateTimeStaleThreshold).to.equal(kINTUUpdateTimeStaleThresholdCity); 89 | request.desiredAccuracy = INTULocationAccuracyNeighborhood; 90 | expect(request.updateTimeStaleThreshold).to.equal(kINTUUpdateTimeStaleThresholdNeighborhood); 91 | request.desiredAccuracy = INTULocationAccuracyBlock; 92 | expect(request.updateTimeStaleThreshold).to.equal(kINTUUpdateTimeStaleThresholdBlock); 93 | request.desiredAccuracy = INTULocationAccuracyHouse; 94 | expect(request.updateTimeStaleThreshold).to.equal(kINTUUpdateTimeStaleThresholdHouse); 95 | request.desiredAccuracy = INTULocationAccuracyRoom; 96 | expect(request.updateTimeStaleThreshold).to.equal(kINTUUpdateTimeStaleThresholdRoom); 97 | }); 98 | }); 99 | 100 | describe(@"timing out a request", ^{ 101 | context(@"when the desired accuracy is not none", ^{ 102 | before(^{ 103 | request.desiredAccuracy = INTULocationAccuracyRoom; 104 | }); 105 | 106 | it(@"ignores a timeout if its 0", ^{ 107 | request.timeout = 0; 108 | [request startTimeoutTimerIfNeeded]; 109 | expect(request.timeAlive).to.equal(0); 110 | }); 111 | 112 | it(@"can force a timeout", ^{ 113 | request.timeout = 10; 114 | [request startTimeoutTimerIfNeeded]; 115 | expect(request.hasTimedOut).to.beFalsy(); 116 | 117 | [request forceTimeout]; 118 | 119 | expect(request.hasTimedOut).to.beTruthy(); 120 | }); 121 | 122 | describe(@"timed out", ^{ 123 | it(@"say its timed out", ^{ 124 | request.timeout = 0.001; 125 | [request startTimeoutTimerIfNeeded]; 126 | expect(request.hasTimedOut).will.beTruthy(); 127 | }); 128 | 129 | it(@"notifies the delegate it has timed out", ^{ 130 | id protocolMock = OCMProtocolMock(@protocol(INTULocationRequestDelegate)); 131 | request.delegate = protocolMock; 132 | 133 | OCMExpect([protocolMock locationRequestDidTimeout:[OCMArg any]]); 134 | 135 | request.timeout = 0.001; 136 | [request startTimeoutTimerIfNeeded]; 137 | 138 | // This will block the timeout 139 | expect(request.hasTimedOut).will.beTruthy(); 140 | OCMVerifyAll(protocolMock); 141 | }); 142 | 143 | it(@"does not say timed out before the specified timeout", ^{ 144 | request.timeout = 10.001; 145 | [request startTimeoutTimerIfNeeded]; 146 | expect(request.hasTimedOut).to.beFalsy(); 147 | 148 | // cleanup 149 | [request forceTimeout]; 150 | }); 151 | }); 152 | }); 153 | }); 154 | 155 | describe(@"cancelling a request", ^{ 156 | it(@"should have a zero time alive", ^{ 157 | request.timeout = 10; 158 | [request startTimeoutTimerIfNeeded]; 159 | 160 | expect(request.timeAlive).to.beGreaterThan(0); 161 | [request cancel]; 162 | expect(request.timeAlive).to.equal(0); 163 | }); 164 | }); 165 | 166 | describe(@"completing a request", ^{ 167 | it(@"should have a zero alive time", ^{ 168 | request.timeout = 10; 169 | [request startTimeoutTimerIfNeeded]; 170 | 171 | expect(request.timeAlive).to.beGreaterThan(0); 172 | [request complete]; 173 | expect(request.timeAlive).to.equal(0); 174 | }); 175 | }); 176 | }); 177 | 178 | 179 | SpecEnd 180 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTULocationRequestDefines.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationRequestDefines.h 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #ifndef INTU_LOCATION_REQUEST_DEFINES_H 27 | #define INTU_LOCATION_REQUEST_DEFINES_H 28 | 29 | #import 30 | #import 31 | 32 | #if __has_feature(objc_generics) 33 | # define __INTU_GENERICS(type, ...) type<__VA_ARGS__> 34 | #else 35 | # define __INTU_GENERICS(type, ...) type 36 | #endif 37 | 38 | #ifdef NS_DESIGNATED_INITIALIZER 39 | # define __INTU_DESIGNATED_INITIALIZER NS_DESIGNATED_INITIALIZER 40 | #else 41 | # define __INTU_DESIGNATED_INITIALIZER 42 | #endif 43 | 44 | static const CLLocationAccuracy kINTUHorizontalAccuracyThresholdCity = 5000.0; // in meters 45 | static const CLLocationAccuracy kINTUHorizontalAccuracyThresholdNeighborhood = 1000.0; // in meters 46 | static const CLLocationAccuracy kINTUHorizontalAccuracyThresholdBlock = 100.0; // in meters 47 | static const CLLocationAccuracy kINTUHorizontalAccuracyThresholdHouse = 15.0; // in meters 48 | static const CLLocationAccuracy kINTUHorizontalAccuracyThresholdRoom = 5.0; // in meters 49 | 50 | static const NSTimeInterval kINTUUpdateTimeStaleThresholdCity = 600.0; // in seconds 51 | static const NSTimeInterval kINTUUpdateTimeStaleThresholdNeighborhood = 300.0; // in seconds 52 | static const NSTimeInterval kINTUUpdateTimeStaleThresholdBlock = 60.0; // in seconds 53 | static const NSTimeInterval kINTUUpdateTimeStaleThresholdHouse = 15.0; // in seconds 54 | static const NSTimeInterval kINTUUpdateTimeStaleThresholdRoom = 5.0; // in seconds 55 | 56 | /** The possible states that location services can be in. */ 57 | typedef NS_ENUM(NSInteger, INTULocationServicesState) { 58 | /** User has already granted this app permissions to access location services, and they are enabled and ready for use by this app. 59 | Note: this state will be returned for both the "When In Use" and "Always" permission levels. */ 60 | INTULocationServicesStateAvailable, 61 | /** User has not yet responded to the dialog that grants this app permission to access location services. */ 62 | INTULocationServicesStateNotDetermined, 63 | /** User has explicitly denied this app permission to access location services. (The user can enable permissions again for this app from the system Settings app.) */ 64 | INTULocationServicesStateDenied, 65 | /** User does not have ability to enable location services (e.g. parental controls, corporate policy, etc). */ 66 | INTULocationServicesStateRestricted, 67 | /** User has turned off location services device-wide (for all apps) from the system Settings app. */ 68 | INTULocationServicesStateDisabled 69 | }; 70 | 71 | /** The possible states that heading services can be in. */ 72 | typedef NS_ENUM(NSInteger, INTUHeadingServicesState) { 73 | /** Heading services are available on the device */ 74 | INTUHeadingServicesStateAvailable, 75 | /** Heading services are available on the device */ 76 | INTUHeadingServicesStateUnavailable, 77 | }; 78 | 79 | /** A unique ID that corresponds to one location request. */ 80 | typedef NSInteger INTULocationRequestID; 81 | 82 | /** A unique ID that corresponds to one heading request. */ 83 | typedef NSInteger INTUHeadingRequestID; 84 | 85 | /** An abstraction of both the horizontal accuracy and recency of location data. 86 | Room is the highest level of accuracy/recency; City is the lowest level. */ 87 | typedef NS_ENUM(NSInteger, INTULocationAccuracy) { 88 | // 'None' is not valid as a desired accuracy. 89 | /** Inaccurate (>5000 meters, and/or received >10 minutes ago). */ 90 | INTULocationAccuracyNone = 0, 91 | 92 | // The below options are valid desired accuracies. 93 | /** 5000 meters or better, and received within the last 10 minutes. Lowest accuracy. */ 94 | INTULocationAccuracyCity, 95 | /** 1000 meters or better, and received within the last 5 minutes. */ 96 | INTULocationAccuracyNeighborhood, 97 | /** 100 meters or better, and received within the last 1 minute. */ 98 | INTULocationAccuracyBlock, 99 | /** 15 meters or better, and received within the last 15 seconds. */ 100 | INTULocationAccuracyHouse, 101 | /** 5 meters or better, and received within the last 5 seconds. Highest accuracy. */ 102 | INTULocationAccuracyRoom, 103 | }; 104 | 105 | /** An alias of the heading filter accuracy in degrees. 106 | Specifies the minimum amount of change in degrees needed for a heading service update. Observers will not be notified of updates less than the stated filter value. */ 107 | typedef CLLocationDegrees INTUHeadingFilterAccuracy; 108 | 109 | /** A status that will be passed in to the completion block of a location request. */ 110 | typedef NS_ENUM(NSInteger, INTULocationStatus) { 111 | // These statuses will accompany a valid location. 112 | /** Got a location and desired accuracy level was achieved successfully. */ 113 | INTULocationStatusSuccess = 0, 114 | /** Got a location, but the desired accuracy level was not reached before timeout. (Not applicable to subscriptions.) */ 115 | INTULocationStatusTimedOut, 116 | 117 | // These statuses indicate some sort of error, and will accompany a nil location. 118 | /** User has not yet responded to the dialog that grants this app permission to access location services. */ 119 | INTULocationStatusServicesNotDetermined, 120 | /** User has explicitly denied this app permission to access location services. */ 121 | INTULocationStatusServicesDenied, 122 | /** User does not have ability to enable location services (e.g. parental controls, corporate policy, etc). */ 123 | INTULocationStatusServicesRestricted, 124 | /** User has turned off location services device-wide (for all apps) from the system Settings app. */ 125 | INTULocationStatusServicesDisabled, 126 | /** An error occurred while using the system location services. */ 127 | INTULocationStatusError 128 | }; 129 | 130 | /** A status that will be passed in to the completion block of a heading request. */ 131 | typedef NS_ENUM(NSInteger, INTUHeadingStatus) { 132 | // These statuses will accompany a valid heading. 133 | /** Got a heading successfully. */ 134 | INTUHeadingStatusSuccess = 0, 135 | 136 | // These statuses indicate some sort of error, and will accompany a nil heading. 137 | /** Heading was invalid. */ 138 | INTUHeadingStatusInvalid, 139 | 140 | /** Heading services are not available on the device */ 141 | INTUHeadingStatusUnavailable 142 | }; 143 | 144 | /** 145 | A block type for a location request, which is executed when the request succeeds, fails, or times out. 146 | 147 | @param currentLocation The most recent & accurate current location available when the block executes, or nil if no valid location is available. 148 | @param achievedAccuracy The accuracy level that was actually achieved (may be better than, equal to, or worse than the desired accuracy). 149 | @param status The status of the location request - whether it succeeded, timed out, or failed due to some sort of error. This can be used to 150 | understand what the outcome of the request was, decide if/how to use the associated currentLocation, and determine whether other 151 | actions are required (such as displaying an error message to the user, retrying with another request, quietly proceeding, etc). 152 | */ 153 | typedef void(^INTULocationRequestBlock)(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status); 154 | 155 | /** 156 | A block type for a heading request, which is executed when the request succeeds. 157 | 158 | @param currentHeading The most recent current heading available when the block executes. 159 | @param status The status of the request - whether it succeeded or failed due to some sort of error. This can be used to understand if any further action is needed. 160 | */ 161 | typedef void(^INTUHeadingRequestBlock)(CLHeading *currentHeading, INTUHeadingStatus status); 162 | 163 | typedef NS_ENUM(NSUInteger, INTUAuthorizationType) { 164 | INTUAuthorizationTypeAuto, 165 | INTUAuthorizationTypeAlways, 166 | INTUAuthorizationTypeWhenInUse, 167 | }; 168 | 169 | #endif /* INTU_LOCATION_REQUEST_DEFINES_H */ 170 | -------------------------------------------------------------------------------- /LocationManager/INTULocationManager/INTULocationManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationManager.h 3 | // 4 | // Copyright (c) 2014-2017 Intuit Inc. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | #import "INTULocationRequestDefines.h" 27 | 28 | //! Project version number for INTULocationManager. 29 | FOUNDATION_EXPORT double INTULocationManagerVersionNumber; 30 | 31 | //! Project version string for INTULocationManager. 32 | FOUNDATION_EXPORT const unsigned char INTULocationManagerVersionString[]; 33 | 34 | 35 | NS_ASSUME_NONNULL_BEGIN 36 | 37 | /** 38 | An abstraction around CLLocationManager that provides a block-based asynchronous API for obtaining the device's location. 39 | INTULocationManager automatically starts and stops system location services as needed to minimize battery drain. 40 | */ 41 | @interface INTULocationManager : NSObject 42 | 43 | /** Returns the current state of location services for this app, based on the system settings and user authorization status. */ 44 | + (INTULocationServicesState)locationServicesState; 45 | 46 | /** Returns the current state of heading services for this device. */ 47 | + (INTUHeadingServicesState)headingServicesState; 48 | 49 | /** Returns the singleton instance of this class. */ 50 | + (instancetype)sharedInstance; 51 | 52 | @property (nonatomic, assign) INTUAuthorizationType preferredAuthorizationType; 53 | 54 | #pragma mark Location Requests 55 | 56 | /** 57 | Asynchronously requests the current location of the device using location services. 58 | 59 | @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). 60 | @param timeout The maximum amount of time (in seconds) to wait for a location with the desired accuracy before completing. If 61 | this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). 62 | @param block The block to execute upon success, failure, or timeout. 63 | 64 | @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. 65 | */ 66 | - (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy 67 | timeout:(NSTimeInterval)timeout 68 | block:(INTULocationRequestBlock)block; 69 | 70 | /** 71 | Asynchronously requests the current location of the device using location services, optionally delaying the timeout countdown until the user has 72 | responded to the dialog requesting permission for this app to access location services. 73 | 74 | @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). 75 | @param timeout The maximum amount of time (in seconds) to wait for a location with the desired accuracy before completing. If 76 | this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). 77 | @param delayUntilAuthorized A flag specifying whether the timeout should only take effect after the user responds to the system prompt requesting 78 | permission for this app to access location services. If YES, the timeout countdown will not begin until after the 79 | app receives location services permissions. If NO, the timeout countdown begins immediately when calling this method. 80 | @param block The block to execute upon success, failure, or timeout. 81 | 82 | @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. 83 | */ 84 | - (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy 85 | timeout:(NSTimeInterval)timeout 86 | delayUntilAuthorized:(BOOL)delayUntilAuthorized 87 | block:(INTULocationRequestBlock)block; 88 | 89 | /** 90 | Asynchronously requests the current location of the device using location services, optionally delaying the timeout countdown until the user has 91 | responded to the dialog requesting permission for this app to access location services. 92 | 93 | @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location). 94 | @param desiredActivityType The activity type desired, which controls when/if pausing occurs. 95 | @param timeout The maximum amount of time (in seconds) to wait for a location with the desired accuracy before completing. If 96 | this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled). 97 | @param delayUntilAuthorized A flag specifying whether the timeout should only take effect after the user responds to the system prompt requesting 98 | permission for this app to access location services. If YES, the timeout countdown will not begin until after the 99 | app receives location services permissions. If NO, the timeout countdown begins immediately when calling this method. 100 | @param block The block to execute upon success, failure, or timeout. 101 | 102 | @return The location request ID, which can be used to force early completion or cancel the request while it is in progress. 103 | */ 104 | - (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy 105 | desiredActivityType:(CLActivityType)desiredActivityType 106 | timeout:(NSTimeInterval)timeout 107 | delayUntilAuthorized:(BOOL)delayUntilAuthorized 108 | block:(INTULocationRequestBlock)block; 109 | 110 | /** 111 | Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location. 112 | This method instructs location services to use the highest accuracy available (which also requires the most power). 113 | If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. 114 | 115 | @param block The block to execute every time an updated location is available. 116 | The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut. 117 | 118 | @return The location request ID, which can be used to cancel the subscription of location updates to this block. 119 | */ 120 | - (INTULocationRequestID)subscribeToLocationUpdatesWithBlock:(INTULocationRequestBlock)block; 121 | 122 | /** 123 | Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location. 124 | The specified desired accuracy is passed along to location services, and controls how much power is used, with higher accuracies using more power. 125 | If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. 126 | 127 | @param desiredAccuracy The accuracy level desired, which controls how much power is used by the device's location services. 128 | @param block The block to execute every time an updated location is available. Note that this block runs for every update, regardless of 129 | whether the achievedAccuracy is at least the desiredAccuracy. 130 | The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut. 131 | 132 | @return The location request ID, which can be used to cancel the subscription of location updates to this block. 133 | */ 134 | - (INTULocationRequestID)subscribeToLocationUpdatesWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy 135 | block:(INTULocationRequestBlock)block; 136 | 137 | /** 138 | Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location. 139 | The specified desired accuracy is passed along to location services, and controls how much power is used, with higher accuracies using more power. 140 | If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. 141 | 142 | @param desiredAccuracy The accuracy level desired, which controls how much power is used by the device's location services. 143 | @param desiredActivityType The activity type desired, which controls when/if pausing occurs. 144 | @param block The block to execute every time an updated location is available. Note that this block runs for every update, regardless of 145 | whether the achievedAccuracy is at least the desiredAccuracy. 146 | The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut. 147 | 148 | @return The location request ID, which can be used to cancel the subscription of location updates to this block. 149 | */ 150 | - (INTULocationRequestID)subscribeToLocationUpdatesWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy 151 | desiredActivityType:(CLActivityType)desiredActivityType 152 | block:(INTULocationRequestBlock)block; 153 | 154 | /** 155 | Creates a subscription for significant location changes that will execute the block once per change indefinitely (until canceled). 156 | If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically. 157 | 158 | @param block The block to execute every time an updated location is available. 159 | The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut. 160 | 161 | @return The location request ID, which can be used to cancel the subscription of significant location changes to this block. 162 | */ 163 | - (INTULocationRequestID)subscribeToSignificantLocationChangesWithBlock:(INTULocationRequestBlock)block; 164 | 165 | /** Immediately forces completion of the location request with the given requestID (if it exists), and executes the original request block with the results. 166 | For one-time location requests, this is effectively a manual timeout, and will result in the request completing with status INTULocationStatusTimedOut. 167 | If the requestID corresponds to a subscription, then the subscription will simply be canceled. */ 168 | - (void)forceCompleteLocationRequest:(INTULocationRequestID)requestID; 169 | 170 | /** Immediately cancels the location request (or subscription) with the given requestID (if it exists), without executing the original request block. */ 171 | - (void)cancelLocationRequest:(INTULocationRequestID)requestID; 172 | 173 | #pragma mark Heading Requests 174 | 175 | /** 176 | Creates a subscription for heading updates that will execute the block once per update indefinitely (until canceled), assuming the heading update exceeded the heading filter threshold. 177 | If an error occurs, the block will execute with a status other than INTUHeadingStatusSuccess, and the subscription will be canceled automatically. 178 | 179 | @param block The block to execute every time an updated heading is available. The status will be INTUHeadingStatusSuccess unless an error occurred. 180 | 181 | @return The heading request ID, which can be used to cancel the subscription of heading updates to this block. 182 | */ 183 | - (INTUHeadingRequestID)subscribeToHeadingUpdatesWithBlock:(INTUHeadingRequestBlock)block; 184 | 185 | /** Immediately cancels the heading subscription request with the given requestID (if it exists), without executing the original request block. */ 186 | - (void)cancelHeadingRequest:(INTUHeadingRequestID)requestID; 187 | 188 | #pragma mark - Additions 189 | 190 | /** It is possible to force enable background location fetch even if your set any kind of Authorizations */ 191 | - (void)setBackgroundLocationUpdate:(BOOL) enabled; 192 | 193 | /** 194 | Sets a Boolean indicating whether the status bar changes its appearance when location services 195 | are used in the background. 196 | 197 | This property affects only apps that received always authorization. When such an app moves to the background, 198 | the system uses this property to determine whether to change the status bar appearance to indicate that 199 | location services are in use. Displaying a modified status bar gives the user a quick way to return to your app. 200 | The default value of this property is false. 201 | 202 | For apps with when-in-use authorization, the system always changes the status bar appearance when 203 | the app uses location services in the background. 204 | 205 | @param shows Boolean indicating whether the status bar changes its appearance when location services are used in the background. 206 | */ 207 | - (void)setShowsBackgroundLocationIndicator:(BOOL) shows; 208 | 209 | /** 210 | Sets a Boolean value indicating whether the location manager object may pause location updates. 211 | 212 | @param pauses Boolean value indicating whether the location manager object may pause location updates. 213 | */ 214 | - (void)setPausesLocationUpdatesAutomatically:(BOOL) pauses; 215 | 216 | @end 217 | 218 | NS_ASSUME_NONNULL_END 219 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/INTUViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTUViewController.m 3 | // LocationManagerExample 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import "INTUViewController.h" 28 | #import 29 | 30 | @interface INTUViewController () 31 | 32 | @property (weak, nonatomic) IBOutlet UILabel *statusLabel; 33 | @property (weak, nonatomic) IBOutlet UISwitch *subscriptionForAllChangesSwitch; 34 | @property (weak, nonatomic) IBOutlet UISwitch *subscriptionForSignificantChangesSwitch; 35 | @property (weak, nonatomic) IBOutlet UISwitch *subscriptionForAllHeadingChangesSwitch; 36 | @property (weak, nonatomic) IBOutlet UILabel *timeoutLabel; 37 | @property (weak, nonatomic) IBOutlet UILabel *desiredAccuracyLabel; 38 | @property (weak, nonatomic) IBOutlet UISegmentedControl *desiredAccuracyControl; 39 | @property (weak, nonatomic) IBOutlet UILabel *desiredActivityLabel; 40 | @property (weak, nonatomic) IBOutlet UISegmentedControl *desiredActivityTypeControl; 41 | @property (weak, nonatomic) IBOutlet UISlider *timeoutSlider; 42 | @property (weak, nonatomic) IBOutlet UIButton *requestCurrentLocationButton; 43 | @property (weak, nonatomic) IBOutlet UIButton *cancelRequestButton; 44 | @property (weak, nonatomic) IBOutlet UIButton *forceCompleteRequestButton; 45 | @property (weak, nonatomic) IBOutlet UIActivityIndicatorView *activityIndicator; 46 | 47 | @property (assign, nonatomic) INTULocationAccuracy desiredAccuracy; 48 | @property (assign, nonatomic) CLActivityType desiredActivityType; 49 | @property (assign, nonatomic) NSTimeInterval timeout; 50 | 51 | @property (assign, nonatomic) INTULocationRequestID locationRequestID; 52 | @property (assign, nonatomic) INTUHeadingRequestID headingRequestID; 53 | 54 | @end 55 | 56 | @implementation INTUViewController 57 | 58 | - (void)viewDidLoad 59 | { 60 | [super viewDidLoad]; 61 | 62 | self.subscriptionForAllChangesSwitch.on = NO; 63 | self.subscriptionForSignificantChangesSwitch.on = NO; 64 | self.subscriptionForAllHeadingChangesSwitch.on = NO; 65 | self.desiredAccuracyControl.selectedSegmentIndex = 0; 66 | self.desiredActivityTypeControl.selectedSegmentIndex = 0; 67 | self.desiredAccuracy = INTULocationAccuracyCity; 68 | self.desiredActivityType = CLActivityTypeAirborne; 69 | self.timeoutSlider.value = 10.0; 70 | self.timeout = 10.0; 71 | 72 | self.locationRequestID = NSNotFound; 73 | self.headingRequestID = NSNotFound; 74 | self.statusLabel.text = @"Tap the button below to start a new location or heading request."; 75 | } 76 | 77 | #pragma mark - Locations 78 | 79 | - (NSString *)getLocationErrorDescription:(INTULocationStatus)status 80 | { 81 | if (status == INTULocationStatusServicesNotDetermined) { 82 | return @"Error: User has not responded to the permissions alert."; 83 | } 84 | if (status == INTULocationStatusServicesDenied) { 85 | return @"Error: User has denied this app permissions to access device location."; 86 | } 87 | if (status == INTULocationStatusServicesRestricted) { 88 | return @"Error: User is restricted from using location services by a usage policy."; 89 | } 90 | if (status == INTULocationStatusServicesDisabled) { 91 | return @"Error: Location services are turned off for all apps on this device."; 92 | } 93 | return @"An unknown error occurred.\n(Are you using iOS Simulator with location set to 'None'?)"; 94 | } 95 | 96 | /** 97 | Starts a new subscription for location updates. 98 | */ 99 | - (void)startLocationUpdateSubscription 100 | { 101 | __weak __typeof(self) weakSelf = self; 102 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 103 | self.locationRequestID = [locMgr subscribeToLocationUpdatesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 104 | __typeof(weakSelf) strongSelf = weakSelf; 105 | 106 | if (status == INTULocationStatusSuccess) { 107 | // A new updated location is available in currentLocation, and achievedAccuracy indicates how accurate this particular location is 108 | strongSelf.statusLabel.text = [NSString stringWithFormat:@"'Location updates' subscription block called with Current Location:\n%@", currentLocation]; 109 | } 110 | else { 111 | // An error occurred 112 | strongSelf.statusLabel.text = [strongSelf getLocationErrorDescription:status]; 113 | } 114 | }]; 115 | } 116 | 117 | /** 118 | Starts a new subscription for significant location changes. 119 | */ 120 | - (void)startMonitoringSignificantLocationChanges 121 | { 122 | __weak __typeof(self) weakSelf = self; 123 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 124 | self.locationRequestID = [locMgr subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 125 | __typeof(weakSelf) strongSelf = weakSelf; 126 | 127 | if (status == INTULocationStatusSuccess) { 128 | // A new updated location is available in currentLocation, and achievedAccuracy indicates how accurate this particular location is 129 | strongSelf.statusLabel.text = [NSString stringWithFormat:@"'Significant changes' subscription block called with Current Location:\n%@", currentLocation]; 130 | } 131 | else { 132 | // An error occurred 133 | strongSelf.statusLabel.text = [strongSelf getLocationErrorDescription:status]; 134 | } 135 | }]; 136 | } 137 | 138 | /** 139 | Starts a new one-time request for the current location. 140 | */ 141 | - (void)startSingleLocationRequest 142 | { 143 | __weak __typeof(self) weakSelf = self; 144 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 145 | self.locationRequestID = [locMgr requestLocationWithDesiredAccuracy:self.desiredAccuracy 146 | desiredActivityType:self.desiredActivityType 147 | timeout:self.timeout 148 | delayUntilAuthorized:YES 149 | block: 150 | ^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 151 | __typeof(weakSelf) strongSelf = weakSelf; 152 | 153 | if (status == INTULocationStatusSuccess) { 154 | // achievedAccuracy is at least the desired accuracy (potentially better) 155 | strongSelf.statusLabel.text = [NSString stringWithFormat:@"Location request successful! Current Location:\n%@", currentLocation]; 156 | } 157 | else if (status == INTULocationStatusTimedOut) { 158 | // You may wish to inspect achievedAccuracy here to see if it is acceptable, if you plan to use currentLocation 159 | strongSelf.statusLabel.text = [NSString stringWithFormat:@"Location request timed out. Current Location:\n%@", currentLocation]; 160 | } 161 | else { 162 | // An error occurred 163 | strongSelf.statusLabel.text = [strongSelf getLocationErrorDescription:status]; 164 | } 165 | 166 | strongSelf.locationRequestID = NSNotFound; 167 | }]; 168 | } 169 | 170 | /** 171 | Callback when the "Request Current Location" or "Start Subscription" button is tapped. 172 | */ 173 | - (IBAction)startButtonTapped:(id)sender 174 | { 175 | if (self.subscriptionForAllChangesSwitch.on) { 176 | [self startLocationUpdateSubscription]; 177 | } else if (self.subscriptionForSignificantChangesSwitch.on) { 178 | [self startMonitoringSignificantLocationChanges]; 179 | } else { 180 | [self startSingleLocationRequest]; 181 | } 182 | } 183 | 184 | /** 185 | Callback when the "Force Complete Request" button is tapped. 186 | */ 187 | - (IBAction)forceCompleteRequest:(id)sender 188 | { 189 | [[INTULocationManager sharedInstance] forceCompleteLocationRequest:self.locationRequestID]; 190 | if (self.subscriptionForAllChangesSwitch.on || self.subscriptionForSignificantChangesSwitch.on) { 191 | // Clear the location request ID, since this will not be handled inside the subscription block 192 | // (This is not necessary for regular one-time location requests, since they will handle this inside the completion block.) 193 | self.locationRequestID = NSNotFound; 194 | self.statusLabel.text = @"Subscription canceled."; 195 | } 196 | } 197 | 198 | /** 199 | Callback when the "Cancel Request" button is tapped. 200 | */ 201 | - (IBAction)cancelRequest:(id)sender 202 | { 203 | [[INTULocationManager sharedInstance] cancelLocationRequest:self.locationRequestID]; 204 | self.locationRequestID = NSNotFound; 205 | self.statusLabel.text = self.subscriptionForAllChangesSwitch.on || self.subscriptionForSignificantChangesSwitch.on ? @"Subscription canceled." : @"Location request canceled."; 206 | } 207 | 208 | - (IBAction)subscriptionSwitchChanged:(UISwitch *)sender 209 | { 210 | if (sender.on) { 211 | if ([sender isEqual:self.subscriptionForAllChangesSwitch]) { 212 | [self.subscriptionForSignificantChangesSwitch setOn:NO animated:YES]; 213 | } else if ([sender isEqual:self.subscriptionForSignificantChangesSwitch]) { 214 | [self.subscriptionForAllChangesSwitch setOn:NO animated:YES]; 215 | } 216 | } 217 | 218 | self.desiredAccuracyControl.userInteractionEnabled = !sender.on; 219 | self.desiredActivityTypeControl.userInteractionEnabled = !sender.on; 220 | self.timeoutSlider.userInteractionEnabled = !sender.on; 221 | 222 | CGFloat alpha = sender.on ? 0.2 : 1.0; 223 | [UIView animateWithDuration:0.3 animations:^{ 224 | self.desiredAccuracyLabel.alpha = alpha; 225 | self.desiredActivityLabel.alpha = alpha; 226 | self.desiredAccuracyControl.alpha = alpha; 227 | self.desiredActivityTypeControl.alpha = alpha; 228 | self.timeoutLabel.alpha = alpha; 229 | self.timeoutSlider.alpha = alpha; 230 | }]; 231 | 232 | NSString *requestLocationButtonTitle = sender.on ? @"Start Subscription" : @"Request Current Location"; 233 | [self.requestCurrentLocationButton setTitle:requestLocationButtonTitle forState:UIControlStateNormal]; 234 | } 235 | 236 | - (IBAction)desiredAccuracyControlChanged:(UISegmentedControl *)sender 237 | { 238 | switch (sender.selectedSegmentIndex) { 239 | case 0: 240 | self.desiredAccuracy = INTULocationAccuracyCity; 241 | break; 242 | case 1: 243 | self.desiredAccuracy = INTULocationAccuracyNeighborhood; 244 | break; 245 | case 2: 246 | self.desiredAccuracy = INTULocationAccuracyBlock; 247 | break; 248 | case 3: 249 | self.desiredAccuracy = INTULocationAccuracyHouse; 250 | break; 251 | case 4: 252 | self.desiredAccuracy = INTULocationAccuracyRoom; 253 | break; 254 | default: 255 | break; 256 | } 257 | } 258 | 259 | - (IBAction)desiredActivityControlChanged:(UISegmentedControl *)sender 260 | { 261 | switch (sender.selectedSegmentIndex) { 262 | case 0: 263 | self.desiredActivityType = CLActivityTypeAirborne; 264 | break; 265 | case 1: 266 | self.desiredActivityType = CLActivityTypeAutomotiveNavigation; 267 | break; 268 | case 2: 269 | self.desiredActivityType = CLActivityTypeFitness; 270 | break; 271 | case 3: 272 | self.desiredActivityType = CLActivityTypeOtherNavigation; 273 | break; 274 | case 4: 275 | self.desiredActivityType = CLActivityTypeOther; 276 | break; 277 | default: 278 | break; 279 | } 280 | } 281 | 282 | - (IBAction)timeoutSliderChanged:(UISlider *)sender 283 | { 284 | self.timeout = round(sender.value); 285 | if (self.timeout == 0) { 286 | self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: 0 seconds (no limit)"]; 287 | } else if (self.timeout == 1) { 288 | self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: 1 second"]; 289 | } else { 290 | self.timeoutLabel.text = [NSString stringWithFormat:@"Timeout: %ld seconds", (long)self.timeout]; 291 | } 292 | } 293 | 294 | /** 295 | Implement the setter for locationRequestID in order to update the UI as needed. 296 | */ 297 | - (void)setLocationRequestID:(INTULocationRequestID)locationRequestID 298 | { 299 | _locationRequestID = locationRequestID; 300 | 301 | BOOL isProcessingLocationRequest = (locationRequestID != NSNotFound); 302 | 303 | self.subscriptionForAllChangesSwitch.enabled = !isProcessingLocationRequest; 304 | self.subscriptionForSignificantChangesSwitch.enabled = !isProcessingLocationRequest; 305 | self.desiredAccuracyControl.enabled = !isProcessingLocationRequest; 306 | self.desiredActivityTypeControl.enabled = !isProcessingLocationRequest; 307 | self.timeoutSlider.enabled = !isProcessingLocationRequest; 308 | self.requestCurrentLocationButton.enabled = !isProcessingLocationRequest; 309 | self.forceCompleteRequestButton.enabled = isProcessingLocationRequest && !self.subscriptionForAllChangesSwitch.on && !self.subscriptionForSignificantChangesSwitch.on; 310 | self.cancelRequestButton.enabled = isProcessingLocationRequest; 311 | 312 | if (isProcessingLocationRequest) { 313 | [self.activityIndicator startAnimating]; 314 | self.statusLabel.text = @"Location request in progress..."; 315 | } else { 316 | [self.activityIndicator stopAnimating]; 317 | } 318 | } 319 | 320 | #pragma mark - Heading 321 | 322 | - (NSString *)getHeadingErrorDescription:(INTUHeadingStatus)status 323 | { 324 | if (status == INTUHeadingStatusUnavailable) { 325 | return @"Error: Heading services are not available on this device."; 326 | } 327 | return @"An unknown error occurred.\n(Are you using iOS Simulator with location set to 'None'?)"; 328 | } 329 | 330 | - (void)startHeadingRequest 331 | { 332 | self.statusLabel.text = @"Heading subscription in progress..."; 333 | 334 | __weak __typeof(self) weakSelf = self; 335 | self.headingRequestID = [[INTULocationManager sharedInstance] subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 336 | __typeof(weakSelf) strongSelf = weakSelf; 337 | if (status == INTUHeadingStatusSuccess) { 338 | // An updated heading is available 339 | strongSelf.statusLabel.text = [NSString stringWithFormat:@"'Heading updates' subscription block called with Current Heading:\n%@", heading]; 340 | } else { 341 | strongSelf.statusLabel.text = [self getHeadingErrorDescription:status]; 342 | } 343 | }]; 344 | } 345 | 346 | - (void)cancelHeadingRequest 347 | { 348 | [[INTULocationManager sharedInstance] cancelHeadingRequest:self.headingRequestID]; 349 | self.headingRequestID = NSNotFound; 350 | self.statusLabel.text = @"Heading subscription canceled."; 351 | } 352 | 353 | - (IBAction)headingSubscriptionSwitchChanged:(UISwitch *)sender 354 | { 355 | if (sender.on) { 356 | [self startHeadingRequest]; 357 | } else { 358 | [self cancelHeadingRequest]; 359 | } 360 | } 361 | 362 | @end 363 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![INTULocationManager](https://github.com/intuit/LocationManager/blob/master/Images/INTULocationManager.png?raw=true)](#) 2 | [![Build Status](http://img.shields.io/travis/intuit/LocationManager.svg?style=flat)](https://travis-ci.org/intuit/LocationManager) [![Test Coverage](http://img.shields.io/coveralls/intuit/LocationManager.svg?style=flat)](https://coveralls.io/r/intuit/LocationManager) [![Version](http://img.shields.io/cocoapods/v/INTULocationManager.svg?style=flat)](http://cocoapods.org/pods/INTULocationManager) [![Platform](http://img.shields.io/cocoapods/p/INTULocationManager.svg?style=flat)](http://cocoapods.org/pods/INTULocationManager) [![License](http://img.shields.io/cocoapods/l/INTULocationManager.svg?style=flat)](LICENSE) 3 | 4 | INTULocationManager makes it easy to get the device's current location and is currently heading on iOS. It is an Objective-C library that also works great in Swift. 5 | 6 | INTULocationManager provides a block-based asynchronous API to request the current location, either once or continuously. It internally manages multiple simultaneous locations and heading requests, and each one-time location request can specify its own desired accuracy level and timeout duration. INTULocationManager automatically starts location services when the first request comes in and stops the location services when all requests have been completed, while dynamically managing the power consumed by location services to reduce the impact on battery life. 7 | 8 | ## What's wrong with CLLocationManager? 9 | CLLocationManager requires you to manually detect and handle things like permissions, stale/inaccurate locations, errors, and more. CLLocationManager uses a more traditional delegate pattern instead of the modern block-based callback pattern. And while it works fine to track changes in the user's location over time (such as, for turn-by-turn navigation), it is extremely cumbersome to correctly request a single location update (such as to determine the user's current city to get a weather forecast, or to autofill an address from the current location). 10 | 11 | INTULocationManager makes it easy to request both the device's current location, either once or continuously, as well as the device's continuous heading. The API is extremely simple for both one-time location requests and recurring subscriptions to location updates. For one-time location requests, you can specify how accurate of a location you need, and how long you're willing to wait to get it. Significant location change monitoring is also supported. INTULocationManager is power efficient and conserves the device's battery by automatically determining and using the most efficient Core Location accuracy settings, and by automatically powering down location services (e.g. GPS or compass) when they are no longer needed. 12 | 13 | ## Installation 14 | *INTULocationManager requires iOS 9.0 or later.* 15 | 16 | ### Using [CocoaPods](http://cocoapods.org) 17 | 18 | 1. Add the pod `INTULocationManager` to your [Podfile](http://guides.cocoapods.org/using/the-podfile.html). 19 | 20 | ```ruby 21 | pod 'INTULocationManager' 22 | ``` 23 | 24 | 1. Run `pod install` from Terminal, then open your app's `.xcworkspace` file to launch Xcode. 25 | 1. Import the `INTULocationManager.h` header. 26 | * With `use_frameworks!` in your Podfile 27 | * Swift: `import INTULocationManager` 28 | * Objective-C: `#import ` (or with Modules enabled: `@import INTULocationManager;`) 29 | * Without `use_frameworks!` in your Podfile 30 | * Swift: Add `#import "INTULocationManager.h"` to your bridging header. 31 | * Objective-C: `#import "INTULocationManager.h"` 32 | 33 | ### Using [Carthage](https://github.com/Carthage/Carthage) 34 | 35 | 1. Add the `intuit/LocationManager` project to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile). 36 | 37 | ```ogdl 38 | github "intuit/LocationManager" 39 | ``` 40 | 41 | 1. Run `carthage update`, then follow the [additional steps required](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) to add the iOS and/or Mac frameworks into your project. 42 | 1. Import the INTULocationManager framework/module. 43 | * Swift: `import INTULocationManager` 44 | * Objective-C: `#import ` (or with Modules enabled: `@import INTULocationManager;`) 45 | 46 | ### Manually from GitHub 47 | 48 | 1. Download all the files in [INTULocationManager subdirectory](LocationManager/INTULocationManager). 49 | 1. Add the source files to your Xcode project (drag and drop is easiest). 50 | 1. Import the `INTULocationManager.h` header. 51 | * Swift: Add `#import "INTULocationManager.h"` to your bridging header. 52 | * Objective-C: `#import "INTULocationManager.h"` 53 | 54 | ## Usage 55 | 56 | ### Requesting Permission to Access Location Services 57 | INTULocationManager automatically handles obtaining permission to access location services when you issue a location request and the user has not already granted your app the permission to access that location services. 58 | 59 | #### iOS 9 and above 60 | Starting with iOS 8, you **must** provide a description for how your app uses location services by setting a string for the key [`NSLocationWhenInUseUsageDescription`](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW26) or [`NSLocationAlwaysUsageDescription`](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW18) in your app's `Info.plist` file. INTULocationManager determines which level of permissions to request based on which description key is present. You should only request the minimum permission level that your app requires, therefore it is recommended that you use the "When In Use" level unless you require more access. If you provide values for both description keys, the more permissive "Always" level is requested. 61 | 62 | #### iOS 11 63 | Starting with iOS 11, you **must** provide a description for how your app uses location services by setting a string for the key `NSLocationAlwaysAndWhenInUseUsageDescription` in your app's `Info.plist` file. 64 | 65 | #### iOS 12 66 | Starting with iOS 12, you will have access to set the `desiredActivityType` as `CLActivityTypeAirborne`. 67 | 68 | ### Getting the Current Location (once) 69 | To get the device's current location, use the method `requestLocationWithDesiredAccuracy:timeout:block:`. 70 | 71 | The `desiredAccuracy` parameter specifies how **accurate and recent** of a location you need. The possible values are: 72 | ```objective-c 73 | INTULocationAccuracyCity // 5000 meters or better, received within the last 10 minutes -- lowest accuracy 74 | INTULocationAccuracyNeighborhood // 1000 meters or better, received within the last 5 minutes 75 | INTULocationAccuracyBlock // 100 meters or better, received within the last 1 minute 76 | INTULocationAccuracyHouse // 15 meters or better, received within the last 15 seconds 77 | INTULocationAccuracyRoom // 5 meters or better, received within the last 5 seconds -- highest accuracy 78 | ``` 79 | 80 | The `desiredActivityType` parameter indicated the **type of activity** that is being tracked. The possible values are: 81 | ```objective-c 82 | CLActivityTypeFitness // Track fitness activities such as walking, running, cycling, and so on 83 | CLActivityTypeAutomotiveNavigation // Track location changes to the automobile 84 | CLActivityTypeAirborne // Track airborne activities - iOS 12 and above 85 | CLActivityTypeOtherNavigation // Track vehicular navigation that are not automobile related 86 | CLActivityTypeOther // Track unknown activities. This is the default value 87 | ``` 88 | 89 | The `timeout` parameter specifies that how long you are willing to wait for a location with the accuracy you requested. The timeout guarantees that your block will execute within this period of time, either with a location of at least the accuracy you requested (`INTULocationStatusSuccess`), or with whichever location could be determined before the timeout interval was up (`INTULocationStatusTimedOut`). Pass `0.0` for no timeout *(not recommended)*. 90 | 91 | By default, the timeout countdown begins as soon as the `requestLocationWithDesiredAccuracy:timeout:block:` method is called. However, there is another variant of this method that includes a `delayUntilAuthorized:` parameter, which allows you to pass `YES` to delay the start of the timeout countdown until the user has responded to the system location services permissions prompt (if the user hasn't allowed or denied the app access yet). 92 | 93 | Here's an example: 94 | ```objective-c 95 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 96 | [locMgr requestLocationWithDesiredAccuracy:INTULocationAccuracyCity 97 | timeout:10.0 98 | delayUntilAuthorized:YES // This parameter is optional, defaults to NO if omitted 99 | block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 100 | if (status == INTULocationStatusSuccess) { 101 | // Request succeeded, meaning achievedAccuracy is at least the requested accuracy, and 102 | // currentLocation contains the device's current location. 103 | } 104 | else if (status == INTULocationStatusTimedOut) { 105 | // Wasn't able to locate the user with the requested accuracy within the timeout interval. 106 | // However, currentLocation contains the best location available (if any) as of right now, 107 | // and achievedAccuracy has info on the accuracy/recency of the location in currentLocation. 108 | } 109 | else { 110 | // An error occurred, more info is available by looking at the specific status returned. 111 | } 112 | }]; 113 | ``` 114 | ```swift 115 | let locationManager = INTULocationManager.sharedInstance() 116 | locationManager.requestLocation(withDesiredAccuracy: .city, 117 | timeout: 10.0, 118 | delayUntilAuthorized: true) { (currentLocation, achievedAccuracy, status) in 119 | if (status == INTULocationStatus.success) { 120 | // Request succeeded, meaning achievedAccuracy is at least the requested accuracy, and 121 | // currentLocation contains the device's current location 122 | } 123 | else if (status == INTULocationStatus.timedOut) { 124 | // Wasn't able to locate the user with the requested accuracy within the timeout interval. 125 | // However, currentLocation contains the best location available (if any) as of right now, 126 | // and achievedAccuracy has info on the accuracy/recency of the location in currentLocation. 127 | } 128 | else { 129 | // An error occurred, more info is available by looking at the specific status returned. 130 | } 131 | } 132 | 133 | 134 | ``` 135 | 136 | ### Subscribing to Continuous Location Updates 137 | To subscribe to continuous location updates, use the method `subscribeToLocationUpdatesWithBlock:`. This method instructs location services to use the highest accuracy available (which also requires the most power). The block will execute indefinitely (even across errors, until canceled), once for every new updated location regardless of its accuracy. 138 | 139 | If you do not need the highest possible accuracy level, you should instead use `subscribeToLocationUpdatesWithDesiredAccuracy:block:`. This method takes the desired accuracy level and uses it to control how much power is used by location services, with lower accuracy levels like Neighborhood and City requiring less power. Note that INTULocationManager will automatically manage the system location services accuracy level, including when there are multiple active location requests/subscriptions with different desired accuracies. 140 | 141 | If an error occurs, the block will execute with a status other than `INTULocationStatusSuccess`, and the subscription will be kept alive. 142 | 143 | Here's an example: 144 | ```objective-c 145 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 146 | [locMgr subscribeToLocationUpdatesWithDesiredAccuracy:INTULocationAccuracyHouse 147 | block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 148 | if (status == INTULocationStatusSuccess) { 149 | // A new updated location is available in currentLocation, and achievedAccuracy indicates how accurate this particular location is. 150 | } 151 | else { 152 | // An error occurred, more info is available by looking at the specific status returned. The subscription has been kept alive. 153 | } 154 | }]; 155 | ``` 156 | 157 | ### Subscribing to Significant Location Changes 158 | To subscribe the significant location changes, use the method `subscribeToSignificantLocationChangesWithBlock:`. This instructs the location services to begin monitoring for significant location changes, which is very power efficient. The block will execute indefinitely (until canceled), once for every new updated location regardless of its accuracy. Note that if there are other simultaneously active location requests or subscriptions, the block will execute for every location update (not just for significant location changes). If you intend to take action only when the location has changed significantly, you should implement custom filtering based on the distance & time received from the last location. 159 | 160 | If an error occurs, the block will execute with a status other than `INTULocationStatusSuccess`, and the subscription will be kept alive. 161 | 162 | Here's an example: 163 | ```objective-c 164 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 165 | [locMgr subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 166 | if (status == INTULocationStatusSuccess) { 167 | // A new updated location is available in currentLocation, and achievedAccuracy indicates how accurate this particular location is. 168 | } 169 | else { 170 | // An error occurred, more info is available by looking at the specific status returned. The subscription has been kept alive. 171 | } 172 | }]; 173 | ``` 174 | 175 | If your app has acquired the "Always" location services authorization and your app is terminated with at least one active significant location change subscription, your app may be launched in the background when the system detects a significant location change. Note that when the app terminates, all of your active location requests & subscriptions with INTULocationManager are canceled. Therefore, when the app launches due to a significant location change, you should immediately use INTULocationManager to set up a new subscription for significant location changes in order to receive the location information. 176 | 177 | Here is an example of how to handle being launched in the background due to a significant location change: 178 | ```objective-c 179 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 180 | { 181 | // If you start monitoring significant location changes and your app is subsequently terminated, the system automatically relaunches the app into the background if a new event arrives. 182 | // Upon relaunch, you must still subscribe to significant location changes to continue receiving location events. 183 | if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) { 184 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 185 | [locMgr subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 186 | // This block will be executed with the details of the significant location change that triggered the background app launch, 187 | // and will continue to execute for any future significant location change events as well (unless canceled). 188 | }]; 189 | } 190 | return YES; 191 | } 192 | ``` 193 | 194 | ### Managing Active Requests or Subscriptions 195 | When issuing a location request, you can optionally store the request ID, which allows you to force complete or cancel the request at any time: 196 | ```objective-c 197 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 198 | INTULocationRequestID requestID = [locMgr requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse 199 | timeout:5.0 200 | block:locationRequestBlock]; 201 | 202 | // Force the request to complete early, like a manual timeout (will execute the block) 203 | [[INTULocationManager sharedInstance] forceCompleteLocationRequest:requestID]; 204 | 205 | // Cancel the request (won't execute the block) 206 | [[INTULocationManager sharedInstance] cancelLocationRequest:requestID]; 207 | ``` 208 | 209 | Note that subscriptions never timeout; calling `forceCompleteLocationRequest:` on a subscription will simply cancel it. 210 | 211 | ### Subscribing to Continuous Heading Updates 212 | To subscribe to continuous heading updates, use the method `subscribeToHeadingUpdatesWithBlock:`. This method does not set any default heading filter value, but you can do so using the `headingFilter` property on the manager instance. It also does not filter based on accuracy of the result, but rather leaves it up to you to check the returned `CLHeading` object's `headingAccuracy` property to determine whether or not it is acceptable. 213 | 214 | The block will execute indefinitely (until canceled), once for every new updated heading regardless of its accuracy. Note that if heading requests are removed or canceled, the manager will automatically stop updating the device heading in order to preserve battery life. 215 | 216 | If an error occurs, the block will execute with a status other than `INTUHeadingStatusSuccess`, and the subscription will only be automatically canceled if the device doesn't have heading support (i.e. for status `INTUHeadingStatusUnavailable`). 217 | 218 | Here's an example: 219 | ```objective-c 220 | INTULocationManager *locMgr = [INTULocationManager sharedInstance]; 221 | [locMgr subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 222 | if (status == INTUHeadingStatusSuccess) { 223 | // An updated heading is available 224 | NSLog(@"'Heading updates' subscription block called with Current Heading:\n%@", heading); 225 | } else { 226 | // An error occurred, more info is available by looking at the specific status returned. The subscription will be canceled only if the device doesn't have heading support. 227 | } 228 | }]; 229 | ``` 230 | 231 | ## Example Project 232 | Open the [project](LocationManager) included in the repository (requires Xcode 6 and iOS 8.0 or later). It contains a `LocationManagerExample` scheme that will run a simple demo app. Please note that it can run in the iOS Simulator, but you need to go to the iOS Simulator's **Debug > Location** menu once running the app to simulate a location (the default is **None**). 233 | 234 | ## Issues & Contributions 235 | Please [open an issue here on GitHub](https://github.com/intuit/LocationManager/issues/new) if you have a problem, suggestion, or other comment. 236 | 237 | Pull requests are welcome and encouraged! There are no official guidelines, but please try to be consistent with the existing code style. 238 | 239 | ## License 240 | INTULocationManager is provided under the MIT license. 241 | 242 | # INTU on GitHub 243 | Check out more [iOS and OS X open source projects from Intuit](https://github.com/search?utf8=✓&q=user%3Aintuit+language%3Aobjective-c&type=Repositories&ref=searchresults)! 244 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 35 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 105 | 118 | 122 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 164 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /LocationManager/LocationManagerTests/INTULocationManagerTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // INTULocationRequestTests.m 3 | // LocationManagerTests 4 | // 5 | // Copyright (c) 2014-2017 Intuit Inc. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining 8 | // a copy of this software and associated documentation files (the 9 | // "Software"), to deal in the Software without restriction, including 10 | // without limitation the rights to use, copy, modify, merge, publish, 11 | // distribute, sublicense, and/or sell copies of the Software, and to 12 | // permit persons to whom the Software is furnished to do so, subject to 13 | // the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | #import 28 | #import 29 | #import 30 | 31 | #import "INTULocationManager.h" 32 | 33 | @interface INTULocationManager (Spec) 34 | @property (nonatomic, strong) CLLocationManager *locationManager; 35 | @property (nonatomic, assign) BOOL isUpdatingHeading; 36 | @end 37 | 38 | SpecBegin(LocationManager) 39 | 40 | __block INTULocationManager *subject; 41 | __block CLLocation *location; 42 | __block CLHeading *mockHeading; 43 | 44 | before(^{ 45 | subject = [[INTULocationManager alloc] init]; 46 | subject.locationManager = OCMClassMock(CLLocationManager.class); 47 | 48 | location = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(1, 1) 49 | altitude:CLLocationDistanceMax 50 | horizontalAccuracy:kCLLocationAccuracyBest 51 | verticalAccuracy:kCLLocationAccuracyBest 52 | timestamp:[NSDate date]]; 53 | 54 | mockHeading = OCMClassMock(CLHeading.class); 55 | OCMStub([mockHeading magneticHeading]).andReturn(180.0); 56 | OCMStub([mockHeading trueHeading]).andReturn(180.0); 57 | OCMStub([mockHeading headingAccuracy]).andReturn(1.0); 58 | OCMStub([mockHeading timestamp]).andReturn([NSDate date]); 59 | }); 60 | 61 | describe(@"location services state", ^{ 62 | it(@"should indicate if location services are disabled", ^{ 63 | id classMock = OCMClassMock(CLLocationManager.class); 64 | OCMStub(ClassMethod([classMock locationServicesEnabled])).andReturn(NO); 65 | 66 | expect([INTULocationManager locationServicesState]).to.equal(INTULocationServicesStateDisabled); 67 | 68 | [classMock stopMocking]; 69 | }); 70 | 71 | it(@"should use the appropriate state based on authorization status", ^{ 72 | id classMock = OCMClassMock(CLLocationManager.class); 73 | OCMStub(ClassMethod([classMock locationServicesEnabled])).andReturn(YES); 74 | OCMStub(ClassMethod([classMock authorizationStatus])).andReturn(kCLAuthorizationStatusNotDetermined); 75 | 76 | expect([INTULocationManager locationServicesState]).to.equal(INTULocationServicesStateNotDetermined); 77 | 78 | [classMock stopMocking]; 79 | }); 80 | }); 81 | 82 | describe(@"heading services state", ^{ 83 | it(@"should indicate if heading services are available", ^{ 84 | id classMock = OCMClassMock(CLLocationManager.class); 85 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(YES); 86 | 87 | expect([INTULocationManager headingServicesState]).to.equal(INTUHeadingServicesStateAvailable); 88 | 89 | [classMock stopMocking]; 90 | }); 91 | it(@"should indicate if heading services are unavailable", ^{ 92 | id classMock = OCMClassMock(CLLocationManager.class); 93 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(NO); 94 | 95 | expect([INTULocationManager headingServicesState]).to.equal(INTUHeadingServicesStateUnavailable); 96 | 97 | [classMock stopMocking]; 98 | }); 99 | }); 100 | 101 | describe(@"forcing a request to complete", ^{ 102 | it(@"should immediately call the request's completion block", ^{ 103 | __block BOOL called = NO; 104 | INTULocationRequestID requestID = [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyCity timeout:10 block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 105 | called = YES; 106 | }]; 107 | [subject forceCompleteLocationRequest:requestID]; 108 | expect(called).will.beTruthy(); 109 | }); 110 | 111 | }); 112 | 113 | describe(@"After requesting a location", ^{ 114 | it(@"can be canceled", ^{ 115 | INTULocationRequestID requestID = [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:10 block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 116 | failure(@"was not canceled"); 117 | }]; 118 | 119 | [subject cancelLocationRequest:requestID]; 120 | 121 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 122 | }); 123 | }); 124 | 125 | describe(@"After subscribing for significant location changes", ^{ 126 | it(@"can be canceled", ^{ 127 | INTULocationRequestID requestID = [subject subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 128 | failure(@"was not canceled"); 129 | }]; 130 | 131 | [subject cancelLocationRequest:requestID]; 132 | 133 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 134 | }); 135 | }); 136 | 137 | describe(@"After subscribing to heading changes", ^{ 138 | it(@"can be canceled", ^{ 139 | id classMock = OCMClassMock(CLLocationManager.class); 140 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(YES); 141 | 142 | INTUHeadingRequestID requestID = [subject subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 143 | failure(@"was not canceled"); 144 | }]; 145 | 146 | [subject cancelHeadingRequest:requestID]; 147 | 148 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 149 | 150 | [classMock stopMocking]; 151 | }); 152 | 153 | it(@"should stop heading services when all requests are canceled", ^{ 154 | id classMock = OCMClassMock(CLLocationManager.class); 155 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(YES); 156 | 157 | INTUHeadingRequestID requestID = [subject subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 158 | // Do nothing with the update 159 | }]; 160 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 161 | 162 | expect(subject.isUpdatingHeading).to.beTruthy(); 163 | 164 | [subject cancelHeadingRequest:requestID]; 165 | 166 | expect(subject.isUpdatingHeading).to.beFalsy(); 167 | 168 | [classMock stopMocking]; 169 | }); 170 | }); 171 | 172 | describe(@"Timeouts", ^{ 173 | it(@"calls the request callback on location update after a timeout", ^{ 174 | __block NSInteger callbackCount = 0; 175 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.1 block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 176 | callbackCount++; 177 | }]; 178 | 179 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 180 | expect(callbackCount).will.equal(1); 181 | }); 182 | 183 | it(@"does not call the request callback on subsequent updates after timeout", ^{ 184 | __block NSInteger callbackCount = 0; 185 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.1 block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 186 | callbackCount++; 187 | }]; 188 | 189 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 190 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 191 | 192 | expect(callbackCount).will.equal(1); 193 | }); 194 | 195 | }); 196 | 197 | describe(@"subscribing for location updates with a block", ^{ 198 | it(@"calls the block on location update", ^{ 199 | __block BOOL called = NO; 200 | [subject subscribeToLocationUpdatesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 201 | called = YES; 202 | }]; 203 | 204 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 205 | 206 | waitUntil(^(DoneCallback done) { 207 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 208 | done(); 209 | }); 210 | }); 211 | 212 | expect(called).to.beTruthy(); 213 | }); 214 | 215 | it(@"passes the location", ^{ 216 | waitUntil(^(DoneCallback done) { 217 | [subject subscribeToLocationUpdatesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 218 | expect(currentLocation).to.equal(location); 219 | done(); 220 | }]; 221 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 222 | }); 223 | 224 | }); 225 | }); 226 | 227 | describe(@"subscribing for significant location changes with a block", ^{ 228 | it(@"calls the block on location change", ^{ 229 | __block BOOL called = NO; 230 | [subject subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 231 | called = YES; 232 | }]; 233 | 234 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 235 | 236 | waitUntil(^(DoneCallback done) { 237 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 238 | done(); 239 | }); 240 | }); 241 | 242 | expect(called).to.beTruthy(); 243 | }); 244 | 245 | it(@"passes the location", ^{ 246 | waitUntil(^(DoneCallback done) { 247 | [subject subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 248 | expect(currentLocation).to.equal(location); 249 | done(); 250 | }]; 251 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 252 | }); 253 | 254 | }); 255 | }); 256 | 257 | describe(@"subscribing to heading changes with a block", ^{ 258 | it(@"calls the block on heading change", ^{ 259 | id classMock = OCMClassMock(CLLocationManager.class); 260 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(YES); 261 | 262 | __block BOOL called = NO; 263 | [subject subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 264 | called = YES; 265 | }]; 266 | 267 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 268 | 269 | waitUntil(^(DoneCallback done) { 270 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 271 | done(); 272 | }); 273 | }); 274 | 275 | expect(called).to.beTruthy(); 276 | 277 | [classMock stopMocking]; 278 | }); 279 | 280 | it(@"passes the heading", ^{ 281 | id classMock = OCMClassMock(CLLocationManager.class); 282 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(YES); 283 | 284 | __block INTUHeadingStatus requestStatus = NSIntegerMax; 285 | [subject subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 286 | expect(heading).to.equal(heading); 287 | requestStatus = status; 288 | }]; 289 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 290 | 291 | waitUntil(^(DoneCallback done) { 292 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 293 | done(); 294 | }); 295 | }); 296 | 297 | expect(requestStatus).to.equal(INTUHeadingStatusSuccess); 298 | 299 | [classMock stopMocking]; 300 | }); 301 | }); 302 | 303 | describe(@"when you want to wait for user auth", ^{ 304 | it(@"doesnt send a callback until it is agreed", ^{ 305 | __block NSInteger callbackCount = 0; 306 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 307 | callbackCount++; 308 | }]; 309 | 310 | id classMock = OCMClassMock(CLLocationManager.class); 311 | OCMStub(ClassMethod([classMock authorizationStatus])).andReturn(kCLAuthorizationStatusNotDetermined); 312 | 313 | 314 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 315 | expect(callbackCount).to.equal(0); 316 | 317 | OCMStub(ClassMethod([classMock authorizationStatus])).andReturn (kCLAuthorizationStatusAuthorizedAlways); 318 | [subject locationManager:subject.locationManager didChangeAuthorizationStatus:kCLAuthorizationStatusAuthorizedAlways]; 319 | 320 | expect(callbackCount).will.equal(1); 321 | }); 322 | }); 323 | 324 | describe(@"authorization status changes", ^{ 325 | it(@"clears out pending requests when it is denied", ^{ 326 | __block NSInteger callbackCount = 0; 327 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 328 | callbackCount++; 329 | }]; 330 | 331 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 332 | expect(callbackCount).to.equal(0); 333 | 334 | [subject locationManager:subject.locationManager didChangeAuthorizationStatus:kCLAuthorizationStatusDenied]; 335 | 336 | expect(callbackCount).will.equal(1); 337 | }); 338 | 339 | it(@"clears out pending requests when restricted", ^{ 340 | __block NSInteger callbackCount = 0; 341 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 342 | callbackCount++; 343 | }]; 344 | 345 | [subject locationManager:subject.locationManager didUpdateLocations:@[location]]; 346 | expect(callbackCount).to.equal(0); 347 | 348 | [subject locationManager:subject.locationManager didChangeAuthorizationStatus:kCLAuthorizationStatusRestricted]; 349 | 350 | expect(callbackCount).will.equal(1); 351 | }); 352 | }); 353 | 354 | describe(@"when the location manager fails", ^{ 355 | it(@"should complete all non-recurring requests", ^{ 356 | __block NSInteger singleCallbackCount = 0; 357 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 358 | singleCallbackCount++; 359 | }]; 360 | 361 | NSError *error = [NSError errorWithDomain:@"domain" code:1337 userInfo:@{}]; 362 | [subject locationManager:subject.locationManager didFailWithError:error]; 363 | 364 | waitUntil(^(DoneCallback done) { 365 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 366 | done(); 367 | }); 368 | }); 369 | 370 | expect(singleCallbackCount).to.equal(1); 371 | 372 | // Fail it a second time and ensure it doesn't get called a second time 373 | [subject locationManager:subject.locationManager didFailWithError:error]; 374 | 375 | waitUntil(^(DoneCallback done) { 376 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 377 | done(); 378 | }); 379 | }); 380 | 381 | expect(singleCallbackCount).to.equal(1); 382 | }); 383 | 384 | it(@"should keep alive all recurring requests", ^{ 385 | __block NSInteger recurringCallbackCount = 0; 386 | [subject subscribeToLocationUpdatesWithDesiredAccuracy:INTULocationAccuracyBlock block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 387 | recurringCallbackCount++; 388 | }]; 389 | 390 | NSError *error = [NSError errorWithDomain:@"domain" code:1337 userInfo:@{}]; 391 | [subject locationManager:subject.locationManager didFailWithError:error]; 392 | 393 | waitUntil(^(DoneCallback done) { 394 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 395 | done(); 396 | }); 397 | }); 398 | 399 | expect(recurringCallbackCount).to.equal(1); 400 | 401 | // Fail it a second time and see if it's still called 402 | [subject locationManager:subject.locationManager didFailWithError:error]; 403 | 404 | waitUntil(^(DoneCallback done) { 405 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 406 | done(); 407 | }); 408 | }); 409 | 410 | expect(recurringCallbackCount).to.equal(2); 411 | }); 412 | }); 413 | 414 | describe(@"when the device doesn't support heading", ^{ 415 | it(@"should cancel it when support becomes unavailable", ^{ 416 | id classMock = OCMClassMock(CLLocationManager.class); 417 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(YES); 418 | 419 | __block BOOL called = NO; 420 | __block INTUHeadingStatus requestStatus = NSIntegerMax; 421 | 422 | [subject subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 423 | called = YES; 424 | requestStatus = status; 425 | }]; 426 | 427 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 428 | 429 | waitUntil(^(DoneCallback done) { 430 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 431 | done(); 432 | }); 433 | }); 434 | 435 | expect(subject.isUpdatingHeading).to.beTruthy(); 436 | expect(called).to.beTruthy(); 437 | expect(requestStatus).to.equal(INTUHeadingStatusSuccess); 438 | 439 | [classMock stopMocking]; 440 | 441 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(NO); 442 | 443 | // Reset and call it again to see if it gave correct status 444 | called = NO; 445 | requestStatus = NSIntegerMax; 446 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 447 | 448 | waitUntil(^(DoneCallback done) { 449 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 450 | done(); 451 | }); 452 | }); 453 | 454 | expect(called).to.beTruthy(); 455 | expect(requestStatus).to.equal(INTUHeadingStatusUnavailable); 456 | 457 | // Reset and call it again to see if it was canceled 458 | called = NO; 459 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 460 | 461 | waitUntil(^(DoneCallback done) { 462 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 463 | done(); 464 | }); 465 | }); 466 | 467 | expect(called).to.beFalsy(); 468 | 469 | // Ensure all heading services are stopped 470 | expect(subject.isUpdatingHeading).to.beFalsy(); 471 | 472 | [classMock stopMocking]; 473 | }); 474 | 475 | it(@"should cancel a new heading subscription before it can even start", ^{ 476 | id classMock = OCMClassMock(CLLocationManager.class); 477 | OCMStub(ClassMethod([classMock headingAvailable])).andReturn(NO); 478 | 479 | __block BOOL called = NO; 480 | __block INTUHeadingStatus requestStatus; 481 | [subject subscribeToHeadingUpdatesWithBlock:^(CLHeading *heading, INTUHeadingStatus status) { 482 | called = YES; 483 | requestStatus = status; 484 | }]; 485 | 486 | [subject locationManager:subject.locationManager didUpdateHeading:mockHeading]; 487 | 488 | waitUntil(^(DoneCallback done) { 489 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 490 | done(); 491 | }); 492 | }); 493 | 494 | expect(called).to.beTruthy(); 495 | expect(requestStatus).to.equal(INTUHeadingStatusUnavailable); 496 | 497 | [classMock stopMocking]; 498 | }); 499 | }); 500 | 501 | describe(@"multiple simultaneous location requests", ^{ 502 | it(@"calls each request block correctly", ^{ 503 | id classMock = OCMClassMock(CLLocationManager.class); 504 | OCMStub(ClassMethod([classMock locationServicesEnabled])).andReturn(YES); 505 | OCMStub(ClassMethod([classMock authorizationStatus])).andReturn(kCLAuthorizationStatusAuthorizedWhenInUse); 506 | 507 | __block NSInteger singleCallbackACount = 0; 508 | __block NSInteger singleCallbackASuccessCount = 0; 509 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyRoom timeout:0.0 block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 510 | singleCallbackACount++; 511 | if (status == INTULocationStatusSuccess) { 512 | singleCallbackASuccessCount++; 513 | } 514 | }]; 515 | 516 | __block NSInteger singleCallbackBCount = 0; 517 | __block NSInteger singleCallbackBSuccessCount = 0; 518 | [subject requestLocationWithDesiredAccuracy:INTULocationAccuracyBlock timeout:0.0 block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 519 | singleCallbackBCount++; 520 | if (status == INTULocationStatusSuccess) { 521 | singleCallbackBSuccessCount++; 522 | } 523 | }]; 524 | 525 | __block NSInteger subscriptionCallbackCount = 0; 526 | [subject subscribeToLocationUpdatesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 527 | subscriptionCallbackCount++; 528 | }]; 529 | 530 | __block NSInteger significantChangesCallbackCount = 0; 531 | [subject subscribeToSignificantLocationChangesWithBlock:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) { 532 | significantChangesCallbackCount++; 533 | }]; 534 | 535 | 536 | CLLocation *inaccurateLocation = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(1, 1) 537 | altitude:CLLocationDistanceMax 538 | horizontalAccuracy:2000.0 539 | verticalAccuracy:2000.0 540 | timestamp:[[NSDate date] dateByAddingTimeInterval:-3600]]; 541 | [subject locationManager:subject.locationManager didUpdateLocations:@[inaccurateLocation]]; 542 | 543 | waitUntil(^(DoneCallback done) { 544 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 545 | done(); 546 | }); 547 | }); 548 | 549 | expect(singleCallbackACount).to.equal(0); 550 | expect(singleCallbackASuccessCount).to.equal(0); 551 | expect(singleCallbackBCount).to.equal(0); 552 | expect(singleCallbackBSuccessCount).to.equal(0); 553 | expect(subscriptionCallbackCount).to.equal(1); 554 | expect(significantChangesCallbackCount).to.equal(1); 555 | 556 | CLLocation *somewhatAccurateLocation = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(1, 1) 557 | altitude:CLLocationDistanceMax 558 | horizontalAccuracy:50.0 559 | verticalAccuracy:5.0 560 | timestamp:[NSDate date]]; 561 | [subject locationManager:subject.locationManager didUpdateLocations:@[somewhatAccurateLocation]]; 562 | 563 | waitUntil(^(DoneCallback done) { 564 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 565 | done(); 566 | }); 567 | }); 568 | 569 | expect(singleCallbackACount).to.equal(0); 570 | expect(singleCallbackASuccessCount).to.equal(0); 571 | expect(singleCallbackBCount).to.equal(1); 572 | expect(singleCallbackBSuccessCount).to.equal(1); 573 | expect(subscriptionCallbackCount).to.equal(2); 574 | expect(significantChangesCallbackCount).to.equal(2); 575 | 576 | CLLocation *veryAccurateLocation = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(1, 1) 577 | altitude:CLLocationDistanceMax 578 | horizontalAccuracy:5.0 579 | verticalAccuracy:5.0 580 | timestamp:[NSDate date]]; 581 | [subject locationManager:subject.locationManager didUpdateLocations:@[veryAccurateLocation]]; 582 | 583 | waitUntil(^(DoneCallback done) { 584 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 585 | done(); 586 | }); 587 | }); 588 | 589 | expect(singleCallbackACount).to.equal(1); 590 | expect(singleCallbackASuccessCount).to.equal(1); 591 | expect(singleCallbackBCount).to.equal(1); 592 | expect(singleCallbackBSuccessCount).to.equal(1); 593 | expect(subscriptionCallbackCount).to.equal(3); 594 | expect(significantChangesCallbackCount).to.equal(3); 595 | 596 | CLLocation *anotherVeryAccurateLocation = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(1, 1) 597 | altitude:CLLocationDistanceMax 598 | horizontalAccuracy:5.0 599 | verticalAccuracy:5.0 600 | timestamp:[NSDate date]]; 601 | [subject locationManager:subject.locationManager didUpdateLocations:@[anotherVeryAccurateLocation]]; 602 | 603 | waitUntil(^(DoneCallback done) { 604 | dispatch_after(0.5, dispatch_get_main_queue(), ^{ 605 | done(); 606 | }); 607 | }); 608 | 609 | expect(singleCallbackACount).to.equal(1); 610 | expect(singleCallbackASuccessCount).to.equal(1); 611 | expect(singleCallbackBCount).to.equal(1); 612 | expect(singleCallbackBSuccessCount).to.equal(1); 613 | expect(subscriptionCallbackCount).to.equal(4); 614 | expect(significantChangesCallbackCount).to.equal(4); 615 | 616 | [classMock stopMocking]; 617 | }); 618 | }); 619 | 620 | xdescribe(@"when determining whether a location update fulfills a request", ^{ 621 | // The logic comparing a request's desired accuracy to the CLLocation's properties 622 | // (all the stuff regarding staleness + horizontal location accuracy threshold) 623 | // should be tested, but it seems complex enough we figured we were better off letting 624 | // someone with more domain knowledge of how it works handle it. 625 | // 626 | // You should write these tests! 627 | }); 628 | 629 | SpecEnd 630 | --------------------------------------------------------------------------------