Last active
February 12, 2022 17:11
-
-
Save lucamegh/593193c44db3824a7500ffb6c7cd6203 to your computer and use it in GitHub Desktop.
ThemeManager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: - Implementation | |
@dynamicMemberLookup | |
public class ThemeManager<Theme> { | |
public var theme: Theme { | |
didSet { | |
for (_, handler) in observations { | |
handler(theme) | |
} | |
} | |
} | |
private var observations = [UUID: (Theme) -> Void]() | |
public init(theme: Theme) { | |
self.theme = theme | |
} | |
public subscript<Root: AnyObject, Value>(dynamicMember keyPath: KeyPath<Theme, Value>) -> Assignable<Root, Value> { | |
Assignable { [weak self] objectKeyPath, object in | |
self?.addObserver(object) { object, theme in | |
object[keyPath: objectKeyPath] = theme[keyPath: keyPath] | |
} | |
} | |
} | |
public subscript<Root: AnyObject, Value>(dynamicMember keyPath: KeyPath<Theme, Value>) -> Assignable<Root, Value?> { | |
Assignable { [weak self] objectKeyPath, object in | |
self?.addObserver(object) { object, theme in | |
object[keyPath: objectKeyPath] = theme[keyPath: keyPath] | |
} | |
} | |
} | |
public subscript<Value>(dynamicMember keyPath: KeyPath<Theme, Value>) -> Observable<Value> { | |
Observable { [weak self] observer, handler in | |
self?.addObserver(observer) { _, theme in | |
handler(theme[keyPath: keyPath]) | |
} | |
} | |
} | |
private func addObserver<Observer: AnyObject>( | |
_ observer: Observer, | |
with handler: @escaping (Observer, Theme) -> Void | |
) { | |
handler(observer, theme) | |
let id = UUID() | |
observations[id] = { [weak self, weak observer] theme in | |
guard let observer = observer else { | |
self?.observations[id] = nil | |
return | |
} | |
handler(observer, theme) | |
} | |
} | |
} | |
public struct Assignable<Root, Value> { | |
private let assign: (_ keyPath: ReferenceWritableKeyPath<Root, Value>, _ object: Root) -> Void | |
init(assign: @escaping (_ keyPath: ReferenceWritableKeyPath<Root, Value>, _ object: Root) -> Void) { | |
self.assign = assign | |
} | |
public func assign(to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) { | |
assign(keyPath, object) | |
} | |
} | |
public struct Observable<Value> { | |
private let addObserver: (_ observer: AnyObject, _ handler: @escaping (Value) -> Void) -> Void | |
init(addObserver: @escaping (_ observer: AnyObject, _ handler: @escaping (Value) -> Void) -> Void) { | |
self.addObserver = addObserver | |
} | |
public func addObserver(_ observer: AnyObject, with handler: @escaping (Value) -> Void) { | |
addObserver(observer, handler) | |
} | |
} | |
// MARK: - Usage | |
import UIKit | |
struct AppTheme: Equatable { | |
var preferredStatusBarStyle: UIStatusBarStyle | |
var label: UIColor | |
var background: UIColor | |
} | |
extension AppTheme { | |
static let standard = AppTheme( | |
preferredStatusBarStyle: .default, | |
label: .label, | |
background: .systemBackground | |
) | |
static let light = AppTheme( | |
preferredStatusBarStyle: .darkContent, | |
label: .black, | |
background: .white | |
) | |
static let dark = AppTheme( | |
preferredStatusBarStyle: .lightContent, | |
label: .white, | |
background: .black | |
) | |
static let aqua = AppTheme( | |
preferredStatusBarStyle: .lightContent, | |
label: .white, | |
background: .systemTeal | |
) | |
} | |
extension ThemeManager where Theme == AppTheme { | |
static let shared = ThemeManager(theme: .standard) | |
} | |
class ViewController: UIViewController { | |
override var preferredStatusBarStyle: UIStatusBarStyle { | |
themeManager.theme.preferredStatusBarStyle | |
} | |
private let label = UILabel() | |
private let themeManager: ThemeManager | |
init(themeManager: ThemeManager = .shared) { | |
self.themeManager = themeManager | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
themeManager.label.assign(to: \.textColor, on: label) | |
themeManager.background.assign(to: \.backgroundColor, on: view) | |
themeManager.preferredStatusBarStyle.addObserver(self) { [weak self] _ in | |
self?.setNeedsStatusBarAppearanceUpdate() | |
} | |
} | |
override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { | |
UIView.transition( | |
with: view, | |
duration: 0.25, | |
options: [ | |
.beginFromCurrentState, | |
.transitionCrossDissolve, | |
.curveEaseInOut | |
] | |
) { | |
self.applyRandomTheme() | |
} | |
} | |
private func applyRandomTheme() { | |
let themes = [.standard, .light, .dark, .aqua].filter { $0 != themeManager.theme } | |
themeManager.theme = themes.randomElement()! | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment