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))))
}
}
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
)
)
)
)
}
}