// // 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 shareExtensionLastStartKey = "shareExtensionLastStart" 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() { if let userDefaults = UserDefaults(suiteName: appGroupIdentifier) { let timestamp = ISO8601DateFormatter().string(from: Date()) userDefaults.set(timestamp, forKey: shareExtensionLastStartKey) userDefaults.synchronize() print("[ShareTarget] shareExtensionLastStart=\(timestamp)") } print("[ShareTarget] viewDidLoad started") super.viewDidLoad() // Set a minimal background (transparent or loading indicator) view.backgroundColor = .systemBackground // Process image immediately without showing UI processAndOpenApp() print("[ShareTarget] viewDidLoad completed") } private func processAndOpenApp() { print("[ShareTarget] processAndOpenApp started") // extensionContext is automatically available on UIViewController when used as extension principal class guard let context = extensionContext, let inputItems = context.inputItems as? [NSExtensionItem] else { print("[ShareTarget] processAndOpenApp failed: missing extensionContext or inputItems") print("[ShareTarget] completeRequest starting") extensionContext?.completeRequest(returningItems: [], completionHandler: nil) print("[ShareTarget] completeRequest completed") print("[ShareTarget] processAndOpenApp completed") return } let attachmentCount = inputItems.reduce(0) { count, item in count + (item.attachments?.count ?? 0) } print("[ShareTarget] processAndOpenApp inputItems=\(inputItems.count) attachmentCount=\(attachmentCount)") processSharedImage(from: inputItems) { [weak self] success in guard let self = self, let context = self.extensionContext else { print("[ShareTarget] processAndOpenApp failed: self or extensionContext unavailable in completion") print("[ShareTarget] processAndOpenApp completed") 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() } else { print("[ShareTarget] processAndOpenApp failed: processSharedImage returned false") } // Complete immediately - no UI shown print("[ShareTarget] completeRequest starting") context.completeRequest(returningItems: [], completionHandler: nil) print("[ShareTarget] completeRequest completed") print("[ShareTarget] processAndOpenApp completed") } } private func setSharedPhotoReadyFlag() { print("[ShareTarget] setSharedPhotoReadyFlag started") guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { print("[ShareTarget] setSharedPhotoReadyFlag failed: UserDefaults unavailable for app group") print("[ShareTarget] setSharedPhotoReadyFlag completed") return } userDefaults.set(true, forKey: "sharedPhotoReady") userDefaults.synchronize() print("[ShareTarget] setSharedPhotoReadyFlag success") print("[ShareTarget] setSharedPhotoReadyFlag completed") } private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) { let attachmentCount = items.reduce(0) { count, item in count + (item.attachments?.count ?? 0) } print("[ShareTarget] processSharedImage started attachmentCount=\(attachmentCount)") // Find the first image attachment for item in items { guard let attachments = item.attachments else { print("[ShareTarget] processSharedImage skipping item with no attachments") continue } for attachment in attachments { // Skip non-image attachments guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else { print("[ShareTarget] processSharedImage skipping non-image attachment") continue } let shareId = UUID().uuidString print("[ShareTarget] processSharedImage found image attachment shareId=\(shareId) UTType=\(UTType.image.identifier)") 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 { print("[ShareTarget] processSharedImage failed: self unavailable in loadItem callback shareId=\(shareId)") print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false") completion(false) return } if let error = error { print("[ShareTarget] processSharedImage failed: loadItem error shareId=\(shareId) error=\(error.localizedDescription)") print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false") 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 { print("[ShareTarget] processSharedImage loadItem returned URL shareId=\(shareId) url=\(url.lastPathComponent)") // 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) { print("[ShareTarget] processSharedImage using UIImage PNG fallback shareId=\(shareId)") imageData = image.pngData() fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png") } else if imageData == nil { print("[ShareTarget] processSharedImage failed: could not read image data from URL shareId=\(shareId)") } } else if let data = data as? Data { print("[ShareTarget] processSharedImage loadItem returned Data shareId=\(shareId)") // Less common: Image provided as raw Data - use directly to preserve format imageData = data fileName = attachment.suggestedName ?? "shared-image" } else { print("[ShareTarget] processSharedImage failed: loadItem returned unexpected type shareId=\(shareId) type=\(String(describing: type(of: data)))") } guard let finalImageData = imageData else { print("[ShareTarget] processSharedImage failed: no image data shareId=\(shareId)") print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false") completion(false) return } print("[ShareTarget] image loaded bytes=\(finalImageData.count) filename=\(fileName) shareId=\(shareId)") // Store image as file in App Group container if self.storeImageData(finalImageData, fileName: fileName, shareId: shareId) { print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=true") completion(true) } else { print("[ShareTarget] processSharedImage failed: storeImageData returned false shareId=\(shareId)") print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false") completion(false) } } return // Process only the first image } } // No image found print("[ShareTarget] processSharedImage failed: no image attachment found attachmentCount=\(attachmentCount)") print("[ShareTarget] processSharedImage completed success=false") 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 { print("[ShareTarget] storeImageData started shareId=\(shareId) bytes=\(imageData.count) filename=\(fileName)") guard let containerURL = appGroupContainerURL else { print("[ShareTarget] storeImageData failed: app group container unavailable shareId=\(shareId)") print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false") 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 { print("[ShareTarget] storeImageData failed: file write error shareId=\(shareId) error=\(error.localizedDescription)") print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false") 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 { print("[ShareTarget] storeImageData failed: UserDefaults unavailable shareId=\(shareId)") print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false") 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)") print("[ShareTarget] storeImageData success shareId=\(shareId)") print("[ShareTarget] storeImageData completed shareId=\(shareId) success=true") return true } private func openMainApp() { print("[ShareTarget] openMainApp starting") // Open the main app with minimal URL - app will detect shared data on activation guard let url = URL(string: "timesafari://") else { print("[ShareTarget] openMainApp failed: could not create timesafari:// URL") print("[ShareTarget] openMainApp completed") return } var responder: UIResponder? = self while responder != nil { if let application = responder as? UIApplication { application.open(url, options: [:], completionHandler: nil) print("[ShareTarget] openMainApp completed via UIApplication") return } responder = responder?.next } // Fallback: use extension context extensionContext?.open(url, completionHandler: nil) print("[ShareTarget] openMainApp completed via extensionContext fallback") } }