@choochmeque/tauri-apple-extensions 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>Share</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>$(EXECUTABLE_NAME)</string>
11
+ <key>CFBundleIdentifier</key>
12
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13
+ <key>CFBundleInfoDictionaryVersion</key>
14
+ <string>6.0</string>
15
+ <key>CFBundleName</key>
16
+ <string>$(PRODUCT_NAME)</string>
17
+ <key>CFBundlePackageType</key>
18
+ <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
19
+ <key>CFBundleShortVersionString</key>
20
+ <string>{{VERSION}}</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>{{VERSION}}</string>
23
+ <key>NSExtension</key>
24
+ <dict>
25
+ <key>NSExtensionAttributes</key>
26
+ <dict>
27
+ <key>NSExtensionActivationRule</key>
28
+ <dict>
29
+ <key>NSExtensionActivationSupportsFileWithMaxCount</key>
30
+ <integer>10</integer>
31
+ <key>NSExtensionActivationSupportsImageWithMaxCount</key>
32
+ <integer>10</integer>
33
+ <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
34
+ <integer>10</integer>
35
+ <key>NSExtensionActivationSupportsText</key>
36
+ <true/>
37
+ <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
38
+ <integer>1</integer>
39
+ </dict>
40
+ </dict>
41
+ <key>NSExtensionPointIdentifier</key>
42
+ <string>com.apple.share-services</string>
43
+ <key>NSExtensionPrincipalClass</key>
44
+ <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
45
+ </dict>
46
+ </dict>
47
+ </plist>
@@ -0,0 +1,246 @@
1
+ import UIKit
2
+ import Social
3
+ import MobileCoreServices
4
+ import UniformTypeIdentifiers
5
+
6
+ class ShareViewController: UIViewController {
7
+
8
+ // MARK: - Configuration (will be replaced by setup script)
9
+ private let appGroupIdentifier = "{{APP_GROUP_IDENTIFIER}}"
10
+ private let appURLScheme = "{{APP_URL_SCHEME}}"
11
+
12
+ override func viewDidLoad() {
13
+ super.viewDidLoad()
14
+ view.backgroundColor = .clear
15
+ }
16
+
17
+ override func viewDidAppear(_ animated: Bool) {
18
+ super.viewDidAppear(animated)
19
+ handleSharedContent()
20
+ }
21
+
22
+ private func handleSharedContent() {
23
+ // Check App Groups configuration early
24
+ if FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) == nil {
25
+ showError("App Groups not configured.\n\nPlease enable 'App Groups' capability in Xcode for both the main app and ShareExtension targets, and configure '\(appGroupIdentifier)' in Apple Developer Portal.")
26
+ return
27
+ }
28
+
29
+ guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
30
+ completeRequest()
31
+ return
32
+ }
33
+
34
+ // Use a serial queue to safely collect results
35
+ let resultQueue = DispatchQueue(label: "sharekit.results")
36
+ var sharedContent: [String: Any] = [:]
37
+ var files: [[String: Any]] = []
38
+ var textContent: String? = nil
39
+
40
+ let group = DispatchGroup()
41
+
42
+ for extensionItem in extensionItems {
43
+ guard let attachments = extensionItem.attachments else { continue }
44
+
45
+ for attachment in attachments {
46
+ group.enter()
47
+
48
+ if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
49
+ // Check image FIRST (before URL) because images can also be URLs
50
+ attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] item, error in
51
+ defer { group.leave() }
52
+ if let url = item as? URL {
53
+ if let fileInfo = self?.copyFileToAppGroup(url: url) {
54
+ resultQueue.sync { files.append(fileInfo) }
55
+ }
56
+ } else if let image = item as? UIImage {
57
+ if let fileInfo = self?.saveImageToAppGroup(image: image) {
58
+ resultQueue.sync { files.append(fileInfo) }
59
+ }
60
+ }
61
+ }
62
+ } else if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
63
+ attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] item, error in
64
+ defer { group.leave() }
65
+ guard let url = item as? URL else { return }
66
+
67
+ if url.isFileURL {
68
+ if let fileInfo = self?.copyFileToAppGroup(url: url) {
69
+ resultQueue.sync { files.append(fileInfo) }
70
+ }
71
+ } else {
72
+ resultQueue.sync { textContent = url.absoluteString }
73
+ }
74
+ }
75
+ } else if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
76
+ attachment.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, error in
77
+ defer { group.leave() }
78
+ if let text = item as? String {
79
+ resultQueue.sync { textContent = text }
80
+ }
81
+ }
82
+ } else if attachment.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
83
+ attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { [weak self] item, error in
84
+ defer { group.leave() }
85
+ if let url = item as? URL {
86
+ if let fileInfo = self?.copyFileToAppGroup(url: url) {
87
+ resultQueue.sync { files.append(fileInfo) }
88
+ }
89
+ }
90
+ }
91
+ } else {
92
+ group.leave()
93
+ }
94
+ }
95
+ }
96
+
97
+ group.notify(queue: .main) { [weak self] in
98
+ guard let self = self else { return }
99
+
100
+ if !files.isEmpty {
101
+ sharedContent["type"] = "files"
102
+ sharedContent["files"] = files
103
+ } else if let text = textContent {
104
+ sharedContent["type"] = "text"
105
+ sharedContent["text"] = text
106
+ }
107
+
108
+ if !sharedContent.isEmpty {
109
+ _ = self.saveToAppGroup(content: sharedContent)
110
+ self.openMainAppAndComplete()
111
+ } else {
112
+ self.completeRequest()
113
+ }
114
+ }
115
+ }
116
+
117
+ private func showError(_ message: String) {
118
+ let alert = UIAlertController(
119
+ title: "ShareKit Error",
120
+ message: message,
121
+ preferredStyle: .alert
122
+ )
123
+ alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
124
+ self?.completeRequest()
125
+ })
126
+ present(alert, animated: true)
127
+ }
128
+
129
+ private func copyFileToAppGroup(url: URL) -> [String: Any]? {
130
+ guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
131
+ return nil
132
+ }
133
+
134
+ let sharedFilesDir = containerURL.appendingPathComponent("shared_files", isDirectory: true)
135
+ try? FileManager.default.createDirectory(at: sharedFilesDir, withIntermediateDirectories: true)
136
+
137
+ let fileName = url.lastPathComponent
138
+ let destinationURL = sharedFilesDir.appendingPathComponent(UUID().uuidString + "_" + fileName)
139
+
140
+ do {
141
+ if url.startAccessingSecurityScopedResource() {
142
+ defer { url.stopAccessingSecurityScopedResource() }
143
+ try FileManager.default.copyItem(at: url, to: destinationURL)
144
+ } else {
145
+ try FileManager.default.copyItem(at: url, to: destinationURL)
146
+ }
147
+
148
+ var fileInfo: [String: Any] = [
149
+ "path": destinationURL.path,
150
+ "name": fileName
151
+ ]
152
+
153
+ if let mimeType = getMimeType(for: url) {
154
+ fileInfo["mimeType"] = mimeType
155
+ }
156
+
157
+ if let attributes = try? FileManager.default.attributesOfItem(atPath: destinationURL.path),
158
+ let size = attributes[.size] as? Int64 {
159
+ fileInfo["size"] = size
160
+ }
161
+
162
+ return fileInfo
163
+ } catch {
164
+ print("ShareKit: Failed to copy file: \(error)")
165
+ return nil
166
+ }
167
+ }
168
+
169
+ private func saveImageToAppGroup(image: UIImage) -> [String: Any]? {
170
+ guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
171
+ return nil
172
+ }
173
+
174
+ let sharedFilesDir = containerURL.appendingPathComponent("shared_files", isDirectory: true)
175
+ try? FileManager.default.createDirectory(at: sharedFilesDir, withIntermediateDirectories: true)
176
+
177
+ let fileName = UUID().uuidString + ".png"
178
+ let destinationURL = sharedFilesDir.appendingPathComponent(fileName)
179
+
180
+ guard let data = image.pngData() else { return nil }
181
+
182
+ do {
183
+ try data.write(to: destinationURL)
184
+
185
+ return [
186
+ "path": destinationURL.path,
187
+ "name": fileName,
188
+ "mimeType": "image/png",
189
+ "size": data.count
190
+ ]
191
+ } catch {
192
+ print("ShareKit: Failed to save image: \(error)")
193
+ return nil
194
+ }
195
+ }
196
+
197
+ private func getMimeType(for url: URL) -> String? {
198
+ if let uti = UTType(filenameExtension: url.pathExtension) {
199
+ return uti.preferredMIMEType
200
+ }
201
+ return nil
202
+ }
203
+
204
+ private func saveToAppGroup(content: [String: Any]) -> Bool {
205
+ guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
206
+ showError("App Groups not configured.\n\nPlease enable 'App Groups' capability in Xcode for both the main app and ShareExtension targets, and configure '\(appGroupIdentifier)' in Apple Developer Portal.")
207
+ return false
208
+ }
209
+
210
+ do {
211
+ let data = try JSONSerialization.data(withJSONObject: content)
212
+ userDefaults.set(data, forKey: "pendingSharedContent")
213
+ userDefaults.synchronize()
214
+ return true
215
+ } catch {
216
+ showError("Failed to save shared content: \(error.localizedDescription)")
217
+ return false
218
+ }
219
+ }
220
+
221
+ private func openMainAppAndComplete() {
222
+ guard let url = URL(string: "\(appURLScheme)://sharekit-content") else {
223
+ completeRequest()
224
+ return
225
+ }
226
+
227
+ var responder: UIResponder? = self
228
+ while responder != nil {
229
+ if let application = responder as? UIApplication {
230
+ if #available(iOS 18.0, *) {
231
+ application.open(url, options: [:], completionHandler: nil)
232
+ } else {
233
+ _ = application.perform(NSSelectorFromString("openURL:"), with: url)
234
+ }
235
+ break
236
+ }
237
+ responder = responder?.next
238
+ }
239
+
240
+ completeRequest()
241
+ }
242
+
243
+ private func completeRequest() {
244
+ extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
245
+ }
246
+ }