From ce0d53a48a89d5475c7a2e72a29c0738da0c82e0 Mon Sep 17 00:00:00 2001 From: Aleksei Sapitskii <45671572+aleksproger@users.noreply.github.com> Date: Tue, 7 May 2024 12:46:16 +0300 Subject: [PATCH] [MAPSIOS-1406] Extend documentation about SwiftUI and Declarative Styling (#2134) --- .../All Examples/RuntimeSlotsExample.swift | 2 +- .../Articles/Declarative Map Styling.md | 105 +++++++++++++++++- .../Articles/SwiftUI User Guide.md | 31 +++++- 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/Apps/Examples/Examples/All Examples/RuntimeSlotsExample.swift b/Apps/Examples/Examples/All Examples/RuntimeSlotsExample.swift index b0c9eaa97edb..6f5c07798060 100644 --- a/Apps/Examples/Examples/All Examples/RuntimeSlotsExample.swift +++ b/Apps/Examples/Examples/All Examples/RuntimeSlotsExample.swift @@ -46,7 +46,7 @@ final class RuntimeSlotsExample: UIViewController, ExampleProtocol { /// - "square" layer /// ... top layers layers ... /// ``` - /// If any other layers or annotations added to the `annotation-placeholder` slot, they will appear above the triangle annotation, but below the square layer. + /// If any other layers or annotations are added to the `annotation-placeholder` slot, they will appear above the triangle annotation, but below the square layer. let manager = mapView.annotations.makePolygonAnnotationManager() manager.slot = "annotation-placeholder" manager.annotations = [ diff --git a/Sources/MapboxMaps/Documentation.docc/Articles/Declarative Map Styling.md b/Sources/MapboxMaps/Documentation.docc/Articles/Declarative Map Styling.md index ff0b9a790ee3..2402b27503a2 100644 --- a/Sources/MapboxMaps/Documentation.docc/Articles/Declarative Map Styling.md +++ b/Sources/MapboxMaps/Documentation.docc/Articles/Declarative Map Styling.md @@ -68,9 +68,10 @@ Atmosphere() Category | Types supported ------------ | ------------------------------------- `Source` | ``VectorSource``, ``RasterSource``, ``RasterDemSource``, ``GeoJSONSource``, ``ImageSource``, ``Model``, ``CustomGeometrySource`` (partial), ``CustomRasterSource`` (partial) -`Layer` | ``FillLayer``, ``LineLayer``, ``SymbolLayer``, ``CircleLayer``, ``HeatmapLayer``, ``FillExtrusionLayer``, ``RasterLayer``, ``HillshadeLayer``, ``BackgroundLayer``, ``LocationIndicatorLayer``, ``SkyLayer``, ``ModelLayer``, ``CustomLayer`` (partial) +`Layer` | ``FillLayer``, ``LineLayer``, ``SymbolLayer``, ``CircleLayer``, ``HeatmapLayer``, ``FillExtrusionLayer``, ``RasterLayer``, ``HillshadeLayer``, ``BackgroundLayer``, ``LocationIndicatorLayer``, ``SkyLayer``, ``ModelLayer``, ``SlotLayer``, ``CustomLayer`` (partial) `Lights` | ``FlatLight``, ``AmbientLight``, ``DirectionalLight`` `Map properties` | ``Projection``, ``Atmosphere``, ``Terrain``, ``TransitionOptions-struct`` +`Fragments` | ``StyleImport`` ### Adding Style Primitives Conditionally @@ -129,6 +130,8 @@ You can create your own primitives in addition to Mapbox style primitives. Defin For example, the code below creates a `CarModelPrimitive` which manages all you need to display a sport care Model on your map: the ``GeoJSONSource`` for the data, the ``Model`` to display, and the ``ModelLayer`` used to position the model. Add your `CarModelPrimitive` to your style body just like Mapbox style primitives. +> Warning: We recommend not using @State or @Binding as part of your custom primitives. @State will only work as part of root view that contain ``Map``. @Binding may work in your components, but it will break content recalculation logic and will lead to worse performance. + ```swift struct CarModelPrimitive: MapStyleContent { var body: some MapStyleContent { @@ -159,6 +162,106 @@ Map { } ``` +### Content positioning + +Our aim was to establish a single source of truth for all content displayed on the map through our declarative approach. Moreover, we invested significant efforts in automating the heavy lifting of manual layer positioning, seamlessly incorporating it into the declarative description. + +Essentially, this means that all layers defined in the declarative description will be positioned on the map relative to each other, following a similar pattern as [SwiftUI's ZStack](https://developer.apple.com/documentation/swiftui/zstack). + +```swift +let coordinate = CLLocationCoordinate2D(latitude: 60.167488, longitude: 24.942747) + +var body: some View { + Map(initialViewport: .camera(center: .init(latitude: 27.2, longitude: -26.9), zoom: 1.53, bearing: 0, pitch: 0)) { + MapViewAnnotation(coordinate: coordinate) { + Circle() + .fill(.purple) + .frame(width: 40, height: 40) + } + + PolygonAnnotation(polygon: Polygon(center: coordinate, radius: 8 * 100, vertices: 60)) + .fillColor(StyleColor(.yellow)) + + + GeoJSONSource(id: "source") + .data(.geometry(.polygon(Polygon(center: coordinate, radius: 4 * 100, vertices: 60)))) + + FillLayer(id: "fill-id", source: "source") + .fillColor(.green) + .fillOpacity(0.7) + } +} +``` + +Referring back to the example above, the order of layers will be: [default map style layers] -> [yellow polygon] -> [green polygon]. It's worth noting that MapViewAnnotation doesn't participate in layer ordering and will always be displayed above any layers. + +In contrast to the imperative API, we intentionally removed the ability to set layer positions using the `.position` modifier for `MapStyleContent`. This emphasizes that the order of layer declarations should precisely reflect the actual layer ordering. As a result, in the declarative API you cannot add runtime layers between style layers already in the Style. + +So, despite the elegance of declarative ordering, there are scenarios where it falls short: + +1. Placing layers between style layers that were not added at runtime. +2. Interoperability with traditional imperative APIs. +3. Slots API. + +To address these scenarios, we introduced ``SlotLayer``, which slightly disrupts the elegance of declarative ordering but provides a crucial mechanism and single entry point for more advanced layer ordering cases. This ensures that the impact of this disruption to ordering remains minimal. + +The following example demonstrates how ``SlotLayer`` allows the addition of runtime slots, facilitating interactions with imperative APIs and dividing existing slots into more granular groups. + +```swift +mapView.mapboxMap.mapStyle = .standard +mapView.mapboxMap.setMapStyleContent { + GeoJSONSource(id: "square-data") + .data(.feature(Feature(geometry: square))) + + /// The MapStyleContent defines the desired layers positions. + /// ... bottom layers ... + /// "middle" slot + /// - "annotation-placeholder" slot + /// - "polygon" layer + /// ... top layers layers ... + SlotLayer(id: "annotation-placeholder") + .slot(.middle) + FillLayer(id: "square", source: "square-data") + .fillColor(.systemPink) + .fillOpacity(0.8) + .slot(.middle) +} + +/// The annotation uses slot `annotation-placeholder` so it will be rendered below the polygon: +/// ... bottom layers ... +/// "middle" slot +/// - triangle annotation +/// - "annotation-placeholder" slot +/// - "square" layer +/// ... top layers layers ... +/// If any other layers or annotations are added to the `annotation-placeholder` slot, they will appear above the triangle annotation, but below the square layer. +let manager = mapView.annotations.makePolygonAnnotationManager() +manager.slot = "annotation-placeholder" +manager.annotations = [ + PolygonAnnotation(polygon: triangle).fillColor(StyleColor(.yellow)) +] + +``` + +Another important feature of ``SlotLayer`` is that it's the only ``MapStyleContent`` component with a .position modifier, allowing developers to set a custom ``LayerPosition``. This effectively resolves the scenario where a developer needs to insert a runtime-added layer between style layers that are part of the Style JSON. +Please note that setting both `.slot()` and `.position()` for ``SlotLayer`` is incorrect and `slot` will always have priority over the `position`. + +```swift +Map { + SlotLayer(id: "below-roads") + .position(.below("roads")) + + GeoJSONSource(id: "square-data") + .data(.feature(Feature(geometry: square))) + + FillLayer(id: "square", source: "square-data") + .fillColor(.systemPink) + .slot(Slot(rawValue: "below-roads") +} +.mapStyle(.streets) +``` + +In the example above ``FillLayer`` will be added below "roads" layer from Mapbox Streets style. ### Performance Optimizations diff --git a/Sources/MapboxMaps/Documentation.docc/Articles/SwiftUI User Guide.md b/Sources/MapboxMaps/Documentation.docc/Articles/SwiftUI User Guide.md index d4cab3288d3b..0237fa4c1737 100644 --- a/Sources/MapboxMaps/Documentation.docc/Articles/SwiftUI User Guide.md +++ b/Sources/MapboxMaps/Documentation.docc/Articles/SwiftUI User Guide.md @@ -27,7 +27,7 @@ Puck 2D/3D | ✅ Map Events | ✅ Gesture Configuration | ✅ Ornaments Configuration | ✅ -Style API | 🚧 | Use ``MapReader`` to access Style API via ``MapProxy/map``. +Style API | ✅ Custom Camera Animations | 🚧 ### Getting started @@ -91,6 +91,35 @@ extension MapStyle { Please consult the ``MapStyle`` documentation to find more information about style loading. +### Declarative Map Styling + +With the advent of Declarative Map Styling, it's now feasible to reuse ``MapStyleContent`` components within SwiftUI, offering a robust and exhaustive method to delineate map content comprehensively in one place. + +The following example illustrates the utilization of both ``MapStyleContent``, which can also be utilized outside of SwiftUI, and SwiftUI-specific ``MapContent`` within a singular declarative ``Map`` description: + +```swift +Map(initialViewport: .camera(center: .init(latitude: 27.2, longitude: -26.9), zoom: 1.53, bearing: 0, pitch: 0)) { + MapViewAnnotation(coordinate: .apple) { + Circle() + .fill(.purple) + .frame(width: 40, height: 40) + } + + PolygonAnnotation(polygon: Polygon(center: .apple, radius: 8 * 100, vertices: 60)) + .fillColor(StyleColor(.yellow)) + + + GeoJSONSource(id: "source") + .data(.geometry(.polygon(Polygon(center: .apple, radius: 4 * 100, vertices: 60)))) + + FillLayer(id: "fill-id", source: "source") + .fillColor(.green) + .fillOpacity(0.7) +} +``` + +Within SwiftUI, all ``MapStyleContent`` elements will be retained during style reloads and appropriately re-added. This ensures that the sole source of truth for map content lies within the declaration itself. SwiftUI's ``MapContent`` serves as an extension of the Declarative Map Styling approach previously introduced for the UIKit API. Therefore, it's advisable to peruse the guide to become acquainted with the underlying concepts of this declarative styling paradigm. + ### Using Viewport to manage camera ``Viewport`` is a powerful abstraction that manages the camera in SwiftUI. It supports multiple modes, such as `camera`, `overview`, `followPuck`, and others.