swifttableviewrx-swiftrxdatasources

Binding to a datasource using RxDatasources


I have a list of models that I fetch from a server, and basically I get array of these:

struct ContactModel: Codable, Equatable {
static func == (lhs: ContactModel, rhs: ContactModel) -> Bool {
    return lhs.name == rhs.name &&
    lhs.avatar == rhs.avatar &&
    lhs.job == rhs.job &&
    lhs.follow == rhs.follow
}

let id: Int
let name: String
let avatar:String?
let job: JobModel?
let follow:Bool

enum CodingKeys: String, CodingKey {
    case id, name, avatar, job, follow
}

}

So I want to show a list of contacts in my tableview.

Now I have this struct as well which is wrapper around this model:

struct ContactCellModel : Equatable, IdentifiableType {
    
    static func == (lhs: ContactCellModel, rhs: ContactCellModel) -> Bool {
        
        return lhs.model == rhs.model
    }
    
    var identity: Int {
        return model.id
    }
    
    var model: ContactModel
    var cellIdentifier = ContactTableViewCell.identifier
}

What I am trying to do, is to create datasource using RxDatasources, and bind to it, like this(ContactsViewController.swift):

let dataSource = RxTableViewSectionedAnimatedDataSource<ContactsSectionModel>(
            configureCell: { dataSource, tableView, indexPath, item in
                if let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseTableViewCell{
                    cell.setup(data: item.model)
                    return cell
                }
                
                return UITableViewCell()
                
            })

but I am not sure what I should do right after. I tried something like this:

Observable.combineLatest(contactsViewModel.output.contacts, self.contactViewModel.changedStatusForContact)
            .map{ (allContacts, changedContact) -> ContactsSectionModel in
               
               //what should I return here?
            }.bind(to: dataSource)

I use combineLatest, cause I have one more observable (self.contactViewModel.changedStatusForContact) that notifies when certain contact has been changed (this happens when you tap a certain button on a contact cell).

So what should I return from .map above in order to successfully bind to previously created dataSource?


Solution

  • You have to replace the contacts that have changed; all the contacts that were changed, but you can't do that because you aren't tracking all of them, only the most recent one. So you can't do it in a map. You need to use scan instead.

    I made a lot of assumptions about code you didn't post, so the below is a compilable example. If your types are different, then you will have to make some changes:

    func example(contacts: Observable<[ContactModel]>, changedStatusForContact: Observable<ContactModel>, tableView: UITableView, disposeBag: DisposeBag) {
        let dataSource = RxTableViewSectionedAnimatedDataSource<ContactsSectionModel>(
            configureCell: { dataSource, tableView, indexPath, item in
                if let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseTableViewCell {
                    cell.setup(data: item.model)
                    return cell
                }
                return UITableViewCell() // this is quite dangerious. Better would be to crash IMO.
    
            })
    
        let contactsSectionModels = Observable.combineLatest(contacts, changedStatusForContact) {
            (original: $0, changed: $1)
        }
            .scan([ContactsSectionModel]()) { state, updates in
                // `state` is the last array handed to the table view.
                // `updates` contains the values from the combineLatest above.
                var contactModels = state.flatMap { $0.items.map { $0.model } }
                // get all the contactModels out of the state.
                if contactModels.isEmpty {
                    contactModels = updates.original
                }
                // if there aren't any, then update with the values coming from `contacts`
                else {
                    guard let index = contactModels
                            .firstIndex(where: { $0.id == updates.changed.id })
                    else { return state }
                    contactModels[index] = updates.changed
                }
                // otherwise find the index of the contact that changed.
                // and update the array with the changed contact.
                return [ContactsSectionModel(
                    model: "",
                    items: updates.original
                        .map { ContactCellModel(model: $0) }
                )]
                // rebuild the section model and return it.
            }
    
        contactsSectionModels
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
    
    typealias ContactsSectionModel = AnimatableSectionModel<String, ContactCellModel>
    
    struct ContactCellModel: Equatable, IdentifiableType {
        var identity: Int { model.id }
        let model: ContactModel
        let cellIdentifier = ContactTableViewCell.identifier
    }
    
    struct ContactModel: Equatable {
        let id: Int
        let name: String
        let avatar: String?
        let job: JobModel?
        let follow: Bool
    }
    
    struct JobModel: Equatable { }
    
    class BaseTableViewCell: UITableViewCell {
        func setup(data: ContactModel) { }
    }
    
    class ContactTableViewCell: BaseTableViewCell {
        static let identifier = "Cell"
    }