Mando
Mando

Reputation: 11722

RxSwift: Remove a row in UITableView without reloading whole table

I can achieve this when not using RxSwift by doing the following. It works great, plays needed animation on the row being removed and doesn't reload the whole table:

let item = self.dataSource[(indexPath as NSIndexPath).row]
self.dataSource.remove(at: (indexPath as NSIndexPath).row)
tableView.deleteRows(at: [indexPath], with: .right)

With RxSwift I'm binding data to the tableView by doing this and when I accept new array, it reloads the whole table and the remove animation is not played:

// ViewModel:
let items: BehaviorRelay<[Item]> = BehaviorRelay(value: [])

func removeItem(_ item: Item) {
    var loadedItems = items.value
    if let removeIndex = loadedItems.firstIndex(of: item) {
        loadedItems.remove(at: removeIndex)
    }
    items.accept(loadedItems)
}

//ViewController:
viewModel.items
    .bind(to: tableView.rx.items) { [weak self] (table, index, item) in
        // Creating cell for the `item`....
        return cell   
    }
    .disposed(by: self.disposeBag)

How can I update the viewModel.item source to remove a single element and make sure that the remove animation is playing on the tableview?

Upvotes: 1

Views: 213

Answers (1)

Daniel T.
Daniel T.

Reputation: 33979

There is a library called RxDataSources that implements this very thing. Or you can do it yourself if your needs are simple. Just implement the one required method.

Here's an example:

class Example: NSObject, RxTableViewDataSourceType, UITableViewDataSource {
    private var items = [Item]()

    func tableView(_ tableView: UITableView, observedEvent: RxSwift.Event<[Item]>) {
        switch observedEvent {
        case let .next(items):
            // figure out which items have been added and which have been removed.
            // animate the table view as usual.
            // then:
            self.items = items
        default:
            break
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // build a cell as usual.
    }
}

You would use this by simply binding with it, like this:

func example(source: Observable<[Item]>, tableView: UITableView, disposeBag: DisposeBag) {
    source
        .bind(to: tableView.rx.items(dataSource: Example()))
        .disposed(by: disposeBag)
}

You would add/remove cells by emitting a new array on the source observable.


Here is a complete example using RxDataSources if you choose to use it:

typealias ItemSection = AnimatableSectionModel<String, Item>

func example(source: Observable<[ItemSection]>, tableView: UITableView, disposeBag: DisposeBag) {
    let dataSource = RxTableViewSectionedAnimatedDataSource<ItemSection>(
        animationConfiguration: AnimationConfiguration(deleteAnimation: .right), // since you specified right delete in your question
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyTableViewCell
            cell.configure(with: item)
            return cell
        }
    )

    source
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)
}

struct Item: IdentifiableType, Equatable {
    let identity: Int
    let name: String
}

final class MyTableViewCell: UITableViewCell {
    func configure(with item: Item) {
        self.textLabel?.text = item.name
    }
}

Note: in neither case do you need to store a property of the data source, the RxCocoa library will take care of that for you.

Upvotes: 1

Related Questions