I have the following code written in Swift:
protocol Theme {
var iconColor: Color { get }
}
struct MyTheme: Theme {
var iconColor: Color = .white
}
class Appearance {
static var theme: Theme = MyTheme()
}
I can use this Appearance
class in my views to conveniently get the current theme's properties
Image("my-icon")
.foregroundColor(Appearance.theme.iconColor)
I enabled -warn-concurrency
compiler flag in XCode as an experiment to see how many warnings my app produces. This is one of the warnings:
Reference to static property 'theme' is not concurrency-safe because it involves shared mutable state
Now I understand why it gives this warning, but how could I change the code so that theme
is concurrency safe and still as convenient as it is?
I could change theme
to static let
but then I would not be able to change the theme of the app during runtime. I also tried combinations of @MainActor
and actor Appearance
but they give mixed result with either the same warning or some other error.
Edit
@MainActor class Appearance
gets rid of all the warnings, but consider this:
struct FooView: View {
var iconColor: Color = Appearance.theme.iconColor
var body: some View {
HStack {
Text("FooView")
Image("foo-logo")
.foregroundColor(iconColor)
}
}
}
The code above will give an error: Main actor-isolated static property 'theme' can not be referenced from a non-isolated context
To remove the warning, you can just add @MainActor
to the Appearance
class. You would also need to mark FooView
with @MainActor
if you are using it outside of body
, because body
is MainActor
-isolated, but FooView
is not.
That said, this won't work in practice. SwiftUI won't know that Appearance.theme
has changed and update your views' colors accordingly. The SwitUI way of implementing such a "theme" system is to use an EnvironmentObject
.
class Appearance: ObservableObject {
@Published var theme: Theme = MyTheme()
}
struct YourRootView: View {
// write this in every view that needs to access the theme
@EnvironmentObject var appearance: Appearance
var body: some View {
Text("Foo")
.foregroundColor(appearance.theme.iconColor)
}
}
When you create YourRootView
, give it a new Appearance
object.
@main
struct FooApp: App {
var body: some Scene {
WindowGroup {
YourRootView()
.environmentObject(Appearance())
}
}
}
Now SwiftUI can observe the changes to the theme
property.