@choochmeque/tauri-apple-extensions 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -8,9 +8,9 @@ Add iOS extensions to Tauri apps with a single command.
8
8
  ## Features
9
9
 
10
10
  - Automatic Xcode project configuration via XcodeGen
11
- - Plugin-based template system
11
+ - Built-in skeleton templates with TODO markers
12
+ - Plugin-based template system for custom implementations
12
13
  - Idempotent - safe to re-run
13
- - Share Extension support (more extension types coming soon)
14
14
 
15
15
  ## Prerequisites
16
16
 
@@ -20,34 +20,57 @@ Add iOS extensions to Tauri apps with a single command.
20
20
  ## Installation
21
21
 
22
22
  ```bash
23
+ # npm
23
24
  npm install -D @choochmeque/tauri-apple-extensions
24
- ```
25
25
 
26
- ## Usage
26
+ # pnpm
27
+ pnpm add -D @choochmeque/tauri-apple-extensions
28
+
29
+ # yarn
30
+ yarn add -D @choochmeque/tauri-apple-extensions
27
31
 
28
- ### With a plugin (recommended)
32
+ # bun
33
+ bun add -D @choochmeque/tauri-apple-extensions
34
+ ```
29
35
 
30
- If you're using a Tauri plugin that provides iOS extension templates:
36
+ ## Usage
31
37
 
32
38
  ```bash
33
- npx @choochmeque/tauri-apple-extensions add share --plugin @choochmeque/tauri-plugin-sharekit-api
39
+ npx @choochmeque/tauri-apple-extensions add share
34
40
  ```
35
41
 
36
- ### With custom templates
42
+ This creates a Share Extension with a minimal skeleton template. Open the generated Swift file and implement your logic where you see `// TODO:` comments.
43
+
44
+ ### Options
37
45
 
38
46
  ```bash
47
+ # Use templates from a plugin (plugin must include tauri-apple-extension config)
48
+ npx @choochmeque/tauri-apple-extensions add share --plugin <plugin-name>
49
+
50
+ # Use custom templates directory
39
51
  npx @choochmeque/tauri-apple-extensions add share --templates ./path/to/templates
40
52
  ```
41
53
 
54
+ > **Note:** When using `--plugin`, the plugin's `package.json` must contain a `tauri-apple-extension` config. See [For Plugin Developers](#for-plugin-developers) below.
55
+
42
56
  ## Supported Extensions
43
57
 
44
- | Type | Status | Description |
45
- |------|--------|-------------|
46
- | `share` | Available | Share Extension for receiving shared content |
58
+ | Type | Description |
59
+ |------|-------------|
60
+ | `share` | Share Extension for receiving shared content |
61
+
62
+ ## Post-Setup Steps
63
+
64
+ After running the tool:
65
+
66
+ 1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
67
+ 2. Select your Apple Developer Team for both targets
68
+ 3. Enable **App Groups** capability for both targets
69
+ 4. Configure App Groups in [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list/applicationGroup)
47
70
 
48
71
  ## For Plugin Developers
49
72
 
50
- To make your plugin compatible with this tool, add the following to your `package.json`:
73
+ To make your plugin compatible, add to your `package.json`:
51
74
 
52
75
  ```json
53
76
  {
@@ -58,10 +81,6 @@ To make your plugin compatible with this tool, add the following to your `packag
58
81
  }
59
82
  ```
60
83
 
61
- Your templates directory should contain:
62
- - Swift source files with `{{VARIABLE}}` placeholders
63
- - `Info.plist` for the extension
64
-
65
84
  ### Template Variables
66
85
 
67
86
  | Variable | Description |
@@ -72,19 +91,10 @@ Your templates directory should contain:
72
91
  | `{{BUNDLE_IDENTIFIER}}` | Extension bundle identifier |
73
92
  | `{{PRODUCT_NAME}}` | App product name |
74
93
 
75
- ## Post-Setup Steps
76
-
77
- After running the tool:
78
-
79
- 1. Open the Xcode project (`src-tauri/gen/apple/*.xcodeproj`)
80
- 2. Select your Apple Developer Team for both targets
81
- 3. Enable required capabilities (e.g., App Groups) for both targets
82
- 4. Configure the capability in Apple Developer Portal
83
-
84
94
  ## Contributing
85
95
 
86
- PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
96
+ PRs welcome! Please open an issue first to discuss what you would like to change.
87
97
 
88
98
  ## License
89
99
 
90
- MIT
100
+ [MIT](LICENSE)
package/dist/cli.js CHANGED
@@ -118,6 +118,25 @@ function addExtensionTarget(projectYml, targetConfig) {
118
118
  }
119
119
  return modified;
120
120
  }
121
+ function addUrlSchemeToTarget(projectYml, targetName, urlScheme, bundleIdentifier) {
122
+ let modified = projectYml;
123
+ // Check if URL scheme already exists
124
+ if (modified.includes(`CFBundleURLSchemes`) && modified.includes(urlScheme)) {
125
+ return modified;
126
+ }
127
+ // Find the target's info section and add URL scheme
128
+ const targetRegex = new RegExp(`(${targetName}:[\\s\\S]*?info:[\\s\\S]*?properties:)([\\s\\S]*?)(\\n \\w|\\n \\w|$)`, "m");
129
+ const match = modified.match(targetRegex);
130
+ if (match) {
131
+ const urlSchemeYaml = `
132
+ CFBundleURLTypes:
133
+ - CFBundleURLName: ${bundleIdentifier}
134
+ CFBundleURLSchemes:
135
+ - ${urlScheme}`;
136
+ modified = modified.replace(targetRegex, `$1$2${urlSchemeYaml}$3`);
137
+ }
138
+ return modified;
139
+ }
121
140
  function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
122
141
  let modified = projectYml;
123
142
  // Remove existing dependency if it exists
@@ -189,41 +208,6 @@ function createExtensionEntitlements(extensionDir, appGroupId) {
189
208
  return entitlementsPath;
190
209
  }
191
210
 
192
- function addUrlSchemeToInfoPlist(appleDir, appInfo) {
193
- const targetName = `${appInfo.productName}_iOS`;
194
- const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
195
- const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
196
- if (!fs.existsSync(infoPlistPath)) {
197
- console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
198
- return;
199
- }
200
- let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
201
- // Check if URL schemes already configured
202
- if (infoPlist.includes("CFBundleURLSchemes")) {
203
- if (!infoPlist.includes(urlScheme)) {
204
- console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
205
- }
206
- return;
207
- }
208
- // Add URL scheme - need to insert before closing </dict></plist>
209
- const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
210
- <array>
211
- <dict>
212
- <key>CFBundleURLSchemes</key>
213
- <array>
214
- <string>${urlScheme}</string>
215
- </array>
216
- <key>CFBundleURLName</key>
217
- <string>${appInfo.identifier}</string>
218
- </dict>
219
- </array>
220
- `;
221
- // Insert before the last </dict>
222
- infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
223
- fs.writeFileSync(infoPlistPath, infoPlist);
224
- console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
225
- }
226
-
227
211
  function runXcodeGen(appleDir) {
228
212
  try {
229
213
  console.log("Running xcodegen to regenerate project...");
@@ -405,6 +389,11 @@ const shareExtension = {
405
389
  modified = addDependencyToTarget(modified, targetName, {
406
390
  target: extensionName,
407
391
  });
392
+ // Add URL scheme to main app for deep linking from extension
393
+ const urlScheme = appInfo.productName
394
+ .toLowerCase()
395
+ .replace(/[^a-z0-9]/g, "");
396
+ modified = addUrlSchemeToTarget(modified, targetName, urlScheme, appInfo.identifier);
408
397
  return modified;
409
398
  },
410
399
  };
@@ -440,8 +429,8 @@ function resolveTemplatesDir(options, extensionType) {
440
429
  }
441
430
  return templatesPath;
442
431
  }
443
- // Option 3: Default templates
444
- const defaultTemplates = path.join(__dirname$1, "../../templates", extensionType);
432
+ // Option 3: Default templates (from bundled dist/cli.js -> ../templates)
433
+ const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
445
434
  if (!fs.existsSync(defaultTemplates)) {
446
435
  throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
447
436
  }
@@ -496,13 +485,11 @@ async function addExtension(type, options) {
496
485
  extension.createFiles(appleDir, appInfo, templatesDir);
497
486
  console.log(`\n2. Updating main app entitlements...`);
498
487
  updateMainAppEntitlements(appleDir, appInfo);
499
- console.log(`\n3. Adding URL scheme to Info.plist...`);
500
- addUrlSchemeToInfoPlist(appleDir, appInfo);
501
- console.log(`\n4. Updating project.yml...`);
488
+ console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
502
489
  projectYml = readProjectYml(appleDir);
503
490
  projectYml = extension.updateProjectYml(projectYml, appInfo);
504
491
  writeProjectYml(appleDir, projectYml);
505
- console.log(`\n5. Regenerating Xcode project...`);
492
+ console.log(`\n4. Regenerating Xcode project...`);
506
493
  runXcodeGen(appleDir);
507
494
  console.log(`\n========================================`);
508
495
  console.log(`${extension.displayName} setup complete!`);
@@ -526,7 +513,7 @@ const program = new Command();
526
513
  program
527
514
  .name("tauri-apple-extensions")
528
515
  .description("Add iOS extensions to Tauri apps")
529
- .version("0.1.0");
516
+ .version("0.1.2");
530
517
  program
531
518
  .command("add <type>")
532
519
  .description("Add an extension (e.g., share)")
@@ -2,4 +2,5 @@ import type { TargetConfig, DependencyConfig } from "../types.js";
2
2
  export declare function readProjectYml(appleDir: string): string;
3
3
  export declare function writeProjectYml(appleDir: string, content: string): void;
4
4
  export declare function addExtensionTarget(projectYml: string, targetConfig: TargetConfig): string;
5
+ export declare function addUrlSchemeToTarget(projectYml: string, targetName: string, urlScheme: string, bundleIdentifier: string): string;
5
6
  export declare function addDependencyToTarget(projectYml: string, mainTargetName: string, dependencyConfig: DependencyConfig): string;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { addExtension } from "./commands/add.js";
2
2
  export { findProjectRoot, findTauriConfig, findAppleProjectDir, getAppInfo, } from "./core/project-discovery.js";
3
- export { readProjectYml, writeProjectYml } from "./core/project-yml.js";
3
+ export { readProjectYml, writeProjectYml, addUrlSchemeToTarget, } from "./core/project-yml.js";
4
4
  export { updateMainAppEntitlements, createExtensionEntitlements, } from "./core/entitlements.js";
5
5
  export { addUrlSchemeToInfoPlist } from "./core/info-plist.js";
6
6
  export { runXcodeGen } from "./core/xcodegen.js";
package/dist/index.js CHANGED
@@ -116,6 +116,25 @@ function addExtensionTarget(projectYml, targetConfig) {
116
116
  }
117
117
  return modified;
118
118
  }
119
+ function addUrlSchemeToTarget(projectYml, targetName, urlScheme, bundleIdentifier) {
120
+ let modified = projectYml;
121
+ // Check if URL scheme already exists
122
+ if (modified.includes(`CFBundleURLSchemes`) && modified.includes(urlScheme)) {
123
+ return modified;
124
+ }
125
+ // Find the target's info section and add URL scheme
126
+ const targetRegex = new RegExp(`(${targetName}:[\\s\\S]*?info:[\\s\\S]*?properties:)([\\s\\S]*?)(\\n \\w|\\n \\w|$)`, "m");
127
+ const match = modified.match(targetRegex);
128
+ if (match) {
129
+ const urlSchemeYaml = `
130
+ CFBundleURLTypes:
131
+ - CFBundleURLName: ${bundleIdentifier}
132
+ CFBundleURLSchemes:
133
+ - ${urlScheme}`;
134
+ modified = modified.replace(targetRegex, `$1$2${urlSchemeYaml}$3`);
135
+ }
136
+ return modified;
137
+ }
119
138
  function addDependencyToTarget(projectYml, mainTargetName, dependencyConfig) {
120
139
  let modified = projectYml;
121
140
  // Remove existing dependency if it exists
@@ -187,41 +206,6 @@ function createExtensionEntitlements(extensionDir, appGroupId) {
187
206
  return entitlementsPath;
188
207
  }
189
208
 
190
- function addUrlSchemeToInfoPlist(appleDir, appInfo) {
191
- const targetName = `${appInfo.productName}_iOS`;
192
- const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
193
- const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
194
- if (!fs.existsSync(infoPlistPath)) {
195
- console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
196
- return;
197
- }
198
- let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
199
- // Check if URL schemes already configured
200
- if (infoPlist.includes("CFBundleURLSchemes")) {
201
- if (!infoPlist.includes(urlScheme)) {
202
- console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
203
- }
204
- return;
205
- }
206
- // Add URL scheme - need to insert before closing </dict></plist>
207
- const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
208
- <array>
209
- <dict>
210
- <key>CFBundleURLSchemes</key>
211
- <array>
212
- <string>${urlScheme}</string>
213
- </array>
214
- <key>CFBundleURLName</key>
215
- <string>${appInfo.identifier}</string>
216
- </dict>
217
- </array>
218
- `;
219
- // Insert before the last </dict>
220
- infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
221
- fs.writeFileSync(infoPlistPath, infoPlist);
222
- console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
223
- }
224
-
225
209
  function runXcodeGen(appleDir) {
226
210
  try {
227
211
  console.log("Running xcodegen to regenerate project...");
@@ -403,6 +387,11 @@ const shareExtension = {
403
387
  modified = addDependencyToTarget(modified, targetName, {
404
388
  target: extensionName,
405
389
  });
390
+ // Add URL scheme to main app for deep linking from extension
391
+ const urlScheme = appInfo.productName
392
+ .toLowerCase()
393
+ .replace(/[^a-z0-9]/g, "");
394
+ modified = addUrlSchemeToTarget(modified, targetName, urlScheme, appInfo.identifier);
406
395
  return modified;
407
396
  },
408
397
  };
@@ -438,8 +427,8 @@ function resolveTemplatesDir(options, extensionType) {
438
427
  }
439
428
  return templatesPath;
440
429
  }
441
- // Option 3: Default templates
442
- const defaultTemplates = path.join(__dirname$1, "../../templates", extensionType);
430
+ // Option 3: Default templates (from bundled dist/cli.js -> ../templates)
431
+ const defaultTemplates = path.join(__dirname$1, "../templates", extensionType);
443
432
  if (!fs.existsSync(defaultTemplates)) {
444
433
  throw new Error(`No templates found. Use --plugin or --templates to specify templates.`);
445
434
  }
@@ -494,13 +483,11 @@ async function addExtension(type, options) {
494
483
  extension.createFiles(appleDir, appInfo, templatesDir);
495
484
  console.log(`\n2. Updating main app entitlements...`);
496
485
  updateMainAppEntitlements(appleDir, appInfo);
497
- console.log(`\n3. Adding URL scheme to Info.plist...`);
498
- addUrlSchemeToInfoPlist(appleDir, appInfo);
499
- console.log(`\n4. Updating project.yml...`);
486
+ console.log(`\n3. Updating project.yml (extension target + URL scheme)...`);
500
487
  projectYml = readProjectYml(appleDir);
501
488
  projectYml = extension.updateProjectYml(projectYml, appInfo);
502
489
  writeProjectYml(appleDir, projectYml);
503
- console.log(`\n5. Regenerating Xcode project...`);
490
+ console.log(`\n4. Regenerating Xcode project...`);
504
491
  runXcodeGen(appleDir);
505
492
  console.log(`\n========================================`);
506
493
  console.log(`${extension.displayName} setup complete!`);
@@ -520,4 +507,39 @@ async function addExtension(type, options) {
520
507
  }
521
508
  }
522
509
 
523
- export { addExtension, addUrlSchemeToInfoPlist, createExtensionEntitlements, findAppleProjectDir, findProjectRoot, findTauriConfig, getAppInfo, readProjectYml, runXcodeGen, shareExtension, updateMainAppEntitlements, writeProjectYml };
510
+ function addUrlSchemeToInfoPlist(appleDir, appInfo) {
511
+ const targetName = `${appInfo.productName}_iOS`;
512
+ const infoPlistPath = path.join(appleDir, targetName, "Info.plist");
513
+ const urlScheme = appInfo.productName.toLowerCase().replace(/[^a-z0-9]/g, "");
514
+ if (!fs.existsSync(infoPlistPath)) {
515
+ console.log(`Info.plist not found at ${infoPlistPath}, skipping URL scheme setup`);
516
+ return;
517
+ }
518
+ let infoPlist = fs.readFileSync(infoPlistPath, "utf8");
519
+ // Check if URL schemes already configured
520
+ if (infoPlist.includes("CFBundleURLSchemes")) {
521
+ if (!infoPlist.includes(urlScheme)) {
522
+ console.log(`URL scheme may need manual configuration. Add '${urlScheme}' to CFBundleURLSchemes.`);
523
+ }
524
+ return;
525
+ }
526
+ // Add URL scheme - need to insert before closing </dict></plist>
527
+ const urlSchemeEntry = ` <key>CFBundleURLTypes</key>
528
+ <array>
529
+ <dict>
530
+ <key>CFBundleURLSchemes</key>
531
+ <array>
532
+ <string>${urlScheme}</string>
533
+ </array>
534
+ <key>CFBundleURLName</key>
535
+ <string>${appInfo.identifier}</string>
536
+ </dict>
537
+ </array>
538
+ `;
539
+ // Insert before the last </dict>
540
+ infoPlist = infoPlist.replace(/(\s*)<\/dict>\s*<\/plist>/, `\n${urlSchemeEntry}$1</dict>\n</plist>`);
541
+ fs.writeFileSync(infoPlistPath, infoPlist);
542
+ console.log(`Added URL scheme '${urlScheme}' to Info.plist`);
543
+ }
544
+
545
+ export { addExtension, addUrlSchemeToInfoPlist, addUrlSchemeToTarget, createExtensionEntitlements, findAppleProjectDir, findProjectRoot, findTauriConfig, getAppInfo, readProjectYml, runXcodeGen, shareExtension, updateMainAppEntitlements, writeProjectYml };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@choochmeque/tauri-apple-extensions",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Add iOS extensions to Tauri apps with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,16 +12,6 @@
12
12
  "templates",
13
13
  "README.md"
14
14
  ],
15
- "scripts": {
16
- "build": "rollup -c",
17
- "prepublishOnly": "pnpm build",
18
- "pretest": "pnpm build",
19
- "test": "vitest run",
20
- "test:watch": "vitest",
21
- "lint": "eslint .",
22
- "format": "prettier --write \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\"",
23
- "format:check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
24
- },
25
15
  "keywords": [
26
16
  "tauri",
27
17
  "ios",
@@ -45,7 +35,6 @@
45
35
  "commander": "^14.0.2"
46
36
  },
47
37
  "devDependencies": {
48
- "@eslint/js": "^9.39.2",
49
38
  "@rollup/plugin-node-resolve": "^16.0.3",
50
39
  "@rollup/plugin-typescript": "^12.3.0",
51
40
  "@types/node": "^25.0.3",
@@ -60,5 +49,13 @@
60
49
  "engines": {
61
50
  "node": ">=18.0.0"
62
51
  },
63
- "packageManager": "pnpm@10.16.0+sha512.8066e7b034217b700a9a4dbb3a005061d641ba130a89915213a10b3ca4919c19c037bec8066afdc559b89635fdb806b16ea673f2468fbb28aabfa13c53e3f769"
64
- }
52
+ "scripts": {
53
+ "build": "rollup -c",
54
+ "pretest": "pnpm build",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "lint": "eslint .",
58
+ "format": "prettier --write \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\"",
59
+ "format:check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
60
+ }
61
+ }
@@ -1,246 +1,126 @@
1
1
  import UIKit
2
2
  import Social
3
- import MobileCoreServices
4
3
  import UniformTypeIdentifiers
5
4
 
6
5
  class ShareViewController: UIViewController {
7
6
 
8
- // MARK: - Configuration (will be replaced by setup script)
7
+ // MARK: - Configuration
9
8
  private let appGroupIdentifier = "{{APP_GROUP_IDENTIFIER}}"
10
9
  private let appURLScheme = "{{APP_URL_SCHEME}}"
11
10
 
11
+ // MARK: - Lifecycle
12
+
12
13
  override func viewDidLoad() {
13
14
  super.viewDidLoad()
14
- view.backgroundColor = .clear
15
+ // TODO: Setup your UI here
15
16
  }
16
17
 
17
18
  override func viewDidAppear(_ animated: Bool) {
18
19
  super.viewDidAppear(animated)
19
- handleSharedContent()
20
+ processSharedItems()
20
21
  }
21
22
 
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
- }
23
+ // MARK: - Share Processing
28
24
 
25
+ private func processSharedItems() {
29
26
  guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
30
- completeRequest()
27
+ complete()
31
28
  return
32
29
  }
33
30
 
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 }
31
+ for item in extensionItems {
32
+ guard let attachments = item.attachments else { continue }
44
33
 
45
34
  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() }
35
+ // TODO: Handle different content types
36
+ // Examples:
37
+ // - UTType.image.identifier for images
38
+ // - UTType.url.identifier for URLs
39
+ // - UTType.text.identifier for text
40
+ // - UTType.fileURL.identifier for files
41
+
42
+ if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
43
+ attachment.loadItem(forTypeIdentifier: UTType.url.identifier) { [weak self] item, error in
52
44
  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
- }
45
+ // TODO: Process the URL
46
+ print("Received URL: \(url)")
60
47
  }
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 }
48
+ DispatchQueue.main.async {
49
+ self?.complete()
73
50
  }
74
51
  }
75
- } else if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
76
- attachment.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, error in
77
- defer { group.leave() }
52
+ return
53
+ }
54
+
55
+ if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
56
+ attachment.loadItem(forTypeIdentifier: UTType.text.identifier) { [weak self] item, error in
78
57
  if let text = item as? String {
79
- resultQueue.sync { textContent = text }
58
+ // TODO: Process the text
59
+ print("Received text: \(text)")
60
+ }
61
+ DispatchQueue.main.async {
62
+ self?.complete()
80
63
  }
81
64
  }
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() }
65
+ return
66
+ }
67
+
68
+ if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
69
+ attachment.loadItem(forTypeIdentifier: UTType.image.identifier) { [weak self] item, error in
85
70
  if let url = item as? URL {
86
- if let fileInfo = self?.copyFileToAppGroup(url: url) {
87
- resultQueue.sync { files.append(fileInfo) }
88
- }
71
+ // TODO: Process image file URL
72
+ print("Received image: \(url)")
73
+ } else if let image = item as? UIImage {
74
+ // TODO: Process UIImage directly
75
+ print("Received UIImage")
76
+ }
77
+ DispatchQueue.main.async {
78
+ self?.complete()
89
79
  }
90
80
  }
91
- } else {
92
- group.leave()
81
+ return
93
82
  }
94
83
  }
95
84
  }
96
85
 
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
- }
86
+ complete()
195
87
  }
196
88
 
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
- }
89
+ // MARK: - App Group Storage (Optional)
203
90
 
204
- private func saveToAppGroup(content: [String: Any]) -> Bool {
91
+ /// Save data to App Group for main app to read
92
+ private func saveToAppGroup(_ data: Data, forKey key: String) -> Bool {
205
93
  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.")
94
+ print("App Groups not configured")
207
95
  return false
208
96
  }
97
+ userDefaults.set(data, forKey: key)
98
+ return true
99
+ }
209
100
 
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
- }
101
+ /// Get App Group container URL for file storage
102
+ private func appGroupContainerURL() -> URL? {
103
+ FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
219
104
  }
220
105
 
221
- private func openMainAppAndComplete() {
222
- guard let url = URL(string: "\(appURLScheme)://sharekit-content") else {
223
- completeRequest()
224
- return
225
- }
106
+ // MARK: - Open Main App (Optional)
107
+
108
+ private func openMainApp() {
109
+ guard let url = URL(string: "\(appURLScheme)://share") else { return }
226
110
 
227
111
  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
- }
112
+ while let r = responder {
113
+ if let application = r as? UIApplication {
114
+ application.open(url, options: [:], completionHandler: nil)
235
115
  break
236
116
  }
237
- responder = responder?.next
117
+ responder = r.next
238
118
  }
239
-
240
- completeRequest()
241
119
  }
242
120
 
243
- private func completeRequest() {
121
+ // MARK: - Complete
122
+
123
+ private func complete() {
244
124
  extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
245
125
  }
246
126
  }