feat(ios): add diagnostic logging to Share Extension share flow

Log start/end, shareId, attachment counts, UTType, byte counts, filenames,
and every early return across viewDidLoad through completeRequest to trace
how far a share progresses when debugging failures.
This commit is contained in:
Jose Olarte III
2026-06-24 19:28:09 +08:00
parent 07463246f0
commit f0ca49b5dc

View File

@@ -9,139 +9,192 @@ import UIKit
import UniformTypeIdentifiers import UniformTypeIdentifiers
class ShareViewController: UIViewController { class ShareViewController: UIViewController {
private let appGroupIdentifier = "group.app.timesafari.share" private let appGroupIdentifier = "group.app.timesafari.share"
private let sharedPhotoFileNameKey = "sharedPhotoFileName" private let sharedPhotoFileNameKey = "sharedPhotoFileName"
private let sharedPhotoFilePathKey = "sharedPhotoFilePath" private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private let sharedPhotoShareIdKey = "sharedPhotoShareId" private let sharedPhotoShareIdKey = "sharedPhotoShareId"
private let sharedImageFileName = "shared-image" private let sharedImageFileName = "shared-image"
/// Get the App Group container URL for storing shared files /// Get the App Group container URL for storing shared files
private var appGroupContainerURL: URL? { private var appGroupContainerURL: URL? {
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
} }
override func viewDidLoad() { override func viewDidLoad() {
print("[ShareTarget] viewDidLoad started")
super.viewDidLoad() super.viewDidLoad()
// Set a minimal background (transparent or loading indicator) // Set a minimal background (transparent or loading indicator)
view.backgroundColor = .systemBackground view.backgroundColor = .systemBackground
// Process image immediately without showing UI // Process image immediately without showing UI
processAndOpenApp() processAndOpenApp()
print("[ShareTarget] viewDidLoad completed")
} }
private func processAndOpenApp() { private func processAndOpenApp() {
print("[ShareTarget] processAndOpenApp started")
// extensionContext is automatically available on UIViewController when used as extension principal class // extensionContext is automatically available on UIViewController when used as extension principal class
guard let context = extensionContext, guard let context = extensionContext,
let inputItems = context.inputItems as? [NSExtensionItem] else { let inputItems = context.inputItems as? [NSExtensionItem] else {
print("[ShareTarget] processAndOpenApp failed: missing extensionContext or inputItems")
print("[ShareTarget] completeRequest starting")
extensionContext?.completeRequest(returningItems: [], completionHandler: nil) extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
print("[ShareTarget] completeRequest completed")
print("[ShareTarget] processAndOpenApp completed")
return 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 processSharedImage(from: inputItems) { [weak self] success in
guard let self = self, let context = self.extensionContext else { guard let self = self, let context = self.extensionContext else {
print("[ShareTarget] processAndOpenApp failed: self or extensionContext unavailable in completion")
print("[ShareTarget] processAndOpenApp completed")
return return
} }
if success { if success {
// Set flag that shared photo is ready // Set flag that shared photo is ready
self.setSharedPhotoReadyFlag() self.setSharedPhotoReadyFlag()
// Open the main app (using minimal URL - app will detect shared data on activation) // Open the main app (using minimal URL - app will detect shared data on activation)
self.openMainApp() self.openMainApp()
} else {
print("[ShareTarget] processAndOpenApp failed: processSharedImage returned false")
} }
// Complete immediately - no UI shown // Complete immediately - no UI shown
print("[ShareTarget] completeRequest starting")
context.completeRequest(returningItems: [], completionHandler: nil) context.completeRequest(returningItems: [], completionHandler: nil)
print("[ShareTarget] completeRequest completed")
print("[ShareTarget] processAndOpenApp completed")
} }
} }
private func setSharedPhotoReadyFlag() { private func setSharedPhotoReadyFlag() {
print("[ShareTarget] setSharedPhotoReadyFlag started")
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
print("[ShareTarget] setSharedPhotoReadyFlag failed: UserDefaults unavailable for app group")
print("[ShareTarget] setSharedPhotoReadyFlag completed")
return return
} }
userDefaults.set(true, forKey: "sharedPhotoReady") userDefaults.set(true, forKey: "sharedPhotoReady")
userDefaults.synchronize() userDefaults.synchronize()
print("[ShareTarget] setSharedPhotoReadyFlag success")
print("[ShareTarget] setSharedPhotoReadyFlag completed")
} }
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) { 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 // Find the first image attachment
for item in items { for item in items {
guard let attachments = item.attachments else { guard let attachments = item.attachments else {
print("[ShareTarget] processSharedImage skipping item with no attachments")
continue continue
} }
for attachment in attachments { for attachment in attachments {
// Skip non-image attachments // Skip non-image attachments
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else { guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
print("[ShareTarget] processSharedImage skipping non-image attachment")
continue continue
} }
let shareId = UUID().uuidString let shareId = UUID().uuidString
print("[ShareTarget] processSharedImage found image attachment shareId=\(shareId) UTType=\(UTType.image.identifier)")
print("[ShareTarget] share received shareId=\(shareId)") print("[ShareTarget] share received shareId=\(shareId)")
// Try to load raw data first to preserve original format // Try to load raw data first to preserve original format
// This preserves the original image format without conversion // This preserves the original image format without conversion
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
guard let self = self else { guard let self = self else {
completion(false) print("[ShareTarget] processSharedImage failed: self unavailable in loadItem callback shareId=\(shareId)")
return print("[ShareTarget] processSharedImage completed shareId=\(shareId) success=false")
} 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
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 // No image found
print("[ShareTarget] processSharedImage failed: no image attachment found attachmentCount=\(attachmentCount)")
print("[ShareTarget] processSharedImage completed success=false")
completion(false) completion(false)
} }
/// Helper to get filename with a new extension, preserving base name /// Helper to get filename with a new extension, preserving base name
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String { private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty { 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 /// All images are stored as files regardless of size for consistency and simplicity
/// Returns true if successful, false otherwise /// Returns true if successful, false otherwise
private func storeImageData(_ imageData: Data, fileName: String, shareId: String) -> Bool { 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 { 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 return false
} }
@@ -186,12 +243,16 @@ class ShareViewController: UIViewController {
do { do {
try imageData.write(to: fileURL) try imageData.write(to: fileURL)
} catch { } catch {
print("[ShareTarget] storeImageData failed: file write error shareId=\(shareId) error=\(error.localizedDescription)")
print("[ShareTarget] storeImageData completed shareId=\(shareId) success=false")
return false return false
} }
print("[ShareTarget] file stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)") print("[ShareTarget] file stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)")
// Store file path and filename in UserDefaults (small data, safe to store) // Store file path and filename in UserDefaults (small data, safe to store)
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { 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 return false
} }
@@ -205,26 +266,34 @@ class ShareViewController: UIViewController {
userDefaults.synchronize() userDefaults.synchronize()
print("[ShareTarget] metadata stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)") 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 return true
} }
private func openMainApp() { private func openMainApp() {
print("[ShareTarget] openMainApp starting")
// Open the main app with minimal URL - app will detect shared data on activation // Open the main app with minimal URL - app will detect shared data on activation
guard let url = URL(string: "timesafari://") else { guard let url = URL(string: "timesafari://") else {
print("[ShareTarget] openMainApp failed: could not create timesafari:// URL")
print("[ShareTarget] openMainApp completed")
return return
} }
var responder: UIResponder? = self var responder: UIResponder? = self
while responder != nil { while responder != nil {
if let application = responder as? UIApplication { if let application = responder as? UIApplication {
application.open(url, options: [:], completionHandler: nil) application.open(url, options: [:], completionHandler: nil)
print("[ShareTarget] openMainApp completed via UIApplication")
return return
} }
responder = responder?.next responder = responder?.next
} }
// Fallback: use extension context // Fallback: use extension context
extensionContext?.open(url, completionHandler: nil) extensionContext?.open(url, completionHandler: nil)
print("[ShareTarget] openMainApp completed via extensionContext fallback")
} }
} }