Building on my previous post, let's take a closer look at theming in SwiftUI. In this discussion, I'll show you how to create a customizable solution that integrates seamlessly with the system. The concepts covered here are implemented in my open-source library, which you can explore on GitHub.
What's Needed
Before diving into the code, it's a good idea to clarify our requirements. To create a robust theming solution for SwiftUI, the following key points should be addressed:
A clearly defined concept of a theme.
Integration with the SwiftUI lifecycle.
Automatic application of themes across all Scenes, including instances of .sheet and .fullScreenCover views.
Adherence to system accessibility guidelines.
Implementing a Theme Definition
With our goals in mind, let's start by defining the core of our theming solution — a Theme. This structure encapsulates the values that define a theme.
The highlight here is the ColorMap type, which links ThemeColorStyle (a semantic representation of colors) to ThemeColor. As explained in the previous post, ThemeColorStyle provides an abstract way of describing colors. Here's how we define it:
Using a struct instead of an enum allows developers to define their styles, providing flexibility without hard-coded enumerations. This approach enables familiar dot notation by allowing developers to define static properties in ThemeColorStyle.
The final piece is the ThemeColor, which represents a color's various variants for light/dark modes, accessibility contrast, and interface levels.
The optional properties reduce complexity for consumers, allowing them to define only what's necessary.
Creating a ThemeManager
Next, we'll implement a ThemeManager to handle theme switching. This class ensures that changes in theme automatically update the app's views.
The @Observable annotation ensures SwiftUI updates views when selectedTheme changes.
After implementing the ThemeManager, the next step is to integrate it smoothly into the SwiftUI lifecycle. We aim to make the manager easy to incorporate into the app's structure.
In SwiftUI applications, the Scene serves as one of the highest levels in the app hierarchy. It encompasses everything displayed within a single window, including not just the "base" views but also elevated views like modals. This makes Scene an ideal point for integrating the ThemeManager.
To achieve this, we'll integrate the manager into the SwiftUI lifecycle by extending the Scene type with a helper.
With this, integrating a theme manager into an app is straightforward:
Only two lines of code are necessary to ulimately connect the theming solution to the SwiftUI lifecycle on the top most level.
Bridging SwiftUI and Theming
Finally, let's connect SwiftUI's color resolution to our theming system using the resolve(in:) method of ShapeStyle. The concepts used here are based on the ThemeColorShapeStyle discussed in the previous post.
To connect the Theme to SwiftUI’s environment, as provided by the ThemeManager, we can hook into the resolution cycle. Before diving into this integration, we’ll add a small helper method to the Theme definition. This method will simplify retrieving a color based on a specific style.
ThemeTraits serves as a small helper that extracts the necessary properties for determining a color from the EnvironmentValues.
Additionally, we define a resolve(with:) method on ThemeColor that aims to replicate the functionality of the resolvedColor(with:) method from UIColor. This method serves as a single source of truth, calculating the appropriate color based on the current environment. It also includes a fallback mechanism to handle situations where a consumer has not provided a corresponding color.
After having the helpers set up, we can now implement the resolve(in:) method of a custom type which then connects the dots.
This approach enables SwiftUI to automatically select the appropriate color whenever the EnvironmentValues change. To simplify usage further, we will create extension methods on ShapeStyle, allowing users to utilize theme-based colors just as they would with any default color in SwiftUI.
Consumers can now utilize a theme color using a straightforward dot notation.
Conclusion
In this post, we discussed how to create a theming system for SwiftUI that integrates smoothly and aligns with system conventions. You can explore the concepts we've implemented in my theming library, which may also serve as inspiration for your own projects.
Have feedback or suggestions? Feel free to reach out or contribute via GitHub. Thanks for reading, and see you next time! 👋