1

I'm learning MVVM and RxSwift, and I want to display data from GitHub api and populate into collectionViewDiffableDataSource. But it's not displaying my data, even with my snapshot is already setup to accept my data. This is my code

class FollowersListViewModel {
    
    let searchText      = BehaviorRelay<String>(value: "")
    let page            = BehaviorRelay<Int>(value: 1)
    var followers       = BehaviorRelay<[FollowerViewModel]>(value: [])
    var filterFollowers = BehaviorRelay<[FollowerViewModel]>(value: [])
    let hasMoreFollower = BehaviorRelay<Bool>(value: false)
    let isLoading       = BehaviorRelay<Bool>(value: true)
    
    private let manager: NetworkManager
        
    let disposeBag      = DisposeBag()
    
    init(manager: NetworkManager) {
        self.manager = manager
    }
    
    func fetchFollowers(with username: String) {
        isLoading.accept(true)
        searchText.asObservable()
            .filter { $0.count > 2 }
            .throttle(.seconds(3), scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .flatMapLatest { query in
                self.manager.getFollowers(with: query, page: self.page.value)
            }.subscribe { followers in
                self.isLoading.accept(false)
                self.followers.accept(followers.map { FollowerViewModel(follower: $0)})
                print(self.followers.value)
            } onError: { error in
                print(error)
            }.disposed(by: disposeBag)
    }
    
}

class FollowersListVC: UIViewController {
    
    var viewModel: FollowersListViewModel
    
    enum Section { case main }
    
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, FollowerViewModel>!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViewController()
        setupSearchController()
        setupCollectionView()
        setupCollectionViewDataSource()
        viewModel.fetchFollowers(with: username)
        setupSnapshot()
    }

    private func setupCollectionViewDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, FollowerViewModel>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, follower) -> UICollectionViewCell? in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FollowersCell.reuseID, for: indexPath) as! FollowersCell
            cell.set(followerVM: follower)
            
            return cell
        })
    }

    private func setupSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, FollowerViewModel>()
        snapshot.appendSections([.main])
        snapshot.appendItems(viewModel.followers.value)
        
        DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: true) }
    }
}

I don't know why but it's like my snapshot is not being called, it's seems different when use in MVVM

ferryawijayanto
  • 569
  • 4
  • 18

1 Answers1

1

Your setupSnapshot() function is being called before the values have been accepted into followers. I haven't used a NSDiffableDataSourceSnapshot yet, but you likely need to do something like this instead:

func setupSnapshot() {
    viewModel.followers
        .map { (followers) in
            with(NSDiffableDataSourceSnapshot<Section, FollowerViewModel>()) {
                $0.appendSections([.main])
                $0.appendItems(followers)
            }
        }
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { [dataSource] snapshot in
            dataSource.apply(snapshot, animatingDifferences: true)
        })
        .disposed(by: disposeBag)
}

The above uses this helper function. It's optional but I think the code looks cleaner when using it:

func with<T>(_ value: T, _ fn: (inout T) -> Void) -> T {
    var temp = value
    fn(&temp)
    return temp
}

By the way...

  • BehaviorRelays should never be var, always declare them using let.
  • Excessive use of BehaviorRelays like this is a code smell.
  • the map above should be put in your viewModel rather than here.
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • hey Daniel thank for your solution, it works like a charm :). Btw if I put the map in my viewModel than I must import the UIKit for UICollectionViewDiffableDataSource right? I hear that, is not a good idea to import UIKit in viewModel – ferryawijayanto Jun 07 '21 at 12:13
  • 1
    Importing UIKit is fine. Just be careful of what you use from it. Using `struct`s from it are fine, as well as immutable classes. The key is that the logic is in the view model while the side effects stay in the view controller. – Daniel T. Jun 07 '21 at 13:03