Skip to content

Instantly share code, notes, and snippets.

@samrayner
Created October 25, 2018 12:02
Show Gist options
  • Save samrayner/fe25d56858cc7c9d8b37c84d8c65a13b to your computer and use it in GitHub Desktop.
Save samrayner/fe25d56858cc7c9d8b37c84d8c65a13b to your computer and use it in GitHub Desktop.
//
// RunTimeTheme.swift
// RunTimeTheme.swift
//
// Created by Sam Rayner on 24/10/2018.
// Copyright © 2018 Sam Rayner. All rights reserved.
//
import UIKit
extension UIAppearance {
fileprivate func applyStyle(key: Int) {
Theme.delegate?.theme(for: self).apply(key: key, to: self)
}
}
extension UIView {
@objc fileprivate dynamic func style(key: Int) {
applyStyle(key: key)
}
}
protocol ThemeDelegate: AnyObject {
func theme(for appearanceProxy: UIAppearance) -> Theme
}
protocol ThemeType: AnyObject {
static var delegate: ThemeDelegate? { get set }
var styleBlocks: [Int : (UIAppearance) -> Void] { get set }
func apply(key: Int, to appearanceProxy: UIAppearance)
}
extension ThemeType {
static func enable(delegate: ThemeDelegate) {
self.delegate = delegate
}
private func appearance<T: UIAppearance>(of objectType: T.Type,
for traitCollection: UITraitCollection?,
within containerTypes: [UIAppearanceContainer.Type]?) -> T {
var object: T
if let traitCollection = traitCollection, let containerTypes = containerTypes {
object = objectType.appearance(for: traitCollection, whenContainedInInstancesOf: containerTypes)
} else if let traitCollection = traitCollection {
object = objectType.appearance(for: traitCollection)
} else if let containerTypes = containerTypes {
object = objectType.appearance(whenContainedInInstancesOf: containerTypes)
} else {
object = objectType.appearance()
}
return object
}
func style<T: UIAppearance>(_ objectType: T.Type,
for traitCollection: UITraitCollection? = nil,
within containerTypes: [UIAppearanceContainer.Type]? = nil,
with closure: @escaping (T) -> Void) {
let appearanceProxy = appearance(of: objectType, for: traitCollection, within: containerTypes)
closure(appearanceProxy)
}
func styleAtRunTime<T: UIView>(_ viewType: T.Type,
for traitCollection: UITraitCollection? = nil,
within containerTypes: [UIAppearanceContainer.Type]? = nil,
with closure: @escaping (T) -> Void) {
let view = appearance(of: viewType, for: traitCollection, within: containerTypes)
let key = ThemeKey(objectType: viewType, traitCollection: traitCollection, containerTypes: containerTypes).hashValue
view.style(key: key)
styleBlocks[key] = { view in
guard let view = view as? T else { return }
closure(view)
}
}
func apply(key: Int, to appearanceProxy: UIAppearance) {
styleBlocks[key]?(appearanceProxy)
}
}
struct ThemeKey: Hashable {
let objectTypeString: String
let traitCollection: UITraitCollection?
let containerTypeStrings: [String]?
init(objectType: UIAppearance.Type, traitCollection: UITraitCollection?, containerTypes: [UIAppearanceContainer.Type]?) {
objectTypeString = "\(objectType)"
self.traitCollection = traitCollection
containerTypeStrings = containerTypes?.map { "\($0)" }
}
}
class Theme: ThemeType {
static weak var delegate: ThemeDelegate?
var styleBlocks: [Int : (UIAppearance) -> Void] = [:]
required init() {}
}
class MyTheme: Theme {
required init() {
super.init()
style(UIView.self) {
$0.backgroundColor = .red
}
styleAtRunTime(UIView.self) {
$0.backgroundColor = .green
}
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let theme = MyTheme()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
Theme.enable(delegate: self)
return true
}
}
extension AppDelegate: ThemeDelegate {
func theme(for appearanceProxy: UIAppearance) -> Theme {
return theme
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment