Skip to content

Commit f3ea116

Browse files
mapbox-github-ci-writer-4[bot]pjleonard37
authored andcommitted
[Backport release/v0.19] Update examples to show scale range layer properties (#10307)
Backport 0fcfdcc9e94c885329c84adeaf678917d1295ba9 from #10276. cc @mapbox/maps-ios cc @mapbox/maps-android Co-authored-by: Patrick Leonard <pjleonard37@users.noreply.github.com> GitOrigin-RevId: 6526544a7cf2e07f30bb723e2732119d375656a3
1 parent d22e1e2 commit f3ea116

3 files changed

Lines changed: 281 additions & 82 deletions

File tree

Sources/Examples/SwiftUI Examples/AccessibilityScaleExample.swift

Lines changed: 255 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,130 +3,306 @@ import SwiftUI
33

44
/// Example demonstrating accessibility scaling for map symbols.
55
///
6-
/// This example shows three modes:
7-
/// - Fixed: Manual scale control with slider (no system listeners)
8-
/// - System: Automatic scaling based on system text size (opt-in)
9-
/// - Custom: System scaling with custom mapping that dampens large accessibility scales
6+
/// This example shows:
7+
/// - **Scale Factor**: Controls ALL symbols in the map (system labels + custom annotations)
8+
/// - Fixed: Manual control with slider
9+
/// - System: Automatic scaling based on system text size
10+
/// - Custom: System scaling with custom mapping
11+
/// - **Icon/Text Size Scale Range**: Only affects the custom annotations (shown in blue)
1012
struct AccessibilityScaleExample: View {
13+
struct Location: Identifiable {
14+
let id = UUID()
15+
let name: String
16+
let coordinate: CLLocationCoordinate2D
17+
}
18+
1119
enum ScaleMode: String, CaseIterable {
1220
case fixed = "Fixed"
1321
case system = "System"
1422
case custom = "Custom"
1523
}
1624

17-
@State private var selectedMode: ScaleMode = .fixed
18-
@State private var customScaleValue: Float = 1.0
25+
// Sample locations
26+
private static let locations = [
27+
Location(name: "Harlem", coordinate: CLLocationCoordinate2D(latitude: 40.8116, longitude: -73.9465)),
28+
Location(name: "Upper West Side", coordinate: CLLocationCoordinate2D(latitude: 40.7870, longitude: -73.9754)),
29+
Location(name: "Midtown", coordinate: CLLocationCoordinate2D(latitude: 40.7549, longitude: -73.9840))
30+
]
31+
32+
@State private var scaleMode: ScaleMode = .fixed
33+
@State private var scaleFactor: Float = 1.0
34+
@State private var iconSizeMin: Double = 0.8
35+
@State private var iconSizeMax: Double = 2.0
36+
@State private var textSizeMin: Double = 0.8
37+
@State private var textSizeMax: Double = 2.0
38+
@State private var showInfoSheet = false
39+
40+
// Debounced range values to avoid flooding the map renderer on every slider tick
41+
@State private var debouncedIconSizeMin: Double = 0.8
42+
@State private var debouncedIconSizeMax: Double = 2.0
43+
@State private var debouncedTextSizeMin: Double = 0.8
44+
@State private var debouncedTextSizeMax: Double = 2.0
1945

2046
var body: some View {
2147
MapReader { mapProxy in
22-
Map(initialViewport: .camera(center: .init(latitude: 40.7128, longitude: -74.0060), zoom: 12))
23-
.mapStyle(.standard)
24-
.onMapLoaded { _ in
25-
applySymbolScaleBehavior(to: mapProxy.map)
48+
Map(initialViewport: .camera(center: CLLocationCoordinate2D(latitude: 40.7489, longitude: -73.9680), zoom: 11.5)) {
49+
// Add custom point annotations with distinct styling
50+
PointAnnotationGroup(Self.locations) { location in
51+
PointAnnotation(coordinate: location.coordinate)
52+
.image(named: "intermediate-pin")
53+
.iconAnchor(.bottom)
54+
.textField(location.name)
55+
.textAnchor(.top)
56+
.textOffset(x: 0, y: 0.3)
57+
.textSize(16)
58+
.textColor(StyleColor(.systemBlue)) // Blue to distinguish from map labels
59+
.textHaloColor(.white)
60+
.textHaloWidth(2)
2661
}
27-
.onChange(of: selectedMode) { _ in
28-
applySymbolScaleBehavior(to: mapProxy.map)
62+
.iconSizeScaleRange(min: debouncedIconSizeMin, max: debouncedIconSizeMax)
63+
.textSizeScaleRange(min: debouncedTextSizeMin, max: debouncedTextSizeMax)
64+
}
65+
.mapStyle(.standard)
66+
.onMapLoaded { _ in
67+
applyScaleBehavior(to: mapProxy.map)
68+
}
69+
.onChange(of: scaleMode) { _ in
70+
applyScaleBehavior(to: mapProxy.map)
71+
}
72+
.onChange(of: scaleFactor) { _ in
73+
if scaleMode == .fixed {
74+
applyScaleBehavior(to: mapProxy.map)
2975
}
30-
.onChange(of: customScaleValue) { _ in
31-
if selectedMode == .fixed {
32-
applySymbolScaleBehavior(to: mapProxy.map)
76+
}
77+
}
78+
.ignoresSafeArea()
79+
.task(id: iconSizeMin) {
80+
try? await Task.sleep(nanoseconds: 150_000_000)
81+
debouncedIconSizeMin = iconSizeMin
82+
}
83+
.task(id: iconSizeMax) {
84+
try? await Task.sleep(nanoseconds: 150_000_000)
85+
debouncedIconSizeMax = iconSizeMax
86+
}
87+
.task(id: textSizeMin) {
88+
try? await Task.sleep(nanoseconds: 150_000_000)
89+
debouncedTextSizeMin = textSizeMin
90+
}
91+
.task(id: textSizeMax) {
92+
try? await Task.sleep(nanoseconds: 150_000_000)
93+
debouncedTextSizeMax = textSizeMax
94+
}
95+
.overlay(alignment: .bottom, content: {
96+
VStack(spacing: 8) {
97+
// Info button above settings panel
98+
HStack {
99+
Spacer()
100+
Button {
101+
showInfoSheet = true
102+
} label: {
103+
Image(systemName: "questionmark.circle")
104+
.font(.system(size: 22))
105+
.padding(8)
106+
.background(.regularMaterial)
107+
.clipShape(Circle())
33108
}
34109
}
35-
}
36-
.ignoresSafeArea(edges: .bottom)
37-
.overlay(alignment: .bottom) {
38-
VStack(spacing: 12) {
39-
// Conditional content (above buttons)
40-
Group {
41-
if selectedMode == .fixed {
42-
VStack(spacing: 8) {
110+
.padding(.horizontal)
111+
112+
// Settings Panel
113+
VStack(alignment: .leading, spacing: 12) {
114+
115+
// Scale Factor Section
116+
VStack(alignment: .leading, spacing: 8) {
117+
Text("Scale Factor (all symbols)")
118+
.font(.caption.bold())
119+
120+
Picker("Mode", selection: $scaleMode) {
121+
ForEach(ScaleMode.allCases, id: \.self) { mode in
122+
Text(mode.rawValue).tag(mode)
123+
}
124+
}
125+
.pickerStyle(.segmented)
126+
127+
if scaleMode == .fixed {
43128
HStack {
44-
Text("Scale factor:")
45-
.font(.caption)
129+
Text("Scale:")
130+
.font(.caption2)
46131
Spacer()
47-
Text(String(format: "%.2fx", customScaleValue))
48-
.font(.caption.monospacedDigit())
132+
Text(String(format: "%.1f", scaleFactor))
133+
.font(.caption2.monospacedDigit())
49134
}
50-
Slider(value: $customScaleValue, in: 0.8...2.0, step: 0.1)
51-
Text("Manual scale control (no system listeners)")
135+
Slider(value: $scaleFactor, in: 0.5...3.0, step: 0.1)
136+
} else if scaleMode == .system {
137+
Text("Automatic scaling from Settings → Accessibility")
138+
.font(.caption2)
139+
.foregroundStyle(.secondary)
140+
} else {
141+
Text("Custom mapping: dampens large accessibility scales")
52142
.font(.caption2)
53143
.foregroundStyle(.secondary)
54-
.multilineTextAlignment(.center)
55144
}
56-
.padding(.horizontal, 16)
57-
.padding(.vertical, 8)
58-
.background(.ultraThinMaterial)
59-
.cornerRadius(8)
60-
.frame(maxWidth: 340)
61-
} else if selectedMode == .system {
62-
Text("Automatic scaling with default mapping.\nChange system text size in Settings to see effect.")
63-
.font(.caption2)
64-
.foregroundStyle(.secondary)
65-
.multilineTextAlignment(.center)
66-
.padding(.horizontal, 16)
67-
.padding(.vertical, 8)
68-
.background(.ultraThinMaterial)
69-
.cornerRadius(8)
70-
.frame(maxWidth: 340)
71-
} else if selectedMode == .custom {
72-
Text("Custom mapping: boosts small scales +10%, dampens large scales.\nChange system text size in Settings to see effect.")
73-
.font(.caption2)
74-
.foregroundStyle(.secondary)
75-
.multilineTextAlignment(.center)
76-
.padding(.horizontal, 16)
77-
.padding(.vertical, 8)
78-
.background(.ultraThinMaterial)
79-
.cornerRadius(8)
80-
.frame(maxWidth: 340)
81145
}
82-
}
83-
.frame(height: selectedMode == .fixed ? 80 : 60)
84-
.opacity(1)
85146

86-
// Mode picker
87-
Picker("Scale Mode", selection: $selectedMode) {
88-
ForEach(ScaleMode.allCases, id: \.self) { mode in
89-
Text(mode.rawValue).tag(mode)
147+
Divider()
148+
149+
// Scale Ranges Section
150+
VStack(alignment: .leading, spacing: 8) {
151+
Text("Scale Ranges (custom annotations)")
152+
.font(.caption.bold())
153+
154+
// Icon Size Scale Range
155+
VStack(alignment: .leading, spacing: 4) {
156+
Text("Icon Size Range")
157+
.font(.caption2)
158+
HStack(spacing: 8) {
159+
VStack(alignment: .leading, spacing: 2) {
160+
Text("Min: \(String(format: "%.1f", iconSizeMin))")
161+
.font(.system(size: 10))
162+
Slider(value: $iconSizeMin, in: 0.1...5.0, step: 0.1)
163+
.onChange(of: iconSizeMin) { _ in
164+
if iconSizeMin > iconSizeMax { iconSizeMax = iconSizeMin }
165+
}
166+
}
167+
VStack(alignment: .leading, spacing: 2) {
168+
Text("Max: \(String(format: "%.1f", iconSizeMax))")
169+
.font(.system(size: 10))
170+
Slider(value: $iconSizeMax, in: 0.1...5.0, step: 0.1)
171+
.onChange(of: iconSizeMax) { _ in
172+
if iconSizeMax < iconSizeMin { iconSizeMin = iconSizeMax }
173+
}
174+
}
175+
}
176+
}
177+
178+
// Text Size Scale Range
179+
VStack(alignment: .leading, spacing: 4) {
180+
Text("Text Size Range")
181+
.font(.caption2)
182+
HStack(spacing: 8) {
183+
VStack(alignment: .leading, spacing: 2) {
184+
Text("Min: \(String(format: "%.1f", textSizeMin))")
185+
.font(.system(size: 10))
186+
Slider(value: $textSizeMin, in: 0.1...5.0, step: 0.1)
187+
.onChange(of: textSizeMin) { _ in
188+
if textSizeMin > textSizeMax { textSizeMax = textSizeMin }
189+
}
190+
}
191+
VStack(alignment: .leading, spacing: 2) {
192+
Text("Max: \(String(format: "%.1f", textSizeMax))")
193+
.font(.system(size: 10))
194+
Slider(value: $textSizeMax, in: 0.1...5.0, step: 0.1)
195+
.onChange(of: textSizeMax) { _ in
196+
if textSizeMax < textSizeMin { textSizeMin = textSizeMax }
197+
}
198+
}
199+
}
200+
}
90201
}
91202
}
92-
.pickerStyle(.segmented)
93-
.frame(maxWidth: 340)
203+
.floating()
94204
}
95-
.padding(.bottom, 70)
96-
.animation(.easeInOut(duration: 0.2), value: selectedMode)
205+
.padding(.bottom, 30)
206+
})
207+
.sheet(isPresented: $showInfoSheet) {
208+
AccessibilityScaleInfoView()
209+
.defaultDetents()
97210
}
98211
}
99212

100-
private func applySymbolScaleBehavior(to mapboxMap: MapboxMap?) {
213+
private func applyScaleBehavior(to mapboxMap: MapboxMap?) {
101214
guard let mapboxMap = mapboxMap else { return }
102-
let behavior = scaleBehaviorForMode()
103-
mapboxMap.symbolScaleBehavior = behavior
104-
}
105215

106-
private func scaleBehaviorForMode() -> SymbolScaleBehavior {
107-
switch selectedMode {
216+
switch scaleMode {
108217
case .fixed:
109-
// Fixed scale: manual control via slider
110-
return .fixed(scaleFactor: Double(customScaleValue))
218+
mapboxMap.symbolScaleBehavior = .fixed(scaleFactor: Double(scaleFactor))
111219
case .system:
112-
// System: automatic scaling with default mapping
113-
return .system
220+
mapboxMap.symbolScaleBehavior = .system
114221
case .custom:
115-
// Custom: increases low scale values proportionally, dampens large accessibility scales
116-
return .system(mapping: { systemScale in
222+
// Custom mapping: dampens large accessibility scales
223+
mapboxMap.symbolScaleBehavior = .system(mapping: { systemScale in
117224
switch systemScale {
118225
case ..<1.0:
119226
return systemScale * 1.1 // Boost small scales by 10%
120227
case 1.0...1.3:
121228
return systemScale // Keep medium scales unchanged
122229
default:
123-
return 1.3 + (systemScale - 1.3) * 0.4 // Dampen large scales (max ~1.6x)
230+
return 1.3 + (systemScale - 1.3) * 0.4 // Dampen large scales
124231
}
125232
})
126233
}
127234
}
128235
}
129236

237+
private struct AccessibilityScaleInfoView: View {
238+
var body: some View {
239+
ScrollView {
240+
VStack(alignment: .leading, spacing: 16) {
241+
Text("Accessibility Scale Example")
242+
.font(.headline)
243+
244+
Text("This example demonstrates how to control symbol scaling for accessibility using the Maps SDK.")
245+
.font(.subheadline)
246+
247+
VStack(alignment: .leading, spacing: 12) {
248+
Group {
249+
Text("Scale Factor")
250+
.font(.subheadline.bold())
251+
Text("Adjusts the global scale factor for all symbol layers on the map using the `symbolScaleBehavior` property. This affects both system map labels and custom annotations.")
252+
.font(.footnote)
253+
254+
(Text("") + Text("Fixed").bold() + Text(": Manual control with a slider"))
255+
.font(.footnote)
256+
(Text("") + Text("System").bold() + Text(": Automatically scales based on the device's accessibility text size setting (Settings → Accessibility → Display & Text Size → Larger Accessibility Sizes)"))
257+
.font(.footnote)
258+
(Text("") + Text("Custom").bold() + Text(": Uses a custom mapping function to modify system scale values (e.g., dampen large scales)"))
259+
.font(.footnote)
260+
}
261+
262+
Divider()
263+
264+
Group {
265+
Text("Icon Size Scale Range")
266+
.font(.subheadline.bold())
267+
Text("Sets the minimum and maximum scaling limits for icons using the `icon-size-scale-range` layout property. This only affects the custom blue annotations in this example.")
268+
.font(.footnote)
269+
270+
Text("Example: Setting `[1.0, 1.0]` prevents icons from scaling regardless of the scale factor value.")
271+
.font(.footnote)
272+
.italic()
273+
}
274+
275+
Divider()
276+
277+
Group {
278+
Text("Text Size Scale Range")
279+
.font(.subheadline.bold())
280+
Text("Sets the minimum and maximum scaling limits for text using the `text-size-scale-range` layout property. This only affects the custom blue text labels in this example.")
281+
.font(.footnote)
282+
283+
Text("Example: Setting `[0.5, 3.0]` allows text to scale from half size to triple size based on the scale factor.")
284+
.font(.footnote)
285+
.italic()
286+
}
287+
}
288+
289+
Divider()
290+
291+
Text("Try experimenting with different combinations:")
292+
.font(.subheadline.bold())
293+
294+
Text("• Set scale factor to 2.0 with icon range [1.0, 1.0] to see text scale while icons stay the same size")
295+
.font(.footnote)
296+
Text("• Switch to System mode and change your device's text size in Settings to see automatic scaling")
297+
.font(.footnote)
298+
Text("• Use Custom mode to see how mapping functions can dampen extreme scale values")
299+
.font(.footnote)
300+
}
301+
.padding()
302+
}
303+
}
304+
}
305+
130306
#Preview {
131307
AccessibilityScaleExample()
132308
}

Sources/MapboxMaps/Foundation/MapboxMap.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,11 @@ public final class MapboxMap: StyleManager {
11991199
private func setScaleFactor(_ scaleFactor: Float) {
12001200
__map.setScaleFactorForScaleFactor(scaleFactor)
12011201
}
1202+
1203+
// Internal accessor to retrieve the current scale factor from the native map.
1204+
internal func getScaleFactor() -> Float {
1205+
__map.getScaleFactor()
1206+
}
12021207
}
12031208

12041209
extension MapboxMap: MapboxMapProtocol {}

0 commit comments

Comments
 (0)