Skip to content

Commit

Permalink
Improvements based on feedback for gestures and similar
Browse files Browse the repository at this point in the history
  • Loading branch information
Archdoog committed Feb 6, 2024
1 parent 8e4c103 commit 00ff5e3
Show file tree
Hide file tree
Showing 22 changed files with 388 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MapLibreSwiftUITests"
BuildableName = "MapLibreSwiftUITests"
BlueprintName = "MapLibreSwiftUITests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
"revision" : "b8deecb8adc3b911de311ead5a13b98fbf2d7824"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "e7b77228b34057041374ebef00c0fd7739d71a2b",
"version" : "1.15.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
Expand Down
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ let package = Package(
dependencies: [
// .package(url: "https://github.com/maplibre/maplibre-gl-native-distribution", .upToNextMajor(from: "5.13.0")),
.package(url: "https://github.com/maplibre/maplibre-gl-native-distribution.git", from: "6.0.0-pre9599200f2529de44ba62d4662cddb445dc19397d"),
.package(url: "https://github.com/stadiamaps/maplibre-swift-macros.git", branch: "main")
.package(url: "https://github.com/stadiamaps/maplibre-swift-macros.git", branch: "main"),
// Testing
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.3"),
],
targets: [
.target(
Expand All @@ -45,6 +47,12 @@ let package = Package(

// MARK: Tests

.testTarget(
name: "MapLibreSwiftUITests",
dependencies: [
"MapLibreSwiftUI"
]
),
.testTarget(
name: "MapLibreSwiftDSLTests",
dependencies: [
Expand Down
47 changes: 25 additions & 22 deletions Sources/MapLibreSwiftDSL/MapViewContentBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
import Foundation

@resultBuilder
public enum MapViewContentBuilder {

public static func buildBlock(_ layers: [StyleLayerDefinition]...) -> [StyleLayerDefinition] {
return layers.flatMap { $0 }
}

public static func buildOptional(_ layers: [StyleLayerDefinition]?) -> [StyleLayerDefinition] {
return layers ?? []
public enum MapViewContentBuilder: DefaultResultBuilder {
public static func buildExpression(_ expression: StyleLayerDefinition) -> [StyleLayerDefinition] {
return [expression]
}

public static func buildExpression(_ layer: StyleLayerDefinition) -> [StyleLayerDefinition] {
return [layer]
public static func buildExpression(_ expression: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return expression
}

public static func buildExpression(_ expression: Void) -> [StyleLayerDefinition] {
return []
}

public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] {
return styleCollection.layers
public static func buildBlock(_ components: [StyleLayerDefinition]...) -> [StyleLayerDefinition] {
return components.flatMap { $0 }
}

public static func buildArray(_ components: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return components
}

public static func buildArray(_ components: [[StyleLayerDefinition]]) -> [StyleLayerDefinition] {
return components.flatMap { $0 }
}

// Handle an array of MLNShape (if you want to directly pass arrays)
public static func buildArray(_ layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return layer
public static func buildEither(first components: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return components
}

// Handle for in of MLNShape
public static func buildArray(_ layer: [[StyleLayerDefinition]]) -> [StyleLayerDefinition] {
return layer.flatMap { $0 }
public static func buildEither(second components: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return components
}

public static func buildEither(first layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return layer
public static func buildOptional(_ components: [StyleLayerDefinition]?) -> [StyleLayerDefinition] {
return components ?? []
}

public static func buildEither(second layer: [StyleLayerDefinition]) -> [StyleLayerDefinition] {
return layer
// MARK: Custom Handler for StyleLayerCollection type.

public static func buildExpression(_ styleCollection: StyleLayerCollection) -> [StyleLayerDefinition] {
return styleCollection.layers
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ public struct ShapeSource: Source {


@resultBuilder
public enum ShapeDataBuilder {
// Handle a single MLNShape element
public enum ShapeDataBuilder: DefaultResultBuilder {
public static func buildExpression(_ expression: MLNShape) -> [MLNShape] {
return [expression]
}
Expand All @@ -55,22 +54,22 @@ public enum ShapeDataBuilder {
return expression
}

// Combine elements into an array
public static func buildExpression(_ expression: Void) -> [MLNShape] {
return []
}

public static func buildBlock(_ components: [MLNShape]...) -> [MLNShape] {
return components.flatMap { $0 }
}

// Handle an array of MLNShape (if you want to directly pass arrays)
public static func buildArray(_ components: [MLNShape]) -> [MLNShape] {
return components
}

// Handle for in of MLNShape
public static func buildArray(_ components: [[MLNShape]]) -> [MLNShape] {
return components.flatMap { $0 }
}

// Handle if statements
public static func buildEither(first components: [MLNShape]) -> [MLNShape] {
return components
}
Expand Down
39 changes: 39 additions & 0 deletions Sources/MapLibreSwiftDSL/Support/DefaultResultBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

/// Enforces a basic set of result builder definiitons.
///
/// This is just a tool to make a result builder easier to build, maintain sorting, etc.
public protocol DefaultResultBuilder {

associatedtype Component

static func buildExpression(_ expression: Component) -> [Component]

static func buildExpression(_ expression: [Component]) -> [Component]

// MARK: Handle void

static func buildExpression(_ expression: Void) -> [Component]

// MARK: Combine elements into an array

static func buildBlock(_ components: [Component]...) -> [Component]

// MARK: Handle Arrays

static func buildArray(_ components: [Component]) -> [Component]

// MARK: Handle for in loops

static func buildArray(_ components: [[Component]]) -> [Component]

// MARK: Handle if statements

static func buildEither(first components: [Component]) -> [Component]

static func buildEither(second components: [Component]) -> [Component]

// MARK: Handle Optionals

static func buildOptional(_ components: [Component]?) -> [Component]
}
2 changes: 1 addition & 1 deletion Sources/MapLibreSwiftUI/Examples/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct CameraDirectManipulationPreview: View {
var body: some View {
MapView(styleURL: styleURL, camera: $camera)
.overlay(alignment: .bottom, content: {
Text("\(camera.coordinate.latitude), \(camera.coordinate.longitude) z \(camera.zoom)")
Text("\(String(describing: camera.state)) z \(camera.zoom)")
.padding()
.foregroundColor(.white)
.background(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
import Foundation
import MapLibre

extension MLNCameraChangeReason: CustomDebugStringConvertible {
public var debugDescription: String {
switch self.lastValue {

case .programmatic: return ".programmatic"
case .resetNorth: return ".resetNorth"
case .gesturePan: return ".gesturePan"
case .gesturePinch: return ".gesturePinch"
case .gestureRotate: return ".gestureRotate"
case .gestureZoomIn: return ".gestureZoomIn"
case .gestureZoomOut: return ".gestureZoomOut"
case .gestureOneFingerZoom: return ".gestureOneFingerZoom"
case .gestureTilt: return ".gestureTilt"
case .transitionCancelled: return ".transitionCancelled"
default: return "none"
}
}
extension MLNCameraChangeReason {

/// Get the last value from the MLNCameraChangeReason option set.
public var lastValue: MLNCameraChangeReason {
Expand Down
64 changes: 64 additions & 0 deletions Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation
import MapLibre

extension MapView {

/// Register a gesture recognizer on the MapView.
///
/// - Parameters:
/// - mapView: The MLNMapView that will host the gesture itself.
/// - context: The UIViewRepresentable context that will orchestrate the response sender
/// - gesture: The gesture definition.
func registerGesture(_ mapView: MLNMapView, _ context: Context, gesture: MapGesture) {
switch gesture.method {

case .tap(numberOfTaps: let numberOfTaps):
let gestureRecognizer = UITapGestureRecognizer(target: context.coordinator,
action: #selector(context.coordinator.captureGesture(_:)))
gestureRecognizer.numberOfTapsRequired = numberOfTaps
mapView.addGestureRecognizer(gestureRecognizer)
gesture.gestureRecognizer = gestureRecognizer

case .longPress(minimumDuration: let minimumDuration):
let gestureRecognizer = UILongPressGestureRecognizer(target: context.coordinator,
action: #selector(context.coordinator.captureGesture(_:)))
gestureRecognizer.minimumPressDuration = minimumDuration

mapView.addGestureRecognizer(gestureRecognizer)
gesture.gestureRecognizer = gestureRecognizer
}
}

/// Runs on each gesture change event and filters the appropriate gesture behavior based on the
/// user definition.
///
/// Since the gestures run "onChange", we run this every time, event when state changes. The implementer is responsible for guarding
/// and handling whatever state logic they want.
///
/// - Parameters:
/// - mapView: The MapView emitting the gesture. This is used to calculate the point and coordinate of the gesture.
/// - sender: The UIGestureRecognizer
func processGesture(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) {
guard let gesture = self.gestures.first(where: { $0.gestureRecognizer == sender }) else {
assertionFailure("\(sender) is not a registered UIGestureRecongizer on the MapView")
return
}

// Build the context of the gesture's event.
var point: CGPoint
switch gesture.method {

case .tap(numberOfTaps: let numberOfTaps):
point = sender.location(ofTouch: numberOfTaps - 1, in: mapView)
case .longPress:
point = sender.location(in: mapView)
}

let context = MapGestureContext(gestureMethod: gesture.method,
state: sender.state,
point: point,
coordinate: mapView.convert(point, toCoordinateFrom: mapView))

gesture.onChange(context)
}
}
59 changes: 6 additions & 53 deletions Sources/MapLibreSwiftUI/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public struct MapView: UIViewRepresentable {
public func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(
parent: self,
onGestureEnd: { processGestureEnd($0, $1) }
onGesture: { processGesture($0, $1) }
)
}

Expand All @@ -60,19 +60,11 @@ public struct MapView: UIViewRepresentable {
// TODO: Make this settable via a modifier
mapView.logoView.isHidden = true

// Gesture recogniser setup
let tapGesture = UITapGestureRecognizer(
target: context.coordinator,
action: #selector(context.coordinator.captureGesture(_:))
)
mapView.addGestureRecognizer(tapGesture)

let longPressGesture = UILongPressGestureRecognizer(
target: context.coordinator,
action: #selector(context.coordinator.captureGesture(_:))
)
mapView.addGestureRecognizer(longPressGesture)

// Add all gesture recognizers
for gesture in gestures {
registerGesture(mapView, context, gesture: gesture)
}

return mapView
}

Expand All @@ -95,45 +87,6 @@ public struct MapView: UIViewRepresentable {
camera: $camera.wrappedValue,
animated: isStyleLoaded)
}

/// Runs on gesture ended.
///
/// Note: Some gestures may need additional behaviors for different gesture.states.
///
/// - Parameters:
/// - mapView: The MapView emitting the gesture. This is used to calculate the point and coordinate of the gesture.
/// - sender: The UIGestureRecognizer
private func processGestureEnd(_ mapView: MLNMapView, _ sender: UIGestureRecognizer) {
guard sender.state == .ended else {
return
}

let point = sender.location(in: mapView)
let coordinate = mapView.convert(point, toCoordinateFrom: mapView)

switch sender {
case is UITapGestureRecognizer:
for gesture in gestures.filter({ $0.method == .tap }) {
gesture.action(
MapGestureContext(gesture: gesture.method,
point: point,
coordinate: coordinate,
numberOfTaps: sender.numberOfTouches)
)
}
case is UILongPressGestureRecognizer:
for gesture in gestures.filter({ $0.method == .longPress }) {
gesture.action(
MapGestureContext(gesture: gesture.method,
point: point,
coordinate: coordinate,
numberOfTaps: sender.numberOfTouches)
)
}
default:
print("Log unhandled gesture")
}
}
}

struct MapView_Previews: PreviewProvider {
Expand Down
Loading

0 comments on commit 00ff5e3

Please sign in to comment.