RxSwift,使用.scan来跟踪对象的状态

时间:2018-05-02 10:51:01

标签: ios mvvm reactive-programming rx-swift

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

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

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

问题在于我必须跟踪书中的变化才能更改用户界面:例如,在删除书之后,按钮之前会显示"删除"现在它必须说"添加"。

我使用作为Variable<Book>向观察者公开的Driver<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<Book>并从方法Driver<Book>返回set(toggleInShelfTrigger: Observable<Void>)我就不会有这个烂摊子但是我不知道我是否有添加或删除图书。

那么,在这种应用程序中跟踪对象状态的真实世界方法是什么?如何通过仅使用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<Book>,一个用来获得真正的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
            }
        })
    }
}

现在我有一个非常干净的视图模型,但不是&#34;完美&#34;。

1 个答案:

答案 0 :(得分:1)

我会将观察更改的责任与视图放在模型对象(Book)上。

此外,不推荐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(),只需转发按钮即可调用该方法。从语义上讲,它可能更准确,具体取决于您对业务逻辑的解释以及您希望在视图模型中收集所有逻辑的程度。

另请注意,示例代码缺少dispose包,但这是另一个主题。