RxSwift, используйте .scan для отслеживания состояния объекта - PullRequest
0 голосов
/ 02 мая 2018

Я знаю, что государство - враг Реактивного программирования, но я имею дело с ним в процессе изучения RxSwift.

Мое приложение очень простое: первый экран представляет собой список и поиск книг, а второй - подробное описание книги, в которой вы можете добавить / удалить книгу на полку и отметить ее как чтение / непрочитанных .

Чтобы показать детали книги, я создаю BookViewModel, передавая BooksService для выполнения сетевых операций и текущий Book для отображения.

Проблема в том, что мне нужно отслеживать изменения в книге, чтобы изменить пользовательский интерфейс: например, после удаления книги кнопка, которая ранее говорила «Удалить», теперь должна сказать «Добавить».

Я добиваюсь этого поведения, используя Variable<Book>, выставляемый наблюдателям как Driver<Book>, но я много путаюсь с ним, когда операция сети возвращается, и мне нужно обновить значение Variable<Book>, чтобы вызвать обновление пользовательского интерфейса.

Это инициализатор модели представления:

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?

EDIT

Мне удалось очистить этот дерьмовый код, но я все еще пытаюсь достичь состояния без 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
            }
        })
    }
}

Теперь у меня очень чистый вид модели, но не "идеальный".

1 Ответ

0 голосов
/ 16 мая 2018

Я бы возложил ответственность за наблюдение изменений на модельном объекте (Книге) на представление.

Кроме того, 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() в модели представления, и просто нажать кнопку, чтобы вызвать этот метод. Это может быть более точным, семантически, в зависимости от вашей интерпретации бизнес-логики и степени, в которой вы хотите собрать все это в модели представления.

Также обратите внимание, что в примере кода отсутствуют сумки для утилизации, но это уже другая тема.

...