Add iOS save action extension
This commit is contained in:
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user