craft-native 0.0.227 → 0.0.228
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/templates/CraftApp.swift +41 -9
- package/dist/ios/templates/CraftHybrid.swift +41 -3
- package/dist/ios/templates/CraftNativeActions.swift +27 -5
- package/dist/ios/templates/CraftNativeScreen.swift +7 -0
- package/dist/ios/templates/Info.plist.template +2 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.228";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -627,6 +627,7 @@ final class CraftChrome: ObservableObject {
|
|
|
627
627
|
@Published var tabs: [Tab] = []
|
|
628
628
|
@Published var tabAccessory: Tab?
|
|
629
629
|
@Published var selected: String?
|
|
630
|
+
@Published private(set) var tabSwipePosition: CGFloat?
|
|
630
631
|
@Published var tabBarVisible = false
|
|
631
632
|
@Published var splashVisible = true
|
|
632
633
|
@Published var tint: UIColor?
|
|
@@ -640,6 +641,7 @@ final class CraftChrome: ObservableObject {
|
|
|
640
641
|
private var pendingHide: DispatchWorkItem?
|
|
641
642
|
private var splashDeadline: DispatchWorkItem?
|
|
642
643
|
private var tabSwipePrevious: String?
|
|
644
|
+
private var tabSwipeTarget: String?
|
|
643
645
|
/// The keyboard is up and covers the tab bar, so the page need not leave
|
|
644
646
|
/// room for it: the web view ends at the keyboard (CraftHybrid's webHost).
|
|
645
647
|
private var keyboardUp = false
|
|
@@ -821,12 +823,34 @@ final class CraftChrome: ObservableObject {
|
|
|
821
823
|
// A page's pending selection report may disagree with the native
|
|
822
824
|
// stack. Cancellation belongs to the stack the finger left.
|
|
823
825
|
tabSwipePrevious = previous
|
|
824
|
-
|
|
826
|
+
tabSwipeTarget = id
|
|
827
|
+
withAnimation(nil) {
|
|
828
|
+
tabSwipePosition = tabs.firstIndex(where: { $0.id == previous }).map { CGFloat($0) }
|
|
829
|
+
selected = id
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/// The capsule shares the gesture's progress, rather than jumping to the
|
|
834
|
+
/// next item as soon as the pan recognizer begins.
|
|
835
|
+
func updateTabSwipe(progress: CGFloat) {
|
|
836
|
+
guard let previous = tabSwipePrevious, let target = tabSwipeTarget,
|
|
837
|
+
let from = tabs.firstIndex(where: { $0.id == previous }),
|
|
838
|
+
let to = tabs.firstIndex(where: { $0.id == target }) else { return }
|
|
839
|
+
let position = CGFloat(from) + CGFloat(to - from) * min(1, max(0, progress))
|
|
840
|
+
if tabSwipePosition != position { withAnimation(nil) { tabSwipePosition = position } }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
func settleTabSwipe(completed: Bool, duration: TimeInterval) {
|
|
844
|
+
guard let id = completed ? tabSwipeTarget : tabSwipePrevious,
|
|
845
|
+
let index = tabs.firstIndex(where: { $0.id == id }) else { return }
|
|
846
|
+
withAnimation(Self.motion(.easeOut(duration: duration))) { tabSwipePosition = CGFloat(index) }
|
|
825
847
|
}
|
|
826
848
|
|
|
827
849
|
func cancelTabSwipe(restoringSelection: Bool = true) {
|
|
828
850
|
guard let previous = tabSwipePrevious else { return }
|
|
829
851
|
tabSwipePrevious = nil
|
|
852
|
+
tabSwipeTarget = nil
|
|
853
|
+
withAnimation(nil) { tabSwipePosition = nil }
|
|
830
854
|
if restoringSelection {
|
|
831
855
|
withAnimation(nil) { selected = previous }
|
|
832
856
|
}
|
|
@@ -834,6 +858,8 @@ final class CraftChrome: ObservableObject {
|
|
|
834
858
|
|
|
835
859
|
func commitTabSwipe(to id: String) {
|
|
836
860
|
tabSwipePrevious = nil
|
|
861
|
+
tabSwipeTarget = nil
|
|
862
|
+
withAnimation(nil) { tabSwipePosition = nil }
|
|
837
863
|
guard tabs.contains(where: { $0.id == id }) else { return }
|
|
838
864
|
withAnimation(nil) { selected = id }
|
|
839
865
|
UISelectionFeedbackGenerator().selectionChanged()
|
|
@@ -934,7 +960,6 @@ final class CraftChromeRelay: NSObject, WKScriptMessageHandler {
|
|
|
934
960
|
/// The floating tab bar: Liquid Glass on iOS 26 and later, a material capsule before.
|
|
935
961
|
struct CraftTabBarView: View {
|
|
936
962
|
@ObservedObject private var chrome = CraftChrome.shared
|
|
937
|
-
@Namespace private var selection
|
|
938
963
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
939
964
|
@Environment(\.scenePhase) private var scenePhase
|
|
940
965
|
// Dynamic Type, relative to the styles UIKit's own bar uses. The bar is
|
|
@@ -1064,13 +1089,6 @@ struct CraftTabBarView: View {
|
|
|
1064
1089
|
}
|
|
1065
1090
|
.foregroundStyle(isSelected ? selectedColor : Color.primary)
|
|
1066
1091
|
.frame(maxWidth: .infinity, minHeight: 54)
|
|
1067
|
-
.background {
|
|
1068
|
-
if isSelected {
|
|
1069
|
-
Capsule()
|
|
1070
|
-
.fill(Color.primary.opacity(0.09))
|
|
1071
|
-
.matchedGeometryEffect(id: "selection", in: selection)
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
1092
|
.contentShape(Capsule())
|
|
1075
1093
|
}
|
|
1076
1094
|
.buttonStyle(.plain)
|
|
@@ -1083,6 +1101,20 @@ struct CraftTabBarView: View {
|
|
|
1083
1101
|
.accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton)
|
|
1084
1102
|
}
|
|
1085
1103
|
}
|
|
1104
|
+
.background {
|
|
1105
|
+
GeometryReader { geometry in
|
|
1106
|
+
if let position = chrome.tabSwipePosition
|
|
1107
|
+
?? chrome.tabs.firstIndex(where: { $0.id == chrome.selected }).map({ CGFloat($0) }) {
|
|
1108
|
+
let width = geometry.size.width / CGFloat(max(1, chrome.tabs.count))
|
|
1109
|
+
Capsule()
|
|
1110
|
+
.fill(Color.primary.opacity(0.09))
|
|
1111
|
+
.frame(width: width, height: geometry.size.height)
|
|
1112
|
+
.offset(x: position * width)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
.allowsHitTesting(false)
|
|
1116
|
+
.accessibilityHidden(true)
|
|
1117
|
+
}
|
|
1086
1118
|
}
|
|
1087
1119
|
|
|
1088
1120
|
private var selectedColor: Color {
|
|
@@ -1104,6 +1104,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1104
1104
|
DispatchQueue.main.async { [weak self] in self?.hybrid.shellReady() }
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
|
+
override func viewDidAppear(_ animated: Bool) {
|
|
1108
|
+
super.viewDidAppear(animated)
|
|
1109
|
+
prepareAdjacentNativeTabs()
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1107
1112
|
private var webAttached = false
|
|
1108
1113
|
|
|
1109
1114
|
private func installTabSwipeGestures() {
|
|
@@ -1152,9 +1157,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1152
1157
|
let speed = velocity * direction
|
|
1153
1158
|
let complete = recognizer.state == .ended && speed > -700
|
|
1154
1159
|
&& (progress >= max(32, view.bounds.width * 0.28) || speed > 700)
|
|
1155
|
-
finishInteractiveTabSwipe(completed: complete) { [weak self] in
|
|
1160
|
+
finishInteractiveTabSwipe(completed: complete, velocity: velocity) { [weak self] in
|
|
1156
1161
|
self?.activeTabSwipeTarget = nil
|
|
1157
1162
|
self?.tabSwipeSettling = false
|
|
1163
|
+
self?.prepareAdjacentNativeTabs()
|
|
1158
1164
|
guard complete else {
|
|
1159
1165
|
CraftChrome.shared.cancelTabSwipe()
|
|
1160
1166
|
return
|
|
@@ -1224,9 +1230,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1224
1230
|
: max(0, min(width, translation))
|
|
1225
1231
|
target.view.frame.origin.x = tabSwipeFrom + offset
|
|
1226
1232
|
current.view.frame.origin.x = offset
|
|
1233
|
+
CraftChrome.shared.updateTabSwipe(progress: abs(offset) / width)
|
|
1227
1234
|
}
|
|
1228
1235
|
|
|
1229
|
-
private func finishInteractiveTabSwipe(completed: Bool, completion: @escaping () -> Void) {
|
|
1236
|
+
private func finishInteractiveTabSwipe(completed: Bool, velocity: CGFloat, completion: @escaping () -> Void) {
|
|
1230
1237
|
guard let targetId = activeTabSwipeTarget, let current = visibleStack,
|
|
1231
1238
|
let target = stacks[targetId] else { completion(); return }
|
|
1232
1239
|
let width = view.bounds.width
|
|
@@ -1235,7 +1242,12 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1235
1242
|
let settlingDuration = completed ? 0.22 : 0.18
|
|
1236
1243
|
let duration = CraftChrome.motion(.easeOut(duration: settlingDuration)) == nil ? 0 : settlingDuration
|
|
1237
1244
|
let generation = tabSwipeGeneration
|
|
1238
|
-
|
|
1245
|
+
CraftChrome.shared.settleTabSwipe(completed: completed, duration: duration)
|
|
1246
|
+
let remaining = end - target.view.frame.minX
|
|
1247
|
+
let initialVelocity = abs(remaining) > 1 ? max(0, min(12, velocity / remaining)) : 0
|
|
1248
|
+
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1,
|
|
1249
|
+
initialSpringVelocity: initialVelocity,
|
|
1250
|
+
options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
1239
1251
|
target.view.frame.origin.x = end
|
|
1240
1252
|
current.view.frame.origin.x = completed ? -direction * width : 0
|
|
1241
1253
|
}, completion: { _ in
|
|
@@ -1449,6 +1461,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1449
1461
|
for (key, other) in stacks { other.view.isHidden = key != id }
|
|
1450
1462
|
nav.view.frame = view.bounds
|
|
1451
1463
|
view.bringSubviewToFront(nav.view)
|
|
1464
|
+
prepareAdjacentNativeTabs()
|
|
1452
1465
|
if let slot = nav.topViewController as? CraftHybridWebSlot, slot.isViewLoaded {
|
|
1453
1466
|
adoptWeb(into: slot)
|
|
1454
1467
|
} else if !(nav.topViewController is CraftHybridWebSlot) {
|
|
@@ -1456,6 +1469,31 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1456
1469
|
}
|
|
1457
1470
|
}
|
|
1458
1471
|
|
|
1472
|
+
/// Prepare one neighbour after the first frame, never inside the finger's
|
|
1473
|
+
/// tracking run-loop mode. A first swipe then uses a laid-out native root
|
|
1474
|
+
/// rather than compiling, measuring and fetching at gesture start.
|
|
1475
|
+
private func prepareAdjacentNativeTabs() {
|
|
1476
|
+
let owner = currentTab
|
|
1477
|
+
let timer = Timer(timeInterval: 0.15, repeats: false) { [weak self] _ in
|
|
1478
|
+
guard let self, self.currentTab == owner, self.activeTabSwipeTarget == nil,
|
|
1479
|
+
self.view.window != nil,
|
|
1480
|
+
let index = self.hybrid.tabIds.firstIndex(of: owner) else { return }
|
|
1481
|
+
let tabs = self.hybrid.tabIds
|
|
1482
|
+
for neighbour in [index + 1, index - 1] where tabs.indices.contains(neighbour) {
|
|
1483
|
+
let id = tabs[neighbour]
|
|
1484
|
+
guard self.stacks[id] == nil, self.hybrid.routes.match(id) != nil else { continue }
|
|
1485
|
+
let nav = self.makeStack(tab: id, rootPath: id)
|
|
1486
|
+
nav.view.isHidden = true
|
|
1487
|
+
nav.view.layoutIfNeeded()
|
|
1488
|
+
(nav.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(false)
|
|
1489
|
+
(self.visibleStack?.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(true)
|
|
1490
|
+
self.prepareAdjacentNativeTabs()
|
|
1491
|
+
break
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
RunLoop.main.add(timer, forMode: .default)
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1459
1497
|
private func cancelPendingTab() {
|
|
1460
1498
|
pendingTab?.deadline.cancel()
|
|
1461
1499
|
pendingTab = nil
|
|
@@ -1436,6 +1436,16 @@ enum CraftSnapshots {
|
|
|
1436
1436
|
private static let lock = NSLock()
|
|
1437
1437
|
/// Writes not on disk yet, so a read right after a write sees it.
|
|
1438
1438
|
private static var pending: [String: String?] = [:]
|
|
1439
|
+
private static var revisions: [String: UInt64] = [:]
|
|
1440
|
+
private static var revision: UInt64 = 0
|
|
1441
|
+
// Screens read shared headers many times while evaluating bindings. Keep
|
|
1442
|
+
// settled JSON in memory instead of reopening the same file on the UI thread.
|
|
1443
|
+
private static let cache: NSCache<NSString, NSString> = {
|
|
1444
|
+
let cache = NSCache<NSString, NSString>()
|
|
1445
|
+
cache.countLimit = 32
|
|
1446
|
+
cache.totalCostLimit = 8 * 1024 * 1024
|
|
1447
|
+
return cache
|
|
1448
|
+
}()
|
|
1439
1449
|
|
|
1440
1450
|
static var directory: URL? {
|
|
1441
1451
|
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
@@ -1458,11 +1468,13 @@ enum CraftSnapshots {
|
|
|
1458
1468
|
static func read(_ name: String) -> String? {
|
|
1459
1469
|
guard let url = url(for: name) else { return nil }
|
|
1460
1470
|
lock.lock()
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
if let
|
|
1471
|
+
defer { lock.unlock() }
|
|
1472
|
+
if let waiting = pending[name] { return waiting }
|
|
1473
|
+
if let cached = cache.object(forKey: name as NSString) { return cached as String }
|
|
1464
1474
|
guard let data = try? Data(contentsOf: url) else { return nil }
|
|
1465
|
-
|
|
1475
|
+
let json = String(decoding: data, as: UTF8.self)
|
|
1476
|
+
cache.setObject(json as NSString, forKey: name as NSString, cost: data.count)
|
|
1477
|
+
return json
|
|
1466
1478
|
}
|
|
1467
1479
|
|
|
1468
1480
|
/// Replaces the snapshot with JSON text (nil deletes it) off the main
|
|
@@ -1477,7 +1489,11 @@ enum CraftSnapshots {
|
|
|
1477
1489
|
return
|
|
1478
1490
|
}
|
|
1479
1491
|
lock.lock()
|
|
1492
|
+
revision &+= 1
|
|
1493
|
+
let mine = revision
|
|
1494
|
+
revisions[name] = mine
|
|
1480
1495
|
pending[name] = .some(json)
|
|
1496
|
+
cache.removeObject(forKey: name as NSString)
|
|
1481
1497
|
lock.unlock()
|
|
1482
1498
|
queue.async {
|
|
1483
1499
|
var ok = true
|
|
@@ -1492,7 +1508,13 @@ enum CraftSnapshots {
|
|
|
1492
1508
|
ok = false
|
|
1493
1509
|
}
|
|
1494
1510
|
lock.lock()
|
|
1495
|
-
if
|
|
1511
|
+
if revisions[name] == mine {
|
|
1512
|
+
revisions.removeValue(forKey: name)
|
|
1513
|
+
pending.removeValue(forKey: name)
|
|
1514
|
+
if ok, let json {
|
|
1515
|
+
cache.setObject(json as NSString, forKey: name as NSString, cost: json.utf8.count)
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1496
1518
|
lock.unlock()
|
|
1497
1519
|
DispatchQueue.main.async {
|
|
1498
1520
|
if ok { NotificationCenter.default.post(name: changed, object: nil, userInfo: ["name": name]) }
|
|
@@ -4224,7 +4224,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4224
4224
|
screenRequestCancellers.removeAll()
|
|
4225
4225
|
}
|
|
4226
4226
|
|
|
4227
|
+
private var appliedTraitDefaults: String?
|
|
4228
|
+
|
|
4227
4229
|
private func refreshTraitDefaults() {
|
|
4230
|
+
// Tab navigation is not a theme change. Reapplying the entire tree
|
|
4231
|
+
// discards every measurement just as an interactive transition starts.
|
|
4232
|
+
let traits = "\(colorScheme):\(traitCollection.preferredContentSizeCategory.rawValue):\(UIAccessibility.isReduceTransparencyEnabled)"
|
|
4233
|
+
guard traits != appliedTraitDefaults else { return }
|
|
4234
|
+
appliedTraitDefaults = traits
|
|
4228
4235
|
view.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
4229
4236
|
applyNavigationBarAppearance()
|
|
4230
4237
|
if let document = mutationDocument.node("root") {
|