craft-native 0.0.158 → 0.0.161
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/android/templates/CraftNative.kt.template +62 -1
- package/dist/android/templates/CraftNativeCapabilities.kt.template +311 -4
- package/dist/android/templates/MainActivityNative.kt.template +638 -55
- package/dist/api/mobile.d.ts +57 -4
- 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 +2 -0
- package/dist/ios/src/index.js +16 -6
- package/dist/ios/templates/CraftApp.swift +24 -0
- package/dist/ios/templates/CraftHealth.swift +123 -0
- package/dist/ios/templates/CraftHybrid.swift +19 -0
- package/dist/ios/templates/CraftNativeScreen.swift +501 -58
- package/dist/mobile.js +12 -0
- package/package.json +1 -1
package/dist/api/mobile.d.ts
CHANGED
|
@@ -1238,9 +1238,13 @@ export declare const pushNotifications: {
|
|
|
1238
1238
|
*/
|
|
1239
1239
|
onReceive(callback: (data: Record<string, unknown>) => void): () => void;
|
|
1240
1240
|
};
|
|
1241
|
-
export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep'
|
|
1241
|
+
export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep'
|
|
1242
|
+
/** Food logged by any app, per day: kcal eaten, then grams of protein, carbohydrate and fat. Read only. */
|
|
1243
|
+
| 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat'
|
|
1244
|
+
/** Mindful minutes (meditation, breathing): read with getMindfulSessions, written with saveMindfulSession. */
|
|
1245
|
+
| 'mindful';
|
|
1242
1246
|
/** The types with one value per day. `sleep` is hours asleep, counted on the day you woke. */
|
|
1243
|
-
export type HealthDailyType = Exclude<HealthDataType, 'workouts'>;
|
|
1247
|
+
export type HealthDailyType = Exclude<HealthDataType, 'workouts' | 'mindful'>;
|
|
1244
1248
|
export interface HealthAuthorizationOptions {
|
|
1245
1249
|
/** `false` asks to read only. Default: also allowed to save workouts. */
|
|
1246
1250
|
write?: boolean;
|
|
@@ -1348,8 +1352,49 @@ export interface HealthSleepInterval {
|
|
|
1348
1352
|
sourceName: string;
|
|
1349
1353
|
deviceName?: string;
|
|
1350
1354
|
}
|
|
1351
|
-
/**
|
|
1352
|
-
export
|
|
1355
|
+
/** A meditation or breathing session to record as mindful minutes. */
|
|
1356
|
+
export interface HealthMindfulSession {
|
|
1357
|
+
/**
|
|
1358
|
+
* The app's own id for the session (up to 128 characters), kept as the
|
|
1359
|
+
* sample's external UUID. Saving the same id again keeps the first sample.
|
|
1360
|
+
*/
|
|
1361
|
+
id: string;
|
|
1362
|
+
/** Epoch milliseconds. */
|
|
1363
|
+
startDate: number;
|
|
1364
|
+
/** Epoch milliseconds, after `startDate` and at most a day later. */
|
|
1365
|
+
endDate: number;
|
|
1366
|
+
}
|
|
1367
|
+
export interface HealthMindfulSessionResult {
|
|
1368
|
+
/** HealthKit's UUID for the sample. */
|
|
1369
|
+
id: string;
|
|
1370
|
+
/** The id it was saved with. */
|
|
1371
|
+
externalId: string;
|
|
1372
|
+
/** True when this id was saved before and that sample was kept. */
|
|
1373
|
+
existing: boolean;
|
|
1374
|
+
}
|
|
1375
|
+
export interface HealthMindfulSessionQuery extends HealthSampleQuery {
|
|
1376
|
+
/** At most this many, oldest first. Defaults to 1000, capped at 5000. Start defaults to 30 days ago. */
|
|
1377
|
+
limit?: number;
|
|
1378
|
+
}
|
|
1379
|
+
/** A mindful session in Apple Health, from any app that writes them. */
|
|
1380
|
+
export interface HealthMindfulSample {
|
|
1381
|
+
/** HealthKit's UUID. */
|
|
1382
|
+
id: string;
|
|
1383
|
+
/** Epoch milliseconds. */
|
|
1384
|
+
startDate: number;
|
|
1385
|
+
/** Epoch milliseconds. */
|
|
1386
|
+
endDate: number;
|
|
1387
|
+
durationSeconds: number;
|
|
1388
|
+
sourceName: string;
|
|
1389
|
+
sourceBundleId: string;
|
|
1390
|
+
/** True when this app wrote it. */
|
|
1391
|
+
ownApp: boolean;
|
|
1392
|
+
/** The id the writing app saved it with, when it gave one. */
|
|
1393
|
+
externalId?: string;
|
|
1394
|
+
deviceName?: string;
|
|
1395
|
+
}
|
|
1396
|
+
/** The health reads and writes a host may or may not have, depending on the app build it runs in. */
|
|
1397
|
+
export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveMindfulSession' | 'getMindfulSessions';
|
|
1353
1398
|
export declare const health: {
|
|
1354
1399
|
/**
|
|
1355
1400
|
* Whether this app build answers `method`. An app installed before a read
|
|
@@ -1372,6 +1417,14 @@ export declare const health: {
|
|
|
1372
1417
|
getHeartRateSamples(options?: HealthHeartRateQuery): Promise<HealthHeartRateSample[]>;
|
|
1373
1418
|
/** Sleep between two instants, oldest first: time in bed and each asleep or awake stage. iOS only for now. */
|
|
1374
1419
|
getSleepIntervals(options?: HealthSampleQuery): Promise<HealthSleepInterval[]>;
|
|
1420
|
+
/**
|
|
1421
|
+
* Record a meditation or breathing session as mindful minutes. Needs the
|
|
1422
|
+
* `mindful` grant with writing allowed. Saving the same `id` again keeps
|
|
1423
|
+
* the first sample. iOS only for now.
|
|
1424
|
+
*/
|
|
1425
|
+
saveMindfulSession(session: HealthMindfulSession): Promise<HealthMindfulSessionResult>;
|
|
1426
|
+
/** Mindful sessions between two instants, oldest first, each marked `ownApp` when this app wrote it. iOS only for now. */
|
|
1427
|
+
getMindfulSessions(options?: HealthMindfulSessionQuery): Promise<HealthMindfulSample[]>;
|
|
1375
1428
|
};
|
|
1376
1429
|
export interface LiveActivityState {
|
|
1377
1430
|
status?: string;
|
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.161";
|
|
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
|
@@ -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/ios/src/index.js
CHANGED
|
@@ -144,13 +144,22 @@ function createNativeMutationValidator() {
|
|
|
144
144
|
|
|
145
145
|
// ../ios/src/index.ts
|
|
146
146
|
var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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;
|
|
152
159
|
}
|
|
153
|
-
|
|
160
|
+
return "0.0.0";
|
|
161
|
+
}
|
|
162
|
+
var CRAFT_VERSION = resolveCraftVersion();
|
|
154
163
|
function normalizeNativePath(raw) {
|
|
155
164
|
if (typeof raw !== "string")
|
|
156
165
|
return null;
|
|
@@ -1277,6 +1286,7 @@ export {
|
|
|
1277
1286
|
run,
|
|
1278
1287
|
restoreShell,
|
|
1279
1288
|
resolveRuntimeDir,
|
|
1289
|
+
resolveCraftVersion,
|
|
1280
1290
|
resolveAppearance,
|
|
1281
1291
|
renderWatchEntitlements,
|
|
1282
1292
|
renderUsageDescriptions,
|
|
@@ -2548,6 +2548,22 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2548
2548
|
} else {
|
|
2549
2549
|
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2550
2550
|
}
|
|
2551
|
+
case "saveHealthMindfulSession":
|
|
2552
|
+
if config.enableHealthKit {
|
|
2553
|
+
CraftHealth.shared.saveMindfulSession(body: body) { [weak self] in self?.answer(callbackId, $0) }
|
|
2554
|
+
} else {
|
|
2555
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2556
|
+
}
|
|
2557
|
+
case "getHealthMindfulSessions":
|
|
2558
|
+
if config.enableHealthKit {
|
|
2559
|
+
CraftHealth.shared.getMindfulSessions(
|
|
2560
|
+
startDate: body["startDate"] as? Double,
|
|
2561
|
+
endDate: body["endDate"] as? Double,
|
|
2562
|
+
limit: body["limit"] as? Int
|
|
2563
|
+
) { [weak self] in self?.answer(callbackId, $0) }
|
|
2564
|
+
} else {
|
|
2565
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2566
|
+
}
|
|
2551
2567
|
|
|
2552
2568
|
// MARK: - Live Activities
|
|
2553
2569
|
case "startLiveActivity":
|
|
@@ -5056,6 +5072,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5056
5072
|
getSleepIntervals: function(options) {
|
|
5057
5073
|
options = options || {};
|
|
5058
5074
|
return craft._invoke('getHealthSleepIntervals', {startDate: options.startDate, endDate: options.endDate});
|
|
5075
|
+
},
|
|
5076
|
+
saveMindfulSession: function(session) {
|
|
5077
|
+
session = session || {};
|
|
5078
|
+
return craft._invoke('saveHealthMindfulSession', {id: session.id, startDate: session.startDate, endDate: session.endDate});
|
|
5079
|
+
},
|
|
5080
|
+
getMindfulSessions: function(options) {
|
|
5081
|
+
options = options || {};
|
|
5082
|
+
return craft._invoke('getHealthMindfulSessions', {startDate: options.startDate, endDate: options.endDate, limit: options.limit});
|
|
5059
5083
|
}
|
|
5060
5084
|
};
|
|
5061
5085
|
// The Music app's song and transport. Nothing is read until
|
|
@@ -43,6 +43,10 @@ final class CraftHealth {
|
|
|
43
43
|
getHeartRateSamples(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
|
|
44
44
|
case "getSleepIntervals":
|
|
45
45
|
getSleepIntervals(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, completion: completion)
|
|
46
|
+
case "saveMindfulSession":
|
|
47
|
+
saveMindfulSession(body: options(0), completion: completion)
|
|
48
|
+
case "getMindfulSessions":
|
|
49
|
+
getMindfulSessions(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
|
|
46
50
|
default:
|
|
47
51
|
completion(.failure(CraftHealthError(message: "Unsupported Health method", code: "UNKNOWN_ACTION")))
|
|
48
52
|
}
|
|
@@ -106,6 +110,20 @@ final class CraftHealth {
|
|
|
106
110
|
if let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) {
|
|
107
111
|
readTypes.insert(sleepType)
|
|
108
112
|
}
|
|
113
|
+
case "mindful":
|
|
114
|
+
// Mindful minutes: a meditation or breathing session. Read
|
|
115
|
+
// to show them, and (unless read only) written by an app
|
|
116
|
+
// that times them.
|
|
117
|
+
if let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) {
|
|
118
|
+
readTypes.insert(mindfulType)
|
|
119
|
+
shareTypes.insert(mindfulType)
|
|
120
|
+
}
|
|
121
|
+
case "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat":
|
|
122
|
+
// What a food log (MyFitnessPal, Lose It!, Cronometer...)
|
|
123
|
+
// writes to Apple Health: read only, never written back.
|
|
124
|
+
if let dietaryType = Self.healthQuantity(type)?.type {
|
|
125
|
+
readTypes.insert(dietaryType)
|
|
126
|
+
}
|
|
109
127
|
default:
|
|
110
128
|
break
|
|
111
129
|
}
|
|
@@ -261,6 +279,11 @@ final class CraftHealth {
|
|
|
261
279
|
case "restingHeartRate": return (HKQuantityType.quantityType(forIdentifier: .restingHeartRate), bpm, .discreteAverage)
|
|
262
280
|
case "heartRateVariability": return (HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN), .secondUnit(with: .milli), .discreteAverage)
|
|
263
281
|
case "bodyMass": return (HKQuantityType.quantityType(forIdentifier: .bodyMass), .gramUnit(with: .kilo), .mostRecent)
|
|
282
|
+
// A day's food, summed over every meal any app logged: kcal and grams.
|
|
283
|
+
case "dietaryEnergy": return (HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed), .kilocalorie(), .cumulativeSum)
|
|
284
|
+
case "dietaryProtein": return (HKQuantityType.quantityType(forIdentifier: .dietaryProtein), .gram(), .cumulativeSum)
|
|
285
|
+
case "dietaryCarbohydrates": return (HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates), .gram(), .cumulativeSum)
|
|
286
|
+
case "dietaryFat": return (HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal), .gram(), .cumulativeSum)
|
|
264
287
|
default: return nil
|
|
265
288
|
}
|
|
266
289
|
}
|
|
@@ -548,4 +571,104 @@ final class CraftHealth {
|
|
|
548
571
|
healthStore.execute(query)
|
|
549
572
|
}
|
|
550
573
|
|
|
574
|
+
/// Saves a mindful session (Mindful Minutes in the Health app): a
|
|
575
|
+
/// meditation or breathing session that ran from `startDate` to
|
|
576
|
+
/// `endDate`, epoch milliseconds. `id` is the app's own id for it, kept
|
|
577
|
+
/// as the sample's external UUID, so saving the same session twice (a
|
|
578
|
+
/// retry, a second device) keeps the first sample instead of adding
|
|
579
|
+
/// another: the answer then says `existing: true`.
|
|
580
|
+
func saveMindfulSession(body: [String: Any], completion: @escaping Completion) {
|
|
581
|
+
guard let healthStore = healthStore else {
|
|
582
|
+
completion(.failure(CraftHealthError(message: "HealthKit not available")))
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
586
|
+
completion(.failure(CraftHealthError(message: "Mindful sessions are unavailable")))
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
guard let id = body["id"] as? String, !id.isEmpty, id.count <= 128,
|
|
590
|
+
let startValue = body["startDate"] as? Double,
|
|
591
|
+
let endValue = body["endDate"] as? Double,
|
|
592
|
+
endValue > startValue,
|
|
593
|
+
endValue - startValue <= 24 * 3600 * 1000 else {
|
|
594
|
+
completion(.failure(CraftHealthError(message: "A valid id, startDate and endDate (at most a day apart) are required", code: "INVALID_ARGUMENT")))
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let start = Date(timeIntervalSince1970: startValue / 1000)
|
|
599
|
+
let end = Date(timeIntervalSince1970: endValue / 1000)
|
|
600
|
+
// Only this app's own samples can be the same session.
|
|
601
|
+
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
|
602
|
+
HKQuery.predicateForObjects(withMetadataKey: HKMetadataKeyExternalUUID, allowedValues: [id]),
|
|
603
|
+
HKQuery.predicateForObjects(from: HKSource.default()),
|
|
604
|
+
])
|
|
605
|
+
let lookup = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: 1, sortDescriptors: nil) { _, samples, _ in
|
|
606
|
+
if let kept = samples?.first {
|
|
607
|
+
completion(.success(["id": kept.uuid.uuidString, "externalId": id, "existing": true]))
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
let sample = HKCategorySample(
|
|
611
|
+
type: mindfulType,
|
|
612
|
+
value: HKCategoryValue.notApplicable.rawValue,
|
|
613
|
+
start: start,
|
|
614
|
+
end: end,
|
|
615
|
+
metadata: [HKMetadataKeyExternalUUID: id]
|
|
616
|
+
)
|
|
617
|
+
healthStore.save(sample) { success, error in
|
|
618
|
+
if success {
|
|
619
|
+
completion(.success(["id": sample.uuid.uuidString, "externalId": id, "existing": false]))
|
|
620
|
+
} else {
|
|
621
|
+
completion(.failure(CraftHealthError(message: error?.localizedDescription ?? "Mindful session could not be saved")))
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
healthStore.execute(lookup)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/// Mindful sessions between two instants, oldest first, from every app
|
|
629
|
+
/// that writes them (the Mindfulness app, Calm, this one). Each says
|
|
630
|
+
/// whether this app wrote it (`ownApp`) and carries the external id it
|
|
631
|
+
/// was saved with, so a page can tell its own sessions from the rest.
|
|
632
|
+
func getMindfulSessions(startDate: Double?, endDate: Double?, limit: Int?, completion: @escaping Completion) {
|
|
633
|
+
guard let healthStore = healthStore else {
|
|
634
|
+
completion(.failure(CraftHealthError(message: "HealthKit not available")))
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
638
|
+
completion(.failure(CraftHealthError(message: "Mindful sessions are unavailable")))
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
643
|
+
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -30, to: end)!
|
|
644
|
+
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
645
|
+
let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
|
|
646
|
+
let cap = max(1, min(limit ?? 1000, 5000))
|
|
647
|
+
let ownBundle = Bundle.main.bundleIdentifier
|
|
648
|
+
|
|
649
|
+
let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: cap, sortDescriptors: [oldestFirst]) { _, samples, error in
|
|
650
|
+
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
651
|
+
completion(.failure(CraftHealthError(message: error.localizedDescription)))
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
let rows: [[String: Any]] = (samples ?? []).map { sample in
|
|
655
|
+
let source = sample.sourceRevision.source
|
|
656
|
+
var item: [String: Any] = [
|
|
657
|
+
"id": sample.uuid.uuidString,
|
|
658
|
+
"startDate": sample.startDate.timeIntervalSince1970 * 1000,
|
|
659
|
+
"endDate": sample.endDate.timeIntervalSince1970 * 1000,
|
|
660
|
+
"durationSeconds": sample.endDate.timeIntervalSince(sample.startDate),
|
|
661
|
+
"sourceName": source.name,
|
|
662
|
+
"sourceBundleId": source.bundleIdentifier,
|
|
663
|
+
"ownApp": ownBundle != nil && source.bundleIdentifier == ownBundle,
|
|
664
|
+
]
|
|
665
|
+
if let external = sample.metadata?[HKMetadataKeyExternalUUID] as? String { item["externalId"] = external }
|
|
666
|
+
if let device = sample.device?.name { item["deviceName"] = device }
|
|
667
|
+
return item
|
|
668
|
+
}
|
|
669
|
+
completion(.success(rows))
|
|
670
|
+
}
|
|
671
|
+
healthStore.execute(query)
|
|
672
|
+
}
|
|
673
|
+
|
|
551
674
|
}
|
|
@@ -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).
|