Skip to content

Instantly share code, notes, and snippets.

@lucamegh
Last active February 12, 2022 17:11
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 lucamegh/593193c44db3824a7500ffb6c7cd6203 to your computer and use it in GitHub Desktop.
Save lucamegh/593193c44db3824a7500ffb6c7cd6203 to your computer and use it in GitHub Desktop.
ThemeManager
// 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