iOS: Share Extension (share to Noo) via an App Group inbox
ShareExtension target copies shared files into group.dev.ayushya.noo and leaves a manifest; the app consumes it on launch/activation through the existing share_intent channel into the destination picker. Extension is embedded before Flutter's script phases; versions follow FLUTTER_BUILD_*. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8b86aa8f34
commit
525504bc47
@@ -8,16 +8,21 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
223784E5DAE5A653F0F61771 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 405A5408F4071B4D12F08B21 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
33B3C3BED4C1F26439DD6825 /* SharedInbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836D37F2376AFCE0EFDD30A7 /* SharedInbox.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
5694337ECF8D3ADD3695CBCD /* NativeServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 901C0CDB4FB819302E2A7563 /* NativeServices.swift */; };
|
||||
63229CACE4281880037D33E6 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BDF61F5197F9B8F648E8AE /* ShareViewController.swift */; };
|
||||
66D3CBDE6B3CD3DC578CECBE /* WebDAV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F798220CB598B611CA03EFC /* WebDAV.swift */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
91C1658DC7467CACB2AAC7FF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AE6F681A3100A461B4A5318 /* Foundation.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
F8D51B4045340275FBD12D16 /* SharedInbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 836D37F2376AFCE0EFDD30A7 /* SharedInbox.swift */; };
|
||||
FDFA55C4921747A9CBCBBEEC /* TransferManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064CC1652B8F0A44E371638 /* TransferManager.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -29,9 +34,27 @@
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
850C66E713F7C56BDA822FB7 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = F9BE9B2F18B10324D7B84776;
|
||||
remoteInfo = ShareExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
201269C4EFA9E5FDBC74F86F /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
223784E5DAE5A653F0F61771 /* ShareExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -45,11 +68,16 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0AE6F681A3100A461B4A5318 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
27BDF61F5197F9B8F648E8AE /* ShareViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
405A5408F4071B4D12F08B21 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
49213989F0FA1B83B0B8A523 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
65F57A1EB0C62774E561860D /* ShareExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||
6F798220CB598B611CA03EFC /* WebDAV.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebDAV.swift; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -57,6 +85,8 @@
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
8064CC1652B8F0A44E371638 /* TransferManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferManager.swift; sourceTree = "<group>"; };
|
||||
836D37F2376AFCE0EFDD30A7 /* SharedInbox.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedInbox.swift; sourceTree = "<group>"; };
|
||||
857A0E7637776A71AFBB0614 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
901C0CDB4FB819302E2A7563 /* NativeServices.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NativeServices.swift; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -68,6 +98,14 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
807C81744907462A25A6C1F8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
91C1658DC7467CACB2AAC7FF /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -79,6 +117,14 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
2B7F16B0F0EA2C5A842ED860 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0AE6F681A3100A461B4A5318 /* Foundation.framework */,
|
||||
);
|
||||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -87,6 +133,17 @@
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3664F08C433ECEB00D1C4BDB /* ShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27BDF61F5197F9B8F648E8AE /* ShareViewController.swift */,
|
||||
49213989F0FA1B83B0B8A523 /* Info.plist */,
|
||||
65F57A1EB0C62774E561860D /* ShareExtension.entitlements */,
|
||||
);
|
||||
name = ShareExtension;
|
||||
path = ShareExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5055E53DC55C0BCABD5F04F4 /* Native */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -98,6 +155,15 @@
|
||||
path = Native;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5DF0A9032A8500B67D6C89AE /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
836D37F2376AFCE0EFDD30A7 /* SharedInbox.swift */,
|
||||
);
|
||||
name = Shared;
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -117,6 +183,9 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
5DF0A9032A8500B67D6C89AE /* Shared */,
|
||||
3664F08C433ECEB00D1C4BDB /* ShareExtension */,
|
||||
A5B80AB7F4AB17F90B745D0D /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -125,6 +194,7 @@
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
405A5408F4071B4D12F08B21 /* ShareExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -142,10 +212,19 @@
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
5055E53DC55C0BCABD5F04F4 /* Native */,
|
||||
857A0E7637776A71AFBB0614 /* Runner.entitlements */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5B80AB7F4AB17F90B745D0D /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2B7F16B0F0EA2C5A842ED860 /* iOS */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -170,6 +249,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
201269C4EFA9E5FDBC74F86F /* Embed Foundation Extensions */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
@@ -180,6 +260,7 @@
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
6E0BAC83BFDF8623DBFB9D75 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
@@ -189,6 +270,23 @@
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
F9BE9B2F18B10324D7B84776 /* ShareExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = E1076120E395D274D37607ED /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
||||
buildPhases = (
|
||||
3CD40C8747E650E6F817CDDB /* Sources */,
|
||||
807C81744907462A25A6C1F8 /* Frameworks */,
|
||||
4EDBC7D61EA0C0974DA27BD6 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = ShareExtension;
|
||||
productName = ShareExtension;
|
||||
productReference = 405A5408F4071B4D12F08B21 /* ShareExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -227,6 +325,7 @@
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
F9BE9B2F18B10324D7B84776 /* ShareExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -239,6 +338,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4EDBC7D61EA0C0974DA27BD6 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -295,6 +401,15 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3CD40C8747E650E6F817CDDB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
63229CACE4281880037D33E6 /* ShareViewController.swift in Sources */,
|
||||
33B3C3BED4C1F26439DD6825 /* SharedInbox.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -305,6 +420,7 @@
|
||||
66D3CBDE6B3CD3DC578CECBE /* WebDAV.swift in Sources */,
|
||||
FDFA55C4921747A9CBCBBEEC /* TransferManager.swift in Sources */,
|
||||
5694337ECF8D3ADD3695CBCD /* NativeServices.swift in Sources */,
|
||||
F8D51B4045340275FBD12D16 /* SharedInbox.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -316,6 +432,12 @@
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
6E0BAC83BFDF8623DBFB9D75 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = ShareExtension;
|
||||
target = F9BE9B2F18B10324D7B84776 /* ShareExtension */;
|
||||
targetProxy = 850C66E713F7C56BDA822FB7 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
@@ -396,6 +518,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -458,6 +581,89 @@
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
3B057BF66761E30DEABF8B30 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.ayushya.noo.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
6BF252CEA7A55024A2F58A36 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.ayushya.noo.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
6FB3F72FB5A94DE254FF4FBF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.ayushya.noo.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -575,6 +781,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -597,6 +804,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -645,6 +853,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
E1076120E395D274D37607ED /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6BF252CEA7A55024A2F58A36 /* Release */,
|
||||
6FB3F72FB5A94DE254FF4FBF /* Debug */,
|
||||
3B057BF66761E30DEABF8B30 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Flutter
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Pushes upload-batch completions to Dart's `UploadService.completions`.
|
||||
final class UploadStatusStreamHandler: NSObject, FlutterStreamHandler {
|
||||
/// Holds the one native-side listener of an `EventChannel` so Swift code can
|
||||
/// push events to Dart whenever they happen.
|
||||
final class SinkStreamHandler: NSObject, FlutterStreamHandler {
|
||||
var sink: FlutterEventSink?
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
||||
@@ -19,12 +21,12 @@ final class UploadStatusStreamHandler: NSObject, FlutterStreamHandler {
|
||||
}
|
||||
|
||||
/// The iOS side of the `dev.ayushya.noo/*` channels `lib/services/` talks to
|
||||
/// (Android's lives in `MainActivity.kt`). Only the upload and download
|
||||
/// services are implemented so far; the rest still throw
|
||||
/// `MissingPluginException`, which the Dart side treats as "unavailable"
|
||||
/// (`lib/services/native_channel.dart`).
|
||||
/// (Android's lives in `MainActivity.kt`). Upload, download and share (the
|
||||
/// receiving half of the Share Extension) are implemented so far; the rest
|
||||
/// still throw `MissingPluginException`, which the Dart side treats as
|
||||
/// "unavailable" (`lib/services/native_channel.dart`).
|
||||
enum NativeServices {
|
||||
private static let uploadStatus = UploadStatusStreamHandler()
|
||||
private static let uploadStatus = SinkStreamHandler()
|
||||
|
||||
static func register(messenger: FlutterBinaryMessenger) {
|
||||
FlutterMethodChannel(name: "dev.ayushya.noo/upload_service", binaryMessenger: messenger)
|
||||
@@ -72,6 +74,46 @@ enum NativeServices {
|
||||
uploadStatus.sink?(["remoteFolder": folder, "succeeded": succeeded, "failed": failed])
|
||||
}
|
||||
TransferManager.shared.reconnect()
|
||||
registerShare(messenger: messenger)
|
||||
}
|
||||
|
||||
// MARK: - Share Extension inbox
|
||||
|
||||
private static let shareEvents = SinkStreamHandler()
|
||||
|
||||
/// `share_intent`: files the Share Extension left in the App Group inbox.
|
||||
/// Cold start asks once (`getInitialShare`); a share that arrives while the
|
||||
/// app is running is pushed when it next becomes active (the extension
|
||||
/// can't signal the app any other way).
|
||||
private static func registerShare(messenger: FlutterBinaryMessenger) {
|
||||
FlutterMethodChannel(name: "dev.ayushya.noo/share_intent", binaryMessenger: messenger)
|
||||
.setMethodCallHandler { call, result in
|
||||
guard call.method == "getInitialShare" else { return result(FlutterMethodNotImplemented) }
|
||||
result(consumeShared().map(encode))
|
||||
}
|
||||
FlutterEventChannel(name: "dev.ayushya.noo/share_intent/new", binaryMessenger: messenger)
|
||||
.setStreamHandler(shareEvents)
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
|
||||
) { _ in
|
||||
guard let sink = shareEvents.sink else { return }
|
||||
let items = consumeShared()
|
||||
if !items.isEmpty { sink(items.map(encode)) }
|
||||
}
|
||||
}
|
||||
|
||||
private static func consumeShared() -> [SharedItem] {
|
||||
guard let root = AppGroup.containerURL else { return [] }
|
||||
return SharedInbox.consume(in: root)
|
||||
}
|
||||
|
||||
private static func encode(_ item: SharedItem) -> [String: Any] {
|
||||
[
|
||||
"uri": URL(fileURLWithPath: item.path).absoluteString,
|
||||
"name": item.name,
|
||||
"mimeType": item.mimeType ?? NSNull(),
|
||||
"size": item.size,
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Argument helpers
|
||||
|
||||
@@ -100,6 +100,11 @@ final class TransferManager: NSObject {
|
||||
let staged = stageDir.appendingPathComponent("\(index)-\(file.name)")
|
||||
do {
|
||||
try FileManager.default.copyItem(at: source, to: staged)
|
||||
// A file the Share Extension left in the App Group is ours to clean
|
||||
// up once it's staged; a picker's temp file isn't.
|
||||
if source.path.contains("/\(SharedInbox.folderName)/") {
|
||||
try? FileManager.default.removeItem(at: source)
|
||||
}
|
||||
} catch {
|
||||
throw TransferError(message: "Can't read \(file.name): \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.dev.ayushya.noo</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -49,4 +49,52 @@ class RunnerTests: XCTestCase {
|
||||
FileManager.default.createFile(atPath: dir.appendingPathComponent("README").path, contents: nil)
|
||||
XCTAssertEqual(LocalFiles.uniqueURL(in: dir, name: "README").lastPathComponent, "README (1)")
|
||||
}
|
||||
|
||||
// MARK: - SharedInbox (Share Extension hand-off)
|
||||
|
||||
private func makeRoot() throws -> URL {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("noo-inbox-\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
addTeardownBlock { try? FileManager.default.removeItem(at: root) }
|
||||
return root
|
||||
}
|
||||
|
||||
private func makeSharedFile(in root: URL, name: String, contents: String = "hi") throws -> SharedItem {
|
||||
let batch = try SharedInbox.makeBatchDirectory(in: root)
|
||||
let file = batch.appendingPathComponent(name)
|
||||
try contents.write(to: file, atomically: true, encoding: .utf8)
|
||||
return SharedItem(path: file.path, name: name, mimeType: "text/plain", size: Int64(contents.utf8.count))
|
||||
}
|
||||
|
||||
func testInboxConsumeReturnsAppendedItemsOnceThenNothing() throws {
|
||||
let root = try makeRoot()
|
||||
let a = try makeSharedFile(in: root, name: "a.txt")
|
||||
let b = try makeSharedFile(in: root, name: "b.txt")
|
||||
try SharedInbox.append([a], in: root)
|
||||
try SharedInbox.append([b], in: root)
|
||||
|
||||
XCTAssertEqual(SharedInbox.consume(in: root), [a, b])
|
||||
XCTAssertEqual(SharedInbox.consume(in: root), [], "each share is delivered once")
|
||||
}
|
||||
|
||||
func testInboxConsumeDropsFilesThatNoLongerExist() throws {
|
||||
let root = try makeRoot()
|
||||
let kept = try makeSharedFile(in: root, name: "kept.txt")
|
||||
let gone = try makeSharedFile(in: root, name: "gone.txt")
|
||||
try SharedInbox.append([kept, gone], in: root)
|
||||
try FileManager.default.removeItem(atPath: gone.path)
|
||||
|
||||
XCTAssertEqual(SharedInbox.consume(in: root), [kept])
|
||||
}
|
||||
|
||||
func testInboxConsumeWithNothingPendingIsEmpty() throws {
|
||||
XCTAssertEqual(SharedInbox.consume(in: try makeRoot()), [])
|
||||
}
|
||||
|
||||
func testInboxAppendOfNothingCreatesNoManifest() throws {
|
||||
let root = try makeRoot()
|
||||
try SharedInbox.append([], in: root)
|
||||
XCTAssertEqual(SharedInbox.consume(in: root), [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Noo</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>50</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>50</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>50</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.dev.ayushya.noo</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,122 @@
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
import UserNotifications
|
||||
|
||||
/// "Share to Noo": copies whatever was shared into the App Group container
|
||||
/// (`SharedInbox`) and finishes. The extension can't pick a destination or
|
||||
/// upload - it has no session and a tight memory/time budget - so the app
|
||||
/// does that when it next opens, through its existing destination picker. A
|
||||
/// notification tells the user to open it, because an extension can't launch
|
||||
/// its host app.
|
||||
final class ShareViewController: UIViewController {
|
||||
private let label = UILabel()
|
||||
private let spinner = UIActivityIndicatorView(style: .large)
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
label.text = "Saving to Noo…"
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
spinner.startAnimating()
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [spinner, label])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 16
|
||||
stack.alignment = .center
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
stack.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 24),
|
||||
stack.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -24),
|
||||
])
|
||||
|
||||
Task { await importSharedItems() }
|
||||
}
|
||||
|
||||
private func importSharedItems() async {
|
||||
guard let root = AppGroup.containerURL else {
|
||||
return finish(message: "Noo isn't set up to receive files yet.", saved: 0)
|
||||
}
|
||||
let providers = (extensionContext?.inputItems as? [NSExtensionItem])?
|
||||
.flatMap { $0.attachments ?? [] } ?? []
|
||||
|
||||
var saved: [SharedItem] = []
|
||||
do {
|
||||
let batch = try SharedInbox.makeBatchDirectory(in: root)
|
||||
for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.item.identifier) {
|
||||
if let item = try? await copy(provider, into: batch) { saved.append(item) }
|
||||
}
|
||||
try SharedInbox.append(saved, in: root)
|
||||
} catch {
|
||||
return finish(message: "Couldn't save to Noo: \(error.localizedDescription)", saved: 0)
|
||||
}
|
||||
|
||||
if saved.isEmpty {
|
||||
finish(message: "Noo can only receive files and photos.", saved: 0)
|
||||
} else {
|
||||
notify(count: saved.count)
|
||||
finish(message: saved.count == 1 ? "Saved 1 file to Noo" : "Saved \(saved.count) files to Noo", saved: saved.count)
|
||||
}
|
||||
}
|
||||
|
||||
/// `loadFileRepresentation`'s URL only lives for the duration of its
|
||||
/// callback, so the copy happens inside it.
|
||||
private func copy(_ provider: NSItemProvider, into directory: URL) async throws -> SharedItem {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
provider.loadFileRepresentation(forTypeIdentifier: UTType.item.identifier) { url, error in
|
||||
guard let url else {
|
||||
return continuation.resume(throwing: error ?? CocoaError(.fileReadUnknown))
|
||||
}
|
||||
do {
|
||||
let destination = Self.uniqueURL(in: directory, name: url.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: url, to: destination)
|
||||
let size = (try? FileManager.default.attributesOfItem(atPath: destination.path))?[.size] as? Int64 ?? 0
|
||||
let mime = UTType(filenameExtension: destination.pathExtension)?.preferredMIMEType
|
||||
continuation.resume(
|
||||
returning: SharedItem(
|
||||
path: destination.path, name: destination.lastPathComponent, mimeType: mime, size: size))
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func uniqueURL(in directory: URL, name: String) -> URL {
|
||||
var candidate = directory.appendingPathComponent(name)
|
||||
var index = 1
|
||||
while FileManager.default.fileExists(atPath: candidate.path) {
|
||||
let ext = (name as NSString).pathExtension
|
||||
let stem = (name as NSString).deletingPathExtension
|
||||
candidate = directory.appendingPathComponent(ext.isEmpty ? "\(stem) (\(index))" : "\(stem) (\(index)).\(ext)")
|
||||
index += 1
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
private func notify(count: Int) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Noo"
|
||||
content.body =
|
||||
count == 1
|
||||
? "Open Noo to choose where to upload 1 file."
|
||||
: "Open Noo to choose where to upload \(count) files."
|
||||
content.sound = .default
|
||||
UNUserNotificationCenter.current().add(
|
||||
UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil))
|
||||
}
|
||||
|
||||
private func finish(message: String, saved: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.spinner.stopAnimating()
|
||||
self.label.text = message
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + (saved > 0 ? 1.2 : 2.5)) {
|
||||
self.extensionContext?.completeRequest(returningItems: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import Foundation
|
||||
|
||||
/// The App Group both the app and its Share Extension belong to - the one
|
||||
/// place they can exchange files.
|
||||
enum AppGroup {
|
||||
static let identifier = "group.dev.ayushya.noo"
|
||||
|
||||
static var containerURL: URL? {
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
/// One file the Share Extension copied into the group container.
|
||||
struct SharedItem: Codable, Equatable {
|
||||
let path: String
|
||||
let name: String
|
||||
let mimeType: String?
|
||||
let size: Int64
|
||||
}
|
||||
|
||||
/// Files the Share Extension received, waiting for the app to ask the user
|
||||
/// where to upload them. The extension appends; the app consumes (reads and
|
||||
/// clears) when it launches or becomes active. Compiled into both targets.
|
||||
///
|
||||
/// Layout under `<root>/SharedInbox/`: `pending.json` (the manifest) and one
|
||||
/// `<batch-id>/` folder per share holding the copied files.
|
||||
enum SharedInbox {
|
||||
static let folderName = "SharedInbox"
|
||||
private static let manifestName = "pending.json"
|
||||
|
||||
static func directory(in root: URL) -> URL {
|
||||
root.appendingPathComponent(folderName, isDirectory: true)
|
||||
}
|
||||
|
||||
/// A fresh folder for one share's files.
|
||||
static func makeBatchDirectory(in root: URL) throws -> URL {
|
||||
let dir = directory(in: root).appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
return dir
|
||||
}
|
||||
|
||||
static func append(_ items: [SharedItem], in root: URL) throws {
|
||||
guard !items.isEmpty else { return }
|
||||
try withManifestLock(in: root) {
|
||||
let existing = (try? readManifest(in: root)) ?? []
|
||||
try writeManifest(existing + items, in: root)
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything pending, dropping entries whose file has gone missing, and
|
||||
/// clears the manifest so each share is delivered once.
|
||||
static func consume(in root: URL) -> [SharedItem] {
|
||||
var items: [SharedItem] = []
|
||||
try? withManifestLock(in: root) {
|
||||
items = ((try? readManifest(in: root)) ?? []).filter {
|
||||
FileManager.default.fileExists(atPath: $0.path)
|
||||
}
|
||||
try? FileManager.default.removeItem(at: manifestURL(in: root))
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private static func manifestURL(in root: URL) -> URL {
|
||||
directory(in: root).appendingPathComponent(manifestName)
|
||||
}
|
||||
|
||||
private static func readManifest(in root: URL) throws -> [SharedItem] {
|
||||
let data = try Data(contentsOf: manifestURL(in: root))
|
||||
return try JSONDecoder().decode([SharedItem].self, from: data)
|
||||
}
|
||||
|
||||
private static func writeManifest(_ items: [SharedItem], in root: URL) throws {
|
||||
try FileManager.default.createDirectory(
|
||||
at: directory(in: root), withIntermediateDirectories: true)
|
||||
try JSONEncoder().encode(items).write(to: manifestURL(in: root), options: .atomic)
|
||||
}
|
||||
|
||||
/// The extension and the app are separate processes that can both touch the
|
||||
/// manifest, so read-modify-write goes through a file coordinator.
|
||||
private static func withManifestLock(in root: URL, _ body: () throws -> Void) throws {
|
||||
try FileManager.default.createDirectory(
|
||||
at: directory(in: root), withIntermediateDirectories: true)
|
||||
var coordinationError: NSError?
|
||||
var thrown: Error?
|
||||
NSFileCoordinator().coordinate(
|
||||
writingItemAt: directory(in: root), options: [], error: &coordinationError
|
||||
) { _ in
|
||||
do { try body() } catch { thrown = error }
|
||||
}
|
||||
if let error = coordinationError ?? thrown.map({ $0 as NSError }) { throw error }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user