You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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: toggleBookshelf and toggleReadStatus are PublishSubjects 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 Book state.
  • Scan Operator: scan takes the initial Book state 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 flatMapLatest to send the updated state to your BooksService. 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 to currentBook without 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

  • scan is 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:24:44