Finished ShareSheet, Basic App, and restructure
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.abunchofknowitalls.GIFCollector</string>
|
||||
<string>group.gifcollector</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,30 +1,354 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// GIFCollector ShareExtension
|
||||
//
|
||||
// Created by Joshua Higgins on 6/3/25.
|
||||
// GIFCollectorShare
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import Social
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class ShareViewController: SLComposeServiceViewController {
|
||||
|
||||
override func isContentValid() -> Bool {
|
||||
// Do validation of contentText and/or NSExtensionContext attachments here
|
||||
return true
|
||||
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.gifcollector"
|
||||
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")
|
||||
}
|
||||
|
||||
override func didSelectPost() {
|
||||
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
|
||||
|
||||
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
|
||||
addDebugMessage("No extension item found")
|
||||
showError(message: "No shared content found")
|
||||
return
|
||||
}
|
||||
|
||||
override func configurationItems() -> [Any]! {
|
||||
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user