#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. Я не эксперт по внедрению зависимостей, но поскольку мы запускаем модель представления в среде только один раз, я считаю, что все они используют один и тот же объект модели представления. В среде есть только один объект модели представления.