使用RxSwift构建视图模型

时间:2018-03-15 12:32:15

标签: swift mvvm viewmodel rx-swift

我的视图模型存在根本缺陷,因为使用驱动程序的模型会在返回错误并且重新订阅无法自动化时完成。

一个例子是我的PickerViewModel,其界面是:

//  MARK: Picker View Modelling
/**
Configures a picker view.
 */
public protocol PickerViewModelling {
    /// The titles of the items to be displayed in the picker view.
    var titles: Driver<[String]> { get }
    /// The currently selected item.
    var selectedItem: Driver<String?> { get }
    /**
    Allows for the fetching of the specific item at the given index.
    - Parameter index:  The index at which the desired item can be found.
    - Returns:  The item at the given index. `nil` if the index is invalid.
    */
    func item(atIndex index: Int) -> String?
    /**
    To be called when the user selects an item.
    - Parameter index:  The index of the selected item.
     */
    func selectItem(at index: Int)
}

我可以在Driver

中找到CountryPickerViewModel问题的示例
    init(client: APIClient, location: LocationService) {
        selectedItem = selectedItemVariable.asDriver().map { $0?.name }
        let isLoadingVariable = Variable(false)
        let countryFetch = location.user
            .startWith(nil)
            .do(onNext: { _ in isLoadingVariable.value = true })
            .flatMap { coordinate -> Observable<ItemsResponse<Country>> in
                let url = try client.url(for: RootFetchEndpoint.countries(coordinate))
                return Country.fetch(with: url, apiClient: client)
            }
            .do(onNext: { _ in isLoadingVariable.value = false },
                onError: { _ in isLoadingVariable.value = false })
        isEmpty = countryFetch.catchError { _ in countryFetch }.map { $0.items.count == 0 }.asDriver(onErrorJustReturn: true)
        isLoading = isLoadingVariable.asDriver()
        titles = countryFetch
            .map { [weak self] response -> [String] in
                guard let `self` = self else { return [] }
                self.countries = response.items
                return response.items.map { $0.name }
            }
            .asDriver(onErrorJustReturn: [])

    }
}

titles驱动UIPickerView,但当countryFetch失败并出现错误时,订阅完成,并且无法手动重试提取。

如果我尝试catchError,我不清楚我可以返回哪些可观察的内容,以后可以在用户恢复其互联网连接后重试。

任何justReturn错误处理(asDriver(onErrorJustReturn:)catchError(justReturn:))显然会在返回值后立即完成,并且对此问题毫无用处。

我需要能够尝试获取,失败,然后显示重试按钮,该按钮将在视图模型上调用refresh()并重试。如何保持订阅开放?

如果答案需要重新构建我的视图模型,因为我想要做的是不可能或干净,我愿意听到更好的解决方案。

3 个答案:

答案 0 :(得分:1)

关于使用RxSwift时的ViewModel结构,在一个相当大的项目的密集工作中,我已经找到了2条规则来帮助保持解决方案的可扩展性和可维护性:

  1. 避免在viewModel中使用任何与UI相关的代码。它包括RxCocoa扩展和驱动程序。 ViewModel应专注于业务逻辑。驱动程序用于驱动UI,因此请将它们留给ViewControllers:)

  2. 尽可能避免使用变量和主题。 AKA试图让一切都“流动”。功能转换为功能,转换为功能等,最终在UI中。当然,有时您需要将非rx事件转换为rx事件(如用户输入) - 对于这种情况,主题是可以的。但是要害怕过度使用 - 否则你的项目将很难维持并立即扩展。

  3. 关于您的特定问题。所以当你想要重试功能时总是有点棘手。 Here与RxSwift作者就此主题进行了很好的讨论。

    第一种方式。在您的示例中,您在init上设置了observable,我也喜欢这样做。在这种情况下,您需要接受这样一个事实:您不希望序列因错误而失败。您确实期望序列可以发出带有标题的结果或带有错误的结果。为此,在RxSwift中我们有.materialize()组合子。

    在ViewModel中:

    // in init
    titles = _reloadTitlesSubject.asObservable() // _reloadTitlesSubject is a BehaviorSubject<Void>
        .flatMap { _ in 
            return countryFetch
                .map { [weak self] response -> [String] in
                    guard let `self` = self else { return [] }
                    self.countries = response.items
                    return response.items.map { $0.name }
                }
                .materialize() // it IS important to be inside flatMap
        }
    
    // outside init
    
    func reloadTitles() {
        _reloadTitlesSubject.onNext(())
    }
    

    在ViewController中:

    viewModel.titles
        .asDriver(onErrorDriveWith: .empty())
        .drive(onNext: [weak self] { titlesEvent in
            if let titles = titlesEvent.element {
                // update UI with 
            }
            else if let error = titlesEvent.error {
                // handle error
            }
        })
        .disposed(by: bag)
    
    retryButton.rx.tap.asDriver()
        .drive(onNext: { [weak self] in
            self?.viewModel.reloadTitles()
        })
        .disposed(by: bag)
    

    第二种方式基本上就是CloackedEddy在答案中的建议。但可以进一步简化以避免变量。在这种方法中,你不应该在viewModel的init中设置你的可观察序列,而是每次都重新返回它:

    // in ViewController
    yourButton.rx.tap.asDriver()
        .startWith(())
        .flatMap { [weak self] _ in
            guard let `self` = self else { return .empty() }
            return self.viewModel.fetchRequest()
                .asDriver(onErrorRecover: { error -> Driver<[String]> in
                    // Handle error.
                    return .empty()
                })
        }
        .drive(onNext: { [weak self] in 
            // update UI
        })
        .disposed(by: disposeBag)
    

答案 1 :(得分:0)

我会将一些职责转移到视图控制器。

一种方法是让视图模型生成一个Observable,作为副作用更新视图模型属性。在下面的代码示例中,视图控制器仍然负责视图绑定,以及通过按钮点击触发viewDidLoad()中的刷新。

class ViewModel {

    let results: Variable<[String]> = Variable([])
    let lastFetchError: Variable<Error?> = Variable(nil)

    func fetchRequest() -> Observable<[String]> {
        return yourNetworkRequest
            .do(onNext: { self.results.value = $0 },
                onError: { self.lastFetchError.value = $0 })
    }
}

class ViewController: UIViewController {

    let viewModel = ViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.results
            .asDriver()
            .drive(onNext: { yourLabel.text = $0 /* .reduce(...) */ })
            .disposed(by: disposeBag) 

        viewModel.lastFetchError
            .asDriver()
            .drive(onNext: { yourButton.isHidden = $0 == nil })
            .disposed(by: disposeBag) 

        yourButton.rx.tap
            .subscribe(onNext: { [weak self] in 
               self?.refresh()
            })
            .disposed(by: disposeBag) 

        // initial attempt
        refresh()
    }

    func refresh() { 
        // trigger the request
        viewModel.fetchRequest()
            .subscribe()
            .disposed(by: disposeBag) 
    }
}

答案 2 :(得分:0)

所有答案都很好,但我想提一下CleanArchitectureRxSwift。这个框架真的帮助我找到了如何将rx应用于我的代码的方式。关于&#34;后端&#34;的部分移动编程(请求,解析器等)可以省略,但使用viewModel / viewController有很多有趣的东西。