@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
|
@@ -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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
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 =
|
|
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
|
|
482
|
-
while (found == nil || !(found!.isHittable)) &&
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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)")
|