craft-native 0.0.155 → 0.0.158
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/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +42 -8
- package/dist/ios/src/index.js +48 -13
- package/dist/ios/src/testing.d.ts +7 -1
- package/dist/ios/templates/CraftApp.swift +114 -18
- package/dist/ios/templates/CraftHybrid.swift +269 -31
- package/dist/ios/templates/CraftNativeActions.swift +57 -3
- package/dist/ios/templates/CraftNativeScreen.swift +98 -22
- package/dist/ios/templates/testing/DriverTests.swift.template +161 -145
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.158";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -83,6 +83,14 @@ export interface CraftConfig {
|
|
|
83
83
|
* Each replaces the generic sentence Craft writes for that key.
|
|
84
84
|
*/
|
|
85
85
|
usageDescriptions?: Record<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* The app's site. With `renderer: "web"` it is the page the web view loads.
|
|
88
|
+
* With `renderer: "native"` and `nativeScreens` there is no web view: its
|
|
89
|
+
* path is the path the app starts on (`/` when unset), and a path no native
|
|
90
|
+
* screen draws (`craft.navigation.open('/terms')`, or a link to this host)
|
|
91
|
+
* opens in the in-app browser against its origin. Native screens read it as
|
|
92
|
+
* `__stxNativeBridge.appURL`.
|
|
93
|
+
*/
|
|
86
94
|
devServerURL?: string;
|
|
87
95
|
iosVersion?: string;
|
|
88
96
|
watchosVersion?: string;
|
|
@@ -168,16 +176,32 @@ export interface CraftConfig {
|
|
|
168
176
|
orientations?: Array<'portrait' | 'landscape-left' | 'landscape-right' | 'portrait-upside-down'>;
|
|
169
177
|
deviceFamilies?: Array<'iphone' | 'ipad'>;
|
|
170
178
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
179
|
+
* App paths drawn as native stx screens, each to the screen in the native
|
|
180
|
+
* bundle that draws it, e.g. `{ "/m": "Today", "/m/calendar": "Calendar" }`.
|
|
181
|
+
* A segment may be a parameter (`/m/workout/:id`) and the last one `*`.
|
|
182
|
+
* Needs `nativeBundle`.
|
|
183
|
+
*
|
|
184
|
+
* With `renderer: "web"` (hybrid) every other path stays in the web view,
|
|
185
|
+
* and a path whose screen is missing or throws falls back to it.
|
|
186
|
+
*
|
|
187
|
+
* With `renderer: "native"` the app is all native, with the same tabs and
|
|
188
|
+
* path routing and no web view at all: every tab id and the start path (the
|
|
189
|
+
* path of `devServerURL`) must be a native screen's, and any other path
|
|
190
|
+
* opens in the in-app browser on the app's site.
|
|
177
191
|
*/
|
|
178
192
|
nativeScreens?: Record<string, string>;
|
|
179
|
-
/**
|
|
193
|
+
/** The bundle `stx native compile --format bundle` wrote, copied to `dist/native-screen.js`. */
|
|
180
194
|
nativeBundle?: string;
|
|
195
|
+
/**
|
|
196
|
+
* The bundle route of a screen-less script that runs once for the app's
|
|
197
|
+
* whole life, beside the screens (Stacks names it `__app`). It hears the
|
|
198
|
+
* app's events (`craftNotificationResponse`, `craftNotificationReceived`,
|
|
199
|
+
* `craftSilentPush`, `craftBackgroundRefresh`, `craftShortcut`,
|
|
200
|
+
* `craftSiriShortcut`) as `CRAFT_EVENT` messages instead of the page, every
|
|
201
|
+
* `APP_STATE`, and can open paths with `craft.navigation.open`. Needs
|
|
202
|
+
* `nativeScreens` or `renderer: "native"`, and the native bundle.
|
|
203
|
+
*/
|
|
204
|
+
nativeApp?: string;
|
|
181
205
|
/**
|
|
182
206
|
* The native tab bar, shown from the first frame, before the page loads.
|
|
183
207
|
* The page's own `tabBar.set` replaces it later. Each `id` is the tab's
|
|
@@ -214,8 +238,18 @@ export interface NativePathMatch {
|
|
|
214
238
|
export declare function matchNativePath(nativeScreens: Record<string, string> | undefined, raw: string): NativePathMatch | null;
|
|
215
239
|
/** Whether a config mixes native screens into its web page. */
|
|
216
240
|
export declare function isHybrid(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
241
|
+
/** Whether a config is an all-native app with routes: `renderer: "native"` and `nativeScreens`, tabs and paths with no web view. */
|
|
242
|
+
export declare function hasNativeRoutes(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
243
|
+
/** Whether a config draws any native screen, and so needs `dist/native-screen.js`. */
|
|
244
|
+
export declare function usesNativeBundle(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
245
|
+
/**
|
|
246
|
+
* The path the app starts on: the path of `devServerURL` (with its query),
|
|
247
|
+
* `/` when there is none. The shell reads it the same way
|
|
248
|
+
* (`CraftHybridRoutes.appPath`).
|
|
249
|
+
*/
|
|
250
|
+
export declare function nativeStartPath(config: Pick<CraftConfig, 'devServerURL'>): string;
|
|
217
251
|
/** What is wrong with a config's hybrid and tab settings, one sentence each. */
|
|
218
|
-
export declare function hybridConfigProblems(config: Pick<CraftConfig, 'renderer' | 'nativeScreens' | 'tabs' | 'shareStorage'>): string[];
|
|
252
|
+
export declare function hybridConfigProblems(config: Pick<CraftConfig, 'renderer' | 'nativeScreens' | 'tabs' | 'shareStorage' | 'devServerURL' | 'nativeApp'>): string[];
|
|
219
253
|
export interface CraftBackgroundRefresh {
|
|
220
254
|
enabled: boolean;
|
|
221
255
|
/** The BGTaskScheduler identifier. Default `<bundleId>.refresh`. */
|
package/dist/ios/src/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() {
|
|
@@ -228,11 +231,25 @@ function matchNativePath(nativeScreens, raw) {
|
|
|
228
231
|
function isHybrid(config) {
|
|
229
232
|
return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
230
233
|
}
|
|
234
|
+
function hasNativeRoutes(config) {
|
|
235
|
+
return config.renderer === "native" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
236
|
+
}
|
|
237
|
+
function usesNativeBundle(config) {
|
|
238
|
+
return config.renderer === "native" || Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
239
|
+
}
|
|
240
|
+
function nativeStartPath(config) {
|
|
241
|
+
if (!config.devServerURL)
|
|
242
|
+
return "/";
|
|
243
|
+
try {
|
|
244
|
+
const url = new URL(config.devServerURL);
|
|
245
|
+
return `${url.pathname || "/"}${url.search}`;
|
|
246
|
+
} catch {
|
|
247
|
+
return "/";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
231
250
|
function hybridConfigProblems(config) {
|
|
232
251
|
const problems = [];
|
|
233
252
|
const screens = Object.entries(config.nativeScreens ?? {});
|
|
234
|
-
if (screens.length && config.renderer === "native")
|
|
235
|
-
problems.push('nativeScreens mixes native screens into a web app; it needs renderer: "web" (renderer: "native" is all native already).');
|
|
236
253
|
for (const [pattern, screen] of screens) {
|
|
237
254
|
if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
|
|
238
255
|
problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
|
|
@@ -254,6 +271,21 @@ function hybridConfigProblems(config) {
|
|
|
254
271
|
if (!key || typeof target !== "string" || !target)
|
|
255
272
|
problems.push(`shareStorage "${key}" needs the Keychain key native screens read it under.`);
|
|
256
273
|
}
|
|
274
|
+
if (hasNativeRoutes(config)) {
|
|
275
|
+
for (const id of ids) {
|
|
276
|
+
if (!matchNativePath(config.nativeScreens, id))
|
|
277
|
+
problems.push(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
|
|
278
|
+
}
|
|
279
|
+
const start = nativeStartPath(config);
|
|
280
|
+
if (!matchNativePath(config.nativeScreens, start))
|
|
281
|
+
problems.push(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
|
|
282
|
+
}
|
|
283
|
+
if (config.nativeApp !== undefined) {
|
|
284
|
+
if (typeof config.nativeApp !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(config.nativeApp))
|
|
285
|
+
problems.push(`nativeApp "${String(config.nativeApp)}" must be a route name from the native bundle.`);
|
|
286
|
+
else if (!usesNativeBundle(config))
|
|
287
|
+
problems.push('nativeApp runs in the native bundle; it needs nativeScreens or renderer: "native".');
|
|
288
|
+
}
|
|
257
289
|
return problems;
|
|
258
290
|
}
|
|
259
291
|
function installNativeBundle(source, output) {
|
|
@@ -1044,11 +1076,13 @@ ${nativeDependencies.join(`
|
|
|
1044
1076
|
writeFileSync2(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1045
1077
|
}
|
|
1046
1078
|
writeFileSync2(join2(output, "dist", "index.html"), renderOfflinePage(config));
|
|
1047
|
-
if (
|
|
1079
|
+
if (usesNativeBundle(config)) {
|
|
1048
1080
|
if (config.nativeBundle)
|
|
1049
1081
|
installNativeBundle(config.nativeBundle, output);
|
|
1050
|
-
else
|
|
1082
|
+
else if (isHybrid(config))
|
|
1051
1083
|
console.warn(" \u26A0 nativeScreens is set without nativeBundle: every path opens in the web view until `craft ios build --native-bundle` supplies one.");
|
|
1084
|
+
else if (hasNativeRoutes(config))
|
|
1085
|
+
console.warn(" \u26A0 nativeScreens is set without nativeBundle: the app has no screens until `craft ios build --native-bundle` supplies one.");
|
|
1052
1086
|
}
|
|
1053
1087
|
console.log("\u2705 Project initialized");
|
|
1054
1088
|
console.log("");
|
|
@@ -1080,11 +1114,9 @@ async function build(options) {
|
|
|
1080
1114
|
if (htmlPath || devServer) {
|
|
1081
1115
|
throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
|
|
1082
1116
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
cpSync(nativeBundlePath, destination);
|
|
1087
|
-
}
|
|
1117
|
+
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
1118
|
+
if (bundle)
|
|
1119
|
+
installNativeBundle(bundle, output);
|
|
1088
1120
|
if (!existsSync(join2(output, "dist", "native-screen.js"))) {
|
|
1089
1121
|
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
1090
1122
|
}
|
|
@@ -1238,6 +1270,7 @@ async function run(options) {
|
|
|
1238
1270
|
}
|
|
1239
1271
|
}
|
|
1240
1272
|
export {
|
|
1273
|
+
usesNativeBundle,
|
|
1241
1274
|
syncWebAssets,
|
|
1242
1275
|
showSimulator,
|
|
1243
1276
|
serializeSimulatorSteps,
|
|
@@ -1266,12 +1299,14 @@ export {
|
|
|
1266
1299
|
orderSimulators,
|
|
1267
1300
|
open,
|
|
1268
1301
|
normalizeNativePath,
|
|
1302
|
+
nativeStartPath,
|
|
1269
1303
|
matchNativePath,
|
|
1270
1304
|
isHybrid,
|
|
1271
1305
|
installRuntime,
|
|
1272
1306
|
initSimulatorDriver,
|
|
1273
1307
|
init,
|
|
1274
1308
|
hybridConfigProblems,
|
|
1309
|
+
hasNativeRoutes,
|
|
1275
1310
|
createNativeMutationValidator,
|
|
1276
1311
|
build,
|
|
1277
1312
|
bootSimulator,
|
|
@@ -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;
|
|
@@ -195,8 +195,14 @@ struct CraftRecoveryBudget {
|
|
|
195
195
|
final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
|
196
196
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
197
197
|
UNUserNotificationCenter.current().delegate = self
|
|
198
|
+
let config = CraftConfig.bundled()
|
|
198
199
|
// Before launch finishes, which is the only time iOS accepts it.
|
|
199
|
-
CraftBackgroundWork.shared.registerRefresh(
|
|
200
|
+
CraftBackgroundWork.shared.registerRefresh(config)
|
|
201
|
+
// The shell, the app script and an all-native app's services now,
|
|
202
|
+
// not when SwiftUI first draws: a launch in the background (a silent
|
|
203
|
+
// push, a refresh, a route recording's location) draws nothing, and
|
|
204
|
+
// its events still need someone to hear them.
|
|
205
|
+
CraftHybrid.shared.configure(config)
|
|
200
206
|
if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
|
|
201
207
|
CraftEventManager.shared.handleShortcut(shortcut)
|
|
202
208
|
// Returning false tells UIKit the launch-time item was handled and
|
|
@@ -269,12 +275,32 @@ final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificatio
|
|
|
269
275
|
}
|
|
270
276
|
|
|
271
277
|
// MARK: - Native Event Manager
|
|
278
|
+
|
|
279
|
+
/// The app's own events: a notification tapped or arriving, a silent push,
|
|
280
|
+
/// background refresh, a home-screen or Siri shortcut. They go to the page,
|
|
281
|
+
/// or, in an app with an app script (`nativeApp`, CraftHybrid.swift), to that
|
|
282
|
+
/// script as `CRAFT_EVENT { event, detail }` instead. Either way an event
|
|
283
|
+
/// that arrives before its listener can hear it (a tap that launched the app)
|
|
284
|
+
/// waits for it, in order.
|
|
272
285
|
class CraftEventManager {
|
|
273
286
|
static let shared = CraftEventManager()
|
|
274
287
|
|
|
288
|
+
/// Who hears the app's events. Undecided until the shell is configured
|
|
289
|
+
/// at launch, which may be after the first event (a launch shortcut).
|
|
290
|
+
enum Listener {
|
|
291
|
+
case undecided
|
|
292
|
+
case page
|
|
293
|
+
case appScript
|
|
294
|
+
/// An all-native app with no app script: nothing hears them.
|
|
295
|
+
case none
|
|
296
|
+
}
|
|
297
|
+
|
|
275
298
|
private weak var webView: WKWebView?
|
|
276
299
|
private var isReady = false
|
|
300
|
+
private var listener: Listener = .undecided
|
|
277
301
|
private var pendingEvents: [(name: String, data: [String: Any])] = []
|
|
302
|
+
/// The app script's delivery, once its bundle has run.
|
|
303
|
+
private var appScriptSink: ((String, [String: Any]) -> Void)?
|
|
278
304
|
|
|
279
305
|
private init() {}
|
|
280
306
|
|
|
@@ -282,12 +308,35 @@ class CraftEventManager {
|
|
|
282
308
|
self.webView = webView
|
|
283
309
|
}
|
|
284
310
|
|
|
311
|
+
/// From `CraftHybrid.configure`: whether an app script hears the app's
|
|
312
|
+
/// events, and whether there is a page to hear them otherwise.
|
|
313
|
+
func configure(appScript: Bool, page: Bool) {
|
|
314
|
+
listener = appScript ? .appScript : (page ? .page : .none)
|
|
315
|
+
guard listener == .none else { return }
|
|
316
|
+
// Nothing will ever hear what is waiting; iOS gets its answer now
|
|
317
|
+
// rather than at the background deadline.
|
|
318
|
+
let events = pendingEvents
|
|
319
|
+
pendingEvents.removeAll()
|
|
320
|
+
for event in events { drop(event.name, data: event.data) }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// The app script has run its bundle: whatever waited for it, in order.
|
|
324
|
+
func appScriptReady(_ sink: @escaping (String, [String: Any]) -> Void) {
|
|
325
|
+
appScriptSink = sink
|
|
326
|
+
guard listener == .appScript else { return }
|
|
327
|
+
let events = pendingEvents
|
|
328
|
+
pendingEvents.removeAll()
|
|
329
|
+
for event in events { sink(event.name, event.data) }
|
|
330
|
+
}
|
|
331
|
+
|
|
285
332
|
func setLoading() {
|
|
286
333
|
isReady = false
|
|
287
334
|
}
|
|
288
335
|
|
|
289
336
|
func setReady() {
|
|
290
337
|
isReady = true
|
|
338
|
+
// Held for the app script, which the page's readiness says nothing about.
|
|
339
|
+
guard listener != .appScript else { return }
|
|
291
340
|
let events = pendingEvents
|
|
292
341
|
pendingEvents.removeAll()
|
|
293
342
|
for event in events {
|
|
@@ -338,13 +387,37 @@ class CraftEventManager {
|
|
|
338
387
|
return true
|
|
339
388
|
}
|
|
340
389
|
|
|
341
|
-
/// Dispatch now if the page can hear it, or hold it
|
|
390
|
+
/// Dispatch now if the page (or the app script) can hear it, or hold it
|
|
391
|
+
/// until it can. The name says "web" for the page it was written for; the
|
|
392
|
+
/// event names are spelled at its call sites, where the conformance test
|
|
393
|
+
/// reads the vocabulary from.
|
|
342
394
|
func sendToWeb(_ event: String, data: [String: Any]) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
395
|
+
switch listener {
|
|
396
|
+
case .appScript:
|
|
397
|
+
guard let appScriptSink else {
|
|
398
|
+
pendingEvents.append((event, data))
|
|
399
|
+
// Background work cannot wait for a first frame that a
|
|
400
|
+
// launch in the background never draws.
|
|
401
|
+
if event == "craftSilentPush" || event == "craftBackgroundRefresh" { CraftHybrid.shared.backgroundWorkWaiting() }
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
appScriptSink(event, data)
|
|
405
|
+
case .none:
|
|
406
|
+
drop(event, data: data)
|
|
407
|
+
case .page, .undecided:
|
|
408
|
+
guard listener == .page, isReady, webView != nil else {
|
|
409
|
+
pendingEvents.append((event, data))
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
dispatch(event, data: data)
|
|
346
413
|
}
|
|
347
|
-
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// An event no one will hear: logged, and background work it holds is
|
|
417
|
+
/// answered at once.
|
|
418
|
+
private func drop(_ event: String, data: [String: Any]) {
|
|
419
|
+
NSLog("[craft] %@: the app has no page and no app script (nativeApp) to hear it", event)
|
|
420
|
+
if let id = data["id"] as? String { CraftBackgroundWork.shared.release(id) }
|
|
348
421
|
}
|
|
349
422
|
|
|
350
423
|
private func dispatch(_ event: String, data: [String: Any]) {
|
|
@@ -363,7 +436,9 @@ class CraftEventManager {
|
|
|
363
436
|
/// The page's share of background time: a silent push (`craftSilentPush`) and
|
|
364
437
|
/// iOS's scheduled app refresh (`craftBackgroundRefresh`). Each holds iOS's
|
|
365
438
|
/// completion until the page calls `craft.background.complete(ok)` (with the
|
|
366
|
-
/// event's `id`, or with none for everything pending), or for 25 seconds.
|
|
439
|
+
/// event's `id`, or with none for everything pending), or for 25 seconds. In
|
|
440
|
+
/// an app with an app script the script hears both and answers the same way,
|
|
441
|
+
/// through the native `Background.complete` (CraftNativeActions.swift).
|
|
367
442
|
///
|
|
368
443
|
/// The page has to be alive to do the work: when iOS wakes an app it had
|
|
369
444
|
/// suspended, it is; when it launches one it had ended, the event waits for
|
|
@@ -400,6 +475,12 @@ final class CraftBackgroundWork {
|
|
|
400
475
|
}
|
|
401
476
|
}
|
|
402
477
|
|
|
478
|
+
/// Work no one will do (nothing hears its event): iOS is answered now,
|
|
479
|
+
/// as the deadline would have.
|
|
480
|
+
func release(_ id: String) {
|
|
481
|
+
end(id, ok: nil)
|
|
482
|
+
}
|
|
483
|
+
|
|
403
484
|
private func end(_ id: String, ok: Bool?) {
|
|
404
485
|
guard let finish = pending.removeValue(forKey: id) else { return }
|
|
405
486
|
finish(ok)
|
|
@@ -461,11 +542,12 @@ struct CraftApp: App {
|
|
|
461
542
|
var body: some SwiftUI.Scene {
|
|
462
543
|
WindowGroup {
|
|
463
544
|
Group {
|
|
464
|
-
if
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
//
|
|
468
|
-
//
|
|
545
|
+
if CraftHybrid.shared.isEnabled {
|
|
546
|
+
// Native screens by path, with tabs (CraftHybrid.swift):
|
|
547
|
+
// the native screen for the start path at once, and in a
|
|
548
|
+
// web app (hybrid) the page warm behind it. An all-native
|
|
549
|
+
// app (renderer "native" with nativeScreens) has the same
|
|
550
|
+
// shell and no web view at all.
|
|
469
551
|
ZStack(alignment: .bottom) {
|
|
470
552
|
CraftColorSchemeObserver()
|
|
471
553
|
CraftHybridRoot(config: appState.config)
|
|
@@ -477,6 +559,9 @@ struct CraftApp: App {
|
|
|
477
559
|
// stack kept out of the keyboard's way and carried the bar
|
|
478
560
|
// up above it, over the field being typed in.
|
|
479
561
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
|
562
|
+
} else if appState.config.renderer == "native" {
|
|
563
|
+
// One native screen and its stack: no paths, no tabs.
|
|
564
|
+
CraftNativeScreen(config: appState.config)
|
|
480
565
|
} else {
|
|
481
566
|
// The page's own chrome, drawn natively over it: the tab
|
|
482
567
|
// bar the page asks for, and the splash until it is ready.
|
|
@@ -927,10 +1012,13 @@ class DeepLinkManager {
|
|
|
927
1012
|
}
|
|
928
1013
|
|
|
929
1014
|
// A hybrid app opens a native path's screen itself, and tells the
|
|
930
|
-
// native screens that listen; every other link is the page's.
|
|
1015
|
+
// native screens that listen; every other link is the page's. An
|
|
1016
|
+
// all-native app has no page: its native screens hear every link.
|
|
931
1017
|
if CraftHybrid.shared.isEnabled {
|
|
932
1018
|
if CraftHybrid.shared.handleDeepLink(url) {
|
|
933
1019
|
dispatchNative(url, initial: false)
|
|
1020
|
+
} else if !CraftHybrid.shared.hasWeb {
|
|
1021
|
+
if nativeListeners.isEmpty { pendingURLs.append(url) } else { dispatchNative(url, initial: false) }
|
|
934
1022
|
} else if isReady && webView != nil {
|
|
935
1023
|
dispatchDeepLink(url, initial: false)
|
|
936
1024
|
} else {
|
|
@@ -958,7 +1046,8 @@ class DeepLinkManager {
|
|
|
958
1046
|
nativeListeners[token] = listener
|
|
959
1047
|
// In a hybrid app the page is the one waiting for the links that
|
|
960
1048
|
// arrived early; a native screen hears only native ones, as they come.
|
|
961
|
-
|
|
1049
|
+
// An all-native app has no page, so its first screen hears them.
|
|
1050
|
+
if CraftHybrid.shared.isEnabled && CraftHybrid.shared.hasWeb { return token }
|
|
962
1051
|
let firstNativeScreen = !hasBeenReady
|
|
963
1052
|
hasBeenReady = true
|
|
964
1053
|
let urls = pendingURLs
|
|
@@ -1027,7 +1116,8 @@ class AppState: ObservableObject {
|
|
|
1027
1116
|
self.config = CraftConfig()
|
|
1028
1117
|
}
|
|
1029
1118
|
// Before the first frame: the hybrid start screen, and the tab bar
|
|
1030
|
-
// from the config, are decided here
|
|
1119
|
+
// from the config, are decided here, if the app delegate has not
|
|
1120
|
+
// already (it does at launch; the first call decides).
|
|
1031
1121
|
CraftHybrid.shared.configure(config)
|
|
1032
1122
|
}
|
|
1033
1123
|
}
|
|
@@ -1149,6 +1239,9 @@ struct CraftConfig: Codable {
|
|
|
1149
1239
|
/// Hybrid: web-storage keys the page's own code writes, mirrored into the
|
|
1150
1240
|
/// Keychain under another name for native screens (`{"auth_token": "auth.token"}`).
|
|
1151
1241
|
var shareStorage: [String: String]? = nil
|
|
1242
|
+
/// The bundle route of the app script: a screen-less script that runs for
|
|
1243
|
+
/// the app's whole life and hears the app's events (CraftHybrid.swift).
|
|
1244
|
+
var nativeApp: String? = nil
|
|
1152
1245
|
|
|
1153
1246
|
struct Tab: Codable {
|
|
1154
1247
|
var id: String
|
|
@@ -8500,8 +8593,10 @@ final class CraftZigRuntime: NSObject {
|
|
|
8500
8593
|
/// started on a page can be followed on a native screen.
|
|
8501
8594
|
///
|
|
8502
8595
|
/// The coordinator exists wherever the web view does: always in a hybrid app.
|
|
8503
|
-
///
|
|
8504
|
-
///
|
|
8596
|
+
/// An app with no web view (renderer "native") has one all the same, without
|
|
8597
|
+
/// a web view (`CraftHybrid.configure`), so its native screens have these
|
|
8598
|
+
/// services too. Calls go to the Swift dispatcher; a build that links the Zig
|
|
8599
|
+
/// runtime keeps the page's calls on Zig.
|
|
8505
8600
|
enum CraftWebServices {
|
|
8506
8601
|
private static var waiting: [String: (Result<Any, CraftNativeActionError>) -> Void] = [:]
|
|
8507
8602
|
/// Coordinator events a native screen hears too.
|
|
@@ -8558,7 +8653,8 @@ enum CraftWebServices {
|
|
|
8558
8653
|
/// how this codebase accumulated five envelopes.
|
|
8559
8654
|
@objc(CraftSwiftShim)
|
|
8560
8655
|
final class CraftSwiftShim: NSObject {
|
|
8561
|
-
/// The coordinator serving hand-offs. Set by `makeCoordinator
|
|
8656
|
+
/// The coordinator serving hand-offs. Set by `makeCoordinator`, or in an
|
|
8657
|
+
/// app with no web view by `CraftHybrid.configure`, which holds it.
|
|
8562
8658
|
static weak var coordinator: CraftWebView.Coordinator?
|
|
8563
8659
|
|
|
8564
8660
|
private typealias DeliverResultFn = @convention(c) (
|