В моем примере приложения я считываю данные в класс DomainService ObservableObject. Я добавляю зависимости в представление, используя Environment. Чтение данных работает нормально, но когда я пытаюсь создать или обновить свои данные, я не могу вызвать мутирующую асинхронную функцию.
Вторая проблема заключается в том, что я не могу показать пользователю .alert при возникновении ошибки. Предупреждение не отображается, поскольку «Публикация изменений из обновлений представления не разрешена».
// 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))))
}
}
Да, существует несколько источников данных/адаптеров/служб данных, все они соответствуют IDataService.
Обычно асинхронная функция внутри службы принимает объект модели в качестве параметра, чтобы можно было установить в него данные. Служба обычно представляет собой структуру только с функциями, без внутреннего состояния.
Result
в основном используется для обработчиков завершения. Поскольку у вас нет обработчиков завершения, лучше использовать функции бросания. И я исправил Cannot call mutating async function 'create' on actor-isolated property 'dataService'
, сделав их классом. Потому что занятия для этого более подходят. И вы можете делать инъекции зависимостей (у вас есть для этого почти идеальная настройка). И оповещение работает.
import SwiftUI
struct Item: Hashable, Sendable {
var name: String
}
class MockDb {
private var items: [Item] = []
func readItems() async throws -> [Item] {
try? await Task.sleep(nanoseconds: 1_000_000_000)
throw AdapterError.dataError
}
func createItem(_ item: Item) async throws -> 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"
}
}
}
class ItemDataAdapter: IDataService {
var mock: MockDb
init(mock: MockDb) {
self.mock = mock
}
func read() async throws -> [Item] {
try await mock.readItems()
}
func create(_ item: Item) async throws -> Bool {
try await mock.createItem(item)
}
}
class ItemDataService: IDataService {
var dataAdapter: IDataService
init(dataAdapter: IDataService) {
self.dataAdapter = dataAdapter
}
func read() async throws -> [Item] {
try await dataAdapter.read()
}
func create(_ item: Item) async throws -> Bool {
try await dataAdapter.create(item)
}
}
protocol IDataService {
func read() async throws -> [Item]
mutating func create(_ item: Item) async throws -> Bool
}
//Domain
@MainActor
class ItemDomainStore: ObservableObject {
@Published var isAlertShowing = false
@Published var items: [Item] = []
@Published var error: AdapterError?
var dataService: ItemDataService
init(dataService: IDataService) {
self.dataService = ItemDataService(dataAdapter: dataService)
}
@Sendable func read() async {
do {
let result = try await dataService.read()
await MainActor.run {
items = result
}
} catch let error {
await MainActor.run {
self.error = (error as! AdapterError)
self.isAlertShowing = true
}
}
}
@Sendable func create(_ item: Item) async {
do {
let result = try await dataService.create(item)
await MainActor.run {
// 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))))
}
}
Спасибо! И еще один вопрос, если вы не возражаете. Я думал, что этот пример кода уже реализует внедрение зависимостей. Я ошибаюсь? Или я просто неправильно понял ваш комментарий по этому поводу?
Структуры не могут использоваться для внедрения зависимостей. Итак, в вашем коде не было внедрения зависимостей. Для внедрения зависимостей требуется ссылочный тип, но структуры являются типом значения.
Вы должны убедиться, что ваши сервисы Sendable
, если вы хотите иметь возможность их изменять.
struct
автоматически согласовывается, поэтому вы можете просто добавить наследование.
protocol IDataService: Sendable {
func read() async -> Result<[Item], AdapterError>
mutating func create(_ item: Item) async -> Result<Bool, AdapterError>
}
Будет ли несколько
ItemDataService
,ItemDataAdapter
илиMockDb
?