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.
8.7 KiB
8.7 KiB
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
- Interstitial UI: Users see
SLComposeServiceViewControllerwith a "Post" button before the app opens - 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
SLComposeServiceViewControllerwhich 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:
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
// 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)
// 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)
// 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:
// 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:
- Use Custom UIViewController (Improvement 1) - Eliminates interstitial UI
- Use App Lifecycle Detection (Improvement 2, Option C) - Direct data flow
Combined Implementation:
// 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:
// 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