Files
crowd-funder-for-time-pwa/doc/ios-share-extension-improvements.md

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

  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:

import UIKit
import UniformTypeIdentifiers

class ShareViewController: UIViewController {
    
    private let appGroupIdentifier = "group.app.timesafari.share"
    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

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.share"
    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)

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:

// 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