在 RxSwift-MVVM 架构中,触发弹出窗口和指示器等 UI 元素的最佳方式是啥?

Posted

技术标签:

【中文标题】在 RxSwift-MVVM 架构中,触发弹出窗口和指示器等 UI 元素的最佳方式是啥?【英文标题】:In the RxSwift-MVVM architecture what is the best way to trigger UI elements like pop-ups and indicators?在 RxSwift-MVVM 架构中,触发弹出窗口和指示器等 UI 元素的最佳方式是什么? 【发布时间】:2018-01-31 14:26:16 【问题描述】:

问题是 - 在哪里最好:

    调用错误处理弹出窗口 显示/隐藏加载指示器

我的应用如下所示:

ViewController 订阅模型变化时触发 UI 更新:

 var viewModel: ViewModel = ViewModel()

...

 viewModel.source.asObservable().subscribe(onNext:  (_ ) in
      self.tableView.reloadData()
     )
    .disposed(by: bag)

视图模型

     var source = Variable<[Student]>([])

并且在初始化时获取源输出

     api.fetchSourceOutput(id: id)
         .do(onError:  (error) in
                //show error here???
         )
         .catchErrorJustReturn([])
         .bind(to: source)
         .disposed(by: bag)

我不能只将 ViewController 的引用传递给 ViewModel,这会破坏它独立于 UI 的想法。那么我应该如何在视图控制器的视图中调用错误弹出窗口?获取顶视图控制器也不是一个好的选择,因为我可能需要特定的视图来显示我的弹出窗口。 在 viewModel 内部调用 onNext 并隐藏 onCompleted 时,可以显示加载指示器。但是我再次没有引用我的加载指示器引用所在的视图控制器。

想法?

【问题讨论】:

【参考方案1】:

调用错误处理弹出窗口

假设你有一些启动 api fetch 的信号

let someSignalWithIdToStartApiFetch = Observable.just(1)

另外,让我们想象一下,当您在错误时显示一些“重试请求”弹出窗口并且当用户单击“重试”按钮时,您会将其绑定到某个观察者。然后将观察者转换为Observable。所以你有一些第二个信号:

let someSignalWhenUserAsksToRetryRequestAfterError = Observable.just(())

当您需要重试请求时,您可以通过这种方式从someSignalWithIdToStartApiFetch 获取最后一个 id:

let someSignalWithIdToRetryApiFetch = someSignalWhenUserAsksToRetryRequestAfterError
    .withLatestFrom(someSignalWithIdToStartApiFetch)
    .share(replay: 1, scope: .whileConnected)

然后你结合两个信号并发出请求:

let apiFetch = Observable
    .of(someSignalWithIdToRetryApiFetch, someSignalWithIdToStartApiFetch)
    .merge()
    .flatMap( id -> Observable<Response> in
        return api
            .fetchSourceOutput(id: id)
            .map( Response.success($0) )
            .catchError( Observable.just(Response.error($0)) )
    )
    .share(replay: 1, scope: .whileConnected)

如您所见,错误被捕获并转换为某种结果。例如:

enum Response 
    case error(Error)
    case success([Student])
    var error: Error? 
        switch self 
        case .error(let error):         return error
        default:                        return nil
        
    
    var students: [Student]? 
        switch self 
        case .success(let students):    return students
        default:                        return nil
        
    

然后你像往常一样获得成功的结果:

apiFetch
    .map( $0.students )
    .filterNil()
    .bind(to: source)
    .disposed(by: bag)

但是错误情况应该绑定到一些触发弹出窗口的观察者:

apiFetch
    .map( $0.error )
    .filterNil()
    .bind(to: observerWhichShowsPopUpWithRetryButton)
    .disposed(by: bag)

因此,当显示弹出窗口并且用户单击“重试”时 - someSignalWhenUserAsksToRetryRequestAfterError 将触发并重试请求

显示/隐藏加载指示器

我使用类似this 的东西。它是一种特殊的结构,可以捕捉可观察对象的活动。如何使用它?

let indicator = ActivityIndicator()

还有问题第一部分的一些代码。

let apiFetch = Observable
    .of(someSignalWithIdToRetryApiFetch, someSignalWithIdToStartApiFetch)
    .merge()
    .flatMap( id -> Observable<[Student]> in
        return indicator
            .trackActivity(api.fetchSourceOutput(id: id))
    )
    .map( Response.success($0) )
    .catchError( Observable.just(Response.error($0)) )
    .share(replay: 1, scope: .whileConnected)

因此,api fetch 的活动被跟踪。现在您应该显示/隐藏您的活动视图。

let observableActivity = indicator.asObservable() // Observable<Bool>
let observableShowLoading = observableActivity.filter( $0 == true )
let observableHideLoading = observableActivity.filter( $0 == false )

绑定observableShowLoadingobservableHideLoading 以隐藏/显示函数。即使您有多个可能同时执行的请求 - 将它们全部绑定到单个 ActivityIndicator

希望对您有所帮助。快乐编码(^

【讨论】:

谢谢。但是,如果 apiFetch 发送错误,订阅将被终止,并且不再点击按钮将重复该过程。要修复它,您可以使用 RxSwiftExt 中的“materialize, elements, errors”。 @RealNmae 很好... apiFetch 和错误捕获都应该在 flatMap 中。我的错误 你能把你的答案给我看吗?因为在我看来,每当您发现错误时,订阅就会完成然后释放。 @RealNmae 不,它不像您期望的那样工作。你可以发现一个错误!我会修正我的答案【参考方案2】:

我会在您的 viewModel 中进行此更改:

// MARK: - Properties
let identifier = Variable(0)

lazy var source: Observable<[Student]> = identifier.asObservable()
    .skip(1)
    .flatMapLatest  id in 
        return api.fetchSourceOutput(id: id)
    
    .observeOn(MainScheduler.instance)
    .share(replay: 1)

...

// MARK: - Initialization
init(id: Int) 
    identifier.value = id
    ...

那么,在你的ViewController

viewModel.source
    .subscribe(onNext:  _ in
        self.tableView.reloadData()
     , onError:  error in 
        // Manage errors
     )
    .disposed(by: bag)

【讨论】:

以上是关于在 RxSwift-MVVM 架构中,触发弹出窗口和指示器等 UI 元素的最佳方式是啥?的主要内容,如果未能解决你的问题,请参考以下文章

关闭表单提交时打开的弹出窗口并触发单击父窗口

模态弹出窗口未在 C# 中触发

MeteorJS:在弹出窗口中不会触发模板事件

JavaScript 在新窗口中打开URL(不触发弹出窗口阻止程序)

弹出窗口内的按钮不会触发弹出窗口外的点击事件

在新窗口中打开URL(不触发弹出窗口阻止程序)