swiftui - 更简单的 ViewModel 实现

标签 swiftui combine

我正在查看将 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)的)。它将转换作为输入,用于转换输入发布者,然后接收到输出发布者。 sinkCancelable 被存储。

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/

相关文章:

SwiftUI 多个选择器无法点击

ios - 如何在 SwiftUI 中更改步进图标前景色?

ios - 在 SwiftUI 中从 @ObservedObject 获取更新的值

ios - 滚动时SwiftUI NavigationBar不会消失

ios - SwiftUI @ObservedObject 不会在异步获取图像时更新

ios - 如何解决 - 合并错误 |错误域=NSURLErrorDomain 代码=-999 "cancelled"

SwiftUI - 无法将修饰符添加到表单内的选取器 View

swift - 如何在透明度降低的 SwiftUI 中获取菜单项的重音背景?

swift - mixLatest 在 Just 与 Future 中具有不同的行为

ios - SwiftUI 动画不会在绑定(bind)更新时发生