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:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user