RxSwift使用.scan跟踪书籍对象状态的技术咨询
Great question! Tracking mutable state like a book's shelf status or read status is exactly where RxSwift's scan operator shines—it lets you build a continuous stream of state updates by applying each user action to the current state. Let's break this down step by step for your BookViewModel.
First, Define an Immutable Book Model (Best Practice)
To avoid unexpected side effects, use an immutable Book struct with helper methods to generate updated copies of the object. This aligns with reactive programming principles where state changes are explicit:
struct Book { let id: String let title: String let author: String let isInBookshelf: Bool let isRead: Bool // Helper methods to create updated copies of the book func togglingBookshelf() -> Book { Book( id: id, title: title, author: author, isInBookshelf: !isInBookshelf, isRead: isRead ) } func togglingReadStatus() -> Book { Book( id: id, title: title, author: author, isInBookshelf: isInBookshelf, isRead: !isRead ) } }
Build the BookViewModel with Scan
Your view model will expose inputs (user actions like toggling shelf status) and outputs (the latest state of the book). We'll use scan to accumulate state changes from these actions:
import RxSwift import RxCocoa class BookViewModel { // MARK: - Inputs let toggleBookshelf = PublishSubject<Void>() let toggleReadStatus = PublishSubject<Void>() // MARK: - Outputs let currentBook: Observable<Book> // MARK: - Dependencies private let initialBook: Book private let booksService: BooksService private let disposeBag = DisposeBag() init(initialBook: Book, booksService: BooksService) { self.initialBook = initialBook self.booksService = booksService // 1. Convert user actions into state update closures let stateUpdateEvents = Observable.merge( toggleBookshelf.map { _ in { (book: Book) -> Book in book.togglingBookshelf() }}, toggleReadStatus.map { _ in { (book: Book) -> Book in book.togglingReadStatus() }} ) // 2. Use scan to accumulate state changes starting from the initial book let updatedBookStream = stateUpdateEvents .scan(initialBook) { currentState, updateClosure in updateClosure(currentState) } // 3. Sync updates with your backend (handle success/failure) currentBook = updatedBookStream .flatMapLatest { [weak self] updatedBook -> Observable<Book> in guard let self = self else { return .just(updatedBook) } // Send update to your BooksService return self.booksService.updateBook(updatedBook) .map { _ in updatedBook } // Keep the updated state if successful .catch { _ in .just(self.initialBook) } // Rollback to initial state if failed } .startWith(initialBook) // Emit the initial state immediately .share(replay: 1) // Ensure all subscribers get the latest state without reprocessing } }
How This Works
- Inputs:
toggleBookshelfandtoggleReadStatusarePublishSubjects that emit events when the user taps the corresponding buttons in the UI. - State Updates: We merge these actions into a single stream of closures, each defining how to update the current
Bookstate. - Scan Operator:
scantakes the initialBookstate and applies each update closure to it, emitting the new state every time an action occurs. This creates a continuous stream of the book's latest state. - Backend Sync: We use
flatMapLatestto send the updated state to yourBooksService. If the request succeeds, we keep the new state; if it fails, we roll back to the initial state (you could adjust this to roll back to the previous state instead if needed). - Share Replay:
share(replay: 1)ensures multiple UI components (like buttons and labels) can subscribe tocurrentBookwithout duplicating network requests or state processing.
Binding to the UI
In your book detail view controller, bind the view model's outputs to your UI elements and bind user actions to the view model's inputs:
class BookDetailViewController: UIViewController { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var authorLabel: UILabel! @IBOutlet weak var shelfButton: UIButton! @IBOutlet weak var readButton: UIButton! var viewModel: BookViewModel! private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() // Bind view model output to UI viewModel.currentBook.subscribe(onNext: { [weak self] book in self?.titleLabel.text = book.title self?.authorLabel.text = book.author self?.shelfButton.setTitle(book.isInBookshelf ? "Remove from Shelf" : "Add to Shelf", for: .normal) self?.readButton.setTitle(book.isRead ? "Mark as Unread" : "Mark as Read", for: .normal) }).disposed(by: disposeBag) // Bind UI actions to view model inputs shelfButton.rx.tap.bind(to: viewModel.toggleBookshelf).disposed(by: disposeBag) readButton.rx.tap.bind(to: viewModel.toggleReadStatus).disposed(by: disposeBag) } }
Key Takeaways
scanis perfect for tracking state that evolves over time from user actions—it turns discrete events into a continuous state stream.- Using immutable models prevents accidental state mutations and makes your state changes predictable.
- Always handle network request failures to keep your UI state in sync with your backend.
内容的提问来源于stack exchange,提问作者emenegro

