Skip to content

Commit f9f2210

Browse files
pjleonard37Release SDK bot for Maps SDK team
andauthored
Update examples and add new interactions example (#2450)
Co-authored-by: Release SDK bot for Maps SDK team <[email protected]>
1 parent 1f5a303 commit f9f2210

File tree

6 files changed

+459
-162
lines changed

6 files changed

+459
-162
lines changed

Examples.xcodeproj/project.pbxproj

Lines changed: 126 additions & 102 deletions
Large diffs are not rendered by default.

Sources/Examples/All Examples/StandardStyleExample.swift

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import UIKit
2-
import MapboxMaps
2+
@_spi(Experimental) import MapboxMaps
33

44
final class StandardStyleExample: UIViewController, ExampleProtocol {
55
private var mapView: MapView!
66
private var cancelables = Set<AnyCancelable>()
77
private var lightPreset = StandardLightPreset.night
88
private var labelsSetting = true
99
private var showRealEstate = true
10+
private var selectedPriceLabel: FeaturesetFeature?
1011

1112
private var mapStyle: MapStyle {
1213
.standard(
@@ -30,7 +31,7 @@ final class StandardStyleExample: UIViewController, ExampleProtocol {
3031

3132
// When the style has finished loading add a line layer representing the border between New York and New Jersey
3233
mapView.mapboxMap.onStyleLoaded.observe { [weak self] _ in
33-
guard let self else { return }
34+
guard let self = self else { return }
3435

3536
// Create and apply basic styling to the line layer, assign the layer to the "bottom" slot
3637
var layer = LineLayer(id: "line-layer", source: "line-layer")
@@ -63,6 +64,33 @@ final class StandardStyleExample: UIViewController, ExampleProtocol {
6364
finish()
6465
}.store(in: &cancelables)
6566

67+
/// The contents of the imported style are private, meaning all the implementation details such as layers and sources are not accessible at runtime.
68+
/// However the style defines a "hotels-price" featureset that represents a portion of features available for interaction.
69+
/// Using the Interactions API you can add interactions to featuresets.
70+
/// See `fragment-realestate-NY.json` for more information.
71+
mapView.mapboxMap.addInteraction(TapInteraction(.featureset("hotels-price", importId: "real-estate-fragment")) { [weak self] priceLabel, _ in
72+
guard let self = self else { return false }
73+
/// Select a price label when it's clicked
74+
self.selectedPriceLabel = priceLabel
75+
76+
/// When there's a selected price label, we use it to set a feature state.
77+
/// The `hidden` state is implemented in `fragment-realestate-NY.json` and hides the label and icon.
78+
self.mapView.mapboxMap.setFeatureState(priceLabel, state: ["hidden": true])
79+
80+
self.updateViewAnnotation()
81+
return true
82+
})
83+
84+
/// An interaction without specified featureset handles all corresponding events that haven't been handled by other interactions.
85+
mapView.mapboxMap.addInteraction(TapInteraction { [weak self] _ in
86+
guard let self = self else { return false }
87+
/// When the user taps the map outside of the price labels, deselect the latest selected label.
88+
self.selectedPriceLabel = nil
89+
self.mapView.mapboxMap.resetFeatureStates(featureset: .featureset("hotels-price", importId: "real-estate-fragment"), callback: nil)
90+
self.updateViewAnnotation()
91+
return true
92+
})
93+
6694
// Add buttons to control the light presets and labels
6795
let lightButton = changeLightButton()
6896
let labelsButton = changeLabelsButton()
@@ -108,14 +136,67 @@ final class StandardStyleExample: UIViewController, ExampleProtocol {
108136
private func toggleRealEstate(isOn: Bool) {
109137
do {
110138
if isOn {
111-
try mapView.mapboxMap.addStyleImport(withId: "real-estate", uri: StyleURI(url: styleURL)!)
139+
try mapView.mapboxMap.addStyleImport(withId: "real-estate-fragment", uri: StyleURI(url: styleURL)!)
112140
} else {
113-
try mapView.mapboxMap.removeStyleImport(withId: "real-estate")
141+
try mapView.mapboxMap.removeStyleImport(withId: "real-estate-fragment")
114142
}
115143
} catch {
116144
print(error)
117145
}
118146
}
147+
148+
private func updateViewAnnotation() {
149+
mapView.viewAnnotations.removeAll()
150+
151+
if let selectedPriceLabel = selectedPriceLabel, let coordinate = selectedPriceLabel.geometry.point?.coordinates {
152+
let calloutView = createCalloutView(for: selectedPriceLabel)
153+
let annotation = ViewAnnotation(coordinate: coordinate, view: calloutView)
154+
annotation.variableAnchors = [.init(anchor: .bottom)]
155+
mapView.viewAnnotations.add(annotation)
156+
}
157+
}
158+
159+
private func createCalloutView(for feature: FeaturesetFeature) -> UIView {
160+
let calloutView = UIView()
161+
calloutView.backgroundColor = .white
162+
calloutView.layer.cornerRadius = 8
163+
calloutView.layer.shadowColor = UIColor.black.cgColor
164+
calloutView.layer.shadowOpacity = 0.2
165+
calloutView.layer.shadowOffset = CGSize(width: 0, height: 2)
166+
calloutView.layer.shadowRadius = 4
167+
168+
let nameLabel = UILabel()
169+
nameLabel.text = feature.name ?? ""
170+
nameLabel.font = UIFont.boldSystemFont(ofSize: 16)
171+
nameLabel.translatesAutoresizingMaskIntoConstraints = false
172+
173+
let priceLabel = UILabel()
174+
priceLabel.text = feature.price ?? ""
175+
priceLabel.font = UIFont.systemFont(ofSize: 14)
176+
priceLabel.textColor = .gray
177+
priceLabel.translatesAutoresizingMaskIntoConstraints = false
178+
179+
calloutView.addSubview(nameLabel)
180+
calloutView.addSubview(priceLabel)
181+
182+
NSLayoutConstraint.activate([
183+
nameLabel.topAnchor.constraint(equalTo: calloutView.topAnchor, constant: 8),
184+
nameLabel.leadingAnchor.constraint(equalTo: calloutView.leadingAnchor, constant: 8),
185+
nameLabel.trailingAnchor.constraint(equalTo: calloutView.trailingAnchor, constant: -8),
186+
187+
priceLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4),
188+
priceLabel.leadingAnchor.constraint(equalTo: calloutView.leadingAnchor, constant: 8),
189+
priceLabel.trailingAnchor.constraint(equalTo: calloutView.trailingAnchor, constant: -8),
190+
priceLabel.bottomAnchor.constraint(equalTo: calloutView.bottomAnchor, constant: -8)
191+
])
192+
193+
return calloutView
194+
}
195+
}
196+
197+
private extension FeaturesetFeature {
198+
var price: String? { properties["price"]??.number.map { "$ \($0)" } }
199+
var name: String? { properties["name"]??.string }
119200
}
120201

121202
private let styleURL = Bundle.main.url(forResource: "fragment-realestate-NY", withExtension: "json")!
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import UIKit
2+
@_spi(Experimental) import MapboxMaps
3+
4+
final class StandardStyleInteractionsExample: UIViewController, ExampleProtocol {
5+
private var mapView: MapView!
6+
private var cancelables = Set<AnyCancelable>()
7+
var lightPreset = StandardLightPreset.day
8+
var theme = StandardTheme.default
9+
var buildingSelectColor = StyleColor("hsl(214, 94%, 59%)") // default color
10+
11+
override func viewDidLoad() {
12+
super.viewDidLoad()
13+
14+
let cameraCenter = CLLocationCoordinate2D(latitude: 60.1718, longitude: 24.9453)
15+
let options = MapInitOptions(cameraOptions: CameraOptions(center: cameraCenter, zoom: 16.35, bearing: 49.92, pitch: 40))
16+
mapView = MapView(frame: view.bounds, mapInitOptions: options)
17+
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
18+
19+
/// DON'T USE Standard Experimental style in production, it will break over time.
20+
/// Currently this feature is in preview.
21+
mapView.mapboxMap.mapStyle = .standardExperimental()
22+
23+
view.addSubview(mapView)
24+
mapView.mapboxMap.onStyleLoaded.observe { [weak self] _ in
25+
guard let self = self else { return }
26+
self.setupInteractions()
27+
finish()
28+
}.store(in: &cancelables)
29+
30+
// Add UI elements for debug panel
31+
setupDebugPanel()
32+
}
33+
34+
private func setupInteractions() {
35+
/// When a POI feature in the Standard POI featureset is tapped replace it with a ViewAnnotation
36+
mapView.mapboxMap.addInteraction(TapInteraction(.standardPoi) { [weak self] poi, _ in
37+
guard let self = self else { return false }
38+
self.addViewAnnotation(for: poi)
39+
self.mapView.mapboxMap.setFeatureState(poi, state: .init(hide: true))
40+
return true /// Returning true stops propagation to features below or the map itself.
41+
})
42+
43+
/// When a building in the Standard Buildings featureset is tapped, set that building as selected to color it.
44+
mapView.mapboxMap.addInteraction(TapInteraction(.standardBuildings) { [weak self] building, _ in
45+
guard let self = self else { return false }
46+
self.mapView.mapboxMap.setFeatureState(building, state: .init(select: true))
47+
return true
48+
})
49+
50+
/// When a place label in the Standard Place Labels featureset is tapped, set that place label as selected.
51+
mapView.mapboxMap.addInteraction(TapInteraction(.standardPlaceLabels) { [weak self] placeLabel, _ in
52+
guard let self = self else { return false }
53+
self.mapView.mapboxMap.setFeatureState(placeLabel, state: .init(select: true))
54+
return true
55+
})
56+
57+
/// When the map is long-pressed, reset all selections
58+
mapView.mapboxMap.addInteraction(LongPressInteraction { [weak self] _ in
59+
guard let self = self else { return false }
60+
self.mapView.mapboxMap.resetFeatureStates(featureset: .standardBuildings, callback: nil)
61+
self.mapView.mapboxMap.resetFeatureStates(featureset: .standardPoi, callback: nil)
62+
self.mapView.mapboxMap.resetFeatureStates(featureset: .standardPlaceLabels, callback: nil)
63+
self.mapView.viewAnnotations.removeAll()
64+
return true
65+
})
66+
}
67+
68+
private func addViewAnnotation(for poi: StandardPoiFeature) {
69+
let view = UIImageView(image: UIImage(named: "intermediate-pin"))
70+
view.contentMode = .scaleAspectFit
71+
let annotation = ViewAnnotation(coordinate: poi.coordinate, view: view)
72+
annotation.variableAnchors = [.init(anchor: .bottom, offsetY: 12)]
73+
mapView.viewAnnotations.add(annotation)
74+
}
75+
76+
private func setupDebugPanel() {
77+
let debugPanel = UIView()
78+
debugPanel.translatesAutoresizingMaskIntoConstraints = false
79+
debugPanel.backgroundColor = .white
80+
debugPanel.layer.cornerRadius = 10
81+
debugPanel.layer.shadowColor = UIColor.black.cgColor
82+
debugPanel.layer.shadowOpacity = 0.2
83+
debugPanel.layer.shadowOffset = CGSize(width: 0, height: 2)
84+
debugPanel.layer.shadowRadius = 4
85+
view.addSubview(debugPanel)
86+
87+
let buildingSelectLabel = UILabel()
88+
buildingSelectLabel.text = "Building Select"
89+
buildingSelectLabel.translatesAutoresizingMaskIntoConstraints = false
90+
debugPanel.addSubview(buildingSelectLabel)
91+
92+
let buildingSelectControl = UISegmentedControl(items: ["Default", "Yellow", "Red"])
93+
buildingSelectControl.selectedSegmentIndex = 0
94+
buildingSelectControl.addTarget(self, action: #selector(buildingSelectColorChanged(_:)), for: .valueChanged)
95+
buildingSelectControl.translatesAutoresizingMaskIntoConstraints = false
96+
debugPanel.addSubview(buildingSelectControl)
97+
98+
let lightLabel = UILabel()
99+
lightLabel.text = "Light"
100+
lightLabel.translatesAutoresizingMaskIntoConstraints = false
101+
debugPanel.addSubview(lightLabel)
102+
103+
let lightControl = UISegmentedControl(items: ["Dawn", "Day", "Dusk", "Night"])
104+
lightControl.selectedSegmentIndex = 1
105+
lightControl.addTarget(self, action: #selector(lightPresetChanged(_:)), for: .valueChanged)
106+
lightControl.translatesAutoresizingMaskIntoConstraints = false
107+
debugPanel.addSubview(lightControl)
108+
109+
let themeLabel = UILabel()
110+
themeLabel.text = "Theme"
111+
themeLabel.translatesAutoresizingMaskIntoConstraints = false
112+
debugPanel.addSubview(themeLabel)
113+
114+
let themeControl = UISegmentedControl(items: ["Default", "Faded", "Monochrome"])
115+
themeControl.selectedSegmentIndex = 0
116+
themeControl.addTarget(self, action: #selector(themeChanged(_:)), for: .valueChanged)
117+
themeControl.translatesAutoresizingMaskIntoConstraints = false
118+
debugPanel.addSubview(themeControl)
119+
120+
NSLayoutConstraint.activate([
121+
debugPanel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
122+
debugPanel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
123+
debugPanel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10),
124+
125+
buildingSelectLabel.topAnchor.constraint(equalTo: debugPanel.topAnchor, constant: 10),
126+
buildingSelectLabel.leadingAnchor.constraint(equalTo: debugPanel.leadingAnchor, constant: 10),
127+
128+
buildingSelectControl.topAnchor.constraint(equalTo: buildingSelectLabel.bottomAnchor, constant: 5),
129+
buildingSelectControl.leadingAnchor.constraint(equalTo: debugPanel.leadingAnchor, constant: 10),
130+
buildingSelectControl.trailingAnchor.constraint(equalTo: debugPanel.trailingAnchor, constant: -10),
131+
132+
lightLabel.topAnchor.constraint(equalTo: buildingSelectControl.bottomAnchor, constant: 10),
133+
lightLabel.leadingAnchor.constraint(equalTo: debugPanel.leadingAnchor, constant: 10),
134+
135+
lightControl.topAnchor.constraint(equalTo: lightLabel.bottomAnchor, constant: 5),
136+
lightControl.leadingAnchor.constraint(equalTo: debugPanel.leadingAnchor, constant: 10),
137+
lightControl.trailingAnchor.constraint(equalTo: debugPanel.trailingAnchor, constant: -10),
138+
139+
themeLabel.topAnchor.constraint(equalTo: lightControl.bottomAnchor, constant: 10),
140+
themeLabel.leadingAnchor.constraint(equalTo: debugPanel.leadingAnchor, constant: 10),
141+
142+
themeControl.topAnchor.constraint(equalTo: themeLabel.bottomAnchor, constant: 5),
143+
themeControl.leadingAnchor.constraint(equalTo: debugPanel.leadingAnchor, constant: 10),
144+
themeControl.trailingAnchor.constraint(equalTo: debugPanel.trailingAnchor, constant: -10),
145+
146+
themeControl.bottomAnchor.constraint(equalTo: debugPanel.bottomAnchor, constant: -10)
147+
])
148+
}
149+
150+
@objc private func buildingSelectColorChanged(_ sender: UISegmentedControl) {
151+
switch sender.selectedSegmentIndex {
152+
case 0:
153+
buildingSelectColor = StyleColor("hsl(214, 94%, 59%)")
154+
case 1:
155+
buildingSelectColor = StyleColor("yellow")
156+
case 2:
157+
buildingSelectColor = StyleColor(.red)
158+
default:
159+
break
160+
}
161+
mapView.mapboxMap.mapStyle = .standardExperimental(theme: theme, lightPreset: lightPreset, buildingSelectColor: buildingSelectColor)
162+
}
163+
164+
@objc private func lightPresetChanged(_ sender: UISegmentedControl) {
165+
switch sender.selectedSegmentIndex {
166+
case 0:
167+
lightPreset = .dawn
168+
case 1:
169+
lightPreset = .day
170+
case 2:
171+
lightPreset = .dusk
172+
case 3:
173+
lightPreset = .night
174+
default:
175+
break
176+
}
177+
mapView.mapboxMap.mapStyle = .standardExperimental(theme: theme, lightPreset: lightPreset, buildingSelectColor: buildingSelectColor)
178+
}
179+
180+
@objc private func themeChanged(_ sender: UISegmentedControl) {
181+
switch sender.selectedSegmentIndex {
182+
case 0:
183+
theme = .default
184+
case 1:
185+
theme = .faded
186+
case 2:
187+
theme = .monochrome
188+
default:
189+
break
190+
}
191+
mapView.mapboxMap.mapStyle = .standardExperimental(theme: theme, lightPreset: lightPreset, buildingSelectColor: buildingSelectColor)
192+
}
193+
}

Sources/Examples/Models/Examples.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,9 @@ struct Examples {
271271

272272
// Examples that show use cases related to user interaction with the map.
273273
static let userInteractionExamples: [Example] = .init {
274+
Example(title: "Standard Style Interactions",
275+
description: "Showcase of Standard style interactions.",
276+
type: StandardStyleInteractionsExample.self)
274277
Example(title: "Find features at a point",
275278
description: "Query the map for rendered features belonging to a specific layer.",
276279
type: FeaturesAtPointExample.self)

0 commit comments

Comments
 (0)