Night mode demonstration
Our goal is to simply add themes to your UI components and dynamically switch between them. To do that, we need to create an agreement called Themed, where anyone who participates in a theme is eligible.
extension MyView: Themed {
func applyTheme(_ theme: AppTheme) {
backgroundColor = theme.backgroundColor
titleLabel.textColor = theme.textColor
subtitleLabel.textColor = theme.textColor
}
}
extension AppTabBarController: Themed {
func applyTheme(_ theme: AppTheme) {
tabBar.barTintColor = theme.barBackgroundColor
tabBar.tintColor = theme.barForegroundColor
}
}
Imagine the application’s performance, and let’s figure out some basic requirements:
- A core area for storing and changing the current theme
- A topic type consisting of labeled color definitions
- When the theme changes, we are notified of the applicable mechanism
- A neat way to let anything participate in the theme
- Use custom views and view controllers to change the status bar, TAB bar, and navigation bar of your application
- Theme changes are represented through elaborate fade-in and fade-out animations
If an application can support night mode, obviously it can support many other modes
With that in mind, let’s get to work on our main content
Defining subject Protocols
We said we need some place to store the current topic and be able to subscribe to notifications to see if the topic has changed. First we need to define what this sentence means.
/// Describes a typethat holds a current `Theme` and allows /// an object to be notified when the theme is changed. protocol ThemeProvider { /// Placeholderfor the theme type that the app will actually use
associatedtype Theme
/// The current theme that is active
var currentTheme: Theme { get }
/// Subscribe to be notified when the theme changes. Handler will be
/// removed from subscription when `object` is deallocated.
func subscribeToChanges(_ object: AnyObject, handler: @escaping (Theme) -> Void)
}
Copy the code
The ThemeProvider describes what we use to get the current theme in time from a single point, and where we subscribe to notifications of theme changes.
Note that we made Theme an association type, we don’t want to define a specific type here, because we want applications to represent themes in any way they want.
The subscription mechanism operates by weak references to objects, which are removed from the subscription list when they are released. We will use this approach in place of Notification and NotificationCenter because we can use protocol extension to avoid sample/duplicate code and thus avoid more complex use of notifications.
Now that we have defined where to process the current topic, let’s see how it is used. Once instantiated or configured, an object to be themed needs to know about the current subject and be able to notify it if it changes.
/// Describes a type that can have a theme applied to it
protocol Themed {
/// A Themed type needs to know about what concrete type the
/// ThemeProvider is. So we don't clash with the protocol,
/// let's call this associated type _ThemeProvider
associatedtype _ThemeProvider: ThemeProvider
/// Will return the current app-wide theme provider
var
themeProvider: _ThemeProvider { get
}
/// This will be called whenever the current theme changes
func applyTheme(_ theme: _ThemeProvider.Theme)
}
extension Themed where Self: AnyObject {
/// This is to be called once when Self wants to start listening for
/// theme changes. This immediately triggers
applyTheme()with the
/// current theme.
func setUpTheming() {
applyTheme(themeProvider.currentTheme)
themeProvider.subscribeToChanges(self) { [weak self] newTheme in
self? .applyTheme(newTheme)
}
}
}
If the type is AnyObject, we use a convenient protocol extension, so we avoid the “apply the original theme, subscribe, apply the next theme when the theme changes” step for every consistency. These are all put into the setUpTheming() method, which each object can call.
To do that, a Themed person needs to know what the current ThemeProvider is. When we know the specific type of ThemeProvider for apps (whatever type ultimately fits ThemeProvider), we can offer a ThemeProvider that offers an extension to return apps in Themed Themed apps, and we’re going to do that right now.
All of this means that the conforming object only needs to call setUpTheming() once and provide an implementation of applyTheme() to configure the theme for it.
The realization of the App
Now that we’ve defined our themed API, we can do something interesting with it and apply it to our app. Let’s define the theme type for our app and declare our day and night themes.
struct AppTheme {
var
statusBarStyle: UIStatusBarStyle
var
barBackgroundColor: UIColor
var
barForegroundColor: UIColor
var
backgroundColor: UIColor
var
textColor: UIColor
}
extension AppTheme {
static
let light = AppTheme(
statusBarStyle: .
default,
barBackgroundColor: .white,
barForegroundColor: .black,
BackgroundColor: UIColor(White: 0.9, alpha: 1),
textColor: .darkText
)
static
let dark = AppTheme(
statusBarStyle: .lightContent,
barBackgroundColor: UIColor(white: 0, alpha: 1),
barForegroundColor: .white,
BackgroundColor: UIColor(White: 0.2, alpha: 1),
textColor: .lightText
)
}
Here we define our AppTheme type as a dumb struct that contains the tagging colors and values used to design our app. We then declare some static features for each of the available topics – in the case of this article, day and night topics.
Now it’s time to set up the ThemeProvider for our app
final
class
AppThemeProvider: ThemeProvider {
static
let shared: AppThemeProvider = .init()
private
var
theme: SubscribableValue
var
currentTheme: AppTheme {
get
{
return
theme.value
}
set
{
theme.value = newTheme
}
}
init() {
// We'll default to the light theme to start with, but
// this could read directly from UserDefaults to get
// the user's last theme choice.
theme = SubscribableValue(value: .light)
}
func subscribeToChanges(_ object: AnyObject, handler: @escaping (AppTheme) -> Void) {
theme.subscribe(object, using: handler)
}
}
Now we have two things to do: first, use a statically shared singleton, and second, what exactly is SubscribableValue
Monomer? A: really?
We set up a singleton instance of app-scoped sharing for our ThemeProvider, which is usually a caution.
Our ThemeProvider is well suited for unit testing, which is an acceptable consideration given that this themeization is work at the presentation layer.
In the real world, an app’s UI is made up of multiple screens, each with a huge hierarchy of embedded views. It’s easy to use dependency injection for a view schema or view controller, but dependency injection for every view on the screen can be a big job, requiring many lines of code.
In general, your business logic should be unit tested, and you should not need to test down to the presentation layer. It’s a really interesting topic, and we’ll probably come back to it later.
SubscribableValue
You may already be wondering what SubscribableValue is! The ThemeProvider requires an object to subscribe to changes to the current theme. This is logically simple and could easily be incorporated into ThemeProvider, but the habit of subscribing to a value can and should become more common.
A separate, generic implementation of subscribeable values means that it can be tested and reused in isolation. It also makes ThemeProvider cleaner by allowing it to handle specific responsibilities that are its own.
Of course, if you use Rx in your project (or one with the same functionality), you can replace it with something similar, such as Variable/BehaviorSubject
The implementation of SubscribableValue looks like this:
/// A box that allows us to weakly hold on to an object
struct Weak {
weak var
value: Object?
}
/// Stores a value of type T, and allows objects to subscribe to
/// be notified with this value is changed.
struct SubscribableValue {
private
typealias Subscription = (object: Weak, handler: (T) -> Void)
private
var
subscriptions: [Subscription] = []
var
value: T {
didSet {
for (object, handler) in subscriptions where object.value ! = nil {
handler(value)
}
}
}
init(value: T) {
self.value = value
}
mutating func subscribe(_ object: AnyObject, using handler: @escaping (T) -> Void) {
subscriptions.append((Weak(value: object), handler))
cleanupSubscriptions()
}
private
mutating func cleanupSubscriptions() {
subscriptions = subscriptions.filter({ entry in
return entry.object.value ! = nil
})
}
}
SubscribableValue contains an array of weak object references and closures. When the values change, we iterate over these subscriptions in didSet and call the closure. When the object is released, it also removes the subscription.
Now that we have a ThemeProvider that works, we are just one thing away from being ready. Now, that’s adding an extension to Themed, which returns a single AppThemeProvider instance of our app.
extension Themed where Self: AnyObject {
var
themeProvider: AppThemeProvider {
return
AppThemeProvider.shared
}
}
If you remember it from Themed conventions and extensions, objects need this feature to use the convenient setUpTheming() method to manage subscriptions to ThemeProvider. Now it means that all anyone now needs to do is implement applyTheme(). Perfect!
Get the Themed
Now that we’re ready to make our views, view controllers, and app columns responsive to theme changes, let’s start harmonizing!
UIView
If you have a nice UIView subclass and you want it to respond to topic changes. Now, all you have to do is make it Themed, call setUpTheming() in init, and make sure that any theme-related Settings are in applyTheme().
Don’t forget to call applyTheme() once when you’re ready, too, so that all your theme code is in one proper place.
class
MyView: UIView {
var
label
= UILabel()
init() {
super.init(frame: .zero)
setUpTheming()
}
}
extension MyView: Themed {
func applyTheme(_ theme: AppTheme) {
backgroundColor = theme.backgroundColor
label.textColor = theme.textColor
}
}
UIStatusBar and UINavigationBar
You may also want to update the look and feel of your app’s status bar and navigation bar based on the current theme. Assuming your app is using a viewcontroller-based status bar look (which is the default), you could subclass your navigation controller and make it themed.
class
AppNavigationController: UINavigationController {
private
var
themedStatusBarStyle: UIStatusBarStyle?
override
var
preferredStatusBarStyle: UIStatusBarStyle {
return
themedStatusBarStyle ?? super.preferredStatusBarStyle
}
override
func viewDidLoad() {
super.viewDidLoad()
setUpTheming()
}
}
extension AppNavigationController: Themed {
func applyTheme(_ theme: AppTheme) {
themedStatusBarStyle = theme.statusBarStyle
setNeedsStatusBarAppearanceUpdate()
navigationBar.barTintColor = theme.barBackgroundColor
navigationBar.tintColor = theme.barForegroundColor
navigationBar.titleTextAttributes = [
NSAttributedStringKey.foregroundColor: theme.barForegroundColor
]
}
}
Similarly for your UITabViewController subclass
class
AppTabBarController: UITabBarController {
override
func viewDidLoad() {
super.viewDidLoad()
setUpTheming()
}
}
extension AppTabBarController: Themed {
func applyTheme(_ theme: AppTheme) {
tabBar.barTintColor = theme.barBackgroundColor
tabBar.tintColor = theme.barForegroundColor
}
}
Now in your storyboard (or code), make sure your app’s TAB bar and navigation controller are your new subclass types.
That’s it, your app’s status and navigation bar will respond to theme changes, very clever!
Now, with every component and view now Themed, an app can respond to a shift in theme.
Having the logic of topic change tightly coupled to each individual component means that each part can do its job within its own scope, so that each part does its job well.
Loop theme
We need some functionality to loop through available themes, and we can tweak some of the app’s ThemeProvider implementation by adding the following code
final
class
AppThemeProvider: ThemeProvider {
// ...
private
var
availableThemes: [AppTheme] = [.light, .dark]
// ...
func nextTheme() {
guard let nextTheme = availableThemes.rotate() else
{
return
}
currentTheme = nextTheme
}
}
extension Array
{
/// Move the last element of the array to the beginning
/// - Returns: The element that was moved
mutating func rotate() -> Element? {
guard let lastElement = popLast() else
{
return
nil
}
insert(lastElement, at: 0)
return
lastElement
}
}
We listed the available themes in ThemeProvider and used a nextTheme() function to loop them through.
A simple way to loop through a set of topics without requiring a variable that records the index is to get the last one in the topic group and move it to the beginning. This operation can be repeated to loop through all values. We do this by extending the topic group and writing a mutating method called Rotate ().
Now when we want to switch the theme can invoke AppThemeProvider. Shared. NextTheme (), it will be updated.
animate
We want to polish it up by adding a synchronized fade in and out animation for the theme change. We could animate each property change in each applyTheme() method, but given that the entire window changes, using UIKit to represent the snapshot transformation of the entire window is much more efficient and requires less code.
Let’s tweak the app’s ThemeProvider again to give us this functionality:
final
class
AppThemeProvider: ThemeProvider {
// ...
var
currentTheme: AppTheme {
// ...
set
{
setNewTheme(newValue)
}
}
// ...
private
func setNewTheme(_ newTheme: AppTheme) {
let window = UIApplication.shared.delegate! .window!! //
UIView.transition(
with: window,
Duration: 0.3,
options: [.transitionCrossDissolve],
animations: {
self.theme.value = newTheme
},
completion: nil
)
}
}
As you can see, we wrap the theme value change in a UIView synchronized fade-in transition. All applyTheme() methods are called by setting a new value for the theme, and all changes take place in the transformed animation block.
To do this, we need the app window, which in this case actually has more forced unpacks (in one line) than there should be in the entire app. For practical reasons, this should be perfectly possible. Let’s face it, if your app doesn’t have a delegate and a window, you have a bigger problem – but feel free to tweak this for your particular implementation to make it more conservative.
Here we have it, a well-implemented nighttime pattern and a deep understanding of theming. If you want to try out an effective implementation, you can play with the sample code.