From ac99ab153c698970d8db7bce84e7b385dbb773ad Mon Sep 17 00:00:00 2001 From: Aleksei Sapitskii <45671572+aleksproger@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:58:52 +0200 Subject: [PATCH] MAPSNAT-2065: Color theme API with example (#2400) --- Examples.xcodeproj/project.pbxproj | 12 +- .../monochrome_lut.imageset/Contents.json | 12 ++ .../monochrome_lut.png | Bin 0 -> 7302 bytes .../SwiftUI Examples/ColorThemeExample.swift | 116 ++++++++++++++++++ .../SwiftUI Examples/SwiftUIRoot.swift | 8 +- .../MapContentUniqueProperties.swift | 18 ++- .../Documentation.docc/API Catalogs/Style.md | 1 + .../MapboxMaps/Foundation/CoreAliases.swift | 1 + Sources/MapboxMaps/Style/ColorTheme.swift | 81 ++++++++++++ Sources/MapboxMaps/Style/StyleManager.swift | 46 ++++++- .../Style/StyleManagerProtocol.swift | 3 + .../Foundation/Mocks/MockStyleManager.swift | 12 ++ .../breakage_allowlist.txt | 6 + 13 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 Sources/Examples/Assets.xcassets/monochrome_lut.imageset/Contents.json create mode 100644 Sources/Examples/Assets.xcassets/monochrome_lut.imageset/monochrome_lut.png create mode 100644 Sources/Examples/SwiftUI Examples/ColorThemeExample.swift create mode 100644 Sources/MapboxMaps/Style/ColorTheme.swift diff --git a/Examples.xcodeproj/project.pbxproj b/Examples.xcodeproj/project.pbxproj index 9b200993b1a7..c580f73d8aa4 100644 --- a/Examples.xcodeproj/project.pbxproj +++ b/Examples.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ D9297596469F9B31C2350B43 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A615EFC3D6CF2A25C9864086 /* UIViewController+Extensions.swift */; platformFilters = (ios, ); }; D94672F30272E31087AB5DDD /* NavigationSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC5980DD30479F30127BA71 /* NavigationSimulator.swift */; platformFilters = (ios, ); }; D98624793DA36578289F02FF /* MapScrollExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65535FB9F190778001AB847A /* MapScrollExample.swift */; }; + DA109856E64BBD8071DF0619 /* ColorThemeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */; }; DA69CB0BD9F0DDA0FD1387B0 /* DataJoinExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D0CD9C2D04EA5B12E7F84C /* DataJoinExample.swift */; platformFilters = (ios, ); }; DCA54F7383085A8FD822F0BF /* GeofencingPlayground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */; }; DFC64A62538E787D57B6514D /* DynamicViewAnnotationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333EF3E0F1C789809F385AF /* DynamicViewAnnotationExample.swift */; platformFilters = (ios, ); }; @@ -198,6 +199,7 @@ 274D496EC7E47F63FD0D1337 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 289434058C4AB25A17655FEF /* PointClusteringExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointClusteringExample.swift; sourceTree = ""; }; 28CE7DA39D29A8311E4A58A4 /* 34M_17.dae */ = {isa = PBXFileReference; lastKnownFileType = text.xml.dae; path = 34M_17.dae; sourceTree = ""; }; + 29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorThemeExample.swift; sourceTree = ""; }; 2C957F9CA07061B793C2DD4A /* Custom3DPuckExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Custom3DPuckExample.swift; sourceTree = ""; }; 2D91A8B64951711546335530 /* VoiceOverAccessibilityExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceOverAccessibilityExample.swift; sourceTree = ""; }; 2DD8B1D25297B7433F4AAF35 /* GradientLine.geojson */ = {isa = PBXFileReference; path = GradientLine.geojson; sourceTree = ""; }; @@ -402,6 +404,7 @@ F890746B56E20150A053B41B /* AnnotationsExample.swift */, 63A3027A7DA59E090DAD25F1 /* ClipLayerExample.swift */, 46CE3D9C2873C0767DD76D85 /* ClusteringExample.swift */, + 29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */, C61CC711054A032EE0446036 /* DynamicStylingExample.swift */, A6B06A1D70F479D8DC5C375A /* FeaturesQueryExample.swift */, 7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */, @@ -730,7 +733,6 @@ mainGroup = AFDB1EA82615CFDF02CE1D4D; packageReferences = ( B50D5CC28BF0DFBA55456D89 /* XCRemoteSwiftPackageReference "Fingertips" */, - 4F0A03F138FCA51E80A1893D /* XCLocalSwiftPackageReference "." */, ); projectDirPath = ""; projectRoot = ""; @@ -856,6 +858,7 @@ 3B4862E6832F23CB115D444A /* ClipLayerExample.swift in Sources */, 1DAE02D73D16E543777C2025 /* ClusteringExample.swift in Sources */, 5A28C124249725578389175A /* ColorExpressionExample.swift in Sources */, + DA109856E64BBD8071DF0619 /* ColorThemeExample.swift in Sources */, C664365A373267B564EC84EE /* CombineExample.swift in Sources */, 215230836B6AD1040D3DA547 /* CombineLocationExample.swift in Sources */, 3E515D1DD1D9CA02F3E95AA2 /* Constants.swift in Sources */, @@ -1353,13 +1356,6 @@ }; /* End XCRemoteSwiftPackageReference section */ -/* Begin XCLocalSwiftPackageReference section */ - 4F0A03F138FCA51E80A1893D /* XCLocalSwiftPackageReference "." */ = { - isa = XCLocalSwiftPackageReference; - relativePath = .; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ 0AF5F744C6369BF1FB233FB6 /* MapboxMaps */ = { isa = XCSwiftPackageProductDependency; diff --git a/Sources/Examples/Assets.xcassets/monochrome_lut.imageset/Contents.json b/Sources/Examples/Assets.xcassets/monochrome_lut.imageset/Contents.json new file mode 100644 index 000000000000..2bfefeaf6193 --- /dev/null +++ b/Sources/Examples/Assets.xcassets/monochrome_lut.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "monochrome_lut.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Examples/Assets.xcassets/monochrome_lut.imageset/monochrome_lut.png b/Sources/Examples/Assets.xcassets/monochrome_lut.imageset/monochrome_lut.png new file mode 100644 index 0000000000000000000000000000000000000000..c62191b2dfd83ae0d57b338afba3da4b6d79262a GIT binary patch literal 7302 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 literal 0 HcmV?d00001 diff --git a/Sources/Examples/SwiftUI Examples/ColorThemeExample.swift b/Sources/Examples/SwiftUI Examples/ColorThemeExample.swift new file mode 100644 index 000000000000..8be5bf3b5fbe --- /dev/null +++ b/Sources/Examples/SwiftUI Examples/ColorThemeExample.swift @@ -0,0 +1,116 @@ +import SwiftUI +@_spi(Experimental) import MapboxMaps + +struct ColorThemeExample: View { + enum Theme: String { + case `default` + case red + case monochrome + } + + @State private var theme: Theme = .red + @State private var panelHeight: CGFloat = 0 + + var body: some View { + Map(initialViewport: .camera(center: .init(latitude: 40.72, longitude: -73.99), zoom: 11, pitch: 45)) { + switch theme { + case .default: + EmptyMapContent() + case .red: + ColorTheme(base64: redTheme) + case .monochrome: + ColorTheme(uiimage: monochromeTheme) + } + + /// Defines a custom layer and source to draw the border line. + NYNJBorder() + } + .mapStyle(.streets) /// In standard style it's possible to provide custom theme using `.standard(themeData: "base64String")` + .additionalSafeAreaInsets(.bottom, panelHeight) + .ignoresSafeArea() + .overlay(alignment: .bottom) { + VStack(alignment: .center) { + Group { + HStack { + ColorButton(color: .white, isOn: Binding(get: { theme == .default }, set: { _, _ in theme = .default })) + ColorButton(color: .red, isOn: Binding(get: { theme == .red }, set: { _, _ in theme = .red })) + ColorButton(color: .secondaryLabel, isOn: Binding(get: { theme == .monochrome }, set: { _, _ in theme = .monochrome })) + } + } + .floating() + } + .padding(.bottom, 30) + } + } +} + +private struct ColorButton: View { + let color1: UIColor + let color2: UIColor + let isOn: Binding + + init(color: UIColor, isOn: Binding) { + self.color1 = color + self.color2 = color + self.isOn = isOn + } + + init(color1: UIColor, color2: UIColor, isOn: Binding) { + self.color1 = color1 + self.color2 = color2 + self.isOn = isOn + } + + var body: some View { + Button { + isOn.wrappedValue.toggle() + } label: { + ZStack { + Circle() + .fill( + LinearGradient( + gradient: Gradient(colors: [Color(color1), Color(color2)]), + startPoint: .leading, + endPoint: .trailing + ) + ) + Circle().strokeBorder(Color(color1.darker), lineWidth: 2) + } + } + .opacity(isOn.wrappedValue ? 1.0 : 0.2) + .frame(width: 50, height: 50) + } +} + +private struct NYNJBorder: MapContent { + var body: some MapContent { + GeoJSONSource(id: "border") + .data(.geometry(.lineString(LineString([ + CLLocationCoordinate2D(latitude: 40.913503418907936, longitude: -73.91912400100642), + CLLocationCoordinate2D(latitude: 40.82943110786286, longitude: -73.9615887363045), + CLLocationCoordinate2D(latitude: 40.75461056309348, longitude: -74.01409059085539), + CLLocationCoordinate2D(latitude: 40.69522028220487, longitude: -74.02798814058939), + CLLocationCoordinate2D(latitude: 40.65188756398558, longitude: -74.05655532615407), + CLLocationCoordinate2D(latitude: 40.64339339389301, longitude: -74.13916853846217), + ])))) + + LineLayer(id: "border", source: "border") + .lineColor(.orange) + .lineWidth(8) + .slot(.bottom) + } +} + +private let styleURL = Bundle.main.url(forResource: "fragment-realestate-NY", withExtension: "json")! +private let monochromeTheme = UIImage(named: "monochrome_lut")! +private let redTheme = "iVBORw0KGgoAAAANSUhEUgAABAAAAAAgCAYAAACM/gqmAAAAAXNSR0IArs4c6QAABSFJREFUeF7t3cFO40AQAFHnBv//wSAEEgmJPeUDsid5h9VqtcMiZsfdPdXVzmVZlo+3ZVm+fr3//L7257Lm778x+prL1ff0/b//H+z/4/M4OkuP/n70Nc7f+nnb+yzb//sY6vxt5xXPn+dP/aH+GsXJekb25izxR/ypZ6ucUefv9g4z2jPP3/HPHwAAgABAABgACIACkAAsAL1SD4yKWQAUAHUBdAG8buKNYoYL8PEX4FcHQAAAAAAAAAAAAAAAAAAAAAAA8LAeGF1mABAABAABQACQbZP7+hk5AwACAAAAAAAAAAAAAAAAAAAAAAAA4EE9AICMx4QBAAAAAAAANgvJsxGQV1dA/PxmMEtxU9YoABQACoC5CgDxX/wvsb2sEf/Ff/Ff/N96l5n73+/5YAB4CeBqx2VvMqXgUfD2npkzBCAXEBeQcrkoa5x/FxAXEBcQF5A2Wy3/t32qNYr8I//Mln+MABgBMAJgBMAIgBEAIwBGAIwAGAEwAmAE4K4eAGCNQIw+qQ0AmQ+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/6gEABAB5RgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN/UAAPKcAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgEFNODICRtDkDO/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOhvlPUWem+h9xKQ+V4CUt9wO6KZnn/Pv+ff8z/bW5DFP59CUnJbWSP+iX/iX78znqED/urxnwHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADoNMcHUAdQAQcAUfAe8xEwH0O86t3IPz8OvClu17WqD/UH+oP9cf1Gdia01d/LQsDgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABkCnSQwABgACj8Aj8D1mItAMAB1wHfDS3S5r5F/5V/6Vf3XAW12h/mIArHY89iZTAAQA2XtmBKAWqOslyf4rgBXACmAFcIur8k/bJ/mnQTr5V/6Vf+fKv0YAjAAYATACYATACIARACMARgCMABgBMAJgBMAIgBEAIwCdZuiA64AjwAgwAtxjpg6cDlztLlLA7/Pr1gueyr56/jx/5ZzUNeof9Y/6R/0zk4HGAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHQaQ4DgAGAgCPgCHiPmTqQOpC1u8gAYACMjAf5V/6Vf+XfmTrQ8l97v8Z/5X8GAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADIBO0xgADAAdCB0IHYgeMxkADAAdkGM7IPbf/pfuWlmj/lH/qH/UPzMZGAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAJ3mMAAYAAg4Ao6A95jJAGAA6EDrQJfuclkj/8q/8q/8O1MHWv47Nv8xABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAB0msYAYADoQOhA6ED0mMkAYADogBzbAbH/9r/YFWWN+kf9o/5R/8xkYDAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABgAHSawwBgACDgCDgC3mMmA4ABoAOtA126y2WN/Cv/yr/y70wdaPnv2PzHAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHQaRoDgAGgA6EDoQPRYyYDgAGgA3JsB8T+2/9iV5Q16h/1j/pH/TOTgcEAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAPgyQ2AT4NBIB3ew5dkAAAAAElFTkSuQmCC" + +private extension StandardTheme { + static let red = StandardTheme(rawValue: "red") +} + +struct ColorThemeExample_Previews: PreviewProvider { + static var previews: some View { + StandardStyleImportExample() + } +} diff --git a/Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift b/Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift index dda69219360c..f7c1bf1825c3 100644 --- a/Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift +++ b/Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift @@ -31,11 +31,9 @@ struct SwiftUIRoot: View { ExampleLink("Query Rendered Features on tap", note: "Use MapReader and MapboxMap to query rendered features.", destination: FeaturesQueryExample()) #endif ExampleLink("Clustering data", note: "Display GeoJSON data with clustering using custom layers and handle interactions with them.", destination: ClusteringExample()) - } header: { Text("Use cases") } - - Section { - ExampleLink("GeofencingUserLocation", note: "Set geofence on user initial location.", destination: GeofencingUserLocation()) - ExampleLink("GeofencingPlayground", note: "Showcase isochrone API together with geofences.", destination: GeofencingPlayground()) + ExampleLink("Geofencing User Location", note: "Set geofence on user initial location.", destination: GeofencingUserLocation()) + ExampleLink("Geofencing Playground", note: "Showcase isochrone API together with geofences.", destination: GeofencingPlayground()) + ExampleLink("Color Themes", note: "Showcase the Color Theme API", destination: ColorThemeExample()) } header: { Text("Use cases") } Section { diff --git a/Sources/MapboxMaps/ContentBuilders/MapContent/MapContentUniqueProperties.swift b/Sources/MapboxMaps/ContentBuilders/MapContent/MapContentUniqueProperties.swift index b2ee493c6816..6a3c70a9ece3 100644 --- a/Sources/MapboxMaps/ContentBuilders/MapContent/MapContentUniqueProperties.swift +++ b/Sources/MapboxMaps/ContentBuilders/MapContent/MapContentUniqueProperties.swift @@ -15,8 +15,10 @@ struct MapContentUniqueProperties: Decodable { var projection: StyleProjection? var snow: Snow? var rain: Rain? + var colorTheme: ColorTheme? var transition: TransitionOptions? var location: LocationOptions? + var lights = Lights() private func update(_ label: String, old: T?, new: T?, initial: T?, setter: (Any) -> Expected) { @@ -42,8 +44,8 @@ struct MapContentUniqueProperties: Decodable { update("terrain", old: old.terrain, new: terrain, initial: initial?.terrain, setter: style.setStyleTerrainForProperties(_:)) update("snow", old: old.snow, new: snow, initial: initial?.snow, setter: style.setStyleSnowForProperties(_:)) update("rain", old: old.rain, new: rain, initial: initial?.rain, setter: style.setStyleRainForProperties(_:)) - lights.update(from: old.lights, style: style, initialLights: initial?.lights) + update(from: old.colorTheme, to: colorTheme, style: style) if old.location != location { locationManager?.options = location ?? LocationOptions() @@ -96,6 +98,20 @@ extension MapContentUniqueProperties { } } +private extension MapContentUniqueProperties { + func update(from oldColorTheme: ColorTheme?, to newColorTheme: ColorTheme?, style: StyleManagerProtocol) { + wrapStyleDSLError { + if newColorTheme != oldColorTheme { + if let newColorTheme { + try handleExpected { style.setStyleColorThemeFor(newColorTheme.core) } + } else { + style.setInitialStyleColorTheme() + } + } + } + } +} + private extension MapContentUniqueProperties.Lights { func update(from old: Self, style: StyleManagerProtocol, initialLights: Self?) { if self != old { diff --git a/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md b/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md index 074b55f9d6b7..ba36f300946d 100644 --- a/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md +++ b/Sources/MapboxMaps/Documentation.docc/API Catalogs/Style.md @@ -28,6 +28,7 @@ - ``TransitionOptions-struct`` - ``Rain`` - ``Snow`` +- ``ColorTheme`` ### Declarative Map Styling diff --git a/Sources/MapboxMaps/Foundation/CoreAliases.swift b/Sources/MapboxMaps/Foundation/CoreAliases.swift index ac90f87e6143..f609721066f1 100644 --- a/Sources/MapboxMaps/Foundation/CoreAliases.swift +++ b/Sources/MapboxMaps/Foundation/CoreAliases.swift @@ -45,3 +45,4 @@ typealias CoreRenderedQueryGeometry = MapboxCoreMaps_Private.RenderedQueryGeomet typealias CoreFeaturesetFeatureId = MapboxCoreMaps_Private.FeaturesetFeatureId typealias CoreFeaturesetQueryTarget = MapboxCoreMaps_Private.FeaturesetQueryTarget typealias CoreFeaturesetDescriptor = MapboxCoreMaps_Private.FeaturesetDescriptor +typealias CoreColorTheme = MapboxCoreMaps_Private.ColorTheme diff --git a/Sources/MapboxMaps/Style/ColorTheme.swift b/Sources/MapboxMaps/Style/ColorTheme.swift new file mode 100644 index 000000000000..000014a239eb --- /dev/null +++ b/Sources/MapboxMaps/Style/ColorTheme.swift @@ -0,0 +1,81 @@ +import UIKit + +/// Map color theme. +/// +/// A color theme modifies the global colors of a style using a LUT (lookup table) for color grading. +/// To use a custom color theme, provide a LUT image. The image must be ≤32 pixels in height and have a width equal to the square of its height. +/// +/// Pass the image either as a base64-encoded string: +/// ```swift +/// let mapView = MapView() +/// mapView.mapboxMap.setMapStyleContent { +/// ColorTheme(base64: "base64EncodedImage") +/// } +/// ``` +/// +/// Or as a `UIImage` for easier asset integration: +/// ```swift +/// let mapView = MapView() +/// let lutImage = UIImage(named: "monochrome_lut")! +/// mapView.mapboxMap.setMapStyleContent { +/// ColorTheme(uiimage: lutImage) +/// } +/// ``` +/// +/// Note: Each style can have only one `ColorTheme`. Setting a new theme overwrites the previous one. +/// Additional information [Mapbox Style Specification](https://docs.mapbox.com/style-spec/reference/root/#color-theme) +@_documentation(visibility: public) +@_spi(Experimental) +public struct ColorTheme: Equatable { + var base64: StylePropertyValue? + var uiimage: UIImage? + + /// Creates a ``ColorTheme`` using base64 encoded LUT image. + /// + /// - Important: Image height must be less or equal to 32 pixels and width of the image should be equal to the height squared. + /// - Parameters: + /// - base64: base64 encoded LUT image. + public init(base64: String) { + self.base64 = StylePropertyValue(value: base64, kind: .constant) + self.uiimage = nil + } + + /// Creates a ``ColorTheme`` using base64 encoded LUT image. + /// + /// - Important: Image height must be less or equal to 32 pixels and width of the image should be equal to the height squared. + /// - Parameters: + /// - base64: base64 encoded LUT image. + public init(base64: Exp) { + self.base64 = base64.asCore.flatMap { StylePropertyValue(value: $0, kind: .expression) } + self.uiimage = nil + } + + /// Creates a ``ColorTheme`` using base64 encoded LUT image. + /// + /// - Important: Image height must be less or equal to 32 pixels and width of the image should be equal to the height squared. + /// - Parameters: + /// - uiimage: UIImage instance which represents color grading LUT. + public init(uiimage: UIImage) { + self.uiimage = uiimage + self.base64 = nil + } +} + +@available(iOS 13.0, *) +extension ColorTheme: MapStyleContent, PrimitiveMapContent { + func visit(_ node: MapContentNode) { + node.mount(MountedUniqueProperty(keyPath: \.colorTheme, value: self)) + } +} + +extension ColorTheme { + var core: CoreColorTheme? { + if let base64 { + return .fromStylePropertyValue(base64) + } else if let uiimage, let coreImage = CoreMapsImage(uiImage: uiimage) { + return .fromImage(coreImage) + } else { + return nil + } + } +} diff --git a/Sources/MapboxMaps/Style/StyleManager.swift b/Sources/MapboxMaps/Style/StyleManager.swift index 76f17693240e..86456d9de183 100644 --- a/Sources/MapboxMaps/Style/StyleManager.swift +++ b/Sources/MapboxMaps/Style/StyleManager.swift @@ -1619,7 +1619,9 @@ extension StyleManager { extension StyleManager { /// Set the snow parameters to animate snowfall. /// ``Snow`` object can be used to set the snow parameters. - @_spi(Experimental) public func setSnow(_ snow: Snow) throws { + @_spi(Experimental) + @_documentation(visibility: public) + public func setSnow(_ snow: Snow) throws { let snowDictionary = try snow.allStyleProperties() let expected = styleManager.setStyleSnowForProperties(snowDictionary) @@ -1629,7 +1631,9 @@ extension StyleManager { } /// Remove snow effect from the style. - @_spi(Experimental) public func removeSnow() throws { + @_spi(Experimental) + @_documentation(visibility: public) + public func removeSnow() throws { let expected = styleManager.setStyleSnowForProperties(NSNull()) if expected.isError() { @@ -1639,7 +1643,9 @@ extension StyleManager { /// Set the rain parameters to animate rain drops. /// ``Rain`` object can be used to set the rain parameters. - @_spi(Experimental) public func setRain(_ rain: Rain) throws { + @_spi(Experimental) + @_documentation(visibility: public) + public func setRain(_ rain: Rain) throws { let rainDictionary = try rain.allStyleProperties() let expected = styleManager.setStyleRainForProperties(rainDictionary) @@ -1649,7 +1655,9 @@ extension StyleManager { } /// Remove rain effect from the style. - @_spi(Experimental) public func removeRain() throws { + @_spi(Experimental) + @_documentation(visibility: public) + public func removeRain() throws { let expected = styleManager.setStyleRainForProperties(NSNull()) if expected.isError() { @@ -1657,6 +1665,36 @@ extension StyleManager { } } + /// Set color theme for style. + /// ``ColorTheme`` is unique per style and setting a new one will effectively overwrite any previous theme. + /// - Parameters: + /// - colorTheme: Color theme to apply on the style. + /// - Throws: ``StyleError`` if the color theme could not be applied. + @_spi(Experimental) + @_documentation(visibility: public) + public func setColorTheme(_ colorTheme: ColorTheme) throws { + guard let coreTheme = colorTheme.core else { + throw StyleError(message: "Cannot construct UIImage object.") + } + + let expected = styleManager.setStyleColorThemeFor(coreTheme) + + if expected.isError() { + throw StyleError(message: expected.error as String) + } + } + + /// Remove color theme from the style. + /// - Throws: ``StyleError`` if the color theme could not be removed. + @_spi(Experimental) + @_documentation(visibility: public) + public func removeColorTheme() throws { + let expected = styleManager.setStyleColorThemeFor(nil) + + if expected.isError() { + throw StyleError(message: expected.error as String) + } + } } // MARK: - Featuresets diff --git a/Sources/MapboxMaps/Style/StyleManagerProtocol.swift b/Sources/MapboxMaps/Style/StyleManagerProtocol.swift index ae317dfc6ff1..240dc7cab53d 100644 --- a/Sources/MapboxMaps/Style/StyleManagerProtocol.swift +++ b/Sources/MapboxMaps/Style/StyleManagerProtocol.swift @@ -211,6 +211,9 @@ internal protocol StyleManagerProtocol { featureIds: [String]) -> Expected func getStyleFeaturesets() -> [CoreFeaturesetDescriptor] + + func setStyleColorThemeFor(_ colorTheme: CoreColorTheme?) -> Expected + func setInitialStyleColorTheme() } // MARK: Conformance diff --git a/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift b/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift index 629f9098c994..3a51317bf810 100644 --- a/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift +++ b/Tests/MapboxMapsTests/Foundation/Mocks/MockStyleManager.swift @@ -3,6 +3,18 @@ import Foundation @_implementationOnly import MapboxCommon_Private class MockStyleManager: StyleManagerProtocol { + let setStyleColorThemeForStub = Stub>( + defaultReturnValue: .init(value: NSNull()) + ) + func setStyleColorThemeFor(_ colorTheme: CoreColorTheme?) -> Expected { + setStyleColorThemeForStub(with: colorTheme) + } + + let setInitialStyleColorThemeStub = Stub() + func setInitialStyleColorTheme() { + setInitialStyleColorThemeStub() + } + let getFeaturesetsStub = Stub(defaultReturnValue: []) func getStyleFeaturesets() -> [CoreFeaturesetDescriptor] { getFeaturesetsStub.call() diff --git a/scripts/api-compatibility-check/breakage_allowlist.txt b/scripts/api-compatibility-check/breakage_allowlist.txt index 455a50b6fde1..f25273f8c284 100644 --- a/scripts/api-compatibility-check/breakage_allowlist.txt +++ b/scripts/api-compatibility-check/breakage_allowlist.txt @@ -2057,3 +2057,9 @@ Func MapStyle.standardSatellite(lightPreset:font:showPointOfInterestLabels:showT # Add ViewAnnotationOptions.min/maxZoom to the constructor Constructor ViewAnnotationOptions.init(annotatedFeature:width:height:allowOverlap:allowOverlapWithPuck:visible:selected:variableAnchors:ignoreCameraPadding:) has been removed + +# Add missed @_documentation annotations +Func StyleManager.removeRain() is now with @_documentation +Func StyleManager.removeSnow() is now with @_documentation +Func StyleManager.setRain(_:) is now with @_documentation +Func StyleManager.setSnow(_:) is now with @_documentation \ No newline at end of file