Applying themes to your App

In the past week I’ve started implementing themes to my app, which is in it’s late state.

I’ve sketched how I’d do it and googled around to see some examples.

In most examples I’ve seen they were all using the appearance() property, based on it’s objects: UINavigationBar.appearance() , UICollectionView.appearance() and so on.

I’ve tried those and I’ve noticed that only a few objects were changing it’s appearance and even worse, I had to remove all the views from the keyWindow() and had to re-add them in order to change the view which was trying to change the app’s theme — I know it may sound confusing.

I wasn’t satisfied with this behavior and this hacks in order to make everything work, until I came accross this Medium post written by Brad Mueller.

Brad explains the same issues I was having and he pointed out how to achieve the desired effect, smoothly, using UIResponder.

He attached a sample project written in Objective-C and after I look into it Ive converted it into Swift.

First I’ve created the following enum

@objc enum ThemeScheme: Int {
	case light, dark
}

Them I’ve created the following protocol

@objc protocol Themeable: class {
	@objc optional func themeDidChange(to themeScheme: ThemeScheme)
}

Notice I’ve added the @objc keyword in order to have an optional protocol function (yes, you need to do this in Swift).

Notice as well that I have subclassed the protocol to be of type class because this allows you to create weak references to the protocol.
Now I needed to create two classes to work both as theme manager and to handle the NSNotifications to detect when the theme is about to change.

final class ThemeManager {
	static let shared = ThemeManager()
	var activeTheme: ThemeScheme = .light
}

let kThemeDidChangeNotification = "didChangeThemeNotification"
var kThemeNotifierKey = "themeNotifierKey"

private final class ThemeNotifier: NSObject {
	private let block: ((_ notification: Notification) -> Void)
	private var observer: AnyObject?

	init(name: String, object: AnyObject?, block: @escaping (_ notification: Notification) -> Void) {
		self.block = block
		super.init()
		observer = NotificationCenter.default.addObserver(forName: Notification.Name(name), object: object, queue: nil, using: block)
	}

	deinit {
		NotificationCenter.default.removeObserver(observer as Any)
	}
}

Now the tricky part; we’ll extend the UIResponder class so it’s available throughout the app so we can listen for theme changes.

extension UIResponder: Themeable {
	private var themeNotifier: ThemeNotifier? {
		get {
			return objc_getAssociatedObject(self, &kThemeNotifierKey) as? ThemeNotifier
		}
		set {
			objc_setAssociatedObject(self, &kThemeNotifierKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
		}
	}

	func registerForThemeChanges() {
		themeNotifier = ThemeNotifier(name: kThemeDidChangeNotification, object: nil, block: { [weak self] (_) in
			(self as? Themeable)?.themeDidChange?(to: ThemeManager.shared.activeTheme)
		})
		(self as? Themeable)?.themeDidChange?(to: ThemeManager.shared.activeTheme)
	}
}

To make things easier I have created the following Theme struct

import UIKit

let SelectedThemeKey: String = "SelectedThemeKey"

struct Theme {
	static var collectionViewBackgroundColor = UIColor.red
	// All your desired color properties goes here...

	static public func currentTheme(shouldSet: Bool) -> ThemeScheme {
		var themeScheme = ThemeScheme.light
		if let storedTheme = (UserDefaults.standard.value(forKey: SelectedThemeKey) as AnyObject).integerValue {
			themeScheme = ThemeScheme(rawValue: storedTheme)!
		}

		if shouldSet {
			setTheme(theme: themeScheme)
		}
		return themeScheme
	}

	static public func setTheme(theme: ThemeScheme) {
		UserDefaults.standard.setValue(theme.rawValue, forKey: SelectedThemeKey)
		UserDefaults.standard.synchronize()

		switch theme {
		case .light: setLightTheme()
		case .dark: setDarkTheme()
		}
	}

	static private func setLightTheme() {
		collectionViewBackgroundColor = UIColor.red
	}

	static private func setDarkTheme() {
		collectionViewBackgroundColor = UIColor.black
	}
}

As you can see the Theme struct handles the theme change with the setTheme functions, updating it’s cached value stored in UserDefaults

Later, whenever you want to change the current theme, you just have to call Theme.setTheme(theme: .light) accordingly to your current theme state change.

You’ll have to go to your viewDidLoad() initialization class method and add the following:

override func viewDidLoad() {
	super.viewDidLoad()
	registerForThemeChanges()
}

// This method will be available throughout the app
func themeDidChange(to themeScheme: ThemeScheme) {
	setupUI()
}

fileprivate func setupUI() {
	self.collectionView?.backgroundColor = Theme.collectionViewBackgroundColor
	// Apply your UI updates accordingly
}

And you’re done.