SampleApp/Theme.swift (83 lines of code) (raw):

// // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import Cyborg final class Theme: ColorProvider, Cyborg.ThemeProviding { func colorFromTheme(named name: String) -> UIColor { colorForKey(name) } } final class Resources: ColorProvider, ResourceProviding { func colorFromResources(named name: String) -> UIColor { colorForKey(name) } } class ColorProvider: Codable { private(set) var colors: [NamedColor] = [] var mappedColors: [String: NamedColor] = [:] { didSet { colors = mappedColors.map { $0.value } } } func removeColor(at index: Int) { mappedColors[colors[index].name] = nil } func colorForKey(_ key: String) -> UIColor { if let color = mappedColors[key] { return UIColor(rgba: color.hex) } else { return .black } } } struct NamedColor: Codable { var name: String var hex: Int64 } extension UIColor { convenience init(rgba value: Int64) { let alpha = CGFloat(value >> 24 & 0xff) / 255.0 let red = CGFloat(value >> 16 & 0xff) / 255.0 let green = CGFloat(value >> 8 & 0xff) / 255.0 let blue = CGFloat(value & 0xff) / 255.0 self.init(red: red, green: green, blue: blue, alpha: alpha) } } class Preferences { enum Key: String { case theme case resources } let backing = UserDefaults() var theme: Theme { didSet { backing.save(object: theme, key: .theme) } } var resources: Resources { didSet { backing.save(object: resources, key: .resources) } } init() { theme = backing.object(for: .theme) ?? Theme() resources = backing.object(for: .resources) ?? Resources() } } extension UserDefaults { // TODO: bubble up errors func object<T: Codable>(for key: Preferences.Key) -> T? { let decoder = JSONDecoder() if let data = object(forKey: key.rawValue) as? Data, let object = try? decoder.decode(T.self, from: data){ return object } else { return nil } } func save<T: Codable>(object: T, key: Preferences.Key) { let encoder = JSONEncoder() if let object = try? encoder.encode(object) { set(object, forKey: key.rawValue) } else { assertionFailure() } } }