Skip to content

Commit 95edefa

Browse files
pjleonard37Release SDK bot for Maps SDK teamkediarov
authored andcommitted
Add Default Markers to iOS and Android Maps SDKs (#3597)
JIRA epic [here](https://mapbox.atlassian.net/browse/MAPSSDK-813). This PR adds default markers to our iOS and Android Maps SDKs. These Markers are convenience implementations of View Annotations with a limited set of customization options (color, stoke, inner color, text). Markers are only available in Swift UI and JetPack Compose. --------- Co-authored-by: Release SDK bot for Maps SDK team <[email protected]> Co-authored-by: Kirill Kediarov <[email protected]> GitOrigin-RevId: c6793cba5540e00cf049861962fa9deab1781630
1 parent eebd7df commit 95edefa

File tree

16 files changed

+371
-5
lines changed

16 files changed

+371
-5
lines changed

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,23 @@ Mapbox welcomes participation and contributions from everyone.
44

55
## main
66

7+
* Introduce experimental `Marker` convenience API in Swift UI. Use `Marker` to quickly add a `MapViewAnnotation` pin at the specified coordinates with custom text and color.
8+
9+
```swift
10+
Map {
11+
Marker(coordinate: CLLocationCoordinate2D(...))
12+
.text("My marker")
13+
.color(.blue)
14+
.stroke(.orange)
15+
}
16+
```
17+
718
## 11.14.0-beta.1 - 02 July, 2025
8-
* Added new `split` expression, which returns an array of substrings from a string, split by a delimiter parameter.
919

20+
## Features ✨ and improvements 🏁
21+
* Added new `split` expression, which returns an array of substrings from a string, split by a delimiter parameter.
1022
* Allow option to set SDF on a `PointAnnotation` image in Style DSL
1123

12-
## Features ✨ and improvements 🏁
1324
## 11.13.1 - 18 June, 2025
1425

1526
## 11.13.0 - 17 June, 2025

Examples.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
68FD9E1F4606B2729BA1E6DC /* SnapshotterExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588FD640D91E9DD366703F7B /* SnapshotterExample.swift */; platformFilter = ios; };
8080
6B040F65241ABF600D70D14D /* Custom3DPuckExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C957F9CA07061B793C2DD4A /* Custom3DPuckExample.swift */; platformFilter = ios; };
8181
7036A19FCD2CCE85BDDF4E00 /* TrackingModeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5E598A16FA446F583344CB /* TrackingModeExample.swift */; platformFilter = ios; };
82+
7350DF212DFCC73A00B47542 /* MarkersExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7350DF1E2DFCC73A00B47542 /* MarkersExample.swift */; };
8283
7352DDB22DD7E9EE000E3000 /* StudioStyleExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7352DDB02DD7E9EE000E3000 /* StudioStyleExample.swift */; };
8384
7365170E39A459EB4DFA198B /* ExamplesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE18E37A8652B4807D2459F1 /* ExamplesUITests.swift */; platformFilter = ios; };
8485
759D42AA5FE93B6FA9DFADF5 /* LocationOverrideExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B39AE24486FED5ED30392D /* LocationOverrideExample.swift */; };
@@ -256,6 +257,7 @@
256257
6E54D3F5943238258AA0A9BE /* ResizeMapViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeMapViewExample.swift; sourceTree = "<group>"; };
257258
70922E748D003176C4A3C60A /* HeatmapLayerGlobeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeatmapLayerGlobeExample.swift; sourceTree = "<group>"; };
258259
7274E152F7FBB7894447F822 /* AnimateGeoJSONLineExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimateGeoJSONLineExample.swift; sourceTree = "<group>"; };
260+
7350DF1E2DFCC73A00B47542 /* MarkersExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkersExample.swift; sourceTree = "<group>"; };
259261
7352DDB02DD7E9EE000E3000 /* StudioStyleExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudioStyleExample.swift; sourceTree = "<group>"; };
260262
7352DDB42DDB93B5000E3000 /* StudioStyleExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StudioStyleExample.swift; path = "Sources/Examples/SwiftUI Examples/Testing Examples/StudioStyleExample.swift"; sourceTree = "<group>"; };
261263
75D03F5A3A0E879717BFE421 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
@@ -421,6 +423,7 @@
421423
C61CC711054A032EE0446036 /* DynamicStylingExample.swift */,
422424
09BBD2991186A6B98F730454 /* ElevatedLineMapView.swift */,
423425
A6B06A1D70F479D8DC5C375A /* FeaturesQueryExample.swift */,
426+
7350DF1E2DFCC73A00B47542 /* MarkersExample.swift */,
424427
7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */,
425428
DD3A816C6E4D7A0A532EEE84 /* GeofencingUserLocation.swift */,
426429
62DA0608D44DEF6C4A82777C /* LocateMeExample.swift */,
@@ -876,6 +879,7 @@
876879
3B4862E6832F23CB115D444A /* ClipLayerExample.swift in Sources */,
877880
1DAE02D73D16E543777C2025 /* ClusteringExample.swift in Sources */,
878881
5A28C124249725578389175A /* ColorExpressionExample.swift in Sources */,
882+
7350DF212DFCC73A00B47542 /* MarkersExample.swift in Sources */,
879883
DA109856E64BBD8071DF0619 /* ColorThemeExample.swift in Sources */,
880884
65E9F2B993AEB394FC2D0080 /* ColorThemeMapExample.swift in Sources */,
881885
C664365A373267B564EC84EE /* CombineExample.swift in Sources */,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import SwiftUI
2+
import Turf
3+
@_spi(Experimental) import MapboxMaps
4+
5+
struct MarkersExample: View {
6+
@State private var markerColor = Color(red: 207/255, green: 218/255, blue: 247/255, opacity: 1.0)
7+
@State private var strokeColor = Color(red: 58/255, green: 89/255, blue: 250/255, opacity: 1.0)
8+
@State private var showStroke: Bool = true
9+
@State private var showText: Bool = true
10+
@State private var overlayHeight: CGFloat = 0
11+
@State private var markerText: String = "Central Helsinki"
12+
@State private var tappedPoints = [CLLocationCoordinate2D]()
13+
14+
var body: some View {
15+
Map(initialViewport: .camera(center: .helsinki, zoom: 15, pitch: 60)) {
16+
Marker(coordinate: .helsinki)
17+
.color(markerColor)
18+
.stroke(showStroke ? strokeColor : nil)
19+
.text(showText ? markerText : nil)
20+
21+
ForEvery(tappedPoints, id: \.latitude) { coord in
22+
Marker(coordinate: coord)
23+
.color(markerColor)
24+
.stroke(showStroke ? strokeColor : nil)
25+
.text(showText ? String(format: "%.3f, %.3f", coord.latitude, coord.longitude) : nil)
26+
}
27+
28+
TapInteraction { tapContext in
29+
tappedPoints.append(tapContext.coordinate)
30+
return true
31+
}
32+
33+
LongPressInteraction { _ in
34+
tappedPoints.removeAll()
35+
return true
36+
}
37+
}
38+
.additionalSafeAreaInsets(.bottom, overlayHeight)
39+
.ignoresSafeArea(edges: [.all] )
40+
.overlay(alignment: .bottom) {
41+
VStack(alignment: .leading) {
42+
Text("Tap to add a Marker")
43+
ColorPicker("Marker Color", selection: $markerColor)
44+
ColorPicker("Stroke Color", selection: $strokeColor)
45+
Toggle("Show stroke", isOn: $showStroke)
46+
Toggle("Show Marker text", isOn: $showText)
47+
}
48+
.padding(.horizontal, 10)
49+
.padding(.vertical, 6)
50+
.floating(RoundedRectangle(cornerRadius: 10))
51+
.limitPaneWidth()
52+
.onChangeOfSize { size in
53+
overlayHeight = size.height
54+
}
55+
}
56+
}
57+
}
58+
59+
struct MarkersExample_Previews: PreviewProvider {
60+
static var previews: some View {
61+
MarkersExample()
62+
}
63+
}

Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct SwiftUIRoot: View {
2121
} header: { Text("Standard Style") }
2222

2323
Section {
24+
ExampleLink("Add Map Markers", note: "Add/remove Markers to your map.", destination: MarkersExample())
2425
ExampleLink("View Annotations", note: "Add/remove view annotation on tap.", destination: ViewAnnotationsExample())
2526
ExampleLink("Weather annotations", note: "Show view annotations with contents changed on selection.", destination: WeatherAnnotationExample())
2627
ExampleLink("Layer Annotations", note: "Add/remove layer annotation on tap.", destination: AnnotationsExample())
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import SwiftUI
2+
3+
/// Displays a simple map Marker at the specified coordinated.
4+
///
5+
/// `Marker` is a convenience struct which creates a simple `MapViewAnnotation` with limited customization options.
6+
/// Use `Marker` to quickly add a pin annotation at the specific coordinates when using SwiftUI.
7+
/// If you need greater customization use `MapViewAnnotation` directly.
8+
///
9+
/// ```swift
10+
/// Map {
11+
/// Marker(coordinate: CLLocationCoordinate2D(...))
12+
/// .text("My marker")
13+
/// .color(.blue)
14+
/// .stroke(.purple)
15+
/// .innerColor(.white)
16+
/// }
17+
/// ```
18+
///
19+
/// - Note: `Marker`s are great for displaying unique interactive features. However, they may be suboptimal for large amounts of data and don't support clustering.
20+
/// Each marker creates a SwiftUI view, so for scenarios with 100+ markers, consider using ``PointAnnotation``,
21+
/// Additionally, `Marker`s appear above all content of MapView (e.g. layers, annotations, puck). If you need to display annotation between layers or below a puck, use ``PointAnnotation``.
22+
@_spi(Experimental)
23+
public struct Marker {
24+
25+
/// The `MapViewAnnotation` which will be displayed on the map
26+
var mapViewAnnotation: MapViewAnnotation {
27+
build()
28+
}
29+
30+
/// The geographic location of the Marker
31+
var coordinate: CLLocationCoordinate2D
32+
33+
/// The optional text the Marker will display
34+
var text: String?
35+
36+
/// The color of the outerImage
37+
var outerColor = Color(red: 207/255, green: 218/255, blue: 247/255, opacity: 1.0)
38+
39+
/// The color of the innerImage
40+
var innerColor = Color(red: 1, green: 1, blue: 1, opacity: 1.0)
41+
42+
/// The color of optional strokes
43+
var strokeColor: Color? = Color(red: 58/255, green: 89/255, blue: 250/255, opacity: 1.0)
44+
45+
/// The outer image of the Marker
46+
private let outerImage = Image("default_marker_outer", bundle: .mapboxMaps)
47+
48+
/// The inner image of the Marker
49+
private let innerImage = Image("default_marker_inner", bundle: .mapboxMaps)
50+
51+
/// The outer stroke of the Marker
52+
private let outerStroke = Image("default_marker_outer_stroke", bundle: .mapboxMaps)
53+
54+
/// The inner stroke of the Marker
55+
private let innerStroke = Image("default_marker_inner_stroke", bundle: .mapboxMaps)
56+
57+
/// Set text for the Marker
58+
public func text(_ text: String?) -> Self {
59+
with(self, setter(\.text, text))
60+
}
61+
62+
/// Set a color for the Marker
63+
public func color(_ color: Color) -> Self {
64+
with(self, setter(\.outerColor, color))
65+
}
66+
67+
/// Set a color for the Marker's inner circle
68+
public func innerColor(_ color: Color) -> Self {
69+
with(self, setter(\.innerColor, color))
70+
}
71+
72+
/// Set a color for the Marker's strokes. Set nil to remove the strokes.
73+
public func stroke(_ color: Color?) -> Self {
74+
with(self, setter(\.strokeColor, color))
75+
}
76+
77+
/// Build a `MapViewAnnotation` with the current Marker properties
78+
private func build() -> MapViewAnnotation {
79+
MapViewAnnotation(coordinate: coordinate) {
80+
ZStack(alignment: .top) {
81+
markerImage
82+
if let text {
83+
markerText(text)
84+
}
85+
}
86+
}
87+
.allowOverlap(true)
88+
}
89+
90+
/// Returns the compiled Marker image
91+
@ViewBuilder
92+
private var markerImage: some View {
93+
ZStack {
94+
applyColor(outerImage, color: outerColor)
95+
.frame(width: 32, height: 40)
96+
.shadow(color: .black.opacity(0.17), radius: 1, x: 0, y: 2)
97+
.shadow(color: .black.opacity(0.15), radius: 0.5, x: 0, y: 0)
98+
if let strokeColor {
99+
applyColor(outerStroke, color: strokeColor)
100+
}
101+
applyColor(innerImage, color: innerColor)
102+
.frame(width: 32, height: 40)
103+
if let strokeColor {
104+
applyColor(innerStroke, color: strokeColor)
105+
}
106+
}
107+
.offset(y: -20) // Center marker on coordinate (half of 40pt height)
108+
}
109+
110+
/// Returns the Marker text
111+
@ViewBuilder
112+
private func markerText(_ text: String) -> some View {
113+
Text(text)
114+
.font(.system(size: 15))
115+
.fontWeight(.medium)
116+
.foregroundColor(.black)
117+
.lineLimit(3)
118+
.frame(width: 200)
119+
.multilineTextAlignment(.center)
120+
.offset(y: 24) // Position text below marker (20pt + 4pt spacing)
121+
.shadow(color: .white, radius: 0, x: -1, y: -1)
122+
.shadow(color: .white, radius: 0, x: 1, y: -1)
123+
.shadow(color: .white, radius: 0, x: -1, y: 1)
124+
.shadow(color: .white, radius: 0, x: 1, y: 1)
125+
}
126+
127+
/// Apply color using foregroundStyle (iOS 15+) or foregroundColor (iOS 14-)
128+
@ViewBuilder
129+
private func applyColor(_ image: Image, color: Color) -> some View {
130+
if #available(iOS 15.0, *) {
131+
image.foregroundStyle(color)
132+
} else {
133+
image.foregroundColor(color)
134+
}
135+
}
136+
137+
/// Create a marker at the specific coordinate
138+
public init(coordinate: CLLocationCoordinate2D) {
139+
self.coordinate = coordinate
140+
}
141+
}
142+
143+
extension Marker: MapContent, PrimitiveMapContent {
144+
func visit(_ node: MapContentNode) {
145+
node.mount(MountedViewAnnotation(mapViewAnnotation: mapViewAnnotation))
146+
}
147+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "default_marker_inner.svg",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"idiom" : "universal",
10+
"scale" : "2x"
11+
},
12+
{
13+
"idiom" : "universal",
14+
"scale" : "3x"
15+
}
16+
],
17+
"info" : {
18+
"author" : "xcode",
19+
"version" : 1
20+
},
21+
"properties" : {
22+
"template-rendering-intent" : "template"
23+
}
24+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "default_marker_inner_stroke.svg",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"idiom" : "universal",
10+
"scale" : "2x"
11+
},
12+
{
13+
"idiom" : "universal",
14+
"scale" : "3x"
15+
}
16+
],
17+
"info" : {
18+
"author" : "xcode",
19+
"version" : 1
20+
},
21+
"properties" : {
22+
"template-rendering-intent" : "template"
23+
}
24+
}
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)