Skip to content

Commit d22e1e2

Browse files
mapbox-github-ci-writer-2[bot]pjleonard37
authored andcommitted
[Backport release/v0.19] [MAPSIOS-2100] Restore map panning on ViewAnnotations (#10338)
Backport e1ef22e3502b82bb6f386dda4666a5971f91ccf4 from #10186. Co-authored-by: Patrick Leonard <pjleonard37@users.noreply.github.com> GitOrigin-RevId: 2717ac3246ccdd0be1a3da6d7ffc34d205205da5
1 parent a501360 commit d22e1e2

10 files changed

Lines changed: 172 additions & 19 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Mapbox welcomes participation and contributions from everyone.
44

55
## main
66

7+
### Features ✨ and improvements 🏁
8+
* Add animation to experimental `Marker` with two animation triggers: `appear` and `disappear`. Each trigger accepts `MarkerAnimationEffect` including `wiggle` (pendulum rotation), `scale`, `fadeIn`, and `fadeOut`. Effects can be customized with parameters (e.g., `scale(from: 0.5, to: 1.5)`, `fade(from: 0.5, to: 1.0)`) and combined for rich animations. See `MarkersExample` for usage.
9+
10+
### Bug fixes 🐞
11+
* Fix map panning not working on ViewAnnotations
12+
713
## 11.19.0-rc.1 - 12 February, 2026
814

915
## 11.19.0-rc.1

Sources/MapboxMaps/Foundation/ViewAnnotationsContainer.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import UIKit
22

3-
final class ViewAnnotationsContainer: UIView {
3+
/// Views conforming to this protocol allow map gestures to pass through them.
4+
protocol AllowsMapGestures {}
5+
6+
final class ViewAnnotationsContainer: UIView, AllowsMapGestures {
47
var subviewDebugFrames: Bool = false {
58
didSet {
69
if subviewDebugFrames != oldValue {

Sources/MapboxMaps/Gestures/GestureHandlers/DoubleTapToZoomInGestureHandler.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ extension DoubleTapToZoomInGestureHandler: UIGestureRecognizerDelegate {
4545
_ gestureRecognizer: UIGestureRecognizer,
4646
shouldReceive touch: UITouch
4747
) -> Bool {
48-
/// Only handle touches that targeting the map, but any of its subviews (including view annotations and ornaments)
4948
assert(self.gestureRecognizer == gestureRecognizer)
50-
return gestureRecognizer.attachedToSameView(as: touch)
49+
return gestureRecognizer.shouldAllowMapGesture(for: touch)
5150
}
5251
}

Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,8 @@ extension PanGestureHandler: UIGestureRecognizerDelegate {
192192
_ gestureRecognizer: UIGestureRecognizer,
193193
shouldReceive touch: UITouch
194194
) -> Bool {
195-
/// Only handle touches that targeting the map, but any of its subviews (including view annotations and ornaments).
196195
assert(self.gestureRecognizer == gestureRecognizer)
197-
return gestureRecognizer.attachedToSameView(as: touch)
196+
return gestureRecognizer.shouldAllowMapGesture(for: touch)
198197
}
199198
}
200199

Sources/MapboxMaps/Gestures/GestureHandlers/PinchGestureHandler.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,8 @@ extension PinchGestureHandler: UIGestureRecognizerDelegate {
9797
_ gestureRecognizer: UIGestureRecognizer,
9898
shouldReceive touch: UITouch
9999
) -> Bool {
100-
/// Only handle touches that targeting the map, but any of its subviews (including view annotations and ornaments)
101100
assert(self.gestureRecognizer == gestureRecognizer)
102-
return gestureRecognizer.attachedToSameView(as: touch)
101+
return gestureRecognizer.shouldAllowMapGesture(for: touch)
103102
}
104103

105104
func gestureRecognizer(

Sources/MapboxMaps/Gestures/GestureHandlers/QuickZoomGestureHandler.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ extension QuickZoomGestureHandler: UIGestureRecognizerDelegate {
5555
_ gestureRecognizer: UIGestureRecognizer,
5656
shouldReceive touch: UITouch
5757
) -> Bool {
58-
/// Only handle touches that targeting the map, but any of its subviews (including view annotations and ornaments)
5958
assert(self.gestureRecognizer == gestureRecognizer)
60-
return gestureRecognizer.attachedToSameView(as: touch)
59+
return gestureRecognizer.shouldAllowMapGesture(for: touch)
6160
}
6261
}

Sources/MapboxMaps/Gestures/GestureHandlers/RotateGestureHandler.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ extension RotateGestureHandler: UIGestureRecognizerDelegate {
105105
_ gestureRecognizer: UIGestureRecognizer,
106106
shouldReceive touch: UITouch
107107
) -> Bool {
108-
/// Only handle touches that targeting the map, but any of its subviews (including view annotations and ornaments)
109108
assert(self.gestureRecognizer == gestureRecognizer)
110-
return gestureRecognizer.attachedToSameView(as: touch)
109+
return gestureRecognizer.shouldAllowMapGesture(for: touch)
111110
}
112111
}

Sources/MapboxMaps/Gestures/GestureHandlers/SingleTapGestureHandler.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ extension SingleTapGestureHandler: UIGestureRecognizerDelegate {
4444
_ gestureRecognizer: UIGestureRecognizer,
4545
shouldReceive touch: UITouch
4646
) -> Bool {
47-
/// Only handle touches that targeting the map, but any of its subviews (including view annotations and ornaments)
4847
assert(self.gestureRecognizer == gestureRecognizer)
49-
return gestureRecognizer.attachedToSameView(as: touch)
48+
return gestureRecognizer.shouldAllowMapGesture(for: touch)
5049
}
5150
}
Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,57 @@
11
import UIKit
22

3-
/// Can be used to distinguish between recognizers attached to the SwiftUI hosting view and recognizers added tp underlying UIKit view
4-
/// SwiftUI API's add recognizers to the hosting view
53
extension UIGestureRecognizer {
4+
// MARK: - SwiftUI Compatibility
5+
6+
/// Checks if two gesture recognizers are attached to the same view.
7+
/// Used to distinguish between recognizers attached to the SwiftUI hosting view
8+
/// and recognizers added to the underlying UIKit view.
69
func attachedToSameView(as other: UIGestureRecognizer) -> Bool {
710
view === other.view
811
}
9-
}
1012

11-
/// Can be used to decide whether the recognizer should receive the touch.
12-
/// In case where the recognizer is known to be attached to some view we may ignore any touches that is going to be delievered to unrelated view.
13-
extension UIGestureRecognizer {
13+
/// Checks if a touch is directly on the same view as the gesture recognizer.
14+
/// Returns true only if `touch.view === gestureRecognizer.view`.
1415
func attachedToSameView(as touch: UITouch) -> Bool {
1516
view === touch.view
1617
}
18+
19+
// MARK: - Map Gesture Filtering
20+
21+
/// Determines whether a map gesture should be allowed for the given touch.
22+
///
23+
/// This method walks up the view hierarchy from the touch location to determine if the gesture
24+
/// should be recognized. Map gestures are allowed when:
25+
/// - The touch is directly on the map view, OR
26+
/// - The touch is on a view that conforms to `AllowsMapGestures` (e.g., `ViewAnnotationsContainer`)
27+
///
28+
/// Map gestures are blocked when:
29+
/// - The touch is on UI controls/ornaments (compass, scale bar, indoor selector, etc.)
30+
/// - The touch is outside the map view hierarchy
31+
///
32+
/// - Parameter touch: The touch to evaluate
33+
/// - Returns: `true` if the map gesture should be recognized, `false` otherwise
34+
func shouldAllowMapGesture(for touch: UITouch) -> Bool {
35+
guard let gestureView = view, let touchView = touch.view else {
36+
return false
37+
}
38+
39+
// Allow touches directly on the map view itself
40+
if touchView === gestureView {
41+
return true
42+
}
43+
44+
// Walk up the view hierarchy to find a view that allows map gestures
45+
// Stop before reaching the gesture view (to block direct children like ornaments)
46+
var currentView: UIView? = touchView
47+
while let view = currentView, view !== gestureView {
48+
if view is AllowsMapGestures {
49+
return true
50+
}
51+
currentView = view.superview
52+
}
53+
54+
// Touch is on a direct child (ornament) or outside hierarchy - reject
55+
return false
56+
}
1757
}

Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,4 +489,114 @@ final class PanGestureHandlerTests: XCTestCase {
489489

490490
panGestureHandler.assertNotRecognizedSimultaneously(gestureRecognizer, with: interruptingRecognizers)
491491
}
492+
493+
// MARK: - shouldReceive touch tests
494+
495+
func testShouldReceiveTouchOnMapView() {
496+
// Given: A touch directly on the map view
497+
let touch = MockUITouch(view: view)
498+
499+
// When: Checking if gesture should receive the touch
500+
let shouldReceive = panGestureHandler.gestureRecognizer(gestureRecognizer, shouldReceive: touch)
501+
502+
// Then: Should accept the touch
503+
XCTAssertTrue(shouldReceive, "Gesture should receive touches directly on the map view")
504+
}
505+
506+
func testShouldReceiveTouchOnViewAnnotation() {
507+
// Given: A ViewAnnotationsContainer (conforms to AllowsMapGestures)
508+
let viewAnnotationsContainer = ViewAnnotationsContainer()
509+
view.addSubview(viewAnnotationsContainer)
510+
511+
// And: A view annotation inside the container
512+
let annotationView = UIView()
513+
viewAnnotationsContainer.addSubview(annotationView)
514+
515+
// And: A touch on the annotation view
516+
let touch = MockUITouch(view: annotationView)
517+
518+
// When: Checking if gesture should receive the touch
519+
let shouldReceive = panGestureHandler.gestureRecognizer(gestureRecognizer, shouldReceive: touch)
520+
521+
// Then: Should accept the touch (this would have failed before the fix)
522+
XCTAssertTrue(shouldReceive, "Gesture should receive touches on view annotations to allow map panning")
523+
}
524+
525+
func testShouldReceiveTouchOnViewAnnotationsContainer() {
526+
// Given: A ViewAnnotationsContainer
527+
let viewAnnotationsContainer = ViewAnnotationsContainer()
528+
view.addSubview(viewAnnotationsContainer)
529+
530+
// And: A touch directly on the container
531+
let touch = MockUITouch(view: viewAnnotationsContainer)
532+
533+
// When: Checking if gesture should receive the touch
534+
let shouldReceive = panGestureHandler.gestureRecognizer(gestureRecognizer, shouldReceive: touch)
535+
536+
// Then: Should accept the touch
537+
XCTAssertTrue(shouldReceive, "Gesture should receive touches on ViewAnnotationsContainer")
538+
}
539+
540+
func testShouldNotReceiveTouchOnOrnament() {
541+
// Given: A regular UIView representing an ornament (does not conform to AllowsMapGestures)
542+
let ornamentView = UIView()
543+
view.addSubview(ornamentView)
544+
545+
// And: A touch on the ornament
546+
let touch = MockUITouch(view: ornamentView)
547+
548+
// When: Checking if gesture should receive the touch
549+
let shouldReceive = panGestureHandler.gestureRecognizer(gestureRecognizer, shouldReceive: touch)
550+
551+
// Then: Should reject the touch to allow ornament to handle it
552+
XCTAssertFalse(shouldReceive, "Gesture should not receive touches on ornaments/UI controls")
553+
}
554+
555+
func testShouldNotReceiveTouchOutsideMapView() {
556+
// Given: A view outside the map view hierarchy
557+
let externalView = UIView()
558+
559+
// And: A touch on the external view
560+
let touch = MockUITouch(view: externalView)
561+
562+
// When: Checking if gesture should receive the touch
563+
let shouldReceive = panGestureHandler.gestureRecognizer(gestureRecognizer, shouldReceive: touch)
564+
565+
// Then: Should reject the touch
566+
XCTAssertFalse(shouldReceive, "Gesture should not receive touches outside the map view hierarchy")
567+
}
568+
569+
func testShouldReceiveTouchOnNestedViewAnnotation() {
570+
// Given: A ViewAnnotationsContainer
571+
let viewAnnotationsContainer = ViewAnnotationsContainer()
572+
view.addSubview(viewAnnotationsContainer)
573+
574+
// And: A nested view hierarchy inside a view annotation
575+
let annotationView = UIView()
576+
viewAnnotationsContainer.addSubview(annotationView)
577+
let button = UIButton()
578+
annotationView.addSubview(button)
579+
let label = UILabel()
580+
button.addSubview(label)
581+
582+
// And: A touch on a deeply nested view
583+
let touch = MockUITouch(view: label)
584+
585+
// When: Checking if gesture should receive the touch
586+
let shouldReceive = panGestureHandler.gestureRecognizer(gestureRecognizer, shouldReceive: touch)
587+
588+
// Then: Should accept the touch (walks up hierarchy to find AllowsMapGestures)
589+
XCTAssertTrue(shouldReceive, "Gesture should receive touches on nested views within view annotations")
590+
}
591+
}
592+
593+
// MARK: - MockUITouch
594+
595+
private final class MockUITouch: UITouch {
596+
override var view: UIView? { _view }
597+
private let _view: UIView
598+
599+
init(view: UIView) {
600+
self._view = view
601+
}
492602
}

0 commit comments

Comments
 (0)