swiftuiswiftui-environment

Cannot call mutating async function 'create' on actor-isolated property 'dataService'


In my sample app I’m reading data into the DomainService class an ObservableObject. I inject the dependencies into the view using the Environment. Reading data works fine, but when I try to create or update my data I can’t call the mutating async function.

A second problem is that I can’t show an .alert to the user on throwing an error. The alert is not shown because 'Publishing changes from within view updates is not allowed’.

// this sample app uses a DataAdapter to read and create items.
// the DataAdapter conforms to IDataService protocol and the DataService makes the data available for the app.
// the DomainService consumes data conforming to IDataService protocol and is the ObervableObject to the screens/ views.
// the main screen/ view injects the dependencies into an EnvironmentObject.

import SwiftUI

//Data
struct Item: Hashable {
    var name: String
}

struct MockDb {
    private var items: [Item] = []
    
    func readItems() async throws -> [Item] {
        throw AdapterError.dataError //testing the error handling.
        
        [Item(name: "Peter"), Item(name: "Joan")]
    }
    mutating func createItem(_ item: Item) async throws -> Bool {
        items.append(item)
        return true
    }
}

enum AdapterError: Error {
    case dataError
}

struct ItemDataAdapter: IDataService {
    var mock: MockDb
    
    func read() async -> Result<[Item], AdapterError> {
        do {
            let result = try await mock.readItems()
            return .success(result)
        } catch {
            return .failure(AdapterError.dataError)
        }
    }
    mutating func create(_ item: Item) async -> Result<Bool, AdapterError> {
        do {
            let result = try await mock.createItem(item)
            return .success(result)
        } catch {
            return .failure(AdapterError.dataError)
        }
    }
}

struct ItemDataService: IDataService {
    var dataAdapter: IDataService
    
    func read() async -> Result<[Item], AdapterError> {
        await dataAdapter.read()
    }
    mutating func create(_ item: Item) async -> Result<Bool, AdapterError> {
        await dataAdapter.create(item)
    }
}

protocol IDataService {
    func read() async -> Result<[Item], AdapterError>
    mutating func create(_ item: Item) async -> Result<Bool, AdapterError>
}

//Domain
@MainActor
class ItemDomainStore: ObservableObject {
    @Published var items: [Item] = []
    
    var dataService: ItemDataService
    
    init(dataService: IDataService) {
        self.dataService = ItemDataService(dataAdapter: dataService)
        
        Task {
            await read()
        }
    }
    
    func read() async {
        let result = await dataService.read()
        
        switch result {
        case .success(let items):
            self.items = items
        case .failure(let error):
            items = []
            
            //thrown error from mockDb is shown here.
            //how can I show the error as an alert to the user?
            print(error)
        }
    }
    func create(_ item: Item) async {
        //debugger message: Cannot call mutating async function 'create' on actor-isolated property 'dataService'
        //how can I call create?
        //let result = await dataService.create(item)
    }
}

//Presentation
struct ItemListScreen: View {
    @EnvironmentObject private var domainStore: ItemDomainStore
    
    var body: some View {
        VStack {
            List(domainStore.items, id: \.self) { item in
                Text(item.name)
            }
            Button("create item") {
                Task {
                    await domainStore.create(Item(name: "Marry"))
                }
            }
        }
    }
}

//main screen.
struct MVConcurrentDataFlow: View {
    let mock = MockDb()
    
    var body: some View {
        ItemListScreen()
            .environmentObject(ItemDomainStore(dataService: ItemDataService(dataAdapter: ItemDataAdapter(mock: mock))))
    }
}

Solution

  • Result's main use is for completion handlers. Since you don't have completion handlers, throwing functions are better. I fixed the Cannot call mutating async function 'create' on actor-isolated property 'dataService'by making them a class because classes are more appropriate. And the alert works. You can use typed throws, too (if you use Swift 6).

    import SwiftUI
    
    struct Item: Hashable, Sendable {
        var name: String
    }
    
    class MockDb {
        private var items: [Item] = []
        
        func readItems() async throws(AdapterError) -> [Item] {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            throw AdapterError.dataError
        }
        func createItem(_ item: Item) async throws(AdapterError) -> Bool {
            items.append(item)
            return true
        }
    }
    
    enum AdapterError: Error {
        case dataError
    }
    
    extension AdapterError: LocalizedError {
        var errorDescription: String? {
            switch self {
                case .dataError: return "Data error accrued"
            }
        }
    }
    
    final class ItemDataAdapter: IDataService {
        var mock: MockDb
        
        init(mock: MockDb) {
            self.mock = mock
        }
        
        func read() async throws(AdapterError) -> [Item] {
            try await mock.readItems()
        }
        func create(_ item: Item) async throws(AdapterError) -> Bool {
            try await mock.createItem(item)
        }
    }
    
    final class ItemDataService: IDataService {
        var dataAdapter: any IDataService
        
        init(dataAdapter: any IDataService) {
            self.dataAdapter = dataAdapter
        }
        
        func read() async throws(AdapterError) -> [Item] {
            try await dataAdapter.read()
        }
        func create(_ item: Item) async throws(AdapterError) -> Bool {
            try await dataAdapter.create(item)
        }
    }
    
    protocol IDataService {
        func read() async throws(AdapterError) -> [Item]
        mutating func create(_ item: Item) async throws(AdapterError) -> Bool
    }
    
    // Domain
    @MainActor
    final class ItemDomainStore: ObservableObject {
        @Published var isAlertShowing = false
        @Published var items: [Item] = []
        @Published var error: AdapterError?
        
        Var dataService: ItemDataService
        
        init(dataService: any IDataService) {
            self.dataService = ItemDataService(dataAdapter: dataService)
        }
        
        @Sendable func read() async {
            do {
                let result = try await dataService.read()
                
                items = result
            } catch let error {
                self.error = error
                self.isAlertShowing = true
            }
        }
        
        @Sendable func create(_ item: Item) async {
            do {
                let result = try await dataService.create(item)
                
                // Do some things
            } catch {
                // Do some things
            }
        }
    }
    
    // Presentation
    struct ItemListScreen: View {
        @EnvironmentObject private var domainStore: ItemDomainStore
        
        var body: some View {
            VStack {
                List(domainStore.items, id: \.self) { item in
                    Text(item.name)
                }
                Button("create item") {
                    Task {
                        await domainStore.create(Item(name: "Marry"))
                    }
                }
                .alert(isPresented: $domainStore.isAlertShowing, error: domainStore.error) { _ in
                    Button("Ok") {}
                } message: { error in
                    Text("Please try again later.")
                }
                
            }
            .task {
                await domainStore.read()
            }
        }
    }
    
    // Main Screen.
    struct ContentView: View {
        let mock = MockDb()
        
        var body: some View {
            ItemListScreen()
                .environmentObject(
                    ItemDomainStore(
                        dataService: ItemDataService(
                            dataAdapter: ItemDataAdapter(
                                mock: mock
                            )
                        )
                    )
                )
        }
    }