208 lines
8.3 KiB
Swift
208 lines
8.3 KiB
Swift
//
|
|
// ShareViewController.swift
|
|
// TimeSafariShareExtension
|
|
//
|
|
// Created by Aardimus on 11/24/25.
|
|
//
|
|
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
|
|
class ShareViewController: UIViewController {
|
|
|
|
private let appGroupIdentifier = "group.app.timesafari.share"
|
|
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
|
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
|
private let sharedImageFileName = "shared-image"
|
|
|
|
/// Get the App Group container URL for storing shared files
|
|
private var appGroupContainerURL: URL? {
|
|
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
// Set a minimal background (transparent or loading indicator)
|
|
view.backgroundColor = .systemBackground
|
|
|
|
// Process image immediately without showing UI
|
|
processAndOpenApp()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
processSharedImage(from: inputItems) { [weak self] success in
|
|
guard let self = self, let context = self.extensionContext else {
|
|
return
|
|
}
|
|
|
|
if success {
|
|
// 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 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 {
|
|
guard let attachments = item.attachments else {
|
|
continue
|
|
}
|
|
|
|
for attachment in attachments {
|
|
// Skip non-image attachments
|
|
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
|
|
continue
|
|
}
|
|
|
|
// Try to load raw data first to preserve original format
|
|
// This preserves the original image format without conversion
|
|
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
|
guard let self = self else {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
if error != nil {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
// Handle different image data types
|
|
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
|
|
var imageData: Data?
|
|
var fileName: String = "shared-image"
|
|
|
|
if let url = data as? URL {
|
|
// Most common case: Image provided as file URL - read raw data to preserve format
|
|
let accessing = url.startAccessingSecurityScopedResource()
|
|
defer {
|
|
if accessing {
|
|
url.stopAccessingSecurityScopedResource()
|
|
}
|
|
}
|
|
|
|
// Read raw data directly to preserve original format
|
|
imageData = try? Data(contentsOf: url)
|
|
fileName = url.lastPathComponent
|
|
|
|
// Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
|
|
if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
|
|
imageData = image.pngData()
|
|
fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
|
|
}
|
|
} else if let data = data as? Data {
|
|
// Less common: Image provided as raw Data - use directly to preserve format
|
|
imageData = data
|
|
fileName = attachment.suggestedName ?? "shared-image"
|
|
}
|
|
|
|
guard let finalImageData = imageData else {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
// Store image as file in App Group container
|
|
if self.storeImageData(finalImageData, fileName: fileName) {
|
|
completion(true)
|
|
} else {
|
|
completion(false)
|
|
}
|
|
}
|
|
return // Process only the first image
|
|
}
|
|
}
|
|
|
|
// No image found
|
|
completion(false)
|
|
}
|
|
|
|
/// Helper to get filename with a new extension, preserving base name
|
|
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
|
|
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty {
|
|
return "\(nameWithoutExt).\(newExtension)"
|
|
}
|
|
return "shared-image.\(newExtension)"
|
|
}
|
|
|
|
/// Store image data as a file in the App Group container
|
|
/// All images are stored as files regardless of size for consistency and simplicity
|
|
/// Returns true if successful, false otherwise
|
|
private func storeImageData(_ imageData: Data, fileName: String) -> Bool {
|
|
guard let containerURL = appGroupContainerURL else {
|
|
return false
|
|
}
|
|
|
|
// Create file URL in the container using the actual filename
|
|
// Extract extension from fileName if present, otherwise use sharedImageFileName
|
|
let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName
|
|
let fileURL = containerURL.appendingPathComponent(actualFileName)
|
|
|
|
// Remove old file if it exists
|
|
try? FileManager.default.removeItem(at: fileURL)
|
|
|
|
// Write image data to file
|
|
do {
|
|
try imageData.write(to: fileURL)
|
|
} catch {
|
|
return false
|
|
}
|
|
|
|
// Store file path and filename in UserDefaults (small data, safe to store)
|
|
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
|
return false
|
|
}
|
|
|
|
// Store relative path and filename
|
|
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
|
|
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
|
|
|
|
// Clean up any old base64 data that might exist
|
|
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
|
|
|
userDefaults.synchronize()
|
|
return true
|
|
}
|
|
|
|
private func openMainApp() {
|
|
// Open the main app with minimal URL - app will detect shared data on activation
|
|
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
|
|
}
|
|
|
|
// Fallback: use extension context
|
|
extensionContext?.open(url, completionHandler: nil)
|
|
}
|
|
|
|
}
|