From 922f3068f96c8fb633a4e7875a44ebbd0565a5a1 Mon Sep 17 00:00:00 2001 From: PW Date: Mon, 15 Jan 2024 12:21:49 +0100 Subject: [PATCH 1/5] add mapViewModifier --- Sources/MapLibreSwiftUI/MapView.swift | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 3611320..3db30a4 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -9,6 +9,7 @@ public struct MapView: UIViewRepresentable { let styleSource: MapStyleSource let userLayers: [StyleLayerDefinition] + var mapViewModifier: ((MLNMapView) -> Void)? public init( styleURL: URL, @@ -28,6 +29,27 @@ public struct MapView: UIViewRepresentable { ) { self.init(styleURL: styleURL, camera: .constant(initialCamera), makeMapContent) } + + /// Allows you to set properties of the underlying MLNMapView directly. + /// Use this function to modify various properties of the MLNMapView instance. + /// For example, you can enable the display of the user's location on the map by setting `showUserLocation` to true. + /// + /// - Parameter modifier: A closure that provides you with an MLNMapView so you can set properties. + /// - Returns: A MapView with the modifications applied. + /// + /// Example: + /// ```swift + /// MapView() + /// .mapViewModifier { mapView in + /// mapView.showUserLocation = true + /// } + /// ``` + /// + public func mapViewModifier(_ modifier: @escaping (MLNMapView) -> Void) -> MapView { + var newMapView = self + newMapView.mapViewModifier = modifier + return newMapView + } public class Coordinator: NSObject, MLNMapViewDelegate { var parent: MapView @@ -192,11 +214,14 @@ public struct MapView: UIViewRepresentable { public func updateUIView(_ mapView: MLNMapView, context: Context) { context.coordinator.parent = self + if let mapViewModifier { + mapViewModifier(mapView) + } // FIXME: This should be a more selective update context.coordinator.updateStyleSource(styleSource, mapView: mapView) context.coordinator.updateLayers(mapView: mapView) - // FIXME: This isn't exactly telling us if the *map* is loaded, and the docs for setCenter say it needs t obe. + // FIXME: This isn't exactly telling us if the *map* is loaded, and the docs for setCenter say it needs to be. let isStyleLoaded = mapView.style != nil context.coordinator.updateCamera(mapView: mapView, From 77a5e07c118d3447c4916c1083ba06b69e6fbd26 Mon Sep 17 00:00:00 2001 From: PW Date: Thu, 18 Jan 2024 13:17:12 +0100 Subject: [PATCH 2/5] improving source builder --- Sources/MapLibreSwiftDSL/Data Sources.swift | 30 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/Sources/MapLibreSwiftDSL/Data Sources.swift b/Sources/MapLibreSwiftDSL/Data Sources.swift index cf0edfb..561d20b 100644 --- a/Sources/MapLibreSwiftDSL/Data Sources.swift +++ b/Sources/MapLibreSwiftDSL/Data Sources.swift @@ -46,9 +46,33 @@ public struct ShapeSource: Source { @resultBuilder public enum ShapeDataBuilder { - public static func buildBlock(_ components: MLNShape...) -> ShapeData { - let features = components.compactMap({ $0 as? MLNShape & MLNFeature }) - + // Handle a single MLNShape element + public static func buildExpression(_ expression: MLNShape) -> [MLNShape] { + return [expression] + } + + public static func buildExpression(_ expression: [MLNShape]) -> [MLNShape] { + return expression + } + + // Combine elements into an array + 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 } + } + + // Convert the collected MLNShape array to ShapeData + public static func buildFinalResult(_ components: [MLNShape]) -> ShapeData { + let features = components.compactMap { $0 as? MLNShape & MLNFeature } if features.count == components.count { return .features(features) } else { From f4c5ac359a5d7eb7b66d3ca9e51d680463537b96 Mon Sep 17 00:00:00 2001 From: PW Date: Mon, 22 Jan 2024 15:04:23 +0100 Subject: [PATCH 3/5] renamed to unsafeViewModifier, added example, added test for ShapeSource --- Sources/MapLibreSwiftUI/Examples/Layers.swift | 13 ++++++++++ Sources/MapLibreSwiftUI/MapView.swift | 24 +++++++++++++------ .../ShapeSourceTests.swift | 20 ++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Sources/MapLibreSwiftUI/Examples/Layers.swift b/Sources/MapLibreSwiftUI/Examples/Layers.swift index ee0205e..99e03f2 100644 --- a/Sources/MapLibreSwiftUI/Examples/Layers.swift +++ b/Sources/MapLibreSwiftUI/Examples/Layers.swift @@ -58,6 +58,19 @@ struct Layer_Previews: PreviewProvider { } .ignoresSafeArea(.all) .previewDisplayName("Rotated Symbols (Dynamic)") + + 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") // FIXME: This appears to be broken upstream; waiting for a new release // MapView(styleURL: demoTilesURL) { diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 3db30a4..95bb663 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -9,7 +9,10 @@ public struct MapView: UIViewRepresentable { let styleSource: MapStyleSource let userLayers: [StyleLayerDefinition] - var mapViewModifier: ((MLNMapView) -> Void)? + + /// 'Escape hatch' to MLNMapView until we have more modifiers. + /// See ``unsafeMapViewModifier(_:)`` + private var unsafeMapViewModifier: ((MLNMapView) -> Void)? public init( styleURL: URL, @@ -30,10 +33,17 @@ public struct MapView: UIViewRepresentable { self.init(styleURL: styleURL, camera: .constant(initialCamera), makeMapContent) } - /// Allows you to set properties of the underlying MLNMapView directly. + /// Allows you to set properties of the underlying MLNMapView directly + /// in cases where these have not been ported to DSL yet. /// Use this function to modify various properties of the MLNMapView instance. /// For example, you can enable the display of the user's location on the map by setting `showUserLocation` to true. /// + /// This is an 'escape hatch' back to the non-DSL world + /// of MapLibre for features that have not been ported to DSL yet. + /// Be careful not to use this to modify properties that are + /// already ported to the DSL, like the camera for example, as your + /// modifications here may break updates that occur with modifiers. + /// /// - Parameter modifier: A closure that provides you with an MLNMapView so you can set properties. /// - Returns: A MapView with the modifications applied. /// @@ -45,9 +55,9 @@ public struct MapView: UIViewRepresentable { /// } /// ``` /// - public func mapViewModifier(_ modifier: @escaping (MLNMapView) -> Void) -> MapView { + public func unsafeMapViewModifier(_ modifier: @escaping (MLNMapView) -> Void) -> MapView { var newMapView = self - newMapView.mapViewModifier = modifier + newMapView.unsafeMapViewModifier = modifier return newMapView } @@ -214,9 +224,9 @@ public struct MapView: UIViewRepresentable { public func updateUIView(_ mapView: MLNMapView, context: Context) { context.coordinator.parent = self - if let mapViewModifier { - mapViewModifier(mapView) - } + + unsafeMapViewModifier?(mapView) + // FIXME: This should be a more selective update context.coordinator.updateStyleSource(styleSource, mapView: mapView) context.coordinator.updateLayers(mapView: mapView) diff --git a/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift b/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift index 58e13e1..eaed93f 100644 --- a/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift +++ b/Tests/MapLibreSwiftDSLTests/ShapeSourceTests.swift @@ -35,4 +35,24 @@ final class ShapeSourceTests: XCTestCase { XCTFail("Expected a feature source") } } + + func testForInAndCombinationFeatureBuilder() throws { + // ShapeSource now accepts 'for in' building, arrays, and combinations of them + let shapeSource = ShapeSource(identifier: "foo") { + for coordinates in samplePedestrianWaypoints { + MLNPointFeature(coordinate: coordinates) + } + MLNPointFeature(coordinate: CLLocationCoordinate2D(latitude: 48.2082, longitude: 16.3719)) + } + + XCTAssertEqual(shapeSource.identifier, "foo") + + switch shapeSource.data { + case .features(let features): + XCTAssertEqual(features.count, 48) + default: + XCTFail("Expected a feature source") + } + } + } From b72ed8f47b3cf3971ae2f5d9aaeb36b6f17f49dc Mon Sep 17 00:00:00 2001 From: PW Date: Mon, 22 Jan 2024 15:36:11 +0100 Subject: [PATCH 4/5] moving example to own file --- Sources/MapLibreSwiftUI/Examples/Layers.swift | 13 ------ Sources/MapLibreSwiftUI/Examples/Other.swift | 40 +++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 Sources/MapLibreSwiftUI/Examples/Other.swift diff --git a/Sources/MapLibreSwiftUI/Examples/Layers.swift b/Sources/MapLibreSwiftUI/Examples/Layers.swift index 99e03f2..ee0205e 100644 --- a/Sources/MapLibreSwiftUI/Examples/Layers.swift +++ b/Sources/MapLibreSwiftUI/Examples/Layers.swift @@ -58,19 +58,6 @@ struct Layer_Previews: PreviewProvider { } .ignoresSafeArea(.all) .previewDisplayName("Rotated Symbols (Dynamic)") - - 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") // FIXME: This appears to be broken upstream; waiting for a new release // MapView(styleURL: demoTilesURL) { diff --git a/Sources/MapLibreSwiftUI/Examples/Other.swift b/Sources/MapLibreSwiftUI/Examples/Other.swift new file mode 100644 index 0000000..0e7dec9 --- /dev/null +++ b/Sources/MapLibreSwiftUI/Examples/Other.swift @@ -0,0 +1,40 @@ +import CoreLocation +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")! + + // 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: 39.02001, longitude: 1.482148)) { feature in + feature.attributes["icon"] = "club" + 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") + } +} From e49b998166371d20977ef0fd2ff0f7a88749463b Mon Sep 17 00:00:00 2001 From: P W Date: Tue, 23 Jan 2024 10:12:41 +0100 Subject: [PATCH 5/5] Update Sources/MapLibreSwiftUI/MapView.swift Co-authored-by: Ian Wagner --- Sources/MapLibreSwiftUI/MapView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/MapLibreSwiftUI/MapView.swift b/Sources/MapLibreSwiftUI/MapView.swift index 95bb663..5364a98 100644 --- a/Sources/MapLibreSwiftUI/MapView.swift +++ b/Sources/MapLibreSwiftUI/MapView.swift @@ -43,6 +43,8 @@ public struct MapView: UIViewRepresentable { /// Be careful not to use this to modify properties that are /// already ported to the DSL, like the camera for example, as your /// modifications here may break updates that occur with modifiers. + /// In particular, this modifier is potentially dangerous as it runs on + /// EVERY call to `updateUIView`. /// /// - Parameter modifier: A closure that provides you with an MLNMapView so you can set properties. /// - Returns: A MapView with the modifications applied.