Skip to content

Commit 0bc0de8

Browse files
pjleonard37claude
authored andcommitted
MAPSIOS-1861 Add ability to force style reload (#7449)
## Summary Adds `StyleReloadPolicy` to control whether a style should reload when the URI or JSON matches the currently loaded style. This enables developers to force style reload events even when the style hasn't changed, which is useful for implementations that rely on style load events triggering consistently. ## Changes ### New - **`StyleReloadPolicy`**: A struct with two options: - `. onlyIfChanged ` (default): Only reloads if URI/JSON differs from currently loaded style - `.always`: Always reload even when URI/JSON matches the currently loaded style ### Modified - **`MapStyle.init(uri:configuration:reloadPolicy:)`**: Added optional `reloadPolicy` parameter - **`MapStyle.init(json:configuration:reloadPolicy:)`**: Added optional `reloadPolicy` parameter - **`MapboxMap.loadStyle(_:transition:reloadPolicy:completion:)`**: Added optional `reloadPolicy` parameter (both URI and JSON variants) ### Implementation - Updated `MapStyleReconciler` to check reload policy when determining if reload is needed - Added comprehensive test coverage with 6 new tests covering all scenarios Co-authored-by: Claude <[email protected]> GitOrigin-RevId: ad38bae56d573e3e84cc449e1100c5f09932551d
1 parent 39a9355 commit 0bc0de8

File tree

8 files changed

+176
-11
lines changed

8 files changed

+176
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ mapView.mapboxMap.setStyleImportConfigProperties(for: "basemap", configs: ["ligh
1818
let options = MapInitOptions(mapStyle: .standardSatellite(lightPreset: .dusk))
1919
let mapView = MapView(frame: view.bounds, mapInitOptions: options)
2020
```
21-
21+
* Add `StyleReloadPolicy` to control style reload behavior. Use `reloadPolicy: .always` parameter in `loadStyle()` methods or `MapStyle` initializers to always reload the style even when the URI or JSON matches the currently loaded style. Defaults to `.onlyIfChanged` for optimal performance.
2222

2323
## 11.16.0 - 21 October, 2025
2424

Sources/MapboxMaps/ContentBuilders/MapContent/MapStyleReconciler.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ final class MapStyleReconciler {
3636
) {
3737
let oldMapStyle = _mapStyle
3838

39-
if _mapStyle?.data != style.data {
39+
// Determine if reload is needed based on data change or reload policy
40+
let shouldReload = _mapStyle?.data != style.data || style.reloadPolicy == .always
41+
42+
if shouldReload {
4043
_mapStyle = style
4144

4245
let callbacks = RuntimeStylingCallbacks(

Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- ``StyleManager-46yjd``
88
- ``StyleURI``
9+
- ``StyleReloadPolicy``
910
- ``LayerPosition-swift.enum``
1011
- ``AmbientLight``
1112
- ``DirectionalLight``

Sources/MapboxMaps/Foundation/MapboxMap.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,14 @@ public final class MapboxMap: StyleManager {
234234
/// - Parameters:
235235
/// - styleURI: StyleURI to load
236236
/// - transition: Options for the style transition.
237+
/// - reloadPolicy: Controls whether the style should reload if the URI matches the currently loaded style. When `nil`, behaves as `.onlyIfChanged`. Defaults to `nil`.
237238
/// - completion: Closure called when the style has been fully loaded.
238239
/// If style has failed to load a `MapLoadingError` is provided to the closure.
239240
public func loadStyle(_ styleURI: StyleURI,
240241
transition: TransitionOptions? = nil,
242+
reloadPolicy: StyleReloadPolicy? = nil,
241243
completion: ((Error?) -> Void)? = nil) {
242-
load(mapStyle: MapStyle(uri: styleURI),
244+
load(mapStyle: MapStyle(uri: styleURI, reloadPolicy: reloadPolicy),
243245
transition: transition,
244246
completion: completion)
245247
}
@@ -265,12 +267,14 @@ public final class MapboxMap: StyleManager {
265267
/// - Parameters:
266268
/// - JSON: Style JSON string
267269
/// - transition: Options for the style transition.
270+
/// - reloadPolicy: Controls whether the style should reload if the JSON matches the currently loaded style. When `nil`, behaves as `.onlyIfChanged`. Defaults to `nil`.
268271
/// - completion: Closure called when the style has been fully loaded.
269272
/// If style has failed to load a `MapLoadingError` is provided to the closure.
270273
public func loadStyle(_ JSON: String,
271274
transition: TransitionOptions? = nil,
275+
reloadPolicy: StyleReloadPolicy? = nil,
272276
completion: ((Error?) -> Void)? = nil) {
273-
load(mapStyle: MapStyle(json: JSON),
277+
load(mapStyle: MapStyle(json: JSON, reloadPolicy: reloadPolicy),
274278
transition: transition,
275279
completion: completion)
276280
}

Sources/MapboxMaps/Style/MapStyle.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ import MapboxCoreMaps
5555
/// mapView.mapboxMap.mapStyle = .standard(stylePreset: .dusk)
5656
/// ```
5757
///
58-
/// The style reloads only when the actual ``StyleURI`` or JSON (when loaded with ``MapStyle/init(json:configuration:)`` is changed. To observe the result of the style load you can subscribe to ``MapboxMap/onStyleLoaded`` or ``Snapshotter/onStyleLoaded`` events, or use use ``StyleManager/load(mapStyle:transition:completion:)`` method.
58+
/// By default, the style reloads only when the actual ``StyleURI`` or JSON (when loaded with ``MapStyle/init(json:configuration:reloadPolicy:)``) is changed. Use ``StyleReloadPolicy/always`` to always reload even when the URI or JSON hasn't changed.
59+
/// To observe the result of the style load you can subscribe to ``MapboxMap/onStyleLoaded`` or ``Snapshotter/onStyleLoaded`` events, or use ``StyleManager/load(mapStyle:transition:completion:)`` method.
5960
public struct MapStyle: Equatable, Sendable {
6061
enum Data: Equatable, Sendable {
6162
case uri(StyleURI)
@@ -75,20 +76,23 @@ public struct MapStyle: Equatable, Sendable {
7576
}
7677
var data: Data
7778
var configuration: JSONObject?
79+
var reloadPolicy: StyleReloadPolicy?
7880

7981
/// Creates a map style using a Mapbox Style JSON.
8082
///
8183
/// Please consult [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/) describing the JSON format.
8284
///
83-
/// - Important: For the better performance with large local Style JSON please consider loading style from the file system via the ``MapStyle/init(uri:configuration:)`` initializer.
85+
/// - Important: For the better performance with large local Style JSON please consider loading style from the file system via the ``MapStyle/init(uri:configuration:reloadPolicy:)`` initializer.
8486
///
8587
/// - Parameters:
8688
/// - json: A Mapbox Style JSON string.
8789
/// - configuration: Style import configuration to be applied on style load.
8890
/// Providing `nil` configuration will make no effect and previous configuration will stay in place. In order to change previous value, you should explicitly override it with the new value.
89-
public init(json: String, configuration: JSONObject? = nil) {
91+
/// - reloadPolicy: Controls whether the style should reload if the JSON matches the currently loaded style. When `nil`, behaves as `.onlyIfChanged`. Defaults to `nil`.
92+
public init(json: String, configuration: JSONObject? = nil, reloadPolicy: StyleReloadPolicy? = nil) {
9093
self.data = .json(json)
9194
self.configuration = configuration
95+
self.reloadPolicy = reloadPolicy
9296
}
9397

9498
/// Creates a map style using a Style URI.
@@ -99,19 +103,21 @@ public struct MapStyle: Equatable, Sendable {
99103
/// - uri: An instance of ``StyleURI`` pointing to a Mapbox Style URI (mapbox://styles/{user}/{style}), a full HTTPS URI, or a path to a local file.
100104
/// - configuration: Style import configuration to be applied on style load.
101105
/// Providing `nil` configuration will make no effect and previous configuration will stay in place. In order to change previous value, you should explicitly override it with the new value.
102-
public init(uri: StyleURI, configuration: JSONObject? = nil) {
106+
/// - reloadPolicy: Controls whether the style should reload if the URI matches the currently loaded style. When `nil`, behaves as `.onlyIfChanged`. Defaults to `nil`.
107+
public init(uri: StyleURI, configuration: JSONObject? = nil, reloadPolicy: StyleReloadPolicy? = nil) {
103108
if let override = Self._overrides[uri] {
104109
self.data = override.data
105110
self.configuration = override.configuration
106-
111+
self.reloadPolicy = reloadPolicy
107112
if let configuration, !configuration.isEmpty {
108113
self.configuration = (self.configuration ?? [:]).merging(configuration, uniquingKeysWith: {_, new in new })
109114
}
110115
return
111116
}
112-
117+
113118
self.data = .uri(uri)
114119
self.configuration = configuration
120+
self.reloadPolicy = reloadPolicy
115121
}
116122

117123
/// [Mapbox Streets](https://www.mapbox.com/maps/streets) is a general-purpose style with detailed road and transit networks.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
3+
/// Defines the reload policy for map styles.
4+
///
5+
/// Use this policy to control whether a style should reload when the URI or JSON
6+
/// matches the currently loaded style.
7+
///
8+
/// ```swift
9+
/// // Default behavior: only reload if style URI/JSON changes
10+
/// mapboxMap.loadStyle(.standard)
11+
///
12+
/// // Always reload even if style URI/JSON is the same
13+
/// mapboxMap.loadStyle(.standard, reloadPolicy: .always) { error in
14+
/// // Style reloaded, events triggered
15+
/// }
16+
/// ```
17+
public struct StyleReloadPolicy: Equatable, Sendable {
18+
let rawValue: String
19+
20+
/// Reload the style only if the URI or JSON differs from the currently loaded style.
21+
///
22+
/// This is the default and provides optimal performance.
23+
public static let onlyIfChanged = StyleReloadPolicy(rawValue: "onlyIfChanged")
24+
25+
/// Always reload the style even if the URI or JSON matches the currently loaded style.
26+
///
27+
/// Use this when you need style load events to trigger for the same style.
28+
///
29+
/// - Note: Pending style loads will be cancelled when reloading.
30+
public static let always = StyleReloadPolicy(rawValue: "always")
31+
}

Tests/MapboxMapsTests/Foundation/Style/MapStyleReconcilerTests.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,4 +298,118 @@ final class MapStyleReconcilerTests: XCTestCase {
298298

299299
token.cancel()
300300
}
301+
302+
// MARK: - Always Reload Tests
303+
304+
func testAlwaysReloadWithSameURI() throws {
305+
styleManager.setStyleURIStub.defaultSideEffect = { _ in
306+
self.styleManager.isStyleLoadedStub.defaultReturnValue = false
307+
}
308+
309+
// Load initial style
310+
me.mapStyle = MapStyle(uri: .standard)
311+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1)
312+
313+
// Load same URI with default policy (should skip)
314+
me.mapStyle = MapStyle(uri: .standard)
315+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1, "Should not reload with same URI")
316+
317+
// Always reload with same URI
318+
me.mapStyle = MapStyle(uri: .standard, reloadPolicy: .always)
319+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 2, "Should reload with .always policy")
320+
}
321+
322+
func testAlwaysReloadWithSameJSON() throws {
323+
let json = "{\"layers\": [], \"sources\": {}}"
324+
styleManager.setStyleJSONStub.defaultSideEffect = { _ in
325+
self.styleManager.isStyleLoadedStub.defaultReturnValue = false
326+
}
327+
328+
// Load initial style
329+
me.mapStyle = MapStyle(json: json)
330+
XCTAssertEqual(styleManager.setStyleJSONStub.invocations.count, 1)
331+
332+
// Load same JSON with default policy (should skip)
333+
me.mapStyle = MapStyle(json: json)
334+
XCTAssertEqual(styleManager.setStyleJSONStub.invocations.count, 1, "Should not reload with same JSON")
335+
336+
// Always reload with same JSON
337+
me.mapStyle = MapStyle(json: json, reloadPolicy: .always)
338+
XCTAssertEqual(styleManager.setStyleJSONStub.invocations.count, 2, "Should reload with .always policy")
339+
}
340+
341+
func testAlwaysReloadCancelsPendingLoads() throws {
342+
var callbacks: RuntimeStylingCallbacks?
343+
styleManager.setStyleURIStub.defaultSideEffect = { invoc in
344+
self.styleManager.isStyleLoadedStub.defaultReturnValue = false
345+
// Cancel previous load when new load starts
346+
if let callbacks {
347+
self.simulateLoad(callbacks: callbacks, result: .cancel)
348+
}
349+
callbacks = invoc.parameters.callbacks
350+
}
351+
352+
// Start loading a style
353+
var firstCallbackReceived = false
354+
me.loadStyle(MapStyle(uri: .standard)) { error in
355+
XCTAssertTrue(error is CancelError, "First load should be cancelled")
356+
firstCallbackReceived = true
357+
}
358+
359+
// Always reload should cancel the first load
360+
me.loadStyle(MapStyle(uri: .standard, reloadPolicy: .always)) { error in
361+
XCTAssertNil(error)
362+
}
363+
364+
// Simulate the second load completing
365+
simulateLoad(callbacks: try XCTUnwrap(callbacks), result: .success)
366+
367+
XCTAssertTrue(firstCallbackReceived, "First completion should have been called with CancelError")
368+
}
369+
370+
func testOnlyIfChangedPolicyDoesNotReloadSameStyle() {
371+
styleManager.setStyleURIStub.defaultSideEffect = { _ in
372+
self.styleManager.isStyleLoadedStub.defaultReturnValue = false
373+
}
374+
375+
me.mapStyle = MapStyle(uri: .standard, reloadPolicy: .onlyIfChanged)
376+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1)
377+
378+
me.mapStyle = MapStyle(uri: .standard, reloadPolicy: .onlyIfChanged)
379+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1, ".onlyIfChanged should not reload same style")
380+
}
381+
382+
func testNilPolicyBehavesLikeOnlyIfChanged() {
383+
styleManager.setStyleURIStub.defaultSideEffect = { _ in
384+
self.styleManager.isStyleLoadedStub.defaultReturnValue = false
385+
}
386+
387+
me.mapStyle = MapStyle(uri: .standard, reloadPolicy: nil)
388+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1)
389+
390+
me.mapStyle = MapStyle(uri: .standard, reloadPolicy: nil)
391+
XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1, "nil policy should behave like .onlyIfChanged")
392+
}
393+
394+
func testAlwaysReloadTriggersStyleLoadedEvent() throws {
395+
var callbacks: RuntimeStylingCallbacks?
396+
styleManager.setStyleURIStub.defaultSideEffect = { invoc in
397+
self.styleManager.isStyleLoadedStub.defaultReturnValue = false
398+
callbacks = invoc.parameters.callbacks
399+
}
400+
401+
// Load and complete initial style
402+
me.mapStyle = MapStyle(uri: .standard)
403+
simulateLoad(callbacks: try XCTUnwrap(callbacks), result: .success)
404+
405+
// Always reload should trigger another load
406+
var reloadCompleted = false
407+
me.loadStyle(MapStyle(uri: .standard, reloadPolicy: .always)) { error in
408+
XCTAssertNil(error)
409+
reloadCompleted = true
410+
}
411+
412+
simulateLoad(callbacks: try XCTUnwrap(callbacks), result: .success)
413+
XCTAssertTrue(reloadCompleted, "Always reload completion should be called")
414+
}
301415
}

scripts/api-compatibility-check/breakage_allowlist.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2205,4 +2205,10 @@ Func MapStyle.standard(theme:lightPreset:font:showPointOfInterestLabels:showTran
22052205
Func MapStyle.standard(theme:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:showPedestrianRoads:show3dObjects:backgroundPointOfInterestLabels:colorAdminBoundaries:colorBuildingHighlight:colorBuildingSelect:colorGreenspace:colorModePointOfInterestLabels:colorMotorways:colorPlaceLabelHighlight:colorPlaceLabels:colorPlaceLabelSelect:colorPointOfInterestLabels:colorRoadLabels:colorRoads:colorTrunks:colorWater:densityPointOfInterestLabels:roadsBrightness:showAdminBoundaries:showLandmarkIconLabels:showLandmarkIcons:themeData:) has parameter 26 type change from Swift.Bool? to Swift.Double?
22062206
Func MapStyle.standard(theme:lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:showPedestrianRoads:show3dObjects:backgroundPointOfInterestLabels:colorAdminBoundaries:colorBuildingHighlight:colorBuildingSelect:colorGreenspace:colorModePointOfInterestLabels:colorMotorways:colorPlaceLabelHighlight:colorPlaceLabels:colorPlaceLabelSelect:colorPointOfInterestLabels:colorRoadLabels:colorRoads:colorTrunks:colorWater:densityPointOfInterestLabels:roadsBrightness:showAdminBoundaries:showLandmarkIconLabels:showLandmarkIcons:themeData:) has parameter 29 type change from Swift.String? to Swift.Bool?
22072207
Func MapStyle.standardSatellite(lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:showRoadsAndTransit:showPedestrianRoads:backgroundPointOfInterestLabels:colorAdminBoundaries:colorModePointOfInterestLabels:colorMotorways:colorPlaceLabelHighlight:colorPlaceLabels:colorPlaceLabelSelect:colorPointOfInterestLabels:colorRoadLabels:colorRoads:colorTrunks:densityPointOfInterestLabels:roadsBrightness:showAdminBoundaries:) has parameter 20 type change from Swift.Double? to MapboxMaps.StandardFuelingStationModePointOfInterestLabels?
2208-
Func MapStyle.standardSatellite(lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:showRoadsAndTransit:showPedestrianRoads:backgroundPointOfInterestLabels:colorAdminBoundaries:colorModePointOfInterestLabels:colorMotorways:colorPlaceLabelHighlight:colorPlaceLabels:colorPlaceLabelSelect:colorPointOfInterestLabels:colorRoadLabels:colorRoads:colorTrunks:densityPointOfInterestLabels:roadsBrightness:showAdminBoundaries:) has parameter 21 type change from Swift.Bool? to Swift.Double?
2208+
Func MapStyle.standardSatellite(lightPreset:font:showPointOfInterestLabels:showTransitLabels:showPlaceLabels:showRoadLabels:showRoadsAndTransit:showPedestrianRoads:backgroundPointOfInterestLabels:colorAdminBoundaries:colorModePointOfInterestLabels:colorMotorways:colorPlaceLabelHighlight:colorPlaceLabels:colorPlaceLabelSelect:colorPointOfInterestLabels:colorRoadLabels:colorRoads:colorTrunks:densityPointOfInterestLabels:roadsBrightness:showAdminBoundaries:) has parameter 21 type change from Swift.Bool? to Swift.Double?
2209+
2210+
// Add StyleReloadPolicy to control style reload behavior
2211+
Constructor MapStyle.init(json:configuration:) has been removed
2212+
Constructor MapStyle.init(uri:configuration:) has been removed
2213+
Func MapboxMap.loadStyle(_:transition:completion:) has been renamed to Func loadStyle(_:transition:reloadPolicy:completion:)
2214+
Func MapboxMap.loadStyle(_:transition:completion:) has parameter 2 type change from (((any Swift.Error)?) -> ())? to MapboxMaps.StyleReloadPolicy?

0 commit comments

Comments
 (0)