├── .gitignore ├── DetectLocations.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── DetectLocations ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── DetectLocations-120x120.png │ │ ├── DetectLocations-180x180.png │ │ ├── DetectLocations-40x40.png │ │ ├── DetectLocations-58x58.png │ │ ├── DetectLocations-60x60.png │ │ ├── DetectLocations-80x80.png │ │ ├── DetectLocations-87x87.png │ │ └── DetectLocations.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── LocationPoint.h ├── LocationPoint.m ├── ViewController.h ├── ViewController.m └── main.m ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── fastlane ├── Appfile ├── Deliverfile ├── Fastfile ├── Matchfile ├── Pluginfile ├── README.md └── metadata │ ├── copyright.txt │ ├── en-US │ ├── description.txt │ ├── keywords.txt │ ├── marketing_url.txt │ ├── name.txt │ ├── privacy_url.txt │ ├── promotional_text.txt │ ├── release_notes.txt │ ├── subtitle.txt │ └── support_url.txt │ ├── primary_category.txt │ ├── primary_first_sub_category.txt │ └── secondary_category.txt └── screenshots ├── DetectLocations.png ├── screenshot0.jpg ├── screenshot1.jpg ├── screenshot2.jpg ├── screenshot3.jpg ├── screenshot4.jpg └── screenshot5.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | # Pods/ 40 | 41 | # fastlane 42 | # 43 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 44 | # screenshots whenever they are needed. 45 | # For more information about the recommended setup visit: 46 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 47 | 48 | fastlane/report.xml 49 | Preview.html 50 | fastlane/screenshots 51 | fastlane/test_output 52 | -------------------------------------------------------------------------------- /DetectLocations.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CA05F7971F72C11A00DD72BB /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA05F7961F72C11A00DD72BB /* Photos.framework */; }; 11 | CA0FB1231F72C64300C587A0 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA0FB1221F72C64300C587A0 /* MapKit.framework */; }; 12 | CA374A451F73EBC300CBC013 /* LocationPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = CA374A441F73EBC300CBC013 /* LocationPoint.m */; }; 13 | CA67F1A01F72BD4500C832BF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CA67F19F1F72BD4500C832BF /* main.m */; }; 14 | CA67F1A31F72BD4500C832BF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CA67F1A21F72BD4500C832BF /* AppDelegate.m */; }; 15 | CA67F1A61F72BD4500C832BF /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CA67F1A51F72BD4500C832BF /* ViewController.m */; }; 16 | CA67F1A91F72BD4500C832BF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CA67F1A71F72BD4500C832BF /* Main.storyboard */; }; 17 | CA67F1AB1F72BD4500C832BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA67F1AA1F72BD4500C832BF /* Assets.xcassets */; }; 18 | CA67F1AE1F72BD4500C832BF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CA67F1AC1F72BD4500C832BF /* LaunchScreen.storyboard */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | CA05F7941F72C01700DD72BB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; 23 | CA05F7961F72C11A00DD72BB /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; 24 | CA0FB1221F72C64300C587A0 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; 25 | CA374A431F73EBC300CBC013 /* LocationPoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocationPoint.h; sourceTree = ""; }; 26 | CA374A441F73EBC300CBC013 /* LocationPoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocationPoint.m; sourceTree = ""; }; 27 | CA67F19B1F72BD4500C832BF /* DetectLocations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DetectLocations.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | CA67F19F1F72BD4500C832BF /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 29 | CA67F1A11F72BD4500C832BF /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 30 | CA67F1A21F72BD4500C832BF /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 31 | CA67F1A41F72BD4500C832BF /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 32 | CA67F1A51F72BD4500C832BF /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 33 | CA67F1A81F72BD4500C832BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 34 | CA67F1AA1F72BD4500C832BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | CA67F1AD1F72BD4500C832BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 36 | CA67F1AF1F72BD4500C832BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | CA67F1981F72BD4500C832BF /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | CA0FB1231F72C64300C587A0 /* MapKit.framework in Frameworks */, 45 | CA05F7971F72C11A00DD72BB /* Photos.framework in Frameworks */, 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | CA05F7931F72C01700DD72BB /* Frameworks */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | CA0FB1221F72C64300C587A0 /* MapKit.framework */, 56 | CA05F7961F72C11A00DD72BB /* Photos.framework */, 57 | CA05F7941F72C01700DD72BB /* CoreMedia.framework */, 58 | ); 59 | name = Frameworks; 60 | sourceTree = ""; 61 | }; 62 | CA67F1921F72BD4500C832BF = { 63 | isa = PBXGroup; 64 | children = ( 65 | CA67F19D1F72BD4500C832BF /* DetectLocations */, 66 | CA67F19C1F72BD4500C832BF /* Products */, 67 | CA05F7931F72C01700DD72BB /* Frameworks */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | CA67F19C1F72BD4500C832BF /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | CA67F19B1F72BD4500C832BF /* DetectLocations.app */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | CA67F19D1F72BD4500C832BF /* DetectLocations */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | CA67F1A11F72BD4500C832BF /* AppDelegate.h */, 83 | CA67F1A21F72BD4500C832BF /* AppDelegate.m */, 84 | CA67F1A41F72BD4500C832BF /* ViewController.h */, 85 | CA67F1A51F72BD4500C832BF /* ViewController.m */, 86 | CA67F1A71F72BD4500C832BF /* Main.storyboard */, 87 | CA67F1AA1F72BD4500C832BF /* Assets.xcassets */, 88 | CA67F1AC1F72BD4500C832BF /* LaunchScreen.storyboard */, 89 | CA67F1AF1F72BD4500C832BF /* Info.plist */, 90 | CA67F19E1F72BD4500C832BF /* Supporting Files */, 91 | CA374A431F73EBC300CBC013 /* LocationPoint.h */, 92 | CA374A441F73EBC300CBC013 /* LocationPoint.m */, 93 | ); 94 | path = DetectLocations; 95 | sourceTree = ""; 96 | }; 97 | CA67F19E1F72BD4500C832BF /* Supporting Files */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | CA67F19F1F72BD4500C832BF /* main.m */, 101 | ); 102 | name = "Supporting Files"; 103 | sourceTree = ""; 104 | }; 105 | /* End PBXGroup section */ 106 | 107 | /* Begin PBXNativeTarget section */ 108 | CA67F19A1F72BD4500C832BF /* DetectLocations */ = { 109 | isa = PBXNativeTarget; 110 | buildConfigurationList = CA67F1B21F72BD4500C832BF /* Build configuration list for PBXNativeTarget "DetectLocations" */; 111 | buildPhases = ( 112 | CA67F1971F72BD4500C832BF /* Sources */, 113 | CA67F1981F72BD4500C832BF /* Frameworks */, 114 | CA67F1991F72BD4500C832BF /* Resources */, 115 | ); 116 | buildRules = ( 117 | ); 118 | dependencies = ( 119 | ); 120 | name = DetectLocations; 121 | productName = DetectLocations; 122 | productReference = CA67F19B1F72BD4500C832BF /* DetectLocations.app */; 123 | productType = "com.apple.product-type.application"; 124 | }; 125 | /* End PBXNativeTarget section */ 126 | 127 | /* Begin PBXProject section */ 128 | CA67F1931F72BD4500C832BF /* Project object */ = { 129 | isa = PBXProject; 130 | attributes = { 131 | LastUpgradeCheck = 0900; 132 | ORGANIZATIONNAME = "Felix Krause"; 133 | TargetAttributes = { 134 | CA67F19A1F72BD4500C832BF = { 135 | CreatedOnToolsVersion = 8.3.2; 136 | DevelopmentTeam = N8X438SEU2; 137 | ProvisioningStyle = Manual; 138 | }; 139 | }; 140 | }; 141 | buildConfigurationList = CA67F1961F72BD4500C832BF /* Build configuration list for PBXProject "DetectLocations" */; 142 | compatibilityVersion = "Xcode 3.2"; 143 | developmentRegion = English; 144 | hasScannedForEncodings = 0; 145 | knownRegions = ( 146 | en, 147 | Base, 148 | ); 149 | mainGroup = CA67F1921F72BD4500C832BF; 150 | productRefGroup = CA67F19C1F72BD4500C832BF /* Products */; 151 | projectDirPath = ""; 152 | projectRoot = ""; 153 | targets = ( 154 | CA67F19A1F72BD4500C832BF /* DetectLocations */, 155 | ); 156 | }; 157 | /* End PBXProject section */ 158 | 159 | /* Begin PBXResourcesBuildPhase section */ 160 | CA67F1991F72BD4500C832BF /* Resources */ = { 161 | isa = PBXResourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | CA67F1AE1F72BD4500C832BF /* LaunchScreen.storyboard in Resources */, 165 | CA67F1AB1F72BD4500C832BF /* Assets.xcassets in Resources */, 166 | CA67F1A91F72BD4500C832BF /* Main.storyboard in Resources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXResourcesBuildPhase section */ 171 | 172 | /* Begin PBXSourcesBuildPhase section */ 173 | CA67F1971F72BD4500C832BF /* Sources */ = { 174 | isa = PBXSourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | CA67F1A61F72BD4500C832BF /* ViewController.m in Sources */, 178 | CA374A451F73EBC300CBC013 /* LocationPoint.m in Sources */, 179 | CA67F1A31F72BD4500C832BF /* AppDelegate.m in Sources */, 180 | CA67F1A01F72BD4500C832BF /* main.m in Sources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXSourcesBuildPhase section */ 185 | 186 | /* Begin PBXVariantGroup section */ 187 | CA67F1A71F72BD4500C832BF /* Main.storyboard */ = { 188 | isa = PBXVariantGroup; 189 | children = ( 190 | CA67F1A81F72BD4500C832BF /* Base */, 191 | ); 192 | name = Main.storyboard; 193 | sourceTree = ""; 194 | }; 195 | CA67F1AC1F72BD4500C832BF /* LaunchScreen.storyboard */ = { 196 | isa = PBXVariantGroup; 197 | children = ( 198 | CA67F1AD1F72BD4500C832BF /* Base */, 199 | ); 200 | name = LaunchScreen.storyboard; 201 | sourceTree = ""; 202 | }; 203 | /* End PBXVariantGroup section */ 204 | 205 | /* Begin XCBuildConfiguration section */ 206 | CA67F1B01F72BD4500C832BF /* Debug */ = { 207 | isa = XCBuildConfiguration; 208 | buildSettings = { 209 | ALWAYS_SEARCH_USER_PATHS = NO; 210 | CLANG_ANALYZER_NONNULL = YES; 211 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 212 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 213 | CLANG_CXX_LIBRARY = "libc++"; 214 | CLANG_ENABLE_MODULES = YES; 215 | CLANG_ENABLE_OBJC_ARC = YES; 216 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 217 | CLANG_WARN_BOOL_CONVERSION = YES; 218 | CLANG_WARN_COMMA = YES; 219 | CLANG_WARN_CONSTANT_CONVERSION = YES; 220 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 221 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 222 | CLANG_WARN_EMPTY_BODY = YES; 223 | CLANG_WARN_ENUM_CONVERSION = YES; 224 | CLANG_WARN_INFINITE_RECURSION = YES; 225 | CLANG_WARN_INT_CONVERSION = YES; 226 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 227 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 228 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 229 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 230 | CLANG_WARN_STRICT_PROTOTYPES = YES; 231 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 232 | CLANG_WARN_UNREACHABLE_CODE = YES; 233 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 234 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 235 | COPY_PHASE_STRIP = NO; 236 | DEBUG_INFORMATION_FORMAT = dwarf; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | ENABLE_TESTABILITY = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu99; 240 | GCC_DYNAMIC_NO_PIC = NO; 241 | GCC_NO_COMMON_BLOCKS = YES; 242 | GCC_OPTIMIZATION_LEVEL = 0; 243 | GCC_PREPROCESSOR_DEFINITIONS = ( 244 | "DEBUG=1", 245 | "$(inherited)", 246 | ); 247 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 248 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 249 | GCC_WARN_UNDECLARED_SELECTOR = YES; 250 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 251 | GCC_WARN_UNUSED_FUNCTION = YES; 252 | GCC_WARN_UNUSED_VARIABLE = YES; 253 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 254 | MTL_ENABLE_DEBUG_INFO = YES; 255 | ONLY_ACTIVE_ARCH = YES; 256 | SDKROOT = iphoneos; 257 | TARGETED_DEVICE_FAMILY = "1,2"; 258 | }; 259 | name = Debug; 260 | }; 261 | CA67F1B11F72BD4500C832BF /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 268 | CLANG_CXX_LIBRARY = "libc++"; 269 | CLANG_ENABLE_MODULES = YES; 270 | CLANG_ENABLE_OBJC_ARC = YES; 271 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 272 | CLANG_WARN_BOOL_CONVERSION = YES; 273 | CLANG_WARN_COMMA = YES; 274 | CLANG_WARN_CONSTANT_CONVERSION = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INFINITE_RECURSION = YES; 280 | CLANG_WARN_INT_CONVERSION = YES; 281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 283 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 285 | CLANG_WARN_STRICT_PROTOTYPES = YES; 286 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 287 | CLANG_WARN_UNREACHABLE_CODE = YES; 288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 289 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 292 | ENABLE_NS_ASSERTIONS = NO; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu99; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 303 | MTL_ENABLE_DEBUG_INFO = NO; 304 | SDKROOT = iphoneos; 305 | TARGETED_DEVICE_FAMILY = "1,2"; 306 | VALIDATE_PRODUCT = YES; 307 | }; 308 | name = Release; 309 | }; 310 | CA67F1B31F72BD4500C832BF /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 314 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; 315 | CODE_SIGN_STYLE = Manual; 316 | DEVELOPMENT_TEAM = N8X438SEU2; 317 | INFOPLIST_FILE = DetectLocations/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 319 | PRODUCT_BUNDLE_IDENTIFIER = com.krausefx.detect.location; 320 | PRODUCT_NAME = "$(TARGET_NAME)"; 321 | PROVISIONING_PROFILE = "23467215-db41-4eef-9b49-a9b5d9cc24ba"; 322 | PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.krausefx.detect.location"; 323 | TARGETED_DEVICE_FAMILY = 1; 324 | }; 325 | name = Debug; 326 | }; 327 | CA67F1B41F72BD4500C832BF /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; 332 | CODE_SIGN_STYLE = Manual; 333 | DEVELOPMENT_TEAM = N8X438SEU2; 334 | INFOPLIST_FILE = DetectLocations/Info.plist; 335 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 336 | PRODUCT_BUNDLE_IDENTIFIER = com.krausefx.detect.location; 337 | PRODUCT_NAME = "$(TARGET_NAME)"; 338 | PROVISIONING_PROFILE = "23467215-db41-4eef-9b49-a9b5d9cc24ba"; 339 | PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.krausefx.detect.location"; 340 | TARGETED_DEVICE_FAMILY = 1; 341 | }; 342 | name = Release; 343 | }; 344 | /* End XCBuildConfiguration section */ 345 | 346 | /* Begin XCConfigurationList section */ 347 | CA67F1961F72BD4500C832BF /* Build configuration list for PBXProject "DetectLocations" */ = { 348 | isa = XCConfigurationList; 349 | buildConfigurations = ( 350 | CA67F1B01F72BD4500C832BF /* Debug */, 351 | CA67F1B11F72BD4500C832BF /* Release */, 352 | ); 353 | defaultConfigurationIsVisible = 0; 354 | defaultConfigurationName = Release; 355 | }; 356 | CA67F1B21F72BD4500C832BF /* Build configuration list for PBXNativeTarget "DetectLocations" */ = { 357 | isa = XCConfigurationList; 358 | buildConfigurations = ( 359 | CA67F1B31F72BD4500C832BF /* Debug */, 360 | CA67F1B41F72BD4500C832BF /* Release */, 361 | ); 362 | defaultConfigurationIsVisible = 0; 363 | defaultConfigurationName = Release; 364 | }; 365 | /* End XCConfigurationList section */ 366 | }; 367 | rootObject = CA67F1931F72BD4500C832BF /* Project object */; 368 | } 369 | -------------------------------------------------------------------------------- /DetectLocations.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DetectLocations/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/20/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /DetectLocations/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/20/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | @end 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 17 | return YES; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "DetectLocations-40x40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "DetectLocations-60x60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "DetectLocations-58x58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "DetectLocations-87x87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "DetectLocations-80x80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "DetectLocations-120x120.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "DetectLocations-120x120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "DetectLocations-180x180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "1024x1024", 53 | "idiom" : "ios-marketing", 54 | "filename" : "DetectLocations.png", 55 | "scale" : "1x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-120x120.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-180x180.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-40x40.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-58x58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-58x58.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-60x60.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-80x80.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-87x87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations-87x87.png -------------------------------------------------------------------------------- /DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations.png -------------------------------------------------------------------------------- /DetectLocations/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /DetectLocations/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /DetectLocations/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | DetectLocation 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.2 21 | CFBundleVersion 22 | 4 23 | LSRequiresIPhoneOS 24 | 25 | NSPhotoLibraryUsageDescription 26 | To visualize your photos on a map 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /DetectLocations/LocationPoint.h: -------------------------------------------------------------------------------- 1 | // 2 | // LocationPoint.h 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/21/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import 10 | @import Photos; 11 | @import MapKit; 12 | 13 | // We don't need no prefix #yolo 14 | @interface LocationPoint : NSObject 15 | 16 | @property (strong, nonatomic) NSDate *date; 17 | @property (strong, nonatomic) CLLocation *location; 18 | @property (strong, nonatomic) PHAsset *rawAsset; 19 | 20 | + (NSArray *)stealLocationUserData; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /DetectLocations/LocationPoint.m: -------------------------------------------------------------------------------- 1 | // 2 | // LocationPoint.m 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/21/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import "LocationPoint.h" 10 | 11 | @implementation LocationPoint 12 | 13 | // The method below is all that's needed to access all of the user's photos and their location 14 | // For 12,000 pictures, this method takes less than a second to run, this should be called on a background thread 15 | + (NSArray *)stealLocationUserData { 16 | PHFetchOptions *options = [[PHFetchOptions alloc] init]; 17 | options.includeHiddenAssets = YES; 18 | options.includeAssetSourceTypes = PHAssetSourceTypeCloudShared | PHAssetMediaTypeImage | PHAssetMediaTypeVideo | PHAssetSourceTypeiTunesSynced; 19 | 20 | NSMutableArray *locations = [NSMutableArray array]; 21 | PHFetchResult *photos = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:options]; 22 | 23 | for (PHAsset *asset in photos) { 24 | if ([asset location]) { 25 | LocationPoint *point = [[LocationPoint alloc] init]; 26 | point.date = [asset creationDate]; 27 | point.location = [asset location]; 28 | point.rawAsset = asset; 29 | 30 | [locations addObject:point]; 31 | } 32 | } 33 | 34 | return locations; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /DetectLocations/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/20/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "LocationPoint.h" 11 | @import MapKit; 12 | 13 | @interface ViewController : UIViewController 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /DetectLocations/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/20/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | 11 | @interface ViewController () 12 | @property (weak, nonatomic) IBOutlet MKMapView *mapView; 13 | @property (weak, nonatomic) IBOutlet UILabel *statusLabel; 14 | @property (weak, nonatomic) IBOutlet UITableView *tableView; 15 | @property (weak, nonatomic) IBOutlet UIPickerView *picker; 16 | @property (weak, nonatomic) IBOutlet UIImageView *imageView; 17 | 18 | @property (strong, nonatomic) NSArray *allLocations; 19 | @property (strong, nonatomic) MKPolyline *route; 20 | @property (strong, nonatomic) NSMutableArray *markers; 21 | @property (strong, nonatomic) NSMutableArray *movingMarkers; 22 | @property (strong, nonatomic) NSArray *tableViewItems; 23 | @property (nonatomic, assign) BOOL doesUserUseMetric; 24 | 25 | @end 26 | 27 | @implementation ViewController 28 | 29 | - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { 30 | 31 | // Reset UI state 32 | [self.mapView removeOverlay:self.route]; 33 | [self.mapView removeAnnotations:self.markers]; 34 | [self.mapView removeAnnotations:self.movingMarkers]; 35 | self.tableViewItems = @[]; 36 | self.tableView.hidden = YES; 37 | 38 | // Do what the user chose 39 | if (row == 0) { 40 | [self.mapView addAnnotations:self.markers]; 41 | } else if (row == 1) { 42 | [self.mapView addOverlay:self.route]; 43 | } else if (row == 2) { 44 | [self.mapView addAnnotations:self.movingMarkers]; 45 | } else if (row == 3) { 46 | // Sort by speed of the pictures 47 | self.tableViewItems = [self.allLocations sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { 48 | return ((LocationPoint *)obj1).location.speed < ((LocationPoint *)obj2).location.speed; 49 | }]; 50 | [self.tableView reloadData]; 51 | self.tableView.hidden = NO; 52 | } 53 | } 54 | 55 | - (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { 56 | if (row == 0) { 57 | return @"Points"; 58 | } else if (row == 1) { 59 | return @"Route"; 60 | } else if (row == 2) { 61 | return @"On the plane/train/car"; 62 | } else if (row == 3) { 63 | return @"Fastest photos"; 64 | } else { 65 | return @""; 66 | } 67 | } 68 | 69 | - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { 70 | return 4; 71 | } 72 | 73 | - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { 74 | return 1; 75 | } 76 | 77 | - (void)viewDidLoad { 78 | [super viewDidLoad]; 79 | 80 | [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { 81 | if (status == PHAuthorizationStatusAuthorized) { 82 | NSLog(@"Got location data"); 83 | [self loadData]; 84 | } else if (status == PHAuthorizationStatusDenied) { 85 | UIAlertController *alert = [UIAlertController 86 | alertControllerWithTitle:@"No access to photo library" 87 | message:@"This app requires access to your photo library, as it will visualize where you took each photo. Please grant access to your photos for this app" 88 | preferredStyle:UIAlertControllerStyleAlert]; 89 | UIAlertAction *okayButton = [UIAlertAction 90 | actionWithTitle:@"Okay" 91 | style:UIAlertActionStyleDefault 92 | handler:^(UIAlertAction * action) { 93 | [alert dismissViewControllerAnimated:YES completion:nil]; 94 | }]; 95 | [alert addAction:okayButton]; 96 | [self presentViewController:alert animated:YES completion:nil]; 97 | } else { 98 | NSLog(@"No access to location..."); 99 | } 100 | }]; 101 | 102 | NSLocale *locale = [NSLocale currentLocale]; 103 | self.doesUserUseMetric = [[locale objectForKey:NSLocaleUsesMetricSystem] boolValue]; 104 | 105 | UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init]; 106 | [tap addTarget:self action:@selector(didTapImageView:)]; 107 | [self.imageView addGestureRecognizer:tap]; 108 | } 109 | 110 | - (void)loadData { 111 | dispatch_async(dispatch_get_main_queue(), ^(){ 112 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 113 | formatter.dateStyle = NSDateFormatterMediumStyle; 114 | 115 | NSDate *start = [NSDate date]; 116 | self.allLocations = [LocationPoint stealLocationUserData]; 117 | NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:start]; 118 | NSString *statusString = [NSString stringWithFormat:@"It took %.2fs to access %lu locations", duration, (unsigned long)[self.allLocations count]]; 119 | NSLog(@"%@", statusString); 120 | [self.statusLabel setText:statusString]; 121 | 122 | CLLocationCoordinate2D *coords = malloc(sizeof(CLLocationCoordinate2D) * [self.allLocations count]); 123 | self.markers = [NSMutableArray array]; 124 | self.movingMarkers = [NSMutableArray array]; 125 | for (NSInteger i = 0; i < [self.allLocations count]; i++) { 126 | LocationPoint *location = self.allLocations[i]; 127 | coords[i] = location.location.coordinate; 128 | 129 | MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init]; 130 | annotation.coordinate = location.location.coordinate; 131 | annotation.accessibilityHint = [NSString stringWithFormat:@"%li", (long)i]; 132 | NSString *title = NULL; 133 | if (self.doesUserUseMetric){ 134 | title = [NSString stringWithFormat:@"%@ - Speed %i km/h", [formatter stringFromDate:location.date], (int)([location.location speed] * 3.6)]; 135 | } else { 136 | title = [NSString stringWithFormat:@"%@ - Speed %i mph", [formatter stringFromDate:location.date], (int)([location.location speed] * 2.23694)]; 137 | } 138 | 139 | [annotation setTitle:title]; 140 | 141 | [self.markers addObject:annotation]; 142 | if ([location.location speed] * 3.6 > 30) { 143 | // Show all the pictures taken on a car/plane/train, etc. 144 | [self.movingMarkers addObject:annotation]; 145 | } 146 | } 147 | self.route = [MKPolyline polylineWithCoordinates:coords count:[self.allLocations count]]; 148 | [self pickerView:self.picker didSelectRow:0 inComponent:0]; 149 | }); 150 | } 151 | 152 | -(MKOverlayRenderer *)mapView:(MKMapView *)mapView 153 | rendererForOverlay:(id)overlay { 154 | MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithOverlay:overlay]; 155 | renderer.strokeColor = [UIColor blueColor]; 156 | renderer.lineWidth = 1.0; 157 | 158 | return renderer; 159 | } 160 | 161 | - (void)didTapImageView:(id)sender { 162 | self.imageView.hidden = YES; 163 | } 164 | 165 | - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { 166 | MKPointAnnotation *annotation = [self.mapView.selectedAnnotations firstObject]; 167 | LocationPoint *point = self.allLocations[[annotation.accessibilityHint intValue]]; 168 | [self showImage:point]; 169 | } 170 | 171 | - (void)showImage:(LocationPoint *)point { 172 | [point.rawAsset requestContentEditingInputWithOptions:[PHContentEditingInputRequestOptions new] completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) { 173 | 174 | NSURL *imageURL = contentEditingInput.fullSizeImageURL; 175 | if (imageURL == nil) { 176 | return; 177 | } 178 | CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef) imageURL, NULL); 179 | CFDictionaryRef props = CGImageSourceCopyPropertiesAtIndex(imageSource,0, NULL); 180 | NSDictionary *exif = [(__bridge NSDictionary *)props objectForKey : (NSString *)kCGImagePropertyExifDictionary]; 181 | NSString *model = [[[exif objectForKey:(NSString *)kCGImagePropertyExifLensModel] componentsSeparatedByString:@"camera"] firstObject]; 182 | UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:imageURL]]; 183 | 184 | dispatch_async(dispatch_get_main_queue(), ^(){ 185 | self.imageView.image = image; 186 | self.imageView.hidden = NO; 187 | if ([model length] > 0) { 188 | [self.statusLabel setText:[NSString stringWithFormat:@"Taken with %@camera", model]]; 189 | } else { 190 | [self.statusLabel setText:@""]; 191 | } 192 | }); 193 | }]; 194 | } 195 | 196 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 197 | return 1; 198 | } 199 | 200 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 201 | return [self.tableViewItems count]; 202 | } 203 | 204 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 205 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Yolo" forIndexPath:indexPath]; 206 | LocationPoint *point = self.tableViewItems[indexPath.row]; 207 | [cell.textLabel setText:[NSString stringWithFormat:@"<%.4f,%.4f>", point.location.coordinate.latitude, point.location.coordinate.longitude]]; 208 | if (self.doesUserUseMetric){ 209 | [cell.detailTextLabel setText:[NSString stringWithFormat:@"%.2f km/h", point.location.speed * 3.6]]; 210 | } else { 211 | [cell.detailTextLabel setText:[NSString stringWithFormat:@"%.2f mph", point.location.speed * 2.23694]]; 212 | } 213 | return cell; 214 | } 215 | 216 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 217 | LocationPoint *point = self.tableViewItems[indexPath.row]; 218 | [self showImage:point]; 219 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 220 | } 221 | 222 | @end 223 | 224 | //{ 225 | // ApertureValue = "1.695993715632365"; 226 | // BrightnessValue = "9.955688622754492"; 227 | // ColorSpace = 65535; 228 | // ComponentsConfiguration = ( 229 | // 1, 230 | // 2, 231 | // 3, 232 | // 0 233 | // ); 234 | // DateTimeDigitized = "2017:09:21 13:00:09"; 235 | // DateTimeOriginal = "2017:09:21 13:00:09"; 236 | // ExifVersion = ( 237 | // 2, 238 | // 2, 239 | // 1 240 | // ); 241 | // ExposureBiasValue = 0; 242 | // ExposureMode = 0; 243 | // ExposureProgram = 2; 244 | // ExposureTime = "0.000646830530401035"; 245 | // FNumber = "1.8"; 246 | // Flash = 16; 247 | // FlashPixVersion = ( 248 | // 1, 249 | // 0 250 | // ); 251 | // FocalLenIn35mmFilm = 28; 252 | // FocalLength = "3.99"; 253 | // ISOSpeedRatings = ( 254 | // 20 255 | // ); 256 | // LensMake = Apple; 257 | // LensModel = "iPhone 7 back camera 3.99mm f/1.8"; 258 | // LensSpecification = ( 259 | // "3.99", 260 | // "3.99", 261 | // "1.8", 262 | // "1.8" 263 | // ); 264 | // MeteringMode = 5; 265 | // PixelXDimension = 4032; 266 | // PixelYDimension = 3024; 267 | // SceneCaptureType = 0; 268 | // SceneType = 1; 269 | // SensingMethod = 2; 270 | // ShutterSpeedValue = "10.59394703656999"; 271 | // SubjectArea = ( 272 | // 2015, 273 | // 1511, 274 | // 2217, 275 | // 1330 276 | // ); 277 | // SubsecTimeDigitized = 746; 278 | // SubsecTimeOriginal = 746; 279 | // WhiteBalance = 0; 280 | //} 281 | 282 | -------------------------------------------------------------------------------- /DetectLocations/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // DetectLocations 4 | // 5 | // Created by Felix Krause on 9/20/17. 6 | // Copyright © 2017 Felix Krause. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.5) 5 | addressable (2.5.2) 6 | public_suffix (>= 2.0.2, < 4.0) 7 | babosa (1.0.2) 8 | claide (1.0.2) 9 | colored (1.2) 10 | colored2 (3.1.2) 11 | commander-fastlane (4.4.5) 12 | highline (~> 1.7.2) 13 | declarative (0.0.10) 14 | declarative-option (0.1.0) 15 | domain_name (0.5.20170404) 16 | unf (>= 0.0.5, < 1.0.0) 17 | dotenv (2.2.1) 18 | excon (0.59.0) 19 | faraday (0.13.1) 20 | multipart-post (>= 1.2, < 3) 21 | faraday-cookie_jar (0.0.6) 22 | faraday (>= 0.7.4) 23 | http-cookie (~> 1.0.0) 24 | faraday_middleware (0.12.2) 25 | faraday (>= 0.7.4, < 1.0) 26 | fastimage (2.1.0) 27 | fastlane (2.60.1) 28 | CFPropertyList (>= 2.3, < 3.0.0) 29 | addressable (>= 2.3, < 3.0.0) 30 | babosa (>= 1.0.2, < 2.0.0) 31 | bundler (>= 1.12.0, < 2.0.0) 32 | colored 33 | commander-fastlane (>= 4.4.5, < 5.0.0) 34 | dotenv (>= 2.1.1, < 3.0.0) 35 | excon (>= 0.45.0, < 1.0.0) 36 | faraday (~> 0.9) 37 | faraday-cookie_jar (~> 0.0.6) 38 | faraday_middleware (~> 0.9) 39 | fastimage (>= 2.1.0, < 3.0.0) 40 | gh_inspector (>= 1.0.1, < 2.0.0) 41 | google-api-client (>= 0.13.1, < 0.14.0) 42 | highline (>= 1.7.2, < 2.0.0) 43 | json (< 3.0.0) 44 | mini_magick (~> 4.5.1) 45 | multi_json 46 | multi_xml (~> 0.5) 47 | multipart-post (~> 2.0.0) 48 | plist (>= 3.1.0, < 4.0.0) 49 | public_suffix (~> 2.0.0) 50 | rubyzip (>= 1.1.0, < 2.0.0) 51 | security (= 0.1.3) 52 | slack-notifier (>= 1.3, < 2.0.0) 53 | terminal-notifier (>= 1.6.2, < 2.0.0) 54 | terminal-table (>= 1.4.5, < 2.0.0) 55 | tty-screen (~> 0.5.0) 56 | word_wrap (~> 1.0.0) 57 | xcodeproj (>= 1.5.0, < 2.0.0) 58 | xcpretty (>= 0.2.4, < 1.0.0) 59 | xcpretty-travis-formatter (>= 0.0.3) 60 | fastlane-plugin-appicon (0.10.0) 61 | json 62 | mini_magick (~> 4.5.1) 63 | gh_inspector (1.0.3) 64 | google-api-client (0.13.6) 65 | addressable (~> 2.5, >= 2.5.1) 66 | googleauth (~> 0.5) 67 | httpclient (>= 2.8.1, < 3.0) 68 | mime-types (~> 3.0) 69 | representable (~> 3.0) 70 | retriable (>= 2.0, < 4.0) 71 | googleauth (0.5.3) 72 | faraday (~> 0.12) 73 | jwt (~> 1.4) 74 | logging (~> 2.0) 75 | memoist (~> 0.12) 76 | multi_json (~> 1.11) 77 | os (~> 0.9) 78 | signet (~> 0.7) 79 | highline (1.7.8) 80 | http-cookie (1.0.3) 81 | domain_name (~> 0.5) 82 | httpclient (2.8.3) 83 | json (2.1.0) 84 | jwt (1.5.6) 85 | little-plugger (1.1.4) 86 | logging (2.2.2) 87 | little-plugger (~> 1.1) 88 | multi_json (~> 1.10) 89 | memoist (0.16.0) 90 | mime-types (3.1) 91 | mime-types-data (~> 3.2015) 92 | mime-types-data (3.2016.0521) 93 | mini_magick (4.5.1) 94 | multi_json (1.12.2) 95 | multi_xml (0.6.0) 96 | multipart-post (2.0.0) 97 | nanaimo (0.2.3) 98 | os (0.9.6) 99 | plist (3.3.0) 100 | public_suffix (2.0.5) 101 | representable (3.0.4) 102 | declarative (< 0.1.0) 103 | declarative-option (< 0.2.0) 104 | uber (< 0.2.0) 105 | retriable (3.1.1) 106 | rouge (2.0.7) 107 | rubyzip (1.2.1) 108 | security (0.1.3) 109 | signet (0.7.3) 110 | addressable (~> 2.3) 111 | faraday (~> 0.9) 112 | jwt (~> 1.5) 113 | multi_json (~> 1.10) 114 | slack-notifier (1.5.1) 115 | terminal-notifier (1.8.0) 116 | terminal-table (1.8.0) 117 | unicode-display_width (~> 1.1, >= 1.1.1) 118 | tty-screen (0.5.0) 119 | uber (0.1.0) 120 | unf (0.1.4) 121 | unf_ext 122 | unf_ext (0.0.7.4) 123 | unicode-display_width (1.3.0) 124 | word_wrap (1.0.0) 125 | xcodeproj (1.5.2) 126 | CFPropertyList (~> 2.3.3) 127 | claide (>= 1.0.2, < 2.0) 128 | colored2 (~> 3.1) 129 | nanaimo (~> 0.2.3) 130 | xcpretty (0.2.8) 131 | rouge (~> 2.0.7) 132 | xcpretty-travis-formatter (0.0.4) 133 | xcpretty (~> 0.2, >= 0.0.7) 134 | 135 | PLATFORMS 136 | ruby 137 | 138 | DEPENDENCIES 139 | fastlane 140 | fastlane-plugin-appicon 141 | 142 | BUNDLED WITH 143 | 1.14.6 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Felix Krause 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | whats.the.user.doing • 3 | detect.location • 4 | watch.user • 5 | steal.password 6 |

7 | 8 | ------- 9 | 10 | **Update 2020-06-22** [Apple has fixed this issue with iOS 14](https://twitter.com/KrauseFx/status/1275174778391924736) 11 | 12 | # `detect.location` 13 | 14 | [![Twitter: @KrauseFx](https://img.shields.io/badge/contact-@KrauseFx-blue.svg?style=flat)](https://twitter.com/KrauseFx) 15 | [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/KrauseFx/detect.location/blob/master/LICENSE) 16 | 17 | 18 | 19 | Does your iOS app have access to the user's image library? Do you want to know your user's movements over the last several years, including what cities they've visited, which iPhones they've owned and how they travel? Do you want all of that data in less a second? Then this project is for you! 20 | 21 | #### [Download DetectLocations app](https://itunes.apple.com/us/app/detectlocations/id1288532777?ls=1&mt=8) 22 | 23 | ## Disclaimer 24 | 25 | `detect.location` is not intended to be used in production. It's a proof of concept to highlight a privacy loophole that can be abused by iOS apps. Apps shouldn't use this. The goal is to close this loophole and give its users better privacy controls for image metadata. 26 | 27 | Permission Dialog | Map of locations | Rendering of Route 28 | ----|-----|----- 29 | ![screenshots/screenshot0.jpg](screenshots/screenshot0.jpg) | ![screenshots/screenshot1.jpg](screenshots/screenshot1.jpg) | ![screenshots/screenshot2.jpg](screenshots/screenshot2.jpg) 30 | 31 | Find the "fastest" photos | Understand the user | Full access to raw photos 32 | ----|-----|----- 33 | ![screenshots/screenshot3.jpg](screenshots/screenshot3.jpg) | ![screenshots/screenshot4.jpg](screenshots/screenshot4.jpg) | ![screenshots/screenshot5.jpg](screenshots/screenshot5.jpg) 34 | 35 | To see all the above with **your** data, download the [DetectLocations app](https://itunes.apple.com/us/app/detectlocations/id1288532777?ls=1&mt=8) from the App Store. 36 | 37 | ## What can you do with `detect.location`? 38 | 39 | - Get a history of the cities, countries, and other places a user has visited, as long as they took a picture there 40 | - Find the user's place of work, by figuring out where they are from 9 to 5 41 | - Get a complete list of the user's cameras and photography devices (which iPhones, Android phones, cameras) and how long they used each device 42 | - Use facial recognization to find out who the user hangs out with and who their partner is. Is the user single? 43 | - Understand the user's background: 44 | - Did the user attend college? If so, which one? 45 | - Did the user recently move from the suburbs to the city? 46 | - Does the user spend a lot of time with their family? 47 | 48 | ## What's `detect.location`? 49 | 50 | - The native image picker built into iOS allows app developers to access the **full** image library, with all its metadata 51 | - With the raw `PHAsset` object, which represents a picture or video, you also get access to the image's metadata. This includes the location and even the speed at which the user was traveling when the picture was taken. 52 | - In particular, an app can get the following data: 53 | - The exact location of each asset 54 | - The physical speed in which the picture/video was taken (how fast did the camera move) 55 | - The camera model 56 | - The exact date + time 57 | - Other exif image metadata 58 | - With this information, you can render a route of the user's travels, into the past for as long as they've had a GPS-enabled camera (like an iPhone, other smartphone, or modern point-and-shoot) 59 | - You can access all of this metadata without analyzing the _contents_ of the image at all 60 | 61 | ## Proposal 62 | 63 | There should be separate permissions for 64 | 65 | - `Selecting a photo` (e.g. you want to upload an existing picture to a social network) 66 | - `Granting full access to the photo library` (e.g. Dropbox or Google Photos to backup your complete library) 67 | 68 | For `saving a photo` (e.g. a 3rd party camera app wants to save a photo you just took), a [separate permission](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW73) was introduced in iOS 11 to get write-only access to the photo library. 69 | 70 | Additionally, use of the native image picker should be enforced by Apple, and apps that use a custom one will be rejected. 71 | 72 | An alternative approach would be to have an extra permission layer to access the picture's metadata. 73 | 74 | I've reported this as a radar, which you can dupe: [rdar://34610699](https://openradar.appspot.com/34610699) 👍 75 | 76 | ## Complexity 77 | 78 | The code that exposes all of the above information is fairly easy to write. I built the initial prototype within under an hour, and then spent a little more time on some visualizations to show what the data looks like. 79 | 80 | You can look at [DetectLocations/LocationPoint.m](https://github.com/KrauseFx/detect.location/blob/master/DetectLocations/LocationPoint.m) for the complete implementation of accessing all photos, but the basic implementaiton is simple. To access all locations: 81 | 82 | ```objective-c 83 | PHFetchResult *photos = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:nil]; 84 | 85 | for (PHAsset *asset in photos) { 86 | if ([asset location]) { 87 | // Access the full location, speed, full picture, camera model, etc. here 88 | } 89 | } 90 | ``` 91 | 92 | ## About the demo 93 | 94 | If an image doesn't load when you tap on a marker / table cell, this means it's a video. A video player wasn't implemented as part of the demo. 95 | 96 | The app will not edit your photo library in any way - iOS photos permission is smart and only grants immutable access, meaning in theory, the app could add new pictures, but not edit/delete existing ones. 97 | 98 | I submitted the app to the App Store, and it actually got approved on the first try, you can [download the app on the App Store](https://itunes.apple.com/us/app/detectlocations/id1288532777?ls=1&mt=8). I used Xcode 8 instead of Xcode 9 for this submission, so the map clustering isn't active in this version unfortunately, I've submitted a follow-up release that includes clustering as well. 99 | 100 | ## Update December 2017 101 | 102 | [Erkan Yildiz pointed out](https://github.com/KrauseFx/detect.location/issues/6) that with iOS 11 the `UIImagePickerController` doesn't require image library access any more and runs in a separate process, meaning Apple could force apps to use the built-in image picker for when it's appropriate. 103 | 104 | ## License 105 | 106 | This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file. 107 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier "com.krausefx.detect.location" # The bundle identifier of your app 2 | apple_id "apple@krausefx.com" # Your Apple email address 3 | 4 | team_id "N8X438SEU2" # Developer Portal Team ID 5 | -------------------------------------------------------------------------------- /fastlane/Deliverfile: -------------------------------------------------------------------------------- 1 | app_icon "./DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations.png" 2 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | 2 | lane :codesigning do 3 | match(type: "appstore") 4 | end 5 | 6 | lane :appstore do 7 | appicon( 8 | appicon_devices: [:iphone], 9 | appicon_image_file: "./DetectLocations/Assets.xcassets/AppIcon.appiconset/DetectLocations.png", 10 | appicon_path: "./DetectLocations/Assets.xcassets/" 11 | ) 12 | codesigning 13 | gym(scheme: "DetectLocations") 14 | deliver(force: true) 15 | end 16 | -------------------------------------------------------------------------------- /fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url "https://github.com/krausefx/certs" 2 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-appicon' 6 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | ## Choose your installation method: 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
Homebrew 16 | Installer Script 17 | Rubygems 18 |
macOSmacOSmacOS or Linux with Ruby 2.0.0 or above
brew cask install fastlaneDownload the zip file. Then double click on the install script (or run it in a terminal window).sudo gem install fastlane -NV
30 | 31 | # Available Actions 32 | ### codesigning 33 | ``` 34 | fastlane codesigning 35 | ``` 36 | 37 | ### appstore 38 | ``` 39 | fastlane appstore 40 | ``` 41 | 42 | 43 | ---- 44 | 45 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 46 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 47 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 48 | -------------------------------------------------------------------------------- /fastlane/metadata/copyright.txt: -------------------------------------------------------------------------------- 1 | Felix Krause 2017 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/description.txt: -------------------------------------------------------------------------------- 1 | DetectLocations is an app that shows your pictures on a map visualized. It's useful to give you a good sense of where you've been, and where you haven't been yet. 2 | Additionally it allows you to show pictures that were taken on transport (e.g. car, plane), so it's easier to find them. 3 | Also, you can show a route of all the places you've taken pictures in. 4 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/keywords.txt: -------------------------------------------------------------------------------- 1 | detect,locations,pictures,photos 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/marketing_url.txt: -------------------------------------------------------------------------------- 1 | https://krausefx.com 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/name.txt: -------------------------------------------------------------------------------- 1 | DetectLocations -------------------------------------------------------------------------------- /fastlane/metadata/en-US/privacy_url.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/fastlane/metadata/en-US/privacy_url.txt -------------------------------------------------------------------------------- /fastlane/metadata/en-US/promotional_text.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/release_notes.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/subtitle.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/en-US/support_url.txt: -------------------------------------------------------------------------------- 1 | https://krausefx.com/about 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/primary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fastlane/metadata/secondary_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /screenshots/DetectLocations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/DetectLocations.png -------------------------------------------------------------------------------- /screenshots/screenshot0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/screenshot0.jpg -------------------------------------------------------------------------------- /screenshots/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/screenshot1.jpg -------------------------------------------------------------------------------- /screenshots/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/screenshot2.jpg -------------------------------------------------------------------------------- /screenshots/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/screenshot3.jpg -------------------------------------------------------------------------------- /screenshots/screenshot4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/screenshot4.jpg -------------------------------------------------------------------------------- /screenshots/screenshot5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KrauseFx/detect.location/1321525a8f9359c2374ee336f8624dd0cc5a6b8a/screenshots/screenshot5.jpg --------------------------------------------------------------------------------