Skip to content

Commit

Permalink
[MAPSIOS-1309] View Annotations's taps propagate to the map (#2055)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksproger authored Mar 6, 2024
1 parent a076ddb commit 4709720
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct ViewAnnotationsExample: View {
.allowOverlap(allowOverlap)
// Allow bottom, top, left, right positions of anchor.
.variableAnchors(
[ViewAnnotationAnchor.bottom, .top, .left, .right].map { .init(anchor: $0) }
[ViewAnnotationAnchor.bottom, .bottomLeft, .bottomRight].map { .init(anchor: $0) }
)
.onAnchorChanged { config in
guard let idx = taps.firstIndex(where: { $0.id == tap.id }) else { return }
Expand Down Expand Up @@ -82,6 +82,9 @@ struct ViewAnnotationsExample: View {
// Add bottom padding for the bottom config panel, View Annotations won't appear there.
.additionalSafeAreaInsets(.bottom, overlayHeight)
.ignoresSafeArea(edges: [.leading, .trailing, .bottom])
.onTapGesture {
print("SwiftUI view tap received.")
}
.safeOverlay(alignment: .bottom) {
VStack(alignment: .leading) {
Text("Tap to add annotations")
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Mapbox welcomes participation and contributions from everyone.
* Update the minimum Xcode version to 15.2 (Swift 5.9).
* Add `onClusterTap` and `onClusterLongPress` to AnnotationManagers(UIKit) and AnnotationGroups(SwiftUI) which support clustering
* Add annotations drag handlers callbacks `dragBeginHandler`, `dragChangeHandler`, `dragEndHandler` to all Annotation types.
* Add `allowHistTesting` modifier on `MapViewAnnotation`.
* Fix taps propagation on `ViewAnnotation`.
* Bump core maps version to 11.2.0 and common sdk to 24.2.0.

## 11.2.0 - 28 February, 2024
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxMaps/Foundation/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ open class MapView: UIView, SizeTrackingLayerDelegate {
}

/// The underlying metal view that is used to render the map
internal private(set) var metalView: MetalView?
private(set) var metalView: MetalView?

private let cameraViewContainerView = UIView()

Expand Down
16 changes: 9 additions & 7 deletions Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import MetalKit

internal protocol MapViewDependencyProviderProtocol: AnyObject {
protocol MapViewDependencyProviderProtocol: AnyObject {
var notificationCenter: NotificationCenterProtocol { get }
var bundle: BundleProtocol { get }
func makeMetalView(frame: CGRect, device: MTLDevice?) -> MetalView
Expand Down Expand Up @@ -31,7 +31,7 @@ internal protocol MapViewDependencyProviderProtocol: AnyObject {
func makeEventsManager() -> EventsManagerProtocol
}

internal final class MapViewDependencyProvider: MapViewDependencyProviderProtocol {
final class MapViewDependencyProvider: MapViewDependencyProviderProtocol {
internal let notificationCenter: NotificationCenterProtocol = NotificationCenter.default

internal let bundle: BundleProtocol = Bundle.main
Expand Down Expand Up @@ -179,11 +179,13 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco
cameraAnimationsManager: cameraAnimationsManager)
}

internal func makeGestureManager(view: UIView,
mapboxMap: MapboxMapProtocol,
mapFeatureQueryable: MapFeatureQueryable,
annotations: AnnotationOrchestratorImplProtocol,
cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureManager {
func makeGestureManager(
view: UIView,
mapboxMap: MapboxMapProtocol,
mapFeatureQueryable: MapFeatureQueryable,
annotations: AnnotationOrchestratorImplProtocol,
cameraAnimationsManager: CameraAnimationsManagerProtocol
) -> GestureManager {
let singleTap = makeSingleTapGestureHandler(
view: view,
cameraAnimationsManager: cameraAnimationsManager)
Expand Down
10 changes: 10 additions & 0 deletions Sources/MapboxMaps/Foundation/MetalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ class MetalView: UIView, CoreMetalView {
fatalError("init(coder:) has not been implemented")
}

open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}

func draw() {
onRender?()
}
Expand Down Expand Up @@ -80,5 +85,10 @@ class MetalView: MTKView, CoreMetalView {
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final class ViewAnnotationsContainer: UIView {
}

/// Forwards all touch events to its subviews
internal override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import UIKit

/// `SingleTapGestureHandler` manages a gesture recognizer looking for single tap touch events
internal final class SingleTapGestureHandler: GestureHandler {
final class SingleTapGestureHandler: GestureHandler {
var onTap: Signal<CGPoint> { onTapSubject.signal }
private let onTapSubject = SignalSubject<CGPoint>()

private let cameraAnimationsManager: CameraAnimationsManagerProtocol

internal init(gestureRecognizer: UITapGestureRecognizer,
cameraAnimationsManager: CameraAnimationsManagerProtocol) {
init(
gestureRecognizer: UITapGestureRecognizer,
cameraAnimationsManager: CameraAnimationsManagerProtocol
) {
self.cameraAnimationsManager = cameraAnimationsManager
gestureRecognizer.numberOfTapsRequired = 1
gestureRecognizer.numberOfTouchesRequired = 1
Expand Down Expand Up @@ -39,4 +41,18 @@ extension SingleTapGestureHandler: UIGestureRecognizerDelegate {

return otherGestureRecognizer is UITapGestureRecognizer
}

func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool {
assert(self.gestureRecognizer == gestureRecognizer)

/// Only handle touches that targeting the map not view annotations.
guard gestureRecognizer.attachedToSameView(as: touch) else {
return false
}

return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ extension UIGestureRecognizer {
view === other.view
}
}

/// Can be used to decide whether the recognizer should receive the touch.
/// 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.
extension UIGestureRecognizer {
func attachedToSameView(as touch: UITouch) -> Bool {
view === touch.view
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public struct MapViewAnnotation: MapContent {
var annotatedFeature: AnnotatedFeature
var allowOverlap: Bool = false
var visible: Bool = true
var allowHitTesting: Bool = true
var selected: Bool = false
var allowOverlapWithPuck: Bool = false
var ignoreCameraPadding: Bool = false
Expand Down Expand Up @@ -93,6 +94,12 @@ public struct MapViewAnnotation: MapContent {
with(self, setter(\.allowOverlap, allowOverlap))
}

/// Configures whether this view participates in hit test operations. Defaults to `true`.
@_documentation(visibility: public)
public func allowHitTesting(_ allowHitTesting: Bool) -> MapViewAnnotation {
with(self, setter(\.allowHitTesting, allowHitTesting))
}

/// When `false`, the annotation won't be shown on top of Puck.
///
/// Default value is `false`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,16 @@ private struct DisplayedViewAnnotation {
weak var annotation: ViewAnnotation?

let wrapContent = { (content: AnyView) in
content.fixedSize().onChangeOfSize { _ in
annotation?.setNeedsUpdateSize()
}
content
.fixedSize()
.allowsHitTesting(viewAnnotation.allowHitTesting)
.onChangeOfSize { _ in
annotation?.setNeedsUpdateSize()
}
}

let vc = UIHostingController(rootView: wrapContent(viewAnnotation.content))

self.viewController = vc
self._update = { content in
vc.rootView = wrapContent(content)
Expand All @@ -89,6 +93,7 @@ private struct DisplayedViewAnnotation {
update(with: viewAnnotation)

vc.view.backgroundColor = .clear
vc.view.isUserInteractionEnabled = viewAnnotation.allowHitTesting
}

func update(with viewAnnotation: MapViewAnnotation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import XCTest
@testable import MapboxMaps

final class SingleTapGestureHandlerTests: XCTestCase {

var gestureRecognizer: MockTapGestureRecognizer!
var cameraAnimationsManager: MockCameraAnimationsManager!
var gestureHandler: SingleTapGestureHandler!
Expand Down Expand Up @@ -64,4 +63,35 @@ final class SingleTapGestureHandlerTests: XCTestCase {
func testShouldRecognizeSimultaneouslyWithAnyRecognizerAttachedToDifferentView() {
gestureHandler.assertRecognizedSimultaneously(gestureRecognizer, with: interruptingRecognizers)
}

func testShouldNotReceiveTouchTargetingDifferentView() {
let touch = MockUITouch(view: UIView())

let isHandled = gestureHandler.gestureRecognizer(
gestureRecognizer,
shouldReceive: touch
)

XCTAssertFalse(isHandled)
}

func testShouldReceiveTouchTargetingSameView() {
let touch = MockUITouch(view: gestureRecognizer.view)

let isHandled = gestureHandler.gestureRecognizer(
gestureRecognizer,
shouldReceive: touch
)

XCTAssertTrue(isHandled)
}
}

private final class MockUITouch: UITouch {
override var view: UIView { _view }
private let _view: UIView

init(view: UIView) {
self._view = view
}
}

0 comments on commit 4709720

Please sign in to comment.