// // ShareViewController.swift // TimeSafariShareExtension // // Created by Aardimus on 11/24/25. // import UIKit import UniformTypeIdentifiers class ShareViewController: UIViewController { private let appGroupIdentifier = "group.app.timesafari.share" private let sharedPhotoFileNameKey = "sharedPhotoFileName" private let sharedPhotoFilePathKey = "sharedPhotoFilePath" private let sharedPhotoShareIdKey = "sharedPhotoShareId" private let sharedImageFileName = "shared-image" /// Get the App Group container URL for storing shared files private var appGroupContainerURL: URL? { return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) } override func viewDidLoad() { super.viewDidLoad() // Set a minimal background (transparent or loading indicator) view.backgroundColor = .systemBackground // Process image immediately without showing UI processAndOpenApp() } private func processAndOpenApp() { // extensionContext is automatically available on UIViewController when used as extension principal class guard let context = extensionContext, let inputItems = context.inputItems as? [NSExtensionItem] else { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) return } processSharedImage(from: inputItems) { [weak self] success in guard let self = self, let context = self.extensionContext else { return } if success { // Set flag that shared photo is ready self.setSharedPhotoReadyFlag() // Open the main app (using minimal URL - app will detect shared data on activation) self.openMainApp() } // Complete immediately - no UI shown context.completeRequest(returningItems: [], completionHandler: nil) } } private func setSharedPhotoReadyFlag() { guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return } userDefaults.set(true, forKey: "sharedPhotoReady") userDefaults.synchronize() } private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) { // Find the first image attachment for item in items { guard let attachments = item.attachments else { continue } for attachment in attachments { // Skip non-image attachments guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else { continue } let shareId = UUID().uuidString print("[ShareTarget] share received shareId=\(shareId)") // Try to load raw data first to preserve original format // This preserves the original image format without conversion attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in guard let self = self else { completion(false) return } if error != nil { completion(false) return } // Handle different image data types // loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage var imageData: Data? var fileName: String = "shared-image" if let url = data as? URL { // Most common case: Image provided as file URL - read raw data to preserve format let accessing = url.startAccessingSecurityScopedResource() defer { if accessing { url.stopAccessingSecurityScopedResource() } } // Read raw data directly to preserve original format imageData = try? Data(contentsOf: url) fileName = url.lastPathComponent // Fallback: if raw data read fails, try UIImage and convert to PNG (lossless) if imageData == nil, let image = UIImage(contentsOfFile: url.path) { imageData = image.pngData() fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png") } } else if let data = data as? Data { // Less common: Image provided as raw Data - use directly to preserve format imageData = data fileName = attachment.suggestedName ?? "shared-image" } guard let finalImageData = imageData else { completion(false) return } // Store image as file in App Group container if self.storeImageData(finalImageData, fileName: fileName, shareId: shareId) { completion(true) } else { completion(false) } } return // Process only the first image } } // No image found completion(false) } /// Helper to get filename with a new extension, preserving base name private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String { if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty { return "\(nameWithoutExt).\(newExtension)" } return "shared-image.\(newExtension)" } /// Extract file extension from original filename, defaulting to jpg when absent private func fileExtension(from fileName: String) -> String { let ext = (fileName as NSString).pathExtension return ext.isEmpty ? "jpg" : ext.lowercased() } /// Build unique on-disk filename: . private func storedFileName(shareId: String, originalFileName: String) -> String { return "\(shareId).\(fileExtension(from: originalFileName))" } /// Store image data as a file in the App Group container /// All images are stored as files regardless of size for consistency and simplicity /// Returns true if successful, false otherwise private func storeImageData(_ imageData: Data, fileName: String, shareId: String) -> Bool { guard let containerURL = appGroupContainerURL else { return false } let originalFileName = fileName.isEmpty ? "\(sharedImageFileName).jpg" : fileName let storedFileName = storedFileName(shareId: shareId, originalFileName: originalFileName) let fileURL = containerURL.appendingPathComponent(storedFileName) // Remove previously pending share file (metadata tracks one share at a time) if let userDefaults = UserDefaults(suiteName: appGroupIdentifier), let previousPath = userDefaults.string(forKey: sharedPhotoFilePathKey) { let previousURL = containerURL.appendingPathComponent(previousPath) if previousURL != fileURL { try? FileManager.default.removeItem(at: previousURL) } } // Write image data to file do { try imageData.write(to: fileURL) } catch { return false } print("[ShareTarget] file stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)") // Store file path and filename in UserDefaults (small data, safe to store) guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return false } // sharedPhotoFilePath = on-disk name; sharedPhotoFileName = original display name userDefaults.set(storedFileName, forKey: sharedPhotoFilePathKey) userDefaults.set(originalFileName, forKey: sharedPhotoFileNameKey) userDefaults.set(shareId, forKey: sharedPhotoShareIdKey) // Clean up any old base64 data that might exist userDefaults.removeObject(forKey: "sharedPhotoBase64") userDefaults.synchronize() print("[ShareTarget] metadata stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)") return true } private func openMainApp() { // Open the main app with minimal URL - app will detect shared data on activation guard let url = URL(string: "timesafari://") else { return } var responder: UIResponder? = self while responder != nil { if let application = responder as? UIApplication { application.open(url, options: [:], completionHandler: nil) return } responder = responder?.next } // Fallback: use extension context extensionContext?.open(url, completionHandler: nil) } }