# iOS Share Extension Improvements **Date:** 2025-11-24 **Purpose:** Explore alternatives to improve user experience by eliminating interstitial UI and simplifying app launch mechanism ## Current Implementation Issues 1. **Interstitial UI**: Users see `SLComposeServiceViewController` with a "Post" button before the app opens 2. **Deep Link Dependency**: App relies on deep link (`timesafari://shared-photo`) to detect shared images, even though data is already in App Group ## Improvement 1: Skip Interstitial UI ### Current Approach - Uses `SLComposeServiceViewController` which shows a UI with "Post" button - User must tap "Post" to proceed ### Alternative: Custom UIViewController (Headless Processing) Replace `SLComposeServiceViewController` with a custom `UIViewController` that: - Processes the image immediately in `viewDidLoad` - Shows no UI (or minimal loading indicator) - Opens the app automatically **Implementation:** ```swift 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() // Process image immediately without showing UI processAndOpenApp() } private func processAndOpenApp() { guard let extensionContext = extensionContext, let inputItems = extensionContext.inputItems as? [NSExtensionItem] else { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) return } processSharedImage(from: inputItems) { [weak self] success in guard let self = self else { self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) return } if success { self.openMainApp() } // Complete immediately - no UI shown self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } } private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) { // ... (same implementation as current) } private func openMainApp() { guard let url = URL(string: "timesafari://shared-photo") 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 } extensionContext?.open(url, completionHandler: nil) } } ``` **Info.plist Changes:** - Already configured correctly with `NSExtensionPrincipalClass` - No storyboard needed (already removed) **Benefits:** - ✅ No interstitial UI - app opens immediately - ✅ Faster user experience - ✅ More seamless integration **Considerations:** - ⚠️ User has less control (can't cancel easily) - ⚠️ No visual feedback during processing (could add minimal loading indicator) - ⚠️ Apple guidelines: Extensions should provide value even if they don't open the app ## Improvement 2: Direct App Launch Without Deep Link ### Current Approach - Share Extension stores data in App Group UserDefaults - Share Extension opens app via deep link (`timesafari://shared-photo`) - App receives deep link → checks App Group → processes image ### Alternative: App Lifecycle Detection Instead of using deep links, the app can check for shared data when it becomes active: **Option A: Check on App Activation** ```swift // In AppDelegate.swift func applicationDidBecomeActive(_ application: UIApplication) { // Check for shared image from Share Extension if let sharedData = getSharedImageData() { // Store in temp file for JS to read writeSharedImageToTempFile(sharedData) // Navigate to shared-photo route directly // This would need to be handled in JS layer } } ``` **Option B: Use Notification (More Reliable)** ```swift // In ShareViewController.swift (after storing data) private func openMainApp() { // Store a flag that image is ready guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return } userDefaults.set(true, forKey: "sharedPhotoReady") userDefaults.synchronize() // Open app (can use any URL scheme or even just launch the app) 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 } } // In AppDelegate.swift func applicationDidBecomeActive(_ application: UIApplication) { let appGroupIdentifier = "group.app.timesafari" guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return } // Check if shared photo is ready if userDefaults.bool(forKey: "sharedPhotoReady") { userDefaults.removeObject(forKey: "sharedPhotoReady") userDefaults.synchronize() // Process shared image if let sharedData = getSharedImageData() { writeSharedImageToTempFile(sharedData) // Trigger JS to check for shared image // This could be done via Capacitor App plugin or custom event } } } ``` **Option C: Check on App Launch (Most Direct)** ```swift // In AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Check for shared image immediately on launch checkForSharedImageOnLaunch() return true } func applicationDidBecomeActive(_ application: UIApplication) { // Also check when app becomes active (in case it was already running) checkForSharedImageOnLaunch() } private func checkForSharedImageOnLaunch() { if let sharedData = getSharedImageData() { writeSharedImageToTempFile(sharedData) // Post a notification or use Capacitor to notify JS NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil) } } ``` **JavaScript Integration:** ```typescript // In main.capacitor.ts import { App } from '@capacitor/app'; // Listen for app becoming active App.addListener('appStateChange', async ({ isActive }) => { if (isActive) { // Check for shared image when app becomes active await checkAndStoreNativeSharedImage(); } }); // Also check on initial load if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios') { checkAndStoreNativeSharedImage().then(result => { if (result.success) { // Navigate to shared-photo route router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : '')); } }); } ``` **Benefits:** - ✅ No deep link routing needed - ✅ More direct data flow - ✅ App can detect shared content even if it was already running - ✅ Simpler URL scheme handling **Considerations:** - ⚠️ Need to ensure app checks on both launch and activation - ⚠️ May need to handle race conditions (app launching vs. share extension writing) - ⚠️ Still need some way to open the app (minimal URL scheme still required) ## Recommended Approach **Best of Both Worlds:** 1. **Use Custom UIViewController** (Improvement 1) - Eliminates interstitial UI 2. **Use App Lifecycle Detection** (Improvement 2, Option C) - Direct data flow **Combined Implementation:** ```swift // ShareViewController.swift - Custom UIViewController class ShareViewController: UIViewController { // Process immediately in viewDidLoad // Store data in App Group // Open app with minimal URL (just "timesafari://") } // AppDelegate.swift func applicationDidBecomeActive(_ application: UIApplication) { // Check for shared image // If found, write to temp file and let JS handle navigation } ``` **JavaScript:** ```typescript // Check on app activation App.addListener('appStateChange', async ({ isActive }) => { if (isActive) { const result = await checkAndStoreNativeSharedImage(); if (result.success) { router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : '')); } } }); ``` This approach: - ✅ No interstitial UI - ✅ No deep link routing complexity - ✅ Direct data flow via App Group - ✅ Works whether app is running or launching fresh