我正在查看将 SwiftUI 与合并结合使用的示例:MVVM with Combine Tutorial for iOS在 raywenderlich.com。 ViewModel 实现如下所示:
class WeeklyWeatherViewModel: ObservableObject, Identifiable {
// 2
@Published var city: String = ""
// 3
@Published var dataSource: [DailyWeatherRowViewModel] = []
private let weatherFetcher: WeatherFetchable
// 4
private var disposables = Set<AnyCancellable>()
init(weatherFetcher: WeatherFetchable) {
self.weatherFetcher = weatherFetcher
}
}
所以,这对我来说是有道理的。在观察模型的 View 中,ViewModel 的实例被声明为 ObservedObject
,如下所示:
@ObservedObject var viewModel: WeeklyWeatherViewModel
然后可以在 View 的 body
定义中使用模型中的 @Published
属性,如下所示:
TextField("e.g. Cupertino", text: $viewModel.city)
在 WeeklyWeatherViewModel
中,Combine 用于获取 city
文本,对其发出请求,然后将其转入 [DailyWeatherRowViewModel]
。到目前为止,一切都很顺利并且有意义。
让我感到困惑的是,相当多的代码被用来:
- 当
城市
更改时触发提取。 - 按住正在查找天气数据的
AnyCancellable
。 - 通过天气获取发布者上的
sink
将天气查找的输出复制到dataSource
`
看起来像这样:
// More in WeeklyWeatherViewModel
init(
weatherFetcher: WeatherFetchable,
scheduler: DispatchQueue = DispatchQueue(label: "WeatherViewModel")
) {
self.weatherFetcher = weatherFetcher
_ = $city
.dropFirst(1)
.debounce(for: .seconds(0.5), scheduler: scheduler)
.sink(receiveValue: fetchWeather(forCity:))
}
func fetchWeather(forCity city: String) {
weatherFetcher.weeklyWeatherForecast(forCity: city)
.map { response in
response.list.map(DailyWeatherRowViewModel.init)
}
.map(Array.removeDuplicates)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] value in
guard let self = self else { return }
switch value {
case .failure:
self.dataSource = []
case .finished:
break
}
},
receiveValue: { [weak self] forecast in
guard let self = self else { return }
self.dataSource = forecast
})
.store(in: &disposables)
}
如果我在Combine中查找@Published
propertyWrapper的定义,似乎所做的一切就是提供projectedValue
,它是一个Publisher
,这使得 WeeklyWeatherViewModel
应该可以简单地提供 Publisher
来获取天气数据,并让 View 直接使用它。我不明白为什么需要复制到 dataSource
中。
基本上,我期望 SwiftUI 有一种方法可以直接使用发布者,并且我能够将该发布者从 View 实现外部放置,以便我可以注入(inject)它。但我不知道那是什么。
如果这看起来没有任何意义,那是数字,因为我很困惑。请告诉我,我会看看是否可以完善我的解释。谢谢!
最佳答案
对此我没有明确的答案,也没有找到一种让 SwiftUI 直接使用 Publisher 的神奇方法 - 完全有可能有一种方法让我无法理解!
不过,我找到了一种相当紧凑且灵活的方法来实现所需的结果。它将 sink
的使用减少到附加到输入的单个事件(原始代码中的 @Published city
),这大大简化了取消工作。
这是一个相当通用的模型,它具有一个 @Published input
属性和一个 @Published output
属性(其设置是私有(private)的)。它将转换作为输入,用于转换输入
发布者,然后接收
到输出发布者。 sink
的 Cancelable
被存储。
final class ObservablePublisher<Input, Output>: ObservableObject, Identifiable {
init(
initialInput: Input,
initialOutput: Output,
publisherTransform: @escaping (AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never>)
{
input = initialInput
output = initialOutput
sinkCancelable =
publisherTransform($input.eraseToAnyPublisher())
.receive(on: DispatchQueue.main)
.sink(receiveValue: { self.output = $0 })
}
@Published var input: Input
@Published private(set) var output: Output
private var sinkCancelable: AnyCancellable? = nil
}
如果您想要一种不太通用的模型,您会发现设置将输入(即发布者)过滤到输出中非常容易。
在 View 中,您可以声明模型的实例并按如下方式使用它:
struct SimpleView: View {
@ObservedObject var model: ObservablePublisher<String, String>
var body: some View {
List {
Section {
// Here's the input to the model taken froma text field.
TextField("Give me some input", text: $model.input)
}
Section {
// Here's the models output which the model is getting from a passed Publisher.
Text(model.output)
}
}
.listStyle(GroupedListStyle())
}
}
这里有一些愚蠢的 View 设置及其模型,取自“SceneDelegate.swift”。该模型只是延迟了一点输入的内容。
let model = ObservablePublisher(initialInput: "Moo moo", initialOutput: []) { textPublisher in
return textPublisher
.delay(for: 1, scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
}
let rootView = NavigationView {
AlbumSearchView(model: model)
}
我在输入
和输出
上使模型通用。我不知道这在实践中是否真的有用,但看起来可能有用。
我对此真的很陌生,其中可能存在一些可怕的缺陷,例如效率低下、内存泄漏或保留周期、竞争条件等。不过,我还没有发现它们。
关于swiftui - 更简单的 ViewModel 实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58149396/