├── 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 |
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 | # [](#)
2 | [](https://travis-ci.org/intuit/LocationManager) [](https://coveralls.io/r/intuit/LocationManager) [](http://cocoapods.org/pods/INTULocationManager) [](http://cocoapods.org/pods/INTULocationManager) [](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 |
119 |
120 |
121 |
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 |
--------------------------------------------------------------------------------