Внедрение зависимостей SwiftUI и удаленного дочернего представления

#swift #dependency-injection #swiftui

#swift #внедрение зависимостей #SwiftUI

Вопрос:

Я пытаюсь понять, как я могу вводить зависимости в дочерние представления с помощью SwiftUI.

Например, если у меня есть представление —

 struct FeedListView: View {
    
    @ObservedObject private(set) var viewModel: FeedViewModel
    @State private var items: [Post] = []
    
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
                ForEach(items, id: .id) { item in
                    FeedListItemView(viewModel: .init(item: item))
                }
        }
        .onAppear(perform: onLoad)
        .padding()
    }
}

private extension FeedListView {
    
    func onLoad() {
        viewModel.onFeedLoad = { self.items = $0 }
        viewModel.loadFeed()
    }
}
 

с помощью модели представления —

 final class FeedViewModel: ObservableObject {
    
    typealias LoaderResult = Result<[Post], Error>
    typealias LoaderCompletion = (LoaderResult) -> Void
    typealias Loader = ((@escaping LoaderCompletion) -> Void)
    
    typealias Observer<T> = (T) -> Void
     
    @Published var onFeedLoad: Observer<[Post]>?
    @Published var onLoadingStateChange: Observer<Bool>?

    private let loader: Loader
    
    init(loader: @escaping Loader) {
        self.loader = loader
    }
    
    func loadFeed() {
        onLoadingStateChange?(true)
        loader { [weak self] result in
            if let feed = try? result.get() {
                self?.onFeedLoad?(feed)
            }
            self?.onLoadingStateChange?(false)
        }
    }
}
 

Поскольку это представление верхнего уровня, я могу внедрить FeedViewModel с его зависимостями в точке композиции.

Однако это представление отображает FeedListItemView — это представление имеет собственную модель представления и будет иметь собственные зависимости.

 final class FeedListItemViewModel: ObservableObject {
        
    private let item: Post
    
    init(item: Post) {
        self.item = item
    }
    
    func loadImage() {
        let imageURL = item.imageURL
        // do something here to load an image
    }
}

struct FeedListItemView: View {
    
    @ObservedObject private(set) var viewModel: FeedListItemViewModel

    init(viewModel: FeedListItemViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        Text("My Awesome Post")
            .onAppear(perform: viewModel.loadImage)
    }
}
 

Как я могу создать загрузчик изображений, например, (_imageURL: URL) -> AnyPublisher<Data, Error> доступный в FeedListItemViewModel, не передавая это в FeedListView, а затем передавая его непосредственно в модель представления дочерних представлений?

Возможность тестирования очень важна, поэтому я хотел бы избежать одноэлементного экземпляра зависимостей, и я также хотел бы избежать передачи группы значений в представление только для того, чтобы это представление передавало их дочернему элементу, который, в свою очередь, передает их другому удаленному дочернему элементу.

Я был бы очень признателен за любые мысли о том, как этого можно достичь.

Ответ №1:

Вы можете использовать оболочку @EnvironmentObject для распространения модели представления из представления-предка в его дочерние и дочерние представления.

Если вы создаете экземпляр FeedListView в своем файле приложения, добавьте этот модификатор:

 .environmentObject(YourViewModel())
 

После создания экземпляра в самом верхнем представлении вы можете использовать оболочку @EnvironmentObject для доступа к объекту среды.

 @EnvironmentObject var yourVM: YourViewModel
 

Однако объекты среды создаются только один раз, и они работают аналогично одиночным элементам. Когда вы вносите изменения в любое из представлений, другие представления видят измененный объект.

Комментарии:

1. хммм, спасибо за ваш вклад, просто чтобы было ясно, если FeedListItemViewModel требуется зависимость, эта зависимость разделяется между всеми элементами списка? В этом случае, если у меня есть 5 элементов списка, которые все пытаются загрузить другое изображение, они будут использовать один и тот же экземпляр загрузчика изображений? Я не уверен, что это сработает, не так ли?

2. Я не эксперт по внедрению зависимостей, но поскольку мы запускаем модель представления в среде только один раз, я считаю, что все они используют один и тот же объект модели представления. В среде есть только один объект модели представления.