2

I have a simple signal, in one of the app components, that returns an array of items:

var itemsSignal: Signal<[Item], Never>

Those items might contain updates for the data that are rendered on the screen in a form of a table view. The task is to apply the updates to the cells if they are present on the screen.

There are two possible ways, that I can think of, on how this can be done. The app is written in MVVM style, but I will simply for the purposes of example.

The first way is to subscribe to this signal once on a level of view controller code, then in an observeValues block check wherever we receive the updates for the items on screen with some for-loop and update the states for the corresponding cells. This way we will have only a single subscription, but this introduce unnecessary, in my mind, code coupling, when we basically use view controller level code to pass updates from the source to individual cells on screen.

The second way is to subscribe to this signal from each individual cell (in reality cell's view model) and apply some filtering like this:

disposables += COMPONENT.itemsSignal
    .flatten()
    .filter({ $0.itemId == itemId })
    .observeValues({ 
        ... 
    })

But this creates multiple subscriptions - one for each individual cell.

I actually prefer the second method, as it much cleaner from a design stand point, in my mind, as it doesn't leak any unnecessary knowledge to view controller level code. And wherever the same cell is re-used on a different screen this self-updating behaviour will be inherited and will work out of the box.

The question is how much more memory/cpu expensive is the second method due to multiple subscriptions? In this project we use ReactiveSwift, but I think this is relevant for other Rx libraries as well.

danylokos
  • 1,464
  • 15
  • 20

2 Answers2

1

Purely from a reactive programming perspective, I understand why the second approach is attractive. But the nature of UITableView is such that you really need to involve the table in row updates. The easiest way to do this is by using using the new-fangled UITableViewDiffableDataSource introduced in iOS 13, but if you're not using that then must call either reloadData or reloadRows(at:) to tell the table to update all rows or specific rows when their data changes.

If you subscribe each cell then it might appear to work in certain or most situations, but if you ever have a data update that changes the height of a cell (e.g. by displaying longer text in a label and causing it to wrap to a second line) then the cell will not resize properly because the table doesn't know that the row was updated.

I agree with you that it is unpleasant that rows can't manage their own updates in a perfectly self-contained manner, but UITableView works that way for performance reasons as far as I understand.

jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • The content height isn’t a factor here, I’m curious only about memory footprint and maybe some other overhead of both solutions. – danylokos Jan 11 '21 at 00:35
  • If you create the subscription in `tableView(_:cellForRowAt:)` and cancel it in when the cell is reused (maybe with something like `.take(until: cell.reactive.prepareForReuse)`?), I can't imagine the memory footprint would be much of a problem, because there will only be as many subscriptions as there are cells visible on screen. But if you're trying to create and subscribe every cell's view model in advance, that would definitely create unnecessary overhead, depending on how many items there are. – jjoelson Jan 11 '21 at 00:50
  • But that would also probably require you to make `itemSignal` a `MutableProperty<[Item]>` so you could always get the latest item array in `tableView(_:cellForRowAt:)`. – jjoelson Jan 11 '21 at 00:51
1

In RxSwift, our RxCocoa library already implements your first idea and does a simple reloadData on the tableView.

items
    .bind(to: tableView.rx.items) { (tableView, row, element) in
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
        cell.textLabel?.text = "\(element) @ row \(row)"
        return cell
    }

or you can tell the library what type the cell will be and it will create the cell for you:

items
    .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
        cell.textLabel?.text = "\(element) @ row \(row)"
    }

We also have an option which you didn't mention. A single subscription but a smarter data source that is capable of adding and removing individual cells based on equality of the elements of the sequence being passed in. That is in a separate library called RxDataSources.

As an answer to your basic question about resources... I often use a hybrid solution where there is an Observable that just contains a Sequence of object IDs; this is your first idea, but it only takes care of item insertion and removal. I will make a second Observable of [ID: Info] that each currently existing cell subscribes to. At cell creation/reuse, it is given an ID and subscribes to this second observable and filters out just the the info it's interested in. In the cell's prepareForReuse it unsubscribes.

Since it's only one subscription per existing cell, it really isn't that many new subscriptions (depending on the height of the cell and the height of the table view.) My apps routinely have thousands of subscriptions running at any one time, so the number of extra subscriptions added from this "per cell" approach isn't even noticeable.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72