-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Theming system
The theming system aims at providing an easy way for developers to find and use colors from designs inside the application. Any colors available inside the Firefox application will be one of the available Firefox colors. Designers then decide which of those colors will be used in their designs, and create different mobile styles. Mobile styles defines how theming is applied inside the application, for example by applying a certain color for actions or icons. Any colors we use in our application needs to be defined in the iOS mobile theme. There's currently light theme and dark theme in our application, but more could eventually come.
The ThemeManager
manages and saves the current theme. By default it's set to be the OS theme (light or dark) but can be changed overridden by the user in the settings either through brightness or selecting directly the wanted theme. Another way to set the theme is through night mode. By selecting night mode we automatically apply the dark theme on the application (as well as in webviews but that is related to night mode and not the theme itself).
- Don't add tokens for new colors in the Theme protocol if it's not in the iOS mobile theme.
- Don't use Firefox colors directly in code (aka, no FXColors.aColor should ever be referenced directly. Colors needs to be themed).
- Don't set colors by checking the theme type. Ex:
If theme == dark then use color #123456
- Do ask designers if a color in a design you need to implement is not in the mobile theme. This comment also applies if colors are in the mobile theme but under different tokens - i.e. the designer wants to use one token for light theme and another token for dark theme (this is a no-no).
- Do use .clear color directly in code, this color isn't themed.
- For any image that requires theming, use
ThemeType
getThemedImageName(name:)
method. Your image might need renaming to follow convention of adding_dark
into the name of a dark themed image. Please use ImageIdentifiers for image name.
From a developer perspective, theming is applied with view controllers. View controllers hold references to views which are theme. No other classes else than view controllers should be Themeable
and listen to theme changes.
- First step in applying the theming system is to make your view controller follow the
Themeable
protocol. Preferably thethemeManager
andnotificationCenter
variables needs to beinit
injection, so they're easier to integrate with unit tests. - You should then in
viewDidLoad
have a call tolistenForThemeChange(view)
. This will make sure your view controller'sapplyTheme
method will be called when there's a theme change inside the application. (Note: some additional considerations are needed to support your view's themeing in a multi-window iPad environment. See section below on Multi-window.) - Make your views hold inside this VC follow the
ThemeApplicable
protocol. This protocol has aapplyTheme(theme:)
method that will be called automatically from the view controller when a theme change happens. This will not be called on cell/view creation, though so you need to make sure it's called once at init/configuration time, but if we change the theme while the view is already existing the theme should be changed automatically. - Use
applyTheme(theme:)
to setup any labels, buttons, background view, spinners, borders, shadows colors.
Note; Make sure you don't resolve the themeManager
object from UIViews
.
SwiftUI theming works slightly differently by leveraging Apple’s environment values struct (EnvironmentValues). We define values in the struct and to read a value from the structure, we declare a property using the Environment property wrapper and specify the value’s key path.
Note: This is currently only available in iOS 14.0 hence the use of if #available(iOS 14.0, *)
below.
Below is a sample with steps on how to add theming for SwiftUI.
import Foundation
import SwiftUI
import Shared
struct SampleView: View, ThemeApplicable {
/// Step 1:
/// Add SwiftUI Theming using `themeType` Environment variable
@Environment(\.themeType) var themeVal
/// Step 2:
/// Add required state variables for associated colors that require
/// theme updates and define default value
@State var btnColor: Color = .green
var body: some View {
VStack(spacing: 11) {
Button("SampleButton") {
// Do some work
print("Sample Button Pressed")
}
/// Step 3:
/// Assign state color variables to the view
.foregroundColor(btnColor)
}
.onAppear {
/// Step 4:
/// If there is an initial theme update then we perform it here
applyTheme(theme: themeVal.theme)
}
.onChange(of: themeVal) { val in
/// Step 5:
/// When the themeType gets updated we
/// listen to the change via `onChange` method
/// attached to the whole view and perform
/// required updates to the state color variables
applyTheme(theme: val.theme)
}
}
/// Step 6:
/// For better cleanup add applyTheme method using ThemeApplicable protocol
func applyTheme(theme: Theme) {
let color = theme.colors
btnColor = Color(color.actionPrimary)
}
}
Firefox on iPadOS now supports multiple browser windows. In order for new UI (and its related theme updates) to work across multiple windows, there are few considerations to keep in mind.
The general theme settings (Light-vs-Dark, System-vs-Manual etc.) apply to all iPad windows app-wide. However, the visual UI changes for private vs non-private browsing are also handled via the ThemeManager
. Because private browsing is a "per-window" feature (any individual window can enter private browsing separately from another), it means that when your UI is updated for a theme change, it now needs to know which window it belongs to.
In general, most new UI should not require any special changes to work across multiple windows. However, there are a few important things to know:
- In order for a view to be updated correctly by
listenForThemeChange
(andupdateThemeApplicableSubviews
, which in turn callsapplyTheme()
), the views need to know which window (UUID) they will be presented in. - This UUID is supplied via the
currentWindowUUID
property defined by theThemeUUIDIdentifiable
protocol. - Support for this is provided automatically for all
UIView
s via an extension (UIView+ThemeUUIDIdentifiable.swift
), but it requires that the view is installed in aUIWindow
. (It does not have to be visible, just part of the view hierarchy.) - In cases where your view may not be installed in a
UIWindow
at the time of a theme change, the UUID will be obtained in one of two other ways:- The view controller that subscribes the view to
listenForThemeChange
will be queried via itscurrentWindowUUID
property - or the view itself may adopt the
InjectedThemeUUIDIdentifiable
protocol to provide an explicit UUID (typically injected)
- The view controller that subscribes the view to
- Remove any conformance to
LegacyNotificationThemeable
- Views/cells shouldn't register for
.DisplayThemeChanged
. Only the view controller should be notified with notifications. - There should be no more calls to
UIColor.Photon
orUIColor.theme
. -
LegacyThemeManager
should also not be used.
This is an example PR of a migration from legacy to new theming system for wallpaper selectors. The PR is for a small section of the app, so easier to understand than let's say the homepage migration PR.