├── .gitignore ├── CHANGELOG.md ├── FSQLocationBroker.podspec ├── FSQLocationBroker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── FSQLocationBroker-AppExtension.xcscheme │ └── FSQLocationBroker.xcscheme ├── FSQLocationBroker ├── FSQLocationBroker.h ├── FSQLocationBroker.m ├── FSQLocationBroker.modulemap ├── FSQSingleLocationSubscriber.h ├── FSQSingleLocationSubscriber.m ├── Info.plist └── module_appextension.modulemap ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | build/* 4 | build_products 5 | 6 | *.pbxuser 7 | !default.pbxuser 8 | 9 | *.mode1v3 10 | !default.mode1v3 11 | 12 | *.mode2v3 13 | !default.mode2v3 14 | 15 | *.perspectivev3 16 | !default.perspectivev3 17 | 18 | xcuserdata 19 | profile 20 | *.moved-aside 21 | *.xccheckout 22 | 23 | # finder 24 | .DS_Store 25 | 26 | Carthage -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.3 (2017-06-21) 2 | 3 | Features: 4 | 5 | - Fix bug where a visit subscriber refresh turns off visit monitoring if it is already on. 6 | 7 | ## 1.3.2 (2017-04-18) 8 | 9 | Features: 10 | 11 | - Fix modulemap bug for Xcode 8 builds. 12 | 13 | ## 1.3.1 (2016-05-19) 14 | 15 | Features: 16 | 17 | - Fix bug where a subscriber running in the background could fire its completion block multiple times. 18 | - Fix bug where the completion block might not return a location if a location with the maximum desired accuracy wasn't found. 19 | 20 | ## 1.3.0 (2015-10-27) 21 | 22 | Features: 23 | 24 | - Added Carthage/framework support. There is now an Xcode project that has schemes to build two frameworks (one for regular apps, one that supports only app-extension apis). The recommended way to use FSQLocationBroker is now to add these frameworks to your project by adding the repo to your Cartfile with Carthage. 25 | - `FSQSingleLocationSubscriber` now calls `stopListening` before executing callback blocks instead of after, so that its state will be correct if you check it from those callbacks. 26 | - The `currentLocation` property is now atomic instead of nonatomic. 27 | 28 | 29 | ## 1.2.0 (2015-09-10) 30 | 31 | Features: 32 | 33 | - Background location updates are now automatically disabled if your bundle does not have the correct background mode or if the user has only granted you "When in Use" permissions. 34 | - Improved handling of iOS 9's `allowsBackgroundLocationUpdates` property. 35 | - Changes to `FSQVisitMonitoringSubscriber`'s `shouldMonitorVisits` property are now monitored through KVO. 36 | - For performance reasons, the broker now caches its own copy of `currentLocation` instead of passing through to the property on `CLLocationManager`. 37 | 38 | Bugfixes: 39 | 40 | - Fixes crash when `allowsBackgroundLocationUpdates` was incorrectly enabled in an app without the correct background mode. 41 | 42 | 43 | ## 1.1.2 (2015-07-06) 44 | 45 | Features: 46 | 47 | - Add support for background location in iOS9 48 | 49 | Bugfixes: 50 | 51 | - Many changes to how region subscribers work to fix issues. Re-connecting regions subscribers to monitored regions from previous app launches should now work as expected. 52 | 53 | ## 1.1.1 (2015-04-07) 54 | 55 | Bugfixes: 56 | 57 | - This fixes a critical bug when building with newer versions of clang (Xcode 6.2+) where the broker would always think that the app was not backgrounded. 58 | - You must now define the FSQ_IS_APP_EXTENSION when compiling an extension in order to have unavailable apis compiled out. 59 | 60 | ## 1.1.0 (2015-04-02) 61 | 62 | Features: 63 | 64 | - Add support for CLVisit. 65 | - Add changelog. 66 | 67 | Bugfixes: 68 | 69 | - CLLocationManger methods are now correctly always called on main thread. 70 | - Guard against compiling in uses of UIApplication for extensions. 71 | 72 | ## 1.0.3 (2014-09-15) 73 | 74 | Features: 75 | 76 | - Support for iOS 8. 77 | 78 | ## 1.0.2 (2014-09-02) 79 | 80 | Bugfixes: 81 | 82 | - Add .gitignore and remove files which should have been ignored. 83 | 84 | ## 1.0.1 (2014-08-04) 85 | 86 | Bugfixes: 87 | 88 | - Compile out NSAssert and related code if asserts are disabled. 89 | 90 | ## 1.0.0 (2014-06-03) 91 | 92 | Initial release. 93 | -------------------------------------------------------------------------------- /FSQLocationBroker.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'FSQLocationBroker' 3 | s.version = '1.4.3' 4 | s.platform = :ios 5 | s.summary = 'A centralized location manager for your app' 6 | s.homepage = 'https://github.com/foursquare/FSQLocationBroker' 7 | s.license = { :type => 'Apache', :file => 'LICENSE.txt' } 8 | s.authors = { 'Brian Dorfman' => 'https://twitter.com/bdorfman', 9 | 'Cameron Mulhern' => 'http://www.cameronmulhern.com', 10 | 'Adam Alix' => 'https://twitter.com/adamalix', 11 | 'Anoop Ranganath' => 'https://twitter.com/anoopr', 12 | 'Mitchell Livingston' => 'https://twitter.com/livings124', 13 | 'Eric Bueno' => 'https://twitter.com/sneakybueno' } 14 | s.source = { :git => 'https://github.com/foursquare/FSQLocationBroker.git', 15 | :tag => "v#{s.version}" } 16 | s.source_files = 'FSQLocationBroker/*.{h,m}' 17 | s.frameworks = 'CoreLocation' 18 | s.requires_arc = true 19 | end 20 | -------------------------------------------------------------------------------- /FSQLocationBroker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F14AD59A1BE00CD600D59271 /* FSQLocationBroker.h in Headers */ = {isa = PBXBuildFile; fileRef = F14AD5941BE00CD600D59271 /* FSQLocationBroker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | F14AD59B1BE00CD600D59271 /* FSQLocationBroker.m in Sources */ = {isa = PBXBuildFile; fileRef = F14AD5951BE00CD600D59271 /* FSQLocationBroker.m */; }; 12 | F14AD59C1BE00CD600D59271 /* FSQSingleLocationSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = F14AD5961BE00CD600D59271 /* FSQSingleLocationSubscriber.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | F14AD59D1BE00CD600D59271 /* FSQSingleLocationSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = F14AD5971BE00CD600D59271 /* FSQSingleLocationSubscriber.m */; }; 14 | F14AD5A01BE00DA500D59271 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F14AD59F1BE00DA500D59271 /* UIKit.framework */; }; 15 | F14AD5AF1BE027FE00D59271 /* FSQLocationBroker.m in Sources */ = {isa = PBXBuildFile; fileRef = F14AD5951BE00CD600D59271 /* FSQLocationBroker.m */; }; 16 | F14AD5B01BE027FE00D59271 /* FSQSingleLocationSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = F14AD5971BE00CD600D59271 /* FSQSingleLocationSubscriber.m */; }; 17 | F14AD5B21BE027FE00D59271 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F14AD59F1BE00DA500D59271 /* UIKit.framework */; }; 18 | F14AD5B31BE027FE00D59271 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F18199B91BE00C59009DB90D /* Foundation.framework */; }; 19 | F14AD5B41BE027FE00D59271 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F18199B71BE00C54009DB90D /* CoreLocation.framework */; }; 20 | F14AD5B61BE027FE00D59271 /* FSQSingleLocationSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = F14AD5961BE00CD600D59271 /* FSQSingleLocationSubscriber.h */; settings = {ATTRIBUTES = (Public, ); }; }; 21 | F14AD5B71BE027FE00D59271 /* FSQLocationBroker.h in Headers */ = {isa = PBXBuildFile; fileRef = F14AD5941BE00CD600D59271 /* FSQLocationBroker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 22 | F18199B81BE00C54009DB90D /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F18199B71BE00C54009DB90D /* CoreLocation.framework */; }; 23 | F18199BA1BE00C59009DB90D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F18199B91BE00C59009DB90D /* Foundation.framework */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | F14AD5941BE00CD600D59271 /* FSQLocationBroker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSQLocationBroker.h; sourceTree = ""; }; 28 | F14AD5951BE00CD600D59271 /* FSQLocationBroker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSQLocationBroker.m; sourceTree = ""; }; 29 | F14AD5961BE00CD600D59271 /* FSQSingleLocationSubscriber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSQSingleLocationSubscriber.h; sourceTree = ""; }; 30 | F14AD5971BE00CD600D59271 /* FSQSingleLocationSubscriber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSQSingleLocationSubscriber.m; sourceTree = ""; }; 31 | F14AD5981BE00CD600D59271 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | F14AD5991BE00CD600D59271 /* FSQLocationBroker.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = FSQLocationBroker.modulemap; sourceTree = ""; }; 33 | F14AD59F1BE00DA500D59271 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 34 | F14AD5BC1BE027FE00D59271 /* FSQLocationBroker_AppExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FSQLocationBroker_AppExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | F18199A31BE0052E009DB90D /* FSQLocationBroker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FSQLocationBroker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | F18199B71BE00C54009DB90D /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; 37 | F18199B91BE00C59009DB90D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 38 | F1E365961BE13C15003BF022 /* module_appextension.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module_appextension.modulemap; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | F14AD5B11BE027FE00D59271 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | F14AD5B21BE027FE00D59271 /* UIKit.framework in Frameworks */, 47 | F14AD5B31BE027FE00D59271 /* Foundation.framework in Frameworks */, 48 | F14AD5B41BE027FE00D59271 /* CoreLocation.framework in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | F181999F1BE0052E009DB90D /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | F14AD5A01BE00DA500D59271 /* UIKit.framework in Frameworks */, 57 | F18199BA1BE00C59009DB90D /* Foundation.framework in Frameworks */, 58 | F18199B81BE00C54009DB90D /* CoreLocation.framework in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | F18199991BE0052E009DB90D = { 66 | isa = PBXGroup; 67 | children = ( 68 | F18199A51BE0052E009DB90D /* FSQLocationBroker */, 69 | F18199A41BE0052E009DB90D /* Products */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | F18199A41BE0052E009DB90D /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | F18199A31BE0052E009DB90D /* FSQLocationBroker.framework */, 77 | F14AD5BC1BE027FE00D59271 /* FSQLocationBroker_AppExtension.framework */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | F18199A51BE0052E009DB90D /* FSQLocationBroker */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | F14AD5941BE00CD600D59271 /* FSQLocationBroker.h */, 86 | F14AD5951BE00CD600D59271 /* FSQLocationBroker.m */, 87 | F14AD5961BE00CD600D59271 /* FSQSingleLocationSubscriber.h */, 88 | F14AD5971BE00CD600D59271 /* FSQSingleLocationSubscriber.m */, 89 | F14AD5981BE00CD600D59271 /* Info.plist */, 90 | F14AD5991BE00CD600D59271 /* FSQLocationBroker.modulemap */, 91 | F1E365961BE13C15003BF022 /* module_appextension.modulemap */, 92 | F18199B71BE00C54009DB90D /* CoreLocation.framework */, 93 | F18199B91BE00C59009DB90D /* Foundation.framework */, 94 | F14AD59F1BE00DA500D59271 /* UIKit.framework */, 95 | ); 96 | path = FSQLocationBroker; 97 | sourceTree = ""; 98 | }; 99 | /* End PBXGroup section */ 100 | 101 | /* Begin PBXHeadersBuildPhase section */ 102 | F14AD5B51BE027FE00D59271 /* Headers */ = { 103 | isa = PBXHeadersBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | F14AD5B61BE027FE00D59271 /* FSQSingleLocationSubscriber.h in Headers */, 107 | F14AD5B71BE027FE00D59271 /* FSQLocationBroker.h in Headers */, 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | F18199A01BE0052E009DB90D /* Headers */ = { 112 | isa = PBXHeadersBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | F14AD59C1BE00CD600D59271 /* FSQSingleLocationSubscriber.h in Headers */, 116 | F14AD59A1BE00CD600D59271 /* FSQLocationBroker.h in Headers */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXHeadersBuildPhase section */ 121 | 122 | /* Begin PBXNativeTarget section */ 123 | F14AD5AD1BE027FE00D59271 /* FSQLocationBroker_AppExtension */ = { 124 | isa = PBXNativeTarget; 125 | buildConfigurationList = F14AD5B91BE027FE00D59271 /* Build configuration list for PBXNativeTarget "FSQLocationBroker_AppExtension" */; 126 | buildPhases = ( 127 | F14AD5AE1BE027FE00D59271 /* Sources */, 128 | F14AD5B11BE027FE00D59271 /* Frameworks */, 129 | F14AD5B51BE027FE00D59271 /* Headers */, 130 | F14AD5B81BE027FE00D59271 /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = FSQLocationBroker_AppExtension; 137 | productName = FSQLocationBroker; 138 | productReference = F14AD5BC1BE027FE00D59271 /* FSQLocationBroker_AppExtension.framework */; 139 | productType = "com.apple.product-type.framework"; 140 | }; 141 | F18199A21BE0052E009DB90D /* FSQLocationBroker */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = F18199AB1BE0052E009DB90D /* Build configuration list for PBXNativeTarget "FSQLocationBroker" */; 144 | buildPhases = ( 145 | F181999E1BE0052E009DB90D /* Sources */, 146 | F181999F1BE0052E009DB90D /* Frameworks */, 147 | F18199A01BE0052E009DB90D /* Headers */, 148 | F18199A11BE0052E009DB90D /* Resources */, 149 | ); 150 | buildRules = ( 151 | ); 152 | dependencies = ( 153 | ); 154 | name = FSQLocationBroker; 155 | productName = FSQLocationBroker; 156 | productReference = F18199A31BE0052E009DB90D /* FSQLocationBroker.framework */; 157 | productType = "com.apple.product-type.framework"; 158 | }; 159 | /* End PBXNativeTarget section */ 160 | 161 | /* Begin PBXProject section */ 162 | F181999A1BE0052E009DB90D /* Project object */ = { 163 | isa = PBXProject; 164 | attributes = { 165 | LastUpgradeCheck = 0920; 166 | ORGANIZATIONNAME = Foursquare; 167 | TargetAttributes = { 168 | F18199A21BE0052E009DB90D = { 169 | CreatedOnToolsVersion = 7.1; 170 | }; 171 | }; 172 | }; 173 | buildConfigurationList = F181999D1BE0052E009DB90D /* Build configuration list for PBXProject "FSQLocationBroker" */; 174 | compatibilityVersion = "Xcode 3.2"; 175 | developmentRegion = English; 176 | hasScannedForEncodings = 0; 177 | knownRegions = ( 178 | English, 179 | en, 180 | ); 181 | mainGroup = F18199991BE0052E009DB90D; 182 | productRefGroup = F18199A41BE0052E009DB90D /* Products */; 183 | projectDirPath = ""; 184 | projectRoot = ""; 185 | targets = ( 186 | F18199A21BE0052E009DB90D /* FSQLocationBroker */, 187 | F14AD5AD1BE027FE00D59271 /* FSQLocationBroker_AppExtension */, 188 | ); 189 | }; 190 | /* End PBXProject section */ 191 | 192 | /* Begin PBXResourcesBuildPhase section */ 193 | F14AD5B81BE027FE00D59271 /* Resources */ = { 194 | isa = PBXResourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | F18199A11BE0052E009DB90D /* Resources */ = { 201 | isa = PBXResourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | ); 205 | runOnlyForDeploymentPostprocessing = 0; 206 | }; 207 | /* End PBXResourcesBuildPhase section */ 208 | 209 | /* Begin PBXSourcesBuildPhase section */ 210 | F14AD5AE1BE027FE00D59271 /* Sources */ = { 211 | isa = PBXSourcesBuildPhase; 212 | buildActionMask = 2147483647; 213 | files = ( 214 | F14AD5AF1BE027FE00D59271 /* FSQLocationBroker.m in Sources */, 215 | F14AD5B01BE027FE00D59271 /* FSQSingleLocationSubscriber.m in Sources */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | F181999E1BE0052E009DB90D /* Sources */ = { 220 | isa = PBXSourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | F14AD59B1BE00CD600D59271 /* FSQLocationBroker.m in Sources */, 224 | F14AD59D1BE00CD600D59271 /* FSQSingleLocationSubscriber.m in Sources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXSourcesBuildPhase section */ 229 | 230 | /* Begin XCBuildConfiguration section */ 231 | F14AD5BA1BE027FE00D59271 /* Debug */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | APPLICATION_EXTENSION_API_ONLY = YES; 235 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 236 | DEFINES_MODULE = YES; 237 | DYLIB_COMPATIBILITY_VERSION = 1; 238 | DYLIB_CURRENT_VERSION = 1; 239 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 240 | ENABLE_BITCODE = YES; 241 | GCC_PREPROCESSOR_DEFINITIONS = ( 242 | "$(inherited)", 243 | "FSQ_IS_APP_EXTENSION=1", 244 | ); 245 | INFOPLIST_FILE = FSQLocationBroker/Info.plist; 246 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 247 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 248 | MODULEMAP_FILE = FSQLocationBroker/module_appextension.modulemap; 249 | PRODUCT_BUNDLE_IDENTIFIER = com.foursquare.FSQLocationBrokerAppExtension; 250 | PRODUCT_NAME = "$(TARGET_NAME)"; 251 | SKIP_INSTALL = YES; 252 | }; 253 | name = Debug; 254 | }; 255 | F14AD5BB1BE027FE00D59271 /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | APPLICATION_EXTENSION_API_ONLY = YES; 259 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 260 | DEFINES_MODULE = YES; 261 | DYLIB_COMPATIBILITY_VERSION = 1; 262 | DYLIB_CURRENT_VERSION = 1; 263 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 264 | ENABLE_BITCODE = YES; 265 | GCC_PREPROCESSOR_DEFINITIONS = ( 266 | "$(inherited)", 267 | "FSQ_IS_APP_EXTENSION=1", 268 | ); 269 | INFOPLIST_FILE = FSQLocationBroker/Info.plist; 270 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 271 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 272 | MODULEMAP_FILE = FSQLocationBroker/module_appextension.modulemap; 273 | PRODUCT_BUNDLE_IDENTIFIER = com.foursquare.FSQLocationBrokerAppExtension; 274 | PRODUCT_NAME = "$(TARGET_NAME)"; 275 | SKIP_INSTALL = YES; 276 | }; 277 | name = Release; 278 | }; 279 | F18199A91BE0052E009DB90D /* Debug */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | ALWAYS_SEARCH_USER_PATHS = NO; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 284 | CLANG_CXX_LIBRARY = "libc++"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_EMPTY_BODY = YES; 294 | CLANG_WARN_ENUM_CONVERSION = YES; 295 | CLANG_WARN_INFINITE_RECURSION = YES; 296 | CLANG_WARN_INT_CONVERSION = YES; 297 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 299 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 300 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 301 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 302 | CLANG_WARN_STRICT_PROTOTYPES = YES; 303 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 304 | CLANG_WARN_UNREACHABLE_CODE = YES; 305 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 306 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 307 | COPY_PHASE_STRIP = NO; 308 | CURRENT_PROJECT_VERSION = 1; 309 | DEBUG_INFORMATION_FORMAT = dwarf; 310 | ENABLE_STRICT_OBJC_MSGSEND = YES; 311 | ENABLE_TESTABILITY = YES; 312 | GCC_C_LANGUAGE_STANDARD = gnu99; 313 | GCC_DYNAMIC_NO_PIC = NO; 314 | GCC_NO_COMMON_BLOCKS = YES; 315 | GCC_OPTIMIZATION_LEVEL = 0; 316 | GCC_PREPROCESSOR_DEFINITIONS = ( 317 | "$(inherited)", 318 | "DEBUG=1", 319 | ); 320 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 321 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 322 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 323 | GCC_WARN_UNDECLARED_SELECTOR = YES; 324 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 325 | GCC_WARN_UNUSED_FUNCTION = YES; 326 | GCC_WARN_UNUSED_VARIABLE = YES; 327 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 328 | MODULEMAP_FILE = FSQLocationBroker/module.modulemap; 329 | MTL_ENABLE_DEBUG_INFO = YES; 330 | ONLY_ACTIVE_ARCH = YES; 331 | SDKROOT = iphoneos; 332 | TARGETED_DEVICE_FAMILY = "1,2"; 333 | VERSIONING_SYSTEM = "apple-generic"; 334 | VERSION_INFO_PREFIX = ""; 335 | }; 336 | name = Debug; 337 | }; 338 | F18199AA1BE0052E009DB90D /* Release */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ALWAYS_SEARCH_USER_PATHS = NO; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 343 | CLANG_CXX_LIBRARY = "libc++"; 344 | CLANG_ENABLE_MODULES = YES; 345 | CLANG_ENABLE_OBJC_ARC = YES; 346 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 347 | CLANG_WARN_BOOL_CONVERSION = YES; 348 | CLANG_WARN_COMMA = YES; 349 | CLANG_WARN_CONSTANT_CONVERSION = YES; 350 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 351 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 352 | CLANG_WARN_EMPTY_BODY = YES; 353 | CLANG_WARN_ENUM_CONVERSION = YES; 354 | CLANG_WARN_INFINITE_RECURSION = YES; 355 | CLANG_WARN_INT_CONVERSION = YES; 356 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 357 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 358 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 360 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 361 | CLANG_WARN_STRICT_PROTOTYPES = YES; 362 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 363 | CLANG_WARN_UNREACHABLE_CODE = YES; 364 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 365 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 366 | COPY_PHASE_STRIP = NO; 367 | CURRENT_PROJECT_VERSION = 1; 368 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 369 | ENABLE_NS_ASSERTIONS = NO; 370 | ENABLE_STRICT_OBJC_MSGSEND = YES; 371 | GCC_C_LANGUAGE_STANDARD = gnu99; 372 | GCC_NO_COMMON_BLOCKS = YES; 373 | GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; 374 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 375 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 376 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 377 | GCC_WARN_UNDECLARED_SELECTOR = YES; 378 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 379 | GCC_WARN_UNUSED_FUNCTION = YES; 380 | GCC_WARN_UNUSED_VARIABLE = YES; 381 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 382 | MODULEMAP_FILE = FSQLocationBroker/module.modulemap; 383 | MTL_ENABLE_DEBUG_INFO = NO; 384 | SDKROOT = iphoneos; 385 | TARGETED_DEVICE_FAMILY = "1,2"; 386 | VALIDATE_PRODUCT = YES; 387 | VERSIONING_SYSTEM = "apple-generic"; 388 | VERSION_INFO_PREFIX = ""; 389 | }; 390 | name = Release; 391 | }; 392 | F18199AC1BE0052E009DB90D /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 396 | DEFINES_MODULE = YES; 397 | DYLIB_COMPATIBILITY_VERSION = 1; 398 | DYLIB_CURRENT_VERSION = 1; 399 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 400 | ENABLE_BITCODE = YES; 401 | INFOPLIST_FILE = FSQLocationBroker/Info.plist; 402 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 404 | MODULEMAP_FILE = FSQLocationBroker/FSQLocationBroker.modulemap; 405 | PRODUCT_BUNDLE_IDENTIFIER = com.foursquare.FSQLocationBroker; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SKIP_INSTALL = YES; 408 | }; 409 | name = Debug; 410 | }; 411 | F18199AD1BE0052E009DB90D /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 415 | DEFINES_MODULE = YES; 416 | DYLIB_COMPATIBILITY_VERSION = 1; 417 | DYLIB_CURRENT_VERSION = 1; 418 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 419 | ENABLE_BITCODE = YES; 420 | INFOPLIST_FILE = FSQLocationBroker/Info.plist; 421 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 422 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 423 | MODULEMAP_FILE = FSQLocationBroker/FSQLocationBroker.modulemap; 424 | PRODUCT_BUNDLE_IDENTIFIER = com.foursquare.FSQLocationBroker; 425 | PRODUCT_NAME = "$(TARGET_NAME)"; 426 | SKIP_INSTALL = YES; 427 | }; 428 | name = Release; 429 | }; 430 | /* End XCBuildConfiguration section */ 431 | 432 | /* Begin XCConfigurationList section */ 433 | F14AD5B91BE027FE00D59271 /* Build configuration list for PBXNativeTarget "FSQLocationBroker_AppExtension" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | F14AD5BA1BE027FE00D59271 /* Debug */, 437 | F14AD5BB1BE027FE00D59271 /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | F181999D1BE0052E009DB90D /* Build configuration list for PBXProject "FSQLocationBroker" */ = { 443 | isa = XCConfigurationList; 444 | buildConfigurations = ( 445 | F18199A91BE0052E009DB90D /* Debug */, 446 | F18199AA1BE0052E009DB90D /* Release */, 447 | ); 448 | defaultConfigurationIsVisible = 0; 449 | defaultConfigurationName = Release; 450 | }; 451 | F18199AB1BE0052E009DB90D /* Build configuration list for PBXNativeTarget "FSQLocationBroker" */ = { 452 | isa = XCConfigurationList; 453 | buildConfigurations = ( 454 | F18199AC1BE0052E009DB90D /* Debug */, 455 | F18199AD1BE0052E009DB90D /* Release */, 456 | ); 457 | defaultConfigurationIsVisible = 0; 458 | defaultConfigurationName = Release; 459 | }; 460 | /* End XCConfigurationList section */ 461 | }; 462 | rootObject = F181999A1BE0052E009DB90D /* Project object */; 463 | } 464 | -------------------------------------------------------------------------------- /FSQLocationBroker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FSQLocationBroker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FSQLocationBroker.xcodeproj/xcshareddata/xcschemes/FSQLocationBroker-AppExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /FSQLocationBroker.xcodeproj/xcshareddata/xcschemes/FSQLocationBroker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /FSQLocationBroker/FSQLocationBroker.h: -------------------------------------------------------------------------------- 1 | // 2 | // FSQLocationBroker.h 3 | // 4 | // Copyright (c) 2014 foursquare. All rights reserved. 5 | // 6 | 7 | @import Foundation; 8 | @import CoreLocation; 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @protocol FSQLocationSubscriber, FSQRegionMonitoringSubscriber; 13 | 14 | @protocol FSQVisitMonitoringSubscriber; 15 | 16 | #pragma mark - FSQLocationBroker interface 17 | /** 18 | Manager for location events application-wide. Subscribers must implement the 19 | _FSQXXXSubscriber_ protocol and themselves to the list of subscribers in order to receive 20 | notifications from the Broker. 21 | 22 | @note When building app extension targets, you must define the FSQ_IS_APP_EXTENSION preprocessor macro to 23 | avoid compiling unavailable APIs. You can do this in your prefix header or the Xcode settings for your target. 24 | */ 25 | 26 | @interface FSQLocationBroker : NSObject 27 | /** 28 | The most recent location received from the broker's CLLocationManager. 29 | 30 | @see [CLLocationManager location] 31 | */ 32 | @property (atomic, readonly, copy, nullable) CLLocation *currentLocation; 33 | 34 | /** 35 | The most accuracy we are currently requesting from the broker's CLLocationManager. 36 | 37 | @see [CLLocationManager desiredAccuracy] 38 | */ 39 | @property (nonatomic, readonly) CLLocationAccuracy currentAccuracy; 40 | 41 | /** 42 | The current set of location subscribers. 43 | 44 | Additions and removals of subscribers are processed on a background queue for thread safety reasons, so this 45 | set might not immediately reflect changes you make. 46 | */ 47 | @property (atomic, readonly) NSSet *locationSubscribers; 48 | 49 | /** 50 | The current set of region monitoring subscribers. 51 | 52 | Additions and removals of subscribers are processed on a background queue for thread safety reasons, so this 53 | set might not immediately reflect changes you make. 54 | */ 55 | @property (atomic, readonly) NSSet *regionSubscribers; 56 | 57 | /** 58 | The current set of visit monitoring subscribers. 59 | 60 | Additions and removals of subscribers are processed on a background queue for thread safety reasons, so this 61 | set might not immediately reflect changes you make. 62 | */ 63 | @property (atomic, readonly) NSSet *visitSubscribers; 64 | 65 | /** 66 | Access point to the location broker shared pointer/singleton. 67 | 68 | Lazily instantiated the first time the method is called. 69 | 70 | Normally creates and returns an instance of FSQLocationBroker. If you would like to use a custom broker subclass 71 | in your app, you can change the class by using the @c setSharedClass: class method, but you must do this before 72 | the first call to @c shared. 73 | 74 | @return The shared singleton location broker instance. 75 | */ 76 | + (instancetype)shared; 77 | 78 | /** 79 | If you would like to use a custom subclass of FSQLocationBroker in your app, you can set its class using this 80 | method. All future calls to [FSQLocationBroker shared] will return the shared method of that class instead. 81 | 82 | If you specify a class that is not a subclass of FSQLocationBroker this method will do nothing. 83 | 84 | @warning If you want to use this method to change the broker class, you must call it before 85 | the first call to @c shared. 86 | 87 | @param locationBrokerSubclass A subclass of FSQLocationBroker to use instead of the base implementation. 88 | */ 89 | + (void)setSharedClass:(Class)locationBrokerSubclass; 90 | 91 | /** 92 | Convenience method for getting if we are currently authorized to get location services from the CLLocationManager. 93 | 94 | @see [CLLocationManager authorizationStatus] 95 | 96 | @return YES if the app is authorized for location services, NO if not. 97 | 98 | @note On iOS 8+, will return YES if authorized for either always or when in use. 99 | Use [CLLocationManager authorizationStatus] to get more details. 100 | */ 101 | + (BOOL)isAuthorized; 102 | 103 | /** 104 | Add a new location subscriber to the broker. 105 | 106 | The subscriber's location options will be taken into account when requesting locations services from the system. 107 | 108 | @param locationSubscriber Location subscriber to add. If this object is already in the broker's location subscriber 109 | list this method does nothing. The subscriber will be retained by the broker. 110 | 111 | @note Additions are processed on a background queue for thread safety reasons, and so might not be immediately 112 | reflected if you access the locationSubscribers property. 113 | */ 114 | - (void)addLocationSubscriber:(NSObject *)locationSubscriber NS_REQUIRES_SUPER; 115 | 116 | /** 117 | Remove a location subscriber from the broker. 118 | 119 | @param locationSubscriber Location subscriber to remove. If this object is not currently in the broker's location 120 | subscriber list this method does nothing. 121 | 122 | @note Removals are processed on a background queue for thread safety reasons, and so might not be immediately 123 | reflected if you access the locationSubscribers property. 124 | */ 125 | - (void)removeLocationSubscriber:(NSObject *)locationSubscriber NS_REQUIRES_SUPER; 126 | 127 | /** 128 | Updates the location services being requested from the system by the broker by checking the current list of 129 | location subscribers. 130 | 131 | This method is called automatically for you when a subscriber is added or removed, or when the relevant properties 132 | on the subscribers change if they are KVO-compliant. 133 | */ 134 | - (void)refreshLocationSubscribers NS_REQUIRES_SUPER; 135 | 136 | /** 137 | Add a new region monitoring subscriber to the broker. 138 | 139 | If there were already monitored regions from a previous app launch that match this added subscribers identifier, 140 | the broker will tell the subscriber to re-add these region to its list via the `addMonitoredRegion:` method. 141 | 142 | @param regionSubscriber Region monitoring subscriber to add. If this object is already in the broker's region 143 | monitoring subscriber list this method does nothing. The subscriber will be retained by the broker. 144 | 145 | @note Additions are processed on a background queue for thread safety reasons, and so might not be immediately 146 | reflected if you access the regionSubscribers property. 147 | */ 148 | - (void)addRegionMonitoringSubscriber:(NSObject *)regionSubscriber NS_REQUIRES_SUPER; 149 | 150 | /** 151 | Remove a region monitoring subscriber from the broker. 152 | 153 | This will stop monitoring for all regions that this subscriber was monitoring. 154 | 155 | @param regionSubscriber Region monitoring subscriber to remove. If this object is not currently in the broker's 156 | region monitoring subscriber list this method does nothing. 157 | 158 | @note Removals are processed on a background queue for thread safety reasons, and so might not be immediately 159 | reflected if you access the regionSubscribers property. 160 | */ 161 | - (void)removeRegionMonitoringSubscriber:(NSObject *)regionSubscriber NS_REQUIRES_SUPER; 162 | 163 | /** 164 | Updates the location services being requested from the system by the broker by checking the current list of 165 | region monitoring subscribers. 166 | 167 | This method is called automatically for you when a subscriber is added, or when the relevant properties 168 | on the subscribers change if they are KVO-compliant. 169 | 170 | @note This will not remove regions being monitored if their subscriber ids do not match any known subscribers, as 171 | those subscribers might be added later and repaired with their regions (eg after an app relaunch). 172 | If you would like to forcibly synchronize the systems set of monitored regions with the current subscriber array, 173 | use `forceSyncRegionMonitorSubscribersWithSystem` 174 | 175 | */ 176 | - (void)refreshRegionMonitoringSubscribers NS_REQUIRES_SUPER; 177 | 178 | /** 179 | This will set the location services being request from the system by the broker to the exact set of regions 180 | monitored by the current array of subscribers. 181 | 182 | If you have regions being monitored by other means, or monitored regions left over from a previous app launch that 183 | have not yet been repaired with their subscribers, this will remove them. 184 | 185 | This method is never called automatically by the broker. 186 | 187 | Under most circumstances you will want to use `refreshRegionMonitoringSubscribers` instead. 188 | 189 | */ 190 | - (void)forceSyncRegionMonitorSubscribersWithSystem NS_REQUIRES_SUPER; 191 | 192 | /** 193 | Calls through to the location managers requestStateForRegion: method. 194 | 195 | Results will be delivered to region monitoring subscribers of the requested region. 196 | 197 | @see [CLLocationManager requestStateForRegion:] 198 | 199 | @param region The region whose state you want to know. This object must be an instance of one of the standard region subclasses provided by Map Kit. You cannot use this method to determine the state of custom regions you define yourself. 200 | */ 201 | - (void)requestStateForRegion:(CLRegion *)region NS_REQUIRES_SUPER; 202 | 203 | /** 204 | Add a new visit subscriber to the broker. 205 | 206 | The subscriber's location options will be taken into account when requesting locations services from the system. 207 | 208 | @param visitSubscriber Visit subscriber to add. If this object is already in the broker's visit subscriber 209 | list this method does nothing. The subscriber will be retained by the broker. 210 | 211 | @note Additions are processed on a background queue for thread safety reasons, and so might not be immediately 212 | reflected if you access the visitSubscribers property. 213 | */ 214 | - (void)addVisitSubscriber:(NSObject *)visitSubscriber NS_REQUIRES_SUPER; 215 | 216 | /** 217 | Remove a visit subscriber from the broker. 218 | 219 | @param visitSubscriber Visit subscriber to remove. If this object is not currently in the broker's visit 220 | subscriber list this method does nothing. 221 | 222 | @note Removals are processed on a background queue for thread safety reasons, and so might not be immediately 223 | reflected if you access the visitSubscribers property. 224 | */ 225 | - (void)removeVisitSubscriber:(NSObject *)visitSubscriber NS_REQUIRES_SUPER; 226 | 227 | /** 228 | Updates the visit services being requested from the system by the broker by checking the current list of 229 | visit subscribers. 230 | 231 | This method is called automatically for you when a subscriber is added or removed. 232 | */ 233 | - (void)refreshVisitSubscribers NS_REQUIRES_SUPER; 234 | 235 | /** 236 | Remove all subscribers of all types and turn off all location services. 237 | 238 | This can be useful if you want to completely reset your location services in some instance, 239 | e.g. when a user logs out of your application. 240 | 241 | @note Removals are processed on a background queue for thread safety reasons, and so might not be immediately 242 | reflected if you access the locationSubscribers property. 243 | */ 244 | - (void)removeAllSubscribers NS_REQUIRES_SUPER; 245 | 246 | /** 247 | Request InUse Authorization. 248 | 249 | This allows location services to run when your app is in the foreground. 250 | */ 251 | - (void)requestWhenInUseAuthorization; 252 | 253 | /** 254 | Request Always Authorization 255 | 256 | This allows location services to run in the background. 257 | */ 258 | - (void)requestAlwaysAuthorization; 259 | 260 | @end 261 | 262 | #pragma mark - FSQLocationSubscriber Protocol 263 | 264 | /** 265 | Bitmask configuration options for the FSQLocationSubscriber protocol 266 | 267 | Subscribers should bitwise OR the options they want together. 268 | */ 269 | typedef NS_OPTIONS(NSUInteger, FSQLocationSubscriberOptions) { 270 | /** 271 | The subscriber wants the broker to subscribe for continuous location updates from the system. 272 | (i.e. [CLLocationManager startUpdatingLocation]). 273 | 274 | The broker will deliver these locations to the subscriber via the locationManagerDidUpdateLocations: method. 275 | 276 | If this option is present, the broker will use the subscriber's desiredAccuracy property to calculate 277 | what desired accuracy to report to the system. 278 | 279 | @note Including this option will make your subscriber receieve all location updates received by the broker 280 | from the system, regardless of which subscriber "caused" those updates. 281 | */ 282 | FSQLocationSubscriberShouldRequestContinuousLocation = (1 << 0), 283 | 284 | /** 285 | The subscriber wants the broker to subscribe for significant location updates from the system 286 | (i.e. [CLLocationManager startMonitoringSignificantLocationChanges]). 287 | 288 | The broker will deliver these locations to the subscriber via the locationManagerDidUpdateLocations: method. 289 | 290 | @note Including this option will make your subscriber receieve all location updates received by the broker 291 | from the system, regardless of which subscriber "caused" those updates. 292 | */ 293 | FSQLocationSubscriberShouldMonitorSLCs = (1 << 1), 294 | 295 | /** 296 | The subscriber wants the broker to forward any location manager errors from the system to it. 297 | 298 | The broker will deliver these errors to the subscriber via the locationManagerFailedWithError: method. 299 | You must also implement this method on your subscriber to actually receive the errors. 300 | */ 301 | FSQLocationSubscriberShouldReceiveErrors = (1 << 2), 302 | 303 | /** 304 | The subscriber wants the broker to forward all locations received from the system to it. 305 | 306 | This is useful if you want to "passively" get location information, e.g. get any locations your app is already 307 | requesting because of other subscribers, but not actually affect what the broker requests from the sytem. 308 | 309 | This option does not have any effect if the ShouldRequestContinuousLocation or ShouldMonitorSLCs options 310 | are included (as those options effectively also imply this forwarding behavior). 311 | */ 312 | FSQLocationSubscriberShouldReceiveAllBrokerLocations = (1 << 3), 313 | 314 | /** 315 | The subscriber wants the broker to keep running its location requests when the app is backgrounded. 316 | 317 | When the app is backgrounded, the broker uses the subset of subscribers which include this option to relcalculate 318 | what location services to request from the system and which subscribers to deliver callbacks to. 319 | */ 320 | FSQLocationSubscriberShouldRunInBackground = (1 << 4), 321 | }; 322 | 323 | /** 324 | The _FSQLocationSubscriber_ protocol. 325 | 326 | Each subscriber is responsible for specifying its desired settings (think of them as a data 327 | source). 328 | 329 | The following properties **must be** KVO compliant OR **implementors must call** 330 | [[FSLocationBroker shared] refreshLocationSubscribers] after changing their the return values 331 | in order for changes to take place: 332 | 333 | * desiredAccuracy 334 | * subscriberOptions 335 | 336 | There is no guarantee changing the return values will affect _FSQLocationBroker_ behavior if 337 | you do not refresh the subscribers list. The broker will automatically try to observe and refresh after 338 | values change for KVO compliant properties. 339 | */ 340 | @protocol FSQLocationSubscriber 341 | 342 | /** 343 | A bitmask of the configuration options for this subscriber. The broker will use this bitmask to determine 344 | what location services to request from the system. 345 | 346 | See the documentation for FSQLocationSubscriberOptions to get information on the available options. 347 | 348 | If the property is KVO-compliant, the broker will automatically update its state when changes occur. Otherwise 349 | you must manually call @c refreshLocationSubscribers on the broker to have your changes reflected. 350 | 351 | @note You can set this property to 0 to effectively remove this subscriber from the broker's consideration without 352 | actually removing it from the broker's subscriber list. 353 | */ 354 | @property (nonatomic, readonly) FSQLocationSubscriberOptions locationSubscriberOptions; 355 | 356 | /** 357 | If the subscriber options include @c FSQLocationSubscriberShouldRequestContinuousLocation then this accuracy 358 | is used to calculate the desiredAccuracy to request from the system. Otherwise the value is unused. 359 | 360 | If the property is KVO-compliant, the broker will automatically update its state when changes occur. Otherwise 361 | you must manually call @c refreshLocationSubscribers on the broker to have your changes reflected. 362 | */ 363 | @property (nonatomic, readonly) CLLocationAccuracy desiredAccuracy; 364 | 365 | /** 366 | Significant location change and continuous location update callbacks from the system will be forwarded to this method 367 | 368 | All SLC and continuous location updates received by the broker will be forwarded to all subscribers that requested 369 | either. E.g. a subscriber that only requested SLCs may receive continuous updates, or a subscriber which only 370 | requested 3km accuracy will high accuracy updates if another subscriber requested 10m updates. 371 | 372 | @param locations The locations that were recieved. 373 | 374 | @see [CLLocationManagerDelegate locationManager:didUpdateLocations:] 375 | 376 | */ 377 | - (void)locationManagerDidUpdateLocations:(NSArray *)locations; 378 | 379 | @optional 380 | 381 | /** 382 | System location manager errors will be forwarded to this method. 383 | 384 | If your subscriber options property includes the FSQLocationSubscriberShouldReceiveErrors you must implement this 385 | method to actually receive the errors. 386 | 387 | @param error The error that was received. 388 | 389 | @see [CLLocationManagerDelegate locationManager:didFailWithError:] 390 | */ 391 | - (void)locationManagerFailedWithError:(NSError *)error; 392 | 393 | @end 394 | 395 | #pragma mark - FSQVisitMonitoringSubscriber Protocol 396 | 397 | @protocol FSQVisitMonitoringSubscriber 398 | 399 | @property (nonatomic, readonly) BOOL shouldMonitorVisits; 400 | 401 | /** 402 | Visits will be forwarded to this method if this object has been added as a visit subscriber and shouldMonitorVisits 403 | returns YES. 404 | 405 | @param visit The CLVisit obtained. 406 | 407 | @see [CLLocationManagerDelegate locationManager:didVisit:] 408 | */ 409 | 410 | - (void)locationManagerDidVisit:(CLVisit *)visit; 411 | 412 | @end 413 | 414 | #pragma mark - FSQRegionMonitoringSubscriber Protocol 415 | 416 | /** 417 | The protocol for subscribing to Region Monitoring events. 418 | 419 | The following properties **must be** KVO compliant OR **implementors must call** 420 | [[FSQLocationBroker shared] refreshRegionMonitoringSubscribers] after changing their the return values 421 | in order for changes to take place: 422 | 423 | * monitoredRegions 424 | 425 | There is no guarantee changing the return values will affect _FSQLocationBroker_ behavior if 426 | you do not refresh the subscribers list. The broker will automatically try to observe and refresh after 427 | values change for KVO compliant properties. 428 | */ 429 | @protocol FSQRegionMonitoringSubscriber 430 | 431 | /** 432 | List of regions the subscriber wants to monitor. 433 | 434 | The FSQRegionMonitoringSubscriber must maintain its own list of monitored regions. 435 | 436 | If the property is KVO-compliant, the broker will automatically update its state when changes occur. Otherwise 437 | you must manually call @c refreshLocationSubscribers on the broker to have your changes reflected. 438 | */ 439 | @property (nonatomic, readonly) NSSet *monitoredRegions; 440 | 441 | /** 442 | If YES, the broker will forward location manager errors related to this subscriber's 443 | monitored region to the subscriber. 444 | 445 | The broker will deliver these errors to the subscriber via the locationManagerFailedWithError: method. 446 | You must also implement this method on your subscriber to actually receive the errors. 447 | */ 448 | @property (nonatomic, readonly) BOOL shouldReceiveRegionMonitoringErrors; 449 | 450 | /** 451 | A subscriber must prefix all CLRegion.identifier with their prefix when monitoring a region. 452 | Format: 453 | 454 | [NSString stringWithFormat: "%@+%@", [self subscriberIdentifier], regionIdentifier] 455 | 456 | Valid identifiers have the same specification as valid C identifiers. The broker will 457 | assert on any regions whose identifiers are not specified with this format. 458 | 459 | Valid C identifiers: http://eli-project.sourceforge.net/c_html/c.html#s6.1.2 460 | */ 461 | - (NSString *)subscriberIdentifier; 462 | 463 | /** 464 | This method should add the specified region to its list of monitored regions. 465 | 466 | The broker will use this method when reassigning the system's monitored regions to the matching subscribers 467 | (based on subscriber identifier) if they get out of sync (e.g. after an app restart). 468 | 469 | @param region A CLRegion to add to the subscriber's list of monitored regions. 470 | 471 | @note If you would like to not continue monitoring this previously monitored region, you can simply do nothing 472 | in this method. 473 | */ 474 | - (void)addMonitoredRegion:(CLRegion *)region; 475 | 476 | /** 477 | Location manager didEnterRegion: calls for your monitored regions will be forwarded to this method. 478 | 479 | @see [CLLocationManagerDelegate locationManager:didEnterRegion:] 480 | 481 | @param region The region that was entered. 482 | 483 | @note If the system sends the broker a callback for a region which does not match any currently registered subscribers, 484 | the region will be unmonitored. 485 | */ 486 | - (void)didEnterRegion:(CLRegion *)region; 487 | 488 | /** 489 | Location manager didExitRegion: calls for your monitored regions will be forwarded to this method. 490 | 491 | @see [CLLocationManagerDelegate locationManager:didExitRegion:] 492 | 493 | @param region The region that was exited. 494 | 495 | @note If the system sends the broker a callback for a region which does not match any currently registered subscribers, 496 | the region will be unmonitored. 497 | */ 498 | - (void)didExitRegion:(CLRegion *)region; 499 | 500 | @optional 501 | 502 | /** 503 | Location manager didDetermineState:forRegion: calls for your monitored regions will be forwarded to this method. 504 | 505 | @see [CLLocationManagerDelegate locationManager:didDetermineState:forRegion:] 506 | 507 | @param state The state of the specified region. For a list of possible values, see the CLRegionState type. 508 | @param region The region whose state was determined. 509 | */ 510 | - (void)didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region; 511 | 512 | /** 513 | System location manager errors for your monitored regions will be forwarded to this method 514 | 515 | @param region The region for which an error occured. 516 | @param error The error that was received. 517 | */ 518 | - (void)monitoringDidFailForRegion:(nullable CLRegion *)region withError:(NSError *)error; 519 | 520 | @end 521 | 522 | NS_ASSUME_NONNULL_END 523 | -------------------------------------------------------------------------------- /FSQLocationBroker/FSQLocationBroker.m: -------------------------------------------------------------------------------- 1 | // 2 | // FSQLocationBroker.m 3 | // 4 | // Copyright (c) 2014 foursquare. All rights reserved. 5 | // 6 | 7 | #import "FSQLocationBroker.h" 8 | @import UIKit; 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | static void *kLocationBrokerLocationSubscriberKVOContext = &kLocationBrokerLocationSubscriberKVOContext; 13 | static void *kLocationBrokerRegionMonitoringSubscriberKVOContext = &kLocationBrokerRegionMonitoringSubscriberKVOContext; 14 | static void *kLocationBrokerVisitSubscriberKVOContext = &kLocationBrokerVisitSubscriberKVOContext; 15 | 16 | // Helper functions for code readability and reuse 17 | BOOL applicationIsBackgrounded(void); 18 | BOOL subscriberShouldRunInBackground(NSObject *locationSubscriber); 19 | BOOL subscriberShouldReceiveLocationUpdates(NSObject *locationSubscriber); 20 | BOOL subscriberShouldReceiveErrors(NSObject *locationSubscriber); 21 | BOOL subscriberWantsContinuousLocation(NSObject *locationSubscriber); 22 | BOOL subscriberWantsSLCMonitoring(NSObject *locationSubscriber); 23 | BOOL subscriberWantsVisitMonitoring(NSObject *locationSubscriber); 24 | 25 | @interface FSQLocationBroker () 26 | 27 | // Publicly exposed as readonly 28 | @property (atomic, readwrite) NSSet *locationSubscribers; 29 | @property (atomic, readwrite) NSSet *regionSubscribers; 30 | @property (atomic, readwrite) NSSet *visitSubscribers; 31 | @property (atomic, copy, nullable) CLLocation *currentLocation; 32 | 33 | // Private 34 | @property (nonatomic) CLLocationManager *locationManager; 35 | @property (nonatomic) BOOL isMonitoringSignificantLocation, isUpdatingLocation, isMonitoringVisits; 36 | @property (nonatomic) dispatch_queue_t serialQueue; 37 | 38 | @end 39 | 40 | @implementation FSQLocationBroker 41 | 42 | static Class sharedInstanceClass = nil; 43 | 44 | + (void)setSharedClass:(Class)locationBrokerSubclass { 45 | if ([locationBrokerSubclass isSubclassOfClass:[FSQLocationBroker class]]) { 46 | static dispatch_once_t onceToken; 47 | dispatch_once(&onceToken, ^{ 48 | sharedInstanceClass = locationBrokerSubclass; 49 | }); 50 | } 51 | else { 52 | NSAssert(0, @"Attempting to assign location broker shared class with class that does not subclass FSQLocationBroker (%@)", [locationBrokerSubclass description]); 53 | } 54 | } 55 | 56 | + (instancetype)shared { 57 | static FSQLocationBroker *sharedInstance = nil; 58 | static dispatch_once_t onceToken; 59 | dispatch_once(&onceToken, ^{ 60 | if (sharedInstanceClass && sharedInstanceClass != [self class]) { 61 | sharedInstance = [sharedInstanceClass shared]; 62 | } 63 | else { 64 | sharedInstance = [[self alloc] init]; 65 | } 66 | }); 67 | return sharedInstance; 68 | } 69 | 70 | + (BOOL)isAuthorized { 71 | CLAuthorizationStatus authStatus = [CLLocationManager authorizationStatus]; 72 | return (authStatus == kCLAuthorizationStatusAuthorizedAlways 73 | || authStatus == kCLAuthorizationStatusAuthorizedWhenInUse); 74 | } 75 | 76 | - (instancetype)init { 77 | if ((self = [super init])) { 78 | self.locationManager = [CLLocationManager new]; 79 | 80 | self.currentLocation = self.locationManager.location; 81 | self.locationManager.delegate = self; 82 | 83 | self.locationSubscribers = [NSSet new]; 84 | self.regionSubscribers = [NSSet new]; 85 | self.visitSubscribers = [NSSet new]; 86 | 87 | self.isMonitoringSignificantLocation = NO; 88 | self.isUpdatingLocation = NO; 89 | self.isMonitoringVisits = NO; 90 | 91 | self.serialQueue = dispatch_queue_create("LocationBrokerSubscriberMutations", DISPATCH_QUEUE_SERIAL); 92 | 93 | [[NSNotificationCenter defaultCenter] addObserver:self 94 | selector:@selector(applicationDidEnterBackground:) 95 | name:UIApplicationDidEnterBackgroundNotification 96 | object:nil]; 97 | 98 | [[NSNotificationCenter defaultCenter] addObserver:self 99 | selector:@selector(applicationDidBecomeActive:) 100 | name:UIApplicationDidBecomeActiveNotification 101 | object:nil]; 102 | } 103 | return self; 104 | } 105 | 106 | - (void)removeAllSubscribers { 107 | dispatch_async(self.serialQueue, ^{ 108 | for (NSObject *locationSubscriber in self.locationSubscribers) { 109 | @try { 110 | [locationSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(desiredAccuracy))]; 111 | } @catch (NSException * __unused exception) {} 112 | @try { 113 | [locationSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(locationSubscriberOptions))]; 114 | } @catch (NSException * __unused exception) {} 115 | } 116 | 117 | for (NSObject *regionSubscriber in self.regionSubscribers) { 118 | @try { 119 | [regionSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(monitoredRegions))]; 120 | } @catch (NSException * __unused exception) {} 121 | } 122 | 123 | self.locationSubscribers = [NSSet new]; 124 | self.regionSubscribers = [NSSet new]; 125 | self.visitSubscribers = [NSSet new]; 126 | 127 | [self.locationManager stopMonitoringSignificantLocationChanges]; 128 | [self.locationManager stopUpdatingLocation]; 129 | 130 | [self.locationManager stopMonitoringVisits]; 131 | 132 | for (CLRegion *region in self.locationManager.monitoredRegions) { 133 | [self.locationManager stopMonitoringForRegion:region]; 134 | } 135 | 136 | for (CLBeaconRegion *region in self.locationManager.rangedRegions) { 137 | [self.locationManager stopRangingBeaconsInRegion:region]; 138 | } 139 | }); 140 | } 141 | 142 | - (CLLocationAccuracy)currentAccuracy { 143 | return self.locationManager.desiredAccuracy; 144 | } 145 | 146 | #pragma mark LocationSubscribers 147 | 148 | - (void)addLocationSubscriber:(NSObject *)locationSubscriber { 149 | dispatch_async(self.serialQueue, ^{ 150 | if (![self.locationSubscribers containsObject:locationSubscriber]) { 151 | self.locationSubscribers = [self.locationSubscribers setByAddingObject:locationSubscriber]; 152 | [locationSubscriber addObserver:self 153 | forKeyPath:NSStringFromSelector(@selector(desiredAccuracy)) 154 | options:0 155 | context:kLocationBrokerLocationSubscriberKVOContext]; 156 | [locationSubscriber addObserver:self 157 | forKeyPath:NSStringFromSelector(@selector(locationSubscriberOptions)) 158 | options:0 159 | context:kLocationBrokerLocationSubscriberKVOContext]; 160 | 161 | [self refreshLocationSubscribers]; 162 | } 163 | }); 164 | } 165 | 166 | - (void)removeLocationSubscriber:(NSObject *)locationSubscriber { 167 | dispatch_async(self.serialQueue, ^{ 168 | if ([self.locationSubscribers containsObject:locationSubscriber]) { 169 | @try { 170 | [locationSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(desiredAccuracy))]; 171 | } @catch (NSException * __unused exception) {} 172 | @try { 173 | [locationSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(locationSubscriberOptions))]; 174 | } @catch (NSException * __unused exception) {} 175 | 176 | NSMutableSet *mutableLocationSubscribers = [self.locationSubscribers mutableCopy]; 177 | [mutableLocationSubscribers removeObject:locationSubscriber]; 178 | self.locationSubscribers = [mutableLocationSubscribers copy]; 179 | 180 | [self refreshLocationSubscribers]; 181 | } 182 | }); 183 | } 184 | 185 | - (BOOL)shouldMonitorSignificantLocationChanges { 186 | 187 | BOOL isBackgrounded = applicationIsBackgrounded(); 188 | 189 | for (NSObject *locationSubscriber in self.locationSubscribers) { 190 | if (subscriberWantsSLCMonitoring(locationSubscriber) 191 | && (!isBackgrounded || subscriberShouldRunInBackground(locationSubscriber))) { 192 | return YES; 193 | } 194 | } 195 | 196 | return NO; 197 | } 198 | 199 | - (BOOL)shouldUpdateLocations { 200 | 201 | BOOL isBackgrounded = applicationIsBackgrounded(); 202 | 203 | for (NSObject *locationSubscriber in self.locationSubscribers) { 204 | if (subscriberWantsContinuousLocation(locationSubscriber) 205 | && (!isBackgrounded || subscriberShouldRunInBackground(locationSubscriber))) { 206 | return YES; 207 | } 208 | } 209 | 210 | return NO; 211 | } 212 | 213 | - (BOOL)shouldAllowBackgroundLocationUpdates { 214 | NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; 215 | BOOL backgroundLocationModeEnabled = [backgroundModes containsObject:@"location"]; 216 | BOOL hasBackgroundLocationPermission = ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways); 217 | 218 | BOOL subscriberWantsBackgroundLocationUpdates = NO; 219 | for (NSObject *locationSubscriber in self.locationSubscribers) { 220 | if (subscriberWantsContinuousLocation(locationSubscriber) && subscriberShouldRunInBackground(locationSubscriber)) { 221 | subscriberWantsBackgroundLocationUpdates = YES; 222 | break; 223 | } 224 | } 225 | 226 | return backgroundLocationModeEnabled && hasBackgroundLocationPermission && subscriberWantsBackgroundLocationUpdates; 227 | } 228 | 229 | - (BOOL)shouldMonitorVisits { 230 | 231 | for (NSObject *locationSubscriber in self.visitSubscribers) { 232 | if (subscriberWantsVisitMonitoring(locationSubscriber)) { 233 | return YES; 234 | } 235 | } 236 | 237 | return NO; 238 | } 239 | 240 | - (CLLocationAccuracy)finestGrainAccuracy { 241 | 242 | BOOL isBackgrounded = applicationIsBackgrounded(); 243 | 244 | NSSet *locationUpdatingSubscribers = [self.locationSubscribers objectsPassingTest:^BOOL(NSObject *locationSubscriber, BOOL *stop) { 245 | return (subscriberWantsContinuousLocation(locationSubscriber) 246 | && (!isBackgrounded || subscriberShouldRunInBackground(locationSubscriber))); 247 | }]; 248 | 249 | if ([locationUpdatingSubscribers count] > 0) { 250 | CLLocationAccuracy lowestAccuracy = kCLLocationAccuracyThreeKilometers; 251 | for (NSObject *locationSubscriber in locationUpdatingSubscribers) { 252 | if (locationSubscriber.desiredAccuracy < lowestAccuracy) { 253 | lowestAccuracy = locationSubscriber.desiredAccuracy; 254 | } 255 | } 256 | return lowestAccuracy; 257 | } 258 | else { 259 | return kCLLocationAccuracyThreeKilometers; 260 | } 261 | } 262 | 263 | - (void)refreshLocationSubscribers { 264 | if (![NSThread isMainThread]) { 265 | dispatch_async(dispatch_get_main_queue(), ^() { 266 | [self refreshLocationSubscribers]; 267 | }); 268 | return; 269 | } 270 | 271 | CLLocationAccuracy newAccuracy = [self finestGrainAccuracy]; 272 | if (self.locationManager.desiredAccuracy != newAccuracy) { 273 | self.locationManager.desiredAccuracy = newAccuracy; 274 | } 275 | 276 | /** 277 | We don't compare against our current state for these ifs because of worries that our state could get 278 | out of sync with the CLLocationManager's state (which we can't introspect). Telling it to start/stop when it 279 | already is in that state just no-ops so we always call. 280 | */ 281 | 282 | // Should be monitoring 283 | if ([self shouldMonitorSignificantLocationChanges]) { 284 | [self.locationManager startMonitoringSignificantLocationChanges]; 285 | self.isMonitoringSignificantLocation = YES; 286 | 287 | } 288 | // Should not be monitoring 289 | else { 290 | [self.locationManager stopMonitoringSignificantLocationChanges]; 291 | self.isMonitoringSignificantLocation = NO; 292 | } 293 | 294 | // Should be updating locations 295 | if ([self shouldUpdateLocations]) { 296 | [self.locationManager startUpdatingLocation]; 297 | self.isUpdatingLocation = YES; 298 | } 299 | // Should not be updating locations 300 | else { 301 | [self.locationManager stopUpdatingLocation]; 302 | self.isUpdatingLocation = NO; 303 | } 304 | 305 | // Should allow background location 306 | if (@available(iOS 9.0, *)) { 307 | self.locationManager.allowsBackgroundLocationUpdates = [self shouldAllowBackgroundLocationUpdates]; 308 | } 309 | } 310 | 311 | #pragma mark RegionMonitoringSubscribers 312 | 313 | 314 | - (void)addRegionMonitoringSubscriber:(NSObject *)regionSubscriber { 315 | dispatch_async(self.serialQueue, ^{ 316 | if (![self.regionSubscribers containsObject:regionSubscriber]) { 317 | 318 | NSString *subscriberIdentifier = [regionSubscriber subscriberIdentifier]; 319 | 320 | for (CLRegion *region in self.locationManager.monitoredRegions) { 321 | NSString *identifier = [self subscriberIdentifierFromRegionIdentifier:region.identifier]; 322 | if ([identifier isEqualToString:subscriberIdentifier]) { 323 | [regionSubscriber addMonitoredRegion:region]; 324 | } 325 | } 326 | 327 | self.regionSubscribers = [self.regionSubscribers setByAddingObject:regionSubscriber]; 328 | [regionSubscriber addObserver:self 329 | forKeyPath:NSStringFromSelector(@selector(monitoredRegions)) 330 | options:0 331 | context:kLocationBrokerRegionMonitoringSubscriberKVOContext]; 332 | [self refreshRegionMonitoringSubscribers]; 333 | } 334 | }); 335 | } 336 | 337 | - (void)removeRegionMonitoringSubscriber:(NSObject *)regionSubscriber { 338 | dispatch_async(self.serialQueue, ^{ 339 | if ([self.regionSubscribers containsObject:regionSubscriber]) { 340 | @try { 341 | [regionSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(monitoredRegions))]; 342 | } @catch (NSException * __unused exception) {} 343 | 344 | NSMutableSet *mutableRegionSubscribers = [self.regionSubscribers mutableCopy]; 345 | [mutableRegionSubscribers removeObject:regionSubscriber]; 346 | self.regionSubscribers = [mutableRegionSubscribers copy]; 347 | 348 | [self refreshRegionMonitoringSubscribersRemovingSubscriberWithIdentifer:[regionSubscriber subscriberIdentifier] 349 | shouldRemoveAllUnmonitoredRegions:NO]; 350 | } 351 | }); 352 | } 353 | 354 | 355 | - (void)refreshRegionMonitoringSubscribers { 356 | [self refreshRegionMonitoringSubscribersRemovingSubscriberWithIdentifer:nil 357 | shouldRemoveAllUnmonitoredRegions:NO]; 358 | } 359 | 360 | - (void)refreshRegionMonitoringSubscribersRemovingSubscriberWithIdentifer:(nullable NSString *)subscriberIdentifier 361 | shouldRemoveAllUnmonitoredRegions:(BOOL)shouldRemoveAllUnmonitoredRegions { 362 | if (![NSThread isMainThread]) { 363 | dispatch_async(dispatch_get_main_queue(), ^() { 364 | [self refreshRegionMonitoringSubscribersRemovingSubscriberWithIdentifer:subscriberIdentifier 365 | shouldRemoveAllUnmonitoredRegions:shouldRemoveAllUnmonitoredRegions]; 366 | }); 367 | return; 368 | } 369 | 370 | [self verifyMonitoredRegionIdentifiers]; 371 | NSMutableSet *allCurrentSubscriberRegions = [self subscriberMonitoredRegions].mutableCopy; 372 | NSDictionary *allSubscribersByIdentifier = [self subscribersByIdentifier]; 373 | 374 | NSSet *allSubscriberRegionIdentifiers = [allCurrentSubscriberRegions valueForKey:NSStringFromSelector(@selector(identifier))]; 375 | 376 | NSSet *unmonitoredRegions = [self.locationManager.monitoredRegions objectsPassingTest:^BOOL(CLRegion *evaluatedObject, BOOL *stop) { 377 | return ![allSubscriberRegionIdentifiers containsObject:[evaluatedObject identifier]]; 378 | }]; 379 | 380 | for (CLRegion *region in unmonitoredRegions) { 381 | NSString *regionsSubscriberIdentifier = [self subscriberIdentifierFromRegionIdentifier:region.identifier]; 382 | 383 | /* 384 | Only remove unmonitored regions with subscriber ids we know about, or are for the subscriber we are removing 385 | 386 | Do not remove unmonitoried regions with unrecognized subscriber ids, as those subscribers may be added 387 | later and we want to resync them. 388 | 389 | Also do not remove regions without valid locbroker identifiers, as they may have been added separately 390 | via some non locbroker code path (since monitored region set is shared by all instances of CLLocationManager) 391 | */ 392 | if (shouldRemoveAllUnmonitoredRegions 393 | || (regionsSubscriberIdentifier 394 | && (allSubscribersByIdentifier[regionsSubscriberIdentifier] != nil 395 | || [regionsSubscriberIdentifier isEqualToString:subscriberIdentifier]))) { 396 | [self.locationManager stopMonitoringForRegion:region]; 397 | } 398 | } 399 | 400 | NSSet *currentlyMonitoringRegionIdentifiers = [self.locationManager.monitoredRegions valueForKey:NSStringFromSelector(@selector(identifier))]; 401 | 402 | // Don't remonitor already monitored regions 403 | NSSet *regionsThatNeedMonitoring = [allCurrentSubscriberRegions objectsPassingTest:^BOOL(CLRegion *evaluatedObject, BOOL *stop) { 404 | return ![currentlyMonitoringRegionIdentifiers containsObject:[evaluatedObject identifier]]; 405 | }]; 406 | 407 | for (CLRegion *newRegion in regionsThatNeedMonitoring) { 408 | [self.locationManager startMonitoringForRegion:newRegion]; 409 | } 410 | } 411 | 412 | - (void)forceSyncRegionMonitorSubscribersWithSystem { 413 | [self refreshRegionMonitoringSubscribersRemovingSubscriberWithIdentifer:nil 414 | shouldRemoveAllUnmonitoredRegions:YES]; 415 | } 416 | 417 | - (void)verifyMonitoredRegionIdentifiers { 418 | #if !NS_BLOCK_ASSERTIONS 419 | 420 | for (NSObject *regionSubscriber in self.regionSubscribers) { 421 | NSString *correctPrefix = [[regionSubscriber subscriberIdentifier] stringByAppendingString:@"+"]; 422 | 423 | NSSet *regions = regionSubscriber.monitoredRegions; 424 | for (CLRegion *region in regions) { 425 | NSAssert([region.identifier hasPrefix:correctPrefix], 426 | @"Subscriber: %@ monitors region without a matching prefix. (Region id: %@)", 427 | [regionSubscriber class], 428 | region.identifier); 429 | } 430 | } 431 | #endif 432 | } 433 | 434 | - (NSSet *)subscriberMonitoredRegions { 435 | NSMutableSet *subscriberRegions = [NSMutableSet set]; 436 | for (NSObject *regionSubscriber in self.regionSubscribers) { 437 | [subscriberRegions unionSet:regionSubscriber.monitoredRegions]; 438 | } 439 | return subscriberRegions; 440 | } 441 | 442 | - (NSDictionary *)subscribersByIdentifier { 443 | NSMutableDictionary *subscribersByIdentifier = [NSMutableDictionary dictionary]; 444 | 445 | for (NSObject *regionSubscriber in self.regionSubscribers) { 446 | subscribersByIdentifier[[regionSubscriber subscriberIdentifier]] = regionSubscriber; 447 | } 448 | 449 | return [subscribersByIdentifier copy]; 450 | } 451 | 452 | - (nullable NSString *)subscriberIdentifierFromRegionIdentifier:(NSString *)regionIdentifier { 453 | NSArray *identifierComponents = [regionIdentifier componentsSeparatedByString:@"+"]; 454 | if (identifierComponents.count > 1) { 455 | return [identifierComponents firstObject]; 456 | } 457 | else { 458 | // Not a valid location broker identifier 459 | return nil; 460 | } 461 | } 462 | 463 | - (void)requestStateForRegion:(CLRegion *)region { 464 | [self.locationManager requestStateForRegion:region]; 465 | } 466 | 467 | #pragma mark VisitSubscriber 468 | 469 | - (void)addVisitSubscriber:(NSObject *)visitSubscriber { 470 | dispatch_async(self.serialQueue, ^{ 471 | if (![self.visitSubscribers containsObject:visitSubscriber]) { 472 | self.visitSubscribers = [self.visitSubscribers setByAddingObject:visitSubscriber]; 473 | 474 | [visitSubscriber addObserver:self 475 | forKeyPath:NSStringFromSelector(@selector(shouldMonitorVisits)) 476 | options:0 477 | context:kLocationBrokerVisitSubscriberKVOContext]; 478 | 479 | [self refreshVisitSubscribers]; 480 | } 481 | }); 482 | } 483 | 484 | - (void)removeVisitSubscriber:(NSObject *)visitSubscriber { 485 | dispatch_async(self.serialQueue, ^{ 486 | if ([self.visitSubscribers containsObject:visitSubscriber]) { 487 | @try { 488 | [visitSubscriber removeObserver:self forKeyPath:NSStringFromSelector(@selector(shouldMonitorVisits))]; 489 | } @catch (NSException * __unused exception) {} 490 | 491 | NSMutableSet *mutableVisitSubscribers = [self.visitSubscribers mutableCopy]; 492 | [mutableVisitSubscribers removeObject:visitSubscriber]; 493 | self.visitSubscribers = [mutableVisitSubscribers copy]; 494 | 495 | [self refreshVisitSubscribers]; 496 | } 497 | }); 498 | } 499 | 500 | - (void)refreshVisitSubscribers { 501 | if (![NSThread isMainThread]) { 502 | dispatch_async(dispatch_get_main_queue(), ^() { 503 | [self refreshVisitSubscribers]; 504 | }); 505 | return; 506 | } 507 | 508 | if ([self shouldMonitorVisits]) { 509 | [self.locationManager startMonitoringVisits]; 510 | self.isMonitoringVisits = YES; 511 | } 512 | else { 513 | [self.locationManager stopMonitoringVisits]; 514 | self.isMonitoringVisits = NO; 515 | } 516 | } 517 | 518 | #pragma mark CLLocationManagerDelegate 519 | 520 | - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { 521 | 522 | BOOL isBackgrounded = applicationIsBackgrounded(); 523 | CLLocation *newestLocation = nil; 524 | for (CLLocation *location in locations) { 525 | if (!newestLocation || 526 | [newestLocation.timestamp earlierDate:location.timestamp] == newestLocation.timestamp) { 527 | newestLocation = location; 528 | } 529 | } 530 | 531 | self.currentLocation = newestLocation; 532 | 533 | 534 | for (NSObject *locationSubscriber in self.locationSubscribers) { 535 | if (subscriberShouldReceiveLocationUpdates(locationSubscriber) 536 | && (!isBackgrounded || subscriberShouldRunInBackground(locationSubscriber))) { 537 | 538 | [locationSubscriber locationManagerDidUpdateLocations:locations]; 539 | } 540 | } 541 | } 542 | 543 | - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { 544 | BOOL isBackgrounded = applicationIsBackgrounded(); 545 | 546 | for (NSObject *locationSubscriber in self.locationSubscribers) { 547 | if (subscriberShouldReceiveErrors(locationSubscriber) 548 | && (!isBackgrounded || subscriberShouldRunInBackground(locationSubscriber))) { 549 | [locationSubscriber locationManagerFailedWithError:error]; 550 | } 551 | } 552 | } 553 | 554 | - (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region { 555 | 556 | NSString *regionsSubscriberIdentifier = [self subscriberIdentifierFromRegionIdentifier:region.identifier]; 557 | 558 | if (regionsSubscriberIdentifier) { 559 | NSDictionary *subscribersByIdentifier = [self subscribersByIdentifier]; 560 | id regionSubscriber = subscribersByIdentifier[regionsSubscriberIdentifier]; 561 | if (regionSubscriber) { 562 | [regionSubscriber didEnterRegion:region]; 563 | } 564 | else { 565 | // This region's subscriber is not registered, so we should stop monitoring it. 566 | [self.locationManager stopMonitoringForRegion:region]; 567 | } 568 | } 569 | } 570 | 571 | - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region { 572 | NSString *regionsSubscriberIdentifier = [self subscriberIdentifierFromRegionIdentifier:region.identifier]; 573 | 574 | if (regionsSubscriberIdentifier) { 575 | NSDictionary *subscribersByIdentifier = [self subscribersByIdentifier]; 576 | id regionSubscriber = subscribersByIdentifier[regionsSubscriberIdentifier]; 577 | if (regionSubscriber) { 578 | [regionSubscriber didExitRegion:region]; 579 | } 580 | else { 581 | // This region's subscriber is not registered, so we should stop monitoring it. 582 | [self.locationManager stopMonitoringForRegion:region]; 583 | } 584 | } 585 | } 586 | 587 | - (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region { 588 | NSString *regionsSubscriberIdentifier = [self subscriberIdentifierFromRegionIdentifier:region.identifier]; 589 | 590 | if (regionsSubscriberIdentifier) { 591 | NSDictionary *subscribersByIdentifier = [self subscribersByIdentifier]; 592 | id regionSubscriber = subscribersByIdentifier[regionsSubscriberIdentifier]; 593 | if (regionSubscriber 594 | && [regionSubscriber respondsToSelector:@selector(didDetermineState:forRegion:)]) { 595 | [regionSubscriber didDetermineState:state forRegion:region]; 596 | } 597 | else { 598 | // This region's subscriber is not registered, so we should stop monitoring it. 599 | [self.locationManager stopMonitoringForRegion:region]; 600 | } 601 | } 602 | } 603 | 604 | - (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(nullable CLRegion *)region withError:(NSError *)error { 605 | NSString *regionsSubscriberIdentifier = [self subscriberIdentifierFromRegionIdentifier:region.identifier]; 606 | 607 | if (regionsSubscriberIdentifier) { 608 | NSDictionary *subscribersByIdentifier = [self subscribersByIdentifier]; 609 | id regionSubscriber = subscribersByIdentifier[regionsSubscriberIdentifier]; 610 | if (regionSubscriber) { 611 | if (regionSubscriber.shouldReceiveRegionMonitoringErrors 612 | && [regionSubscriber respondsToSelector:@selector(monitoringDidFailForRegion:withError:)]) { 613 | [regionSubscriber monitoringDidFailForRegion:region withError:error]; 614 | } 615 | } 616 | else { 617 | // This region's subscriber is not registered, so we should stop monitoring it. 618 | [self.locationManager stopMonitoringForRegion:region]; 619 | } 620 | } 621 | } 622 | 623 | - (void)locationManager:(CLLocationManager *)manager didVisit:(CLVisit *)visit { 624 | 625 | for (NSObject *visitSubscriber in self.visitSubscribers) { 626 | if (subscriberWantsVisitMonitoring(visitSubscriber)) { 627 | 628 | [visitSubscriber locationManagerDidVisit:visit]; 629 | } 630 | } 631 | } 632 | 633 | #pragma mark - Backgrounding - 634 | 635 | - (void)applicationDidEnterBackground:(NSNotification *)notification { 636 | // Refresh so it will drop non-background enabled subscribers from accounting 637 | [self refreshLocationSubscribers]; 638 | } 639 | 640 | - (void)applicationDidBecomeActive:(NSNotification *)notification { 641 | // Refresh so it will re-account for non-background enabled subscribers 642 | [self refreshLocationSubscribers]; 643 | 644 | if ([[self class] isAuthorized]) { 645 | self.currentLocation = self.locationManager.location; 646 | } 647 | } 648 | 649 | #pragma mark - Authorization - 650 | 651 | - (void)requestWhenInUseAuthorization { 652 | [self.locationManager requestWhenInUseAuthorization]; 653 | } 654 | 655 | - (void)requestAlwaysAuthorization { 656 | [self.locationManager requestAlwaysAuthorization]; 657 | } 658 | 659 | #pragma mark - KVO callbacks - 660 | 661 | - (void)observeValueForKeyPath:(nullable NSString *)keyPath 662 | ofObject:(nullable id)object 663 | change:(nullable NSDictionary *)change 664 | context:(nullable void *)context { 665 | if (context == kLocationBrokerLocationSubscriberKVOContext) { 666 | [self refreshLocationSubscribers]; 667 | } 668 | else if (context == kLocationBrokerRegionMonitoringSubscriberKVOContext) { 669 | [self refreshRegionMonitoringSubscribers]; 670 | } 671 | else if (context == kLocationBrokerVisitSubscriberKVOContext) { 672 | [self refreshVisitSubscribers]; 673 | } 674 | } 675 | 676 | @end 677 | 678 | BOOL applicationIsBackgrounded() { 679 | 680 | #if defined(FSQ_IS_APP_EXTENSION) 681 | return NO; 682 | #else 683 | return ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground); 684 | #endif 685 | 686 | } 687 | 688 | BOOL subscriberShouldRunInBackground(NSObject *locationSubscriber) { 689 | return (locationSubscriber.locationSubscriberOptions & FSQLocationSubscriberShouldRunInBackground); 690 | } 691 | 692 | BOOL subscriberShouldReceiveLocationUpdates(NSObject *locationSubscriber) { 693 | return (locationSubscriber.locationSubscriberOptions & (FSQLocationSubscriberShouldRequestContinuousLocation 694 | | FSQLocationSubscriberShouldMonitorSLCs 695 | | FSQLocationSubscriberShouldReceiveAllBrokerLocations)); 696 | } 697 | 698 | BOOL subscriberShouldReceiveErrors(NSObject *locationSubscriber) { 699 | return ((locationSubscriber.locationSubscriberOptions & FSQLocationSubscriberShouldReceiveErrors) 700 | && [locationSubscriber respondsToSelector:@selector(locationManagerFailedWithError:)]); 701 | } 702 | 703 | BOOL subscriberWantsContinuousLocation(NSObject *locationSubscriber) { 704 | return (locationSubscriber.locationSubscriberOptions & FSQLocationSubscriberShouldRequestContinuousLocation); 705 | } 706 | 707 | BOOL subscriberWantsSLCMonitoring(NSObject *locationSubscriber) { 708 | return (locationSubscriber.locationSubscriberOptions & FSQLocationSubscriberShouldMonitorSLCs); 709 | } 710 | 711 | BOOL subscriberWantsVisitMonitoring(NSObject *locationSubscriber) { 712 | return locationSubscriber.shouldMonitorVisits; 713 | } 714 | 715 | NS_ASSUME_NONNULL_END 716 | -------------------------------------------------------------------------------- /FSQLocationBroker/FSQLocationBroker.modulemap: -------------------------------------------------------------------------------- 1 | framework module FSQLocationBroker { 2 | 3 | header "FSQLocationBroker.h" 4 | header "FSQSingleLocationSubscriber.h" 5 | 6 | export * 7 | } 8 | -------------------------------------------------------------------------------- /FSQLocationBroker/FSQSingleLocationSubscriber.h: -------------------------------------------------------------------------------- 1 | // 2 | // FSQSingleLocationSubscriber.h 3 | // 4 | // Copyright (c) 2014 foursquare. All rights reserved. 5 | // 6 | 7 | @import Foundation; 8 | #ifndef FSQ_IS_APP_EXTENSION 9 | #import 10 | #else 11 | #import 12 | #endif 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | /** 17 | Callback block when the subscriber finds an acceptable location or times out or fails due to an error. 18 | 19 | @param didSucceed YES if an acceptable location was found successfully, NO if there was a timeout or error 20 | @param location A location matching the specifications you set or the location with the best horizontal 21 | accuracy that the subscriber was able to get within the cutoff time. 22 | 23 | This location will not be older than the requested maximumAcceptableRecency. 24 | @param elapsedTime The amount of time the subscriber took to get your location (boxed NSTimeInterval). 25 | If there was already an acceptable location on the broker and the block thus is being 26 | called synchronously, this value will be nil. 27 | @param error The system location manager error if this block is being called due to an error, else nil. 28 | 29 | @see [CLLocationManagerDelegate locationManager:didFailWithError:] 30 | */ 31 | typedef void (^FSQSingleLocSubCompletionBlock)(BOOL didSucceed, CLLocation *_Nullable location, NSNumber *_Nullable elapsedTime, NSError *_Nullable error); 32 | 33 | /** 34 | This class is intended as a simple way to get a single location from the system. 35 | 36 | It has properties that define all the location information you should consider when trying to get/use a location. 37 | It uses FSQLocationBroker to try to find a location which matches your parameters and then return the result 38 | via a block. 39 | */ 40 | @interface FSQSingleLocationSubscriber : NSObject 41 | 42 | /** 43 | The accuracy to ask the system location manager for. 44 | */ 45 | @property (nonatomic, readonly) CLLocationAccuracy desiredAccuracy; 46 | 47 | /** 48 | The first received location with a horizontal accuracy less than or equal to this value will stop the 49 | subscriber and be returned via the completion block. 50 | */ 51 | @property (nonatomic, readonly) CLLocationAccuracy maximumAcceptableAccuracy; 52 | 53 | /** 54 | If an acceptable location hasn't been received within this time, the subscriber 55 | will stop listening and call your completion block. 56 | */ 57 | @property (nonatomic, readonly) NSTimeInterval cutoffTimeInterval; 58 | 59 | /** 60 | If YES, the subscriber will keep trying to get a location when backgrounded. 61 | May have not have the intended effect unless your app has background location entitlements. 62 | 63 | If NO, the subscriber will stop and call your completion block when the app is backgrounded. 64 | */ 65 | @property (nonatomic, readonly) BOOL shouldRunInBackground; 66 | 67 | /** 68 | This block will be called with the first location the subscriber finds that 69 | meets your requirements. 70 | 71 | If the subscriber does not receive an acceptable location within the 72 | cutoffTime you set it will send you the best accuracy location it could get in that time. 73 | 74 | This will also be called if the app is backgrounded before the subscriber could finish and 75 | shouldRunInBackground was not YES, or if the system location manager encounters an error. 76 | */ 77 | @property (nonatomic, readonly) FSQSingleLocSubCompletionBlock onCompletion; 78 | 79 | /** 80 | True if this subscriber is currently listening for location updates. 81 | */ 82 | @property (nonatomic, readonly) BOOL isListening; 83 | 84 | /** 85 | Will create a new single loc subscriber and start it listening for location updates 86 | As soon as it receives a location that meets the criteria you specify, it will 87 | stop listening for updates and pass the location to the callback you provide. 88 | 89 | @param desiredAccuracy 90 | The accuracy to ask the system location manager for. 91 | @param maximumAcceptableAccuracy 92 | The first received location with a horizontal accuracy less than or equal to this value will stop the 93 | subscriber and be returned via the completion block. 94 | @param maximumAcceptableRecency 95 | If the location broker already has a location under your max accuracy 96 | it will immediately return if the location is no older than this time interval. 97 | @param cutoffTimeInterval 98 | If an acceptable location hasn't been received within this time, the subscriber 99 | will stop listening and call your completion block. 100 | @param shouldRunInBackground 101 | If YES, the subscriber will keep trying to get a location when backgrounded. 102 | May have not have the intended effect unless your app has background location entitlements. 103 | If NO, the subscriber will stop and call your completion block when the app is backgrounded. 104 | @param onCompletion 105 | This block will be called with the first location the subscriber finds that 106 | meets your requirements. If the subscriber does not receive an acceptable location within the 107 | cutoffTime you set it will send you the best accuracy location it could get in that time. 108 | (for the most recent location, you should check FSLocationBroker instead). 109 | This will also be called if the app is backgrounded before the subscriber could finish and 110 | shouldRunInBackground was not YES, or if the system location manager encounters an error. 111 | 112 | @return A pointer to the subscriber instance if a location could not be sent synchronously. 113 | You do not have to do anything with this value, it will be retained automatically for you for the 114 | duration of the operation. 115 | 116 | @note You must provide an onCompletion block, a max accuracy > 0 and a 117 | cutoff time > 0 or this method will assert and return nil. 118 | 119 | @note If there is already an acceptable accuracy on the location broker, the completion block will be 120 | called synchronously and this method will return nil. 121 | */ 122 | + (nullable instancetype)startWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy 123 | maximumAcceptableAccuracy:(CLLocationAccuracy)maximumAcceptableAccuracy 124 | maximumAcceptableLocationRecency:(NSTimeInterval)maximumAcceptableRecency 125 | cutoffTime:(NSTimeInterval)cutoffTimeInterval 126 | shouldRunInBackground:(BOOL)shouldRunInBackground 127 | onCompletion:(FSQSingleLocSubCompletionBlock)onCompletion; 128 | /** 129 | Manually stop the subscriber listening for updates. 130 | 131 | There is no effect if the subscriber is not currently running. 132 | 133 | @note This will decerement the retain count of the subscriber if it was previously running. 134 | If you are not retaining it yourself it may be released in the future. 135 | */ 136 | - (void)cancel; 137 | 138 | @end 139 | 140 | NS_ASSUME_NONNULL_END 141 | -------------------------------------------------------------------------------- /FSQLocationBroker/FSQSingleLocationSubscriber.m: -------------------------------------------------------------------------------- 1 | // 2 | // FSQSingleLocationSubscriber.m 3 | // 4 | // Copyright (c) 2014 foursquare. All rights reserved. 5 | // 6 | 7 | #import "FSQSingleLocationSubscriber.h" 8 | @import UIKit; 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface FSQSingleLocationSubscriber () 13 | 14 | @property (nonatomic, nullable) NSTimer *cutoffTimer; 15 | @property (nonatomic, nullable) CLLocation *bestLocationReceived; 16 | @property (nonatomic, nullable) NSDate *startTime; 17 | @property (nonatomic) FSQLocationSubscriberOptions locationSubscriberOptions; 18 | 19 | @end 20 | 21 | @implementation FSQSingleLocationSubscriber 22 | 23 | - (instancetype)initWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy 24 | maximumAcceptableAccuracy:(CLLocationAccuracy)maximumAcceptableAccuracy 25 | maximumAcceptableLocationRecency:(NSTimeInterval)maximumAcceptableRecency 26 | cutoffTime:(NSTimeInterval)cutoffTimeInterval 27 | shouldRunInBackground:(BOOL)shouldRunInBackground 28 | onCompletion:(FSQSingleLocSubCompletionBlock)onCompletion { 29 | 30 | if ((self = [super init])) { 31 | _desiredAccuracy = desiredAccuracy; 32 | _maximumAcceptableAccuracy = maximumAcceptableAccuracy; 33 | _onCompletion = [onCompletion copy]; 34 | _cutoffTimeInterval = cutoffTimeInterval; 35 | _locationSubscriberOptions = (FSQLocationSubscriberShouldRequestContinuousLocation | FSQLocationSubscriberShouldReceiveErrors); 36 | if (shouldRunInBackground) { 37 | _locationSubscriberOptions |= FSQLocationSubscriberShouldRunInBackground; 38 | } 39 | else { 40 | [[NSNotificationCenter defaultCenter] addObserver:self 41 | selector:@selector(applicationDidEnterBackground:) 42 | name:UIApplicationDidEnterBackgroundNotification 43 | object:nil]; 44 | } 45 | } 46 | return self; 47 | } 48 | 49 | 50 | + (nullable instancetype)startWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy 51 | maximumAcceptableAccuracy:(CLLocationAccuracy)maximumAcceptableAccuracy 52 | maximumAcceptableLocationRecency:(NSTimeInterval)maximumAcceptableRecency 53 | cutoffTime:(NSTimeInterval)cutoffTimeInterval 54 | shouldRunInBackground:(BOOL)shouldRunInBackground 55 | onCompletion:(FSQSingleLocSubCompletionBlock)onCompletion { 56 | if (!onCompletion || !(cutoffTimeInterval > 0) || !(maximumAcceptableAccuracy > 0)) { 57 | NSAssert(0, @"FSQSimpleLocationSubscriber: Must include a completion block, cutoff time >0 and max accuracy > 0"); 58 | return nil; 59 | } 60 | 61 | CLLocation *currentLocation = [FSQLocationBroker shared].currentLocation; 62 | 63 | if (currentLocation 64 | && (-[currentLocation.timestamp timeIntervalSinceNow] <= maximumAcceptableRecency)) { 65 | if (currentLocation.horizontalAccuracy <= maximumAcceptableAccuracy) { 66 | onCompletion(YES, currentLocation, nil, nil); 67 | return nil; 68 | } 69 | } 70 | else { 71 | currentLocation = nil; 72 | } 73 | 74 | FSQSingleLocationSubscriber *subscriber = [[self alloc] initWithDesiredAccuracy:desiredAccuracy 75 | maximumAcceptableAccuracy:maximumAcceptableAccuracy 76 | maximumAcceptableLocationRecency:maximumAcceptableRecency 77 | cutoffTime:cutoffTimeInterval 78 | shouldRunInBackground:shouldRunInBackground 79 | onCompletion:onCompletion]; 80 | 81 | [subscriber startListening]; 82 | subscriber.bestLocationReceived = currentLocation; 83 | 84 | return subscriber; 85 | } 86 | 87 | 88 | - (void)startListening { 89 | self.bestLocationReceived = nil; 90 | self.startTime = [NSDate new]; 91 | [[FSQLocationBroker shared] addLocationSubscriber:self]; 92 | self.cutoffTimer = [NSTimer scheduledTimerWithTimeInterval:self.cutoffTimeInterval 93 | target:self 94 | selector:@selector(cutoffTimerFired:) 95 | userInfo:nil 96 | repeats:NO]; 97 | } 98 | 99 | - (void)stopListening { 100 | [self.cutoffTimer invalidate]; 101 | self.cutoffTimer = nil; 102 | [[FSQLocationBroker shared] removeLocationSubscriber:self]; 103 | } 104 | 105 | - (void)cancel { 106 | [self stopListening]; 107 | } 108 | 109 | - (void)cutoffTimerFired:(NSTimer *)cutoffTimer { 110 | [self stopListening]; 111 | if (self.onCompletion) { 112 | self.onCompletion(NO, self.bestLocationReceived, @(-[self.startTime timeIntervalSinceNow]), nil); 113 | } 114 | } 115 | 116 | - (BOOL)isListening { 117 | return self.cutoffTimer.isValid; 118 | } 119 | 120 | - (void)setShouldRunInBackground:(BOOL)shouldRunInBackground { 121 | if (shouldRunInBackground && !self.shouldRunInBackground) { 122 | self.locationSubscriberOptions |= FSQLocationSubscriberShouldRunInBackground; 123 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 124 | } 125 | else if (!shouldRunInBackground && self.shouldRunInBackground) { 126 | self.locationSubscriberOptions &= ~FSQLocationSubscriberShouldRunInBackground; 127 | [[NSNotificationCenter defaultCenter] addObserver:self 128 | selector:@selector(applicationDidEnterBackground:) 129 | name:UIApplicationDidEnterBackgroundNotification 130 | object:nil]; 131 | } 132 | } 133 | 134 | - (BOOL)shouldRunInBackground { 135 | return ((self.locationSubscriberOptions & FSQLocationSubscriberShouldRunInBackground) == FSQLocationSubscriberShouldRunInBackground); 136 | } 137 | 138 | - (void)locationManagerDidUpdateLocations:(NSArray *)locations { 139 | for (CLLocation *location in locations) { 140 | if (location.horizontalAccuracy <= self.maximumAcceptableAccuracy) { 141 | [self stopListening]; 142 | if (self.onCompletion) { 143 | self.onCompletion(YES, location, @(-[self.startTime timeIntervalSinceNow]), nil); 144 | } 145 | return; 146 | } 147 | else if (!self.bestLocationReceived || location.horizontalAccuracy < self.bestLocationReceived.horizontalAccuracy) { 148 | self.bestLocationReceived = location; 149 | } 150 | } 151 | } 152 | 153 | - (void)locationManagerFailedWithError:(NSError *)error { 154 | if (kCLErrorLocationUnknown != error.code) { 155 | [self stopListening]; 156 | if (self.onCompletion) { 157 | self.onCompletion(NO, self.bestLocationReceived, @(-[self.startTime timeIntervalSinceNow]), error); 158 | } 159 | } 160 | } 161 | 162 | - (void)applicationDidEnterBackground:(NSNotification *)notification { 163 | if (self.isListening) { 164 | /** 165 | NSNotificationCenter maintains non-strong references to objects. 166 | If it calls this method, when we stopListening later and remove ourself from the broker, 167 | it is possible our retain count will immediately go to 0 mid-method execution if no one 168 | else is retaining us, so we have to make sure we have a strong reference to ourself here 169 | so we don't crash. 170 | */ 171 | __typeof(self) strongSelf = self; 172 | [strongSelf cutoffTimerFired:strongSelf.cutoffTimer]; 173 | } 174 | } 175 | 176 | - (void)dealloc { 177 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 178 | [self.cutoffTimer invalidate]; 179 | self.cutoffTimer = nil; 180 | } 181 | 182 | @end 183 | 184 | NS_ASSUME_NONNULL_END 185 | -------------------------------------------------------------------------------- /FSQLocationBroker/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 | 1.4.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /FSQLocationBroker/module_appextension.modulemap: -------------------------------------------------------------------------------- 1 | framework module FSQLocationBroker_AppExtension { 2 | 3 | header "FSQLocationBroker.h" 4 | header "FSQSingleLocationSubscriber.h" 5 | 6 | export * 7 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Foursquare Labs, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSQLocationBroker 2 | ------------- 3 | A centralized location manager for your app. 4 | 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | 7 | # Overview 8 | 9 | FSQLocationBroker sits between the other classes of your app and CoreLocation's CLLocationManager, giving you a centralized place to manage location services in your app. The broker uses a list of location subscribers to determine which services to request from the system and then forwards the data back where appropriate. 10 | 11 | This repo also includes the FSQSingleLocationSubscriber class, a helper that works with the broker for the common case where you only need to get a single location. 12 | 13 | Both these classes are thoroughly documented in their headers (which should be automatically parsed and available through Xcode's documentation popovers), but this file includes a brief overview of their features. 14 | 15 | Note: A test project is included but it is used for build testing only - an example application is currently not included in this repository. 16 | 17 | # Setup 18 | 19 | ## Carthage Installation 20 | 21 | If your minimum iOS version requirement is 8.0 or greater, Carthage is the recommended way to integrate FSQLocationBroker with your app. 22 | Add `github "foursquare/FSQLocationBroker"` to your Cartfile and follow the instructions from [Carthage's README](https://github.com/Carthage/Carthage) for adding Carthage-built frameworks to your project. 23 | 24 | ## CocoaPods Installation 25 | 26 | If you use CocoaPods, you can add `pod 'FSQLocationBroker', '~> [desired version here]'` to your Podfile. Further instructions on setting up and using CocoaPods can be found on [their website](https://cocoapods.org) 27 | 28 | ## Manual Installation 29 | 30 | You can also simply add the four objc files in this project to your project, either by copying them over, or using git submodules. 31 | 32 | ## App Extensions 33 | 34 | By default FSQLocationBroker uses UIApplication to tell whether or not your app is running in the background. This feature must be disabled if you wish to use the class in an app extension, since UIApplication is not available (the broker will always assume you are running in the foreground). 35 | 36 | If you are using Carthage, simply have your extensions use and link against the **FSQLocationBroker_AppExtension.framework** instead of the standard version. 37 | 38 | If you are using a different installation method, you must make sure the `FSQ_IS_APP_EXTENSION` preprocessor macro is defined when building for an app extension. 39 | 40 | Note: FSQLocationBroker cannot currently be used in watchOS 2.0 apps due to its more limited version of the CoreLocation framework. 41 | 42 | ## Custom Subclasses 43 | 44 | If you would like to use a custom subclass of FSQLocationBroker in your app, you should set your subclass's class using `[FSQLocationBroker setSharedClass:]` early on in your app life cycle, before the broker singleton is created (like in your app delegate's `application:didFinishLaunchingWithOptions:`). This will cause the FSQLocationBroker's `shared` class method to return an instance of your subclass instead of itself. Make sure you call `setSharedClass:` on the base class and not your subclass. 45 | 46 | ## Using Location Broker 47 | 48 | To get location data in your class, implement either the FSQLocationSubscriber or FSQRegionMonitoringSubscriber protocols as approriate and add your class to the broker's subscriber list. You should now start receiving location data in your class. 49 | 50 | If you want to get location while running in the background, you will need to add the "Location updates" background mode entitlement and add the `location` key to your Info.plist's "Required Background Modes" (`UIBackgroundModes`). 51 | 52 | 53 | # Location Subscribers 54 | 55 | If your class is interested in getting location callbacks from the broker, it should implement the FSQLocationSubscriber protocol, then add itself to the subscriber list using `addLocationSubscriber:` 56 | 57 | Your subscriber can define what sorts of location information it is interested in using the **locationSubscriberOptions** bitmask property. The broker will then request the correct services from its CLLocationManager based on what all of its subscribers need. 58 | 59 | # Region Monitoring Subscribers 60 | 61 | If your class is interested in region monitoring (geofences) it should implement the FSQRegionMonitoringSubscriber protocol, then add itself to the subscriber list using `addRegionMonitoringSubscriber:` 62 | 63 | Your subscriber can define a list of regions it would like to monitor and the broker will take care of requesting them from the system for you. Your subscriber should also define an identifer string and include that in its region's identifiers so that the broker can re-assign the regions from CLLocationManager back to your class after an app restart (see the header documentation for more information). 64 | 65 | # Broker Methods and Properties 66 | 67 | You can access the shared pointer singleton for the location broker via the `shared` method. If you want to use a custom subclass of FSQLocationBroker in your app, you should first set your subclass's class by calling the `setSharedClass:` method on the base FSQLocationBroker. This will cause the base implementation of `[FSQLocationBroker shared]` to forward onto the `shared` method of the class you specify and avoid creating multiple singletons. 68 | 69 | You can add location or region monitoring subscribers to the broker using the following methods: 70 | ```objc 71 | - (void)addLocationSubscriber:(NSObject *)locationSubscriber; 72 | - (void)removeLocationSubscriber:(NSObject *)locationSubscriber 73 | - (void)addRegionMonitoringSubscriber:(NSObject *)regionSubscriber 74 | - (void)removeRegionMonitoringSubscriber:(NSObject *)regionSubscriber 75 | ``` 76 | 77 | You can then get the list of subscribers via the **locationSubscribers** and **regionSubscribers** properties. The same class can implement both subscriber protocols, but it must add itself to both subscriber lists independently. 78 | 79 | The **currentLocation** property returns the most recent location received by the CLLocationManager. In most cases if you are interested in getting only a single location, you should use FSQSingleLocationSubscriber instead of accessing this property directly. 80 | 81 | See the comments in FSQLocationBroker.h for more in depth documentation. 82 | 83 | # FSQSingleLocationSubscriber 84 | 85 | This subscriber class acts as a helper for when you just need to get a single location. You pass in the accuracy you want to request from the system, the maximum acceptable accuracy that you want from the location returned, how recent the location has to be, and how long to try to get this location. It takes care of adding itself to the broker, finding, and returning an acceptable location to you. 86 | 87 | See the comments in FSQSingleLocationSubscriber.h for more in depth documentation. 88 | 89 | # Contributors 90 | 91 | The classes were initially developed by Foursquare Labs for internal use. 92 | 93 | FSQLocationBroker was originally written by Anoop Ranganath ([@anoopr](https://twitter.com/anoopr)), Adam Alix ([@adamalix](https://twitter.com/adamalix)), and Brian Dorfman ([@bdorfman](https://twitter.com/bdorfman)). It is currently maintained by Sam Grossberg ([@samgro](https://github.com/samgro)). 94 | --------------------------------------------------------------------------------