From 303012f1a723fc5eb08a16d5e4b60c22ae37adc9 Mon Sep 17 00:00:00 2001 From: PW Date: Sat, 30 Mar 2024 19:31:49 +0100 Subject: [PATCH 1/3] adding example usage, fixing double tap issues --- .../MapLibreSwiftUI/Examples/Gestures.swift | 40 +++++++++++++++++++ .../Extensions/MapView/MapViewGestures.swift | 23 ++++++++++- Sources/MapLibreSwiftUI/MapView.swift | 1 + .../MapLibreSwiftUI/MapViewModifiers.swift | 17 +++++++- .../Models/Gesture/MapGesture.swift | 10 ++++- .../MapView/MapViewGestureTests.swift | 11 +++-- .../Models/Gesture/MapGestureTests.swift | 16 ++++++-- 7 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 Sources/MapLibreSwiftUI/Examples/Gestures.swift diff --git a/Sources/MapLibreSwiftUI/Examples/Gestures.swift b/Sources/MapLibreSwiftUI/Examples/Gestures.swift new file mode 100644 index 0000000..cef027f --- /dev/null +++ b/Sources/MapLibreSwiftUI/Examples/Gestures.swift @@ -0,0 +1,40 @@ +// +// File.swift +// +// +// Created by patrick on 29.03.24. +// + +import CoreLocation +import MapLibre +import MapLibreSwiftDSL +import SwiftUI + + +#Preview("Tappable Circles") { + let tappableID = "simple-circles" + return MapView(styleURL: demoTilesURL) { + // Simple symbol layer demonstration with an icon + CircleStyleLayer(identifier: tappableID, source: pointSource) + .radius(16) + .color(.systemRed) + .strokeWidth(2) + .strokeColor(.white) + + SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) + .iconImage(UIImage(systemName: "mappin")!.withRenderingMode(.alwaysTemplate)) + .iconColor(.white) + } + .onTapMapGesture(on: [tappableID], onTapChanged: { _, features in + print("Tapped on \(features.first)") + }) + .ignoresSafeArea(.all) +} + +#Preview("Tappable Countries") { + MapView(styleURL: demoTilesURL) + .onTapMapGesture(on: ["countries-fill"], onTapChanged: { _, features in + print("Tapped on \(features.first)") + }) + .ignoresSafeArea(.all) +} diff --git a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift index 34f46e3..93a0061 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift @@ -15,6 +15,13 @@ extension MapView { let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.captureGesture(_:))) gestureRecognizer.numberOfTapsRequired = numberOfTaps + if numberOfTaps == 1 { + // If a user double taps to zoom via the built in gesture, a normal + // tap should not be triggered. + if let doubleTapRecognizer = mapView.gestureRecognizers?.first(where: { $0 is UITapGestureRecognizer && ($0 as! UITapGestureRecognizer).numberOfTapsRequired == 2 }) { + gestureRecognizer.require(toFail: doubleTapRecognizer) + } + } mapView.addGestureRecognizer(gestureRecognizer) gesture.gestureRecognizer = gestureRecognizer @@ -42,15 +49,29 @@ extension MapView { /// gesture. /// - sender: The UIGestureRecognizer @MainActor func processGesture(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) { + + guard sender.state == .ended else { + // We should only process gestures that have ended, else built in gestures like double tapping the map + // interfere with ours. + return + } guard let gesture = gestures.first(where: { $0.gestureRecognizer == sender }) else { assertionFailure("\(sender) is not a registered UIGestureRecongizer on the MapView") return } + // Process the gesture into a context response. let context = processContextFromGesture(mapView, gesture: gesture, sender: sender) // Run the context through the gesture held on the MapView (emitting to the MapView modifier). - gesture.onChange(context) + switch gesture.onChange { + case .context(let action): + action(context) + case .feature(let action, let layers): + let point = sender.location(in: sender.view) + let features = mapView.visibleFeatures(at: point, styleLayerIdentifiers: layers) + action(context, features) + } } /// Convert the sender data into a MapGestureContext diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 5b10d9e..99f51e8 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -10,6 +10,7 @@ public struct MapView: UIViewRepresentable { let userLayers: [StyleLayerDefinition] var gestures = [MapGesture]() + var onStyleLoaded: ((MLNStyle) -> Void)? public var mapViewContentInset: UIEdgeInsets = .zero diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 7f4641a..583f3b6 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -59,11 +59,24 @@ public extension MapView { // Build the gesture and link it to the map view. let gesture = MapGesture(method: .tap(numberOfTaps: count), - onChange: onTapChanged) + onChange: .context(onTapChanged)) newMapView.gestures.append(gesture) return newMapView } + + func onTapMapGesture(count: Int = 1, on layers: Set?, + onTapChanged: @escaping (MapGestureContext, [any MLNFeature]) -> Void) -> MapView + { + var newMapView = self + + // Build the gesture and link it to the map view. + let gesture = MapGesture(method: .tap(numberOfTaps: count), + onChange: .feature(onTapChanged, layers: layers)) + newMapView.gestures.append(gesture) + + return newMapView + } /// Add a long press gesture handler ot the MapView /// @@ -78,7 +91,7 @@ public extension MapView { // Build the gesture and link it to the map view. let gesture = MapGesture(method: .longPress(minimumDuration: minimumDuration), - onChange: onPressChanged) + onChange: .context(onPressChanged)) newMapView.gestures.append(gesture) return newMapView diff --git a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift index 35ebff0..228269e 100644 --- a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift +++ b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift @@ -1,4 +1,5 @@ import UIKit +import MapLibre public class MapGesture: NSObject { public enum Method: Equatable { @@ -19,7 +20,7 @@ public class MapGesture: NSObject { let method: Method /// The onChange action that runs when the gesture changes on the map view. - let onChange: (MapGestureContext) -> Void + let onChange: GestureAction /// The underlying gesture recognizer weak var gestureRecognizer: UIGestureRecognizer? @@ -29,8 +30,13 @@ public class MapGesture: NSObject { /// - Parameters: /// - method: The gesture recognizer method /// - onChange: The action to perform when the gesture is changed - init(method: Method, onChange: @escaping (MapGestureContext) -> Void) { + init(method: Method, onChange: GestureAction) { self.method = method self.onChange = onChange } } + +public enum GestureAction { + case context((MapGestureContext) -> Void) + case feature((MapGestureContext, [any MLNFeature]) -> Void, layers: Set?) +} diff --git a/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift b/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift index 4baacde..4ad9f86 100644 --- a/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift +++ b/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift @@ -28,9 +28,10 @@ final class MapViewGestureTests: XCTestCase { // MARK: Gesture Processing @MainActor func testTapGesture() { - let gesture = MapGesture(method: .tap(numberOfTaps: 2)) { _ in + + let gesture = MapGesture(method: .tap(numberOfTaps: 2), onChange: .context({ _ in // Do nothing - } + })) let mockTapGesture = MockUIGestureRecognizing() @@ -53,9 +54,11 @@ final class MapViewGestureTests: XCTestCase { } @MainActor func testLongPressGesture() { - let gesture = MapGesture(method: .longPress(minimumDuration: 1)) { _ in + + let gesture = MapGesture(method: .longPress(minimumDuration: 1), onChange: .context({ _ in // Do nothing - } + })) + let mockTapGesture = MockUIGestureRecognizing() diff --git a/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift b/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift index c699809..712a872 100644 --- a/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift @@ -4,7 +4,9 @@ import XCTest final class MapGestureTests: XCTestCase { func testTapGestureDefaults() { let gesture = MapGesture(method: .tap(), - onChange: { _ in }) + onChange: .context({ _ in + + })) XCTAssertEqual(gesture.method, .tap()) XCTAssertNil(gesture.gestureRecognizer) @@ -12,7 +14,9 @@ final class MapGestureTests: XCTestCase { func testTapGesture() { let gesture = MapGesture(method: .tap(numberOfTaps: 3), - onChange: { _ in }) + onChange: .context({ _ in + + })) XCTAssertEqual(gesture.method, .tap(numberOfTaps: 3)) XCTAssertNil(gesture.gestureRecognizer) @@ -20,7 +24,9 @@ final class MapGestureTests: XCTestCase { func testLongPressGestureDefaults() { let gesture = MapGesture(method: .longPress(), - onChange: { _ in }) + onChange: .context({ _ in + + })) XCTAssertEqual(gesture.method, .longPress()) XCTAssertNil(gesture.gestureRecognizer) @@ -28,7 +34,9 @@ final class MapGestureTests: XCTestCase { func testLongPressGesture() { let gesture = MapGesture(method: .longPress(minimumDuration: 3), - onChange: { _ in }) + onChange: .context({ _ in + + })) XCTAssertEqual(gesture.method, .longPress(minimumDuration: 3)) XCTAssertNil(gesture.gestureRecognizer) From d1a65d88248927d406c005642352463c1152cbe0 Mon Sep 17 00:00:00 2001 From: PW Date: Sun, 31 Mar 2024 16:35:37 +0200 Subject: [PATCH 2/3] formatting --- .../MapLibreSwiftUI/Examples/Gestures.swift | 18 ++++---------- .../Extensions/MapView/MapViewGestures.swift | 12 ++++++---- Sources/MapLibreSwiftUI/MapView.swift | 2 +- .../MapLibreSwiftUI/MapViewModifiers.swift | 6 ++--- .../Models/Gesture/MapGesture.swift | 2 +- .../MapView/MapViewGestureTests.swift | 11 ++++----- .../Models/Gesture/MapGestureTests.swift | 24 +++++++++---------- 7 files changed, 33 insertions(+), 42 deletions(-) diff --git a/Sources/MapLibreSwiftUI/Examples/Gestures.swift b/Sources/MapLibreSwiftUI/Examples/Gestures.swift index cef027f..e7f9fc3 100644 --- a/Sources/MapLibreSwiftUI/Examples/Gestures.swift +++ b/Sources/MapLibreSwiftUI/Examples/Gestures.swift @@ -1,16 +1,8 @@ -// -// File.swift -// -// -// Created by patrick on 29.03.24. -// - import CoreLocation import MapLibre import MapLibreSwiftDSL import SwiftUI - #Preview("Tappable Circles") { let tappableID = "simple-circles" return MapView(styleURL: demoTilesURL) { @@ -20,7 +12,7 @@ import SwiftUI .color(.systemRed) .strokeWidth(2) .strokeColor(.white) - + SymbolStyleLayer(identifier: "simple-symbols", source: pointSource) .iconImage(UIImage(systemName: "mappin")!.withRenderingMode(.alwaysTemplate)) .iconColor(.white) @@ -33,8 +25,8 @@ import SwiftUI #Preview("Tappable Countries") { MapView(styleURL: demoTilesURL) - .onTapMapGesture(on: ["countries-fill"], onTapChanged: { _, features in - print("Tapped on \(features.first)") - }) - .ignoresSafeArea(.all) + .onTapMapGesture(on: ["countries-fill"], onTapChanged: { _, features in + print("Tapped on \(features.first)") + }) + .ignoresSafeArea(.all) } diff --git a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift index 93a0061..7db46be 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift @@ -18,7 +18,11 @@ extension MapView { if numberOfTaps == 1 { // If a user double taps to zoom via the built in gesture, a normal // tap should not be triggered. - if let doubleTapRecognizer = mapView.gestureRecognizers?.first(where: { $0 is UITapGestureRecognizer && ($0 as! UITapGestureRecognizer).numberOfTapsRequired == 2 }) { + if let doubleTapRecognizer = mapView.gestureRecognizers? + .first(where: { + $0 is UITapGestureRecognizer && ($0 as! UITapGestureRecognizer).numberOfTapsRequired == 2 + }) + { gestureRecognizer.require(toFail: doubleTapRecognizer) } } @@ -49,7 +53,6 @@ extension MapView { /// gesture. /// - sender: The UIGestureRecognizer @MainActor func processGesture(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) { - guard sender.state == .ended else { // We should only process gestures that have ended, else built in gestures like double tapping the map // interfere with ours. @@ -59,15 +62,14 @@ extension MapView { assertionFailure("\(sender) is not a registered UIGestureRecongizer on the MapView") return } - // Process the gesture into a context response. let context = processContextFromGesture(mapView, gesture: gesture, sender: sender) // Run the context through the gesture held on the MapView (emitting to the MapView modifier). switch gesture.onChange { - case .context(let action): + case let .context(action): action(context) - case .feature(let action, let layers): + case let .feature(action, layers): let point = sender.location(in: sender.view) let features = mapView.visibleFeatures(at: point, styleLayerIdentifiers: layers) action(context, features) diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 99f51e8..dc435ab 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -10,7 +10,7 @@ public struct MapView: UIViewRepresentable { let userLayers: [StyleLayerDefinition] var gestures = [MapGesture]() - + var onStyleLoaded: ((MLNStyle) -> Void)? public var mapViewContentInset: UIEdgeInsets = .zero diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index 583f3b6..f2dea09 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -64,17 +64,17 @@ public extension MapView { return newMapView } - + func onTapMapGesture(count: Int = 1, on layers: Set?, onTapChanged: @escaping (MapGestureContext, [any MLNFeature]) -> Void) -> MapView { var newMapView = self - + // Build the gesture and link it to the map view. let gesture = MapGesture(method: .tap(numberOfTaps: count), onChange: .feature(onTapChanged, layers: layers)) newMapView.gestures.append(gesture) - + return newMapView } diff --git a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift index 228269e..a54821d 100644 --- a/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift +++ b/Sources/MapLibreSwiftUI/Models/Gesture/MapGesture.swift @@ -1,5 +1,5 @@ -import UIKit import MapLibre +import UIKit public class MapGesture: NSObject { public enum Method: Equatable { diff --git a/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift b/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift index 4ad9f86..1332d24 100644 --- a/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift +++ b/Tests/MapLibreSwiftUITests/MapView/MapViewGestureTests.swift @@ -28,10 +28,9 @@ final class MapViewGestureTests: XCTestCase { // MARK: Gesture Processing @MainActor func testTapGesture() { - - let gesture = MapGesture(method: .tap(numberOfTaps: 2), onChange: .context({ _ in + let gesture = MapGesture(method: .tap(numberOfTaps: 2), onChange: .context { _ in // Do nothing - })) + }) let mockTapGesture = MockUIGestureRecognizing() @@ -54,11 +53,9 @@ final class MapViewGestureTests: XCTestCase { } @MainActor func testLongPressGesture() { - - let gesture = MapGesture(method: .longPress(minimumDuration: 1), onChange: .context({ _ in + let gesture = MapGesture(method: .longPress(minimumDuration: 1), onChange: .context { _ in // Do nothing - })) - + }) let mockTapGesture = MockUIGestureRecognizing() diff --git a/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift b/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift index 712a872..500c312 100644 --- a/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift +++ b/Tests/MapLibreSwiftUITests/Models/Gesture/MapGestureTests.swift @@ -4,9 +4,9 @@ import XCTest final class MapGestureTests: XCTestCase { func testTapGestureDefaults() { let gesture = MapGesture(method: .tap(), - onChange: .context({ _ in - - })) + onChange: .context { _ in + + }) XCTAssertEqual(gesture.method, .tap()) XCTAssertNil(gesture.gestureRecognizer) @@ -14,9 +14,9 @@ final class MapGestureTests: XCTestCase { func testTapGesture() { let gesture = MapGesture(method: .tap(numberOfTaps: 3), - onChange: .context({ _ in - - })) + onChange: .context { _ in + + }) XCTAssertEqual(gesture.method, .tap(numberOfTaps: 3)) XCTAssertNil(gesture.gestureRecognizer) @@ -24,9 +24,9 @@ final class MapGestureTests: XCTestCase { func testLongPressGestureDefaults() { let gesture = MapGesture(method: .longPress(), - onChange: .context({ _ in - - })) + onChange: .context { _ in + + }) XCTAssertEqual(gesture.method, .longPress()) XCTAssertNil(gesture.gestureRecognizer) @@ -34,9 +34,9 @@ final class MapGestureTests: XCTestCase { func testLongPressGesture() { let gesture = MapGesture(method: .longPress(minimumDuration: 3), - onChange: .context({ _ in - - })) + onChange: .context { _ in + + }) XCTAssertEqual(gesture.method, .longPress(minimumDuration: 3)) XCTAssertNil(gesture.gestureRecognizer) From ca182dc2e62003b83d97fc7615b19ec71a3ac327 Mon Sep 17 00:00:00 2001 From: PW Date: Sun, 31 Mar 2024 17:06:00 +0200 Subject: [PATCH 3/3] adding documentation --- .../Extensions/MapView/MapViewGestures.swift | 5 ----- Sources/MapLibreSwiftUI/MapViewModifiers.swift | 13 ++++++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift index 7db46be..3c46bf0 100644 --- a/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift +++ b/Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift @@ -53,11 +53,6 @@ extension MapView { /// gesture. /// - sender: The UIGestureRecognizer @MainActor func processGesture(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) { - guard sender.state == .ended else { - // We should only process gestures that have ended, else built in gestures like double tapping the map - // interfere with ours. - return - } guard let gesture = gestures.first(where: { $0.gestureRecognizer == sender }) else { assertionFailure("\(sender) is not a registered UIGestureRecongizer on the MapView") return diff --git a/Sources/MapLibreSwiftUI/MapViewModifiers.swift b/Sources/MapLibreSwiftUI/MapViewModifiers.swift index f2dea09..5dce748 100644 --- a/Sources/MapLibreSwiftUI/MapViewModifiers.swift +++ b/Sources/MapLibreSwiftUI/MapViewModifiers.swift @@ -50,7 +50,8 @@ public extension MapView { /// /// - Parameters: /// - count: The number of taps required to run the gesture. - /// - onTapChanged: Emits the context whenever the gesture changes (e.g. began, ended, etc). + /// - onTapChanged: Emits the context whenever the gesture changes (e.g. began, ended, etc), that also contains + /// information like the latitude and longitude of the tap. /// - Returns: The modified map view. func onTapMapGesture(count: Int = 1, onTapChanged: @escaping (MapGestureContext) -> Void) -> MapView @@ -65,6 +66,16 @@ public extension MapView { return newMapView } + /// Add an tap gesture handler to the MapView that returns any visible map features that were tapped. + /// + /// - Parameters: + /// - count: The number of taps required to run the gesture. + /// - on layers: The set of layer ids that you would like to check for visible features that were tapped. If no + /// set is provided, all map layers are checked. + /// - onTapChanged: Emits the context whenever the gesture changes (e.g. began, ended, etc), that also contains + /// information like the latitude and longitude of the tap. Also emits an array of map features that were tapped. + /// Returns an empty array when nothing was tapped on the "on" layer ids that were provided. + /// - Returns: The modified map view. func onTapMapGesture(count: Int = 1, on layers: Set?, onTapChanged: @escaping (MapGestureContext, [any MLNFeature]) -> Void) -> MapView {