also looking at this
fix(native): interaction, audio, document and checkpoint gaps stranded after #401
#415
19 files
+987
-50
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -4295,6 +4295,36 @@ final class CraftMapTapDelegate: NSObject, UIGestureRecognizerDelegate { | ||
| 4295 | 4295 | } |
| 4296 | 4296 | } |
| 4297 | 4297 | |
| 4298 | /// A pressable wrapper must yield to an actionable descendant. Without this | |
| 4299 | /// ordering UIKit may let the ancestor's tap recognizer win the same touch, | |
| 4300 | /// so a nested Button never receives its physical action. A descendant | |
| 4301 | /// control or tap recognizer is the source of the event; STX handles the | |
| 4302 | /// logical bubbling from there. | |
| 4303 | final class CraftPressGestureDelegate: NSObject, UIGestureRecognizerDelegate { | |
| 4304 | static let shared = CraftPressGestureDelegate() | |
| 4305 | ||
| 4306 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { | |
| 4307 | guard let parent = gestureRecognizer.view, | |
| 4308 | let child = otherGestureRecognizer.view, | |
| 4309 | child !== parent else { return false } | |
| 4310 | return child.isDescendant(of: parent) | |
| 4311 | } | |
| 4312 | ||
| 4313 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { | |
| 4314 | guard let container = gestureRecognizer.view else { return true } | |
| 4315 | let hitView = container.hitTest(touch.location(in: container), with: nil) | |
| 4316 | guard var candidate = hitView ?? touch.view else { return true } | |
| 4317 | while candidate !== container { | |
| 4318 | if candidate is UIControl { return false } | |
| 4319 | if candidate.gestureRecognizers?.contains(where: { $0 !== gestureRecognizer && $0 is UITapGestureRecognizer }) == true { | |
| 4320 | return false | |
| 4321 | } | |
| 4322 | candidate = candidate.superview ?? container | |
| 4323 | } | |
| 4324 | return true | |
| 4325 | } | |
| 4326 | } | |
| 4327 | ||
| 4298 | 4328 | /// The delegate of a Pressable's press-in/out feedback. That recognizer is |
| 4299 | 4329 | /// a long press of no duration, so it begins the moment a finger lands; on its |
| 4300 | 4330 | /// own it would claim the touch, and the tap that is the Pressable's onPress |
| @@ -9148,6 +9178,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 9148 | 9178 | let actionable = handler != nil || linkTargets[id] != nil |
| 9149 | 9179 | if actionable, tapRecognizers[id] == nil { |
| 9150 | 9180 | let recognizer = UITapGestureRecognizer(target: self, action: #selector(viewPressed(_:))) |
| 9181 | recognizer.cancelsTouchesInView = false | |
| 9182 | recognizer.delegate = CraftPressGestureDelegate.shared | |
| 9151 | 9183 | tapRecognizers[id] = recognizer |
| 9152 | 9184 | view.addGestureRecognizer(recognizer) |
| 9153 | 9185 | } else if !actionable, let recognizer = tapRecognizers.removeValue(forKey: id) { |