Reputation: 65
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))))
}
}
Upvotes: 1
Views: 202
Reputation: 700
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
)
)
)
)
}
}
Upvotes: 1
Reputation: 29614
You have to make sure that your services are Sendable
if you want to be able to mutate them.
struct
's automatically conform so you can just add the inheritance.
protocol IDataService: Sendable {
func read() async -> Result<[Item], AdapterError>
mutating func create(_ item: Item) async -> Result<Bool, AdapterError>
}
Upvotes: 0