// // 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 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 } // 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) { 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)" } /// 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) -> Bool { guard let containerURL = appGroupContainerURL else { return false } // Create file URL in the container using the actual filename // Extract extension from fileName if present, otherwise use sharedImageFileName let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName let fileURL = containerURL.appendingPathComponent(actualFileName) // Remove old file if it exists try? FileManager.default.removeItem(at: fileURL) // Write image data to file do { try imageData.write(to: fileURL) } catch { return false } // Store file path and filename in UserDefaults (small data, safe to store) guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return false } // Store relative path and filename userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey) userDefaults.set(fileName, forKey: sharedPhotoFileNameKey) // Clean up any old base64 data that might exist userDefaults.removeObject(forKey: "sharedPhotoBase64") userDefaults.synchronize() 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) } }