Improve iOS share extension implementation to skip interstitial UI and fix issues with subsequent image shares not updating the view. iOS Share Extension Improvements: - Replace SLComposeServiceViewController with custom UIViewController to eliminate interstitial "Post" button UI - Use minimal URL (timesafari://) instead of deep link for app launch - Implement app lifecycle detection via Capacitor appStateChange listener instead of relying solely on deep links Deep Link and Navigation Fixes: - Remove "shared-photo" from deep link schemas (no longer needed) - Add empty path URL handling for share extension launches - Implement processing lock to prevent duplicate image processing - Add retry mechanism (300ms delay) to handle race conditions with AppDelegate writing temp files - Use router.replace() when already on /shared-photo route to force refresh - Clear old images from temp DB before storing new ones - Delete temp file immediately after reading to prevent stale data SharedPhotoView Component: - Add route watcher (@Watch) to reload image when fileName query parameter changes - Extract image loading logic into reusable loadSharedImage() method - Improve error handling to clear image state on failures This fixes the issue where sharing a second image while already on SharedPhotoView would display the previous image instead of the new one.
147 lines
5.5 KiB
Swift
147 lines
5.5 KiB
Swift
//
|
|
// ShareViewController.swift
|
|
// TimeSafariShareExtension
|
|
//
|
|
// Created by Aardimus on 11/24/25.
|
|
//
|
|
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
|
|
class ShareViewController: UIViewController {
|
|
|
|
private let appGroupIdentifier = "group.app.timesafari"
|
|
private let sharedPhotoBase64Key = "sharedPhotoBase64"
|
|
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
// Set a minimal background (transparent or loading indicator)
|
|
view.backgroundColor = .systemBackground
|
|
|
|
// Process image immediately without showing UI
|
|
processAndOpenApp()
|
|
}
|
|
|
|
private func processAndOpenApp() {
|
|
// extensionContext is automatically available on UIViewController when used as extension principal class
|
|
guard let context = extensionContext,
|
|
let inputItems = context.inputItems as? [NSExtensionItem] else {
|
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
return
|
|
}
|
|
|
|
processSharedImage(from: inputItems) { [weak self] success in
|
|
guard let self = self, let context = self.extensionContext else {
|
|
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()
|
|
}
|
|
|
|
// Complete immediately - no UI shown
|
|
context.completeRequest(returningItems: [], completionHandler: nil)
|
|
}
|
|
}
|
|
|
|
private func setSharedPhotoReadyFlag() {
|
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
|
return
|
|
}
|
|
userDefaults.set(true, forKey: "sharedPhotoReady")
|
|
userDefaults.synchronize()
|
|
}
|
|
|
|
private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
|
|
// Find the first image attachment
|
|
for item in items {
|
|
guard let attachments = item.attachments else {
|
|
continue
|
|
}
|
|
|
|
for attachment in attachments {
|
|
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
|
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
|
guard let self = self else {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
if let error = error {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
// Handle different image data types
|
|
var imageData: Data?
|
|
var fileName: String = "shared-image.jpg"
|
|
|
|
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"
|
|
} else if let data = data as? Data {
|
|
// Image provided as raw Data
|
|
imageData = data
|
|
fileName = "shared-image.jpg"
|
|
}
|
|
|
|
guard let finalImageData = imageData else {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
// Convert to base64
|
|
let base64String = finalImageData.base64EncodedString()
|
|
|
|
// Store in App Group UserDefaults
|
|
guard let userDefaults = UserDefaults(suiteName: self.appGroupIdentifier) 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
|
|
}
|
|
}
|
|
}
|
|
|
|
// No image found
|
|
completion(false)
|
|
}
|
|
|
|
private func openMainApp() {
|
|
// Open the main app with minimal URL - app will detect shared data on activation
|
|
guard let url = URL(string: "timesafari://") else {
|
|
return
|
|
}
|
|
|
|
var responder: UIResponder? = self
|
|
while responder != nil {
|
|
if let application = responder as? UIApplication {
|
|
application.open(url, options: [:], completionHandler: nil)
|
|
return
|
|
}
|
|
responder = responder?.next
|
|
}
|
|
|
|
// Fallback: use extension context
|
|
extensionContext?.open(url, completionHandler: nil)
|
|
}
|
|
|
|
}
|