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.
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!