Skip to content

Commit

Permalink
[MAPSIOS-1272] Add support for style transitions to StyleDSL (#2045)
Browse files Browse the repository at this point in the history
Enables style transitions in the style DSL by creating a TransitionOptions struct
  • Loading branch information
pjleonard37 authored Mar 27, 2024
1 parent 603ca4f commit cc38f82
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct DynamicStylingExample: View {

@State var connectionKind = ConnectionComponent.Kind.line
@State var settingsHeight = 0.0
@State var styleTransitions = false
@State var cities = CityCollection.northern
@State var pinFeatures: FeaturesRef?
@State var connectionFeatures: FeaturesRef?
Expand Down Expand Up @@ -68,6 +69,9 @@ struct DynamicStylingExample: View {
StyleImage(id: "pin-icon", image: pinIcon.image)
SymbolLayer(id: "pin", source: "points")
.iconImage("pin-icon")
if styleTransitions {
TransitionOptions(duration: 5)
}
}

if let route {
Expand Down Expand Up @@ -129,6 +133,7 @@ struct DynamicStylingExample: View {

Toggle("Custom Atmosphere", isOn: $customAtmosphere)
Toggle("Custom Lights", isOn: $customLights)
Toggle("Transition styles slowly", isOn: $styleTransitions)
}
.padding(10)
.floating(RoundedRectangle(cornerRadius: 10))
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Mapbox welcomes participation and contributions from everyone.

## main

### Breaking changes ⚠️
### Potentially breaking changes ⚠️
* Experimental `MapStyle` no longer conforms to Equatable.
* `TransitionOptions` is now a Swift struct rather than an Objective-C class.

### Features ✨ and improvements 🏁
* Introduce an experimental Style DSL, enabling developers to add map style content like Sources, Layers, Style Images, Terrain, Light and Atmosphere to their map style at runtime in a declarative pattern. See the documentation [here](https://docs.mapbox.com/ios/maps/api/11.2.0-beta.1/documentation/mapboxmaps/style-dsl) for more information. For SwiftUI users, this Style DSL provides a more natural approach to manipulating content.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ Maps rendering SDK also known as GL-Native. These are for internal use only.
- ``MapLoadedCallback``
- ``StyleImageRemoveUnusedCallback``
- ``StyleLoadedCallback``
- ``TransitionOptions-class``
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
- ``TextTranslateAnchor``
- ``TextWritingMode``
- ``Visibility``
- ``TransitionOptions``
- ``TransitionOptions-struct``
- ``ModelScaleMode``

<!-- Next two are arguable regarding it's category -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- ``StyleProjection``
- ``StyleProjectionName``
- ``CancelError``
- ``TransitionOptions-struct``

### Style DSL

Expand Down
4 changes: 2 additions & 2 deletions Sources/MapboxMaps/Documentation.docc/Extensions/MapboxMap.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

### Style loading

- ``MapboxMap/loadStyle(_:transition:completion:)-2jmep``
- ``MapboxMap/loadStyle(_:transition:completion:)-4vvrf``
- ``MapboxMap/loadStyle(_:transition:completion:)-6icex``
- ``MapboxMap/loadStyle(_:transition:completion:)-1ilz1``

### Map events

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
import Foundation
extension TransitionOptions {

/**
* The `TransitionOptions` control timing for the interpolation between a transitionable style
* property's previous value and new value. These can be used to define the style default property
* transition behavior. Also, any transitionable style property may also have its own `-transition`
* property that defines specific transition timing for that specific layer property, overriding
* the global transition values.
*/
public struct TransitionOptions: Equatable {
/// Initializes `TransitionOptions` with provided `duration`, `delay` and `enablePlacementTransitions` flag.
/// - Parameters:
/// - duration: Time allotted for transitions to complete.
/// - delay: Length of time before the transition begins.
/// - enablePlacementTransitions: Whether the fade in/out symbol placement transition is enabled.
public convenience init(duration: TimeInterval?,
delay: TimeInterval?,
enablePlacementTransitions: Bool?) {

self.init(__duration: duration.map(NSNumber.init(value:)),
delay: delay.map(NSNumber.init(value:)),
enablePlacementTransitions: enablePlacementTransitions.map(NSNumber.init(value:)))
public init(duration: TimeInterval? = nil,
delay: TimeInterval? = nil,
enablePlacementTransitions: Bool? = nil) {
self.duration = duration
self.delay = delay
self.enablePlacementTransitions = enablePlacementTransitions
}

/// Time allotted for transitions to complete. Defaults to `0.3` seconds.
public var duration: TimeInterval? {
__duration?.doubleValue
}
public var duration: TimeInterval?

/// Length of time before a transition begins. Defaults to `0.0` seconds.
public var delay: TimeInterval? {
__delay?.doubleValue
}
public var delay: TimeInterval?

/// Whether the fade in/out symbol placement transition is enabled. Defaults to `true`.
public var enablePlacementTransitions: Bool? {
__enablePlacementTransitions?.boolValue
public var enablePlacementTransitions: Bool?

internal init(_ objValue: MapboxCoreMaps.TransitionOptions) {
self.init(duration: objValue.__duration?.doubleValue,
delay: objValue.__delay?.doubleValue,
enablePlacementTransitions: objValue.__enablePlacementTransitions?.boolValue)
}

internal var coreOptions: MapboxCoreMaps.TransitionOptions {
.init(self)
}
}

extension MapboxCoreMaps.TransitionOptions {
internal convenience init(_ swiftValue: TransitionOptions) {
self.init(__duration: swiftValue.duration.map(NSNumber.init(value:)),
delay: swiftValue.delay.map(NSNumber.init(value:)),
enablePlacementTransitions: swiftValue.enablePlacementTransitions.map(NSNumber.init(value:)))
}
}

@_spi(Experimental)
@available(iOS 13.0, *)
extension TransitionOptions: MapStyleContent, PrimitiveMapStyleContent {
func visit(_ node: MapStyleNode) {
node.mount(MountedUniqueProperty(keyPath: \.transition, value: self))
}
}
2 changes: 1 addition & 1 deletion Sources/MapboxMaps/Foundation/MapboxMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ extension MapboxMap {
public var onStyleLoaded: Signal<StyleLoaded> { events.signal(for: \.onStyleLoaded) }

/// The requested style data has been loaded. The `type` property defines what kind of style data has been loaded.
/// Event may be emitted synchronously, for example, when ``MapboxMap/loadStyle(_:transition:completion:)-2jmep`` is used to load style.
/// Event may be emitted synchronously, for example, when ``MapboxMap/loadStyle(_:transition:completion:)-1ilz1`` is used to load style.
///
/// Based on an event data `type` property value, following use-cases may be implemented:
/// - `style`: Style is parsed, style layer properties could be read and modified, style layers and sources could be
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxMaps/Snapshot/Snapshotter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ extension Snapshotter {
public var onStyleLoaded: Signal<StyleLoaded> { events.signal(for: \.onStyleLoaded) }

/// The requested style data has been loaded. The `type` property defines what kind of style data has been loaded.
/// Event may be emitted synchronously, for example, when ``MapboxMap/loadStyle(_:transition:completion:)-2jmep`` is used to load style.
/// Event may be emitted synchronously, for example, when ``MapboxMap/loadStyle(_:transition:completion:)-1ilz1`` is used to load style.
///
/// Based on an event data `type` property value, following use-cases may be implemented:
/// - `style`: Style is parsed, style layer properties could be read and modified, style layers and sources could be
Expand Down
6 changes: 3 additions & 3 deletions Sources/MapboxMaps/Style/MapStyle/MapStyleReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ final class MapStyleReconciler {
/// can continue loading.
/// We can safely add new content via style DSL.
guard let self else { return }
if let transition {
self.styleManager.setStyleTransitionFor(transition)
}
self.reconcileStyleImports(from: oldMapStyle?.importConfigurations)
self._isStyleRootLoaded.value = true
if let transition {
self.styleManager.setStyleTransitionFor(transition.coreOptions)
}
},
completed: { [weak self] in
completion?(nil)
Expand Down
37 changes: 24 additions & 13 deletions Sources/MapboxMaps/Style/MapStyle/MapStyleUniqueProperties.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct MapStyleUniqueProperties {
var terrain: Terrain?
var atmosphere: Atmosphere?
var projection: StyleProjection?
var transition: TransitionOptions?
var lights = Lights()

private func update<T: Equatable & Encodable>(_ label: String, old: T?, new: T?, setter: (Any) -> Expected<NSNull, NSString>) {
Expand All @@ -37,20 +38,30 @@ struct MapStyleUniqueProperties {
update("projection", old: old.projection, new: projection, setter: style.setStyleProjectionForProperties(_:))
update("terrain", old: old.terrain, new: terrain, setter: style.setStyleTerrainForProperties(_:))

guard old.lights != lights else { return }
if old.lights != lights {
wrapStyleDSLError {
if let directional = lights.directional, let ambient = lights.ambient {
os_log(.debug, log: .styleDsl, "set 3d lights")
try style.setLights(ambient: ambient, directional: directional)
} else if lights.directional != nil || lights.ambient != nil {
Log.warning(forMessage: "Incorrect 3D light configuration. Specify both directional and ambient lights.", category: "StyleDSL")
} else if let flat = lights.flat {
os_log(.debug, log: .styleDsl, "set flat light")
try style.setLights(flat)
} else {
os_log(.debug, log: .styleDsl, "remove lights")
try handleExpected { style.setStyleLightsForLights(NSNull()) }
}
}
}

wrapStyleDSLError {
if let directional = lights.directional, let ambient = lights.ambient {
os_log(.debug, log: .styleDsl, "set 3d lights")
try style.setLights(ambient: ambient, directional: directional)
} else if lights.directional != nil || lights.ambient != nil {
Log.warning(forMessage: "Incorrect 3D light configuration. Specify both directional and ambient lights.", category: "StyleDSL")
} else if let flat = lights.flat {
os_log(.debug, log: .styleDsl, "set flat light")
try style.setLights(flat)
} else {
os_log(.debug, log: .styleDsl, "remove lights")
try handleExpected { style.setStyleLightsForLights(NSNull()) }
if old.transition != transition {
wrapStyleDSLError {
if let transition {
style.setStyleTransitionFor(transition.coreOptions)
} else {
style.setStyleTransitionFor(TransitionOptions().coreOptions)
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/MapboxMaps/Style/StyleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,10 @@ public class StyleManager {
/// - SeeAlso: ``MapboxMap/onNext(event:handler:)``
public var styleTransition: TransitionOptions {
get {
styleManager.getStyleTransition()
TransitionOptions(styleManager.getStyleTransition())
}
set {
styleManager.setStyleTransitionFor(newValue)
styleManager.setStyleTransitionFor(newValue.coreOptions)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import XCTest
import MapboxMaps
@testable import MapboxMaps

final class TransitionOptionsTests: XCTestCase {
func testInitWithNils() {
Expand All @@ -8,9 +8,9 @@ final class TransitionOptionsTests: XCTestCase {
delay: nil,
enablePlacementTransitions: nil)

XCTAssertNil(options.__duration)
XCTAssertNil(options.__delay)
XCTAssertNil(options.__enablePlacementTransitions)
XCTAssertNil(options.coreOptions.__duration)
XCTAssertNil(options.coreOptions.__delay)
XCTAssertNil(options.coreOptions.__enablePlacementTransitions)
}

func testInitWithNonNils() {
Expand All @@ -23,19 +23,18 @@ final class TransitionOptionsTests: XCTestCase {
delay: delay,
enablePlacementTransitions: enablePlacementTransition)

XCTAssertEqual(options.__duration, NSNumber(value: duration))
XCTAssertEqual(options.__delay, NSNumber(value: delay))
XCTAssertEqual(options.__enablePlacementTransitions, NSNumber(value: enablePlacementTransition))
XCTAssertEqual(options.coreOptions.__duration, NSNumber(value: duration))
XCTAssertEqual(options.coreOptions.__delay, NSNumber(value: delay))
XCTAssertEqual(options.coreOptions.__enablePlacementTransitions, NSNumber(value: enablePlacementTransition))
}

func testRefinedPropertiesWithNonNils() {
let duration = TimeInterval.random(in: 0...10)
let delay = TimeInterval.random(in: 0...10)
let enablePlacementTransition = Bool.random()
let options = TransitionOptions(
__duration: NSNumber(value: duration),
delay: NSNumber(value: delay),
enablePlacementTransitions: NSNumber(value: enablePlacementTransition))
let options = TransitionOptions(duration: duration,
delay: delay,
enablePlacementTransitions: enablePlacementTransition)

XCTAssertEqual(options.duration, duration)
XCTAssertEqual(options.delay, delay)
Expand All @@ -44,12 +43,23 @@ final class TransitionOptionsTests: XCTestCase {

func testRefinedPropertiesWithNils() {
let options = TransitionOptions(
__duration: nil,
delay: nil,
enablePlacementTransitions: nil)
duration: nil,
delay: nil,
enablePlacementTransitions: nil)

XCTAssertNil(options.duration)
XCTAssertNil(options.delay)
XCTAssertNil(options.enablePlacementTransitions)
}

func testTransitionOptionsEquality() {
let options = TransitionOptions(duration: 12, delay: 3, enablePlacementTransitions: true)
let options2 = TransitionOptions(duration: 12, delay: 3, enablePlacementTransitions: true)

XCTAssertEqual(options.duration, options2.duration)
XCTAssertEqual(options.delay, options2.delay)
XCTAssertEqual(options.enablePlacementTransitions, options2.enablePlacementTransitions)
XCTAssertEqual(options, options2)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ final class FeatureTests: XCTestCase {
}

func testSetPropertiesWithFunction() throws {
var feature = Feature(geometry: geometry)
let feature = Feature(geometry: geometry)
.properties([
"a": 123,
"b": "c",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class MockStyleManager: StyleManagerProtocol {
}

let getStyleTransitionStub = Stub<Void, MapboxCoreMaps.TransitionOptions>(
defaultReturnValue: TransitionOptions(duration: nil, delay: nil, enablePlacementTransitions: nil)
defaultReturnValue: .init(TransitionOptions())
)
func getStyleTransition() -> MapboxCoreMaps.TransitionOptions {
getStyleTransitionStub.call()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ final class MapStyleReconcilerTests: XCTestCase {

XCTAssertEqual(styleManager.setStyleURIStub.invocations.count, 1)
XCTAssertEqual(styleManager.setStyleTransitionStub.invocations.count, 1)
XCTAssertEqual(styleManager.setStyleTransitionStub.invocations.last?.parameters, transition)

let coreTransitionOptions = try XCTUnwrap(styleManager.setStyleTransitionStub.invocations.last?.parameters)
XCTAssertEqual(TransitionOptions(coreTransitionOptions), transition)

XCTAssertEqual(calls, 2)
}
Expand Down
11 changes: 6 additions & 5 deletions Tests/MapboxMapsTests/Foundation/Style/StyleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,24 @@ final class StyleManagerTests: XCTestCase {
}

func testGetStyleTransition() {
let stubTransition = MapboxCoreMaps.TransitionOptions(
let stubTransition = TransitionOptions(
duration: .random(in: 0...300),
delay: .random(in: 0...300),
enablePlacementTransitions: .random())
styleManager.getStyleTransitionStub.defaultReturnValue = stubTransition
styleManager.getStyleTransitionStub.defaultReturnValue = stubTransition.coreOptions

XCTAssertEqual(style.styleTransition, stubTransition)
}

func testSetStyleTransition() {
let stubTransition = MapboxCoreMaps.TransitionOptions(
func testSetStyleTransition() throws {
let stubTransition = TransitionOptions(
duration: .random(in: 0...300),
delay: .random(in: 0...300),
enablePlacementTransitions: .random())
style.styleTransition = stubTransition

XCTAssertEqual(styleManager.setStyleTransitionStub.invocations.last?.parameters, stubTransition)
let coreTransitionOptions = try XCTUnwrap(styleManager.setStyleTransitionStub.invocations.last?.parameters)
XCTAssertEqual(TransitionOptions(coreTransitionOptions), stubTransition)
}

// MARK: Layer
Expand Down
Loading

0 comments on commit cc38f82

Please sign in to comment.