Skip to content

Commit ac99ab1

Browse files
authored
MAPSNAT-2065: Color theme API with example (#2400)
1 parent 2e9007d commit ac99ab1

File tree

13 files changed

+298
-18
lines changed

13 files changed

+298
-18
lines changed

Examples.xcodeproj/project.pbxproj

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
D9297596469F9B31C2350B43 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A615EFC3D6CF2A25C9864086 /* UIViewController+Extensions.swift */; platformFilters = (ios, ); };
132132
D94672F30272E31087AB5DDD /* NavigationSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC5980DD30479F30127BA71 /* NavigationSimulator.swift */; platformFilters = (ios, ); };
133133
D98624793DA36578289F02FF /* MapScrollExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65535FB9F190778001AB847A /* MapScrollExample.swift */; };
134+
DA109856E64BBD8071DF0619 /* ColorThemeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */; };
134135
DA69CB0BD9F0DDA0FD1387B0 /* DataJoinExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D0CD9C2D04EA5B12E7F84C /* DataJoinExample.swift */; platformFilters = (ios, ); };
135136
DCA54F7383085A8FD822F0BF /* GeofencingPlayground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */; };
136137
DFC64A62538E787D57B6514D /* DynamicViewAnnotationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3333EF3E0F1C789809F385AF /* DynamicViewAnnotationExample.swift */; platformFilters = (ios, ); };
@@ -198,6 +199,7 @@
198199
274D496EC7E47F63FD0D1337 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
199200
289434058C4AB25A17655FEF /* PointClusteringExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointClusteringExample.swift; sourceTree = "<group>"; };
200201
28CE7DA39D29A8311E4A58A4 /* 34M_17.dae */ = {isa = PBXFileReference; lastKnownFileType = text.xml.dae; path = 34M_17.dae; sourceTree = "<group>"; };
202+
29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorThemeExample.swift; sourceTree = "<group>"; };
201203
2C957F9CA07061B793C2DD4A /* Custom3DPuckExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Custom3DPuckExample.swift; sourceTree = "<group>"; };
202204
2D91A8B64951711546335530 /* VoiceOverAccessibilityExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceOverAccessibilityExample.swift; sourceTree = "<group>"; };
203205
2DD8B1D25297B7433F4AAF35 /* GradientLine.geojson */ = {isa = PBXFileReference; path = GradientLine.geojson; sourceTree = "<group>"; };
@@ -402,6 +404,7 @@
402404
F890746B56E20150A053B41B /* AnnotationsExample.swift */,
403405
63A3027A7DA59E090DAD25F1 /* ClipLayerExample.swift */,
404406
46CE3D9C2873C0767DD76D85 /* ClusteringExample.swift */,
407+
29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */,
405408
C61CC711054A032EE0446036 /* DynamicStylingExample.swift */,
406409
A6B06A1D70F479D8DC5C375A /* FeaturesQueryExample.swift */,
407410
7613C4E19DCD679A2620223C /* GeofencingPlayground.swift */,
@@ -730,7 +733,6 @@
730733
mainGroup = AFDB1EA82615CFDF02CE1D4D;
731734
packageReferences = (
732735
B50D5CC28BF0DFBA55456D89 /* XCRemoteSwiftPackageReference "Fingertips" */,
733-
4F0A03F138FCA51E80A1893D /* XCLocalSwiftPackageReference "." */,
734736
);
735737
projectDirPath = "";
736738
projectRoot = "";
@@ -856,6 +858,7 @@
856858
3B4862E6832F23CB115D444A /* ClipLayerExample.swift in Sources */,
857859
1DAE02D73D16E543777C2025 /* ClusteringExample.swift in Sources */,
858860
5A28C124249725578389175A /* ColorExpressionExample.swift in Sources */,
861+
DA109856E64BBD8071DF0619 /* ColorThemeExample.swift in Sources */,
859862
C664365A373267B564EC84EE /* CombineExample.swift in Sources */,
860863
215230836B6AD1040D3DA547 /* CombineLocationExample.swift in Sources */,
861864
3E515D1DD1D9CA02F3E95AA2 /* Constants.swift in Sources */,
@@ -1353,13 +1356,6 @@
13531356
};
13541357
/* End XCRemoteSwiftPackageReference section */
13551358

1356-
/* Begin XCLocalSwiftPackageReference section */
1357-
4F0A03F138FCA51E80A1893D /* XCLocalSwiftPackageReference "." */ = {
1358-
isa = XCLocalSwiftPackageReference;
1359-
relativePath = .;
1360-
};
1361-
/* End XCLocalSwiftPackageReference section */
1362-
13631359
/* Begin XCSwiftPackageProductDependency section */
13641360
0AF5F744C6369BF1FB233FB6 /* MapboxMaps */ = {
13651361
isa = XCSwiftPackageProductDependency;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "monochrome_lut.png",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
}
12+
}
7.13 KB
Loading
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import SwiftUI
2+
@_spi(Experimental) import MapboxMaps
3+
4+
struct ColorThemeExample: View {
5+
enum Theme: String {
6+
case `default`
7+
case red
8+
case monochrome
9+
}
10+
11+
@State private var theme: Theme = .red
12+
@State private var panelHeight: CGFloat = 0
13+
14+
var body: some View {
15+
Map(initialViewport: .camera(center: .init(latitude: 40.72, longitude: -73.99), zoom: 11, pitch: 45)) {
16+
switch theme {
17+
case .default:
18+
EmptyMapContent()
19+
case .red:
20+
ColorTheme(base64: redTheme)
21+
case .monochrome:
22+
ColorTheme(uiimage: monochromeTheme)
23+
}
24+
25+
/// Defines a custom layer and source to draw the border line.
26+
NYNJBorder()
27+
}
28+
.mapStyle(.streets) /// In standard style it's possible to provide custom theme using `.standard(themeData: "base64String")`
29+
.additionalSafeAreaInsets(.bottom, panelHeight)
30+
.ignoresSafeArea()
31+
.overlay(alignment: .bottom) {
32+
VStack(alignment: .center) {
33+
Group {
34+
HStack {
35+
ColorButton(color: .white, isOn: Binding(get: { theme == .default }, set: { _, _ in theme = .default }))
36+
ColorButton(color: .red, isOn: Binding(get: { theme == .red }, set: { _, _ in theme = .red }))
37+
ColorButton(color: .secondaryLabel, isOn: Binding(get: { theme == .monochrome }, set: { _, _ in theme = .monochrome }))
38+
}
39+
}
40+
.floating()
41+
}
42+
.padding(.bottom, 30)
43+
}
44+
}
45+
}
46+
47+
private struct ColorButton: View {
48+
let color1: UIColor
49+
let color2: UIColor
50+
let isOn: Binding<Bool>
51+
52+
init(color: UIColor, isOn: Binding<Bool>) {
53+
self.color1 = color
54+
self.color2 = color
55+
self.isOn = isOn
56+
}
57+
58+
init(color1: UIColor, color2: UIColor, isOn: Binding<Bool>) {
59+
self.color1 = color1
60+
self.color2 = color2
61+
self.isOn = isOn
62+
}
63+
64+
var body: some View {
65+
Button {
66+
isOn.wrappedValue.toggle()
67+
} label: {
68+
ZStack {
69+
Circle()
70+
.fill(
71+
LinearGradient(
72+
gradient: Gradient(colors: [Color(color1), Color(color2)]),
73+
startPoint: .leading,
74+
endPoint: .trailing
75+
)
76+
)
77+
Circle().strokeBorder(Color(color1.darker), lineWidth: 2)
78+
}
79+
}
80+
.opacity(isOn.wrappedValue ? 1.0 : 0.2)
81+
.frame(width: 50, height: 50)
82+
}
83+
}
84+
85+
private struct NYNJBorder: MapContent {
86+
var body: some MapContent {
87+
GeoJSONSource(id: "border")
88+
.data(.geometry(.lineString(LineString([
89+
CLLocationCoordinate2D(latitude: 40.913503418907936, longitude: -73.91912400100642),
90+
CLLocationCoordinate2D(latitude: 40.82943110786286, longitude: -73.9615887363045),
91+
CLLocationCoordinate2D(latitude: 40.75461056309348, longitude: -74.01409059085539),
92+
CLLocationCoordinate2D(latitude: 40.69522028220487, longitude: -74.02798814058939),
93+
CLLocationCoordinate2D(latitude: 40.65188756398558, longitude: -74.05655532615407),
94+
CLLocationCoordinate2D(latitude: 40.64339339389301, longitude: -74.13916853846217),
95+
]))))
96+
97+
LineLayer(id: "border", source: "border")
98+
.lineColor(.orange)
99+
.lineWidth(8)
100+
.slot(.bottom)
101+
}
102+
}
103+
104+
private let styleURL = Bundle.main.url(forResource: "fragment-realestate-NY", withExtension: "json")!
105+
private let monochromeTheme = UIImage(named: "monochrome_lut")!
106+
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"
107+
108+
private extension StandardTheme {
109+
static let red = StandardTheme(rawValue: "red")
110+
}
111+
112+
struct ColorThemeExample_Previews: PreviewProvider {
113+
static var previews: some View {
114+
StandardStyleImportExample()
115+
}
116+
}

Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ struct SwiftUIRoot: View {
3131
ExampleLink("Query Rendered Features on tap", note: "Use MapReader and MapboxMap to query rendered features.", destination: FeaturesQueryExample())
3232
#endif
3333
ExampleLink("Clustering data", note: "Display GeoJSON data with clustering using custom layers and handle interactions with them.", destination: ClusteringExample())
34-
} header: { Text("Use cases") }
35-
36-
Section {
37-
ExampleLink("GeofencingUserLocation", note: "Set geofence on user initial location.", destination: GeofencingUserLocation())
38-
ExampleLink("GeofencingPlayground", note: "Showcase isochrone API together with geofences.", destination: GeofencingPlayground())
34+
ExampleLink("Geofencing User Location", note: "Set geofence on user initial location.", destination: GeofencingUserLocation())
35+
ExampleLink("Geofencing Playground", note: "Showcase isochrone API together with geofences.", destination: GeofencingPlayground())
36+
ExampleLink("Color Themes", note: "Showcase the Color Theme API", destination: ColorThemeExample())
3937
} header: { Text("Use cases") }
4038

4139
Section {

Sources/MapboxMaps/ContentBuilders/MapContent/MapContentUniqueProperties.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ struct MapContentUniqueProperties: Decodable {
1515
var projection: StyleProjection?
1616
var snow: Snow?
1717
var rain: Rain?
18+
var colorTheme: ColorTheme?
1819
var transition: TransitionOptions?
1920
var location: LocationOptions?
21+
2022
var lights = Lights()
2123

2224
private func update<T: Equatable & Encodable>(_ label: String, old: T?, new: T?, initial: T?, setter: (Any) -> Expected<NSNull, NSString>) {
@@ -42,8 +44,8 @@ struct MapContentUniqueProperties: Decodable {
4244
update("terrain", old: old.terrain, new: terrain, initial: initial?.terrain, setter: style.setStyleTerrainForProperties(_:))
4345
update("snow", old: old.snow, new: snow, initial: initial?.snow, setter: style.setStyleSnowForProperties(_:))
4446
update("rain", old: old.rain, new: rain, initial: initial?.rain, setter: style.setStyleRainForProperties(_:))
45-
4647
lights.update(from: old.lights, style: style, initialLights: initial?.lights)
48+
update(from: old.colorTheme, to: colorTheme, style: style)
4749

4850
if old.location != location {
4951
locationManager?.options = location ?? LocationOptions()
@@ -96,6 +98,20 @@ extension MapContentUniqueProperties {
9698
}
9799
}
98100

101+
private extension MapContentUniqueProperties {
102+
func update(from oldColorTheme: ColorTheme?, to newColorTheme: ColorTheme?, style: StyleManagerProtocol) {
103+
wrapStyleDSLError {
104+
if newColorTheme != oldColorTheme {
105+
if let newColorTheme {
106+
try handleExpected { style.setStyleColorThemeFor(newColorTheme.core) }
107+
} else {
108+
style.setInitialStyleColorTheme()
109+
}
110+
}
111+
}
112+
}
113+
}
114+
99115
private extension MapContentUniqueProperties.Lights {
100116
func update(from old: Self, style: StyleManagerProtocol, initialLights: Self?) {
101117
if self != old {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- ``TransitionOptions-struct``
2929
- ``Rain``
3030
- ``Snow``
31+
- ``ColorTheme``
3132

3233
### Declarative Map Styling
3334

Sources/MapboxMaps/Foundation/CoreAliases.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ typealias CoreRenderedQueryGeometry = MapboxCoreMaps_Private.RenderedQueryGeomet
4545
typealias CoreFeaturesetFeatureId = MapboxCoreMaps_Private.FeaturesetFeatureId
4646
typealias CoreFeaturesetQueryTarget = MapboxCoreMaps_Private.FeaturesetQueryTarget
4747
typealias CoreFeaturesetDescriptor = MapboxCoreMaps_Private.FeaturesetDescriptor
48+
typealias CoreColorTheme = MapboxCoreMaps_Private.ColorTheme
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import UIKit
2+
3+
/// Map color theme.
4+
///
5+
/// A color theme modifies the global colors of a style using a LUT (lookup table) for color grading.
6+
/// 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.
7+
///
8+
/// Pass the image either as a base64-encoded string:
9+
/// ```swift
10+
/// let mapView = MapView()
11+
/// mapView.mapboxMap.setMapStyleContent {
12+
/// ColorTheme(base64: "base64EncodedImage")
13+
/// }
14+
/// ```
15+
///
16+
/// Or as a `UIImage` for easier asset integration:
17+
/// ```swift
18+
/// let mapView = MapView()
19+
/// let lutImage = UIImage(named: "monochrome_lut")!
20+
/// mapView.mapboxMap.setMapStyleContent {
21+
/// ColorTheme(uiimage: lutImage)
22+
/// }
23+
/// ```
24+
///
25+
/// Note: Each style can have only one `ColorTheme`. Setting a new theme overwrites the previous one.
26+
/// Additional information [Mapbox Style Specification](https://docs.mapbox.com/style-spec/reference/root/#color-theme)
27+
@_documentation(visibility: public)
28+
@_spi(Experimental)
29+
public struct ColorTheme: Equatable {
30+
var base64: StylePropertyValue?
31+
var uiimage: UIImage?
32+
33+
/// Creates a ``ColorTheme`` using base64 encoded LUT image.
34+
///
35+
/// - Important: Image height must be less or equal to 32 pixels and width of the image should be equal to the height squared.
36+
/// - Parameters:
37+
/// - base64: base64 encoded LUT image.
38+
public init(base64: String) {
39+
self.base64 = StylePropertyValue(value: base64, kind: .constant)
40+
self.uiimage = nil
41+
}
42+
43+
/// Creates a ``ColorTheme`` using base64 encoded LUT image.
44+
///
45+
/// - Important: Image height must be less or equal to 32 pixels and width of the image should be equal to the height squared.
46+
/// - Parameters:
47+
/// - base64: base64 encoded LUT image.
48+
public init(base64: Exp) {
49+
self.base64 = base64.asCore.flatMap { StylePropertyValue(value: $0, kind: .expression) }
50+
self.uiimage = nil
51+
}
52+
53+
/// Creates a ``ColorTheme`` using base64 encoded LUT image.
54+
///
55+
/// - Important: Image height must be less or equal to 32 pixels and width of the image should be equal to the height squared.
56+
/// - Parameters:
57+
/// - uiimage: UIImage instance which represents color grading LUT.
58+
public init(uiimage: UIImage) {
59+
self.uiimage = uiimage
60+
self.base64 = nil
61+
}
62+
}
63+
64+
@available(iOS 13.0, *)
65+
extension ColorTheme: MapStyleContent, PrimitiveMapContent {
66+
func visit(_ node: MapContentNode) {
67+
node.mount(MountedUniqueProperty(keyPath: \.colorTheme, value: self))
68+
}
69+
}
70+
71+
extension ColorTheme {
72+
var core: CoreColorTheme? {
73+
if let base64 {
74+
return .fromStylePropertyValue(base64)
75+
} else if let uiimage, let coreImage = CoreMapsImage(uiImage: uiimage) {
76+
return .fromImage(coreImage)
77+
} else {
78+
return nil
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)