Can ViewModel be coded by ObservableObject actor in SwiftUI View? - iOS16

I practice in iOS16, but this form of coding is not confirmed to be used in production. There are also some confusing.

struct TestView: View {
    @StateObject private var viewModel = TestViewModel()
    
    var body: some View {
        VStack {
            Button("mutatingFight") {
                Task {
                    // calling a SideActor method on MainActor context will be await even without explicitly async mark
                    await viewModel.mutatingFight()
                }
            }
            Button("mutatingSpell") {
                Task {
                    // Innerly, asking for @MainActor on SideActor method will be async method
                    await viewModel.mutatingSpell()
                }
            }
            
            showMainActorProperty(viewModel.counter)
        }
        .frame(width: 300, height: 400)
    }
    
    func showMainActorProperty(_ property: Int) -> some View {
        Text("\(property)")
    }
}
// final class? Sendable?
actor TestViewModel: ObservableObject {
    @MainActor @Published
    var counter: Int = 0
    
    @SideActor
    private var spell: Int = 0 {
        didSet {
            pushToMain()
        }
    }
    @SideActor
    private var fight: Int = 0 {
        didSet {
            pushToMain()
        }
    }
    
    @SideActor
    func mutatingSpell() async {
        if await counter >= 2 {
            spell -= 2
        }
    }
    @SideActor
    func mutatingFight() {
        fight += 1
    }
    @SideActor
    func pushToMain() {
        let value = spell + fight
        Task { @SideActor in
            await MainActor.run {
                counter = value
            }
        }
    }
}
/// lower level above UI's
@globalActor
public actor SideActor: GlobalActor {
    public typealias ActorType = SideActor
    
    static public let shared = SideActor()
    static private let _sharedExecutor = SideExecutor()
    static public let sharedUnownedExecutor: UnownedSerialExecutor = _sharedExecutor.asUnownedSerialExecutor()
    public let unownedExecutor: UnownedSerialExecutor = sharedUnownedExecutor
    
    final private class SideExecutor: SerialExecutor {
        private static let dispatcher = DispatchQueue(label: "MySideExecutor", qos: .default)
        internal func enqueue(_ job: UnownedJob) {
            print(Self.self, #function)
            Self.dispatcher.async {
                job.runSynchronously(on: sharedUnownedExecutor)
            }
        }
        internal func asUnownedSerialExecutor() -> UnownedSerialExecutor {
            UnownedSerialExecutor(ordinary: self)
        }
    }
}

Mixing isolations in one class is not a good idea. View model is designed to represent UI state, so it makes sense to isolate it as a whole on @MainActor and that’s all. Having each view model as a separate actor is more likely overkill.

1 Like