iosswiftiphonerx-swiftrxdatasources

How to create multiple Sections in UICollectionView with multiple Headers using RxDatasource


Before Anyone makes it duplicate I have scene all the RxDatasource tags on SO and other sites also. But anyone didn't worked for me.

So my question is totally related to this one that I followed for my case also. But also I have no idea what's going on in here. And its been two weeks of struggling. I have checked gitHub code sample also but was unable to understand. I have created an app using RxSwift and Realm on MVVM architecture pattern, things are working fine but now I need to create two sections in my view using UICollectioView for that I read about RxdataSource and tried to apply it but i didn't get at all whats it actually doing. I tried creating other projects for learning also those also didn't worked. Still I tried and make this code but it gives me error.

What I did is from the link provided above is in the code below. I also do not know how to give my data source the data or lists from one array after spliting it. below is my whole code.

I don't have idea what is this block doing.

//Changed
struct SectionViewModel {
    var header: String!
    var items: [StudentModel]
}

extension SectionViewModel: SectionModelType {
    typealias Item  = StudentModel
    init(original: SectionViewModel, items: [StudentModel]) {
        self = original
        self.items = items
    }
}

then my CollectionView class is like

class StudentCV: UIViewController, UICollectionViewDelegateFlowLayout {

    //MARK: - Outlets

    @IBOutlet weak var studentsView: UICollectionView!

    let studentCells = BehaviorRelay<[StudentModel]>(value: [])
    var notificationToken: NotificationToken? = nil

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let flowLayout = UICollectionViewFlowLayout()
        let size = CGSize(width: 105, height: 135)
        flowLayout.itemSize = size
        studentsView.setCollectionViewLayout(flowLayout, animated: true)
        studentsView.rx.setDelegate(self).disposed(by: disposeBag)

        setupBinding()
    }

    func studentLeft(value: Int, id: Int) {
        SignalRService.sharedClass.chatHub.invoke(method: "StudentLeft", withArgs: [id, value]){ (result, error) in
            if let e = error {
                print("Error: \(e)")
            } else {
                print("Done!")
                let vale = Database.singleton.updatePickupStatus(studentId: id, pickupValue: value)
                TestDebug.debugInfo(fileName: "", message: "STUDENT LEFTTT:: \(vale)")
                if let r = result {
                    print("Result: \(r)")
                }
            }
        }
    }

    deinit {
        notificationToken?.invalidate()
    }
    func setupBinding() {
        studentsView.register(UINib(nibName: "StudentCVCell", bundle: nil), forCellWithReuseIdentifier: "studentCV")

        //Cell creation Changed here..............................

           dataSource.configureCell = { (ds, cv, ip, item) in
            let cell = cv.dequeueReusableCell(withReuseIdentifier: "studentCV", for: ip) as! StudentCVCell
            cell.viewModel = item
            return cell
        }


            studentCells
                .asObservable()
                .debug("STudent View: ")
                .map({ SectionViewModel(header: "Pickups Arrived", items: $0 ) })
                .bind(to: studentsView.rx.items(dataSource: dataSource)) // now here it is giving me this error (Instance method 'items(dataSource:)' requires the types '[SectionViewModel]' and 'SectionViewModel' be equivalent)
                .disposed(by: disposeBag)


        // item selection with model details.
        Observable
        .zip(
            studentsView
            .rx
            .itemSelected,
            studentsView
            .rx
            .modelSelected(StudentModel.self))
            .bind { [weak self] indexPath, model in

                let cell = self?.studentsView.cellForItem(at: indexPath) as? StudentCVCell
                if (model.pickupStatus == 2) {
                    // updating view accordingly
                }

        }.disposed(by: disposeBag)
    }

and ViewModels looks like this. from where I am populating.

class StudentCollectionViewViewModel {


    //MARK: Outlets
    let disposeBag = DisposeBag()
    var notificationToken : NotificationToken? = nil
    let studentCells = BehaviorRelay<[StudentModel]>(value: [])

    var studentCell : Observable<[StudentModel]> {
        return studentCells.asObservable()
    }


    deinit {
        notificationToken?.invalidate()
    }

    func getStudentsData(id: Int) {

        let studentsData = Database.singleton.fetchStudents(byCLassId: id)
        self.notificationToken = studentsData.observe{[weak self] change in
            TestDebug.debugInfo(fileName: "", message: "Switch:::: change")
            switch change {
            case .initial(let initial):
                TestDebug.debugInfo(fileName: "", message: "INIT: \(initial)")
                self!.studentCells.accept(Array(studentsData))
            case .update(_, let deletions, let insertions, let modifications):
                TestDebug.debugInfo(fileName: "", message: "MODIF::: \(modifications)")
                self!.studentCells.accept(Array(studentsData))
            case .error(let error):
                print(error)
            }
        }


    }

}

I am populating data from DB but I need to make two lists , I also don't get where i have to send two lists of data to populate. plus when I tried to use it in my code to see how things works but it gives my following error. Instance method 'items(dataSource:)' requires the types '[SectionModel]' and '[StudentModel]' be equivalent. any advise or help will be appreciated. thanks in advance


Solution

  • RxCollectionViewSectionedReloadDataSource<SectionModel> expects that you will bind items of SectionModel type, because you passed SectionModel as a generic parameter. Apparently, you would like to use StudentModel. To achieve this, you might make your StudentModel conform to SectionModelType protocol, and then use RxCollectionViewSectionedReloadDataSource<StudentModel>:

    extension StudentModel: SectionModelType {
        // implement
    }
    
    let dataSource = RxCollectionViewSectionedReloadDataSource<StudentModel>(configureCell: { (datasource, collectionView, indexPath, element) in
        // configure a cell     
    })
    studentCells.bind(to: studentsView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag) // don't forget to setup disposal
    

    But I suppose that StudentModel is describing a single cell, rather than an entire section. Is such case, it may be a better idea to map StudentModel to SectionModel, like this:

    let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel>(configureCell: { (datasource, collectionView, indexPath, element) in
        // configure a cell      
    })
    studentCells
        .map { [SectionModel(model: "", items: $0)] }
        .bind(to: studentsView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)
    

    Obviously, I mapped all your studentCells into a single section, which might not be your case. In more complex scenarios, you might consider implementing a custom type conforming to SectionModelType.

    Also, you may pass something more valuable than an empty string as a model, but again it depends on your needs.

    Attension! In an example above SectionModel stands for RxDataSources.SectionModel, not:

    enum SectionModel {
        case SectionOne(items: [SectionItem])
        case SectionTwo(items: [SectionItem])
    }