Trying to hook up Compositional Layout CollectionView with PageControl. visibleItemsInvalidationHandler is not calling

Viewed 2529

I want to implement paging on a welcome screen in iOS app (iOS 13, swift 5.2, xcode 11.5).

For this purpose, I use a UICollectionView and UIPageControl. Now I need to bind pageControl to Collection view.

At first, I tried to use UIScrollViewDelegate but soon found out that it does not work with the compositional layout. Then I discovered visibleItemsInvalidationHandler which is available for compositional layout sections. I tried different options like this:

section.visibleItemsInvalidationHandler = { (visibleItems, point, env) -> Void in
   self.pageControl.currentPage = visibleItems.last?.indexPath.row ?? 0 
}

and like this:

section.visibleItemsInvalidationHandler = { items, contentOffset, environment in
    let currentPage = Int(max(0, round(contentOffset.x / environment.container.contentSize.width)))
    self.pageControl.currentPage = currentPage
}

but nothing works...

Seems like that callback is not triggering at all. If I place a print statement inside of it it is not executed.

Please find below the whole code:

import Foundation
import UIKit

class WelcomeVC: UIViewController, UICollectionViewDelegate {
    
    //MARK: - PROPERTIES
    var cardsDataSource: UICollectionViewDiffableDataSource<Int, WalkthroughCard>! = nil
    var cardsSnapshot = NSDiffableDataSourceSnapshot<Int, WalkthroughCard>()
    var cards: [WalkthroughCard]!
    var currentPage = 0
    
    
    //MARK: - OUTLETS
    @IBOutlet weak var signInButton: PrimaryButton!
    
    @IBOutlet weak var walkthroughCollectionView: UICollectionView!
    
    @IBOutlet weak var pageControl: UIPageControl!
    
    //MARK: - VIEW DID LOAD
    override func viewDidLoad() {
        super.viewDidLoad()
        walkthroughCollectionView.isScrollEnabled = true
        walkthroughCollectionView.delegate = self
        setupCards()
        pageControl.numberOfPages = cards.count
        configureCardsDataSource()
        configureCardsLayout()
    }
   
    //MARK: - SETUP CARDS
    
    func setupCards() {
        cards = [
            WalkthroughCard(title: "Welcome to abc", image: "Hello", description: "abc is an assistant to your xyz account at asdf"),
            WalkthroughCard(title: "Have all asdf projects at your fingertips", image: "Graphs", description: "Enjoy all project related data whithin a few taps. Even offline")
        ]
    }
    
    //MARK: - COLLECTION VIEW DIFFABLE DATA SOURCE
    
    private func configureCardsDataSource() {
        cardsDataSource = UICollectionViewDiffableDataSource<Int, WalkthroughCard>(collectionView: walkthroughCollectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, card: WalkthroughCard) -> UICollectionViewCell? in
            // Create cell
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: "WalkthroughCollectionViewCell",
                for: indexPath) as? WalkthroughCollectionViewCell else { fatalError("Cannot create new cell") }
            //cell.layer.cornerRadius = 15
            cell.walkthroughImage.image = UIImage(named: card.image)
            cell.walkthroughTitle.text = card.title
            cell.walkthroughDescription.text = card.description
            return cell
        }
        setupCardsSnapshot()
    }
    
    private func setupCardsSnapshot() {
        cardsSnapshot = NSDiffableDataSourceSnapshot<Int, WalkthroughCard>()
        cardsSnapshot.appendSections([0])
        cardsSnapshot.appendItems(cards)
        cardsDataSource.apply(self.cardsSnapshot, animatingDifferences: true)
    }
    
    
    //MARK: - CONFIGURE COLLECTION VIEW LAYOUT
    func configureCardsLayout() {
        walkthroughCollectionView.collectionViewLayout = generateCardsLayout()
    }
    
    func generateCardsLayout() -> UICollectionViewLayout {
        
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(1.0))
        let fullPhotoItem = NSCollectionLayoutItem(layoutSize: itemSize)

        
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(1.0))
        let group = NSCollectionLayoutGroup.horizontal(
            layoutSize: groupSize,
            subitem: fullPhotoItem,
            count: 1
        )
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .groupPagingCentered
        let layout = UICollectionViewCompositionalLayout(section: section)
        
        //setup pageControl
        section.visibleItemsInvalidationHandler = { (items, offset, env) -> Void in
            self.pageControl.currentPage = items.last?.indexPath.row ?? 0
        }
        
        return layout
    }
    
 
}
3 Answers

It should be working if you set up the invalidation handler for the section before passing the section to the layout:

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.visibleItemsInvalidationHandler = { (items, offset, env) -> Void in
    self.pageControl.currentPage = items.last?.indexPath.row ?? 0
}

return UICollectionViewCompositionalLayout(section: section)

I think you should give a chance to UIPageViewController. I'm attaching its implementation so that you can easily integrate with your code.

You can design your slider items in a separate UIViewController and use wherever area you want within your main view for UIPageViewController.

Cheers!

import UIKit
import SnapKit

class WelcomeViewController: BaseViewController<WelcomeViewModel> {
        
    @IBOutlet weak var pageControl: UIPageControl!
    @IBOutlet weak var viewForPageController: UIView!

    private var pageViewController: UIPageViewController!
    private var introductionSliderViewControllers: [UIViewController] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.welcomeSlidersLoaded = { [weak self] sliders in
            self?.loadSliders(sliders: sliders)
        }
     
    }
    
    private func loadSliders(sliders: [IntroductionSliderViewModel]) {
        guard sliders.count > 0 else { return }
        self.introductionSliderViewControllers = sliders.map{
            IntroductionSliderViewController(viewModel: $0)
        }
        self.initializePageViewController()
    }
    
    private func initializePageViewController(){
        
        self.pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        self.viewForPageController.addSubview(self.pageViewController.view)
        self.addChild(pageViewController)
        self.pageViewController.view.snp.makeConstraints { (make) in
            make.edges.equalToSuperview()
        }
        self.pageViewController.delegate = self
        self.pageViewController.dataSource = self
        
        self.pageViewController.setViewControllers([introductionSliderViewControllers[0]], direction: .forward, animated: false, completion: nil)
        
        self.pageControl.numberOfPages = introductionSliderViewControllers.count
        self.pageControl.currentPage = 0
        
    }

}

extension WelcomeViewController: UIPageViewControllerDataSource{
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let index = self.introductionSliderViewControllers.firstIndex(of: viewController),
            index > 0 else{return nil}
        return self.introductionSliderViewControllers[index - 1]
        
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let index = self.introductionSliderViewControllers.firstIndex(of: viewController),
            index < self.introductionSliderViewControllers.count - 1 else{return nil}
        return self.introductionSliderViewControllers[index + 1]
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if let vc = pageViewController.viewControllers?.first, let index = self.introductionSliderViewControllers.firstIndex(of: vc){
            pageControl.currentPage = index
        }
    }
}

This should work

section.visibleItemsInvalidationHandler = { items, contentOffset, environment in
      let currentPage = Int(max(0, round(contentOffset.x / environment.container.contentSize.width)))
      self.pageControl.currentPage = currentPage
}
Related