Skip to content

Commit

Permalink
Added testing for many of the swiftui mapview behaviors
Browse files Browse the repository at this point in the history
  • Loading branch information
Archdoog committed Feb 8, 2024
1 parent 1209d54 commit a076c35
Show file tree
Hide file tree
Showing 32 changed files with 751 additions and 137 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ on:

jobs:
test:
runs-on: macos-13
runs-on: macos-14
strategy:
matrix:
scheme: [
MapLibreSwiftUI-Package
]
destination: [
# TODO: Add more destinations
'platform=iOS Simulator,name=iPhone 15,OS=17.0.1'
'platform=iOS Simulator,name=iPhone 15,OS=17.2'
]

steps:
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" : "mockable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Kolos65/Mockable.git",
"state" : {
"revision" : "7af00c08880d375f2742ca55705abd69837fe6c3",
"version" : "0.0.2"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
Expand Down
9 changes: 8 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let package = Package(
.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"),
// Testing
.package(url: "https://github.com/Kolos65/Mockable.git", from: "0.0.2"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.3"),
],
targets: [
Expand All @@ -32,6 +33,10 @@ let package = Package(
.target(name: "InternalUtils"),
.target(name: "MapLibreSwiftDSL"),
.product(name: "MapLibre", package: "maplibre-gl-native-distribution"),
.product(name: "Mockable", package: "Mockable")
],
swiftSettings: [
.define("MOCKING", .when(configuration: .debug))
]),
.target(
name: "MapLibreSwiftDSL",
Expand All @@ -50,7 +55,9 @@ let package = Package(
.testTarget(
name: "MapLibreSwiftUITests",
dependencies: [
"MapLibreSwiftUI"
"MapLibreSwiftUI",
.product(name: "MockableTest", package: "Mockable"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
]
),
.testTarget(
Expand Down
20 changes: 11 additions & 9 deletions Sources/MapLibreSwiftUI/Examples/Camera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ struct CameraDirectManipulationPreview: View {
@State private var camera = MapViewCamera.center(switzerland, zoom: 4)

let styleURL: URL
var onStyleLoaded: (() -> Void)? = nil

var body: some View {
MapView(styleURL: styleURL, camera: $camera)
.onStyleLoaded { _ in
print("Style is loaded")
onStyleLoaded?()
}
.overlay(alignment: .bottom, content: {
Text("\(String(describing: camera.state)) z \(camera.zoom)")
.padding()
Expand All @@ -22,19 +27,16 @@ struct CameraDirectManipulationPreview: View {
.padding(.bottom, 42)
})
.task {
try! await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
try? await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)

camera = MapViewCamera.center(switzerland, zoom: 6)
}
}
}

struct Camera_Previews: PreviewProvider {
static var previews: some View {
let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!

CameraDirectManipulationPreview(styleURL: demoTilesURL)
.ignoresSafeArea(.all)
.previewDisplayName("Camera Binding")
}
#Preview("Camera Preview") {
CameraDirectManipulationPreview(
styleURL: URL(string: "https://demotiles.maplibre.org/style.json")!
)
.ignoresSafeArea(.all)
}
152 changes: 77 additions & 75 deletions Sources/MapLibreSwiftUI/Examples/Layers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,91 @@ import MapLibre
import MapLibreSwiftDSL
import SwiftUI

struct Layer_Previews: PreviewProvider {
static var previews: some View {
let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!
let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!

// A collection of points with various
// attributes
let pointSource = ShapeSource(identifier: "points") {
// Uses the DSL to quickly construct point features inline
MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 51.47778, longitude: -0.00139))
// A collection of points with various
// attributes
let pointSource = ShapeSource(identifier: "points") {
// Uses the DSL to quickly construct point features inline
MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 51.47778, longitude: -0.00139))

MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) { feature in
feature.attributes["icon"] = "missing"
feature.attributes["heading"] = 45
}
MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0)) { feature in
feature.attributes["icon"] = "missing"
feature.attributes["heading"] = 45
}

MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 39.02001, longitude: 1.482148)) { feature in
feature.attributes["icon"] = "club"
feature.attributes["heading"] = 145
}
}
MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 39.02001, longitude: 1.482148)) { feature in
feature.attributes["icon"] = "club"
feature.attributes["heading"] = 145
}
}

MapView(styleURL: demoTilesURL) {
// Silly example: a background layer on top of everything to create a tint effect
BackgroundLayer(identifier: "rose-colored-glasses")
.backgroundColor(constant: .systemPink.withAlphaComponent(0.3))
.renderAboveOthers()
}
.ignoresSafeArea(.all)
.previewDisplayName("Rose Tint")
#Preview("Rose Tint") {
MapView(styleURL: demoTilesURL) {
// Silly example: a background layer on top of everything to create a tint effect
BackgroundLayer(identifier: "rose-colored-glasses")
.backgroundColor(constant: .systemPink.withAlphaComponent(0.3))
.renderAboveOthers()
}
.ignoresSafeArea(.all)
}

MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "mappin")!)
}
.ignoresSafeArea(.all)
.previewDisplayName("Simple Symbol")
#Preview("Simple Symbol") {
MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "mappin")!)
}
.ignoresSafeArea(.all)
}

MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
SymbolStyleLayer(identifier: "rotated-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "location.north.circle.fill")!)
.iconRotation(constant: 45)
}
.ignoresSafeArea(.all)
.previewDisplayName("Rotated Symbols (Const)")
#Preview("Rotated Symbols (Const)") {
MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
SymbolStyleLayer(identifier: "rotated-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "location.north.circle.fill")!)
.iconRotation(constant: 45)
}
.ignoresSafeArea(.all)
}

MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
SymbolStyleLayer(identifier: "rotated-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "location.north.circle.fill")!)
.iconRotation(featurePropertyNamed: "heading")
}
.ignoresSafeArea(.all)
.previewDisplayName("Rotated Symbols (Dynamic)")

MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
CircleStyleLayer(identifier: "simple-circles", source: pointSource)
.radius(constant: 16)
.color(constant: .systemRed)
.strokeWidth(constant: 2)
.strokeColor(constant: .white)

SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "mappin")!.withRenderingMode(.alwaysTemplate))
.iconColor(constant: .white)
}
#Preview("Rotated Symbols (Dynamic)") {
MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
SymbolStyleLayer(identifier: "rotated-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "location.north.circle.fill")!)
.iconRotation(featurePropertyNamed: "heading")
}
.ignoresSafeArea(.all)
.previewDisplayName("Circles with Symbols")
}

// FIXME: This appears to be broken upstream; waiting for a new release
// MapView(styleURL: demoTilesURL) {
// // Simple symbol layer demonstration with an icon
// SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
// .iconImage(attribute: "icon",
// mappings: [
// "missing": UIImage(systemName: "mappin.slash")!,
// "club": UIImage(systemName: "figure.dance")!
// ],
// default: UIImage(systemName: "mappin")!)
// }
// .edgesIgnoringSafeArea(.all)
// .previewDisplayName("Multiple Symbol Icons")
#Preview("Circles with Symbols") {
MapView(styleURL: demoTilesURL) {
// Simple symbol layer demonstration with an icon
CircleStyleLayer(identifier: "simple-circles", source: pointSource)
.radius(constant: 16)
.color(constant: .systemRed)
.strokeWidth(constant: 2)
.strokeColor(constant: .white)

SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "mappin")!.withRenderingMode(.alwaysTemplate))
.iconColor(constant: .white)
}
.ignoresSafeArea(.all)
}

// TODO: Fixme
//#Preview("Multiple Symbol Icons") {
// MapView(styleURL: demoTilesURL) {
// // Simple symbol layer demonstration with an icon
// SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
// .iconImage(attribute: "icon",
// mappings: [
// "missing": UIImage(systemName: "mappin.slash")!,
// "club": UIImage(systemName: "figure.dance")!
// ],
// default: UIImage(systemName: "mappin")!)
// }
// .edgesIgnoringSafeArea(.all)
//}
28 changes: 11 additions & 17 deletions Sources/MapLibreSwiftUI/Examples/Other.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import MapLibre
import MapLibreSwiftDSL
import SwiftUI

struct Other_Previews: PreviewProvider {
static var previews: some View {
let demoTilesURL = URL(string: "https://demotiles.maplibre.org/style.json")!

#Preview("Unsafe MapView Modifier") {
MapView(styleURL: demoTilesURL) {
// A collection of points with various
// attributes
let pointSource = ShapeSource(identifier: "points") {
Expand All @@ -23,18 +21,14 @@ struct Other_Previews: PreviewProvider {
feature.attributes["heading"] = 145
}
}

MapView(styleURL: demoTilesURL) {
// Demonstrates how to use the unsafeMapModifier to set MLNMapView properties that have not been exposed as modifiers yet.
SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "mappin")!)
}
.unsafeMapViewModifier({ mapView in
// Not all properties have modifiers yet. Until they do, you can use this 'escape hatch' to the underlying MLNMapView. Be careful: if you modify properties that the DSL controls already, they may be overridden. This modifier is a "hack", not a final function.
mapView.logoView.isHidden = false
mapView.compassViewPosition = .topLeft
})
.ignoresSafeArea(.all)
.previewDisplayName("Unsafe MapView Modifier")

// Demonstrates how to use the unsafeMapModifier to set MLNMapView properties that have not been exposed as modifiers yet.
SymbolStyleLayer(identifier: "simple-symbols", source: pointSource)
.iconImage(constant: UIImage(systemName: "mappin")!)
}
.unsafeMapViewModifier { mapView in
// Not all properties have modifiers yet. Until they do, you can use this 'escape hatch' to the underlying MLNMapView. Be careful: if you modify properties that the DSL controls already, they may be overridden. This modifier is a "hack", not a final function.
mapView.logoView.isHidden = false
mapView.compassViewPosition = .topLeft
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import MapLibre

extension MLNCameraChangeReason {

/// Get the last value from the MLNCameraChangeReason option set.
public var lastValue: MLNCameraChangeReason {
/// Get the MLNCameraChangeReason from the option set with the largest
/// bitwise value.
public var largestBitwiseReason: MLNCameraChangeReason {
// Start at 1
var mask: UInt = 1
var result: UInt = 0
Expand Down
20 changes: 20 additions & 0 deletions Sources/MapLibreSwiftUI/Extensions/MapLibre/MLNMapViewCamera.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import CoreLocation
import MapLibre
import Mockable

@Mockable
protocol MLNMapViewCamera: AnyObject {
var userTrackingMode: MLNUserTrackingMode { get set }
var minimumPitch: CGFloat { get set }
var maximumPitch: CGFloat { get set }
func setCenter(_ coordinate: CLLocationCoordinate2D,
zoomLevel: Double,
direction: CLLocationDirection,
animated: Bool)
func setZoomLevel(_ zoomLevel: Double, animated: Bool)
}

extension MLNMapView: MLNMapViewCamera {
// No definition
}
27 changes: 21 additions & 6 deletions Sources/MapLibreSwiftUI/Extensions/MapView/MapViewGestures.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import SwiftUI
import MapLibre

extension MapView {
Expand Down Expand Up @@ -44,21 +45,35 @@ extension 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)
}

/// Convert the sender data into a MapGestureContext
///
/// - Parameters:
/// - mapView: The mapview that's emitting the gesture.
/// - gesture: The gesture definition for this event.
/// - sender: The UIKit gesture emitting from the map view.
/// - Returns: The calculated context from the sending UIKit gesture
func processContextFromGesture(_ mapView: MLNMapView, gesture: MapGesture, sender: UIGestureRecognizerProtocol) -> MapGestureContext {
// Build the context of the gesture's event.
var point: CGPoint
switch gesture.method {

case .tap(numberOfTaps: let numberOfTaps):
// Calculate the CGPoint of the last gesture tap
point = sender.location(ofTouch: numberOfTaps - 1, in: mapView)
case .longPress:
// Calculate the CGPoint of the long process gesture.
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)
return MapGestureContext(gestureMethod: gesture.method,
state: sender.state,
point: point,
coordinate: mapView.convert(point, toCoordinateFrom: mapView))
}
}
Loading

0 comments on commit a076c35

Please sign in to comment.