diff --git a/ios/App/TimeSafariShareExtension/ShareViewController.swift b/ios/App/TimeSafariShareExtension/ShareViewController.swift index eb667b48..f37f3adf 100644 --- a/ios/App/TimeSafariShareExtension/ShareViewController.swift +++ b/ios/App/TimeSafariShareExtension/ShareViewController.swift @@ -9,139 +9,192 @@ 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() { + 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 { - 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) - } + 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 } - return // Process only the first image + + 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 { @@ -165,7 +218,11 @@ class ShareViewController: UIViewController { /// 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 } @@ -186,12 +243,16 @@ class ShareViewController: UIViewController { 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 } @@ -205,26 +266,34 @@ class ShareViewController: UIViewController { 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") } }