How can I simulate a tableView row selection using RxSwift - ios

I have a UITableViewController setup as below.
View Controller
class FeedViewController: BaseTableViewController, ViewModelAttaching {
var viewModel: Attachable<FeedViewModel>!
var bindings: FeedViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return FeedViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func bind(viewModel: FeedViewModel) -> FeedViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: FeedTableViewCell.reuseID, cellType: FeedTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
View Model
class FeedViewModel: ViewModelType {
let fetching: Driver<Bool>
let posts: Driver<[FeedDetailViewModel]>
let selectedPost: Driver<Post>
let errors: Driver<Error>
required init(dependency: Dependency, bindings: Bindings) {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
posts = bindings.fetchTrigger
.flatMapLatest {
return dependency.feedService.getFeed()
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriverOnErrorJustComplete()
.map { $0.map(FeedDetailViewModel.init) }
}
fetching = activityIndicator.asDriver()
errors = errorTracker.asDriver()
selectedPost = bindings.selection
.withLatestFrom(self.posts) { (indexPath, posts: [FeedDetailViewModel]) -> Post in
return posts[indexPath.row].post
}
}
typealias Dependency = HasFeedService
struct Bindings {
let fetchTrigger: Driver<Void>
let selection: Driver<IndexPath>
}
}
I have binding that detects when a row is selected, a method elsewhere is triggered by that binding and presents a view controller.
I am trying to assert that when a row is selected, a view is presented, however I cannot successfully simulate a row being selected.
My attempted test is something like
func test_ViewController_PresentsDetailView_On_Selection() {
let indexPath = IndexPath(row: 0, section: 0)
sut.start().subscribe().disposed(by: rx_disposeBag)
let viewControllers = sut.navigationController.viewControllers
let vc = viewControllers.first as! FeedViewController
vc.tableView(tableView, didSelectRowAt: indexPath)
XCTAssertNotNil(sut.navigationController.presentedViewController)
}
But this fails with the exception
unrecognized selector sent to instance
How can I simulate the row is selected in my unit test?

The short answer is you don't. Unit tests are not a place for UIView objects and UI object manipulation. You want to add a UI Testing target for that sort of test.

Related

How to update data at cell in MVVM?

I'm making a Twitter clone app.
If I press the like button on the posts in each cell, I want to reflect the number of likes to increase. Also, I want to make it reflect in the cell as soon as I delete or modify the post. There is also a video player in the cell like Twitter, but every time I press the like button, the ViewModel accepts again, which causes the video that was running to turn off. Please tell me the best way to implement the Like button...
Cell.swift
func bind(_ viewModel: PostItemViewModel) {
.....
likesLabel.text = String(viewModel.likes)
....
}
....
likesBtn.rx.tapGesture().when(.ended).mapToVoid().asSignalOnErrorJustComplete()
.emit(onNext: { [unowned self] in
delegate?.pressLikesBtn(tag)
}).disposed(by: disposeBag)
ViewController.swift
class PostsViewController: UIViewController {
private let pressLikesRelay = PublishRelay<Int>()
private let deleteRelay = PublishRelay<Int>()
private let disposeBag = DisposeBag()
var viewModel: PostsViewModel!
.......
.......
private func bindViewModel() {
assert(viewModel != nil)
let viewWillAppear = rx.viewWillAppear
.mapToVoid()
.asSignalOnErrorJustComplete()
let pull = timeLineTableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asSignal()
let reachedBottom = timeLineTableView.rx.reachedBottom().asSignal()
let input = PostsViewModel.Input(fetchInitial: Signal.merge(viewWillAppear, pull), fetchNext: reachedBottom, likesTrigger: pressLikesRelay.asSignal(), deleteTrigger: deleteRelay.asSignal())
let output = viewModel.transform(input: input)
output.posts.drive(timeLineTableView.rx.items(cellIdentifier: PostTableViewCell.identifier, cellType: PostTableViewCell.self)) {
row, item, cell in
cell.delegate = self
cell.bind(item)
cell.tag = row
}.disposed(by: disposeBag)
}
extension PostsViewController: PostTableCellDelegate {
func pressLikesBtn(_ index: Int) {
pressRelay.accept(index)
}
func deletePost(_ index: Int) {
deleteRelay.accept(index)
}
}
ViewModel.swift
final class PostsViewModel: ViewModelType {
...
private let postsRelay = BehaviorRelay<[PostItemViewModel]>(value: [])
.....
func transform(input: Input) -> Output {
.......
input.likesTrigger.emit(onNext: { [unowned self] index in
var updated = self.postsRelay.value
updated[index].plusLikes()
self.postsRelay.accept(updated)
}).disposed(by: disposeBag)
input.deleteTrigger.emit(onNext: { [unowned self] index in
var deleted = self.postsRelay.value
deleted.remove(at: index)
self.postsRelay.accept(deleted)
}).disposed(by: disposeBag)
return Output(
...,
posts: postsRelay.asDriver(),
...)
}
}
PostItemViewModel.swift
public struct PostItemViewModel {
.....
var likes: Int
....
mutating func plusLikes() {
self.vote1 +=1
}
}
Here is a sample app that shows how to do what you want. RxMultiCounter.
The key is to only update the table view when you add or remove cells. Each cell needs to independently observe the state and when the inner state of the cell changes, the cell updates directly.

Cannot get my cell to update with user edited info in diffable tableView

I am trying to create a Diffable tableView who's cells can be updated (info and layout) by the user (ie- hiding or showing a label or image that holds an optional property) in a separate editing tableview attached to the cell through a segue. I started by adding a backingStore property who's values are a custom ListItem class. The class is hashable with a UUID() as one of the properties.
class ListDrillDownTableViewController: UITableViewController {
fileprivate var dataSource: ListDrillDownDataSource!
fileprivate var currentSnapshot: ListDrillDownSnaphot?
var list: List = List(name: "Test List", listStyle: .preSetStyle[0], listItems: [], users: [])
var backingStore = [UUID: ListItem]()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = createDataSource()
updateUI(animated: false)
}
Then I created a method to create the dataSource giving the cell provider the UUID to look up the listItem. I also updated the UI with the initial snapshot populating the UUIDs and the backingStore.
fileprivate func createDataSource() -> ListDrillDownDataSource {
let dataSource = ListDrillDownDataSource(tableView: tableView) { (tableView, indexPath, uuid) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "ListItemCell", for: indexPath) as! ListItemCell
guard let listItem = self.backingStore[uuid] else { return cell }
cell.titleLabel.text = listItem.title
if let cost = listItem.cost {
cell.costLabel.isHidden = false
cell.costLabel.text = (listItem.costString(cost: cost))
} else {
cell.costLabel.isHidden = true
}
if listItem.note == "" {
cell.noteIcon.isHidden = true
} else {
cell.noteIcon.isHidden = false
}
return cell
}
self.tableView.dataSource = dataSource
return dataSource
}
func updateUI(animated: Bool = true) {
var snapshot = ListDrillDownSnaphot()
snapshot.appendSections(["main"])
let uuids = self.list.listItems.map { _ in UUID() }
snapshot.appendItems(uuids)
for (uuid, listItem) in zip(uuids, list.listItems) {
self.backingStore[uuid] = listItem
}
self.dataSource.apply(snapshot, animatingDifferences: animated)
}
Finally, when the user taps a cell, makes there edits and taps done the view unwinds back to this controller and I attempt to update the dataSource in the unwind method. As it stands the app will create the first new cell but when a second is attempted it reloads the first row twice and as I keep adding it doubles the existing rows. I'm guessing because I'm appending the whole list over and over? I can't figure out how to access JUST the appended uuid.
When I tap into an existing cell and edit the info it works, but if I go in again to the row and come out the cell goes back to it's original state. If I keep trying it bounces back and forth between the original state and the updated, almost like it is bouncing back and forth between snapshots? I feel like I'm missing something obvious but I can't seem to figure it out. Any thoughts?
#IBAction func unwindToListDrillDownTableView(_ unwindSegue: UIStoryboardSegue) {
guard unwindSegue.identifier == "DoneAddEditListItemUnwind" else { return }
let sourceViewController = unwindSegue.source as! ListItemDetailTableViewController
self.list.listStyle?.categories = sourceViewController.currentCategories
if let listItem = sourceViewController.listItem {
if let indexOfExistingListItem = list.listItems.firstIndex(of: listItem) {
// If existing, update the ListItem and then the view.
list.listItems[indexOfExistingListItem] = listItem
let uuid = self.dataSource.itemIdentifier(for: IndexPath(row: indexOfExistingListItem, section: 0))
let uuids = self.list.listItems.map { _ in UUID() }
var snapshot = self.dataSource.snapshot()
snapshot.reloadItems([uuid!])
for (uuid, listItem) in zip(uuids, list.listItems) {
self.backingStore[uuid] = listItem
}
self.dataSource.apply(snapshot, animatingDifferences: true)
} else {
list.listItems.append(listItem)
if self.backingStore.keys.isEmpty {
updateUI()
} else {
var snapshot = self.dataSource.snapshot()
let uuids = self.list.listItems.map { _ in UUID() }
snapshot.reloadSections(["main"])
snapshot.appendItems(uuids)
for (uuid, listItem) in zip(uuids, list.listItems) {
self.backingStore[uuid] = listItem
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}

How edit/delete UICollectionView cells using MVVM and RxSwift

I am trying to understand how to implement MVVM with a list of objects and an UICollectionView. I am not understanding how to implement the User iteration -> Model flow.
I have setup a test application, the Model is just a class with an Int, and the View is an UICollectionViewCell which shows a text with the corresponding Int value and have plus, minus and delete buttons to increment, decrease and remove a element respectively.
Each entry looks like:
I would like to know the best way to use MVVM and RxSwift the update/remove a cell.
I have a list random generated Int values
let items: [Model]
the Model which just have the Int value
class Model {
var number: Int
init(_ n: Int = 0) {
self.number = n
}
}
The ViewModel class which just hold the Model and has an Observable
class ViewModel {
var value: Observable<Model>
init(_ model: Model) {
self.value = Observable.just(model)
}
}
And the Cell
class Cell : UICollectionViewCell {
class var identifier: String { return "\(self)" }
var bag = DisposeBag()
let label: UILabel
let plus: UIButton
let minus: UIButton
let delete: UIButton
....
var viewModel: ViewModel? = nil {
didSet {
....
viewModel.value
.map({ "number is \($0.number)" })
.asDriver(onErrorJustReturn: "")
.drive(self.label.rx.text)
.disposed(by: self.bag)
....
}
}
}
What I don't understand clearly how to do is how to connect the buttons to the corresponding action, update the model and the view afterwards.
Is the Cell's ViewModel responsible for this? Should it be the one receiving the tap event, updating the Model and then the view?
In the remove case, the cell's delete button needs to remove the current Model from the data list. How can this be done without mixing everything all together?
Here is the project with the updates below in GitHub: https://github.com/dtartaglia/RxCollectionViewTester
The first thing we do is to outline all our inputs and outputs. The outputs should be members of the view model struct and the inputs should be members of an input struct.
In this case, we have three inputs from the cell:
struct CellInput {
let plus: Observable<Void>
let minus: Observable<Void>
let delete: Observable<Void>
}
One output for the cell itself (the label) and two outputs for the cell's parent (presumably the view controller's view model.)
struct CellViewModel {
let label: Observable<String>
let value: Observable<Int>
let delete: Observable<Void>
}
Also we need to setup the cell to accept a factory function so it can create a view model instance. The cell also needs to be able to reset itself:
class Cell : UICollectionViewCell {
var bag = DisposeBag()
var label: UILabel!
var plus: UIButton!
var minus: UIButton!
var delete: UIButton!
// code to configure UIProperties omitted.
override func prepareForReuse() {
super.prepareForReuse()
bag = DisposeBag() // this resets the cell's bindings
}
func configure(with factory: #escaping (CellInput) -> CellViewModel) {
// create the input object
let input = CellInput(
plus: plus.rx.tap.asObservable(),
minus: minus.rx.tap.asObservable(),
delete: delete.rx.tap.asObservable()
)
// create the view model from the factory
let viewModel = factory(input)
// bind the view model's label property to the label
viewModel.label
.bind(to: label.rx.text)
.disposed(by: bag)
}
}
Now we need to build the view model's init method. This is where all the real work happens.
extension CellViewModel {
init(_ input: CellInput, initialValue: Int) {
let add = input.plus.map { 1 } // plus adds one to the value
let subtract = input.minus.map { -1 } // minus subtracts one
value = Observable.merge(add, subtract)
.scan(initialValue, accumulator: +) // the logic is here
label = value
.startWith(initialValue)
.map { "number is \($0)" } // create the string from the value
delete = input.delete // delete is just a passthrough in this case
}
}
You will notice that the view model's init method needs more than what is provided by the factory function. The extra info will be provided by the view controller when it creates the factory.
The view controller will have this in its viewDidLoad:
viewModel.counters
.bind(to: collectionView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { index, element, cell in
cell.configure(with: { input in
let vm = CellViewModel(input, initialValue: element.value)
// Remember the value property tracks the current value of the counter
vm.value
.map { (id: element.id, value: $0) } // tell the main view model which counter's value this is
.bind(to: values)
.disposed(by: cell.bag)
vm.delete
.map { element.id } // tell the main view model which counter should be deleted
.bind(to: deletes)
.disposed(by: cell.bag)
return vm // hand the cell view model to the cell
})
}
.disposed(by: bag)
For the above example I assume that:
counters is of type Observable<[(id: UUID, value: Int)]> and comes from the view controller's view model.
values is of type PublishSubject<(id: UUID, value: Int)> and is input into the view controller's view model.
deletes is of type PublishSubject<UUID> and is input into the view controller's view model.
The construction of the view controller's view model follows the same pattern as the one for the cell:
Inputs:
struct Input {
let value: Observable<(id: UUID, value: Int)>
let add: Observable<Void>
let delete: Observable<UUID>
}
Outputs:
struct ViewModel {
let counters: Observable<[(id: UUID, value: Int)]>
}
Logic:
extension ViewModel {
private enum Action {
case add
case value(id: UUID, value: Int)
case delete(id: UUID)
}
init(_ input: Input, initialValues: [(id: UUID, value: Int)]) {
let addAction = input.add.map { Action.add }
let valueAction = input.value.map(Action.value)
let deleteAction = input.delete.map(Action.delete)
counters = Observable.merge(addAction, valueAction, deleteAction)
.scan(into: initialValues) { model, new in
switch new {
case .add:
model.append((id: UUID(), value: 0))
case .value(let id, let value):
if let index = model.index(where: { $0.id == id }) {
model[index].value = value
}
case .delete(let id):
if let index = model.index(where: { $0.id == id }) {
model.remove(at: index)
}
}
}
}
}
I'm doing it this way:
ViewModel.swift
import Foundation
import RxSwift
import RxCocoa
typealias Model = (String, Int)
class ViewModel {
let disposeBag = DisposeBag()
let items = BehaviorRelay<[Model]>(value: [])
let add = PublishSubject<Model>()
let remove = PublishSubject<Model>()
let addRandom = PublishSubject<()>()
init() {
addRandom
.map { _ in (UUID().uuidString, Int.random(in: 0 ..< 10)) }
.bind(to: add)
.disposed(by: disposeBag)
add.map { newItem in self.items.value + [newItem] }
.bind(to: items)
.disposed(by: disposeBag)
remove.map { removedItem in
self.items.value.filter { (name, _) -> Bool in
name != removedItem.0
}
}
.bind(to: items)
.disposed(by: disposeBag)
}
}
Cell.swift
import Foundation
import Material
import RxSwift
import SnapKit
class Cell: Material.TableViewCell {
var disposeBag: DisposeBag?
let nameLabel = UILabel(frame: .zero)
let valueLabel = UILabel(frame: .zero)
let removeButton = FlatButton(title: "REMOVE")
var model: Model? = nil {
didSet {
guard let (name, value) = model else {
nameLabel.text = ""
valueLabel.text = ""
return
}
nameLabel.text = name
valueLabel.text = "\(value)"
}
}
override func prepare() {
super.prepare()
let textWrapper = UIStackView()
textWrapper.axis = .vertical
textWrapper.distribution = .fill
textWrapper.alignment = .fill
textWrapper.spacing = 8
nameLabel.font = UIFont.boldSystemFont(ofSize: 24)
textWrapper.addArrangedSubview(nameLabel)
textWrapper.addArrangedSubview(valueLabel)
let wrapper = UIStackView()
wrapper.axis = .horizontal
wrapper.distribution = .fill
wrapper.alignment = .fill
wrapper.spacing = 8
addSubview(wrapper)
wrapper.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(8)
}
wrapper.addArrangedSubview(textWrapper)
wrapper.addArrangedSubview(removeButton)
}
}
ViewController.swift
import UIKit
import Material
import RxSwift
import SnapKit
class ViewController: Material.ViewController {
let disposeBag = DisposeBag()
let vm = ViewModel()
let tableView = UITableView()
let addButton = FABButton(image: Icon.cm.add, tintColor: .white)
override func prepare() {
super.prepare()
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
addButton.pulseColor = .white
addButton.backgroundColor = Color.red.base
view.layout(addButton)
.width(48)
.height(48)
.bottomRight(bottom: 16, right: 16)
addButton.rx.tap
.bind(to: vm.addRandom)
.disposed(by: disposeBag)
tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
vm.items
.bind(to: tableView.rx.items) { (tableView, row, model) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.model = model
cell.disposeBag = DisposeBag()
cell.removeButton.rx.tap
.map { _ in model }
.bind(to: self.vm.remove)
.disposed(by: cell.disposeBag!)
return cell
}
.disposed(by: disposeBag)
}
}
Note that a common mistake is creating the DisposeBag inside Cell only once, this will causing confusing when the action got triggered.
The DisposeBag must be re-created every time the Cell got reused.
A complete working example can be found here.

RxSwift correct way to subscribe to data model property change

New to RxSwift here. I have a (MVVM) view model that represents a Newsfeed-like page, what's the correct way to subscribe to change in data model's properties? In the following example, startUpdate() constantly updates post. The computed properties messageToDisplay and shouldShowHeart drives some UI event.
struct Post {
var iLiked: Bool
var likes: Int
...
}
class PostViewModel: NSObject {
private var post: Post
var messageToDisplay: String {
if post.iLiked { return ... }
else { return .... }
}
var shouldShowHeart: Bool {
return iLiked && likes > 10
}
func startUpdate() {
// network request and update post
}
...
}
It seems to me in order to make this whole thing reactive, I have to turn each properties of Post and all computed properties into Variable? It doesn't look quite right to me.
// Class NetworkRequest or any name
static func request(endpoint: String, query: [String: Any] = [:]) -> Observable<[String: Any]> {
do {
guard let url = URL(string: API)?.appendingPathComponent(endpoint) else {
throw EOError.invalidURL(endpoint)
}
return manager.rx.responseJSON(.get, url)
.map({ (response, json) -> [String: Any] in
guard let result = json as? [String: Any] else {
throw EOError.invalidJSON(url.absoluteString)
}
return result
})
} catch {
return Observable.empty()
}
}
static var posts: Observable<[Post]> = {
return NetworkRequest.request(endpoint: postEndpoint)
.map { data in
let posts = data["post"] as? [[String: Any]] ?? []
return posts
.flatMap(Post.init)
.sorted { $0.name < $1.name }
}
.shareReplay(1)
}()
class PostViewModel: NSObject {
let posts = Variable<[Post]>([])
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
posts
.asObservable()
.subscribe(onNext: { [weak self] _ in
DispatchQueue.main.async {
//self?.tableView?.reloadData() if you want to reload all tableview
self.tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation.none) // OR if you want to insert one or multiple rows.
//OR update UI
}
})
.disposed(by: disposeBag)
posts.asObservable()
.bind(to: tableView.rx.items) { [unowned self] (tableView: UITableView, index: Int, element: Posts) in
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell") as! PostCell
//for example you need to update the view model and cell with textfield.. if you want to update the ui with a cell then use cell.button.tap{}. hope it works for you.
cell.textField.rx.text
.orEmpty.asObservable()
.bind(to: self.posts.value[index].name!)
.disposed(by: cell.disposeBag)
return cell
}
startDownload()
}
}
func startDownload {
posts.value = NetworkRequest.posts
}
If you want to change anything then use subscribe, bind, concat .. There are many methods and properties which you can use.

Dynamically filter results with RxSwift and Realm

I have a very simple project, where I want to dynamically filter content in UITableView regarding pressed index in UISegmentedControl. I'm using MVVM with RxSwift, Realm and RxDataSources. So my problem, that if I want to update content in UITableView I need to create 'special' DisposeBag, only for that purposes, and on each selection in UISegmentedControl nil it and create again. Only in this case, if I'm understand right, subscription is re-newed, and UITableView displays new results from Realm.
So is there any better way to do such operation? Without subscribing every time, when I switch tab in UISegmentedControl. Here's my code:
//ViewController
class MyViewController : UIViewController {
//MARK: - Props
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var segmentedControl: UISegmentedControl!
let dataSource = RxTableViewSectionedReloadDataSource<ItemsSection>()
let disposeBag = DisposeBag()
var tableViewBag: DisposeBag!
var viewModel: MyViewModel = MyViewModel()
//MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.setupRxTableView()
}
//MARK: - Setup observables
fileprivate func setupRxTableView() {
dataSource.configureCell = { ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "ItemCell") as! ItemTableViewCell
return cell
}
bindDataSource()
segmentedControl.rx.value.asDriver()
.drive(onNext: {[weak self] index in
guard let sSelf = self else { return }
switch index {
case 1:
sSelf.bindDataSource(filter: .active)
case 2:
sSelf.bindDataSource(filter: .groups)
default:
sSelf.bindDataSource()
}
}).disposed(by: disposeBag)
}
private func bindDataSource(filter: Filter = .all) {
tableViewBag = nil
tableViewBag = DisposeBag()
viewModel.populateApplying(filter: filter)
}).bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: tableViewBag)
}
}
//ViewModel
class MyViewModel {
func populateApplying(filter: Filter) -> Observable<[ItemsSection]> {
return Observable.create { [weak self] observable -> Disposable in
guard let sSelf = self else { return Disposables.create() }
let realm = try! Realm()
var items = realm.objects(Item.self).sorted(byKeyPath: "date", ascending: false)
if let predicate = filter.makePredicate() { items = items.filter(predicate) }
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
sSelf.itemsToken = items.addNotificationBlock { changes in
switch changes {
case .update(_, _, _, _):
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
default: break
}
}
return Disposables.create()
}
}
}
Don't recall if this is breaking MVVM off the top of my head, but would Variable not be what you're looking for?
Variable<[TableData]> data = new Variable<[TableData]>([])
func applyFilter(filter: Predicate){
data.value = items.filter(predicate) //Any change to to the value will cause the table to reload
}
and somewhere in the viewController
viewModel.data.rx.asDriver().drive
(tableView.rx.items(cellIdentifier: "ItemCell", cellType: ItemTableViewCell.self))
{ row, data, cell in
//initialize cells with data
}

Resources