更简单的ViewModel实现

时间:2019-09-28 18:58:42

标签: swiftui combine

我正在看raywenderlich.com上结合使用SwiftUI和Combine:MVVM with Combine Tutorial for iOS的示例。一个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
  }
}

所以,这对我来说很有意义。在观察模型的视图中,将ViewModel的实例声明为ObservedObject,如下所示:

@ObservedObject var viewModel: WeeklyWeatherViewModel

然后可以在视图的@Published定义中利用模型中的body属性,如下所示:

TextField("e.g. Cupertino", text: $viewModel.city)

WeeklyWeatherViewModel中,Combine用于获取city文本,对其进行请求,然后将其上移至[DailyWeatherRowViewModel]。到这里为止,一切都是玫瑰花的,而且很有意义。

让我感到困惑的是,然后使用了很多代码:

  • 更改city时触发提取。
  • 保留AnyCancellable用来查找天气数据的信息。
  • dataSource上的天气查询的输出复制到天气获取发布者上的sink

它看起来像这样:

  // 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来获取天气数据,并让视图直接使用此数据。我不明白为什么需要复制到dataSource中。

基本上,我期望SwiftUI可以直接利用发布者,并且可以将其发布到View实现的外部,这样我就可以注入它。但是我不知道这是什么。

如果这似乎没有任何意义,那么这个数字,我很困惑。请让我知道,我看看是否可以完善我的解释。谢谢!

2 个答案:

答案 0 :(得分:1)

您可以使用m = df.melt(['subject','subject_type']) n = m['variable'].str.split('_',expand=True).iloc[:,[0,-1]] n.columns = ['cond','value_type'] m = m.drop('variable',1).assign(**n).sort_values('subject') 并从所有视图模型中重构网络。

如果您觉得本教程的某些部分似乎多余,那是因为。

MVVM 在这种用法中是多余的,并且不能做得更好。

如果您对详细信息感兴趣,我有该教程的重构版本(重构了网络,删除了所有视图模型)。

答案 1 :(得分:0)

我对此没有确切的答案,我也没有找到一种让SwiftUI直接利用Publisher的神奇方法–完全有可能让我感到困惑!

尽管如此,我已经找到了一种合理紧凑和灵活的方法来达到预期的结果。它将sink的使用减少到附加到输入的单个事件(原始代码中的@Published city),从而大大简化了取消工作。


这是一个相当通用的模型,具有一个@Published input属性和一个@Published output属性(其设置是私有的)。它以转换为输入,并用于转换input发布者,然后sink插入输出发布者。 Cancelable中的sink已存储。

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
}

如果您想要一种通用性稍差的模型,可以看到将输入(它是发布者)过滤到输出中非常容易。

在视图中,您可以声明模型的实例并像这样使用它:

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())
    }
}

这是从“ 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)
}

我使模型在InputOutput上通用。我不知道这在实践中是否真的有用,但似乎可能有用。

我真的很陌生,其中可能存在一些可怕的缺陷,例如效率低下,内存泄漏或保留周期,竞争条件等。不过我还没有找到它们。