craft-native 0.0.242 → 0.0.244
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/src/index.js +3 -1
- package/dist/android/templates/CraftNativeMutation.kt.template +7 -3
- package/dist/android/templates/MainActivityNative.kt.template +17 -8
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +10 -8
- package/dist/ios/templates/CraftHybrid.swift +7 -4
- package/dist/ios/templates/CraftNativeMutation.swift +4 -0
- package/dist/ios/templates/CraftNativeScreen.swift +71 -24
- package/dist/ios/templates/testing/DriverTests.swift.template +5 -1
- package/dist/macos/templates/CraftNativeMutation.swift +4 -0
- package/package.json +1 -1
|
@@ -323,7 +323,9 @@ async function createAndroidEmulator(options) {
|
|
|
323
323
|
const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
|
|
324
324
|
const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
|
|
325
325
|
const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
|
|
326
|
-
|
|
326
|
+
const home = await run([adb, "-s", serial, "shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.HOME"], undefined, probeTimeout(), true);
|
|
327
|
+
const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? "";
|
|
328
|
+
booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !homeActivity.startsWith("com.android.sdksetup/");
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
const checkedAt = Date.now();
|
|
@@ -30,7 +30,8 @@ class CraftNativeMutationDocument {
|
|
|
30
30
|
var events: JSONObject,
|
|
31
31
|
var text: MutableList<String>,
|
|
32
32
|
var children: MutableList<String>,
|
|
33
|
-
var parent: String
|
|
33
|
+
var parent: String?,
|
|
34
|
+
val key: String? = null
|
|
34
35
|
) {
|
|
35
36
|
fun duplicate(): Node = copy(
|
|
36
37
|
props = JSONObject(props.toString()),
|
|
@@ -175,7 +176,8 @@ class CraftNativeMutationDocument {
|
|
|
175
176
|
dictionary(value.opt("events")),
|
|
176
177
|
textChildren(if (value.has("children")) value.opt("children") else value.opt("text")),
|
|
177
178
|
mutableListOf(),
|
|
178
|
-
null
|
|
179
|
+
null,
|
|
180
|
+
key = value.opt("key") as? String
|
|
179
181
|
)
|
|
180
182
|
return if (makeRoot) id else root
|
|
181
183
|
}
|
|
@@ -299,6 +301,7 @@ class CraftNativeMutationDocument {
|
|
|
299
301
|
.put("style", node.style)
|
|
300
302
|
.put("events", node.events)
|
|
301
303
|
.put("children", children)
|
|
304
|
+
.also { value -> node.key?.let { value.put("key", it) } }
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
private fun importNode(
|
|
@@ -322,7 +325,8 @@ class CraftNativeMutationDocument {
|
|
|
322
325
|
dictionary(value.opt("events")),
|
|
323
326
|
mutableListOf(),
|
|
324
327
|
mutableListOf(),
|
|
325
|
-
parent
|
|
328
|
+
parent,
|
|
329
|
+
key = value.opt("key") as? String
|
|
326
330
|
)
|
|
327
331
|
nodes[id] = node
|
|
328
332
|
for (index in 0 until children.length()) {
|
|
@@ -4937,8 +4937,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4937
4937
|
val explicitKey = authoredKey(node, props)
|
|
4938
4938
|
// Descendants are scoped to this retained identity, not its sibling
|
|
4939
4939
|
// position. Moving a keyed parent must retain its nested inputs too.
|
|
4940
|
+
// A test selector may intentionally identify several list rows. The
|
|
4941
|
+
// compiler's unique protocol ID must win over that legacy fallback.
|
|
4940
4942
|
val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
4941
4943
|
?: protocolId?.let { "id:$it" }
|
|
4944
|
+
?: props.optString("testID").takeIf { it.isNotBlank() }?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
4942
4945
|
?: path
|
|
4943
4946
|
val previous = previousControls[identity]
|
|
4944
4947
|
val reusableInput = previous as? EditText
|
|
@@ -5824,7 +5827,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5824
5827
|
}
|
|
5825
5828
|
|
|
5826
5829
|
private fun authoredKey(node: JSONObject, props: JSONObject): String? {
|
|
5827
|
-
val direct = listOf(node.optString("key"), props.optString("key")
|
|
5830
|
+
val direct = listOf(node.optString("key"), props.optString("key"))
|
|
5828
5831
|
.firstOrNull { it.isNotBlank() }
|
|
5829
5832
|
if (direct != null) return direct
|
|
5830
5833
|
return null
|
|
@@ -5880,17 +5883,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
5880
5883
|
}
|
|
5881
5884
|
if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse()
|
|
5882
5885
|
val desiredSet = desired.toSet()
|
|
5883
|
-
for (
|
|
5884
|
-
if (layout.getChildAt(index)
|
|
5885
|
-
|
|
5886
|
+
for (index in layout.childCount - 1 downTo 0) {
|
|
5887
|
+
if (layout.getChildAt(index) !in desiredSet) layout.removeViewAt(index)
|
|
5888
|
+
}
|
|
5889
|
+
for (childView in desired) {
|
|
5890
|
+
if (childView.parent !== layout) {
|
|
5886
5891
|
(childView.parent as? ViewGroup)?.removeView(childView)
|
|
5887
|
-
layout.addView(childView
|
|
5892
|
+
layout.addView(childView)
|
|
5888
5893
|
}
|
|
5889
5894
|
val params = childView.layoutParams as? LinearLayout.LayoutParams
|
|
5890
5895
|
?: LinearLayout.LayoutParams(childView.layoutParams?.width ?: -1, childView.layoutParams?.height ?: -2)
|
|
5891
5896
|
childView.layoutParams = params
|
|
5892
5897
|
}
|
|
5893
|
-
|
|
5898
|
+
// Reorder attached children without removing their focused inputs.
|
|
5899
|
+
// Removing and adding an existing row clears Android's native focus.
|
|
5900
|
+
if (desired.indices.any { layout.getChildAt(it) !== desired[it] }) {
|
|
5901
|
+
desired.forEach(layout::bringChildToFront)
|
|
5902
|
+
}
|
|
5894
5903
|
layout.requestLayout()
|
|
5895
5904
|
}
|
|
5896
5905
|
|
|
@@ -6573,11 +6582,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
6573
6582
|
}
|
|
6574
6583
|
|
|
6575
6584
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
6576
|
-
|
|
6585
|
+
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
6586
|
+
view.tag = testId?.takeIf { it.isNotBlank() } ?: fallbackLabel
|
|
6577
6587
|
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
6578
6588
|
?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" }
|
|
6579
6589
|
val value = craftNativeOptionalString(props.opt("accessibilityValue"))
|
|
6580
|
-
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
6581
6590
|
view.contentDescription = when {
|
|
6582
6591
|
label != null && value != null -> "$label, $value"
|
|
6583
6592
|
label != null -> label
|
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.244";
|
|
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
|
@@ -670,6 +670,7 @@ final class CraftChrome: ObservableObject {
|
|
|
670
670
|
|
|
671
671
|
@Published var tabs: [Tab] = []
|
|
672
672
|
@Published var tabAccessory: Tab?
|
|
673
|
+
private weak var tabAccessoryOwner: AnyObject?
|
|
673
674
|
@Published var selected: String?
|
|
674
675
|
let tabSwipeProgress = CraftTabSwipeProgress()
|
|
675
676
|
private(set) var tabSwipePosition: CGFloat? {
|
|
@@ -771,7 +772,7 @@ final class CraftChrome: ObservableObject {
|
|
|
771
772
|
tabs[index] = next
|
|
772
773
|
}
|
|
773
774
|
/// A destination beside the tab pill, outside the tab stacks and swipe order.
|
|
774
|
-
func updateTabAccessory(_ item: [String: Any]?) {
|
|
775
|
+
func updateTabAccessory(_ item: [String: Any]?, owner: AnyObject? = nil) {
|
|
775
776
|
let next: Tab?
|
|
776
777
|
if let item, let path = item["path"] as? String,
|
|
777
778
|
path.hasPrefix("/"), !path.hasPrefix("//"),
|
|
@@ -782,10 +783,17 @@ final class CraftChrome: ObservableObject {
|
|
|
782
783
|
} else {
|
|
783
784
|
next = nil
|
|
784
785
|
}
|
|
786
|
+
tabAccessoryOwner = owner
|
|
785
787
|
guard tabAccessory != next else { return }
|
|
786
788
|
tabAccessory = next
|
|
787
789
|
}
|
|
788
790
|
|
|
791
|
+
/// A retained or prepared screen cannot clear another screen's chrome.
|
|
792
|
+
func clearTabAccessory(ownedBy owner: AnyObject) {
|
|
793
|
+
guard tabAccessoryOwner === owner else { return }
|
|
794
|
+
updateTabAccessory(nil)
|
|
795
|
+
}
|
|
796
|
+
|
|
789
797
|
func setHiddenByScreen(_ hidden: Bool) {
|
|
790
798
|
guard hiddenByScreen != hidden else { return }
|
|
791
799
|
hiddenByScreen = hidden
|
|
@@ -896,12 +904,9 @@ final class CraftChrome: ObservableObject {
|
|
|
896
904
|
// stack. Cancellation belongs to the stack the finger left.
|
|
897
905
|
tabSwipePrevious = previous
|
|
898
906
|
tabSwipeTarget = id
|
|
899
|
-
// Only the indicator moves now. The highlight follows it across the
|
|
900
|
-
// midpoint (updateTabSwipe), as iOS's own tab bar lens does; turning
|
|
901
|
-
// it at once rebuilt every tab in the swipe's first frame, which is
|
|
902
|
-
// also when UIKit puts the incoming screen on screen.
|
|
903
907
|
withAnimation(nil) {
|
|
904
908
|
tabSwipePosition = tabs.firstIndex(where: { $0.id == previous }).map { CGFloat($0) }
|
|
909
|
+
selected = id
|
|
905
910
|
}
|
|
906
911
|
}
|
|
907
912
|
|
|
@@ -913,14 +918,11 @@ final class CraftChrome: ObservableObject {
|
|
|
913
918
|
let to = tabs.firstIndex(where: { $0.id == target }) else { return }
|
|
914
919
|
let position = CGFloat(from) + CGFloat(to - from) * min(1, max(0, progress))
|
|
915
920
|
if tabSwipePosition != position { withAnimation(nil) { tabSwipePosition = position } }
|
|
916
|
-
let lit = progress >= 0.5 ? target : previous
|
|
917
|
-
if selected != lit { withAnimation(nil) { selected = lit } }
|
|
918
921
|
}
|
|
919
922
|
|
|
920
923
|
func settleTabSwipe(completed: Bool, duration: TimeInterval) {
|
|
921
924
|
guard let id = completed ? tabSwipeTarget : tabSwipePrevious,
|
|
922
925
|
let index = tabs.firstIndex(where: { $0.id == id }) else { return }
|
|
923
|
-
if selected != id { withAnimation(nil) { selected = id } }
|
|
924
926
|
withAnimation(Self.motion(.easeOut(duration: duration))) { tabSwipePosition = CGFloat(index) }
|
|
925
927
|
}
|
|
926
928
|
|
|
@@ -1402,7 +1402,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1402
1402
|
|
|
1403
1403
|
// MARK: Stacks
|
|
1404
1404
|
|
|
1405
|
-
|
|
1405
|
+
/// `hidden`: a stack prepared off screen. It is hidden before it joins
|
|
1406
|
+
/// the view hierarchy, so its screen never takes the shell's chrome (the
|
|
1407
|
+
/// bar, the tab accessory) from the one on screen.
|
|
1408
|
+
private func makeStack(tab: String, rootPath: String, hidden: Bool = false) -> UINavigationController {
|
|
1406
1409
|
let nav = UINavigationController(rootViewController: entry(for: rootPath, isTabRoot: true, depth: nil))
|
|
1407
1410
|
nav.delegate = self
|
|
1408
1411
|
nav.view.backgroundColor = config.resolvedBackgroundColor
|
|
@@ -1411,6 +1414,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1411
1414
|
addChild(nav)
|
|
1412
1415
|
nav.view.frame = view.bounds
|
|
1413
1416
|
nav.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
1417
|
+
nav.view.isHidden = hidden
|
|
1414
1418
|
view.addSubview(nav.view)
|
|
1415
1419
|
nav.didMove(toParent: self)
|
|
1416
1420
|
// Only once the stack's view has loaded: the recognizer is made then,
|
|
@@ -1515,8 +1519,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1515
1519
|
let id = tabs[next]
|
|
1516
1520
|
guard self.stacks[id] == nil else { self.prepareAdjacentNativeTabs(); return }
|
|
1517
1521
|
let nav = CraftFramePacing.shared.measure("prepare \(id)") { () -> UINavigationController in
|
|
1518
|
-
let nav = self.makeStack(tab: id, rootPath: id)
|
|
1519
|
-
nav.view.isHidden = true
|
|
1522
|
+
let nav = self.makeStack(tab: id, rootPath: id, hidden: true)
|
|
1520
1523
|
nav.view.layoutIfNeeded()
|
|
1521
1524
|
// Draw its text and pictures now. A hidden layer is not
|
|
1522
1525
|
// drawn at commit, so the first swipe to this tab drew
|
|
@@ -1752,7 +1755,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1752
1755
|
// Keep this screen up until the page has switched, rather than
|
|
1753
1756
|
// showing it half way.
|
|
1754
1757
|
cancelPendingTab()
|
|
1755
|
-
if stacks[id] == nil { _ = makeStack(tab: id, rootPath: id
|
|
1758
|
+
if stacks[id] == nil { _ = makeStack(tab: id, rootPath: id, hidden: true); view.bringSubviewToFront(visibleStack?.view ?? view) }
|
|
1756
1759
|
let deadline = DispatchWorkItem { [weak self] in self?.show(tab: id) }
|
|
1757
1760
|
pendingTab = (id, deadline)
|
|
1758
1761
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45, execute: deadline)
|
|
@@ -29,6 +29,7 @@ struct CraftNativeMutationResult {
|
|
|
29
29
|
final class CraftNativeMutationDocument {
|
|
30
30
|
private struct Node {
|
|
31
31
|
let id: String
|
|
32
|
+
let key: String?
|
|
32
33
|
var type: String
|
|
33
34
|
var props: [String: Any]
|
|
34
35
|
var style: [String: Any]
|
|
@@ -177,6 +178,7 @@ final class CraftNativeMutationDocument {
|
|
|
177
178
|
}
|
|
178
179
|
nodes[id] = Node(
|
|
179
180
|
id: id,
|
|
181
|
+
key: value["key"] as? String,
|
|
180
182
|
type: type,
|
|
181
183
|
props: dictionary(value["props"]),
|
|
182
184
|
style: dictionary(value["style"]),
|
|
@@ -330,6 +332,7 @@ final class CraftNativeMutationDocument {
|
|
|
330
332
|
"style": node.style,
|
|
331
333
|
"events": node.events,
|
|
332
334
|
]
|
|
335
|
+
if let key = node.key { value["key"] = key }
|
|
333
336
|
let text: [Any] = node.text
|
|
334
337
|
let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
|
|
335
338
|
value["children"] = text + children
|
|
@@ -350,6 +353,7 @@ final class CraftNativeMutationDocument {
|
|
|
350
353
|
let children = value["children"] as? [Any] ?? []
|
|
351
354
|
nodes[id] = Node(
|
|
352
355
|
id: id,
|
|
356
|
+
key: value["key"] as? String,
|
|
353
357
|
type: value["type"] as? String ?? "View",
|
|
354
358
|
props: props,
|
|
355
359
|
style: dictionary(value["style"]),
|
|
@@ -1435,21 +1435,47 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1435
1435
|
|
|
1436
1436
|
private func configureMarkerView(_ view: MKAnnotationView, id: String) {
|
|
1437
1437
|
if let bearing = markerBearings[id] {
|
|
1438
|
-
let
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
// the contrasting route arrow when displayed over map content.
|
|
1442
|
-
view.image = UIImage(systemName: "arrow.up.circle.fill", withConfiguration: configuration)?
|
|
1443
|
-
.withRenderingMode(.alwaysOriginal)
|
|
1438
|
+
let color = (markerPinColors[id] ?? .systemGreen).resolvedColor(with: traitCollection)
|
|
1439
|
+
view.tintColor = color
|
|
1440
|
+
view.image = Self.markerImage(color: color, direction: true)
|
|
1444
1441
|
view.transform = CGAffineTransform(rotationAngle: CGFloat((bearing - camera.heading) * .pi / 180))
|
|
1445
1442
|
} else {
|
|
1446
|
-
|
|
1447
|
-
view.
|
|
1448
|
-
|
|
1443
|
+
let color = (markerPinColors[id] ?? .systemRed).resolvedColor(with: traitCollection)
|
|
1444
|
+
view.tintColor = color
|
|
1445
|
+
view.image = Self.markerImage(color: color, direction: false)
|
|
1449
1446
|
view.transform = .identity
|
|
1450
1447
|
}
|
|
1451
1448
|
}
|
|
1452
1449
|
|
|
1450
|
+
private static let markerImages: NSCache<NSString, UIImage> = {
|
|
1451
|
+
let cache = NSCache<NSString, UIImage>()
|
|
1452
|
+
cache.countLimit = 64
|
|
1453
|
+
return cache
|
|
1454
|
+
}()
|
|
1455
|
+
|
|
1456
|
+
private static func markerImage(color: UIColor, direction: Bool) -> UIImage {
|
|
1457
|
+
let key = "\(direction ? "direction" : "pin"):\(color.description)" as NSString
|
|
1458
|
+
if let cached = markerImages.object(forKey: key) { return cached }
|
|
1459
|
+
let size = direction ? CGSize(width: 20, height: 24) : CGSize(width: 28, height: 28)
|
|
1460
|
+
// Explicit native drawing preserves the light backing independently
|
|
1461
|
+
// of MapKit's tint and SF Symbols' multicolor rendering preferences.
|
|
1462
|
+
let image = UIGraphicsImageRenderer(size: size).image { _ in
|
|
1463
|
+
let border = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 1, dy: 1),
|
|
1464
|
+
cornerRadius: (size.width - 2) / 2)
|
|
1465
|
+
UIColor.white.setFill()
|
|
1466
|
+
border.fill()
|
|
1467
|
+
color.setStroke()
|
|
1468
|
+
border.lineWidth = 1.5
|
|
1469
|
+
border.stroke()
|
|
1470
|
+
let symbol = UIImage(systemName: direction ? "arrow.up" : "mappin",
|
|
1471
|
+
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))?
|
|
1472
|
+
.withTintColor(color, renderingMode: .alwaysOriginal)
|
|
1473
|
+
symbol?.draw(in: direction ? CGRect(x: 5, y: 6, width: 10, height: 12) : CGRect(x: 7, y: 5, width: 14, height: 18))
|
|
1474
|
+
}.withRenderingMode(.alwaysOriginal)
|
|
1475
|
+
markerImages.setObject(image, forKey: key)
|
|
1476
|
+
return image
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1453
1479
|
override func layoutSubviews() {
|
|
1454
1480
|
super.layoutSubviews()
|
|
1455
1481
|
if tileTemplate != nil {
|
|
@@ -4598,19 +4624,32 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4598
4624
|
}
|
|
4599
4625
|
applyNavigationBarAppearance()
|
|
4600
4626
|
// Each screen says whether it wants the tab bar, so one that hid it
|
|
4601
|
-
// never leaves it hidden for the next.
|
|
4602
|
-
|
|
4627
|
+
// never leaves it hidden for the next. Only the screen on screen:
|
|
4628
|
+
// UIKit tells a tab's screen prepared in a hidden stack that it
|
|
4629
|
+
// appears too, and that one took the accessory of the screen showing.
|
|
4630
|
+
if isShownOnScreen { restoreShellChrome() }
|
|
4603
4631
|
refreshTraitDefaults()
|
|
4604
4632
|
}
|
|
4605
4633
|
|
|
4634
|
+
/// In the window with nothing above it hidden: the screen the person
|
|
4635
|
+
/// sees, not a tab prepared behind it.
|
|
4636
|
+
private var isShownOnScreen: Bool {
|
|
4637
|
+
guard let window = view.window else { return false }
|
|
4638
|
+
var candidate: UIView? = view
|
|
4639
|
+
while let current = candidate, current !== window {
|
|
4640
|
+
if current.isHidden { return false }
|
|
4641
|
+
candidate = current.superview
|
|
4642
|
+
}
|
|
4643
|
+
return true
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4606
4646
|
override func viewWillDisappear(_ animated: Bool) {
|
|
4607
4647
|
super.viewWillDisappear(animated)
|
|
4608
4648
|
screenVisibilityChanged(false)
|
|
4609
4649
|
stopCelebrations(in: view)
|
|
4610
|
-
//
|
|
4611
|
-
//
|
|
4612
|
-
|
|
4613
|
-
CraftChrome.shared.updateTabAccessory(nil)
|
|
4650
|
+
// Release only this screen's accessory. A retained or prepared screen
|
|
4651
|
+
// can disappear after the destination has already published its own.
|
|
4652
|
+
CraftChrome.shared.clearTabAccessory(ownedBy: self)
|
|
4614
4653
|
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
4615
4654
|
// back for whatever shows next, a web page included.
|
|
4616
4655
|
if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
@@ -4620,11 +4659,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4620
4659
|
|
|
4621
4660
|
override func viewDidAppear(_ animated: Bool) {
|
|
4622
4661
|
super.viewDidAppear(animated)
|
|
4623
|
-
|
|
4624
|
-
//
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4662
|
+
// A tab prepared in a hidden stack "appears" to UIKit as well; the
|
|
4663
|
+
// shell says when it really shows (screenVisibilityChanged).
|
|
4664
|
+
if isShownOnScreen {
|
|
4665
|
+
screenVisibilityChanged(true)
|
|
4666
|
+
// A navigation breadcrumb for the crash reporter: the trail a
|
|
4667
|
+
// crash report carries is the screens that led to it.
|
|
4668
|
+
if !isAppScript, let routeName, !routeName.isEmpty {
|
|
4669
|
+
CraftCrashReporter.shared.screenAppeared(routeName)
|
|
4670
|
+
}
|
|
4628
4671
|
}
|
|
4629
4672
|
// onLayout reports views that are on screen: a pushed screen lays
|
|
4630
4673
|
// out before it has a window, and nothing moved again once it had
|
|
@@ -4651,7 +4694,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4651
4694
|
private func restoreShellChrome() {
|
|
4652
4695
|
guard !isAppScript else { return }
|
|
4653
4696
|
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
4654
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
4697
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
4655
4698
|
}
|
|
4656
4699
|
|
|
4657
4700
|
/// The screen came on screen or went off it: a screen pushed over it was
|
|
@@ -5322,7 +5365,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5322
5365
|
if options.keys.contains("tabAccessory") {
|
|
5323
5366
|
tabAccessory = options["tabAccessory"] as? [String: Any]
|
|
5324
5367
|
if navigationController?.topViewController === self, view.window != nil {
|
|
5325
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
5368
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
5326
5369
|
}
|
|
5327
5370
|
}
|
|
5328
5371
|
if options.keys.contains("title") {
|
|
@@ -6847,7 +6890,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6847
6890
|
|
|
6848
6891
|
private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
6849
6892
|
if let authored = authoredKey(node, props: props) { return authored }
|
|
6850
|
-
|
|
6893
|
+
// Repeated test selectors are valid for list rows. Retain each row by
|
|
6894
|
+
// its unique protocol ID before considering legacy selector identity.
|
|
6895
|
+
return [node["id"], props["testID"]]
|
|
6896
|
+
.compactMap { $0 as? String }
|
|
6897
|
+
.first { !$0.isEmpty }
|
|
6851
6898
|
}
|
|
6852
6899
|
|
|
6853
6900
|
/// The key a node's author gave it. A wrapper of one child (a
|
|
@@ -6859,7 +6906,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6859
6906
|
private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
6860
6907
|
// The current @stacksjs/stx native compiler emits key in props; accept the IR
|
|
6861
6908
|
// field too so keyed children keep working when the compiler adopts it.
|
|
6862
|
-
let direct = [node["key"], props["key"]
|
|
6909
|
+
let direct = [node["key"], props["key"]]
|
|
6863
6910
|
.compactMap { $0 as? String }
|
|
6864
6911
|
.first { !$0.isEmpty }
|
|
6865
6912
|
if let direct { return direct }
|
|
@@ -448,7 +448,11 @@ final class DriverTests: XCTestCase {
|
|
|
448
448
|
var found = element(containing: arg)
|
|
449
449
|
var swipes = 0
|
|
450
450
|
while (found == nil || !(found!.isHittable)) && swipes < 8 {
|
|
451
|
-
app.
|
|
451
|
+
if let target = found, !target.frame.isEmpty, target.frame.midY < app.frame.midY {
|
|
452
|
+
app.swipeDown()
|
|
453
|
+
} else {
|
|
454
|
+
app.swipeUp()
|
|
455
|
+
}
|
|
452
456
|
swipes += 1
|
|
453
457
|
found = element(containing: arg)
|
|
454
458
|
}
|
|
@@ -29,6 +29,7 @@ struct CraftNativeMutationResult {
|
|
|
29
29
|
final class CraftNativeMutationDocument {
|
|
30
30
|
private struct Node {
|
|
31
31
|
let id: String
|
|
32
|
+
let key: String?
|
|
32
33
|
var type: String
|
|
33
34
|
var props: [String: Any]
|
|
34
35
|
var style: [String: Any]
|
|
@@ -177,6 +178,7 @@ final class CraftNativeMutationDocument {
|
|
|
177
178
|
}
|
|
178
179
|
nodes[id] = Node(
|
|
179
180
|
id: id,
|
|
181
|
+
key: value["key"] as? String,
|
|
180
182
|
type: type,
|
|
181
183
|
props: dictionary(value["props"]),
|
|
182
184
|
style: dictionary(value["style"]),
|
|
@@ -330,6 +332,7 @@ final class CraftNativeMutationDocument {
|
|
|
330
332
|
"style": node.style,
|
|
331
333
|
"events": node.events,
|
|
332
334
|
]
|
|
335
|
+
if let key = node.key { value["key"] = key }
|
|
333
336
|
let text: [Any] = node.text
|
|
334
337
|
let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
|
|
335
338
|
value["children"] = text + children
|
|
@@ -350,6 +353,7 @@ final class CraftNativeMutationDocument {
|
|
|
350
353
|
let children = value["children"] as? [Any] ?? []
|
|
351
354
|
nodes[id] = Node(
|
|
352
355
|
id: id,
|
|
356
|
+
key: value["key"] as? String,
|
|
353
357
|
type: value["type"] as? String ?? "View",
|
|
354
358
|
props: props,
|
|
355
359
|
style: dictionary(value["style"]),
|