feat(ios): improve share extension UX and fix image reload issues

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.
This commit is contained in:
Jose Olarte III
2025-11-25 18:22:43 +08:00
parent ae49c0e907
commit eff4126043
6 changed files with 565 additions and 75 deletions

View File

@@ -6,10 +6,9 @@
//
import UIKit
import Social
import UniformTypeIdentifiers
class ShareViewController: SLComposeServiceViewController {
class ShareViewController: UIViewController {
private let appGroupIdentifier = "group.app.timesafari"
private let sharedPhotoBase64Key = "sharedPhotoBase64"
@@ -18,64 +17,46 @@ class ShareViewController: SLComposeServiceViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Set placeholder text (required for SLComposeServiceViewController)
self.placeholder = "Share image to TimeSafari"
// Set a minimal background (transparent or loading indicator)
view.backgroundColor = .systemBackground
// Validate content on load
self.validateContent()
// Process image immediately without showing UI
processAndOpenApp()
}
override func isContentValid() -> Bool {
// Validate that we have image attachments
guard let extensionContext = extensionContext else {
return false
}
guard let inputItems = extensionContext.inputItems as? [NSExtensionItem] else {
return false
}
for item in inputItems {
if let attachments = item.attachments {
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
return true
}
}
}
}
return false
}
override func didSelectPost() {
// Extract and process the shared image
guard let extensionContext = extensionContext else {
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
}
guard let inputItems = extensionContext.inputItems as? [NSExtensionItem] else {
extensionContext.completeRequest(returningItems: [], completionHandler: nil)
return
}
// Process the first image found
processSharedImage(from: inputItems) { [weak self] success in
guard let self = self else {
extensionContext.completeRequest(returningItems: [], completionHandler: nil)
guard let self = self, let context = self.extensionContext else {
return
}
if success {
// Open the main app via deep link
// 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 the extension context
extensionContext.completeRequest(returningItems: [], completionHandler: nil)
// 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 {
@@ -144,8 +125,8 @@ class ShareViewController: SLComposeServiceViewController {
}
private func openMainApp() {
// Open the main app via deep link
guard let url = URL(string: "timesafari://shared-photo") else {
// Open the main app with minimal URL - app will detect shared data on activation
guard let url = URL(string: "timesafari://") else {
return
}
@@ -162,9 +143,4 @@ class ShareViewController: SLComposeServiceViewController {
extensionContext?.open(url, completionHandler: nil)
}
override func configurationItems() -> [Any]! {
// No additional configuration options needed
return []
}
}