Achievement unlocked - SwiftUI. I had mastered SwiftUI few months back. It took me a while to fully grasp all these concepts in the framework. But now I will never go back to using UIKit again which is so arcane. Programming in SwiftUI is that pleasant experience. It's so much easier and fun to develop the UI declaratively and intuitively. I am using iOS 17 and above using the new state management methods and not Combine.

I will provide an example of how I implemented theming of the entire app using SwiftUI. It's basically changing the font colour and tint colour of all the UI components based on the selected colour scheme like we have in macOS accent colour.

SwiftUI Theme Green SwiftUI Theme Indigo

First the theme picker circles view.

import SwiftUI

struct ThemeSwitcherView: View {
    @Environment(\.theme) var theme
    @Environment(\.self) var env
    @State private var themes: [Color] = []
    private let uiViewState = UIViewState.shared
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .center, spacing: 2) {
                ForEach(themes, id: \.self) { color in
                    ZStack { // to layer circles
                        if (theme?.wrappedValue ?? .blue) == color {  // draw selection outer circle
                            Circle()
                                .stroke(lineWidth: 1.5)
                                .foregroundColor(color)
                                .frame(width: 28, height: 28)
                        }
                        // theme circle
                        Circle()
                            .fill(color)
                            .frame(width: 20, height: 20)
                            .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                            .onTapGesture {
                                withAnimation(.smooth) {
                                    self.theme?.wrappedValue = color
                                    self.uiViewState.saveThemeColor(color)
                                }
                            }
                    }
                    
                }
            }
       }
        .frame(maxWidth: .infinity, alignment: .leading)
        .onAppear {
            Log.debug("on appear theme switcher")
            self.themes = self.uiViewState.getThemes()
            self.theme?.wrappedValue = self.uiViewState.getThemeColor()
        }
    }
}

Here when the theme circle is tapped, it saves the selected theme to the environment which is available throughout the app. This is set in the main file MainViewPhone.swift.

The SettingsView.swift is as follows.

import SwiftUI

struct SettingsView: View {
    @Environment(\.theme) var theme
    // ..

    var body: some View {
        List {
            // ..
            Section(header: Text("TOOLS").font(.caption)) {
                ChevronListItemView("Base64")
            }
            Section {
                ChevronListItemView("Import")
                ChevronListItemView("Export")
            }
            Section(header: Text("THEME").font(.caption)) {
                ThemeSwitcherView()
            }
            // ..
        }
        .listStyle(.insetGrouped)
        .navigationTitle("Settings")
    }
}

The MainViewPhone.swift is as follows.

import SwiftUI

struct MainViewPhone: View {
    let uiViewState = UIViewState.shared
    @State var theme: Color = .blue

    var body: some View {
        ProjectListView()
            // ..
            .environment(\.theme, $theme)
            .accentColor(theme)
            .tint(theme)
            .onAppear {
                self.theme = self.uiViewState.getThemeColor()
            }
    }
}

Here we can see that the theme is stored in a state variable and added to the environment. And accent color and tint uses that value. So any change in the environment variable reflects in the state and will retrigger the draw which will update these UI elements.

The UIViewState.swift is as follows.

import SwiftUI

class UIViewState {
    static let shared = UIViewState()
    private let utils = EAUtils.shared
    private let themeColorKey = "themeColor"
    private let themes: [Color] = [.purple, .indigo, .blue, .cyan, .mint, .green, .yellow]
    private let themeNames: [String: Color] = [
        "purple": .purple,
        "indigo": .indigo,
        "blue": .blue,
        "cyan": .cyan,
        "mint": .mint,
        "green": .green,
        // ..
    ]

    // MARK: - Colour
    
    func getThemes() -> [Color] {
        return self.themes
    }
    
    /// Returns theme color from user defaults if present or default
    func getThemeColor() -> Color {
        if let color = self.utils.getValue(self.themeColorKey) as? String {
            return self.themeNames[color] ?? .blue
        }
        return .blue
    }

    /// Save selected theme color to user defaults
    func saveThemeColor(_ color: Color) {
        self.utils.setValue(key: self.themeColorKey, value: color.description)
    }
}

Finally the utils class EAUtils.swift.

import Foundation

public final class EAUtils {
    static let shared: EAUtils = EAUtils()
    private let userDefaults = UserDefaults.standard
    
    // MARK: - UserDefaults
    
    func getValue(_ key: String) -> Any? {
        return self.userDefaults.value(forKey: key)
    }
    
    func setValue(key: String, value: Any) {
        self.userDefaults.set(value, forKey: key)
    }
    
    func removeValue(_ key: String) {
        self.userDefaults.removeObject(forKey: key)
    }
}

This is all dynamic and requires no app restart. Consider implementing this with UIKit!