Skip to content

Commit 3056ad9

Browse files
pjleonard37github-actions[bot]
authored andcommitted
Add Appearances examples for iOS and Android [MAPSIOS-1994][MAPSAND-2367] (#7521)
Adds Appearances example implementations for both iOS and Android, demonstrating how to use feature-state to dynamically change symbol icons based on user interaction. The examples were adapted from the GL JS appearances example to maintain consistency across platforms. GitOrigin-RevId: 60cbaf554bdb8637d0d44a100a5220ad16b8ac45
1 parent 0bc0de8 commit 3056ad9

File tree

9 files changed

+383
-127
lines changed

9 files changed

+383
-127
lines changed

Examples.xcodeproj/project.pbxproj

Lines changed: 152 additions & 127 deletions
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "hotel-active.png",
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+
}
2.83 KB
Loading
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "hotel-clicked.png",
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+
}
1.95 KB
Loading
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "hotel.png",
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+
}
1.45 KB
Loading
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import SwiftUI
2+
@_spi(Experimental) import MapboxMaps
3+
4+
/// Example demonstrating the experimental Appearances API for dynamic icon states.
5+
/// Shows how to use appearances with feature-state to change icon images based on user interaction.
6+
/// - Default: hotel icon
7+
/// - Currently Selected: hotel-active icon
8+
/// - Previously Clicked: hotel-clicked icon
9+
struct AppearancesExample: View {
10+
private static let currentlySelectedKey = "currentlySelected"
11+
private static let hasBeenClickedKey = "hasBeenClicked"
12+
13+
@State private var selectedFeature: FeaturesetFeature?
14+
@State private var clickedFeatureIds: Set<String> = []
15+
16+
var body: some View {
17+
MapReader { proxy in
18+
Map(initialViewport: .camera(center: .pyrenees, zoom: 15.5)) {
19+
// When a hotel icon is tapped, set the currentlySelected feature state to true,
20+
// unselect the previous one if any, and store this feature both as the selected
21+
// feature and in the list of features that have been clicked
22+
TapInteraction(.layer("points")) { feature, _ in
23+
guard let map = proxy.map else { return false }
24+
25+
// Clear the currently selected feature by resetting its feature state
26+
if let previousFeature = selectedFeature {
27+
map.setFeatureState(previousFeature, state: [Self.currentlySelectedKey: false])
28+
}
29+
30+
// Store this feature as the currently selected feature and in the list
31+
// of features that have been clicked
32+
if let featureId = feature.id?.id {
33+
clickedFeatureIds.insert(featureId)
34+
map.setFeatureState(feature, state: [
35+
Self.currentlySelectedKey: true,
36+
Self.hasBeenClickedKey: true
37+
])
38+
selectedFeature = feature
39+
print("✅ Selected feature \(featureId)")
40+
}
41+
42+
return true
43+
}
44+
45+
// When the map is tapped outside of any feature, unselect the currently selected
46+
// feature if there's any, or remove all features from the list of features that
47+
// have been clicked to get back to the initial state
48+
TapInteraction { _ in
49+
guard let map = proxy.map else { return false }
50+
51+
if let previousFeature = selectedFeature {
52+
// Unselect the currently selected feature
53+
map.setFeatureState(previousFeature, state: [Self.currentlySelectedKey: false])
54+
selectedFeature = nil
55+
print("✅ Cleared selection")
56+
} else {
57+
// Reset the state of all features to the default one
58+
clickedFeatureIds.forEach { id in
59+
map.setFeatureState(
60+
sourceId: "points",
61+
featureId: id,
62+
state: [Self.hasBeenClickedKey: false]
63+
) { _ in }
64+
}
65+
clickedFeatureIds.removeAll()
66+
print("✅ Reset all features")
67+
}
68+
69+
return true
70+
}
71+
}
72+
.onStyleLoaded { _ in
73+
guard let map = proxy.map else { return }
74+
setupAppearances(map)
75+
}
76+
.mapStyle(.standard)
77+
.ignoresSafeArea()
78+
}
79+
}
80+
81+
private func setupAppearances(_ map: MapboxMap) {
82+
// Load an image for every feature state
83+
let hotelIcon = UIImage(resource: .hotel)
84+
let hotelActiveIcon = UIImage(resource: .hotelActive)
85+
let hotelClickedIcon = UIImage(resource: .hotelClicked)
86+
87+
do {
88+
try map.addImage(hotelIcon, id: "hotel", sdf: false)
89+
try map.addImage(hotelActiveIcon, id: "hotel-active", sdf: false)
90+
try map.addImage(hotelClickedIcon, id: "hotel-clicked", sdf: false)
91+
print("✅ Added all images (hotel, hotel-active, hotel-clicked)")
92+
} catch {
93+
print("❌ Failed to add images: \(error)")
94+
return
95+
}
96+
97+
// Add a GeoJSON source with hotel locations
98+
let sourceJSON: [String: Any] = [
99+
"type": "geojson",
100+
"data": [
101+
"type": "FeatureCollection",
102+
"features": [
103+
["type": "Feature", "id": 1, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8452993238082342, 42.100164223399275]]],
104+
["type": "Feature", "id": 2, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8438590191857145, 42.1004178052402]]],
105+
["type": "Feature", "id": 3, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.844225198327564, 42.10130533369667]]],
106+
["type": "Feature", "id": 4, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8443594640122, 42.0990955459275]]],
107+
["type": "Feature", "id": 5, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8449697625811154, 42.09869705141318]]],
108+
["type": "Feature", "id": 6, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8471058075726603, 42.09978384873651]]],
109+
["type": "Feature", "id": 7, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8455739474818813, 42.10182152060625]]],
110+
["type": "Feature", "id": 8, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8427787800360136, 42.10039061289771]]],
111+
["type": "Feature", "id": 9, "properties": [:], "geometry": ["type": "Point", "coordinates": [1.8433280487479635, 42.0994396753579]]]
112+
]
113+
]
114+
]
115+
116+
do {
117+
try map.addSource(withId: "points", properties: sourceJSON)
118+
print("✅ Added GeoJSON source")
119+
} catch {
120+
print("❌ Failed to add source: \(error)")
121+
return
122+
}
123+
124+
// Add a layer to show an icon on every point
125+
let layerJSON: [String: Any] = [
126+
"id": "points",
127+
"type": "symbol",
128+
"source": "points",
129+
"layout": [
130+
"icon-allow-overlap": true,
131+
"icon-image": "hotel",
132+
"icon-size": 0.75
133+
],
134+
// appearances are experimental and subject to change in future versions
135+
"appearances": [
136+
[
137+
"name": "clicked",
138+
"condition": ["boolean", ["feature-state", Self.currentlySelectedKey], false],
139+
"properties": ["icon-image": "hotel-active"]
140+
],
141+
[
142+
"name": "has-been-clicked",
143+
"condition": ["boolean", ["feature-state", Self.hasBeenClickedKey], false],
144+
"properties": ["icon-image": "hotel-clicked"]
145+
]
146+
]
147+
]
148+
149+
do {
150+
try map.addLayer(with: layerJSON, layerPosition: nil)
151+
print("✅ Added symbol layer with appearances")
152+
} catch {
153+
print("❌ Failed to add layer: \(error)")
154+
}
155+
}
156+
157+
}
158+
159+
private extension CLLocationCoordinate2D {
160+
static let pyrenees = CLLocationCoordinate2D(latitude: 42.10025506, longitude: 1.8447281852)
161+
}
162+
163+
struct AppearancesExample_Previews: PreviewProvider {
164+
static var previews: some View {
165+
AppearancesExample()
166+
}
167+
}

Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ struct SwiftUIExamples {
2727
Example("Query Rendered Features on tap", note: "Use MapReader and MapboxMap to query rendered features.", destination: FeaturesQueryExample())
2828
#endif
2929
Example("Clustering data", note: "Display GeoJSON data with clustering using custom layers and handle interactions with them.", destination: ClusteringExample())
30+
Example("Appearances", note: "Change icon images dynamically using the Appearances API with feature-state.", destination: AppearancesExample())
3031
Example("Geofencing User Location", note: "Set geofence on user initial location.", destination: GeofencingUserLocation())
3132
Example("Geofencing Playground", note: "Showcase isochrone API together with geofences.", destination: GeofencingPlayground())
3233
Example("Color Themes", note: "Showcase the Color Theme API", destination: ColorThemeExample())

0 commit comments

Comments
 (0)