316 lines
8.8 KiB
Swift
316 lines
8.8 KiB
Swift
import MobileCoreServices
|
|
import Social
|
|
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, CaseIterable {
|
|
case image
|
|
case video
|
|
case text
|
|
case file
|
|
case url
|
|
|
|
var typeIdentifier: String {
|
|
if #available(iOS 14.0, *) {
|
|
switch self {
|
|
case .image:
|
|
return UTType.image.identifier
|
|
case .video:
|
|
return UTType.movie.identifier
|
|
case .text:
|
|
return UTType.text.identifier
|
|
case .file:
|
|
return UTType.fileURL.identifier
|
|
case .url:
|
|
return UTType.url.identifier
|
|
}
|
|
}
|
|
|
|
switch self {
|
|
case .image:
|
|
return kUTTypeImage as String
|
|
case .video:
|
|
return kUTTypeMovie as String
|
|
case .text:
|
|
return kUTTypeText as String
|
|
case .file:
|
|
return kUTTypeFileURL as String
|
|
case .url:
|
|
return kUTTypeURL as String
|
|
}
|
|
}
|
|
}
|
|
|
|
final class ShareViewController: SLComposeServiceViewController {
|
|
private var hostAppBundleIdentifier = ""
|
|
private var appGroupId = ""
|
|
private var sharedMedia: [SharedMediaFile] = []
|
|
private let group = DispatchGroup()
|
|
private var didFinishLoadingAttachments = false
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
loadIdentifiers()
|
|
}
|
|
|
|
override func isContentValid() -> Bool {
|
|
true
|
|
}
|
|
|
|
override func didSelectPost() {
|
|
appendContentTextIfNeeded()
|
|
saveAndRedirect(message: contentText)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
guard !didFinishLoadingAttachments else {
|
|
return
|
|
}
|
|
didFinishLoadingAttachments = true
|
|
loadAttachments()
|
|
}
|
|
|
|
override func configurationItems() -> [Any]! {
|
|
[]
|
|
}
|
|
|
|
private func loadIdentifiers() {
|
|
let shareExtensionIdentifier = Bundle.main.bundleIdentifier ?? ""
|
|
if let lastDotIndex = shareExtensionIdentifier.lastIndex(of: ".") {
|
|
hostAppBundleIdentifier = String(shareExtensionIdentifier[..<lastDotIndex])
|
|
} else {
|
|
hostAppBundleIdentifier = shareExtensionIdentifier
|
|
}
|
|
|
|
let defaultAppGroupId = "group.\(hostAppBundleIdentifier)"
|
|
appGroupId =
|
|
Bundle.main.object(forInfoDictionaryKey: appGroupIdKey) as? String
|
|
?? defaultAppGroupId
|
|
}
|
|
|
|
private func loadAttachments() {
|
|
guard
|
|
let inputItem = extensionContext?.inputItems.first as? NSExtensionItem,
|
|
let attachments = inputItem.attachments
|
|
else {
|
|
appendContentTextIfNeeded()
|
|
saveAndRedirect(message: contentText)
|
|
return
|
|
}
|
|
|
|
NSLog("RelationshipSaverShare: loading %d attachment(s)", attachments.count)
|
|
for attachment in attachments {
|
|
loadAttachment(attachment)
|
|
}
|
|
|
|
group.notify(queue: .main) { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.appendContentTextIfNeeded()
|
|
if !self.sharedMedia.isEmpty {
|
|
self.saveAndRedirect()
|
|
} else {
|
|
NSLog("RelationshipSaverShare: no shareable media or text found")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadAttachment(_ attachment: NSItemProvider) {
|
|
for type in SharedMediaType.allCases where attachment.hasItemConformingToTypeIdentifier(type.typeIdentifier) {
|
|
group.enter()
|
|
attachment.loadItem(forTypeIdentifier: type.typeIdentifier, options: nil) { [weak self] item, _ in
|
|
defer {
|
|
self?.group.leave()
|
|
}
|
|
self?.appendSharedItem(item, type: type)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
private func appendSharedItem(_ item: NSSecureCoding?, type: SharedMediaType) {
|
|
switch (item, type) {
|
|
case let (text as String, .text):
|
|
appendLiteral(text, type: .text, mimeType: "text/plain")
|
|
case let (url as URL, .url):
|
|
appendLiteral(url.absoluteString, type: .url, mimeType: nil)
|
|
case let (url as URL, _):
|
|
appendFile(url, type: type)
|
|
case let (image as UIImage, .image):
|
|
appendImage(image)
|
|
default:
|
|
NSLog("RelationshipSaverShare: unsupported item for type %@", type.rawValue)
|
|
break
|
|
}
|
|
}
|
|
|
|
private func appendContentTextIfNeeded() {
|
|
let trimmed = contentText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else {
|
|
return
|
|
}
|
|
guard !sharedMedia.contains(where: { $0.type == .text && $0.path == trimmed }) else {
|
|
return
|
|
}
|
|
NSLog("RelationshipSaverShare: using compose contentText fallback length=%d", trimmed.count)
|
|
appendLiteral(trimmed, 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
|
|
}
|
|
|
|
sharedMedia.append(
|
|
SharedMediaFile(
|
|
path: trimmed,
|
|
mimeType: mimeType,
|
|
thumbnail: nil,
|
|
duration: nil,
|
|
message: nil,
|
|
type: type
|
|
)
|
|
)
|
|
}
|
|
|
|
private func appendFile(_ sourceUrl: URL, type: SharedMediaType) {
|
|
guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
|
|
return
|
|
}
|
|
|
|
let fileName = sourceUrl.lastPathComponent.isEmpty ? UUID().uuidString : sourceUrl.lastPathComponent
|
|
let destinationUrl = containerUrl.appendingPathComponent(fileName)
|
|
|
|
do {
|
|
if FileManager.default.fileExists(atPath: destinationUrl.path) {
|
|
try FileManager.default.removeItem(at: destinationUrl)
|
|
}
|
|
try FileManager.default.copyItem(at: sourceUrl, to: destinationUrl)
|
|
sharedMedia.append(
|
|
SharedMediaFile(
|
|
path: destinationUrl.absoluteString.removingPercentEncoding ?? destinationUrl.absoluteString,
|
|
mimeType: sourceUrl.mimeType,
|
|
thumbnail: nil,
|
|
duration: nil,
|
|
message: nil,
|
|
type: type
|
|
)
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
private func appendImage(_ image: UIImage) {
|
|
guard
|
|
let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId),
|
|
let imageData = image.pngData()
|
|
else {
|
|
return
|
|
}
|
|
|
|
let destinationUrl = containerUrl.appendingPathComponent("\(UUID().uuidString).png")
|
|
|
|
do {
|
|
try imageData.write(to: destinationUrl)
|
|
sharedMedia.append(
|
|
SharedMediaFile(
|
|
path: destinationUrl.absoluteString.removingPercentEncoding ?? destinationUrl.absoluteString,
|
|
mimeType: "image/png",
|
|
thumbnail: nil,
|
|
duration: nil,
|
|
message: nil,
|
|
type: .image
|
|
)
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
private func saveAndRedirect(message: String? = nil) {
|
|
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(message, forKey: userDefaultsMessageKey)
|
|
userDefaults?.synchronize()
|
|
NSLog("RelationshipSaverShare: saved %d shared item(s), appGroup=%@", sharedMedia.count, appGroupId)
|
|
redirectToHostApp()
|
|
}
|
|
|
|
private func redirectToHostApp() {
|
|
loadIdentifiers()
|
|
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)
|
|
}
|
|
}
|
|
|
|
private extension URL {
|
|
var mimeType: String {
|
|
if #available(iOS 14.0, *),
|
|
let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType
|
|
{
|
|
return mimeType
|
|
}
|
|
|
|
guard
|
|
let identifier = UTTypeCreatePreferredIdentifierForTag(
|
|
kUTTagClassFilenameExtension,
|
|
pathExtension as NSString,
|
|
nil
|
|
)?.takeRetainedValue(),
|
|
let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue()
|
|
else {
|
|
return "application/octet-stream"
|
|
}
|
|
|
|
return mimeType as String
|
|
}
|
|
}
|