Search code examples
swiftswiftuiconcurrency

How can I make this code concurrency safe?


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


Solution

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