Skip to content

Commit

Permalink
MAPSNAT-2065: Color theme API with example (#2400)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksproger authored Jan 20, 2025
1 parent 2e9007d commit ac99ab1
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 18 deletions.
12 changes: 4 additions & 8 deletions Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); };
Expand Down Expand Up @@ -198,6 +199,7 @@
274D496EC7E47F63FD0D1337 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
289434058C4AB25A17655FEF /* PointClusteringExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointClusteringExample.swift; sourceTree = "<group>"; };
28CE7DA39D29A8311E4A58A4 /* 34M_17.dae */ = {isa = PBXFileReference; lastKnownFileType = text.xml.dae; path = 34M_17.dae; sourceTree = "<group>"; };
29DD4C2F0049E575A6B5BF66 /* ColorThemeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorThemeExample.swift; sourceTree = "<group>"; };
2C957F9CA07061B793C2DD4A /* Custom3DPuckExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Custom3DPuckExample.swift; sourceTree = "<group>"; };
2D91A8B64951711546335530 /* VoiceOverAccessibilityExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceOverAccessibilityExample.swift; sourceTree = "<group>"; };
2DD8B1D25297B7433F4AAF35 /* GradientLine.geojson */ = {isa = PBXFileReference; path = GradientLine.geojson; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -730,7 +733,6 @@
mainGroup = AFDB1EA82615CFDF02CE1D4D;
packageReferences = (
B50D5CC28BF0DFBA55456D89 /* XCRemoteSwiftPackageReference "Fingertips" */,
4F0A03F138FCA51E80A1893D /* XCLocalSwiftPackageReference "." */,
);
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "monochrome_lut.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
116 changes: 116 additions & 0 deletions Sources/Examples/SwiftUI Examples/ColorThemeExample.swift
Original file line number Diff line number Diff line change
@@ -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<Bool>

init(color: UIColor, isOn: Binding<Bool>) {
self.color1 = color
self.color2 = color
self.isOn = isOn
}

init(color1: UIColor, color2: UIColor, isOn: Binding<Bool>) {
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()
}
}
8 changes: 3 additions & 5 deletions Sources/Examples/SwiftUI Examples/SwiftUIRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Equatable & Encodable>(_ label: String, old: T?, new: T?, initial: T?, setter: (Any) -> Expected<NSNull, NSString>) {
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- ``TransitionOptions-struct``
- ``Rain``
- ``Snow``
- ``ColorTheme``

### Declarative Map Styling

Expand Down
1 change: 1 addition & 0 deletions Sources/MapboxMaps/Foundation/CoreAliases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
81 changes: 81 additions & 0 deletions Sources/MapboxMaps/Style/ColorTheme.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading

0 comments on commit ac99ab1

Please sign in to comment.