Add iOS save action extension

This commit is contained in:
Rijad Zuzo
2026-05-18 20:50:23 +02:00
parent 42a59e959f
commit d81215db73
6 changed files with 425 additions and 0 deletions
+3
View File
@@ -148,6 +148,9 @@ flutter run --dart-define=ENABLE_WHATSAPP_SHARE_INTAKE=false
Behavior notes: Behavior notes:
- iOS/Android: integrates with share-intent plugin (`receive_sharing_intent`) - iOS/Android: integrates with share-intent plugin (`receive_sharing_intent`)
- iOS also ships a `Save to Relationship Saver` Action extension for the lower
share-sheet action row; use it when that row is easier to reach than the app
share row
- shared text is parsed and ingested into: - shared text is parsed and ingested into:
- person profile (auto-create when identity is confident) - person profile (auto-create when identity is confident)
- source->profile link map for follow-up matching - source->profile link map for follow-up matching
+170
View File
@@ -20,6 +20,8 @@
A1B2C3021111111111111111 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30D1111111111111111 /* ShareViewController.swift */; }; A1B2C3021111111111111111 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C30D1111111111111111 /* ShareViewController.swift */; };
A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3181111111111111111 /* MainInterface.storyboard */; }; A1B2C3031111111111111111 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3181111111111111111 /* MainInterface.storyboard */; };
A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1B2C30C1111111111111111 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1B2C30C1111111111111111 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
B2C3D4012222222222222222 /* SaveActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C3D4062222222222222222 /* SaveActionViewController.swift */; };
B2C3D4022222222222222222 /* Save Action.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B2C3D4052222222222222222 /* Save Action.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -37,6 +39,13 @@
remoteGlobalIDString = A1B2C3131111111111111111; remoteGlobalIDString = A1B2C3131111111111111111;
remoteInfo = "Share Extension"; remoteInfo = "Share Extension";
}; };
B2C3D4042222222222222222 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = B2C3D40B2222222222222222;
remoteInfo = "Save Action";
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@@ -57,6 +66,7 @@
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */, A1B2C3041111111111111111 /* Share Extension.appex in Embed App Extensions */,
B2C3D4022222222222222222 /* Save Action.appex in Embed App Extensions */,
); );
name = "Embed App Extensions"; name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -91,6 +101,10 @@
A1B2C30E1111111111111111 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; }; A1B2C30E1111111111111111 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
A1B2C30F1111111111111111 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; A1B2C30F1111111111111111 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A1B2C3101111111111111111 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; }; A1B2C3101111111111111111 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
B2C3D4052222222222222222 /* Save Action.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Save Action.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
B2C3D4062222222222222222 /* SaveActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveActionViewController.swift; sourceTree = "<group>"; };
B2C3D4072222222222222222 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B2C3D4082222222222222222 /* Save Action.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Save Action.entitlements"; sourceTree = "<group>"; };
CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CD3F9117486EC23DF50E2D6A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E9B506D809ABF2A50BF6135B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; E9B506D809ABF2A50BF6135B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; F2C7EE505B336533B62B23EB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
@@ -112,6 +126,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
B2C3D4092222222222222222 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
E2BAEB33085D4BFE88A55D45 /* Frameworks */ = { E2BAEB33085D4BFE88A55D45 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -170,6 +191,7 @@
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
A1B2C3121111111111111111 /* Share Extension */, A1B2C3121111111111111111 /* Share Extension */,
B2C3D40A2222222222222222 /* Save Action */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
3D195739A0A78D037BF244F8 /* Pods */, 3D195739A0A78D037BF244F8 /* Pods */,
@@ -182,6 +204,7 @@
children = ( children = (
97C146EE1CF9000F007C117D /* Runner.app */, 97C146EE1CF9000F007C117D /* Runner.app */,
A1B2C30C1111111111111111 /* Share Extension.appex */, A1B2C30C1111111111111111 /* Share Extension.appex */,
B2C3D4052222222222222222 /* Save Action.appex */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
); );
name = Products; name = Products;
@@ -215,6 +238,16 @@
path = "Share Extension"; path = "Share Extension";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B2C3D40A2222222222222222 /* Save Action */ = {
isa = PBXGroup;
children = (
B2C3D4082222222222222222 /* Save Action.entitlements */,
B2C3D4062222222222222222 /* SaveActionViewController.swift */,
B2C3D4072222222222222222 /* Info.plist */,
);
path = "Save Action";
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -255,6 +288,7 @@
); );
dependencies = ( dependencies = (
A1B2C3171111111111111111 /* PBXTargetDependency */, A1B2C3171111111111111111 /* PBXTargetDependency */,
B2C3D40E2222222222222222 /* PBXTargetDependency */,
); );
name = Runner; name = Runner;
productName = Runner; productName = Runner;
@@ -278,6 +312,23 @@
productReference = A1B2C30C1111111111111111 /* Share Extension.appex */; productReference = A1B2C30C1111111111111111 /* Share Extension.appex */;
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
B2C3D40B2222222222222222 /* Save Action */ = {
isa = PBXNativeTarget;
buildConfigurationList = B2C3D4122222222222222222 /* Build configuration list for PBXNativeTarget "Save Action" */;
buildPhases = (
B2C3D40D2222222222222222 /* Sources */,
B2C3D4092222222222222222 /* Frameworks */,
B2C3D40C2222222222222222 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "Save Action";
productName = "Save Action";
productReference = B2C3D4052222222222222222 /* Save Action.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -299,6 +350,9 @@
A1B2C3131111111111111111 = { A1B2C3131111111111111111 = {
CreatedOnToolsVersion = 15.0.1; CreatedOnToolsVersion = 15.0.1;
}; };
B2C3D40B2222222222222222 = {
CreatedOnToolsVersion = 15.0.1;
};
}; };
}; };
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@@ -316,6 +370,7 @@
targets = ( targets = (
97C146ED1CF9000F007C117D /* Runner */, 97C146ED1CF9000F007C117D /* Runner */,
A1B2C3131111111111111111 /* Share Extension */, A1B2C3131111111111111111 /* Share Extension */,
B2C3D40B2222222222222222 /* Save Action */,
331C8080294A63A400263BE5 /* RunnerTests */, 331C8080294A63A400263BE5 /* RunnerTests */,
); );
}; };
@@ -348,6 +403,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
B2C3D40C2222222222222222 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
@@ -472,6 +534,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
B2C3D40D2222222222222222 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B2C3D4012222222222222222 /* SaveActionViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
@@ -485,6 +555,11 @@
target = A1B2C3131111111111111111 /* Share Extension */; target = A1B2C3131111111111111111 /* Share Extension */;
targetProxy = A1B2C3051111111111111111 /* PBXContainerItemProxy */; targetProxy = A1B2C3051111111111111111 /* PBXContainerItemProxy */;
}; };
B2C3D40E2222222222222222 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B2C3D40B2222222222222222 /* Save Action */;
targetProxy = B2C3D4042222222222222222 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
@@ -520,6 +595,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver; APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver;
APP_ACTION_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).SaveAction";
APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared; APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared;
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
@@ -651,6 +727,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver; APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver;
APP_ACTION_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).SaveAction";
APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared; APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared;
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
@@ -713,6 +790,7 @@
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver; APP_BUNDLE_IDENTIFIER = com.rijadzuzo.relationshipSaver;
APP_ACTION_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).SaveAction";
APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared; APP_GROUP_ID = group.com.rijadzuzo.relationshipSaver.shared;
APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests"; APP_RUNNER_TESTS_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension"; APP_SHARE_EXTENSION_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).ShareExtension";
@@ -895,6 +973,88 @@
}; };
name = Profile; name = Profile;
}; };
B2C3D40F2222222222222222 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "Save Action/Save Action.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
DEVELOPMENT_TEAM = SR3UKQVKP6;
INFOPLIST_FILE = "Save Action/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to Relationship Saver";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ACTION_EXTENSION_BUNDLE_IDENTIFIER)";
PRODUCT_MODULE_NAME = SaveAction;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
B2C3D4102222222222222222 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "Save Action/Save Action.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
DEVELOPMENT_TEAM = SR3UKQVKP6;
INFOPLIST_FILE = "Save Action/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to Relationship Saver";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ACTION_EXTENSION_BUNDLE_IDENTIFIER)";
PRODUCT_MODULE_NAME = SaveAction;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
B2C3D4112222222222222222 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "Save Action/Save Action.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = "$(APP_GROUP_ID)";
DEVELOPMENT_TEAM = SR3UKQVKP6;
INFOPLIST_FILE = "Save Action/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to Relationship Saver";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ACTION_EXTENSION_BUNDLE_IDENTIFIER)";
PRODUCT_MODULE_NAME = SaveAction;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@@ -938,6 +1098,16 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
B2C3D4122222222222222222 /* Build configuration list for PBXNativeTarget "Save Action" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B2C3D40F2222222222222222 /* Debug */,
B2C3D4102222222222222222 /* Release */,
B2C3D4112222222222222222 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
+41
View File
@@ -0,0 +1,41 @@
<?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>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CFBundleDisplayName</key>
<string>Save to Relationship Saver</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>XPC!</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>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.ui-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SaveActionViewController</string>
</dict>
</dict>
</plist>
+10
View File
@@ -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>$(CUSTOM_GROUP_ID)</string>
</array>
</dict>
</plist>
@@ -0,0 +1,194 @@
import MobileCoreServices
import UniformTypeIdentifiers
import UIKit
private let schemePrefix = "ShareMedia"
private let userDefaultsKey = "ShareKey"
private let userDefaultsMessageKey = "ShareMessageKey"
private let appGroupIdKey = "AppGroupId"
private struct SharedMediaFile: Codable {
let path: String
let mimeType: String?
let thumbnail: String?
let duration: Double?
let message: String?
let type: SharedMediaType
}
private enum SharedMediaType: String, Codable {
case text
case url
}
final class SaveActionViewController: UIViewController {
private var hostAppBundleIdentifier = ""
private var appGroupId = ""
private var sharedMedia: [SharedMediaFile] = []
private let group = DispatchGroup()
private var didStart = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !didStart else {
return
}
didStart = true
loadIdentifiers()
loadSharedContent()
}
private func loadIdentifiers() {
let actionExtensionIdentifier = Bundle.main.bundleIdentifier ?? ""
if let lastDotIndex = actionExtensionIdentifier.lastIndex(of: ".") {
hostAppBundleIdentifier = String(actionExtensionIdentifier[..<lastDotIndex])
} else {
hostAppBundleIdentifier = actionExtensionIdentifier
}
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
appGroupId =
Bundle.main.object(forInfoDictionaryKey: appGroupIdKey) as? String
?? defaultAppGroupId
}
private func loadSharedContent() {
guard let inputItems = extensionContext?.inputItems as? [NSExtensionItem] else {
finish()
return
}
for item in inputItems {
appendAttributedText(item.attributedContentText)
for provider in item.attachments ?? [] {
load(provider)
}
}
group.notify(queue: .main) { [weak self] in
self?.finish()
}
}
private func load(_ provider: NSItemProvider) {
if provider.hasItemConformingToTypeIdentifier(urlTypeIdentifier) {
group.enter()
provider.loadItem(forTypeIdentifier: urlTypeIdentifier, options: nil) { [weak self] item, _ in
defer {
self?.group.leave()
}
if let url = item as? URL {
self?.appendLiteral(url.absoluteString, type: .url, mimeType: nil)
} else if let text = item as? String {
self?.appendLiteral(text, type: .url, mimeType: nil)
}
}
return
}
if provider.hasItemConformingToTypeIdentifier(textTypeIdentifier) {
group.enter()
provider.loadItem(forTypeIdentifier: textTypeIdentifier, options: nil) { [weak self] item, _ in
defer {
self?.group.leave()
}
if let text = item as? String {
self?.appendLiteral(text, type: .text, mimeType: "text/plain")
}
}
}
}
private var textTypeIdentifier: String {
if #available(iOS 14.0, *) {
return UTType.text.identifier
}
return kUTTypeText as String
}
private var urlTypeIdentifier: String {
if #available(iOS 14.0, *) {
return UTType.url.identifier
}
return kUTTypeURL as String
}
private func appendAttributedText(_ text: NSAttributedString?) {
guard let value = text?.string else {
return
}
appendLiteral(value, type: .text, mimeType: "text/plain")
}
private func appendLiteral(_ value: String, type: SharedMediaType, mimeType: String?) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
return
}
guard !sharedMedia.contains(where: { $0.type == type && $0.path == trimmed }) else {
return
}
sharedMedia.append(
SharedMediaFile(
path: trimmed,
mimeType: mimeType,
thumbnail: nil,
duration: nil,
message: nil,
type: type
)
)
}
private func finish() {
guard !sharedMedia.isEmpty else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
return
}
saveAndRedirect()
}
private func saveAndRedirect() {
guard let encodedData = try? JSONEncoder().encode(sharedMedia) else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
return
}
let userDefaults = UserDefaults(suiteName: appGroupId)
userDefaults?.set(encodedData, forKey: userDefaultsKey)
userDefaults?.set(nil, forKey: userDefaultsMessageKey)
userDefaults?.synchronize()
redirectToHostApp()
}
private func redirectToHostApp() {
guard let url = URL(string: "\(schemePrefix)-\(hostAppBundleIdentifier):share") else {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
return
}
var responder: UIResponder? = self
let selectorOpenUrl = sel_registerName("openURL:")
if #available(iOS 18.0, *) {
while responder != nil {
if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil)
break
}
responder = responder?.next
}
} else {
while responder != nil {
if responder?.responds(to: selectorOpenUrl) == true {
_ = responder?.perform(selectorOpenUrl, with: url)
break
}
responder = responder?.next
}
}
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
@@ -118,6 +118,13 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
} }
} }
if (file.type == SharedMediaType.url) {
final String url = file.path.trim();
if (url.isNotEmpty) {
return url;
}
}
final String? mimeType = file.mimeType?.toLowerCase(); final String? mimeType = file.mimeType?.toLowerCase();
if (mimeType != null && mimeType.startsWith('text/')) { if (mimeType != null && mimeType.startsWith('text/')) {
final String text = file.path.trim(); final String text = file.path.trim();