Skip to content

Instantly share code, notes, and snippets.

@bryanjclark
Created May 9, 2019 15:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bryanjclark/2bff6f83d85a45dd1d403a51e20bff43 to your computer and use it in GitHub Desktop.
Save bryanjclark/2bff6f83d85a45dd1d403a51e20bff43 to your computer and use it in GitHub Desktop.
Locket Photo's color-theme management code
// Locket's color-management stuff
// First up, here's the protocol for a "color theme".
// I only have two implemented; they're listed below.
public enum SemanticColor: CaseIterable {
case backgroundColor
case backgroundColorSecondary
case backgroundColorCell
case backgroundColorCellHighlighted
case tintColor
case tintColorPressed
case backgroundColorTintPressed
case tableCellSeparator
case shuffleButtonBackground
case shuffleButtonBackgroundPressed
case mapButtonBackground
case mapButtonBackgroundPressed
case textPrimary
case textSecondary
case emptyStateImage
case placeholderImage
// eg the loading-cloud on PhotoCell.
case placeholderImageForeground
case sliderFill
}
public protocol ColorThemeType {
func color(for semanticColor: SemanticColor) -> UIColor
var colors: [SemanticColor: UIColor] { get }
var barStyle: UIBarStyle { get }
var statusBarStyle: UIStatusBarStyle { get }
var themeID: ColorThemeIdentifier { get }
}
public extension ColorThemeType {
subscript(semanticColor: SemanticColor) -> UIColor? {
return colors[semanticColor]
}
}
// Used by views / view controllers that consume color themes.
public protocol ColorThemeConsuming: class {
func apply(theme: ColorThemeType)
}
public extension ColorThemeConsuming {
func reapplyCurrentColorTheme() {
self.apply(theme: ColorThemeManager.shared.currentColorTheme.value)
}
var currentColorTheme: ColorThemeType {
return ColorThemeManager.shared.currentColorTheme.value
}
}
public enum ColorThemeIdentifier: String, CaseIterable {
case light // default
case dark
public var theme: ColorThemeType {
switch self {
case .light:
return LightColorTheme.shared
case .dark:
return DarkColorTheme.shared
}
}
}
public struct LightColorTheme: ColorThemeType {
public let themeID: ColorThemeIdentifier = .light
public var colors: [SemanticColor : UIColor]
public func color(for semanticColor: SemanticColor) -> UIColor {
switch semanticColor {
case .backgroundColor:
return AppTheme.Colors.backgroundColor
case .backgroundColorSecondary:
return UIColor(hex: 0xE8EBED) // UIKit table background
case .backgroundColorCellHighlighted:
return UIColor(hex: 0xD9D9D9) // from UIKit
case .backgroundColorCell:
return AppTheme.Colors.backgroundColor
case .backgroundColorTintPressed:
return AppTheme.Colors.backgroundColorTintPressed
case .emptyStateImage:
return UIColor(hex: 0xEFEFF4) // matches backgroundColorCell
case .mapButtonBackground:
return AppTheme.Colors.mapButtonBackground
case .mapButtonBackgroundPressed:
return AppTheme.Colors.mapButtonBackgroundPressed
case .textPrimary:
return AppTheme.Colors.textPrimary
case .textSecondary:
return AppTheme.Colors.textSecondary
case .tintColor:
return AppTheme.Colors.tintColor
case .tintColorPressed:
return AppTheme.Colors.tintColorPressed
case .shuffleButtonBackground:
return AppTheme.Colors.gridHeaderButtonBackground
case .shuffleButtonBackgroundPressed:
return AppTheme.Colors.gridHeaderButtonBackgroundPressed
case .placeholderImage:
return UIColor(hex: 0xE8EBED)
case .placeholderImageForeground:
return UIColor.white
case .tableCellSeparator:
return AppTheme.Colors.lightGray
case .sliderFill:
return UIColor(hex: 0xC7C7CC) // UIKit
}
}
public let barStyle: UIBarStyle = .default
public let statusBarStyle = UIStatusBarStyle.default
public static let shared = LightColorTheme()
private init() {
// HACK omg I bet I can do this better, but let's just try this out for now.
self.colors = [:]
SemanticColor.allCases.forEach { self.colors[$0] = self.color(for: $0) }
}
}
public struct DarkColorTheme: ColorThemeType {
public let themeID: ColorThemeIdentifier = .dark
public static let shared = DarkColorTheme()
public func color(for semanticColor: SemanticColor) -> UIColor {
switch semanticColor {
case .backgroundColor:
return UIColor(hex: 0x0A0A0A)
case .backgroundColorSecondary:
return color(for: .backgroundColor)
case .backgroundColorCellHighlighted:
return UIColor(hex: 0x1A1A1A)
case .backgroundColorCell:
return UIColor(hex: 0x262626)
case .backgroundColorTintPressed:
return color(for: .backgroundColorCellHighlighted)
case .emptyStateImage:
return UIColor(hex: 0x3B3B3B)
case .mapButtonBackground:
return color(for: .backgroundColorCell)
case .mapButtonBackgroundPressed:
return color(for: .backgroundColorCellHighlighted)
case .textPrimary:
return UIColor.white
case .textSecondary:
return UIColor(hex: 0xB1B6B9)
case .tintColor:
return UIColor(hex: 0xED3B41)
case .tintColorPressed:
return UIColor(hex: 0xD62B31)
case .shuffleButtonBackground:
return UIColor(hex: 0x171515)
case .shuffleButtonBackgroundPressed:
return UIColor(hex: 0x0D0909)
case .placeholderImage:
return AppTheme.Colors.lightGray
case .placeholderImageForeground:
return color(for: .textSecondary)
case .tableCellSeparator:
return UIColor(hex: 0x383838)
case .sliderFill:
return UIColor(hex: 0x707070)
}
}
public let barStyle = UIBarStyle.black
public let statusBarStyle = UIStatusBarStyle.lightContent
public var colors: [SemanticColor : UIColor]
private init() {
// HACK omg I bet I can do this better, but let's just try this out for now.
self.colors = [:]
SemanticColor.allCases.forEach { self.colors[$0] = self.color(for: $0) }
}
}
// Second, here's the ColorThemeManager, that toggles between themes.
// (Sorry for the ReactiveCocoa, if that's confusing!)
public class ColorThemeManager {
public static let shared = ColorThemeManager()
public var currentColorThemeID: ColorThemeIdentifier {
didSet {
// If the theme isn't dark, and they're not subscribed, well then, uh, don't.
if currentColorThemeID == .dark && !SubscriptionManager.shared.isSubscribed.value {
print("Ignoring theme change because they're not subscribed.")
self.currentColorThemeID = .light
return
}
UserDefaults.standard.set(
currentColorThemeID.rawValue,
forKey: ColorThemeManager.userDefaultsThemeKey
)
guard oldValue != currentColorThemeID else { return }
print("Toggled current color theme to \(currentColorThemeID.rawValue)")
self.prepareForTransitionAnimation()
self.currentColorThemeMutable.value = currentColorThemeID.theme
self.completeTransitionAnimation()
}
}
public var currentColorTheme: Property<ColorThemeType> {
return currentColorThemeMutable.immutableView
}
private let currentColorThemeMutable: MutableProperty<ColorThemeType>
public func register(consumer: ColorThemeConsuming) {
if let nsObject = consumer as? NSObject {
self.currentColorTheme.producer.take(during: nsObject.reactive.lifetime).startWithValues { [weak nsObject] theme in
(nsObject as? ColorThemeConsuming)?.apply(theme: theme)
}
} else {
self.currentColorTheme.producer
.startWithValues { [weak consumer] (theme) in
consumer?.apply(theme: theme)
}
}
}
private static let userDefaultsThemeKey = "userDefaultsColorThemeKey"
private init() {
if
let colorThemeIdentifierString = UserDefaults.standard
.string(forKey: ColorThemeManager.userDefaultsThemeKey),
let colorThemeID = ColorThemeIdentifier(rawValue: colorThemeIdentifierString),
SubscriptionManager.shared.isSubscribed.value
{
self.currentColorThemeID = colorThemeID
} else {
self.currentColorThemeID = .light
}
self.currentColorThemeMutable = MutableProperty(self.currentColorThemeID.theme)
self.screenBrightnessChanged()
NotificationCenter.default.reactive.notifications(forName: UIScreen.brightnessDidChangeNotification).observeValues { [weak self] _ in
self?.screenBrightnessChanged()
}
// Verify that all colors are enabled for all possible cases
ColorThemeIdentifier.allCases.forEach { themeID in
let theme = themeID.theme
SemanticColor.allCases.forEach { color in
assert(theme[color] != nil)
}
}
SubscriptionManager.shared.isSubscribed.producer.uniqueValues().startWithValues { [weak self] isSubscribed in
if !isSubscribed {
self?.currentColorThemeID = .light
}
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
public func screenBrightnessChanged() {
guard SubscriptionManager.shared.isSubscribed.value else { return }
let brightness = UIScreen.main.brightness
if UserDefaultsBoolean.automaticDarkMode.enabled {
let cutoff = LocketTweaks.assign(LocketTweaks.DarkMode.automaticCutOff)
self.currentColorThemeID = (brightness < cutoff) ? .dark : .light
}
}
public weak var window: UIWindow? = nil
private var snapshot: UIView? = nil
private func prepareForTransitionAnimation() {
if
let window = self.window,
let snapshot = window.snapshotView(afterScreenUpdates: false)
{
snapshot.frame = window.bounds
window.addSubview(snapshot)
self.snapshot = snapshot
}
}
private func completeTransitionAnimation() {
UIView.animate(withDuration: 0.3, delay: 0, options: .beginFromCurrentState, animations: { [weak self] in
self?.snapshot?.alpha = 0
self?.window?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}) { [weak self] _ in
self?.snapshot?.removeFromSuperview()
self?.snapshot = nil
}
}
}
// Third, here's how I use it in my views / view controllers / table cells / etc:
internal class BasicSettingsCell: UITableViewCell, ColorThemeConsuming {
static let identifier = "BasicSettingsCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
self.textLabel?.textColor = AppTheme.Colors.textPrimary
self.detailTextLabel?.textColor = AppTheme.Colors.textSecondary
self.textLabel?.font = AppTheme.Fonts.tableCellTitleRegular
self.detailTextLabel?.font = AppTheme.Fonts.tableCellTitleRegular
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
self.title = nil
self.subtitle = nil
}
var title: String? {
didSet {
self.textLabel?.text = title
}
}
var subtitle: String? {
didSet {
self.detailTextLabel?.text = subtitle
}
}
func apply(theme: ColorThemeType) {
self.backgroundColor = theme[.backgroundColorCell]
self.selectedBackgroundView = UIView(backgroundColor: theme[.backgroundColorCellHighlighted]!)
self.textLabel?.textColor = theme[.textPrimary]
self.detailTextLabel?.textColor = theme[.textSecondary]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment