@craft-native/ios 0.0.156 → 0.0.160
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 +10 -2
- package/dist/cli.js +15 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -10
- package/dist/testing.d.ts +7 -1
- package/package.json +1 -1
- package/templates/CraftHealth.swift +11 -0
- package/templates/CraftHybrid.swift +19 -0
- package/templates/CraftNativeScreen.swift +411 -46
- package/templates/testing/DriverTests.swift.template +161 -145
package/README.md
CHANGED
|
@@ -112,10 +112,15 @@ In Xcode:
|
|
|
112
112
|
|
|
113
113
|
### Experimental WebView-free screen
|
|
114
114
|
|
|
115
|
+
Native screens also support `ImageBackground`, which loads an image behind
|
|
116
|
+
ordinary native children while preserving the shared flex layout contract.
|
|
117
|
+
`KeyboardAvoidingView` also responds to the native keyboard with `padding`,
|
|
118
|
+
`position`, or `height` behavior.
|
|
119
|
+
|
|
115
120
|
Keyboard frame changes update the native controller's additional safe area, so
|
|
116
121
|
focused inputs and responsive `onLayout` measurements move with the keyboard.
|
|
117
122
|
|
|
118
|
-
The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `TextInput`, `Picker`, `Switch`, `Slider`, `ActivityIndicator`, `Image`, `Video`, `ScrollView`, and recycling `FlatList` nodes as UIKit controls. `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. `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `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).
|
|
123
|
+
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`, `ScrollView`, and recycling `FlatList` nodes as UIKit controls. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `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).
|
|
119
124
|
|
|
120
125
|
```bash
|
|
121
126
|
craft ios init NativeDemo --renderer native --output ./ios-native
|
|
@@ -274,7 +279,8 @@ The web view also: shows `alert`, `confirm` and `prompt` as system alerts;
|
|
|
274
279
|
opens `target=_blank` and `window.open` for the app's own origins in place and
|
|
275
280
|
anything else in `SFSafariViewController`; grants `getUserMedia` to the app's
|
|
276
281
|
own origin without WebKit's extra prompt; dismisses the keyboard with a drag;
|
|
277
|
-
allows picture in picture; ends its user agent with
|
|
282
|
+
allows picture in picture; ends its user agent with the builder package's
|
|
283
|
+
resolved `Craft/<version>
|
|
278
284
|
<AppName>/<version>`; brings a page whose content process died back under the
|
|
279
285
|
splash, on the page it was showing, at most three times a minute before
|
|
280
286
|
showing the offline page; and saves its `interactionState` when the app goes to
|
|
@@ -385,6 +391,8 @@ await health.requestAuthorization(['workouts', 'heartRate', 'sleep'], { write: f
|
|
|
385
391
|
const workouts = await health.getWorkouts({ startDate: Date.now() - 30 * 86400000 })
|
|
386
392
|
// One value per local day: sums, averages, the latest weight, hours asleep.
|
|
387
393
|
const hrv = await health.getDailyStatistics('heartRateVariability', { startDate: Date.now() - 30 * 86400000 })
|
|
394
|
+
// What a food log wrote (MyFitnessPal, Lose It!...): kcal, then grams of protein, carbohydrate and fat, summed per day.
|
|
395
|
+
const eaten = await health.getDailyStatistics('dietaryEnergy', { startDate: Date.now() - 7 * 86400000 })
|
|
388
396
|
// Every heart-rate reading in a range (bpm, start/end, source and device), oldest first.
|
|
389
397
|
const readings = await health.getHeartRateSamples({ startDate: Date.now() - 86400000 })
|
|
390
398
|
// The night as Apple Health recorded it: in bed, awake, and each asleep stage.
|
package/dist/cli.js
CHANGED
|
@@ -15,13 +15,22 @@ var templates = join(dirname(import.meta.dir), "templates/testing");
|
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
17
17
|
var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
function resolveCraftVersion(startDir = import.meta.dir) {
|
|
19
|
+
let directory = resolve(startDir);
|
|
20
|
+
while (true) {
|
|
21
|
+
try {
|
|
22
|
+
const packageJSON = JSON.parse(readFileSync(join2(directory, "package.json"), "utf8"));
|
|
23
|
+
if (packageJSON.name === "@craft-native/ios" && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
|
|
24
|
+
return packageJSON.version;
|
|
25
|
+
} catch {}
|
|
26
|
+
const parent = dirname2(directory);
|
|
27
|
+
if (parent === directory)
|
|
28
|
+
break;
|
|
29
|
+
directory = parent;
|
|
23
30
|
}
|
|
24
|
-
|
|
31
|
+
return "0.0.0";
|
|
32
|
+
}
|
|
33
|
+
var CRAFT_VERSION = resolveCraftVersion();
|
|
25
34
|
function normalizeNativePath(raw) {
|
|
26
35
|
if (typeof raw !== "string")
|
|
27
36
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates native iOS apps from web content using WKWebView.
|
|
5
5
|
*/
|
|
6
|
+
/** This package's version, which the app names in its user agent (`Craft/<version>`). */
|
|
7
|
+
export declare function resolveCraftVersion(startDir?: string): string;
|
|
6
8
|
export type CraftAppearance = 'light' | 'dark' | 'system';
|
|
7
9
|
export interface CraftConfig {
|
|
8
10
|
appName: string;
|
package/dist/index.js
CHANGED
|
@@ -66,9 +66,11 @@ function serializeSimulatorSteps(steps) {
|
|
|
66
66
|
throw new Error("Simulator identifiers must be nonempty single tokens");
|
|
67
67
|
return value;
|
|
68
68
|
};
|
|
69
|
-
|
|
69
|
+
const line = (step) => {
|
|
70
70
|
let arg = "";
|
|
71
|
-
if ("
|
|
71
|
+
if ("step" in step)
|
|
72
|
+
arg = `${token(step.id)} ${line(step.step)}`;
|
|
73
|
+
else if ("seconds" in step && !("text" in step))
|
|
72
74
|
arg = String(number(step.seconds));
|
|
73
75
|
else if ("milliseconds" in step)
|
|
74
76
|
arg = String(number(step.milliseconds));
|
|
@@ -100,8 +102,9 @@ function serializeSimulatorSteps(steps) {
|
|
|
100
102
|
if (/[\r\n]/.test(arg))
|
|
101
103
|
throw new Error("Simulator arguments cannot contain newlines");
|
|
102
104
|
return `${step.command}${arg ? ` ${arg}` : ""}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
+
};
|
|
106
|
+
return `${steps.map(line).join(`
|
|
107
|
+
`)}
|
|
105
108
|
`;
|
|
106
109
|
}
|
|
107
110
|
function createNativeMutationValidator() {
|
|
@@ -141,13 +144,22 @@ function createNativeMutationValidator() {
|
|
|
141
144
|
|
|
142
145
|
// src/index.ts
|
|
143
146
|
var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
function resolveCraftVersion(startDir = import.meta.dir) {
|
|
148
|
+
let directory = resolve(startDir);
|
|
149
|
+
while (true) {
|
|
150
|
+
try {
|
|
151
|
+
const packageJSON = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
|
|
152
|
+
if (packageJSON.name === "@craft-native/ios" && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
|
|
153
|
+
return packageJSON.version;
|
|
154
|
+
} catch {}
|
|
155
|
+
const parent = dirname2(directory);
|
|
156
|
+
if (parent === directory)
|
|
157
|
+
break;
|
|
158
|
+
directory = parent;
|
|
149
159
|
}
|
|
150
|
-
|
|
160
|
+
return "0.0.0";
|
|
161
|
+
}
|
|
162
|
+
var CRAFT_VERSION = resolveCraftVersion();
|
|
151
163
|
function normalizeNativePath(raw) {
|
|
152
164
|
if (typeof raw !== "string")
|
|
153
165
|
return null;
|
|
@@ -1274,6 +1286,7 @@ export {
|
|
|
1274
1286
|
run,
|
|
1275
1287
|
restoreShell,
|
|
1276
1288
|
resolveRuntimeDir,
|
|
1289
|
+
resolveCraftVersion,
|
|
1277
1290
|
resolveAppearance,
|
|
1278
1291
|
renderWatchEntitlements,
|
|
1279
1292
|
renderUsageDescriptions,
|
package/dist/testing.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type SimulatorStep = {
|
|
|
36
36
|
index: number;
|
|
37
37
|
text: string;
|
|
38
38
|
} | {
|
|
39
|
-
command: 'typeid';
|
|
39
|
+
command: 'typeid' | 'labelid';
|
|
40
40
|
id: string;
|
|
41
41
|
text: string;
|
|
42
42
|
} | {
|
|
@@ -52,6 +52,12 @@ export type SimulatorStep = {
|
|
|
52
52
|
} | {
|
|
53
53
|
command: 'openurl';
|
|
54
54
|
url: string;
|
|
55
|
+
}
|
|
56
|
+
/** Runs `step` only when the element with this identifier is there (a screen shown the first time). */
|
|
57
|
+
| {
|
|
58
|
+
command: 'ifid';
|
|
59
|
+
id: string;
|
|
60
|
+
step: SimulatorStep;
|
|
55
61
|
};
|
|
56
62
|
/** Typed journeys use the same wire protocol as existing line-based scripts. */
|
|
57
63
|
export declare function serializeSimulatorSteps(steps: readonly SimulatorStep[]): string;
|
package/package.json
CHANGED
|
@@ -106,6 +106,12 @@ final class CraftHealth {
|
|
|
106
106
|
if let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) {
|
|
107
107
|
readTypes.insert(sleepType)
|
|
108
108
|
}
|
|
109
|
+
case "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat":
|
|
110
|
+
// What a food log (MyFitnessPal, Lose It!, Cronometer...)
|
|
111
|
+
// writes to Apple Health: read only, never written back.
|
|
112
|
+
if let dietaryType = Self.healthQuantity(type)?.type {
|
|
113
|
+
readTypes.insert(dietaryType)
|
|
114
|
+
}
|
|
109
115
|
default:
|
|
110
116
|
break
|
|
111
117
|
}
|
|
@@ -261,6 +267,11 @@ final class CraftHealth {
|
|
|
261
267
|
case "restingHeartRate": return (HKQuantityType.quantityType(forIdentifier: .restingHeartRate), bpm, .discreteAverage)
|
|
262
268
|
case "heartRateVariability": return (HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN), .secondUnit(with: .milli), .discreteAverage)
|
|
263
269
|
case "bodyMass": return (HKQuantityType.quantityType(forIdentifier: .bodyMass), .gramUnit(with: .kilo), .mostRecent)
|
|
270
|
+
// A day's food, summed over every meal any app logged: kcal and grams.
|
|
271
|
+
case "dietaryEnergy": return (HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed), .kilocalorie(), .cumulativeSum)
|
|
272
|
+
case "dietaryProtein": return (HKQuantityType.quantityType(forIdentifier: .dietaryProtein), .gram(), .cumulativeSum)
|
|
273
|
+
case "dietaryCarbohydrates": return (HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates), .gram(), .cumulativeSum)
|
|
274
|
+
case "dietaryFat": return (HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal), .gram(), .cumulativeSum)
|
|
264
275
|
default: return nil
|
|
265
276
|
}
|
|
266
277
|
}
|
|
@@ -1457,6 +1457,19 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1457
1457
|
return popRecognizers(of: nav).contains { $0 === recognizer }
|
|
1458
1458
|
}
|
|
1459
1459
|
|
|
1460
|
+
private func nativeScreenContainsHorizontalScroll(_ screen: CraftNativeScreenController) -> Bool {
|
|
1461
|
+
guard let root = screen.viewIfLoaded else { return false }
|
|
1462
|
+
func visit(_ view: UIView) -> Bool {
|
|
1463
|
+
if let scroll = view as? UIScrollView,
|
|
1464
|
+
scroll.isScrollEnabled,
|
|
1465
|
+
scroll.alwaysBounceHorizontal || scroll.contentSize.width > scroll.bounds.width + 1 {
|
|
1466
|
+
return true
|
|
1467
|
+
}
|
|
1468
|
+
return view.subviews.contains(where: visit)
|
|
1469
|
+
}
|
|
1470
|
+
return visit(root)
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1460
1473
|
/// The edge swipe goes ahead of the web view's own gestures, which
|
|
1461
1474
|
/// otherwise hold every touch over the page until the page has answered it.
|
|
1462
1475
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
@@ -1470,6 +1483,12 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1470
1483
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
1471
1484
|
guard let nav = visibleStack, isPopRecognizer(gestureRecognizer),
|
|
1472
1485
|
nav.viewControllers.count > 1, nav.transitionCoordinator == nil else { return false }
|
|
1486
|
+
if let native = nav.topViewController as? CraftNativeScreenController,
|
|
1487
|
+
gestureRecognizer !== nav.interactivePopGestureRecognizer,
|
|
1488
|
+
nativeScreenContainsHorizontalScroll(native) {
|
|
1489
|
+
let moved = (gestureRecognizer as? UIPanGestureRecognizer)?.translation(in: nav.view).x ?? 0
|
|
1490
|
+
if gestureRecognizer.location(in: nav.view).x - moved > 32 { return false }
|
|
1491
|
+
}
|
|
1473
1492
|
guard let slot = nav.topViewController as? CraftHybridWebSlot else { return true }
|
|
1474
1493
|
// Over the page, only from its left edge: a swipe across the page is
|
|
1475
1494
|
// the page's (a carousel, a slider).
|