@bacons/apple-targets 0.1.6 → 0.1.8
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 +118 -25
- package/app.plugin.js +1 -1
- package/build/ExtensionStorage.d.ts +6 -0
- package/build/ExtensionStorage.js +48 -0
- package/build/cli/cli.d.ts +1 -0
- package/build/cli/cli.js +2 -0
- package/build/target.js +16 -1
- package/build/withXcodeChanges.js +7 -2
- package/expo-module.config.json +6 -0
- package/ios/ExtensionStorage.podspec +21 -0
- package/ios/ExtensionStorageModule.swift +53 -0
- package/package.json +5 -2
- /package/build/{index.d.ts → config-plugin.d.ts} +0 -0
- /package/build/{index.js → config-plugin.js} +0 -0
package/README.md
CHANGED
|
@@ -9,14 +9,30 @@ An experimental Expo Config Plugin that generates native Apple Targets like Widg
|
|
|
9
9
|
|
|
10
10
|
## 🚀 How to use
|
|
11
11
|
|
|
12
|
-
> This plugin requires at least CocoaPods 1.16.2 and
|
|
12
|
+
> This plugin requires at least CocoaPods 1.16.2, Xcode 16, and Expo SDK +52.
|
|
13
|
+
|
|
14
|
+
Run the following command in your Expo project:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
npx create-target
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Select a target to generate, I recommend starting with a `widget`.
|
|
21
|
+
|
|
22
|
+
This will generate the required widget files in the `targets` directory.
|
|
23
|
+
|
|
24
|
+
Ensure the `ios.appleTeamId` property is set in your `app.json`, then run `npx expo prebuild -p ios --clean` to generate the Xcode project.
|
|
25
|
+
|
|
26
|
+
You can now open Xcode and develop the widget inside the `expo:targets` folder. When you're ready to build, run:
|
|
27
|
+
|
|
28
|
+
### Manual usage
|
|
13
29
|
|
|
14
30
|
- Add targets to `targets/` directory with an `expo-target.config.json` file.
|
|
15
|
-
-
|
|
31
|
+
- If you don't have an `Info.plist`, it'll be generated on `npx expo prebuild`. This may be changed in the future so if you have an `Info.plist` it'll be used, otherwise, it'll be generated.
|
|
16
32
|
- Any files in a top-level `target/*/assets` directory will be linked as resources of the target. This was added to support Safari Extensions.
|
|
17
33
|
- A single top-level `*.entitlements` file will be linked as the entitlements of the target. This is not currently used in EAS Capability signing, but may be in the future.
|
|
18
34
|
- All Swift files will be linked as build sources of the target. There is currently no support for storyboard or `.xib` files because I can't be bothered.
|
|
19
|
-
- All
|
|
35
|
+
- All `*.xcassets` files will be linked as resources, and accessible in the targets. If you add files outside of Xcode, you'll need to re-run `npx expo prebuild` to link them.
|
|
20
36
|
- In Expo SDK +52, set the `ios.appleTeamId`, for SDK 51 and below, set the `appleTeamId` prop in the Config Plugin in `app.config.js`:
|
|
21
37
|
|
|
22
38
|
```json
|
|
@@ -32,49 +48,43 @@ An experimental Expo Config Plugin that generates native Apple Targets like Widg
|
|
|
32
48
|
}
|
|
33
49
|
```
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
## Using React Native in Targets
|
|
38
|
-
|
|
39
|
-
I'm not sure, that's not the purpose of this plugin. I built this so I could easily build iOS widgets and other minor targets with SwiftUI. I imagine it would be straightforward to use React Native in share, notification, iMessage, Safari, and photo editing extensions, you can build that on top of this plugin if you want. Look at the App Clip example for a starting point.
|
|
40
|
-
|
|
41
|
-
## `expo-target.config.json`
|
|
51
|
+
## `expo-target.config.js`
|
|
42
52
|
|
|
43
53
|
This file can have the following properties:
|
|
44
54
|
|
|
45
|
-
```
|
|
46
|
-
{
|
|
47
|
-
|
|
55
|
+
```js
|
|
56
|
+
module.exports = {
|
|
57
|
+
type: "widget",
|
|
48
58
|
|
|
49
59
|
// Name of the target/product. Defaults to the directory name.
|
|
50
|
-
|
|
60
|
+
name: "My Widget",
|
|
51
61
|
|
|
52
62
|
// Generates colorset files for the target.
|
|
53
|
-
|
|
63
|
+
colors: {
|
|
54
64
|
// or "$accent": "red",
|
|
55
|
-
|
|
65
|
+
$accent: { color: "red", darkColor: "blue" },
|
|
56
66
|
},
|
|
57
|
-
|
|
67
|
+
icon: "../assets/icon.png",
|
|
58
68
|
// Can also be a URL
|
|
59
|
-
|
|
69
|
+
frameworks: [
|
|
60
70
|
// Frameworks without the extension, these will be added to the target.
|
|
61
|
-
"SwiftUI"
|
|
71
|
+
"SwiftUI",
|
|
62
72
|
],
|
|
63
|
-
|
|
73
|
+
entitlements: {
|
|
64
74
|
// Serialized entitlements. Useful for configuring with environment variables.
|
|
65
75
|
},
|
|
66
76
|
// Generates xcassets for the target.
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
images: {
|
|
78
|
+
thing: "../assets/thing.png",
|
|
69
79
|
},
|
|
70
80
|
|
|
71
81
|
// The iOS version fot the target.
|
|
72
|
-
|
|
82
|
+
deploymentTarget: "13.4",
|
|
73
83
|
|
|
74
84
|
// Optional bundle identifier for the target. Will default to a sanitized version of the root project bundle id + target name.
|
|
75
85
|
// If the specified bundle identifier is prefixed with a dot (.), the bundle identifier will be appended to the main app's bundle identifier.
|
|
76
|
-
|
|
77
|
-
}
|
|
86
|
+
bundleIdentifier: ".mywidget",
|
|
87
|
+
};
|
|
78
88
|
```
|
|
79
89
|
|
|
80
90
|
You can also use `.js` with the typedoc for autocomplete:
|
|
@@ -312,3 +322,86 @@ Some workarounds:
|
|
|
312
322
|
|
|
313
323
|
- Prebuild without React Native: `npx expo prebuild --template node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean`
|
|
314
324
|
- If the widget doesn't show on the home screen when building the app, use iOS 18. You can long press the app icon and select the widget display options to transform the app icon into the widget.
|
|
325
|
+
|
|
326
|
+
## Sharing data between targets
|
|
327
|
+
|
|
328
|
+
To share values between the app and the target, you must use App Groups and NSUserDefaults. I've added a native module to make the React Native API a bit easier.
|
|
329
|
+
|
|
330
|
+
### Configuring App Groups
|
|
331
|
+
|
|
332
|
+
Start by defining an App Group, a good default is `group.<bundle identifier>`. App Groups can be used across apps so you may want something more generic or less generic if you plan on having multiple extensions.
|
|
333
|
+
|
|
334
|
+
First, define your main App Group entitlement in your `app.json`:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"expo": {
|
|
339
|
+
"ios": {
|
|
340
|
+
"entitlements": {
|
|
341
|
+
"com.apple.security.application-groups": ["group.bacon.data"]
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"plugins": ["@bacons/apple-targets"]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Second, define the same App Group in your target's `expo-target.config.js`:
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
/** @type {import('@bacons/apple-targets').ConfigFunction} */
|
|
353
|
+
module.exports = (config) => ({
|
|
354
|
+
type: "widget",
|
|
355
|
+
entitlements: {
|
|
356
|
+
// Use the same app groups:
|
|
357
|
+
"com.apple.security.application-groups":
|
|
358
|
+
config.ios.entitlements["com.apple.security.application-groups"],
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Now you can prebuild to generate the entitlements. You may need to create an EAS Build or open Xcode to sync the entitlements.
|
|
364
|
+
|
|
365
|
+
### Setting shared data
|
|
366
|
+
|
|
367
|
+
To define shared data, we'll use a native module (`ExtensionStorage`) that interacts with `NSUserDefaults`.
|
|
368
|
+
|
|
369
|
+
Somewhere in your Expo app, you can set a value:
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
import { ExtensionStorage } from "@bacons/apple-targets";
|
|
373
|
+
|
|
374
|
+
// Create a storage object with the App Group.
|
|
375
|
+
const storage = new ExtensionStorage(
|
|
376
|
+
// Your app group identifier. Should match the values in the app.json and expo-target.config.json.
|
|
377
|
+
"group.bacon.data"
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Then you can set data:
|
|
381
|
+
storage.set("myKey", "myValue");
|
|
382
|
+
|
|
383
|
+
// Finally, you can reload the widget:
|
|
384
|
+
ExtensionStorage.reloadWidget();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`ExtensionStorage` has the following API:
|
|
388
|
+
|
|
389
|
+
- `set(key: string, value: string | number | Record<string, string | number> | Array<Record<string, string | number>> | undefined): void` - Sets a value in the shared storage for a given key. Setting `undefined` will remove the key.
|
|
390
|
+
- `ExtensionStorage.reloadWidget(name?: string): void` - A static method for reloading the widget. Behind the scenes, this calls `WidgetCenter.shared.reloadAllTimelines()`. If given a name, it will reload a specific widget using `WidgetCenter.shared.reloadTimelines(ofKind: timeline)`.
|
|
391
|
+
|
|
392
|
+
### Accessing shared data
|
|
393
|
+
|
|
394
|
+
Assuming this is done using Swift code, you'll access data using `NSUserDefaults` directly. Here's an example of how you might access the data in a widget:
|
|
395
|
+
|
|
396
|
+
```swift
|
|
397
|
+
let defaults = UserDefaults(suiteName:
|
|
398
|
+
// Use the App Group from earlier.
|
|
399
|
+
"group.bacon.data"
|
|
400
|
+
)
|
|
401
|
+
// Access the value you set:
|
|
402
|
+
let index = defaults?.string(forKey: "myKey")
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Using React Native in Targets
|
|
406
|
+
|
|
407
|
+
I'm not sure, that's not the purpose of this plugin. I built this so I could easily build iOS widgets and other minor targets with SwiftUI. I imagine it would be straightforward to use React Native in share, notification, iMessage, Safari, and photo editing extensions, you can build that on top of this plugin if you want. Look at the App Clip example for a starting point.
|
package/app.plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = require(
|
|
1
|
+
module.exports = require("./build/config-plugin");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class ExtensionStorage {
|
|
2
|
+
private readonly appGroup;
|
|
3
|
+
static reloadWidget(name?: string): void;
|
|
4
|
+
constructor(appGroup: string);
|
|
5
|
+
set(key: string, value?: string | number | Record<string, string | number> | Array<Record<string, string | number>>): void;
|
|
6
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var _a;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ExtensionStorage = void 0;
|
|
5
|
+
// @ts-expect-error
|
|
6
|
+
const ExtensionStorageModule = (_a = expo === null || expo === void 0 ? void 0 : expo.modules) === null || _a === void 0 ? void 0 : _a.ExtensionStorage;
|
|
7
|
+
const nativeModule = ExtensionStorageModule !== null && ExtensionStorageModule !== void 0 ? ExtensionStorageModule : {
|
|
8
|
+
setInt() { },
|
|
9
|
+
setString() { },
|
|
10
|
+
reloadWidget() { },
|
|
11
|
+
setObject() { },
|
|
12
|
+
remove() { },
|
|
13
|
+
setArray() { },
|
|
14
|
+
};
|
|
15
|
+
const originalSetObject = nativeModule.setObject;
|
|
16
|
+
// Sweet API doesn't support doing this natively.
|
|
17
|
+
nativeModule.setObject = (key, value, suite) => {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return nativeModule.setArray(key, value, suite);
|
|
20
|
+
}
|
|
21
|
+
return originalSetObject(key, value, suite);
|
|
22
|
+
};
|
|
23
|
+
class ExtensionStorage {
|
|
24
|
+
static reloadWidget(name) {
|
|
25
|
+
nativeModule.reloadWidget(name);
|
|
26
|
+
}
|
|
27
|
+
constructor(appGroup) {
|
|
28
|
+
this.appGroup = appGroup;
|
|
29
|
+
}
|
|
30
|
+
set(key, value) {
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
nativeModule.setInt(key, value, this.appGroup);
|
|
33
|
+
}
|
|
34
|
+
else if (Array.isArray(value)) {
|
|
35
|
+
nativeModule.setArray(key, value, this.appGroup);
|
|
36
|
+
}
|
|
37
|
+
else if (typeof value === "string") {
|
|
38
|
+
nativeModule.setString(key, value, this.appGroup);
|
|
39
|
+
}
|
|
40
|
+
else if (value == null) {
|
|
41
|
+
nativeModule.remove(key, this.appGroup);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
nativeModule.setObject(key, value, this.appGroup);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.ExtensionStorage = ExtensionStorage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/build/cli/cli.js
ADDED
package/build/target.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.KNOWN_EXTENSION_POINT_IDENTIFIERS = {
|
|
|
23
23
|
"com.apple.authentication-services-account-authentication-modification-ui": "account-auth",
|
|
24
24
|
"com.apple.services": "action",
|
|
25
25
|
"com.apple.appintents-extension": "app-intent",
|
|
26
|
+
"com.apple.deviceactivity.monitor-extension": "device-activity-monitor",
|
|
26
27
|
// "com.apple.intents-service": "intents",
|
|
27
28
|
};
|
|
28
29
|
// TODO: Maybe we can replace `NSExtensionPrincipalClass` with the `@main` annotation that newer extensions use?
|
|
@@ -53,7 +54,14 @@ function getTargetInfoPlistForType(type) {
|
|
|
53
54
|
},
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
|
-
if (type === "
|
|
57
|
+
else if (type === "app-intent") {
|
|
58
|
+
return plist_1.default.build({
|
|
59
|
+
EXAppExtensionAttributes: {
|
|
60
|
+
EXExtensionPointIdentifier: "com.apple.appintents-extension",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else if (type === "clip") {
|
|
57
65
|
return plist_1.default.build({
|
|
58
66
|
CFBundleName: "$(PRODUCT_NAME)",
|
|
59
67
|
CFBundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER)",
|
|
@@ -66,6 +74,13 @@ function getTargetInfoPlistForType(type) {
|
|
|
66
74
|
NSAppClipRequestEphemeralUserNotification: false,
|
|
67
75
|
NSAppClipRequestLocationConfirmation: false,
|
|
68
76
|
},
|
|
77
|
+
NSAppTransportSecurity: {
|
|
78
|
+
NSAllowsArbitraryLoads: false,
|
|
79
|
+
NSAllowsLocalNetworking: true,
|
|
80
|
+
},
|
|
81
|
+
UILaunchStoryboardName: "SplashScreen",
|
|
82
|
+
UIUserInterfaceStyle: "Automatic",
|
|
83
|
+
UIViewControllerBasedStatusBarAppearance: false,
|
|
69
84
|
});
|
|
70
85
|
}
|
|
71
86
|
const NSExtensionPointIdentifier = Object.keys(exports.KNOWN_EXTENSION_POINT_IDENTIFIERS).find((key) => exports.KNOWN_EXTENSION_POINT_IDENTIFIERS[key] === type);
|
|
@@ -653,9 +653,14 @@ async function applyXcodeChanges(config, project, props) {
|
|
|
653
653
|
console.log(`Target "${targetToUpdate.props.productName}" already exists, updating instead of creating a new one`);
|
|
654
654
|
}
|
|
655
655
|
const magicCwd = path_1.default.join(config._internal.projectRoot, "ios", props.cwd);
|
|
656
|
-
|
|
656
|
+
let developmentTeamId = (_b = props.teamId) !== null && _b !== void 0 ? _b : mainAppTarget.getDefaultBuildSetting("DEVELOPMENT_TEAM");
|
|
657
657
|
if (!developmentTeamId) {
|
|
658
|
-
|
|
658
|
+
console.error("Couldn't find DEVELOPMENT_TEAM in Xcode project and none were provided in the Expo config. Using placeholder value.");
|
|
659
|
+
// Using a placeholder gives users a chance to see the value in Xcode.
|
|
660
|
+
developmentTeamId = "XXXXXXXXXX";
|
|
661
|
+
// throw new Error(
|
|
662
|
+
// "Couldn't find DEVELOPMENT_TEAM in Xcode project and none were provided in the Expo config."
|
|
663
|
+
// );
|
|
659
664
|
}
|
|
660
665
|
function applyDevelopmentTeamIdToTargets() {
|
|
661
666
|
var _a, _b;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'ExtensionStorage'
|
|
3
|
+
s.version = '1.0.0'
|
|
4
|
+
s.summary = 'A sample project summary'
|
|
5
|
+
s.description = 'A sample project description'
|
|
6
|
+
s.author = ''
|
|
7
|
+
s.homepage = 'https://docs.expo.dev/modules/'
|
|
8
|
+
s.platform = :ios, '15.1'
|
|
9
|
+
s.source = { git: '' }
|
|
10
|
+
s.static_framework = true
|
|
11
|
+
|
|
12
|
+
s.dependency 'ExpoModulesCore'
|
|
13
|
+
|
|
14
|
+
# Swift/Objective-C compatibility
|
|
15
|
+
s.pod_target_xcconfig = {
|
|
16
|
+
'DEFINES_MODULE' => 'YES',
|
|
17
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
21
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import WidgetKit
|
|
3
|
+
|
|
4
|
+
public class ExtensionStorageModule: Module {
|
|
5
|
+
public func definition() -> ModuleDefinition {
|
|
6
|
+
Name("ExtensionStorage")
|
|
7
|
+
|
|
8
|
+
Function("remove") { (forKey: String, suiteName: String?) in
|
|
9
|
+
UserDefaults(suiteName: suiteName)?.removeObject(forKey: forKey)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Function("reloadWidget") { (timeline: String?) in
|
|
13
|
+
if let timeline = timeline {
|
|
14
|
+
WidgetCenter.shared.reloadTimelines(ofKind: timeline)
|
|
15
|
+
} else {
|
|
16
|
+
WidgetCenter.shared.reloadAllTimelines()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Function("setArray") { (forKey: String, data: [[String: Any]], suiteName: String?) -> Bool in
|
|
21
|
+
// Convert the incoming array of dictionaries directly to JSON data
|
|
22
|
+
do {
|
|
23
|
+
let jsonData = try JSONSerialization.data(withJSONObject: data, options: [])
|
|
24
|
+
UserDefaults(suiteName: suiteName)?.set(jsonData, forKey: forKey)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
// If encoding fails for some reason, return false
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Function("setObject") { (forKey: String, data: [String: Any], suiteName: String?) -> Bool in
|
|
33
|
+
do {
|
|
34
|
+
let jsonData = try JSONSerialization.data(withJSONObject: data, options: [])
|
|
35
|
+
UserDefaults(suiteName: suiteName)?.set(jsonData, forKey: forKey)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
// If encoding fails for some reason, return false
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Function("setInt") { (key: String, value: Int, group: String?) in
|
|
44
|
+
let userDefaults = UserDefaults(suiteName: group)
|
|
45
|
+
userDefaults?.set(value, forKey: key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Function("setString") { (key: String, value: String, group: String?) in
|
|
49
|
+
let userDefaults = UserDefaults(suiteName: group)
|
|
50
|
+
userDefaults?.set(value, forKey: key)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacons/apple-targets",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Generate Apple Targets with Expo Prebuild",
|
|
5
|
-
"main": "build/
|
|
5
|
+
"main": "build/ExtensionStorage.js",
|
|
6
|
+
"types": "build/ExtensionStorage.d.ts",
|
|
6
7
|
"files": [
|
|
7
8
|
"app.plugin.js",
|
|
8
9
|
"build",
|
|
10
|
+
"ios",
|
|
11
|
+
"expo-module.config.json",
|
|
9
12
|
"prebuild-blank.tgz"
|
|
10
13
|
],
|
|
11
14
|
"scripts": {
|
|
File without changes
|
|
File without changes
|