Упрощенная реализация ViewModel - PullRequest
0 голосов
/ 28 сентября 2019

Я смотрю на пример использования SwiftUI с Combine: MVVM с руководством по Combine для 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
  }
}

Итак, это имеет некоторый смысл для меня.В представлении, наблюдающем модель, экземпляр ViewModel объявляется как ObservedObject следующим образом:

@ObservedObject var viewModel: WeeklyWeatherViewModel

И тогда можно использовать свойства @Published в модели в body определение вида выглядит следующим образом:

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

В WeeklyWeatherViewModel Комбинация используется для получения текста city, создания запроса и преобразования его в [DailyWeatherRowViewModel].Вплоть до этого, все розовое и имеет смысл.

Когда я запутался, это то, что довольно много кода используется для:

  • Запуск извлечения, когда cityизменено.
  • Удерживайте AnyCancellable, который ищет данные о погоде.
  • Скопируйте вывод поиска погоды в dataSource с помощью sink на выборке погоды Publisher`

Это выглядит так:

  // 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 будет возможность напрямую использовать Publisher, а для меня -быть в состоянии поместить этого издателя извне из реализации View, чтобы я мог внедрить его.Но я понятия не имею, что это такое.

Если это не имеет никакого смысла, эти цифры, как я запутался.Пожалуйста, дайте мне знать, и я посмотрю, смогу ли я уточнить мое объяснение.Спасибо!

1 Ответ

0 голосов
/ 29 сентября 2019

У меня нет однозначного ответа на этот вопрос, и я не нашел волшебного способа, чтобы SwiftUI напрямую использовал Publisher - вполне возможно, что есть один, который ускользает от меня!

Iоднако нашли достаточно компактный и гибкий подход к достижению желаемого результата.Он сокращает использование 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)
}

Я сделал модель общей для Input и Output.Я не знаю, будет ли это на самом деле полезно на практике, но кажется, что это может быть.

Я действительно новичок в этом, и в этом могут быть некоторые ужасные недостатки, такие как неэффективность, памятьутечки или сохранение циклов, условия гонки и т. д. Я еще не нашел их.

...