Fix iOS share extension large image handling
- Fix large images not loading from share sheet (exceeded UserDefaults 4MB limit) - Store all shared images as files in App Group container instead of base64 in UserDefaults - Fix image refresh when sharing new image while already on SharedPhotoView - Add route query watcher to detect _refresh parameter changes - Remove debug logging and redundant UIImage check The previous implementation tried to store base64-encoded images in UserDefaults, which failed for large images (HEIF images were often 6MB+ when base64 encoded). Now all images are stored as files in the App Group container, with only the file path stored in UserDefaults. This supports images of any size and provides consistent behavior regardless of format. Also fixes an issue where sharing a new image while already viewing SharedPhotoView wouldn't refresh the displayed image.
This commit is contained in:
@@ -11,8 +11,14 @@ import UniformTypeIdentifiers
|
||||
class ShareViewController: UIViewController {
|
||||
|
||||
private let appGroupIdentifier = "group.app.timesafari"
|
||||
private let sharedPhotoBase64Key = "sharedPhotoBase64"
|
||||
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
||||
private let sharedImageFileName = "shared-image.jpg"
|
||||
|
||||
/// 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()
|
||||
@@ -66,13 +72,56 @@ class ShareViewController: UIViewController {
|
||||
|
||||
for attachment in attachments {
|
||||
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||
// First, try to load as UIImage directly (best for HEIF and other formats)
|
||||
// This uses the system's built-in image decoding which handles HEIF automatically
|
||||
if attachment.canLoadObject(ofClass: UIImage.self) {
|
||||
attachment.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
|
||||
guard let self = self else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
guard let image = image as? UIImage else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to JPEG for web compatibility
|
||||
guard let imageData = image.jpegData(compressionQuality: 0.9) else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Get filename from attachment if available
|
||||
var fileName = "shared-image.jpg"
|
||||
if let identifier = attachment.suggestedName {
|
||||
let nameWithoutExt = (identifier as NSString).deletingPathExtension
|
||||
fileName = "\(nameWithoutExt).jpg"
|
||||
}
|
||||
|
||||
// Store image as file in App Group container
|
||||
if self.storeImageData(imageData, fileName: fileName) {
|
||||
completion(true)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
return // Process only the first image
|
||||
}
|
||||
|
||||
// Fallback: load as data/URL (for formats that don't support UIImage loading)
|
||||
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
||||
guard let self = self else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
if let error = error {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
@@ -83,15 +132,39 @@ class ShareViewController: UIViewController {
|
||||
|
||||
if let url = data as? URL {
|
||||
// Image provided as file URL
|
||||
imageData = try? Data(contentsOf: url)
|
||||
fileName = url.lastPathComponent
|
||||
} else if let image = data as? UIImage {
|
||||
// Image provided as UIImage
|
||||
imageData = image.jpegData(compressionQuality: 0.9)
|
||||
fileName = "shared-image.jpg"
|
||||
// For security-scoped resources, we need to access them properly
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
// Try loading as UIImage first (handles HEIF, RAW, etc.)
|
||||
if let image = UIImage(contentsOfFile: url.path) {
|
||||
// Convert to JPEG for web compatibility
|
||||
imageData = image.jpegData(compressionQuality: 0.9)
|
||||
// Preserve original filename but change extension to .jpg
|
||||
let originalName = url.lastPathComponent
|
||||
if let nameWithoutExt = originalName.components(separatedBy: ".").first {
|
||||
fileName = "\(nameWithoutExt).jpg"
|
||||
} else {
|
||||
fileName = "shared-image.jpg"
|
||||
}
|
||||
} else {
|
||||
// Fallback: try reading raw data (for already web-compatible formats)
|
||||
imageData = try? Data(contentsOf: url)
|
||||
fileName = url.lastPathComponent
|
||||
}
|
||||
} else if let data = data as? Data {
|
||||
// Image provided as raw Data
|
||||
imageData = data
|
||||
// Try to load as UIImage first to handle HEIF/RAW formats
|
||||
if let image = UIImage(data: data) {
|
||||
imageData = image.jpegData(compressionQuality: 0.9)
|
||||
} else {
|
||||
// Fallback: use raw data (assumes already web-compatible)
|
||||
imageData = data
|
||||
}
|
||||
fileName = "shared-image.jpg"
|
||||
}
|
||||
|
||||
@@ -100,20 +173,12 @@ class ShareViewController: UIViewController {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to base64
|
||||
let base64String = finalImageData.base64EncodedString()
|
||||
|
||||
// Store in App Group UserDefaults
|
||||
guard let userDefaults = UserDefaults(suiteName: self.appGroupIdentifier) else {
|
||||
// Store image as file in App Group container
|
||||
if self.storeImageData(finalImageData, fileName: fileName) {
|
||||
completion(true)
|
||||
} else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
userDefaults.set(base64String, forKey: self.sharedPhotoBase64Key)
|
||||
userDefaults.set(fileName, forKey: self.sharedPhotoFileNameKey)
|
||||
userDefaults.synchronize()
|
||||
|
||||
completion(true)
|
||||
}
|
||||
return // Process only the first image
|
||||
}
|
||||
@@ -124,6 +189,43 @@ class ShareViewController: UIViewController {
|
||||
completion(false)
|
||||
}
|
||||
|
||||
/// 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
|
||||
let fileURL = containerURL.appendingPathComponent(sharedImageFileName)
|
||||
|
||||
// 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(sharedImageFileName, 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 {
|
||||
|
||||
Reference in New Issue
Block a user