Refine backendless share intake
This commit is contained in:
@@ -1,3 +1,315 @@
|
||||
import receive_sharing_intent
|
||||
import MobileCoreServices
|
||||
import Social
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
class ShareViewController: RSIShareViewController {}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user