Theme

How to control appearance across all components.

What is a Theme?

A theme is a predefined set of colors and layout attributes that ensure visual consistency across your application. It simplifies managing and updating your app's appearance.


Customizing the Current Theme

The default theme comes with a set of predefined values for colors, fonts, and layout, but you can fully customize it to match your brand or design system.

Theme.current.update {
  // Different background color
  $0.colors.background = .themed(
    light: .hex("F3F5F7"),
    dark: .hex("0B0C0E")
  )
  // Custom font for medium body text
  $0.layout.typography.body.medium = .custom(
    name: "SpaceGrotesk-Regular",
    size: 16
  )
  // Slightly increased animation scaling
  $0.layout.animationScale.medium = 0.97
}

Note:

The best place to set up the initial config is in the func application(_:, didFinishLaunchingWithOptions:) -> Bool method in your AppDelegate or a similar method in SceneDelegate.


Creating Custom Themes

You can define multiple themes for your app. To do this, first create a new instance:

let halloweenTheme = Theme {
  $0.colors.background = .themed(
    light: .hex("#e38f36"),
    dark: .hex("#ba5421")
  )
  // Add more customizations
}

Then switch themes dynamically based on user preferences or events like time of day or app mode by assigning it to the current property:

Theme.current = halloweenTheme

Responding to Theme Changes

When themes change at runtime, your UI should reflect those changes immediately. The library offers convenient mechanisms for both SwiftUI and UIKit environments.

SwiftUI

Wrap your root view in a ThemeChangeObserver:

@main
struct Root: App {
  var body: some Scene {
    WindowGroup {
      ThemeChangeObserver {
        ContentView()
      }
    }
  }
}

This ensures automatic view updates when the theme changes.

UIKit

In UIKit, observe changes and update the UI accordingly:

override func viewDidLoad() {
  super.viewDidLoad()
  style()

  observeThemeChange { [weak self] in
    self?.style()
  }
}

func style() {
  view.backgroundColor = UniversalColor.background.uiColor
  button.model = ButtonVM {
    $0.title = "Tap me"
    $0.color = .accent
  }
}

Manual Observation

You can also listen for the notification manually:

NotificationCenter.default.addObserver(
  self,
  selector: #selector(handleThemeChange),
  name: Theme.didChangeThemeNotification,
  object: nil
)

@objc private func handleThemeChange() {
  view.backgroundColor = UniversalColor.background.uiColor
}

Don't forget to remove the observer when the view is deallocated:

deinit {
  NotificationCenter.default.removeObserver(self, name: Theme.didChangeThemeNotification, object: nil)
}