Skip to content

Commit

Permalink
add gesture handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
paulsUsername authored and persidskiy committed Feb 9, 2024
1 parent 90168b1 commit 54ad83e
Show file tree
Hide file tree
Showing 26 changed files with 869 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,57 @@ final class CircleAnnotationExample: UIViewController, ExampleProtocol {
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)

// Create the CircleAnnotationManager
// Annotation managers are kept alive by `AnnotationOrchestrator`
// (`mapView.annotations`) until you explicitly destroy them
// by calling `mapView.annotations.removeAnnotationManager(withId:)`
/// Create the CircleAnnotationManager
/// Annotation managers are kept alive by `AnnotationOrchestrator`
/// (`mapView.annotations`) until you explicitly destroy them
/// by calling `mapView.annotations.removeAnnotationManager(withId:)`
let circleAnnotationManager = mapView.annotations.makeCircleAnnotationManager()

var annotations = [CircleAnnotation]()
for _ in 0...2000 {
var annotation = CircleAnnotation(centerCoordinate: .random)
annotation.circleColor = StyleColor(.random)
annotation.circleStrokeColor = StyleColor(UIColor.black)
annotation.circleRadius = 12
annotation.isDraggable = true
annotation.tapHandler = { [id = annotation.id] _ in
print("tapped annotation \(id)")
return false
annotation.circleStrokeWidth = 0

/// The following handlers add tap and longpress gesture handlers. The `context` parameter
/// contains the `point` of the gesture in view coordinate system and a geographical `coordinate`.
annotation.tapHandler = { [id = annotation.id] context in
let latlon = String(format: "lat: %.3f, lon: %.3f", context.coordinate.latitude, context.coordinate.longitude)
print("annotation tap: \(id), \(latlon)")
return true // don't propagate tap to annotations below
}
annotation.longPressHandler = { [id = annotation.id] context in
let latlon = String(format: "lat: %.3f, lon: %.3f", context.coordinate.latitude, context.coordinate.longitude)
print("annotation longpress: \(id), \(latlon)")
return true // don't propagate tap to annotations below
}

/// The following gesture handlers create the dragging effect.
/// The dragged annotation becomes larger and receives a stroke.
///
/// - Important: In order to modify the annotation while it is being dragged,
/// use the inout `annotation` that comes as the first argument to the handler.
/// Don't use the source annotation that you used to configure it initially.
/// The annotations are value types.
///
/// The second `context` argument is similar to tap and longpress gestures.
annotation.dragBeginHandler = { annotation, _ in
annotation.circleRadius = 22
annotation.circleStrokeWidth = 2
print("annotation drag begin: \(annotation.id)")
return true // allow drag gesture begin
}
annotation.dragChangeHandler = { annotation, context in
let latlon = String(format: "lat: %.3f, lon: %.3f", context.coordinate.latitude, context.coordinate.longitude)
print("annotation drag: \(annotation.id), \(latlon)")
}
annotation.dragEndHandler = { annotation, _ in
annotation.circleRadius = 12
annotation.circleStrokeWidth = 0
print("annotation drag ended: \(annotation.id)")
}
annotations.append(annotation)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ final class CustomPointAnnotationExample: UIViewController, ExampleProtocol {
return true
}

customPointAnnotation.dragBeginHandler = { annotation, _ in
annotation.iconSize = 1.2
return true // allow drag gesture begin
}
customPointAnnotation.dragEndHandler = { annotation, _ in
annotation.iconSize = 1
}

// Add the annotation to the manager in order to render it on the map.
pointAnnotationManager.annotations = [customPointAnnotation]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct PuckPlayground: View {
}
}
.mapStyle(mapStyle)
.debugOptions(.padding)
.additionalSafeAreaInsets(sidePanel ? .trailing : .bottom, settingsHeight)
.ignoresSafeArea()
.safeOverlay(alignment: sidePanel ? .trailing : .bottom) {
Expand All @@ -49,8 +50,8 @@ struct PuckPlayground: View {
.onChangeOfSize { settingsHeight = sidePanel ? $0.width : $0.height }
}
.safeOverlay(alignment: .trailing) {
MapStyleSelectorButton(mapStyle: $mapStyle)
.padding(.trailing, sidePanel ? 300 : 0)
MapStyleSelectorButton(mapStyle: $mapStyle)
.padding(.trailing, sidePanel ? 300 : 0)
}
.onChange(of: puckType) { newValue in
if puckType == .d3 { // Switch to dusk mode to see model light emission
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Mapbox welcomes participation and contributions from everyone.
## main

* 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.

## 11.2.0-beta.1 - 1 February, 2024

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

9 changes: 6 additions & 3 deletions Sources/MapboxMaps/Annotations/AnnotationOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ protocol AnnotationManagerInternal: AnnotationManager {

func handleDragBegin(with featureId: String, context: MapContentGestureContext) -> Bool

func handleDragChanged(with translation: CGPoint)
func handleDragChange(with translation: CGPoint, context: MapContentGestureContext)

func handleDragEnded()
func handleDragEnd(context: MapContentGestureContext)
}

struct AnnotationGestureHandlers {
struct AnnotationGestureHandlers<T: Annotation> {
var tap: ((MapContentGestureContext) -> Bool)?
var longPress: ((MapContentGestureContext) -> Bool)?
var dragBegin: ((inout T, MapContentGestureContext) -> Bool)?
var dragChange: ((inout T, MapContentGestureContext) -> Void)?
var dragEnd: ((inout T, MapContentGestureContext) -> Void)?
}

/// A delegate that is called when a tap is detected on an annotation (or on several of them).
Expand Down
41 changes: 37 additions & 4 deletions Sources/MapboxMaps/Annotations/Generated/CircleAnnotation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,54 @@ public struct CircleAnnotation: Annotation, Equatable {
get { gestureHandlers.value.longPress }
set { gestureHandlers.value.longPress = newValue }
}


/// The handler is invoked when the user begins to drag the annotation.
///
/// The annotation should have `isDraggable` set to `true` to make id draggable.
///
/// - Note: In SwiftUI, draggable annotations are not supported.
///
/// The handler receives the `annotation` and the `context` parameters of the gesture:
/// - Use the `annotation` inout property to update properties of the annotation.
/// - The `context` contains position of the gesture.
/// Return `true` to allow dragging to begin, or `false` to prevent it and propagate the gesture to the map's other annotations or layers.
public var dragBeginHandler: ((inout CircleAnnotation, MapContentGestureContext) -> Bool)? {
get { gestureHandlers.value.dragBegin }
set { gestureHandlers.value.dragBegin = newValue }
}

/// The handler is invoked when annotation is being dragged.
///
/// The handler receives the `annotation` and the `context` parameters of the gesture:
/// - Use the `annotation` inout property to update properties of the annotation.
/// - The `context` contains position of the gesture.
public var dragChangeHandler: ((inout CircleAnnotation, MapContentGestureContext) -> Void)? {
get { gestureHandlers.value.dragChange }
set { gestureHandlers.value.dragChange = newValue }
}

/// The handler receives the `annotation` and the `context` parameters of the gesture:
/// - Use the `annotation` inout property to update properties of the annotation.
/// - The `context` contains position of the gesture.
public var dragEndHandler: ((inout CircleAnnotation, MapContentGestureContext) -> Void)? {
get { gestureHandlers.value.dragEnd }
set { gestureHandlers.value.dragEnd = newValue }
}

/// JSON convertible properties associated with the annotation, used to enrich Feature GeoJSON `properties["custom_data"]` field.
public var customData = JSONObject()

/// Properties associated with the annotation.
///
/// - Note: This propert doesn't participate in `Equatable` comparisions and will strip non-JSON values when encoding to Feature GeoJSON.
/// - Note: This property doesn't participate in `Equatable` comparisions and will strip non-JSON values when encoding to Feature GeoJSON.
@available(*, deprecated, message: "Use customData instead.")
public var userInfo: [String: Any]? {
get { _userInfo.value }
set { _userInfo.value = newValue }
}

private var _userInfo: AlwaysEqual<[String: Any]?> = nil
private var gestureHandlers = AlwaysEqual(value: AnnotationGestureHandlers())
private var gestureHandlers = AlwaysEqual(value: AnnotationGestureHandlers<CircleAnnotation>())

var layerProperties: [String: Any] {
var properties: [String: Any] = [:]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
public var sourceId: String { id }

public var layerId: String { id }

private var dragId: String { "\(id)_drag" }

public let id: String
Expand Down Expand Up @@ -282,15 +282,15 @@ public class CircleAnnotationManager: AnnotationManagerInternal {

// MARK: - User interaction handling


func handleTap(layerId: String, feature: Feature, context: MapContentGestureContext) -> Bool {

guard let featureId = feature.identifier?.string else { return false }

let tappedIndex = annotations.firstIndex { $0.id == featureId }
guard let tappedIndex else { return false }
var tappedAnnotation = annotations[tappedIndex]

tappedAnnotation.isSelected.toggle()

if !isSwiftUI {
Expand All @@ -315,36 +315,61 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
func handleDragBegin(with featureId: String, context: MapContentGestureContext) -> Bool {
guard !isSwiftUI else { return false }

let predicate = { (annotation: CircleAnnotation) -> Bool in
func predicate(annotation: CircleAnnotation) -> Bool {
annotation.id == featureId && annotation.isDraggable
}

func tryBeginDragging(_ annotations: inout [CircleAnnotation], idx: Int) -> Bool {
var annotation = annotations[idx]
// If no drag handler set, the dragging is allowed
let dragAllowed = annotation.dragBeginHandler?(&annotation, context) ?? true
annotations[idx] = annotation
return dragAllowed
}

/// First, try to drag annotations that are already on the dragging layer.
if let idx = draggedAnnotations.firstIndex(where: predicate) {
let dragAllowed = tryBeginDragging(&draggedAnnotations, idx: idx)
guard dragAllowed else {
return false
}

draggedAnnotationIndex = idx
return true
}

/// Then, try to start dragging from the main set of annotations.
if let idx = mainAnnotations.lastIndex(where: predicate) {
insertDraggedLayerAndSourceOnce {
let source = GeoJSONSource(id: dragId)
let layer = CircleLayer(id: dragId, source: dragId)
do {
try style.addSource(source)
try style.addPersistentLayer(layer, layerPosition: .above(layerId))
} catch {
Log.error(forMessage: "Add drag source/layer \(error)", category: "Annotations")
}
let dragAllowed = tryBeginDragging(&mainAnnotations, idx: idx)
guard dragAllowed else {
return false
}

insertDraggedLayerAndSource()

let annotation = mainAnnotations.remove(at: idx)
draggedAnnotations.append(annotation)
draggedAnnotationIndex = draggedAnnotations.endIndex - 1
return true
}

return false
}

func handleDragChanged(with translation: CGPoint) {
private func insertDraggedLayerAndSource() {
insertDraggedLayerAndSourceOnce {
let source = GeoJSONSource(id: dragId)
let layer = CircleLayer(id: dragId, source: dragId)
do {
try style.addSource(source)
try style.addPersistentLayer(layer, layerPosition: .above(layerId))
} catch {
Log.error(forMessage: "Add drag source/layer \(error)", category: "Annotations")
}
}
}

func handleDragChange(with translation: CGPoint, context: MapContentGestureContext) {
guard !isSwiftUI,
let draggedAnnotationIndex,
draggedAnnotationIndex < draggedAnnotations.endIndex,
Expand All @@ -353,12 +378,31 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
}

draggedAnnotations[draggedAnnotationIndex].point = point

callDragHandler(\.dragChangeHandler, context: context)
}

internal func handleDragEnded() {
func handleDragEnd(context: MapContentGestureContext) {
guard !isSwiftUI else { return }
callDragHandler(\.dragEndHandler, context: context)
draggedAnnotationIndex = nil
}

private func callDragHandler(
_ keyPath: KeyPath<CircleAnnotation, ((inout CircleAnnotation, MapContentGestureContext) -> Void)?>,
context: MapContentGestureContext
) {
guard let draggedAnnotationIndex, draggedAnnotationIndex < draggedAnnotations.endIndex else {
return
}

if let handler = draggedAnnotations[draggedAnnotationIndex][keyPath: keyPath] {
var copy = draggedAnnotations[draggedAnnotationIndex]
handler(&copy, context)
draggedAnnotations[draggedAnnotationIndex] = copy

This comment has been minimized.

Copy link
@pfriedrix

pfriedrix Feb 10, 2024

I tested this code and in 402 line draggedAnnotations[draggedAnnotationIndex] = copy, I got Index out of range

This comment has been minimized.

Copy link
@persidskiy

persidskiy Feb 10, 2024

Contributor

@pfriedrix What was your use case? Did you modify the annotations array inside of the handler?

This comment has been minimized.

Copy link
@pfriedrix

pfriedrix Feb 10, 2024

@persidskiy for circle I set dragEndedHandler and then I added this annotation to manager annotations

This comment has been minimized.

Copy link
@persidskiy

persidskiy Feb 10, 2024

Contributor

Can you please attach a sample of code that leads to the crash?

This comment has been minimized.

Copy link
@pfriedrix

pfriedrix Feb 10, 2024

@persidskiy

    guard let circleAnnotationManager = circleAnnotationManager else { return }

    let id = location.id.uuidString
    let isContains = circleAnnotationManager.annotations.contains {
        $0.id == id
    }
    
    guard !isContains else { return }
    
    var circle = CircleAnnotation(id: id, centerCoordinate: CLLocation(model: location).coordinate)
    circle.circleColor = StyleColor(UIColor.originRed)
    circle.circleRadius = 10
    
    if type == .start {
        if circleAnnotationManager.annotations.contains(where: { $0.id == type.rawValue }) {
            return
        }
    } else if type == .interval {
        circle.circleRadius = 5
        circle.circleStrokeColor = StyleColor(UIColor.originRed)
        circle.circleColor = StyleColor(UIColor.white)
        circle.circleStrokeWidth = 5
    }
    
    circle.isDraggable = true

    circle.dragBeginHandler = { _, _ in
        UIImpactFeedbackGenerator(style: .medium).impactOccurred()
        return true
    }

    circle.dragEndHanlder = { [ weak self ] annotation, _ in
        let location = Location(location: .init(latitude: annotation.point.coordinates.latitude, longitude: annotation.point.coordinates.longitude))
        self?.gestureDelegate?.annotationDragEnded(with: annotation.id, location: location)
    }
    
    circleAnnotationManager.annotations.append(circle)

annotationDragEnded from gestureDelegate

    func annotationDragEnded(with id: String, location: Location) {
        if let index = points.firstIndex(where: { $0.id.uuidString == id }) {
            points[index] = location
            points[index].id = UUID(uuidString: id) ?? .init()
        }
     }

This comment has been minimized.

Copy link
@persidskiy

persidskiy Feb 10, 2024

Contributor

Does update of points trigger the update of circleAnnotationManager.annotations in your code?

Whenever you update the circleAnnotationManager.annotations, the drag state gets reset. If you do it inside of handler, it will lead to the crash you observe.

This comment has been minimized.

Copy link
@pfriedrix

pfriedrix Feb 10, 2024

@persidskiy Okay, it works, thx

This comment has been minimized.

Copy link
@pfriedrix

pfriedrix Feb 13, 2024

@persidskiy I come back with question why not change places handler(&copy, context) and draggedAnnotations[draggedAnnotationIndex] = copy

}
}
}


// End of generated file.
41 changes: 37 additions & 4 deletions Sources/MapboxMaps/Annotations/Generated/PointAnnotation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,54 @@ public struct PointAnnotation: Annotation, Equatable {
get { gestureHandlers.value.longPress }
set { gestureHandlers.value.longPress = newValue }
}


/// The handler is invoked when the user begins to drag the annotation.
///
/// The annotation should have `isDraggable` set to `true` to make id draggable.
///
/// - Note: In SwiftUI, draggable annotations are not supported.
///
/// The handler receives the `annotation` and the `context` parameters of the gesture:
/// - Use the `annotation` inout property to update properties of the annotation.
/// - The `context` contains position of the gesture.
/// Return `true` to allow dragging to begin, or `false` to prevent it and propagate the gesture to the map's other annotations or layers.
public var dragBeginHandler: ((inout PointAnnotation, MapContentGestureContext) -> Bool)? {
get { gestureHandlers.value.dragBegin }
set { gestureHandlers.value.dragBegin = newValue }
}

/// The handler is invoked when annotation is being dragged.
///
/// The handler receives the `annotation` and the `context` parameters of the gesture:
/// - Use the `annotation` inout property to update properties of the annotation.
/// - The `context` contains position of the gesture.
public var dragChangeHandler: ((inout PointAnnotation, MapContentGestureContext) -> Void)? {
get { gestureHandlers.value.dragChange }
set { gestureHandlers.value.dragChange = newValue }
}

/// The handler receives the `annotation` and the `context` parameters of the gesture:
/// - Use the `annotation` inout property to update properties of the annotation.
/// - The `context` contains position of the gesture.
public var dragEndHandler: ((inout PointAnnotation, MapContentGestureContext) -> Void)? {
get { gestureHandlers.value.dragEnd }
set { gestureHandlers.value.dragEnd = newValue }
}

/// JSON convertible properties associated with the annotation, used to enrich Feature GeoJSON `properties["custom_data"]` field.
public var customData = JSONObject()

/// Properties associated with the annotation.
///
/// - Note: This propert doesn't participate in `Equatable` comparisions and will strip non-JSON values when encoding to Feature GeoJSON.
/// - Note: This property doesn't participate in `Equatable` comparisions and will strip non-JSON values when encoding to Feature GeoJSON.
@available(*, deprecated, message: "Use customData instead.")
public var userInfo: [String: Any]? {
get { _userInfo.value }
set { _userInfo.value = newValue }
}

private var _userInfo: AlwaysEqual<[String: Any]?> = nil
private var gestureHandlers = AlwaysEqual(value: AnnotationGestureHandlers())
private var gestureHandlers = AlwaysEqual(value: AnnotationGestureHandlers<PointAnnotation>())

var layerProperties: [String: Any] {
var properties: [String: Any] = [:]
Expand Down
Loading

0 comments on commit 54ad83e

Please sign in to comment.