@craft-native/ios 0.0.250 → 0.0.254

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.250",
3
+ "version": "0.0.254",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2080,7 +2080,14 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
2080
2080
  /// The edge swipe goes ahead of the web view's own gestures, which
2081
2081
  /// otherwise hold every touch over the page until the page has answered it.
2082
2082
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
2083
- gestureRecognizer === visibleStack?.interactivePopGestureRecognizer
2083
+ if gestureRecognizer === visibleStack?.interactivePopGestureRecognizer { return true }
2084
+ guard tabSwipeRecognizers.contains(where: { $0 === gestureRecognizer }),
2085
+ let scroll = otherGestureRecognizer.view as? UIScrollView,
2086
+ otherGestureRecognizer === scroll.panGestureRecognizer,
2087
+ scroll.isScrollEnabled else { return false }
2088
+ // A vertical list must not suppress the shell's horizontal tab pan.
2089
+ // Horizontal content keeps ownership of its own drag instead.
2090
+ return !scroll.alwaysBounceHorizontal && scroll.contentSize.width <= scroll.bounds.width + 1
2084
2091
  }
2085
2092
 
2086
2093
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@@ -371,24 +371,9 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
371
371
  @objc private func keyboardTap() {
372
372
  guard keyboardShouldPersistTaps != "always" else { return }
373
373
  let point = keyboardTapGesture.location(in: self)
374
- var hit = hitTest(point, with: nil)
375
- var onControl = false
376
- var onButton = false
377
- while let view = hit, view !== self {
378
- if view is UIButton { onButton = true }
379
- if view is UIControl || !(view.gestureRecognizers ?? []).isEmpty { onControl = true; break }
380
- hit = view.superview
374
+ craftHandleKeyboardTap(in: self, hit: hitTest(point, with: nil), persistTaps: keyboardShouldPersistTaps) {
375
+ self.craftScreenController?.forgetFocusedInput()
381
376
  }
382
- if keyboardShouldPersistTaps == "handled", onControl { return }
383
- // A button keeps the keyboard: its press gives the focus straight
384
- // back. Ending editing first only let the keyboard's inset go while
385
- // the finger was down, the button moved out from under it, and the
386
- // press was lost.
387
- if onButton { return }
388
- // Otherwise the keyboard goes; after a tap on nothing, for good (the
389
- // next render must not bring the last input back).
390
- if !onControl { craftScreenController?.forgetFocusedInput() }
391
- window?.endEditing(true)
392
377
  }
393
378
 
394
379
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@@ -3954,6 +3954,23 @@ func craftScrollGesture(_ gesture: UIGestureRecognizer, runsWith other: UIGestur
3954
3954
  return false
3955
3955
  }
3956
3956
 
3957
+ /// Scroll containers observe taps without cancelling native input focus or
3958
+ /// selection. Text views are not UIControls, and their selection subviews may
3959
+ /// have recognizers of their own, so inspect the whole ancestor chain.
3960
+ func craftHandleKeyboardTap(in container: UIView, hit: UIView?, persistTaps: String, forgetFocus: () -> Void) {
3961
+ guard persistTaps != "always" else { return }
3962
+ var candidate = hit
3963
+ var onControl = false
3964
+ while let view = candidate, view !== container {
3965
+ if view is UITextField || view is UITextView || view is UIButton { return }
3966
+ if view is UIControl || !(view.gestureRecognizers ?? []).isEmpty { onControl = true }
3967
+ candidate = view.superview
3968
+ }
3969
+ if persistTaps == "handled", onControl { return }
3970
+ if !onControl { forgetFocus() }
3971
+ container.window?.endEditing(true)
3972
+ }
3973
+
3957
3974
  private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDelegate {
3958
3975
  let contentStack = CraftNativeFlowView()
3959
3976
  private let keyboardTapGesture = UITapGestureRecognizer()
@@ -4226,24 +4243,9 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
4226
4243
  @objc private func keyboardTap() {
4227
4244
  guard keyboardShouldPersistTaps != "always" else { return }
4228
4245
  let point = keyboardTapGesture.location(in: self)
4229
- var hit = hitTest(point, with: nil)
4230
- var onControl = false
4231
- var onButton = false
4232
- while let view = hit, view !== self {
4233
- if view is UIButton { onButton = true }
4234
- if view is UIControl || !(view.gestureRecognizers ?? []).isEmpty { onControl = true; break }
4235
- hit = view.superview
4236
- }
4237
- if keyboardShouldPersistTaps == "handled", onControl { return }
4238
- // A button keeps the keyboard: its press gives the focus straight
4239
- // back. Ending editing first only let the keyboard's inset go while
4240
- // the finger was down, the button moved out from under it, and the
4241
- // press was lost.
4242
- if onButton { return }
4243
- // Otherwise the keyboard goes; after a tap on nothing, for good (the
4244
- // next render must not bring the last input back).
4245
- if !onControl { craftScreenController?.forgetFocusedInput() }
4246
- window?.endEditing(true)
4246
+ craftHandleKeyboardTap(in: self, hit: hitTest(point, with: nil), persistTaps: keyboardShouldPersistTaps) {
4247
+ self.craftScreenController?.forgetFocusedInput()
4248
+ }
4247
4249
  }
4248
4250
 
4249
4251
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@@ -68,6 +68,26 @@ final class DriverTests: XCTestCase {
68
68
  return result
69
69
  }
70
70
 
71
+ /// The journey, from the runner's control server, asked for again
72
+ /// until a minute has passed.
73
+ ///
74
+ /// The server is up before the test starts, but a freshly created
75
+ /// simulator's first request can outlast one wait: the first journey of
76
+ /// a CI run spent 15s on it and failed before doing anything. XCTWaiter
77
+ /// reports a timeout instead of failing the test, so a slow first answer
78
+ /// is retried rather than fatal.
79
+ func remoteScript(seconds: Double = 60) -> Data? {
80
+ let end = Date().addingTimeInterval(seconds)
81
+ repeat {
82
+ var result: Data?
83
+ let done = XCTestExpectation(description: "/script")
84
+ URLSession.shared.dataTask(with: URL(string: control + "/script")!) { data, _, _ in result = data; done.fulfill() }.resume()
85
+ if XCTWaiter().wait(for: [done], timeout: 15) == .completed, let result, !result.isEmpty { return result }
86
+ Thread.sleep(forTimeInterval: 1)
87
+ } while Date() < end
88
+ return nil
89
+ }
90
+
71
91
  func post(_ path: String, _ body: Data) {
72
92
  if bundledJourney {
73
93
  guard path.hasPrefix("/save?"),
@@ -139,6 +159,30 @@ final class DriverTests: XCTestCase {
139
159
  return nil
140
160
  }
141
161
 
162
+ /// About a third of a screen of scrolling, as a held drag.
163
+ ///
164
+ /// `app.swipeUp()` is a flick, and how far a flick throws a scroll view
165
+ /// depends on how its touches arrive: on a loaded machine, with a map
166
+ /// keeping the app busy, eight of them moved a trail page less than one
167
+ /// screen. A drag that stops before it lifts moves the content by its
168
+ /// own length every time. It runs between the middle of the screen and
169
+ /// the upper part, where a flick began too, so it stays off the keys when
170
+ /// a keyboard is up - an iOS 27 simulator reports its keyboard's frame
171
+ /// below the screen while drawing it over the lower half - and it stops
172
+ /// short of a keyboard whose frame is known.
173
+ func scrollPage(towardTop: Bool) {
174
+ let frame = app.frame
175
+ var lower = frame.minY + frame.height * 0.55
176
+ let keyboard = app.keyboards.firstMatch
177
+ if keyboard.exists, !keyboard.frame.isEmpty, keyboard.frame.minY < frame.maxY {
178
+ lower = min(lower, keyboard.frame.minY - 24)
179
+ }
180
+ let upper = max(frame.minY + frame.height * 0.15, lower - frame.height * 0.4)
181
+ let point = { (y: CGFloat) in self.app.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: frame.midX, dy: y)) }
182
+ let (from, to) = towardTop ? (point(upper), point(lower)) : (point(lower), point(upper))
183
+ from.press(forDuration: 0.05, thenDragTo: to, withVelocity: .default, thenHoldForDuration: 0.2)
184
+ }
185
+
142
186
  /// Whether `target`, or a field inside it, holds the keyboard.
143
187
  func hasKeyboardFocus(_ target: XCUIElement) -> Bool {
144
188
  if (target.value(forKey: "hasKeyboardFocus") as? Bool) == true { return true }
@@ -222,7 +266,7 @@ final class DriverTests: XCTestCase {
222
266
  bundledJourney = true
223
267
  data = try Data(contentsOf: resource)
224
268
  } else {
225
- data = get("/script")
269
+ data = remoteScript()
226
270
  }
227
271
  guard let data, let script = String(data: data, encoding: .utf8) else {
228
272
  XCTFail("no script")
@@ -478,14 +522,11 @@ final class DriverTests: XCTestCase {
478
522
  else { XCTFail("Element not found"); note("FAIL tab \(arg): NOT FOUND") }
479
523
  case "scrollto":
480
524
  var found = element(containing: arg)
481
- var swipes = 0
482
- while (found == nil || !(found!.isHittable)) && swipes < 8 {
483
- if let target = found, !target.frame.isEmpty, target.frame.midY < app.frame.midY {
484
- app.swipeDown()
485
- } else {
486
- app.swipeUp()
487
- }
488
- swipes += 1
525
+ var drags = 0
526
+ while (found == nil || !(found!.isHittable)) && drags < 12 {
527
+ let towardTop = found.map { !$0.frame.isEmpty && $0.frame.midY < app.frame.midY } ?? false
528
+ scrollPage(towardTop: towardTop)
529
+ drags += 1
489
530
  found = element(containing: arg)
490
531
  }
491
532
  check(found != nil && found!.isHittable, "scrollto \(arg)")