swiftswiftuiactorcombine

Using await keyword in SwiftUI's ViewModifier


There is a Model made with Actor. ButtonView changes variables in Model. ColorView displays colors differently depending on the model variable values.

But this example doesn't work. Because await keyword cannot be used in ViewModifier.

Is this problem solvable? What am I missing?

Thank you for reading.

actor ActorModel: ObservableObject {
    static let shared = ActorModel()
    private init() {}
    
    @Published var isActive: Bool = false
    
    func toggleActive() {
        isActive.toggle()
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            ColorView()
            ButtonView()
        }
        .padding()
    }
}

struct ColorView: View {
    @ObservedObject private var model = ActorModel.shared
    
    var body: some View {
        VStack {
            Rectangle()
                .foregroundStyle(await model.isActive ? .red : .blue) // <-- Problem!
                .frame(width: 100, height: 100)
        }
        .padding()
    }
}

struct ButtonView: View {
    @ObservedObject private var model = ActorModel.shared
    
    var body: some View {
        VStack {
            Button("Toggle Color") {
                Task {
                    await model.toggleActive()
                }
            }
        }
        .padding()
    }
}

Solution

  • If you must use an actor then you should do something like this:

    struct ColorView: View {
        @ObservedObject private var model = ActorModel.shared
    
        @State private var color: Color = .gray //Default
        
        var body: some View {
            VStack {
                Rectangle()
                    .foregroundStyle(color) // <-- Problem!
                    .frame(width: 100, height: 100)
            }
            .padding()
            .onAppear {
                Task {
                    color = await model.isActive ? .red : .blue
                }
            }
        }
    }
    

    Although, @Published attribute has no meaning here. As you won't be getting updates from an anctor.

    If you need to subscribe to updates then you'd try another approach:

    struct ColorView: View {
        @ObservedObject private var model = ActorModel.shared
        @State private var isActiveState: Bool = false
    
        var body: some View {
            VStack {
                // ...
            }
            .onAppear {
                Task {
                    await subscribeToModelChanges()
                }
            }
        }
    
        private func subscribeToModelChanges() async {
            await model.subscribeToIsActiveChanges { isActive in
                self.isActiveState = isActive
            }
        }
    }
    
    actor ActorModel: ObservableObject {
        // ...
    
        func subscribeToIsActiveChanges(_ onChange: @escaping (Bool) -> Void) async {
            // Logic to notify the subscriber about changes
            // This part is tricky because Actor doesn't have a built-in observation mechanism
            // You might need to implement your own mechanism to notify about changes
        }
    }