【问题标题】:RxSwift, use .scan to keep track of the state of an objectRxSwift,使用 .scan 来跟踪对象的状态
【发布时间】:2018-05-02 10:51:01
【问题描述】:

我知道状态是响应式编程的敌人,但我在学习 RxSwift 的过程中正在处理它。

我的应用程序非常简单,第一个屏幕是一个列表和书籍搜索,第二个屏幕是书籍的详细信息,您可以在其中添加/删除一本书到您的书架并标记它作为已读/未读

为了显示本书的详细信息,我创建了一个BookViewModel,传递了一个BooksService 来执行网络操作并显示当前的Book

问题是我必须跟踪书中的更改才能更改 UI:例如,在删除书籍后,之前显示“删除”的按钮现在必须显示“添加”。

我使用作为Driver<Book> 向观察者公开的Variable<Book> 来实现此行为,但是当网络操作返回时我搞砸了很多,我必须按顺序更新Variable<Book> 的值触发 UI 的更新。

这是视图模型的初始化器:

init(book: Book, booksService: BooksService) {
    self._book = Variable(book)
    self.booksService = booksService
}

这是我公开的可观察对象

var book: Driver<Book> {
    return _book.asDriver()
}

这是我添加/删除书籍的功能:

func set(toggleInShelfTrigger: Observable<Void>) {
    toggleInShelfTrigger // An observable from a UIBarButtonItem tap
        .map({ self._book.value }) // I have to map the variable's value to the actual book
        .flatMap({ [booksService] book -> Observable<Book> in
            return (book.isInShelf ?
                    booksService.delete(book: book) :
                    booksService.add(book: book))
        }) // Here I have to know if the books is in the shelf or not in order to perform one operation or another.
        .subscribe(onNext: { self._book.value = $0 }) // I have to update the variable's value in order to trigger the UI update
        .disposed(by: disposeBag)
}

我对这段代码和整个视图模型非常不满意。它可以工作,但很笨重,而且本质上是错误的,因为如果网络操作失败,订阅将被丢弃,我的按钮将变得无响应。

如果我摆脱Variable&lt;Book&gt; 并从set(toggleInShelfTrigger: Observable&lt;Void&gt;) 方法返回Driver&lt;Book&gt;,我就不会遇到这种混乱,但我将无法知道是否必须添加或删除这本书。

那么,真实世界在这种应用程序中跟踪对象状态的方法是什么?如何仅使用 Rx 运算符来实现这一点?

编辑

我已经设法清理了那些糟糕的代码,但我仍在尝试在没有 Variable 和使用 scan 运算符的情况下实现状态。

这是我的新 BookViewModel 初始化器:

init(book: Book, booksService: BooksService) {
    self.bookVariable = Variable(book)
    
    let addResult = addBook
        .mapBookFrom(bookVariable)
        .flatMapLatest({ booksService.add(book: $0) })
        .updateBookVariable(bookVariable)

    let removeResult = ... // Similar to addResult, changing service call
    let markReadResult = ... // Similar to addResult, changing service call
    let markUnreadResult = ... // Similar to addResult, changing service call
    
    self.book = Observable.of(addResult, removeResult, markReadResult, markUnreadResult).merge()
        .startWith(.success(book))
}

我制作了几个自定义运算符来帮助我管理Variable&lt;Book&gt;,一个得到真正的Book

private extension ObservableType where E == Void {
    func mapBookFrom(_ variable: Variable<Book>) -> Observable<Book> {
        return map({ _ in return variable.value })
    }
}

另一个在服务返回后更新Variable

private extension ObservableType where E == BookResult<Book> {
    func updateBookVariable(_ variable: Variable<Book>) -> Observable<BookResult<Book>> {
        return self.do(onNext: { result in
            if case let .success(book) = result {
                variable.value = book
            }
        })
    }
}

现在我有一个非常干净的视图模型,但并不“完美”。

【问题讨论】:

    标签: ios mvvm reactive-programming rx-swift


    【解决方案1】:

    我会将观察模型对象(书)的变化的责任放在视图中。

    另外,Variable 已弃用,最好改用PublishRelay

    当然,这取决于你想在多大程度上设计这个架构,但离你的例子不太远的地方是:

    class BookDetailViewController: UIViewController {
        let viewModel = BookViewModel(book: Book, booksService: BooksService)
    
        func loadView() {
            view = BookDetailView(viewModel: viewModel)
        }
    
        // ...
    }
    
    class BookDetailViewModel {
        let book: PublishRelay<Book>
    
        func addBook() {
            book
                .flatMap(booksService.add)
                .bind(to: book)
                .subscribe()
        }
    
        // ...
    }
    
    class BookDetailView: UIView {
        let button: UIButton
    
        init(viewModel: BookDetailViewModel) {
            viewModel.book
                .asObservable()
                .subscribe(onNext: { book [button] in 
                   button.setText(book.isSaved ? "Remove" : "Add")
                })
    
            button.rx.tap
                .map { _ in viewModel.book.isSaved }
                .subscribe(onNext: { 
                    $0 ? viewModel.removeBook() : viewModel.addBook() 
                }) 
        }
    }
    

    您也可以在视图模型中实现func toggle(),然后只需转发按钮点击即可调用该方法。它可能在语义上更准确,具体取决于您对业务逻辑的解释以及您希望在视图模型中收集所有这些逻辑的程度。

    还要注意示例代码缺少处理包,但这是另一个主题。

    【讨论】:

    • 谢谢,我喜欢你的方法! :D 但是在PublishRelay 的状态方面我会遇到同样的问题,因为它是一个主题。我想重构它以使用 .scan 运算符而不是使用任何类型的备份变量。
    • 好的,我明白了。也许您可以进一步详细说明将模型对象封装在scan 中会得到什么好处?我不确定这就是运营商的本意。
    • 都与无国籍有关。由于我正在学习 Rx,并且 Rx 与功能范式有着密切的关系,我更愿意在第一步中获得有关这方面的所有知识。您可以通过观看 Jake Wharton 关于 Reactive State 的演讲了解更多信息,这非常有趣:youtube.com/watch?v=0IKHxjkgop4
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-22
    • 2010-11-27
    • 2010-09-05
    相关资源
    最近更新 更多