// // ShareViewController.swift // GIFCollectorShare // import MobileCoreServices import Social import UIKit import UniformTypeIdentifiers class ShareViewController: SLComposeServiceViewController { private var receivedURL: URL? private var gifData: Data? private var isProcessing = false private var originalURL: String = "" private var debugMessages: [String] = [] // Use the same constants as in the main app private let appGroupID = "group."+Bundle.main.bundleIdentifier.unsafelyUnwrapped.replacing(".Share", with:"") private let pendingGIFsKey = "pendingGIFs" override func viewDidLoad() { super.viewDidLoad() title = "Save to GIF Collector" placeholder = "Add a note (optional)" // Add debug button in development #if DEBUG let debugButton = UIBarButtonItem( title: "Debug", style: .plain, target: self, action: #selector(showDebugInfo)) navigationItem.leftBarButtonItem = debugButton #endif // Start processing the shared item processSharedItem() addDebugMessage("ShareViewController loaded") } override func isContentValid() -> Bool { // No validation needed for the text field, we just want the GIF return true } override func didSelectPost() { // Show loading isProcessing = true navigationController?.navigationBar.isUserInteractionEnabled = false // If we have a URL but no data, we need to download it if let receivedURL = receivedURL, gifData == nil { downloadGIF(from: receivedURL) { [weak self] data, error in guard let self = self else { return } if let data = data { self.gifData = data self.saveGIF() } else { self.showError( message: "Could not download GIF: \(error?.localizedDescription ?? "Unknown error")") } } } else if let gifData = gifData { // We already have the data, save it directly saveGIF() } else { // No valid GIF was found showError(message: "No valid GIF found in the shared content") } } override func didSelectCancel() { // User canceled, close the extension completeRequest() } // MARK: - Private Methods private func processSharedItem() { addDebugMessage("Starting to process shared item") // Log extension context info if let context = extensionContext { addDebugMessage("Extension context exists") addDebugMessage("Input items count: \(context.inputItems.count)") } else { addDebugMessage("Extension context is nil") } guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { addDebugMessage("No extension item found") showError(message: "No shared content found") return } guard let attachments = extensionItem.attachments else { addDebugMessage("No attachments found") showError(message: "No attachments found") return } addDebugMessage("Found \(attachments.count) attachments") // Process each attachment for attachment in attachments { // Log attachment types let typeIdentifiers = attachment.registeredTypeIdentifiers addDebugMessage("Attachment types: \(typeIdentifiers.joined(separator: ", "))") // Check for URLs first (for web links to GIFs) if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) { addDebugMessage("Processing URL type attachment") attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] item, error in guard let self = self else { return } if let url = item as? URL, url.absoluteString.lowercased().hasSuffix(".gif") { DispatchQueue.main.async { self.receivedURL = url self.originalURL = url.absoluteString self.updateUI() } } else if let url = item as? URL { // It's a URL but not directly to a GIF, might be a webpage containing a GIF DispatchQueue.main.async { self.originalURL = url.absoluteString self.receivedURL = url self.updateUI() self.addDebugMessage("URL processed: \(url.absoluteString)") } } } continue } // Check for GIF files if attachment.hasItemConformingToTypeIdentifier("com.compuserve.gif") || attachment.hasItemConformingToTypeIdentifier("public.gif") { addDebugMessage("Processing GIF type attachment") attachment.loadItem(forTypeIdentifier: "com.compuserve.gif", options: nil) { [weak self] item, error in guard let self = self else { return } if let url = item as? URL { DispatchQueue.main.async { self.receivedURL = url self.originalURL = url.absoluteString self.loadGIFData(from: url) self.updateUI() } } else if let data = item as? Data { DispatchQueue.main.async { self.gifData = data self.updateUI() } } } continue } // Check for images that might be GIFs if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) { attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] item, error in guard let self = self else { return } if let url = item as? URL, url.pathExtension.lowercased() == "gif" { DispatchQueue.main.async { self.receivedURL = url self.originalURL = url.absoluteString self.loadGIFData(from: url) self.updateUI() } } else if let data = item as? Data, self.isGIFData(data) { DispatchQueue.main.async { self.gifData = data self.updateUI() } } } } } } private func updateUI() { // Enable the Post button if we have a URL or data navigationItem.rightBarButtonItem?.isEnabled = (receivedURL != nil || gifData != nil) // Update content area if let url = receivedURL { let displayText = "GIF from: \(url.host ?? "URL")" if contentText.isEmpty { } } } private func loadGIFData(from url: URL) { do { let data = try Data(contentsOf: url) if isGIFData(data) { gifData = data } } catch { print("Error loading GIF data: \(error)") } } private func isGIFData(_ data: Data) -> Bool { // Simple check for GIF file signature: "GIF87a" or "GIF89a" guard data.count > 6 else { return false } let header = data.prefix(6) let gif87a = Data([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) // "GIF87a" let gif89a = Data([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]) // "GIF89a" return header == gif87a || header == gif89a } private func downloadGIF(from url: URL, completion: @escaping (Data?, Error?) -> Void) { URLSession.shared.dataTask(with: url) { data, response, error in DispatchQueue.main.async { if let data = data, self.isGIFData(data) { completion(data, nil) } else { completion( nil, error ?? NSError( domain: "GIFShare", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid GIF data"])) } } }.resume() } private func saveGIF() { guard let gifData = gifData else { showError(message: "No GIF data to save") return } addDebugMessage("Starting to save GIF, size: \(gifData.count) bytes") // Save the GIF using the shared App Group container let userDefaults = UserDefaults(suiteName: appGroupID) let fileManager = FileManager.default // IMPORTANT: Use the same path conventions as GIFFileManager guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else { showError(message: "Could not access shared container") return } // Use the same folder name as in GIFFileManager let gifsFolder = containerURL.appendingPathComponent("SharedGIFs", isDirectory: true) // Create GIFs directory if needed if !fileManager.fileExists(atPath: gifsFolder.path) { do { try fileManager.createDirectory(at: gifsFolder, withIntermediateDirectories: true) addDebugMessage("Created shared GIFs directory") } catch { showError(message: "Could not create GIFs directory: \(error.localizedDescription)") return } } // Create a unique filename using the same pattern as GIFFileManager let urlHash = originalURL.hashValue let timestamp = Int(Date().timeIntervalSince1970) let filename = "gif_\(urlHash)_\(timestamp).gif" let fileURL = gifsFolder.appendingPathComponent(filename) // Save the GIF file do { try gifData.write(to: fileURL) addDebugMessage("GIF saved to: \(fileURL.path)") // Create a UUID that will be used consistently across the app let gifId = UUID() // Store GIF entry in UserDefaults to notify main app let gifInfo: [String: Any] = [ "localFilePath": fileURL.path, "originalURL": originalURL, "createdAt": Date().timeIntervalSince1970, "id": gifId.uuidString, ] // Add to pending GIFs list (using the same key as in GIFStorageService) var pendingGIFs = userDefaults?.array(forKey: pendingGIFsKey) as? [[String: Any]] ?? [] pendingGIFs.append(gifInfo) userDefaults?.set(pendingGIFs, forKey: pendingGIFsKey) // Force synchronize to ensure changes are visible immediately userDefaults?.synchronize() addDebugMessage("GIF info saved to UserDefaults") // Success completeRequest() } catch { showError(message: "Error saving GIF: \(error.localizedDescription)") } } private func addDebugMessage(_ message: String) { let timestamp = DateFormatter.localizedString( from: Date(), dateStyle: .none, timeStyle: .medium) debugMessages.append("[\(timestamp)] \(message)") print("GIFCollector Debug: \(message)") } @objc private func showDebugInfo() { let debugText = debugMessages.joined(separator: "\n\n") let alert = UIAlertController( title: "Debug Information", message: debugText, preferredStyle: .alert) alert.addAction( UIAlertAction(title: "Copy", style: .default) { _ in UIPasteboard.general.string = debugText }) alert.addAction(UIAlertAction(title: "OK", style: .cancel)) present(alert, animated: true) } private func showError(message: String) { addDebugMessage("ERROR: \(message)") let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) alert.addAction( UIAlertAction(title: "OK", style: .default) { [weak self] _ in self?.completeRequest() }) // Add debug info button #if DEBUG alert.addAction( UIAlertAction(title: "Debug Info", style: .default) { [weak self] _ in self?.showDebugInfo() }) #endif present(alert, animated: true) } private func completeRequest() { addDebugMessage("Completing extension request") extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } }