import UIKit import UniformTypeIdentifiers import AVFoundation // Datenmodell muss byte-für-byte zu dem passen, was // SwiftReceiveSharingIntentPlugin auf der Host-App-Seite decodiert. private enum SharedMediaType: String, Codable { case image, video, text, file, url } private struct SharedMediaFile: Codable { let path: String let mimeType: String? let thumbnail: String? let duration: Double? let message: String? let type: SharedMediaType } final class ShareViewController: UIViewController { // Schlüssel sind die, die das Plugin liest. private let userDefaultsKey = "ShareKey" private let userDefaultsMessageKey = "ShareMessageKey" private let urlSchemePrefix = "ShareMedia" private var appGroupId = "" private var hostBundleId = "" override func viewDidLoad() { super.viewDidLoad() resolveIds() setupPlaceholderUI() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) processAttachments() } private func resolveIds() { let extBundleId = Bundle.main.bundleIdentifier ?? "" if let lastDot = extBundleId.lastIndex(of: ".") { hostBundleId = String(extBundleId[.. Void) { let order: [(String, SharedMediaType)] = [ (UTType.image.identifier, .image), (UTType.movie.identifier, .video), (UTType.text.identifier, .text), (UTType.fileURL.identifier, .file), (UTType.url.identifier, .url), (UTType.data.identifier, .file), ] for (typeId, kind) in order { if provider.hasItemConformingToTypeIdentifier(typeId) { provider.loadItem(forTypeIdentifier: typeId) { [weak self] data, error in guard let self else { completion(nil); return } if error != nil { completion(nil); return } completion(self.toSharedFile(data: data, kind: kind)) } return } } completion(nil) } private func toSharedFile(data: Any?, kind: SharedMediaType) -> SharedMediaFile? { switch kind { case .text: guard let s = data as? String else { return nil } return SharedMediaFile(path: s, mimeType: "text/plain", thumbnail: nil, duration: nil, message: nil, type: .text) case .url: guard let u = data as? URL else { return nil } return SharedMediaFile(path: u.absoluteString, mimeType: nil, thumbnail: nil, duration: nil, message: nil, type: .url) case .image: if let u = data as? URL, let dst = copyIntoAppGroup(src: u) { return SharedMediaFile(path: pathString(for: dst), mimeType: mime(for: u), thumbnail: nil, duration: nil, message: nil, type: .image) } if let img = data as? UIImage, let dst = writePng(image: img) { return SharedMediaFile(path: pathString(for: dst), mimeType: "image/png", thumbnail: nil, duration: nil, message: nil, type: .image) } return nil case .video: guard let u = data as? URL, let dst = copyIntoAppGroup(src: u) else { return nil } return SharedMediaFile(path: pathString(for: dst), mimeType: mime(for: u), thumbnail: nil, duration: videoDurationMs(url: u), message: nil, type: .video) case .file: guard let u = data as? URL, let dst = copyIntoAppGroup(src: u) else { return nil } return SharedMediaFile(path: pathString(for: dst), mimeType: mime(for: u), thumbnail: nil, duration: nil, message: nil, type: .file) } } private func copyIntoAppGroup(src: URL) -> URL? { guard let container = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else { return nil } let name = src.lastPathComponent.isEmpty ? UUID().uuidString : src.lastPathComponent let dst = container.appendingPathComponent(name) do { if FileManager.default.fileExists(atPath: dst.path) { try FileManager.default.removeItem(at: dst) } try FileManager.default.copyItem(at: src, to: dst) return dst } catch { return nil } } private func writePng(image: UIImage) -> URL? { guard let container = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: appGroupId), let data = image.pngData() else { return nil } let dst = container.appendingPathComponent("\(UUID().uuidString).png") return (try? data.write(to: dst)) != nil ? dst : nil } // Das Plugin macht `path.replacingOccurrences(of: "file://", with: "")`, // also liefern wir absoluteString mit Prozent-Decoding — selbes Format wie // im Original-RSIShareViewController. private func pathString(for url: URL) -> String { url.absoluteString.removingPercentEncoding ?? url.absoluteString } private func mime(for url: URL) -> String? { UTType(filenameExtension: url.pathExtension)?.preferredMIMEType } private func videoDurationMs(url: URL) -> Double { (CMTimeGetSeconds(AVURLAsset(url: url).duration) * 1000).rounded() } private func saveAndRedirect(items: [SharedMediaFile]) { guard !items.isEmpty else { finish() return } let defaults = UserDefaults(suiteName: appGroupId) guard let encoded = try? JSONEncoder().encode(items) else { finish() return } defaults?.set(encoded, forKey: userDefaultsKey) defaults?.removeObject(forKey: userDefaultsMessageKey) let urlStr = "\(urlSchemePrefix)-\(hostBundleId):share" guard let url = URL(string: urlStr) else { finish() return } // Apple-DTS says Share Extensions are not officially allowed to open // URLs. Both `extensionContext.open` and the responder-chain trick // succeed in some iOS versions and fail silently in others, so we fire // both in parallel and let whichever Apple is honouring this release // win the race. extensionContext?.open(url, completionHandler: nil) var responder: UIResponder? = self while responder != nil { if let app = responder as? UIApplication { app.open(url, options: [:], completionHandler: nil) break } responder = responder?.next } // Brief window so the open request reaches the system before we tear // the extension down. DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in self?.finish() } } private func finish() { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } }