feat(ios): implement native share target for images
Implement iOS Share Extension to enable native image sharing from Photos and other apps directly into TimeSafari. Users can now share images from the iOS share sheet, which will open in SharedPhotoView for use as gifts or profile pictures. iOS Native Implementation: - Add TimeSafariShareExtension target with ShareViewController - Configure App Groups for data sharing between extension and main app - Implement ShareViewController to process shared images and convert to base64 - Store shared image data in App Group UserDefaults - Add ShareImageBridge utility to read shared data from App Group - Update AppDelegate to handle shared-photo deep link and bridge data to JS JavaScript Integration: - Add checkAndStoreNativeSharedImage() in main.capacitor.ts to read shared images from native layer via temporary file bridge - Convert base64 data to data URL format for compatibility with base64ToBlob - Integrate with existing SharedPhotoView component - Add "shared-photo" to deep link validation schema Build System: - Integrate Xcode 26 / CocoaPods compatibility workaround into build-ios.sh - Add run_pod_install_with_workaround() for explicit pod install - Add run_cap_sync_with_workaround() for Capacitor sync (which runs pod install internally) - Automatically detect project format version 70 and apply workaround - Remove standalone pod-install-workaround.sh script Code Cleanup: - Remove verbose debug logs from ShareViewController, AppDelegate, and main.capacitor.ts - Retain essential logger calls for production debugging Documentation: - Add ios-share-extension-setup.md with manual Xcode setup instructions - Add ios-share-extension-git-commit-guide.md for version control best practices - Add ios-share-implementation-status.md tracking implementation progress - Add native-share-target-implementation.md with overall architecture - Add xcode-26-cocoapods-workaround.md documenting the compatibility issue The implementation uses a temporary file bridge (AppDelegate writes to Documents directory, JS reads via Capacitor Filesystem plugin) as a workaround for Capacitor plugin auto-discovery issues. This can be improved in the future by properly registering ShareImagePlugin in Capacitor's plugin registry.
This commit is contained in:
170
ios/App/TimeSafariShareExtension/ShareViewController.swift
Normal file
170
ios/App/TimeSafariShareExtension/ShareViewController.swift
Normal file
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// TimeSafariShareExtension
|
||||
//
|
||||
// Created by Aardimus on 11/24/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Social
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class ShareViewController: SLComposeServiceViewController {
|
||||
|
||||
private let appGroupIdentifier = "group.app.timesafari"
|
||||
private let sharedPhotoBase64Key = "sharedPhotoBase64"
|
||||
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Set placeholder text (required for SLComposeServiceViewController)
|
||||
self.placeholder = "Share image to TimeSafari"
|
||||
|
||||
// Validate content on load
|
||||
self.validateContent()
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if success {
|
||||
// Open the main app via deep link
|
||||
self.openMainApp()
|
||||
}
|
||||
|
||||
// Complete the extension context
|
||||
extensionContext.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
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 via deep link
|
||||
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
|
||||
}
|
||||
|
||||
// Fallback: use extension context
|
||||
extensionContext?.open(url, completionHandler: nil)
|
||||
}
|
||||
|
||||
override func configurationItems() -> [Any]! {
|
||||
// No additional configuration options needed
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user