iosswiftrx-swiftreactive-swift

ReactiveSwift one vs multiple signal subscriptions and related memory overhead


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.


Solution

  • 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.