@craft-native/ios 0.0.175 → 0.0.177
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 +9 -3
- package/dist/cli.js +7 -1
- package/dist/index.d.ts +19 -0
- package/dist/index.js +7 -1
- package/dist/testing.d.ts +5 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +51 -1
- package/templates/CraftCrashReporter.swift +1010 -0
- package/templates/CraftHybrid.swift +10 -0
- package/templates/CraftNativeActions.swift +49 -1
- package/templates/CraftNativeFlatList.swift +30 -28
- package/templates/CraftNativeMutation.swift +1 -1
- package/templates/CraftNativeScreen.swift +559 -86
- package/templates/project.yml.template +1 -0
- package/templates/testing/DriverTests.swift.template +8 -0
package/README.md
CHANGED
|
@@ -74,6 +74,11 @@ views when rows are materialized or recycled.
|
|
|
74
74
|
bun add craft-native
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
## Native map input contract
|
|
78
|
+
|
|
79
|
+
Map coordinates are finite and range-checked at the native boundary; invalid
|
|
80
|
+
points and pole-crossing regions are ignored on iOS just as on Android.
|
|
81
|
+
|
|
77
82
|
## Quick Start
|
|
78
83
|
|
|
79
84
|
### 1. Initialize Project
|
|
@@ -124,7 +129,7 @@ authorization or requiring local notifications to be enabled.
|
|
|
124
129
|
Keyboard frame changes update the native controller's additional safe area, so
|
|
125
130
|
focused inputs and responsive `onLayout` measurements move with the keyboard.
|
|
126
131
|
|
|
127
|
-
The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `Video`, `Map`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays and can fit the visible region to their bounds. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
|
|
132
|
+
The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays and can fit the visible region to their bounds. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
|
|
128
133
|
|
|
129
134
|
```bash
|
|
130
135
|
craft ios init NativeDemo --renderer native --output ./ios-native
|
|
@@ -159,8 +164,9 @@ the [Native STX guide](../../docs/guides/native-stx.md). On macOS,
|
|
|
159
164
|
`bun packages/ios/scripts/test-native-render.ts` runs the renderer's unit and
|
|
160
165
|
simulator UI tests.
|
|
161
166
|
|
|
162
|
-
The iOS host uses the shared layout ABI
|
|
163
|
-
|
|
167
|
+
The iOS host uses the shared tree-layout ABI for eligible definite-size,
|
|
168
|
+
non-grid, non-wrapped containers and retains a local fallback for older
|
|
169
|
+
runtimes or unsupported styles. Both paths implement wrapped-line and grid
|
|
164
170
|
`alignContent`, including `space-evenly`, so a missing optional shared symbol
|
|
165
171
|
does not change placement semantics.
|
|
166
172
|
Reverse flex directions also retain wrapped line order on the cross axis while
|
package/dist/cli.js
CHANGED
|
@@ -515,7 +515,9 @@ function renderFeatureSettings(config) {
|
|
|
515
515
|
}
|
|
516
516
|
function renderPrivacyManifest(config) {
|
|
517
517
|
const privacy = config.privacy ?? {};
|
|
518
|
-
const collected = privacy.collectedDataTypes ?? [];
|
|
518
|
+
const collected = [...privacy.collectedDataTypes ?? []];
|
|
519
|
+
if (config.crashReporting?.key && !collected.some((item) => item.type === "NSPrivacyCollectedDataTypeCrashData"))
|
|
520
|
+
collected.push({ type: "NSPrivacyCollectedDataTypeCrashData", linked: true, tracking: false, purposes: ["NSPrivacyCollectedDataTypePurposeAppFunctionality"] });
|
|
519
521
|
const accessed = privacy.accessedApiTypes ?? [];
|
|
520
522
|
const collectedXml = collected.map((item) => ` <dict>
|
|
521
523
|
<key>NSPrivacyCollectedDataType</key>
|
|
@@ -877,6 +879,7 @@ function writeShell(output, config) {
|
|
|
877
879
|
cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
|
|
878
880
|
cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
|
|
879
881
|
cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
|
|
882
|
+
writeFileSync(join3(output, "Sources", "CraftCrashReporter.swift"), readFileSync(join3(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
|
|
880
883
|
cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
|
|
881
884
|
for (const [enabled, folder, template, filename] of [
|
|
882
885
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
@@ -951,6 +954,7 @@ async function init(options) {
|
|
|
951
954
|
- Shared
|
|
952
955
|
settings:
|
|
953
956
|
INFOPLIST_FILE: WidgetExtension/Info.plist
|
|
957
|
+
PRODUCT_NAME: "$(TARGET_NAME)"
|
|
954
958
|
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
|
|
955
959
|
SWIFT_VERSION: "5.0"
|
|
956
960
|
APPLICATION_EXTENSION_API_ONLY: YES
|
|
@@ -966,6 +970,7 @@ async function init(options) {
|
|
|
966
970
|
settings:
|
|
967
971
|
INFOPLIST_FILE: WatchApp/Info.plist
|
|
968
972
|
CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
|
|
973
|
+
PRODUCT_NAME: "$(TARGET_NAME)"
|
|
969
974
|
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
|
|
970
975
|
SWIFT_VERSION: "5.0"
|
|
971
976
|
SKIP_INSTALL: YES
|
|
@@ -980,6 +985,7 @@ async function init(options) {
|
|
|
980
985
|
settings:
|
|
981
986
|
INFOPLIST_FILE: WatchExtension/Info.plist
|
|
982
987
|
CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
|
|
988
|
+
PRODUCT_NAME: "$(TARGET_NAME)"
|
|
983
989
|
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
|
|
984
990
|
SWIFT_VERSION: "5.0"
|
|
985
991
|
APPLICATION_EXTENSION_API_ONLY: YES
|
package/dist/index.d.ts
CHANGED
|
@@ -239,6 +239,25 @@ export interface CraftConfig {
|
|
|
239
239
|
* `craft.secureStorage.getSync('auth.token')`. Turns on secure storage.
|
|
240
240
|
*/
|
|
241
241
|
shareStorage?: Record<string, string>;
|
|
242
|
+
/**
|
|
243
|
+
* Crash and error reporting to bughq (CraftCrashReporter.swift): crashes
|
|
244
|
+
* and hangs (signal handlers, NSException, MetricKit), uncaught JavaScript
|
|
245
|
+
* errors in native screens and the app script, and what the page sends
|
|
246
|
+
* through `craft.crashReporter`. Queued on disk and sent when online.
|
|
247
|
+
*/
|
|
248
|
+
crashReporting?: CraftCrashReporting;
|
|
249
|
+
}
|
|
250
|
+
export interface CraftCrashReporting {
|
|
251
|
+
/** The bughq project's public ingest key. Ships in the app; it is not a secret. */
|
|
252
|
+
key: string;
|
|
253
|
+
/** The bughq host. Default https://bughq.org. */
|
|
254
|
+
host?: string;
|
|
255
|
+
/** Default `production`. */
|
|
256
|
+
environment?: string;
|
|
257
|
+
/** The release every event names. Default `<version>+<build>`, e.g. `1.0.0+41`. */
|
|
258
|
+
release?: string;
|
|
259
|
+
/** One session per launch, for crash-free sessions per release. Default on. */
|
|
260
|
+
sessions?: boolean;
|
|
242
261
|
}
|
|
243
262
|
export interface CraftTab {
|
|
244
263
|
/** The tab's root path; comes back to the page in `craftTabSelect`. */
|
package/dist/index.js
CHANGED
|
@@ -643,7 +643,9 @@ function renderFeatureSettings(config) {
|
|
|
643
643
|
}
|
|
644
644
|
function renderPrivacyManifest(config) {
|
|
645
645
|
const privacy = config.privacy ?? {};
|
|
646
|
-
const collected = privacy.collectedDataTypes ?? [];
|
|
646
|
+
const collected = [...privacy.collectedDataTypes ?? []];
|
|
647
|
+
if (config.crashReporting?.key && !collected.some((item) => item.type === "NSPrivacyCollectedDataTypeCrashData"))
|
|
648
|
+
collected.push({ type: "NSPrivacyCollectedDataTypeCrashData", linked: true, tracking: false, purposes: ["NSPrivacyCollectedDataTypePurposeAppFunctionality"] });
|
|
647
649
|
const accessed = privacy.accessedApiTypes ?? [];
|
|
648
650
|
const collectedXml = collected.map((item) => ` <dict>
|
|
649
651
|
<key>NSPrivacyCollectedDataType</key>
|
|
@@ -1009,6 +1011,7 @@ function writeShell(output, config) {
|
|
|
1009
1011
|
cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
|
|
1010
1012
|
cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
|
|
1011
1013
|
cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
|
|
1014
|
+
writeFileSync2(join3(output, "Sources", "CraftCrashReporter.swift"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
|
|
1012
1015
|
cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
|
|
1013
1016
|
for (const [enabled, folder, template, filename] of [
|
|
1014
1017
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
@@ -1083,6 +1086,7 @@ async function init(options) {
|
|
|
1083
1086
|
- Shared
|
|
1084
1087
|
settings:
|
|
1085
1088
|
INFOPLIST_FILE: WidgetExtension/Info.plist
|
|
1089
|
+
PRODUCT_NAME: "$(TARGET_NAME)"
|
|
1086
1090
|
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
|
|
1087
1091
|
SWIFT_VERSION: "5.0"
|
|
1088
1092
|
APPLICATION_EXTENSION_API_ONLY: YES
|
|
@@ -1098,6 +1102,7 @@ async function init(options) {
|
|
|
1098
1102
|
settings:
|
|
1099
1103
|
INFOPLIST_FILE: WatchApp/Info.plist
|
|
1100
1104
|
CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
|
|
1105
|
+
PRODUCT_NAME: "$(TARGET_NAME)"
|
|
1101
1106
|
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
|
|
1102
1107
|
SWIFT_VERSION: "5.0"
|
|
1103
1108
|
SKIP_INSTALL: YES
|
|
@@ -1112,6 +1117,7 @@ async function init(options) {
|
|
|
1112
1117
|
settings:
|
|
1113
1118
|
INFOPLIST_FILE: WatchExtension/Info.plist
|
|
1114
1119
|
CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
|
|
1120
|
+
PRODUCT_NAME: "$(TARGET_NAME)"
|
|
1115
1121
|
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
|
|
1116
1122
|
SWIFT_VERSION: "5.0"
|
|
1117
1123
|
APPLICATION_EXTENSION_API_ONLY: YES
|
package/dist/testing.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export type SimulatorStep = {
|
|
|
24
24
|
} | {
|
|
25
25
|
command: 'swipe';
|
|
26
26
|
direction: 'up' | 'down' | 'left' | 'right';
|
|
27
|
+
}
|
|
28
|
+
/** The system back-swipe: a drag in from the left edge (or the right). */
|
|
29
|
+
| {
|
|
30
|
+
command: 'edge';
|
|
31
|
+
direction: 'left' | 'right';
|
|
27
32
|
} | {
|
|
28
33
|
command: 'tabbar';
|
|
29
34
|
state: 'shown' | 'hidden';
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -194,8 +194,10 @@ struct CraftRecoveryBudget {
|
|
|
194
194
|
|
|
195
195
|
final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
|
196
196
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
197
|
-
UNUserNotificationCenter.current().delegate = self
|
|
198
197
|
let config = CraftConfig.bundled()
|
|
198
|
+
// First, so a crash anywhere in the rest of launch is reported too.
|
|
199
|
+
CraftCrashReporter.shared.start(config)
|
|
200
|
+
UNUserNotificationCenter.current().delegate = self
|
|
199
201
|
// Before launch finishes, which is the only time iOS accepts it.
|
|
200
202
|
CraftBackgroundWork.shared.registerRefresh(config)
|
|
201
203
|
// The shell, the app script and an all-native app's services now,
|
|
@@ -1297,6 +1299,9 @@ struct CraftConfig: Codable {
|
|
|
1297
1299
|
/// The bundle route of the app script: a screen-less script that runs for
|
|
1298
1300
|
/// the app's whole life and hears the app's events (CraftHybrid.swift).
|
|
1299
1301
|
var nativeApp: String? = nil
|
|
1302
|
+
/// Crash and error reporting to bughq (CraftCrashReporter.swift). Off
|
|
1303
|
+
/// unless the config names a project key.
|
|
1304
|
+
var crashReporting: CrashReporting? = nil
|
|
1300
1305
|
|
|
1301
1306
|
struct Tab: Codable {
|
|
1302
1307
|
var id: String
|
|
@@ -1311,6 +1316,18 @@ struct CraftConfig: Codable {
|
|
|
1311
1316
|
/// The soonest iOS may run it again, in minutes (15 if unset).
|
|
1312
1317
|
var minimumIntervalMinutes: Double? = nil
|
|
1313
1318
|
}
|
|
1319
|
+
|
|
1320
|
+
struct CrashReporting: Codable {
|
|
1321
|
+
/// The bughq project's public ingest key.
|
|
1322
|
+
var key: String
|
|
1323
|
+
/// The bughq host; https://bughq.org if unset.
|
|
1324
|
+
var host: String? = nil
|
|
1325
|
+
var environment: String? = nil
|
|
1326
|
+
/// The release events name; `<version>+<build>` if unset.
|
|
1327
|
+
var release: String? = nil
|
|
1328
|
+
/// A session per launch, for crash-free sessions. On unless false.
|
|
1329
|
+
var sessions: Bool? = nil
|
|
1330
|
+
}
|
|
1314
1331
|
}
|
|
1315
1332
|
|
|
1316
1333
|
extension CraftConfig {
|
|
@@ -3458,6 +3475,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3458
3475
|
heartRate: \(config.enableHeartRate),
|
|
3459
3476
|
scheduledWorkouts: \(config.enableScheduledWorkouts),
|
|
3460
3477
|
speech: true,
|
|
3478
|
+
crashReporter: \(CraftCrashReporter.shared.enabled),
|
|
3461
3479
|
network: true,
|
|
3462
3480
|
deviceInfo: true,
|
|
3463
3481
|
badge: true,
|
|
@@ -4451,6 +4469,38 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4451
4469
|
}
|
|
4452
4470
|
},
|
|
4453
4471
|
|
|
4472
|
+
// Crash and error reporting (CraftCrashReporter.swift). The
|
|
4473
|
+
// page's error SDK hands its events here, so they share the
|
|
4474
|
+
// app's on-disk outbox (sent when online), its session and the
|
|
4475
|
+
// device's own context. Each resolves at once: the work is a
|
|
4476
|
+
// file write, and the send happens later.
|
|
4477
|
+
crashReporter: {
|
|
4478
|
+
_post: function(message) {
|
|
4479
|
+
var self = window.craft;
|
|
4480
|
+
var id = 'cb_' + (++self._callbackId);
|
|
4481
|
+
message.callbackId = id;
|
|
4482
|
+
window.webkit.messageHandlers.craft.postMessage(message);
|
|
4483
|
+
return new Promise(function(resolve, reject) {
|
|
4484
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
4485
|
+
});
|
|
4486
|
+
},
|
|
4487
|
+
capture: function(event) {
|
|
4488
|
+
return this._post({action: 'crashReporterCapture', event: event || {}});
|
|
4489
|
+
},
|
|
4490
|
+
setUser: function(id) {
|
|
4491
|
+
return this._post({action: 'crashReporterSetUser', id: id == null ? null : String(id)});
|
|
4492
|
+
},
|
|
4493
|
+
addBreadcrumb: function(breadcrumb) {
|
|
4494
|
+
return this._post({action: 'crashReporterAddBreadcrumb', breadcrumb: breadcrumb || {}});
|
|
4495
|
+
},
|
|
4496
|
+
flush: function() {
|
|
4497
|
+
return this._post({action: 'crashReporterFlush'});
|
|
4498
|
+
},
|
|
4499
|
+
context: function() {
|
|
4500
|
+
return this._post({action: 'crashReporterContext'});
|
|
4501
|
+
}
|
|
4502
|
+
},
|
|
4503
|
+
|
|
4454
4504
|
// Orientation Lock
|
|
4455
4505
|
lockOrientation: function(orientation) {
|
|
4456
4506
|
var self = this;
|