Skip to content

Commit d456f00

Browse files
persidskiygithub-actions[bot]
authored andcommitted
Update viewport states when map size is changed (#8503)
GitOrigin-RevId: 17f135863ae737e239a4d372b0576e8cad063549
1 parent 35ee93f commit d456f00

File tree

14 files changed

+346
-210
lines changed

14 files changed

+346
-210
lines changed

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ disabled_rules:
1818
- trailing_comma
1919
- type_name
2020
- type_body_length
21+
- large_tuple
2122
custom_rules:
2223
trojan_source:
2324
regex: "[\u202A\u202B\u202D\u202E\u2066\u2067\u2068\u202C\u2069]"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Mapbox welcomes participation and contributions from everyone.
1717
### Features ✨ and improvements 🏁
1818
* Promote Geofencing APIs to stable. Remove `@_spi(Experimental)` from Geofencing APIs.
1919
* Fix an issue where the location indicator could show incorrect direction in landscape orientation.
20+
* Update viewport state when map size is changed.
2021

2122
## 11.17.0-rc.1 - 20 November, 2025
2223

Examples.xcodeproj/project.pbxproj

Lines changed: 144 additions & 137 deletions
Large diffs are not rendered by default.

Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift renamed to Sources/Examples/SwiftUI Examples/Testing Examples/SwiftUIRoot.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ struct SwiftUIExamples {
4040
Example("Map settings", note: "Showcase of the most possible map configurations.", destination: MapSettingsExample())
4141
Example("Interactions playground", note: "Interactions edge cases", destination: InteractionsPlayground())
4242
Example("Viewport Playground", note: "Showcase of the possible viewport states.", destination: ViewportPlayground())
43+
Example("Viewport In Fixed Frame", note: "Overview viewport in Map with fixed frame", destination: ViewportInFixedFrameExample())
4344
Example("Puck playground", note: "Display user location using puck.", destination: PuckPlayground())
4445
Example("Annotation Order", note: "Test the rendering order of annotations.", destination: AnnotationsOrderTestExample())
4546
Example("Snapshot Map", note: "Make a snapshot of the map.", destination: SnapshotMapExample())
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import SwiftUI
2+
import MapboxMaps
3+
4+
struct ViewportInFixedFrameExample: View {
5+
var body: some View {
6+
Map(
7+
initialViewport: .overview(
8+
geometry: Geometry.lineString(LineString(routeCoordinates)),
9+
geometryPadding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
10+
)
11+
) {
12+
PolylineAnnotation(lineString: LineString(routeCoordinates))
13+
.lineBorderColor(UIColor(red: 62, green: 66, blue: 181, alpha: 0))
14+
.lineBorderWidth(8)
15+
.lineColor(.blue)
16+
.lineWidth(3)
17+
}
18+
.frame(width: 300, height: 300)
19+
}
20+
21+
private let routeCoordinates: [CLLocationCoordinate2D] = [
22+
.init(latitude: 61.493343399275375, longitude: 21.79401104323395),
23+
.init(latitude: 61.369485877583685, longitude: 21.6497937188623),
24+
.init(latitude: 61.20121331932606, longitude: 21.723373986398713),
25+
.init(latitude: 61.15722935505474, longitude: 21.579156662028026),
26+
.init(latitude: 61.1174491345069, longitude: 21.517349237298163),
27+
.init(latitude: 61.03916342463762, longitude: 21.532065290804866),
28+
.init(latitude: 60.922086661997184, longitude: 21.611531979743546),
29+
.init(latitude: 60.82754056454698, longitude: 21.82049993954618),
30+
.init(latitude: 60.662131116075585, longitude: 22.02063826724438),
31+
.init(latitude: 60.543665503992116, longitude: 22.129538402396946),
32+
.init(latitude: 60.461061119997595, longitude: 22.206061880634536),
33+
.init(latitude: 60.4538050749446, longitude: 22.270812516066485)
34+
]
35+
}

Sources/Examples/SwiftUI Examples/Testing Examples/ViewportPlayground.swift

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,61 @@
1-
import SwiftUI
21
import MapboxMaps
2+
import SwiftUI
33

44
struct ViewportPlayground: View {
55
@State var viewport: Viewport = .styleDefault
66
@State var mapStyle: MapStyle = .standard
77
@State var useSafeAreaAsPaddings: Bool = true
88
@State var additionalSafeArea: Bool = true
99
@State var settingsHeight = 0.0
10+
@State var collapsed = false
1011

1112
var body: some View {
12-
Map(viewport: $viewport) {
13-
Puck2D(bearing: .course)
14-
15-
ForEvery(parks.coordinates, id: \.latitude) { coord in
16-
MapViewAnnotation(coordinate: coord) {
17-
Image(systemName: "tree")
18-
.foregroundColor(.white)
19-
.padding(.all, 5)
20-
.background(
21-
Circle()
22-
.strokeBorder(.black, lineWidth: 0.5)
23-
.background(Circle().fill(Color(.systemGreen)))
24-
)
13+
ZStack {
14+
Map(viewport: $viewport) {
15+
Puck2D(bearing: .course)
16+
17+
ForEvery(parks.coordinates, id: \.latitude) { coord in
18+
MapViewAnnotation(coordinate: coord) {
19+
Image(systemName: "tree")
20+
.foregroundColor(.white)
21+
.padding(.all, 5)
22+
.background(
23+
Circle()
24+
.strokeBorder(.black, lineWidth: 0.5)
25+
.background(Circle().fill(Color(.systemGreen)))
26+
)
27+
}
28+
.allowOverlap(true)
2529
}
26-
.allowOverlap(true)
27-
}
2830

29-
PolygonAnnotation(id: "polygon", polygon: maineBoundaries)
30-
.fillColor(StyleColor(red: 0, green: 128, blue: 255, alpha: 0.5)!)
31-
.fillOutlineColor(StyleColor(.black))
32-
}
33-
.mapStyle(mapStyle)
34-
.debugOptions([.camera, .padding])
35-
.usesSafeAreaInsetsAsPadding(useSafeAreaAsPaddings)
36-
.additionalSafeAreaInsets(.bottom, additionalBottomSafeArea)
37-
.ignoresSafeArea()
38-
.overlay(alignment: .bottomLeading) {
39-
VStack(alignment: .leading, spacing: 0) {
40-
Text("Viewport sate: \(viewportShortDescription)")
41-
MiniToggle(title: "Use safe area as padding", isOn: $useSafeAreaAsPaddings)
42-
MiniToggle(title: "Use additional safe area", isOn: $additionalSafeArea)
31+
PolygonAnnotation(id: "polygon", polygon: maineBoundaries)
32+
.fillColor(StyleColor(red: 0, green: 128, blue: 255, alpha: 0.5)!)
33+
.fillOutlineColor(StyleColor(.black))
4334
}
44-
.font(.safeMonospaced)
45-
.floating()
46-
.onChangeOfSize { size in
47-
settingsHeight = size.height
35+
.mapStyle(mapStyle)
36+
.debugOptions([.camera, .padding])
37+
.usesSafeAreaInsetsAsPadding(useSafeAreaAsPaddings)
38+
.additionalSafeAreaInsets(.bottom, additionalBottomSafeArea)
39+
.frame(width: collapsed ? 400 : nil, height: collapsed ? 200 : nil)
40+
.ignoresSafeArea()
41+
.overlay(alignment: .trailing) {
42+
MapStyleSelectorButton(mapStyle: $mapStyle)
43+
}
44+
45+
VStack(alignment: .leading) {
46+
Spacer()
47+
VStack(alignment: .leading, spacing: 0) {
48+
Text("Viewport sate: \(viewportShortDescription)")
49+
MiniToggle(title: "Use safe area as padding", isOn: $useSafeAreaAsPaddings)
50+
MiniToggle(title: "Use additional safe area", isOn: $additionalSafeArea)
51+
MiniToggle(title: "Collapsed", isOn: $collapsed)
52+
}
53+
.font(.safeMonospaced)
54+
.floating()
55+
.onChangeOfSize { size in
56+
settingsHeight = size.height
57+
}
4858
}
49-
}
50-
.overlay(alignment: .trailing) {
51-
MapStyleSelectorButton(mapStyle: $mapStyle)
5259
}
5360
.toolbar {
5461
ToolbarItem {
@@ -58,7 +65,7 @@ struct ViewportPlayground: View {
5865
}
5966

6067
private var additionalBottomSafeArea: CGFloat {
61-
additionalSafeArea ? settingsHeight : 0
68+
(additionalSafeArea && !collapsed) ? settingsHeight : 0
6269
}
6370

6471
var viewportShortDescription: String {
@@ -86,8 +93,8 @@ struct ViewportPlayground: View {
8693
case .geometryCollection:
8794
geometryType = "geometryCollection"
8895
#if USING_TURF_WITH_LIBRARY_EVOLUTION
89-
@unknown default:
90-
geometryType = "unknownType"
96+
@unknown default:
97+
geometryType = "unknownType"
9198
#else
9299
#endif
93100
}
@@ -126,7 +133,9 @@ private struct ViewportMenu: View {
126133
viewport = .styleDefault
127134
}
128135
Button(".camera()") {
129-
viewport = .camera(center: CLLocationCoordinate2D(latitude: 41.8915, longitude: -87.6087), zoom: 16.52, bearing: 290, pitch: 68.5)
136+
viewport = .camera(
137+
center: CLLocationCoordinate2D(latitude: 41.8915, longitude: -87.6087), zoom: 16.52, bearing: 290,
138+
pitch: 68.5)
130139
}
131140
Button(".overview(.multiPoint())") {
132141
viewport = viewport(for: parks, coordinatePadding: 20)
@@ -152,7 +161,9 @@ private struct ViewportMenu: View {
152161
}
153162
Button("[easeIn] .camera()") {
154163
withViewportAnimation(.easeIn(duration: 1)) {
155-
viewport = .camera(center: CLLocationCoordinate2D(latitude: 41.8915, longitude: -87.6087), zoom: 16.52, bearing: 290, pitch: 68.5)
164+
viewport = .camera(
165+
center: CLLocationCoordinate2D(latitude: 41.8915, longitude: -87.6087), zoom: 16.52,
166+
bearing: 290, pitch: 68.5)
156167
}
157168
}
158169
Button("[fly] .overview(.multiPoint())") {
@@ -206,28 +217,30 @@ private let parks = MultiPoint([
206217
CLLocationCoordinate2D(latitude: 35.141689, longitude: -115.510399),
207218
])
208219

209-
private let maineBoundaries = Polygon([[
210-
CLLocationCoordinate2D(latitude: 45.13745, longitude: -67.13734),
211-
CLLocationCoordinate2D(latitude: 44.8097, longitude: -66.96466),
212-
CLLocationCoordinate2D(latitude: 44.3252, longitude: -68.03252),
213-
CLLocationCoordinate2D(latitude: 43.98, longitude: -69.06),
214-
CLLocationCoordinate2D(latitude: 43.68405, longitude: -70.11617),
215-
CLLocationCoordinate2D(latitude: 43.09008, longitude: -70.64573),
216-
CLLocationCoordinate2D(latitude: 43.08003, longitude: -70.75102),
217-
CLLocationCoordinate2D(latitude: 43.21973, longitude: -70.79761),
218-
CLLocationCoordinate2D(latitude: 43.36789, longitude: -70.98176),
219-
CLLocationCoordinate2D(latitude: 43.46633, longitude: -70.94416),
220-
CLLocationCoordinate2D(latitude: 45.30524, longitude: -71.08482),
221-
CLLocationCoordinate2D(latitude: 45.46022, longitude: -70.66002),
222-
CLLocationCoordinate2D(latitude: 45.91479, longitude: -70.30495),
223-
CLLocationCoordinate2D(latitude: 46.69317, longitude: -70.00014),
224-
CLLocationCoordinate2D(latitude: 47.44777, longitude: -69.23708),
225-
CLLocationCoordinate2D(latitude: 47.18479, longitude: -68.90478),
226-
CLLocationCoordinate2D(latitude: 47.35462, longitude: -68.2343),
227-
CLLocationCoordinate2D(latitude: 47.06624, longitude: -67.79035),
228-
CLLocationCoordinate2D(latitude: 45.70258, longitude: -67.79141),
229-
CLLocationCoordinate2D(latitude: 45.13745, longitude: -67.13734)
230-
]])
220+
private let maineBoundaries = Polygon([
221+
[
222+
CLLocationCoordinate2D(latitude: 45.13745, longitude: -67.13734),
223+
CLLocationCoordinate2D(latitude: 44.8097, longitude: -66.96466),
224+
CLLocationCoordinate2D(latitude: 44.3252, longitude: -68.03252),
225+
CLLocationCoordinate2D(latitude: 43.98, longitude: -69.06),
226+
CLLocationCoordinate2D(latitude: 43.68405, longitude: -70.11617),
227+
CLLocationCoordinate2D(latitude: 43.09008, longitude: -70.64573),
228+
CLLocationCoordinate2D(latitude: 43.08003, longitude: -70.75102),
229+
CLLocationCoordinate2D(latitude: 43.21973, longitude: -70.79761),
230+
CLLocationCoordinate2D(latitude: 43.36789, longitude: -70.98176),
231+
CLLocationCoordinate2D(latitude: 43.46633, longitude: -70.94416),
232+
CLLocationCoordinate2D(latitude: 45.30524, longitude: -71.08482),
233+
CLLocationCoordinate2D(latitude: 45.46022, longitude: -70.66002),
234+
CLLocationCoordinate2D(latitude: 45.91479, longitude: -70.30495),
235+
CLLocationCoordinate2D(latitude: 46.69317, longitude: -70.00014),
236+
CLLocationCoordinate2D(latitude: 47.44777, longitude: -69.23708),
237+
CLLocationCoordinate2D(latitude: 47.18479, longitude: -68.90478),
238+
CLLocationCoordinate2D(latitude: 47.35462, longitude: -68.2343),
239+
CLLocationCoordinate2D(latitude: 47.06624, longitude: -67.79035),
240+
CLLocationCoordinate2D(latitude: 45.70258, longitude: -67.79141),
241+
CLLocationCoordinate2D(latitude: 45.13745, longitude: -67.13734),
242+
]
243+
])
231244

232245
struct MapViewportExample_Previews: PreviewProvider {
233246
static var previews: some View {

Sources/MapboxMaps/Foundation/MapboxMap.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ protocol MapboxMapProtocol: AnyObject {
1111
var size: CGSize { get }
1212
var anchor: CGPoint { get }
1313
var options: MapOptions { get }
14+
var sizeSignal: Signal<CGSize> { get }
1415
func setCamera(to cameraOptions: CameraOptions)
1516
func setCameraBounds(with options: CameraBoundsOptions) throws
1617
func setNorthOrientation(_ northOrientation: NorthOrientation)
@@ -203,6 +204,9 @@ public final class MapboxMap: StyleManager {
203204

204205
private let _isDefaultCameraInitialized = CurrentValueSignalProxy<Bool>()
205206

207+
private let sizeSubject = CurrentValueSignalSubject(CGSize.zero)
208+
private(set) lazy var sizeSignal = sizeSubject.signal.skipRepeats()
209+
206210
/// Triggered when map is loaded for the first time, and camera is initialized with default style camera options.
207211
var isDefaultCameraInitialized: Signal<Bool> { _isDefaultCameraInitialized.signal.skipRepeats() }
208212

@@ -467,6 +471,7 @@ public final class MapboxMap: StyleManager {
467471
}
468472
set {
469473
__map.setSizeFor(Size(newValue))
474+
self.sizeSubject.value = newValue
470475
}
471476
}
472477

Sources/MapboxMaps/Foundation/Signal/Signal.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,14 @@ extension Signal {
169169
}
170170

171171
extension Signal {
172-
static func combineLatest<P1, P2>(_ s1: Signal<P1>, _ s2: Signal<P2>) -> Signal where Payload == (P1, P2) {
172+
static func combineLatest<P1, P2, P3>(_ s1: Signal<P1>, _ s2: Signal<P2>, _ s3: Signal<P3>) -> Signal where Payload == (P1, P2, P3) {
173173
Signal { handler in
174174
var last1: P1?
175175
var last2: P2?
176+
var last3: P3?
176177
let handle = {
177-
if let last1, let last2 {
178-
handler((last1, last2))
178+
if let last1, let last2, let last3 {
179+
handler((last1, last2, last3))
179180
}
180181
}
181182
return AnyCancelable([
@@ -186,10 +187,17 @@ extension Signal {
186187
s2.observe { value in
187188
last2 = value
188189
handle()
190+
},
191+
s3.observe { value in
192+
last3 = value
193+
handle()
189194
}
190195
])
191196
}
192197
}
198+
static func combineLatest<P1, P2>(_ s1: Signal<P1>, _ s2: Signal<P2>) -> Signal where Payload == (P1, P2) {
199+
Signal<(P1, P2, Int)>.combineLatest(s1, s2, Signal<Int>(just: 1)).map { ($0.0, $0.1) }
200+
}
193201
}
194202

195203
extension Signal {

Sources/MapboxMaps/Viewport/States/FollowPuck/FollowPuckViewportState.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ public final class FollowPuckViewportState {
2626
self.mapboxMap = mapboxMap
2727

2828
let resultCamera = Signal
29-
.combineLatest(optionsSubject.signal.skipRepeats(), onPuckRender.map(\.followPuckState).skipRepeats())
30-
.map { (options, renderingState) in
29+
.combineLatest(
30+
optionsSubject.signal.skipRepeats(),
31+
mapboxMap.sizeSignal, // trigger viewport reevaluation on map size change
32+
onPuckRender.map(\.followPuckState).skipRepeats())
33+
.map { (options, _, renderingState) in
3134
CameraOptions(
3235
center: renderingState.coordinate,
3336
padding: options.padding,

Sources/MapboxMaps/Viewport/States/Overview/OverviewViewportState.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ public final class OverviewViewportState {
3434
self.optionsSubject = .init(options)
3535
self.mapboxMap = mapboxMap
3636
self.cameraAnimationsManager = cameraAnimationsManager
37-
self.cameraOptions = Signal.combineLatest(safeAreaPadding, optionsSubject.signal.skipRepeats())
38-
.map { safeAreaPadding, options in
37+
self.cameraOptions =
38+
Signal.combineLatest(
39+
safeAreaPadding,
40+
mapboxMap.sizeSignal, // trigger viewport reevaluation on map size change
41+
optionsSubject.signal.skipRepeats()
42+
)
43+
.map { safeAreaPadding, _, options in
3944
let padding = safeAreaPadding + options.padding
4045
let cam = try? mapboxMap.camera(
4146
for: options.geometry.coordinates,

0 commit comments

Comments
 (0)