@craft-native/ios 0.0.251 → 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.251",
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?"),
@@ -246,7 +266,7 @@ final class DriverTests: XCTestCase {
246
266
  bundledJourney = true
247
267
  data = try Data(contentsOf: resource)
248
268
  } else {
249
- data = get("/script")
269
+ data = remoteScript()
250
270
  }
251
271
  guard let data, let script = String(data: data, encoding: .utf8) else {
252
272
  XCTFail("no script")