├── OpenInPlace.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── ander.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── OpenInPlace.xcscheme └── xcuserdata │ └── ander.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── OpenInPlace ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── appstore1024.png │ │ ├── ipad152.png │ │ ├── ipad76.png │ │ ├── ipadNotification20.png │ │ ├── ipadNotification40.png │ │ ├── ipadPro167.png │ │ ├── ipadSettings29.png │ │ ├── ipadSettings58.png │ │ ├── ipadSpotlight40.png │ │ ├── ipadSpotlight80.png │ │ ├── iphone120.png │ │ ├── iphone180.png │ │ ├── mac1024.png │ │ ├── mac128.png │ │ ├── mac16.png │ │ ├── mac256.png │ │ ├── mac32.png │ │ ├── mac512.png │ │ ├── mac64.png │ │ ├── notification40.png │ │ ├── notification60.png │ │ ├── settings58.png │ │ ├── settings87.png │ │ ├── spotlight120.png │ │ └── spotlight80.png │ ├── Contents.json │ └── empty.imageset │ │ ├── Contents.json │ │ └── empty.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── EditController.swift ├── Info.plist ├── ListController.swift ├── OpenInPlace.entitlements ├── SceneDelegate.swift ├── ShowError.swift ├── UrlCoordination.swift ├── Working Copy │ ├── WorkingCopyUrlService.h │ ├── WorkingCopyUrlService.m │ └── WorkingCopyUrlService.swift ├── XCallbackOpener.swift ├── XDocumentNotify.swift └── XDocumentSource2.swift └── README.md /OpenInPlace.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BE0263C12D7C630A004304F9 /* XDocumentNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE0263C02D7C630A004304F9 /* XDocumentNotify.swift */; }; 11 | BE24B3EA1EFB02A3002D5D2A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE24B3E91EFB02A3002D5D2A /* AppDelegate.swift */; }; 12 | BE24B3EC1EFB02A3002D5D2A /* ListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE24B3EB1EFB02A3002D5D2A /* ListController.swift */; }; 13 | BE24B3EE1EFB02A3002D5D2A /* EditController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE24B3ED1EFB02A3002D5D2A /* EditController.swift */; }; 14 | BE24B3F11EFB02A3002D5D2A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BE24B3EF1EFB02A3002D5D2A /* Main.storyboard */; }; 15 | BE24B3F31EFB02A3002D5D2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BE24B3F21EFB02A3002D5D2A /* Assets.xcassets */; }; 16 | BE24B3F61EFB02A4002D5D2A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BE24B3F41EFB02A4002D5D2A /* LaunchScreen.storyboard */; }; 17 | BE5FCD052159570E0025C3F6 /* WorkingCopyUrlService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE5FCD042159570E0025C3F6 /* WorkingCopyUrlService.swift */; }; 18 | BE7BBD301EFBB947003FE5D4 /* UrlCoordination.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7BBD2F1EFBB947003FE5D4 /* UrlCoordination.swift */; }; 19 | BE7BBD321EFBBA26003FE5D4 /* ShowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7BBD311EFBBA26003FE5D4 /* ShowError.swift */; }; 20 | BE98E568221592F400672721 /* XDocumentSource2.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE98E567221592F400672721 /* XDocumentSource2.swift */; }; 21 | BEAC7B5F2962D9ED0022F97C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEAC7B5E2962D9ED0022F97C /* SceneDelegate.swift */; }; 22 | BEAC7B612962E1AA0022F97C /* XCallbackOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEAC7B602962E1A90022F97C /* XCallbackOpener.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | BE0263C02D7C630A004304F9 /* XDocumentNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDocumentNotify.swift; sourceTree = ""; }; 27 | BE24B3E61EFB02A3002D5D2A /* OpenInPlace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenInPlace.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | BE24B3E91EFB02A3002D5D2A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 29 | BE24B3EB1EFB02A3002D5D2A /* ListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListController.swift; sourceTree = ""; }; 30 | BE24B3ED1EFB02A3002D5D2A /* EditController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditController.swift; sourceTree = ""; }; 31 | BE24B3F01EFB02A3002D5D2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 32 | BE24B3F21EFB02A3002D5D2A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | BE24B3F51EFB02A4002D5D2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 34 | BE24B3F71EFB02A4002D5D2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | BE24B3FD1EFB02BD002D5D2A /* OpenInPlace.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInPlace.entitlements; sourceTree = ""; }; 36 | BE5FCD042159570E0025C3F6 /* WorkingCopyUrlService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkingCopyUrlService.swift; sourceTree = ""; }; 37 | BE7BBD2F1EFBB947003FE5D4 /* UrlCoordination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlCoordination.swift; sourceTree = ""; }; 38 | BE7BBD311EFBBA26003FE5D4 /* ShowError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowError.swift; sourceTree = ""; }; 39 | BE98E567221592F400672721 /* XDocumentSource2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XDocumentSource2.swift; sourceTree = ""; }; 40 | BEAC7B5E2962D9ED0022F97C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 41 | BEAC7B602962E1A90022F97C /* XCallbackOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCallbackOpener.swift; sourceTree = ""; }; 42 | BEC920D11EFBF9D100F142B4 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 43 | BEDCB31A2126FB32000547BA /* WorkingCopyUrlService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WorkingCopyUrlService.h; sourceTree = ""; }; 44 | BEDCB31B2126FB32000547BA /* WorkingCopyUrlService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WorkingCopyUrlService.m; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | BE24B3E31EFB02A3002D5D2A /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | BE24B3DD1EFB02A3002D5D2A = { 59 | isa = PBXGroup; 60 | children = ( 61 | BEC920D11EFBF9D100F142B4 /* README.md */, 62 | BE24B3E81EFB02A3002D5D2A /* OpenInPlace */, 63 | BE24B3E71EFB02A3002D5D2A /* Products */, 64 | ); 65 | sourceTree = ""; 66 | }; 67 | BE24B3E71EFB02A3002D5D2A /* Products */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | BE24B3E61EFB02A3002D5D2A /* OpenInPlace.app */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | BE24B3E81EFB02A3002D5D2A /* OpenInPlace */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | BEDCB31D2126FB3A000547BA /* Working Copy */, 79 | BE24B3E91EFB02A3002D5D2A /* AppDelegate.swift */, 80 | BE24B3ED1EFB02A3002D5D2A /* EditController.swift */, 81 | BE24B3EB1EFB02A3002D5D2A /* ListController.swift */, 82 | BEAC7B5E2962D9ED0022F97C /* SceneDelegate.swift */, 83 | BE7BBD311EFBBA26003FE5D4 /* ShowError.swift */, 84 | BE7BBD2F1EFBB947003FE5D4 /* UrlCoordination.swift */, 85 | BEAC7B602962E1A90022F97C /* XCallbackOpener.swift */, 86 | BE0263C02D7C630A004304F9 /* XDocumentNotify.swift */, 87 | BE98E567221592F400672721 /* XDocumentSource2.swift */, 88 | BE24B3EF1EFB02A3002D5D2A /* Main.storyboard */, 89 | BE24B3F41EFB02A4002D5D2A /* LaunchScreen.storyboard */, 90 | BE24B3F21EFB02A3002D5D2A /* Assets.xcassets */, 91 | BE24B3FD1EFB02BD002D5D2A /* OpenInPlace.entitlements */, 92 | BE24B3F71EFB02A4002D5D2A /* Info.plist */, 93 | ); 94 | path = OpenInPlace; 95 | sourceTree = ""; 96 | }; 97 | BEDCB31D2126FB3A000547BA /* Working Copy */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | BEDCB31A2126FB32000547BA /* WorkingCopyUrlService.h */, 101 | BEDCB31B2126FB32000547BA /* WorkingCopyUrlService.m */, 102 | BE5FCD042159570E0025C3F6 /* WorkingCopyUrlService.swift */, 103 | ); 104 | path = "Working Copy"; 105 | sourceTree = ""; 106 | }; 107 | /* End PBXGroup section */ 108 | 109 | /* Begin PBXNativeTarget section */ 110 | BE24B3E51EFB02A3002D5D2A /* OpenInPlace */ = { 111 | isa = PBXNativeTarget; 112 | buildConfigurationList = BE24B3FA1EFB02A4002D5D2A /* Build configuration list for PBXNativeTarget "OpenInPlace" */; 113 | buildPhases = ( 114 | BE24B3E21EFB02A3002D5D2A /* Sources */, 115 | BE24B3E31EFB02A3002D5D2A /* Frameworks */, 116 | BE24B3E41EFB02A3002D5D2A /* Resources */, 117 | ); 118 | buildRules = ( 119 | ); 120 | dependencies = ( 121 | ); 122 | name = OpenInPlace; 123 | productName = OpenInPlace; 124 | productReference = BE24B3E61EFB02A3002D5D2A /* OpenInPlace.app */; 125 | productType = "com.apple.product-type.application"; 126 | }; 127 | /* End PBXNativeTarget section */ 128 | 129 | /* Begin PBXProject section */ 130 | BE24B3DE1EFB02A3002D5D2A /* Project object */ = { 131 | isa = PBXProject; 132 | attributes = { 133 | LastSwiftUpdateCheck = 0900; 134 | LastUpgradeCheck = 1420; 135 | ORGANIZATIONNAME = "Applied Phasor"; 136 | TargetAttributes = { 137 | BE24B3E51EFB02A3002D5D2A = { 138 | CreatedOnToolsVersion = 9.0; 139 | LastSwiftMigration = 1150; 140 | SystemCapabilities = { 141 | com.apple.iCloud = { 142 | enabled = 1; 143 | }; 144 | }; 145 | }; 146 | }; 147 | }; 148 | buildConfigurationList = BE24B3E11EFB02A3002D5D2A /* Build configuration list for PBXProject "OpenInPlace" */; 149 | compatibilityVersion = "Xcode 8.0"; 150 | developmentRegion = en; 151 | hasScannedForEncodings = 0; 152 | knownRegions = ( 153 | en, 154 | Base, 155 | ); 156 | mainGroup = BE24B3DD1EFB02A3002D5D2A; 157 | productRefGroup = BE24B3E71EFB02A3002D5D2A /* Products */; 158 | projectDirPath = ""; 159 | projectRoot = ""; 160 | targets = ( 161 | BE24B3E51EFB02A3002D5D2A /* OpenInPlace */, 162 | ); 163 | }; 164 | /* End PBXProject section */ 165 | 166 | /* Begin PBXResourcesBuildPhase section */ 167 | BE24B3E41EFB02A3002D5D2A /* Resources */ = { 168 | isa = PBXResourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | BE24B3F61EFB02A4002D5D2A /* LaunchScreen.storyboard in Resources */, 172 | BE24B3F31EFB02A3002D5D2A /* Assets.xcassets in Resources */, 173 | BE24B3F11EFB02A3002D5D2A /* Main.storyboard in Resources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXSourcesBuildPhase section */ 180 | BE24B3E21EFB02A3002D5D2A /* Sources */ = { 181 | isa = PBXSourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | BE0263C12D7C630A004304F9 /* XDocumentNotify.swift in Sources */, 185 | BE7BBD301EFBB947003FE5D4 /* UrlCoordination.swift in Sources */, 186 | BE24B3EE1EFB02A3002D5D2A /* EditController.swift in Sources */, 187 | BE5FCD052159570E0025C3F6 /* WorkingCopyUrlService.swift in Sources */, 188 | BE24B3EC1EFB02A3002D5D2A /* ListController.swift in Sources */, 189 | BE98E568221592F400672721 /* XDocumentSource2.swift in Sources */, 190 | BEAC7B612962E1AA0022F97C /* XCallbackOpener.swift in Sources */, 191 | BE24B3EA1EFB02A3002D5D2A /* AppDelegate.swift in Sources */, 192 | BE7BBD321EFBBA26003FE5D4 /* ShowError.swift in Sources */, 193 | BEAC7B5F2962D9ED0022F97C /* SceneDelegate.swift in Sources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXSourcesBuildPhase section */ 198 | 199 | /* Begin PBXVariantGroup section */ 200 | BE24B3EF1EFB02A3002D5D2A /* Main.storyboard */ = { 201 | isa = PBXVariantGroup; 202 | children = ( 203 | BE24B3F01EFB02A3002D5D2A /* Base */, 204 | ); 205 | name = Main.storyboard; 206 | sourceTree = ""; 207 | }; 208 | BE24B3F41EFB02A4002D5D2A /* LaunchScreen.storyboard */ = { 209 | isa = PBXVariantGroup; 210 | children = ( 211 | BE24B3F51EFB02A4002D5D2A /* Base */, 212 | ); 213 | name = LaunchScreen.storyboard; 214 | sourceTree = ""; 215 | }; 216 | /* End PBXVariantGroup section */ 217 | 218 | /* Begin XCBuildConfiguration section */ 219 | BE24B3F81EFB02A4002D5D2A /* Debug */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | ALWAYS_SEARCH_USER_PATHS = NO; 223 | CLANG_ANALYZER_NONNULL = YES; 224 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 226 | CLANG_CXX_LIBRARY = "libc++"; 227 | CLANG_ENABLE_MODULES = YES; 228 | CLANG_ENABLE_OBJC_ARC = YES; 229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 230 | CLANG_WARN_BOOL_CONVERSION = YES; 231 | CLANG_WARN_COMMA = YES; 232 | CLANG_WARN_CONSTANT_CONVERSION = YES; 233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 236 | CLANG_WARN_EMPTY_BODY = YES; 237 | CLANG_WARN_ENUM_CONVERSION = YES; 238 | CLANG_WARN_INFINITE_RECURSION = YES; 239 | CLANG_WARN_INT_CONVERSION = YES; 240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 246 | CLANG_WARN_STRICT_PROTOTYPES = YES; 247 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 249 | CLANG_WARN_UNREACHABLE_CODE = YES; 250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 251 | CODE_SIGN_IDENTITY = "iPhone Developer"; 252 | COPY_PHASE_STRIP = NO; 253 | DEBUG_INFORMATION_FORMAT = dwarf; 254 | ENABLE_STRICT_OBJC_MSGSEND = YES; 255 | ENABLE_TESTABILITY = YES; 256 | GCC_C_LANGUAGE_STANDARD = gnu11; 257 | GCC_DYNAMIC_NO_PIC = NO; 258 | GCC_NO_COMMON_BLOCKS = YES; 259 | GCC_OPTIMIZATION_LEVEL = 0; 260 | GCC_PREPROCESSOR_DEFINITIONS = ( 261 | "DEBUG=1", 262 | "$(inherited)", 263 | ); 264 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 265 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 266 | GCC_WARN_UNDECLARED_SELECTOR = YES; 267 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 268 | GCC_WARN_UNUSED_FUNCTION = YES; 269 | GCC_WARN_UNUSED_VARIABLE = YES; 270 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 271 | MTL_ENABLE_DEBUG_INFO = YES; 272 | ONLY_ACTIVE_ARCH = YES; 273 | SDKROOT = iphoneos; 274 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 275 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 276 | }; 277 | name = Debug; 278 | }; 279 | BE24B3F91EFB02A4002D5D2A /* Release */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | ALWAYS_SEARCH_USER_PATHS = NO; 283 | CLANG_ANALYZER_NONNULL = YES; 284 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 285 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 286 | CLANG_CXX_LIBRARY = "libc++"; 287 | CLANG_ENABLE_MODULES = YES; 288 | CLANG_ENABLE_OBJC_ARC = YES; 289 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 290 | CLANG_WARN_BOOL_CONVERSION = YES; 291 | CLANG_WARN_COMMA = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 294 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 295 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 296 | CLANG_WARN_EMPTY_BODY = YES; 297 | CLANG_WARN_ENUM_CONVERSION = YES; 298 | CLANG_WARN_INFINITE_RECURSION = YES; 299 | CLANG_WARN_INT_CONVERSION = YES; 300 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 302 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 304 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 305 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 306 | CLANG_WARN_STRICT_PROTOTYPES = YES; 307 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 308 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 309 | CLANG_WARN_UNREACHABLE_CODE = YES; 310 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 311 | CODE_SIGN_IDENTITY = "iPhone Developer"; 312 | COPY_PHASE_STRIP = NO; 313 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 314 | ENABLE_NS_ASSERTIONS = NO; 315 | ENABLE_STRICT_OBJC_MSGSEND = YES; 316 | GCC_C_LANGUAGE_STANDARD = gnu11; 317 | GCC_NO_COMMON_BLOCKS = YES; 318 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 319 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 320 | GCC_WARN_UNDECLARED_SELECTOR = YES; 321 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 322 | GCC_WARN_UNUSED_FUNCTION = YES; 323 | GCC_WARN_UNUSED_VARIABLE = YES; 324 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 325 | MTL_ENABLE_DEBUG_INFO = NO; 326 | SDKROOT = iphoneos; 327 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 328 | VALIDATE_PRODUCT = YES; 329 | }; 330 | name = Release; 331 | }; 332 | BE24B3FB1EFB02A4002D5D2A /* Debug */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 336 | CLANG_ENABLE_MODULES = YES; 337 | CODE_SIGN_ENTITLEMENTS = OpenInPlace/OpenInPlace.entitlements; 338 | DEVELOPMENT_TEAM = 4U9NSKBV63; 339 | INFOPLIST_FILE = OpenInPlace/Info.plist; 340 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 341 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 342 | PRODUCT_BUNDLE_IDENTIFIER = com.appliedphasor.OpenInPlace; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 345 | SUPPORTS_MACCATALYST = NO; 346 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 347 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 348 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 349 | SWIFT_VERSION = 5.0; 350 | TARGETED_DEVICE_FAMILY = "1,2"; 351 | }; 352 | name = Debug; 353 | }; 354 | BE24B3FC1EFB02A4002D5D2A /* Release */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 358 | CLANG_ENABLE_MODULES = YES; 359 | CODE_SIGN_ENTITLEMENTS = OpenInPlace/OpenInPlace.entitlements; 360 | DEVELOPMENT_TEAM = 4U9NSKBV63; 361 | INFOPLIST_FILE = OpenInPlace/Info.plist; 362 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 363 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 364 | PRODUCT_BUNDLE_IDENTIFIER = com.appliedphasor.OpenInPlace; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 367 | SUPPORTS_MACCATALYST = NO; 368 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 369 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 370 | SWIFT_VERSION = 5.0; 371 | TARGETED_DEVICE_FAMILY = "1,2"; 372 | }; 373 | name = Release; 374 | }; 375 | /* End XCBuildConfiguration section */ 376 | 377 | /* Begin XCConfigurationList section */ 378 | BE24B3E11EFB02A3002D5D2A /* Build configuration list for PBXProject "OpenInPlace" */ = { 379 | isa = XCConfigurationList; 380 | buildConfigurations = ( 381 | BE24B3F81EFB02A4002D5D2A /* Debug */, 382 | BE24B3F91EFB02A4002D5D2A /* Release */, 383 | ); 384 | defaultConfigurationIsVisible = 0; 385 | defaultConfigurationName = Release; 386 | }; 387 | BE24B3FA1EFB02A4002D5D2A /* Build configuration list for PBXNativeTarget "OpenInPlace" */ = { 388 | isa = XCConfigurationList; 389 | buildConfigurations = ( 390 | BE24B3FB1EFB02A4002D5D2A /* Debug */, 391 | BE24B3FC1EFB02A4002D5D2A /* Release */, 392 | ); 393 | defaultConfigurationIsVisible = 0; 394 | defaultConfigurationName = Release; 395 | }; 396 | /* End XCConfigurationList section */ 397 | }; 398 | rootObject = BE24B3DE1EFB02A3002D5D2A /* Project object */; 399 | } 400 | -------------------------------------------------------------------------------- /OpenInPlace.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OpenInPlace.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OpenInPlace.xcodeproj/project.xcworkspace/xcuserdata/ander.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace.xcodeproj/project.xcworkspace/xcuserdata/ander.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /OpenInPlace.xcodeproj/xcshareddata/xcschemes/OpenInPlace.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /OpenInPlace.xcodeproj/xcuserdata/ander.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /OpenInPlace.xcodeproj/xcuserdata/ander.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | OpenInPlace.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | OpenInPlace.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | BE24B3E51EFB02A3002D5D2A 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /OpenInPlace/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 21/06/2017. 6 | // Copyright © 2017 Applied Phasor. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, 14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | 16 | XCallbackOpener.shared.openCallback = { url, vc in 17 | let split = vc as? UISplitViewController 18 | let nav = split?.viewControllers.first as? UINavigationController 19 | let list = nav?.viewControllers.first as? ListController 20 | list?.addURL(url) 21 | } 22 | 23 | return true 24 | } 25 | 26 | func application(_ application: UIApplication, 27 | configurationForConnecting connectingSceneSession: UISceneSession, 28 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 29 | return UISceneConfiguration(name: "Default", sessionRole: connectingSceneSession.role) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "notification40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "notification60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "settings58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "settings87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "spotlight80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "spotlight120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iphone120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iphone180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "ipadNotification20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "ipadNotification40.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "ipadSettings29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "ipadSettings58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "ipadSpotlight40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "ipadSpotlight80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "ipad76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "ipad152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "ipadPro167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "appstore1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "mac16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "mac32.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "mac32.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "mac64.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "mac128.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "mac256.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "mac256.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "mac512.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "mac512.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "mac1024.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/appstore1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/appstore1024.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipad152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipad152.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipad76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipad76.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadPro167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadPro167.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/iphone120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/iphone120.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/iphone180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/iphone180.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac1024.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac128.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac16.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac256.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac32.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac512.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/mac64.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/notification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/notification40.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/notification60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/notification60.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/settings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/settings58.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/settings87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/settings87.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/spotlight120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/spotlight120.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/AppIcon.appiconset/spotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/AppIcon.appiconset/spotlight80.png -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/empty.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "empty.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /OpenInPlace/Assets.xcassets/empty.imageset/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palmin/open-in-place/9db5fff42e26c2c197d471611d1893756269eb9e/OpenInPlace/Assets.xcassets/empty.imageset/empty.png -------------------------------------------------------------------------------- /OpenInPlace/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 | -------------------------------------------------------------------------------- /OpenInPlace/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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 114 | 115 | 116 | 117 | 118 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 146 | 147 | 148 | 149 | 150 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /OpenInPlace/EditController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditController.swift 3 | // OpenInPlace 4 | // 5 | // This view controller shows how to 6 | // 1) read contents in coordinated manner from a remote file from iCloud Drive 7 | // or another document providers: 8 | // loadContent() 9 | // 10 | // 2) write back content in coordinated manner: 11 | // writeContentIfNeeded() and writeContentUpdatingUI() 12 | // 13 | // 3) observe changes and coordinate with other processes accessing file: 14 | // appMovedToBackground(), appMovedToForeground() and NSFilePresenter delegate methods 15 | // 16 | // 4) auto-save changes: 17 | // textViewDidChange() and appMovedToBackground() 18 | // 19 | // 5) use WorkingCopyUrlService file-provider SDK to get file status and compose 20 | // x-callback-url for initiating commit: 21 | // loadStatusWithService() and statusTapped() 22 | // 23 | // If you are using UIDocument you mostly get all this for free. 24 | // 25 | // 26 | // Created by Anders Borum on 21/06/2017. 27 | // Copyright © 2017 Applied Phasor. All rights reserved. 28 | // 29 | 30 | import UIKit 31 | 32 | class EditController: UIViewController, UITextViewDelegate, NSFilePresenter { 33 | 34 | @IBOutlet var textView: UITextView! 35 | @IBOutlet var statusButton: UIBarButtonItem! 36 | @IBOutlet var detailsItem: UIBarButtonItem! 37 | 38 | private func configureDetailMenu() { 39 | let notify = UIAction(title: NSLocalizedString("Send didAccess notification", comment: "")) { _ in 40 | self.url?.xDocumentNotifyDidAccess { error in 41 | if let error = error { 42 | self.showError(error) 43 | } 44 | } 45 | } 46 | if #available(iOS 15.0, *) { 47 | notify.subtitle = "dk.andersborum.document-notify" 48 | } 49 | 50 | detailsItem.menu = UIMenu(children: [notify]) 51 | } 52 | 53 | private func loadContent() { 54 | // do not load unless we have both url and view loaded 55 | guard isViewLoaded else { return } 56 | guard url != nil else { 57 | self.navigationItem.title = "" 58 | self.textView.text = "" 59 | return 60 | } 61 | 62 | navigationItem.title = _url?.lastPathComponent 63 | 64 | let coordinator = NSFileCoordinator(filePresenter: self) 65 | url!.coordinatedRead(coordinator, callback: { (text, error) in 66 | 67 | DispatchQueue.main.async { 68 | if(error != nil) { 69 | self.showError(error!) 70 | } else { 71 | self.textView.text = text 72 | } 73 | } 74 | }) 75 | } 76 | 77 | private var urlService: WorkingCopyUrlService? 78 | 79 | private func loadStatusWithService(_ service: WorkingCopyUrlService) { 80 | service.fetchStatus(completionHandler: { 81 | (linesAdded, linesDeleted, error) in 82 | 83 | self.statusButton.isEnabled = true 84 | 85 | switch (linesAdded, linesDeleted) { 86 | 87 | case (UInt(NSNotFound), _): 88 | // modified binary file 89 | self.statusButton.title = "binary" 90 | 91 | case (0,0): 92 | // file is current 93 | self.statusButton.title = "" 94 | self.statusButton.isEnabled = false 95 | 96 | case (0, _): 97 | // modified text file 98 | self.statusButton.title = "-\(linesDeleted)" 99 | 100 | case (_, 0): 101 | // modified text file 102 | self.statusButton.title = "+\(linesAdded)" 103 | 104 | default: 105 | // modified text file 106 | self.statusButton.title = "-\(linesDeleted)+\(linesAdded)" 107 | } 108 | }) 109 | } 110 | 111 | @IBAction func statusTapped(_ sender: Any) { 112 | guard let service = urlService else { return } 113 | 114 | // request deep link 115 | service.determineDeepLink(completionHandler: { (url, error) in 116 | if let error = error { 117 | self.showError(error) 118 | } 119 | 120 | guard let url = url else { return } 121 | 122 | // we escape everything outside urlQueryAllowed but also & that starts next url parameter 123 | let allowChars = CharacterSet.urlQueryAllowed.intersection(CharacterSet(charactersIn: "&").inverted) 124 | guard let escaped = url.absoluteString.addingPercentEncoding(withAllowedCharacters: allowChars) else { return } 125 | guard let callbackUrl = URL(string: "working-copy://x-callback-url/commit?url=\(escaped)&x-cancel=open-in-place://&x-success=open-in-place://") else { return } 126 | 127 | UIApplication.shared.open(callbackUrl) 128 | }) 129 | } 130 | 131 | private func loadStatus() { 132 | guard isViewLoaded else { return } 133 | guard let url = url else { return } 134 | 135 | if #available(iOS 11.0, *) { 136 | 137 | // try to use existing service instance 138 | if let service = urlService { 139 | loadStatusWithService(service) 140 | return 141 | } 142 | 143 | // Try to get file provider icon from Working Copy service. 144 | WorkingCopyUrlService.getFor(url, completionHandler: { (service, error) in 145 | // the service might very well be missing if you are picking from some other 146 | // Location than Working Copy or the version of Working Copy isn't new enough 147 | guard let service = service else { return } 148 | self.urlService = service 149 | 150 | self.loadStatusWithService(service) 151 | }) 152 | } 153 | } 154 | 155 | private var unwrittenChanges = false 156 | private func writeContentIfNeeded(callback: @escaping ((Error?) -> ())) { 157 | 158 | guard unwrittenChanges else { 159 | callback(nil) 160 | return 161 | 162 | } 163 | unwrittenChanges = false 164 | 165 | guard url != nil else { 166 | callback(nil) 167 | return 168 | } 169 | 170 | let coordinator = NSFileCoordinator(filePresenter: self) 171 | url!.coordinatedWrite(textView.text, coordinator, callback: callback) 172 | } 173 | 174 | // calls writeContentIfNeeded(callback:) showing errors 175 | @objc private func writeContentUpdatingUI() { 176 | 177 | writeContentIfNeeded(callback: { error in 178 | DispatchQueue.main.async { 179 | if let error = error { 180 | self.showError(error) 181 | } else { 182 | self.loadStatus() 183 | } 184 | } 185 | }) 186 | } 187 | 188 | override func viewDidLoad() { 189 | super.viewDidLoad() 190 | loadContent() 191 | loadStatus() 192 | configureDetailMenu() 193 | 194 | let notifications = NotificationCenter.default 195 | notifications.addObserver(self, selector: #selector(appMovedToBackground), 196 | name: UIApplication.willResignActiveNotification, object: nil) 197 | notifications.addObserver(self, selector: #selector(appMovedToForeground), 198 | name: UIApplication.didBecomeActiveNotification, object: nil) 199 | } 200 | 201 | override func viewWillDisappear(_ animated: Bool) { 202 | super.viewWillDisappear(animated) 203 | 204 | // clean up when removed 205 | if url != nil && self.isMovingFromParent { 206 | url = nil 207 | } 208 | } 209 | 210 | private var securityScoped = false 211 | private var _url: URL? 212 | private var isFilePresenting = false 213 | 214 | public var url: URL? { 215 | set { 216 | if(_url != nil) { 217 | if securityScoped { 218 | _url!.stopAccessingSecurityScopedResource() 219 | securityScoped = false 220 | } 221 | if(isFilePresenting) { 222 | NSFileCoordinator.removeFilePresenter(self) 223 | isFilePresenting = false 224 | } 225 | } 226 | 227 | _url = newValue 228 | urlService = nil 229 | 230 | if(_url != nil) { 231 | securityScoped = _url!.startAccessingSecurityScopedResource() 232 | NSFileCoordinator.addFilePresenter(self) 233 | isFilePresenting = true 234 | } 235 | loadContent() 236 | loadStatus() 237 | } 238 | get { 239 | return _url 240 | } 241 | } 242 | 243 | //MARK: - NSFilePresenter 244 | var presentedItemURL: URL? { 245 | return url 246 | } 247 | 248 | private var presenterQueue : OperationQueue? 249 | var presentedItemOperationQueue: OperationQueue { 250 | if(presenterQueue == nil) { 251 | presenterQueue = OperationQueue() 252 | } 253 | return presenterQueue! 254 | } 255 | 256 | // file was changed by someone else and we want to reload 257 | func presentedItemDidChange() { 258 | DispatchQueue.main.async { 259 | self.loadContent() 260 | } 261 | } 262 | 263 | // someone wants to read the file and we make sure pending changes are written 264 | func relinquishPresentedItem(toReader reader: @escaping ((() -> Void)?) -> Void) { 265 | writeContentIfNeeded(callback: { error in 266 | reader(nil) 267 | }) 268 | } 269 | 270 | // someone wants to write the file and we make sure pending changes are written 271 | func relinquishPresentedItem(toWriter writer: @escaping ((() -> Void)?) -> Void) { 272 | writeContentIfNeeded(callback: { error in 273 | writer(nil) 274 | }) 275 | } 276 | 277 | // file is being renamed by someone else and we keep work in new file location 278 | func presentedItemDidMove(to newURL: URL) { 279 | url = newURL 280 | } 281 | 282 | // file is being deleted and we stop tracking it 283 | func accommodatePresentedItemDeletion(completionHandler: @escaping (Error?) -> Void) { 284 | url = nil 285 | completionHandler(nil) 286 | } 287 | 288 | @objc func appMovedToBackground() { 289 | // it can lead to deadlocks to present files in the background and we back off 290 | if(isFilePresenting) { 291 | NSFileCoordinator.removeFilePresenter(self) 292 | isFilePresenting = false 293 | } 294 | 295 | // write if anything is pending 296 | writeContentUpdatingUI() 297 | 298 | } 299 | 300 | @objc func appMovedToForeground() { 301 | // we are back after being in the background and listen again and refresh from file 302 | if(!isFilePresenting && url != nil) { 303 | NSFileCoordinator.addFilePresenter(self) 304 | isFilePresenting = true 305 | } 306 | 307 | loadContent() 308 | loadStatus() 309 | } 310 | 311 | //MARK: - UITextViewDelegate 312 | 313 | func textViewDidChange(_ textView: UITextView) { 314 | // we want to write changes, but not after every keystroke and wait for a 315 | // whole second without changes 316 | unwrittenChanges = true 317 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(writeContentUpdatingUI), object: nil) 318 | perform(#selector(writeContentUpdatingUI), with: nil, afterDelay: 1.0) 319 | } 320 | 321 | //MARK: - 322 | } 323 | 324 | -------------------------------------------------------------------------------- /OpenInPlace/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | UIApplicationSceneManifest 8 | 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | UIWindowSceneSessionRoleApplication 14 | 15 | 16 | UISceneConfigurationName 17 | Default 18 | UISceneDelegateClassName 19 | $(PRODUCT_MODULE_NAME).SceneDelegate 20 | UISceneStoryboardFile 21 | Main 22 | 23 | 24 | 25 | 26 | CFBundleDocumentTypes 27 | 28 | 29 | CFBundleTypeIconFiles 30 | 31 | CFBundleTypeName 32 | Text 33 | CFBundleTypeRole 34 | Editor 35 | LSHandlerRank 36 | Owner 37 | LSItemContentTypes 38 | 39 | public.plain-text 40 | 41 | 42 | 43 | CFBundleExecutable 44 | $(EXECUTABLE_NAME) 45 | CFBundleIdentifier 46 | $(PRODUCT_BUNDLE_IDENTIFIER) 47 | CFBundleInfoDictionaryVersion 48 | 6.0 49 | CFBundleName 50 | $(PRODUCT_NAME) 51 | CFBundlePackageType 52 | APPL 53 | CFBundleShortVersionString 54 | 1.0 55 | CFBundleURLTypes 56 | 57 | 58 | CFBundleTypeRole 59 | Editor 60 | CFBundleURLSchemes 61 | 62 | open-in-place 63 | 64 | 65 | 66 | CFBundleVersion 67 | 1 68 | LSRequiresIPhoneOS 69 | 70 | LSSupportsOpeningDocumentsInPlace 71 | 72 | UILaunchStoryboardName 73 | LaunchScreen 74 | UIMainStoryboardFile 75 | Main 76 | UIRequiredDeviceCapabilities 77 | 78 | armv7 79 | 80 | UIStatusBarTintParameters 81 | 82 | UINavigationBar 83 | 84 | Style 85 | UIBarStyleDefault 86 | Translucent 87 | 88 | 89 | 90 | UISupportedInterfaceOrientations 91 | 92 | UIInterfaceOrientationPortrait 93 | UIInterfaceOrientationLandscapeLeft 94 | UIInterfaceOrientationLandscapeRight 95 | 96 | UISupportedInterfaceOrientations~ipad 97 | 98 | UIInterfaceOrientationPortrait 99 | UIInterfaceOrientationPortraitUpsideDown 100 | UIInterfaceOrientationLandscapeLeft 101 | UIInterfaceOrientationLandscapeRight 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /OpenInPlace/ListController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListController.swift 3 | // OpenInPlace 4 | // 5 | // This view controller shows how to 6 | // 1) open directories from iCloud Drive and other document providers as security scoped URLs: 7 | // pickURLs() and the UIDocumentPickerDelegate delegate methods. 8 | // 9 | // 2) drag in-place references to files and directories from other applications as security scoped URLs: 10 | // UITableViewDropDelegate methods 11 | // 12 | // 3) persist security scoped URLs: 13 | // saveUrlBookmarks() and restoreUrlBookmarks() converts arrays of security scoped URL objects into 14 | // arrays of bookmark Data. 15 | // 16 | // 4) list contents of directories accessed through document providers in file coordinated manner: 17 | // reloadContent() does this but not for the root list. 18 | // 19 | // 5) delete files in other document providers with file coordination: 20 | // happens in tableView(tableView,editingStyle,forRowAt) but not for the root list where a bookmark 21 | // is deleted. 22 | // 23 | // 6) how to watch a directory for changes: 24 | // appMovedToBackground(), appMovedToForeground() and NSFilePresenter delegate methods 25 | // 26 | // 7) use WorkingCopyUrlService file-provider SDK to get app icon and file information for 27 | // entries inside the Working Copy Location: 28 | // tableView(_ tableView, cellForRowAt) 29 | // 30 | // Created by Anders Borum on 21/06/2017. 31 | // Copyright © 2017 Applied Phasor. All rights reserved. 32 | // 33 | 34 | import UIKit 35 | import MobileCoreServices 36 | 37 | class ListController: UITableViewController, UIDocumentPickerDelegate, NSFilePresenter, UITableViewDropDelegate { 38 | var detailViewController: EditController? = nil 39 | 40 | public var baseURL: URL? = nil 41 | 42 | // security scoped URL's are shown when baseURL is nil, otherwise the contents of directory 43 | var urls = [URL]() 44 | 45 | private func reloadContent() { 46 | guard baseURL != nil else { return } 47 | 48 | let coordinator = NSFileCoordinator(filePresenter: self) 49 | baseURL!.coordinatedList(coordinator, callback: { (newUrls, error) in 50 | 51 | DispatchQueue.main.async { 52 | if(error != nil) { 53 | self.showError(error!) 54 | } 55 | if(newUrls != nil) { 56 | self.urls = newUrls! 57 | self.tableView.reloadData() 58 | } 59 | } 60 | }) 61 | } 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | 66 | let notifications = NotificationCenter.default 67 | notifications.addObserver(self, selector: #selector(appMovedToBackground), 68 | name: UIApplication.willResignActiveNotification, object: nil) 69 | notifications.addObserver(self, selector: #selector(appMovedToForeground), 70 | name: UIApplication.didBecomeActiveNotification, object: nil) 71 | 72 | restoreUrlBookmarks() 73 | 74 | if #available(iOS 11.0, *) { 75 | // enable items dropped on tableView 76 | tableView.dropDelegate = self 77 | } 78 | 79 | // we only have edit button for root list, as we want to be able to go back 80 | let isRoot = baseURL == nil 81 | if(isRoot) { 82 | navigationItem.leftBarButtonItem = editButtonItem 83 | 84 | let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTapped)) 85 | navigationItem.rightBarButtonItems = [addButton] 86 | 87 | } else { 88 | // read contents of directory 89 | let _ = baseURL!.startAccessingSecurityScopedResource() 90 | self.navigationItem.title = baseURL?.lastPathComponent 91 | 92 | reloadContent() 93 | 94 | NSFileCoordinator.addFilePresenter(self) 95 | isFilePresenting = true 96 | } 97 | 98 | 99 | if let split = splitViewController { 100 | let controllers = split.viewControllers 101 | detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? EditController 102 | } 103 | } 104 | 105 | @objc func addTapped(_ sender: UIBarButtonItem) { 106 | if #available(iOS 13.0, *) { 107 | // on iOS 13 we cannot ask for both files and directories when showing the document picker 108 | // and we need to ask the user what they want to pick first 109 | let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 110 | 111 | sheet.addAction(UIAlertAction(title: NSLocalizedString("Pick Directory", comment: ""), 112 | style: .default, handler: { _ in 113 | self.pickURLs(types: [kUTTypeFolder as String], sender) 114 | })) 115 | 116 | sheet.addAction(UIAlertAction(title: NSLocalizedString("Pick File", comment: ""), 117 | style: .default, handler: { _ in 118 | self.pickURLs(types: [kUTTypeText as String], sender) 119 | })) 120 | 121 | sheet.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), 122 | style: .cancel, handler: nil)) 123 | 124 | sheet.popoverPresentationController?.barButtonItem = sender 125 | self.present(sheet, animated: true) 126 | 127 | } else { 128 | 129 | // we can ask for either files or directories on iOS 12 and before 130 | pickURLs(types: [kUTTypeText as String, kUTTypeDirectory as String], sender) 131 | } 132 | 133 | } 134 | 135 | override func viewWillDisappear(_ animated: Bool) { 136 | super.viewWillDisappear(animated) 137 | 138 | // make sure we only remove file presenter and stop security scope once 139 | // and only when list is being removed fully from view hierarchy 140 | if self.isMovingFromParent { 141 | if(isFilePresenting) { 142 | NSFileCoordinator.removeFilePresenter(self) 143 | isFilePresenting = false 144 | } 145 | 146 | if baseURL != nil { 147 | baseURL!.stopAccessingSecurityScopedResource() 148 | baseURL = nil 149 | } 150 | } 151 | } 152 | 153 | // MARK: - Segues 154 | 155 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 156 | if segue.identifier == "subdir" { 157 | if let indexPath = tableView.indexPathForSelectedRow { 158 | 159 | let controller = segue.destination as! ListController 160 | controller.baseURL = urls[indexPath.row] 161 | } 162 | } 163 | 164 | if segue.identifier == "edit" { 165 | if let indexPath = tableView.indexPathForSelectedRow, 166 | let nav = segue.destination as? UINavigationController, 167 | let controller = nav.topViewController as? EditController { 168 | 169 | controller.url = urls[indexPath.row] 170 | controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem 171 | controller.navigationItem.leftItemsSupplementBackButton = true 172 | } 173 | } 174 | } 175 | 176 | // MARK: - UITableViewDropDelegate 177 | 178 | @available(iOS 11.0, *) 179 | func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { 180 | let canOpenInPlace = baseURL == nil 181 | return canOpenInPlace 182 | } 183 | 184 | @available(iOS 11.0, *) 185 | func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { 186 | // root list can open in-place and the other lists import copies, but we do not yet support this 187 | let canOpenInPlace = baseURL == nil 188 | guard canOpenInPlace else { return } 189 | 190 | for dropItem in coordinator.items { 191 | let itemProvider = dropItem.dragItem.itemProvider 192 | let uti = itemProvider.registeredTypeIdentifiers.first ?? "public.data" 193 | itemProvider.loadInPlaceFileRepresentation(forTypeIdentifier: uti, 194 | completionHandler: {(url, inPlace, error) in 195 | if error != nil { self.showError(error!) } 196 | guard inPlace else { return } 197 | 198 | guard let url = url else { return } 199 | 200 | // remember this url 201 | self.urls.append(url) 202 | self.saveUrlBookmarks() 203 | 204 | DispatchQueue.main.async { 205 | self.tableView.reloadData() 206 | } 207 | }) 208 | } 209 | } 210 | 211 | // MARK: - Table View 212 | 213 | override func numberOfSections(in tableView: UITableView) -> Int { 214 | return 1 215 | } 216 | 217 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 218 | return urls.count 219 | } 220 | 221 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 222 | 223 | let url = urls[indexPath.row] 224 | let _ = url.startAccessingSecurityScopedResource() 225 | 226 | let identifier = url.isDirectory ? "dir" : "file" 227 | let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) 228 | cell.textLabel!.text = url.lastPathComponent 229 | 230 | if #available(iOS 11.0, *) { 231 | // Try to get file provider icon from x-document-source-2 or Working Copy service. 232 | // 233 | // A real app wouldn't do this every time the cell was loaded, as there is 234 | // some communication overhead betweenn file-provider and app. 235 | 236 | url.fetchDocumentInfo(pixelSize: 120, completionHandler: { (path, appName, appVersion, icon) in 237 | if path != nil { 238 | cell.detailTextLabel?.text = path 239 | cell.imageView?.image = icon 240 | cell.setNeedsLayout() 241 | return 242 | } 243 | }) 244 | } 245 | 246 | url.stopAccessingSecurityScopedResource() 247 | 248 | return cell 249 | } 250 | 251 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 252 | // Return false if you do not want the specified item to be editable. 253 | return true 254 | } 255 | 256 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 257 | if editingStyle == .delete { 258 | 259 | if(baseURL != nil) { 260 | 261 | let url = urls[indexPath.row] 262 | let coordinator = NSFileCoordinator(filePresenter: self) 263 | 264 | url.coordinatedDelete(coordinator, callback: { error in 265 | if error != nil { self.showError(error!) } 266 | }) 267 | } 268 | 269 | urls.remove(at: indexPath.row) 270 | saveUrlBookmarks() 271 | 272 | tableView.deleteRows(at: [indexPath], with: .fade) 273 | } 274 | } 275 | 276 | // MARK: - 277 | 278 | private let bookmarksDefaultsKey = "bookmarks" 279 | 280 | // to be able to save and restore security scoped URL's these must be stored as bookmarks 281 | func saveUrlBookmarks() { 282 | guard baseURL == nil else { return } 283 | 284 | var bookmarks = [Data]() 285 | 286 | for url in urls { 287 | let _ = url.startAccessingSecurityScopedResource() 288 | do { 289 | let bookmark = try url.bookmarkData(options: [], 290 | includingResourceValuesForKeys: nil, 291 | relativeTo: nil) 292 | bookmarks.append(bookmark) 293 | 294 | } catch { 295 | self.showError(error, title: "saveUrlBookmarks") 296 | } 297 | url.stopAccessingSecurityScopedResource() 298 | } 299 | 300 | // store in user defaults, since this is just a demo 301 | UserDefaults.standard.set(bookmarks, forKey: bookmarksDefaultsKey) 302 | } 303 | 304 | func restoreUrlBookmarks() { 305 | guard baseURL == nil else { return } 306 | 307 | var newUrls = [URL]() 308 | var anyStale = false 309 | 310 | let bookmarks = UserDefaults.standard.object(forKey: bookmarksDefaultsKey) as? [Data] 311 | if(bookmarks != nil) { 312 | for bookmark in bookmarks! { 313 | 314 | do { 315 | var stale = false 316 | let url = try URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &stale) 317 | 318 | anyStale = anyStale || stale 319 | newUrls.append(url) 320 | 321 | } catch { 322 | print("\(error)") 323 | } 324 | } 325 | 326 | } 327 | urls = newUrls 328 | 329 | // stale bookmarks need to be recreated and we just recreate all of them where 330 | // a proper application would want to be smarter about this 331 | if anyStale { saveUrlBookmarks() } 332 | } 333 | 334 | func pickURLs(types: [String], _ sender: Any) { 335 | let picker = UIDocumentPickerViewController(documentTypes: types, in: .open) 336 | if #available(iOS 11.0, *) { 337 | let _ = picker.view // force loading of view, since allowsMultipleSelection does not stick otherwise 338 | picker.allowsMultipleSelection = true 339 | } 340 | picker.delegate = self 341 | present(picker, animated: true, completion: nil) 342 | } 343 | 344 | public func addURL(_ url: URL) { 345 | urls.append(url) 346 | saveUrlBookmarks() 347 | 348 | tableView.reloadData() 349 | } 350 | 351 | //MARK: - UIDocumentPickerDelegate 352 | 353 | func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt newUrls: [URL]) { 354 | urls.append(contentsOf: newUrls) 355 | saveUrlBookmarks() 356 | 357 | tableView.reloadData() 358 | } 359 | 360 | // this is called on iOS versions before 11 and we just pass URL along in array 361 | func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { 362 | documentPicker(controller, didPickDocumentsAt: [url]) 363 | } 364 | 365 | //MARK: - NSFilePresenter 366 | var presentedItemURL: URL? { 367 | return baseURL 368 | } 369 | 370 | private var presenterQueue : OperationQueue? 371 | var presentedItemOperationQueue: OperationQueue { 372 | if(presenterQueue == nil) { 373 | presenterQueue = OperationQueue() 374 | } 375 | return presenterQueue! 376 | } 377 | 378 | func presentedItemDidChange() { 379 | reloadContent() 380 | } 381 | 382 | func presentedSubitemDidAppear(at url: URL) { 383 | reloadContent() 384 | } 385 | 386 | private var isFilePresenting = false 387 | 388 | @objc func appMovedToBackground() { 389 | // it can lead to deadlocks to present files in the background and we back off 390 | if(isFilePresenting) { 391 | NSFileCoordinator.removeFilePresenter(self) 392 | isFilePresenting = false 393 | } 394 | } 395 | 396 | @objc func appMovedToForeground() { 397 | // we are back after being in the background and listen again and refresh from file 398 | if(!isFilePresenting && baseURL != nil) { 399 | NSFileCoordinator.addFilePresenter(self) 400 | isFilePresenting = true 401 | } 402 | 403 | reloadContent() 404 | } 405 | 406 | //MARK: - 407 | } 408 | 409 | 410 | -------------------------------------------------------------------------------- /OpenInPlace/OpenInPlace.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.icloud-container-identifiers 6 | 7 | iCloud.com.appliedphasor.OpenInPlace 8 | 9 | com.apple.developer.icloud-services 10 | 11 | CloudDocuments 12 | 13 | com.apple.developer.ubiquity-container-identifiers 14 | 15 | iCloud.com.appliedphasor.OpenInPlace 16 | 17 | com.apple.security.app-sandbox 18 | 19 | com.apple.security.files.user-selected.read-write 20 | 21 | com.apple.security.network.client 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /OpenInPlace/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 02/01/2023. 6 | // Copyright © 2023 Applied Phasor. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: NSObject, UIWindowSceneDelegate { 12 | var window: UIWindow? 13 | 14 | var split: UISplitViewController? { 15 | return window?.rootViewController as? UISplitViewController 16 | } 17 | 18 | var list : ListController? { 19 | guard let nav = split?.viewControllers.first as? UINavigationController, 20 | let list = nav.viewControllers.first as? ListController else { 21 | return nil 22 | } 23 | 24 | return list 25 | } 26 | 27 | func sceneDidBecomeActive(_ scene: UIScene) { 28 | split?.delegate = self 29 | 30 | let nav = split?.viewControllers.last as? UINavigationController 31 | nav?.topViewController?.navigationItem.leftBarButtonItem = split?.displayModeButtonItem 32 | } 33 | 34 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, 35 | options connectionOptions: UIScene.ConnectionOptions) { 36 | for context in connectionOptions.urlContexts { 37 | handleUrl(context.url) 38 | } 39 | } 40 | 41 | // MARK: - Open in support 42 | 43 | func handleUrl(_ url: URL) { 44 | if let vc = split, XCallbackOpener.shared.couldHandleUrl(url) { 45 | do { 46 | let _ = try XCallbackOpener.shared.didHandleUrl(url, vc) 47 | } catch { 48 | vc.showError(error) 49 | } 50 | return 51 | } 52 | 53 | list?.addURL(url) 54 | } 55 | 56 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 57 | for context in URLContexts { 58 | handleUrl(context.url) 59 | } 60 | } 61 | 62 | } 63 | 64 | extension SceneDelegate : UISplitViewControllerDelegate { 65 | func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { 66 | guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false } 67 | guard let topAsDetailController = secondaryAsNavController.topViewController as? EditController else { return false } 68 | if topAsDetailController.url == nil { 69 | // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. 70 | return true 71 | } 72 | return false 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /OpenInPlace/ShowError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowError.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 22/06/2017. 6 | // Copyright © 2017 Applied Phasor. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | func showError(_ error : Error, title: String = "Error") { 13 | let alert = UIAlertController.init(title: title, 14 | message: error.localizedDescription, 15 | preferredStyle: .alert) 16 | alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) 17 | present(alert, animated: true, completion: nil) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /OpenInPlace/UrlCoordination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlCoordination.swift 3 | // OpenInPlace 4 | // 5 | // These helper methods do file coordinated operations on URL objects. 6 | // The callbacks are not guaranteed to happen on any particular thread. 7 | // 8 | // Created by Anders Borum on 22/06/2017. 9 | // Copyright © 2017 Applied Phasor. All rights reserved. 10 | // 11 | 12 | import Foundation 13 | 14 | extension URL { 15 | 16 | public func coordinatedDelete(_ coordinator : NSFileCoordinator, 17 | callback: @escaping ((Error?) -> ())) { 18 | 19 | let error: NSErrorPointer = nil 20 | coordinator.coordinate(writingItemAt: self, 21 | options: NSFileCoordinator.WritingOptions.forDeleting, 22 | error: error, byAccessor: { url in 23 | do { 24 | try FileManager.default.removeItem(at: url) 25 | callback(nil) 26 | 27 | } catch { 28 | callback(error) 29 | } 30 | }) 31 | 32 | // only do callback if there is error, as it will be made during coordination 33 | if error != nil { callback(error!.pointee! as NSError) } 34 | } 35 | 36 | public func coordinatedList(_ coordinator : NSFileCoordinator, 37 | callback: @escaping (([URL]?, Error?) -> ())) { 38 | 39 | let error: NSErrorPointer = nil 40 | coordinator.coordinate(readingItemAt: self, options: [], 41 | error: error, byAccessor: { url in 42 | do { 43 | let urls = try FileManager.default.contentsOfDirectory(at: url, 44 | includingPropertiesForKeys: nil, 45 | options: []) 46 | 47 | // sometimes placeholder files are not resolved and we do this manually as a work-around 48 | let resolved = urls.map({ url -> URL in 49 | let filename = url.lastPathComponent 50 | if filename.hasPrefix(".") && filename.hasSuffix(".icloud") { 51 | let fixed = String(filename.dropFirst().dropLast(7)) // drop leading . and trailing .icloud 52 | let directory = url.deletingLastPathComponent() 53 | return directory.appendingPathComponent(fixed, isDirectory: url.isDirectory) 54 | } 55 | 56 | return url 57 | }) 58 | 59 | callback(resolved, nil) 60 | 61 | } catch { 62 | callback(nil, error) 63 | } 64 | }) 65 | 66 | // only do callback if there is error, as it will be made during coordination 67 | if error != nil { callback(nil, error!.pointee! as NSError) } 68 | } 69 | 70 | public func coordinatedRead(_ coordinator : NSFileCoordinator, 71 | callback: @escaping ((String?, Error?) -> ())) { 72 | 73 | let error: NSErrorPointer = nil 74 | coordinator.coordinate(readingItemAt: self, options: [], 75 | error: error, byAccessor: { url in 76 | do { 77 | let text = try String.init(contentsOf: url) 78 | callback(text, nil) 79 | 80 | } catch { 81 | callback(nil, error) 82 | } 83 | }) 84 | 85 | // only do callback if there is error, as it will be made during coordination 86 | if error != nil { callback(nil, error!.pointee! as NSError) } 87 | } 88 | 89 | public func coordinatedWrite(_ text : String, _ coordinator : NSFileCoordinator, 90 | callback: @escaping ((Error?) -> ())) { 91 | 92 | let error: NSErrorPointer = nil 93 | coordinator.coordinate(writingItemAt: self, options: [], 94 | error: error, byAccessor: { url in 95 | do { 96 | try text.write(to: url, atomically: false, encoding: .utf8) 97 | callback(nil) 98 | 99 | } catch { 100 | callback(error) 101 | } 102 | }) 103 | 104 | // only do callback if there is error, as it will be made during coordination 105 | if error != nil { callback(error!.pointee! as NSError) } 106 | } 107 | 108 | // this will trigger a call to file provider changing lastUseDate to the value it had previously, 109 | // as a sort of no-operation giving the file provider a chance to do work 110 | func coordinatedUpdateLastUseDate(_ coordinator : NSFileCoordinator, 111 | callback: @escaping ((Error?) -> ())) { 112 | let error: NSErrorPointer = nil 113 | coordinator.coordinate(writingItemAt: self, 114 | options: [.contentIndependentMetadataOnly], 115 | error: error, byAccessor: { url in 116 | callback(nil) 117 | }) 118 | 119 | // only do callback if there is error, as it will be made during coordination 120 | if error != nil { callback(error!.pointee! as NSError) } 121 | } 122 | 123 | // shorthand to check if URL is directory 124 | public var isDirectory: Bool { 125 | let keys = Set([URLResourceKey.isDirectoryKey]) 126 | let value = try? self.resourceValues(forKeys: keys) 127 | switch value?.isDirectory { 128 | case .some(true): 129 | return true 130 | 131 | default: 132 | return false 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /OpenInPlace/Working Copy/WorkingCopyUrlService.h: -------------------------------------------------------------------------------- 1 | // 2 | // WorkingCopyGitService.h 3 | // WorkingCopy 4 | // 5 | // Created by Anders Borum on 13/08/2018. 6 | // Copyright © 2018 Applied Phasor. All rights reserved. 7 | // 8 | // There is a Swift version of this file with the same behaviour. 9 | 10 | #import 11 | 12 | @interface WorkingCopyUrlService : NSObject 13 | 14 | // Try to inquire and connect to WorkingCopyUrlService on the given URL. 15 | // Note that you can get a nil-service even without a error when url is outside 16 | // a Working Copy file provider. 17 | // Completion block is called on main thread. 18 | +(void)getServiceForUrl:(nonnull NSURL*)url 19 | completionHandler:(void (^_Nonnull)(WorkingCopyUrlService* _Nullable service, 20 | NSError* _Nullable error))completionHandler 21 | API_AVAILABLE(ios(11.0)); 22 | 23 | // Determine deep-link for opening a the given URL inside Working Copy, 24 | // which is something on the form: 25 | // working-copy://open?repo=welcome%20to%20working%20copy&path=README.md 26 | // Completion block is called on main thread. 27 | -(void)determineDeepLinkWithCompletionHandler:(void (^_Nonnull)(NSURL* _Nullable url, 28 | NSError* _Nullable error))completionHandler; 29 | 30 | // Determine path relative to Working Copy storage and app information 31 | // that is shared by all Working Copy URLs. Completion block is called on main thread. 32 | -(void)fetchDocumentSourceInfoWithCompletionHandler:(void (^_Nonnull)(NSString* _Nullable path, 33 | NSString* _Nullable appName, 34 | NSString* _Nullable appVersion, 35 | UIImage* _Nullable appIcon, 36 | NSError* _Nullable error))completionHandler; 37 | 38 | // Determine the lines added or deleted for the file at the given URL compared to last commit. 39 | // If the file is current both lines added and deleted are zero, while NSNotFound indicates 40 | // a modified binary file. 41 | -(void)fetchStatusWithCompletionHandler:(void (^_Nonnull)(NSUInteger linesAdded, 42 | NSUInteger linesDeleted, 43 | NSError* _Nullable error))completionHandler; 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /OpenInPlace/Working Copy/WorkingCopyUrlService.m: -------------------------------------------------------------------------------- 1 | // 2 | // WorkingCopyGitService.m 3 | // WorkingCopy 4 | // 5 | // Created by Anders Borum on 13/08/2018. 6 | // Copyright © 2018 Applied Phasor. All rights reserved. 7 | // 8 | 9 | #import "WorkingCopyUrlService.h" 10 | 11 | #define ServiceNameVer1 @"working-copy-v1" 12 | #define ServiceNameVer352 @"working-copy-v3.5.2" 13 | 14 | @protocol WorkingCopyProtocolVer1 15 | 16 | -(void)determineDeepLinkWithCompletionHandler:(void (^)(NSURL* url))completionHandler; 17 | 18 | -(void)fetchDocumentSourceInfoWithCompletionHandler:(void (^)(NSString* path, 19 | NSString* appName, 20 | NSString* appVersion, 21 | NSData* appIconPNG))completionHandler; 22 | 23 | @end 24 | 25 | @protocol WorkingCopyProtocolVer352 26 | 27 | -(void)fetchStatusWithCompletionHandler:(void (^)(NSUInteger linesAdded, 28 | NSUInteger linesDeleted, 29 | NSError* error))completionHandler; 30 | 31 | @end 32 | 33 | @interface WorkingCopyUrlService () { 34 | NSXPCConnection* connection; 35 | id proxy1; 36 | id proxy352; 37 | 38 | NSError* error; 39 | void (^errorHandler)(NSError* error); 40 | } 41 | 42 | @end 43 | 44 | @implementation WorkingCopyUrlService 45 | 46 | -(void)determineDeepLinkWithCompletionHandler:(void (^_Nonnull)(NSURL* _Nullable url, 47 | NSError* _Nullable error))completionHandler { 48 | errorHandler = ^(NSError* error) { 49 | completionHandler(nil, error); 50 | }; 51 | 52 | [proxy1 determineDeepLinkWithCompletionHandler:^(NSURL* url) { 53 | NSError* theError = [self->error copy]; 54 | 55 | dispatch_async(dispatch_get_main_queue(), ^{ 56 | completionHandler(url, theError); 57 | }); 58 | }]; 59 | } 60 | 61 | -(void)fetchDocumentSourceInfoWithCompletionHandler:(void (^_Nonnull)(NSString* _Nullable path, 62 | NSString* _Nullable appName, 63 | NSString* _Nullable appVersion, 64 | UIImage* _Nullable appIcon, 65 | NSError* _Nullable error))completionHandler { 66 | errorHandler = ^(NSError* error) { 67 | completionHandler(nil, nil, nil, nil, error); 68 | }; 69 | 70 | [proxy1 fetchDocumentSourceInfoWithCompletionHandler:^(NSString* path, 71 | NSString* appName, 72 | NSString* appVersion, 73 | NSData* iconPNG) { 74 | NSError* theError = [self->error copy]; 75 | UIImage* icon = iconPNG == nil ? nil : [UIImage imageWithData:iconPNG]; 76 | 77 | dispatch_async(dispatch_get_main_queue(), ^{ 78 | completionHandler(path, appName, appVersion, icon, theError); 79 | }); 80 | }]; 81 | } 82 | 83 | -(void)fetchStatusWithCompletionHandler:(void (^_Nonnull)(NSUInteger linesAdded, 84 | NSUInteger linesDeleted, 85 | NSError* _Nullable error))completionHandler { 86 | if(proxy352 == nil) { 87 | NSString* message = NSLocalizedString(@"Status check requires Working Copy 3.5.2 or later.", nil); 88 | NSDictionary* userInfo = @{NSLocalizedDescriptionKey: message}; 89 | NSError* error = [NSError errorWithDomain:@"Working Copy" code:400 userInfo:userInfo]; 90 | completionHandler(0,0, error); 91 | return; 92 | } 93 | 94 | errorHandler = ^(NSError* error) { 95 | completionHandler(0,0, error); 96 | }; 97 | 98 | [proxy352 fetchStatusWithCompletionHandler:^(NSUInteger linesAdded, 99 | NSUInteger linesDeleted, 100 | NSError* error) { 101 | 102 | NSError* theError = error ?: [self->error copy]; 103 | 104 | dispatch_async(dispatch_get_main_queue(), ^{ 105 | completionHandler(linesAdded, linesDeleted, 106 | theError); 107 | }); 108 | }]; 109 | } 110 | 111 | -(instancetype)initWithConnection:(NSXPCConnection*)theConnection 112 | serviceName:(NSString*)serviceName { 113 | self = [super init]; 114 | if(self != nil) { 115 | connection = theConnection; 116 | 117 | Protocol* protocol = nil; 118 | if([serviceName isEqualToString:ServiceNameVer352]) { 119 | protocol = @protocol(WorkingCopyProtocolVer352); 120 | } else { 121 | protocol = @protocol(WorkingCopyProtocolVer1); 122 | } 123 | 124 | connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:protocol]; 125 | [connection resume]; 126 | 127 | proxy1 = [connection remoteObjectProxyWithErrorHandler:^(NSError* theError) { 128 | self->error = theError; 129 | [self->connection invalidate]; 130 | 131 | if(self->errorHandler) { 132 | // make sure error handler is only called once 133 | void (^copy)(NSError* error) = [self->errorHandler copy]; 134 | self->errorHandler = nil; 135 | dispatch_async(dispatch_get_main_queue(), ^{ 136 | copy(theError); 137 | }); 138 | } 139 | }]; 140 | 141 | if([serviceName isEqualToString:ServiceNameVer352]) { 142 | proxy352 = (id)proxy1; 143 | } 144 | } 145 | return self; 146 | } 147 | 148 | -(void)dealloc { 149 | [connection invalidate]; 150 | } 151 | 152 | +(void)getServiceForUrl:(nonnull NSURL*)url 153 | completionHandler:(void (^_Nonnull)(WorkingCopyUrlService* _Nullable service, 154 | NSError* _Nullable error))completionHandler { 155 | 156 | BOOL securityScoped = [url startAccessingSecurityScopedResource]; 157 | 158 | [[NSFileManager defaultManager] getFileProviderServicesForItemAtURL:url 159 | completionHandler:^(NSDictionary* services, 160 | NSError* error) { 161 | // check that we have provider service 162 | NSFileProviderService* providerService = services[ServiceNameVer352]; 163 | if(providerService == nil) providerService = services[ServiceNameVer1]; 164 | 165 | if(error != nil || providerService == nil) { 166 | dispatch_async(dispatch_get_main_queue(), ^{ 167 | completionHandler(nil, error); 168 | }); 169 | if(securityScoped) { 170 | [url stopAccessingSecurityScopedResource]; 171 | } 172 | return; 173 | } 174 | 175 | // attempt connection 176 | [providerService getFileProviderConnectionWithCompletionHandler:^(NSXPCConnection* connection, 177 | NSError* error) { 178 | 179 | if(securityScoped) { 180 | [url stopAccessingSecurityScopedResource]; 181 | } 182 | 183 | // make sure we have connection 184 | if(error != nil || connection == nil) { 185 | dispatch_async(dispatch_get_main_queue(), ^{ 186 | completionHandler(nil, error); 187 | }); 188 | return; 189 | } 190 | 191 | // setup proxy object 192 | WorkingCopyUrlService* service = [[WorkingCopyUrlService alloc] initWithConnection:connection 193 | serviceName:providerService.name]; 194 | dispatch_async(dispatch_get_main_queue(), ^{ 195 | completionHandler(service, nil); 196 | }); 197 | }]; 198 | }]; 199 | } 200 | 201 | @end 202 | -------------------------------------------------------------------------------- /OpenInPlace/Working Copy/WorkingCopyUrlService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkingCopyUrlService.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 24/09/2018. 6 | // Copyright © 2018 Applied Phasor. All rights reserved. 7 | // 8 | // There is a Objective-C version of this file with the same behaviour. 9 | 10 | import UIKit 11 | 12 | @objc fileprivate protocol WorkingCopyProtocolVer1 { 13 | func determineDeepLinkWithCompletionHandler(_ completionHandler: 14 | @escaping ((URL?) -> Void)) 15 | 16 | func fetchDocumentSourceInfoWithCompletionHandler(_ completionHandler: 17 | @escaping ((String?, String?, 18 | String?, Data?) -> Void)) 19 | } 20 | 21 | @objc fileprivate protocol WorkingCopyProtocolVer352 : WorkingCopyProtocolVer1 { 22 | func fetchStatusWithCompletionHandler(_ completionHandler: 23 | @escaping ((UInt, UInt, Error?) -> Void)) 24 | } 25 | 26 | /// Retrieves information about files and directories stored in a Working Copy 27 | /// file provider. 28 | /// 29 | /// You initialise instances from a file URL that the user has granted your app 30 | /// access using the document picker, document browser, drag and drop or by 31 | /// opening a file in-place. 32 | class WorkingCopyUrlService { 33 | private static let serviceNameVer1 = NSFileProviderServiceName("working-copy-v1") 34 | private static let serviceNameVer352 = NSFileProviderServiceName("working-copy-v3.5.2") 35 | 36 | private var connection: NSXPCConnection 37 | private var proxy1: WorkingCopyProtocolVer1? 38 | private var proxy352: WorkingCopyProtocolVer352? 39 | 40 | private var error: Error? 41 | private var errorHandler: ((Error) -> Void)? 42 | 43 | /// Try to inquire and connect to WorkingCopyUrlService on the given URL. 44 | /// 45 | /// Note that you can get a nil-service even without a error when url is outside 46 | /// a Working Copy file provider. 47 | /// 48 | /// Completion handler is called on main thread. 49 | @available(iOS 11.0, *) class public func getFor(_ url: URL, 50 | completionHandler: @escaping ((WorkingCopyUrlService?, Error?) -> ())) { 51 | let securityScoped = url.startAccessingSecurityScopedResource(); 52 | FileManager.default.getFileProviderServicesForItem(at: url, completionHandler: { 53 | (services, error) in 54 | 55 | // check that we have provider service 56 | let potentialService = services?[serviceNameVer352] ?? services?[serviceNameVer1] 57 | 58 | guard let providerService = potentialService, error == nil else { 59 | DispatchQueue.main.async { 60 | completionHandler(nil, error) 61 | } 62 | if securityScoped { 63 | url.stopAccessingSecurityScopedResource() 64 | } 65 | return 66 | } 67 | 68 | // attempt connection 69 | providerService.getFileProviderConnection(completionHandler: { 70 | (connection, error) in 71 | 72 | if securityScoped { 73 | url.stopAccessingSecurityScopedResource() 74 | } 75 | 76 | // make sure we have connection 77 | guard let theConnection = connection, error == nil else { 78 | DispatchQueue.main.async { 79 | completionHandler(nil, error) 80 | } 81 | return 82 | } 83 | 84 | // setup proxy object 85 | let service = WorkingCopyUrlService(theConnection, providerService.name) 86 | DispatchQueue.main.async { 87 | completionHandler(service, nil) 88 | } 89 | }) 90 | }) 91 | } 92 | 93 | /// Determine deep-link for opening a the given URL inside Working Copy. 94 | /// 95 | /// This link is something on the form: 96 | /// 97 | /// working-copy://open?repo=welcome%20to%20working%20copy&path=README.md 98 | /// 99 | /// Completion block is called on main thread. 100 | public func determineDeepLink(completionHandler: @escaping ((_ url: URL?, 101 | Error?) -> Void)) { 102 | 103 | errorHandler = { error in 104 | completionHandler(nil, error) 105 | } 106 | 107 | proxy1?.determineDeepLinkWithCompletionHandler({ url in 108 | let theError = self.error 109 | DispatchQueue.main.async { 110 | completionHandler(url, theError) 111 | } 112 | }) 113 | } 114 | 115 | /// Determine path relative to Working Copy storage and app information 116 | /// that is shared by all Working Copy URLs. 117 | /// 118 | /// Completion block is called on main thread. 119 | public func fetchDocumentSourceInfo(completionHandler: @escaping ((_ path: String?, 120 | _ appName: String?, 121 | _ appVersion: String?, 122 | _ icon: UIImage?, 123 | Error?) -> Void)) { 124 | 125 | errorHandler = { error in 126 | completionHandler(nil, nil, nil, nil, error) 127 | } 128 | 129 | proxy1?.fetchDocumentSourceInfoWithCompletionHandler({ 130 | (path, appName, appVersion, iconPNG) in 131 | 132 | let theError = self.error 133 | var icon: UIImage? 134 | if let png = iconPNG { 135 | icon = UIImage(data: png) 136 | } 137 | 138 | DispatchQueue.main.async { 139 | completionHandler(path, appName, appVersion, icon, theError) 140 | } 141 | }) 142 | } 143 | 144 | /// Determine the lines added or deleted for the file at the given URL compared to last commit. 145 | /// If the file is current both lines added and deleted are zero, while NSNotFound indicates 146 | /// a modified binary file. 147 | /// 148 | /// Completion block is called on main thread. 149 | public func fetchStatus(completionHandler: @escaping ((_ linesAdded: UInt, 150 | _ linesDeleted: UInt, 151 | Error?) -> Void)) { 152 | 153 | guard let proxy = proxy352 else { 154 | let message = NSLocalizedString("Status check requires Working Copy 3.5.2 or later.", comment: "") 155 | let userInfo = [NSLocalizedDescriptionKey: message] 156 | let error = NSError.init(domain: "Working Copy", code: 400, userInfo: userInfo) 157 | completionHandler(0,0, error) 158 | return 159 | } 160 | 161 | errorHandler = { error in 162 | completionHandler(0,0, error) 163 | } 164 | 165 | proxy.fetchStatusWithCompletionHandler({ 166 | (linesAdded, linesDeleted, error) in 167 | 168 | let theError = error ?? self.error 169 | DispatchQueue.main.async { 170 | completionHandler(linesAdded, linesDeleted, 171 | theError) 172 | } 173 | }) 174 | } 175 | 176 | private init(_ theConnection: NSXPCConnection, 177 | _ serviceName: NSFileProviderServiceName) { 178 | connection = theConnection 179 | 180 | var theProtocol: Protocol? 181 | if serviceName == WorkingCopyUrlService.serviceNameVer352 { 182 | theProtocol = WorkingCopyProtocolVer352.self 183 | } else { 184 | theProtocol = WorkingCopyProtocolVer1.self 185 | } 186 | 187 | connection.remoteObjectInterface = NSXPCInterface(with: theProtocol!) 188 | connection.resume() 189 | 190 | proxy1 = connection.remoteObjectProxyWithErrorHandler({ 191 | error in 192 | 193 | self.error = error 194 | self.connection.invalidate() 195 | 196 | if let handler = self.errorHandler { 197 | // make sure error handler is only called once 198 | self.errorHandler = nil 199 | 200 | DispatchQueue.main.async { 201 | handler(error) 202 | } 203 | } 204 | }) as? WorkingCopyProtocolVer1 205 | 206 | if serviceName == WorkingCopyUrlService.serviceNameVer352 { 207 | proxy352 = proxy1 as? WorkingCopyProtocolVer352 208 | } 209 | 210 | } 211 | 212 | deinit { 213 | connection.invalidate() 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /OpenInPlace/XCallbackOpener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemeOpener.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 02/01/2023. 6 | // Copyright © 2023 Applied Phasor. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MobileCoreServices 11 | 12 | // Helps manage programmatically opening of files from other apps once 13 | // user has granted access to directory files reside in. You need to set 14 | // feed URLs opened with app to didHandleUrl and to implemenent file 15 | // opening in openCallback. 16 | // 17 | // The other app would open URLs on the form: 18 | // open-in-place://x-callback-url/open-in-place?root=/ShellFishRootFolder&path=InnerDir/README.md 19 | // and the first time the user would be asked to pick the root folder before the file was opened 20 | // but after this any files inside root folder can be opened without showing the document picker. 21 | // 22 | // You need to set XCallbackOpener.shared.openCallback in `application(_:didFinishLaunchingWithOptions:)` 23 | // to open the file similarly to what you would do when users trigerred opening of URLs with document 24 | // picker or document browser. This often includes starting security scope and opening the file under 25 | // file coordination. 26 | // 27 | // You probably want to change XCallbackOpener.shared.handleError to report errors in a nicer 28 | // way when there is no way to report the error back to source app triggering x-callback-url. 29 | // Currently it will show a system alert. 30 | class XCallbackOpener { 31 | public var openCallback: (URL, UIViewController) -> Void = { _, _ in } 32 | 33 | // default error handler shows alert 34 | public var handleError: (Error, UIViewController) -> Void = { error, vc in 35 | let title = NSLocalizedString("Error", comment: "") 36 | let message = error.localizedDescription 37 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 38 | alert.addAction(.init(title: NSLocalizedString("Cancel", comment: ""), style: .cancel)) 39 | vc.present(alert, animated: true) 40 | } 41 | 42 | private init() {} 43 | 44 | public static var shared: XCallbackOpener = { XCallbackOpener() }() 45 | 46 | public func couldHandleUrl(_ url: URL) -> Bool { 47 | return url.host == "x-callback-url" && url.path == "/open-in-place" 48 | } 49 | 50 | // vc is used if we need to prompt the user to pick root folder 51 | // 52 | // false is returned for URLs that don't make sense 53 | // exception is thrown if there are errors and no "on-error" handler 54 | // to pass that error back on 55 | public func didHandleUrl(_ url: URL, _ vc: UIViewController) throws -> Bool { 56 | guard couldHandleUrl(url) else { 57 | return false 58 | } 59 | 60 | // decode url parameters 61 | var parameters = [String: String]() 62 | let query = url.query ?? "" 63 | for keyval in query.components(separatedBy: "&") { 64 | let parts = keyval.components(separatedBy: "=") 65 | if parts.count == 2, 66 | let key = parts[0].removingPercentEncoding, 67 | let val = parts[1].removingPercentEncoding { 68 | 69 | parameters[key] = val 70 | } 71 | } 72 | 73 | let request = XCallbackOpenerRequest(vc, parameters) 74 | request.work() 75 | 76 | return true 77 | } 78 | 79 | // we need to retain requests during folder picking 80 | fileprivate var retainedRequest = Set() 81 | 82 | fileprivate lazy var appName: String? = { 83 | let infoDictionary = Bundle.main.infoDictionary 84 | return infoDictionary?["CFBundleDisplayName"] as? String ?? 85 | infoDictionary?["CFBundleName"] as? String 86 | }() 87 | 88 | private let bookmarkDefaultsKey = "open-in-place.bookmarks" 89 | 90 | fileprivate func urlWithRoot(_ root: String) -> URL? { 91 | guard let array = UserDefaults.standard.array(forKey: bookmarkDefaultsKey) as? [Data] else { 92 | return nil 93 | } 94 | 95 | // we write back fixed version of array as needed 96 | var modified: [Data]? 97 | defer { 98 | if let modified = modified { 99 | UserDefaults.standard.set(modified, forKey: bookmarkDefaultsKey) 100 | } 101 | } 102 | 103 | for bookmark in array { 104 | var stale = false 105 | let url = try? URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &stale) 106 | 107 | let securityScoped = url?.startAccessingSecurityScopedResource() ?? false 108 | defer { 109 | if securityScoped { 110 | url?.stopAccessingSecurityScopedResource() 111 | } 112 | } 113 | 114 | // modify array to refresh stale and remove missing url 115 | if url == nil || stale { 116 | if modified == nil { 117 | modified = array 118 | } 119 | modified?.removeAll(where: { $0 == bookmark }) 120 | if stale, let newBookmark = try? url?.bookmarkData() { 121 | modified?.append(newBookmark) 122 | } 123 | } 124 | 125 | // check if url matches 126 | if let path = url?.path, path.hasPrefix(root) { 127 | return url 128 | } 129 | } 130 | 131 | return nil 132 | } 133 | 134 | fileprivate func rememberUrlForRoot(_ url: URL) throws { 135 | let securityScoped = url.startAccessingSecurityScopedResource() 136 | let bookmark = try url.bookmarkData() 137 | if securityScoped { 138 | url.stopAccessingSecurityScopedResource() 139 | } 140 | 141 | var array = UserDefaults.standard.array(forKey: bookmarkDefaultsKey) as? [Data] ?? [] 142 | array.append(bookmark) 143 | UserDefaults.standard.set(array, forKey: bookmarkDefaultsKey) 144 | } 145 | } 146 | 147 | fileprivate class XCallbackOpenerRequest : NSObject, UIDocumentPickerDelegate { 148 | let vc: UIViewController 149 | let parameters: [String: String] 150 | let shared: XCallbackOpener 151 | 152 | var path = "" 153 | var root = "" 154 | 155 | init(_ vc: UIViewController, _ parameters: [String: String]) { 156 | self.vc = vc 157 | self.parameters = parameters 158 | shared = XCallbackOpener.shared 159 | 160 | super.init() 161 | } 162 | 163 | func work() { 164 | // we need root directory and path to proceed 165 | guard let root = parameters["root"], 166 | var path = parameters["path"] else { 167 | 168 | let message = NSLocalizedString("root and path parameters required", comment: "") 169 | callbackErrorMessage(message); 170 | return 171 | } 172 | 173 | // path is supposed to be relative to root, but to be helpful we allow it to be a full path 174 | if path.hasPrefix(root) { 175 | path = String(path.suffix(path.count - root.count)) 176 | } 177 | if path.hasPrefix("/") { 178 | path = String(path.suffix(path.count - 1)) 179 | } 180 | 181 | self.root = root 182 | self.path = path 183 | 184 | if let rootUrl = XCallbackOpener.shared.urlWithRoot(root) { 185 | // we already have permission 186 | deliverUrlFromRoot(rootUrl) 187 | } else { 188 | // we need user to pick folder 189 | shared.retainedRequest.insert(self) 190 | let picker = UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String], 191 | in: .open) 192 | picker.directoryURL = URL(fileURLWithPath: root) 193 | picker.delegate = self 194 | vc.present(picker, animated: true) 195 | } 196 | } 197 | 198 | private func deliverUrlFromRoot(_ rootUrl: URL) { 199 | let securityScoped = rootUrl.startAccessingSecurityScopedResource() 200 | let url = rootUrl.appendingPathComponent(path) 201 | shared.openCallback(url, vc) 202 | if securityScoped { 203 | rootUrl.stopAccessingSecurityScopedResource() 204 | } 205 | } 206 | 207 | func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { 208 | guard let url = urls.first else { 209 | return 210 | } 211 | 212 | shared.retainedRequest.remove(self) 213 | do { 214 | try shared.rememberUrlForRoot(url) 215 | deliverUrlFromRoot(url) 216 | } catch { 217 | callbackError(error) 218 | } 219 | } 220 | 221 | func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { 222 | let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError) 223 | callbackError(cancelError) 224 | 225 | shared.retainedRequest.remove(self) 226 | } 227 | 228 | private func didMakeCallback(_ key: String, 229 | _ result: [String: String]) -> Bool { 230 | guard var callback = parameters[key] else { 231 | return false 232 | } 233 | 234 | // first delimiter is ? if there are no other parameters 235 | callback.append(callback.contains("?") ? "&" : "?") 236 | 237 | // add app name if possible 238 | if let appName = XCallbackOpener.shared.appName?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { 239 | callback.append("x-source=") 240 | callback.append(appName) 241 | callback.append("&") 242 | } 243 | 244 | for (key, val) in result { 245 | guard let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), 246 | let escapedVal = val.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { 247 | return false 248 | } 249 | 250 | callback += "\(escapedKey)=\(escapedVal)&" 251 | } 252 | 253 | guard let url = URL(string: callback) else { 254 | return false 255 | } 256 | 257 | UIApplication.shared.open(url) 258 | return true 259 | } 260 | 261 | // do error callback if possible otherwise throw as exception 262 | private func callbackError(_ error: Error) { 263 | let result = ["errorCode": "\((error as NSError).code)", 264 | "errorMessage": error.localizedDescription] 265 | if !didMakeCallback("on-error", result) { 266 | // show error internally when not able to callback to source app 267 | shared.handleError(error, vc) 268 | } 269 | } 270 | 271 | private func callbackErrorMessage(_ message: String) { 272 | let userInfo = [NSLocalizedDescriptionKey: message] 273 | let error = NSError(domain: "XCallbackOpener", code: 0, userInfo: userInfo) 274 | callbackError(error) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /OpenInPlace/XDocumentNotify.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XDocumentSource2.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 14/02/2019. 6 | // Copyright © 2019 Applied Phasor. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @available(iOS 11.0, *) 12 | extension URL { 13 | // Call didAccess on dk.andersborum.document-notify if available. 14 | // 15 | // Callback is always made on main thread if dk.andersborum.document-notify service 16 | // is missing the result is NSCocoaDomain, NSFeatureUnsupportedError. 17 | func xDocumentNotifyDidAccess(_ completion: @escaping (_ error: Error?) -> Void) { 18 | 19 | func done(_ error: Error?) { 20 | DispatchQueue.main.async { 21 | completion(error) 22 | } 23 | } 24 | 25 | let securityScoped = startAccessingSecurityScopedResource() 26 | FileManager.default.getFileProviderServicesForItem(at: self, completionHandler: { 27 | (services, error) in 28 | 29 | // check that we have provider service 30 | let name = NSFileProviderServiceName("dk.andersborum.document-notify") 31 | guard let service = services?[name] else { 32 | if securityScoped { self.stopAccessingSecurityScopedResource() } 33 | 34 | done(NSError(domain: NSCocoaErrorDomain, 35 | code: NSFeatureUnsupportedError)) 36 | return 37 | } 38 | 39 | // attempt connection 40 | service.getFileProviderConnection(completionHandler: { 41 | (connection, error) in 42 | 43 | if securityScoped { 44 | self.stopAccessingSecurityScopedResource() 45 | } 46 | 47 | // make sure we have connection 48 | guard let connection = connection, error == nil else { 49 | done(error) 50 | return 51 | } 52 | 53 | // setup proxy object 54 | connection.remoteObjectInterface = NSXPCInterface(with: XDocumentNotifyProtocol.self) 55 | connection.resume() 56 | let proxy = connection.remoteObjectProxy as! XDocumentNotifyProtocol 57 | 58 | // make remote call 59 | proxy.didAccess(completionHandler: done) 60 | }) 61 | }) 62 | } 63 | } 64 | 65 | @objc fileprivate protocol XDocumentNotifyProtocol { 66 | func didAccess(completionHandler: (((any Error)?) -> Void)!) 67 | } 68 | -------------------------------------------------------------------------------- /OpenInPlace/XDocumentSource2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XDocumentSource2.swift 3 | // OpenInPlace 4 | // 5 | // Created by Anders Borum on 14/02/2019. 6 | // Copyright © 2019 Applied Phasor. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 11.0, *) 12 | extension URL { 13 | func fetchDocumentInfo(pixelSize: UInt = 0, 14 | completionHandler: @escaping ((_ path: String?, 15 | _ appName: String?, 16 | _ appVersion: String?, 17 | _ icon: UIImage?) -> Void)) { 18 | 19 | let securityScoped = startAccessingSecurityScopedResource() 20 | FileManager.default.getFileProviderServicesForItem(at: self, completionHandler: { 21 | (services, error) in 22 | 23 | // check that we have provider service 24 | let name = NSFileProviderServiceName("x-document-source-2") 25 | guard let service = services?[name] else { 26 | if securityScoped { self.stopAccessingSecurityScopedResource() } 27 | 28 | DispatchQueue.main.async { 29 | completionHandler(nil, nil, nil, nil) 30 | } 31 | return 32 | } 33 | 34 | // attempt connection 35 | service.getFileProviderConnection(completionHandler: { 36 | (connection, error) in 37 | 38 | if securityScoped { 39 | self.stopAccessingSecurityScopedResource() 40 | } 41 | 42 | // make sure we have connection 43 | guard let connection = connection, error == nil else { 44 | DispatchQueue.main.async { 45 | completionHandler(nil, nil, nil, nil) 46 | } 47 | return 48 | } 49 | 50 | // setup proxy object 51 | connection.remoteObjectInterface = NSXPCInterface(with: XDocumentSource2Protocol.self) 52 | connection.resume() 53 | let proxy = connection.remoteObjectProxy as! XDocumentSource2Protocol 54 | 55 | // make remote call 56 | proxy.fetchDocumentSourceInfoPixelSize(pixelSize, 57 | completionHandler: { (path, appName, appVersion, iconPNG) in 58 | var image: UIImage? 59 | if let data = iconPNG { 60 | image = UIImage(data: data) 61 | } 62 | 63 | DispatchQueue.main.async { 64 | completionHandler(path, appName, appVersion, image) 65 | } 66 | }) 67 | }) 68 | }) 69 | } 70 | } 71 | 72 | @objc fileprivate protocol XDocumentSource2Protocol { 73 | 74 | func fetchDocumentSourceInfoPixelSize(_ pixelSize: UInt, // use pixelSize=0 to not get icon back 75 | completionHandler: ((String?, String?, String?, Data?) -> Void)!) 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Swift 3 | 4 | Platform: iOS 5 | Twitter 6 |

7 | 8 | # Open in Place. 9 | Example app that illustrates how to get open-in-place to work well with iOS file providers. 10 | 11 | It shows how to: 12 | 13 | - invoke the document picker in open mode for files and directories 14 | - receive open-in-place file references through Drag and Drop 15 | - use and persist the security scoped URLs the document picker or Drag and Drop gives you 16 | - work with a directory in a coordinated manner to stay in sync 17 | - edit a text file in a coordinated manner such that your changes are written safely and such that outside changes appear in the editor automatically 18 | - use the [WorkingCopyUrlService](OpenInPlace/Working%20Copy/WorkingCopyUrlService.swift) file-provider SDK to fetch information about entries 19 | - open using x-callback-url without user interaction for files in folders user has previously granted access using [XCallbackOpener](OpenInPlace/XCallbackOpener.swift) 20 | 21 | Using the document picker to open directories will probably only work for iCloud Drive, external drives and 22 | a few third party apps. I am the author of [Working Copy](https://itunes.apple.com/us/app/working-copy/id896694807?mt=8&uo=6&at=1000lHq&ct=openinplace), 23 | [S3 Files](https://apps.apple.com/us/app/s3-files/id6447647340?mt=openinplace) and 24 | [Secure ShellFish](https://apps.apple.com/us/app/secure-shellfish-sftp-client/id1336634154?mt=openinplace) 25 | that fully supports opening directories in-place. Opening files in-place should be supported by all file providers. 26 | 27 | The excellent [Textastic](https://geo.itunes.apple.com/us/app/id1049254261?ct=textasticapp.com&at=11lNQP&pt=15967&mt=8) 28 | has been [doing](http://blach.io/2016/08/02/opening-git-repository-folders-in-textastic-6-2/) this for a while and so 29 | does [Codea](http://itunes.apple.com/app/id439571171?mt=8), [iA Writer](https://ia.net/writer) and 30 | [Pythonista](https://apps.apple.com/us/app/pythonista-3/id1085978097?ls=1). 31 | My hope is that providing sample code will encourage others to follow suit. 32 | 33 | A good place to start is at the top of [ListController](OpenInPlace/ListController.swift) and 34 | [EditController](OpenInPlace/EditController.swift). 35 | 36 | If you have any questions the easiest way to catch me is on Twitter as [@palmin](https://twitter.com/palmin). 37 | --------------------------------------------------------------------------------