Search code examples
iosswiftuicollectionviewuikitscroll-paging

How to implement custom paging on UICollectionView to make it appear like the one in the AppStore?


I want to implement paging in the UICollectionView to make it appear like the AppStore

The previous and next cells should be partially visible from leading and trailing edges. I want to give it a feel just like we get to see in the Apps section of AppStore.

Below is my configuration code for the collection view. I tried implementing insetForSectionAt function but that completely messed up the scrolling behaviour of the collection view. Any help will be appreciated. Thank you!

//MARK: Configure collectionView
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func setupCollectionView() {
        imagesColletionView.delegate = self
        imagesColletionView.dataSource = self
        imagesColletionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.cellIdentifier)
        view.addSubview(imagesColletionView)
        
        // Set up horizontal scrolling and paging
        if let layout = imagesColletionView.collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            imagesColletionView.isPagingEnabled = true
            imagesColletionView.setCollectionViewLayout(layout, animated: false)
        }
        
        //Set constraints
        imagesColletionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imagesColletionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            imagesColletionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 10),
            imagesColletionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: -10),
            imagesColletionView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height/4)
        ])
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return colors.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.cellIdentifier, for: indexPath) as! CustomCollectionViewCell
        
        cell.backgroundColor = colors[indexPath.row]
        
        return cell
    }
    
    // UIScrollViewDelegate method to detect scrolling
        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
                // Calculate the visible index based on content offset
                let visibleRect = CGRect(origin: imagesColletionView.contentOffset, size: imagesColletionView.bounds.size)
                let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
                if let indexPath = imagesColletionView.indexPathForItem(at: visiblePoint) {
                    // Scroll to the corresponding row in the table view
                    let selectedRowIndexPath = IndexPath(row: indexPath.item, section: 0)
                    namesTableView.selectRow(at: selectedRowIndexPath, animated: true, scrollPosition: .top)
                }
        }
    
    func scrollToCollectionViewItem(at indexPath: IndexPath) {
        // Determine the corresponding index path in the collection view
        let collectionViewIndexPath = IndexPath(item: indexPath.row, section: 0)
        
        // Scroll to the item in the collection view
        imagesColletionView.scrollToItem(at: collectionViewIndexPath, at: .centeredHorizontally, animated: true)
    }

}
//enables horizontal scroll
extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
        let cellWidth = collectionView.bounds.width
        let cellHeight = collectionView.bounds.height
        
        return CGSize(width: cellWidth, height: cellHeight)
    }
}

Solution

  • Here's the solution:

    var imagesColletionView: UICollectionView!
    
    extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func setupCollectionView() {
        imagesColletionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
        imagesColletionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        imagesColletionView.backgroundColor = .systemBackground
        
        imagesColletionView.delegate = self
        imagesColletionView.dataSource = self
        imagesColletionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.cellIdentifier)
        view.addSubview(imagesColletionView)
        imagesColletionView.isScrollEnabled = false //disable vertical scroll
        //Set constraints
        imagesColletionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imagesColletionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            imagesColletionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            imagesColletionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            imagesColletionView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height/3)
        ])
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return images.count
        }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.cellIdentifier, for: indexPath) as! CustomCollectionViewCell
        
        cell.cellImageView.image = UIImage(named: images[indexPath.row])
        return cell
    }
    
    //used in taking data from tableview to go to respective cell
    func scrollToCollectionViewItem(at indexPath: IndexPath) {
        // Determine the corresponding index path in the collection view
        let collectionViewIndexPath = IndexPath(item: indexPath.row, section: 0)
        
        // Scroll to the item in the collection view
        imagesColletionView.scrollToItem(at: collectionViewIndexPath, at: .centeredHorizontally, animated: true)
        }
    
    //use in custom paging
    func createCompositionalLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout {
            sectionIndex, layoutEnvironment in
            let section = self.images[sectionIndex]
            return self.createFeaturedSection()
        }
        return layout
    }
    
    func createFeaturedSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
        
        let layOutItem = NSCollectionLayoutItem(layoutSize: itemSize)
        layOutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)
        
        //this fractional width value decides the leading & trailing partial showing of cells.
        let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .estimated(350))
        let layOutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layOutItem])
        
        let layOutSection = NSCollectionLayoutSection(group: layOutGroup)
        layOutSection.orthogonalScrollingBehavior = .groupPagingCentered
        return layOutSection
        }
    }
    

    However, on implementing UICollectionViewCompositionalLayout, in horizontal direction UIScrollView delegate methods stopped working as setting orthogonalScrollingBehavior to a section embeds internal UICollectionViewOrthogonalScrollerEmbeddedScrollView this new embedded scroll view handles scrolling behaviour in the section So ultimately it sets another internal subview to the collection view (the scrolling behaviour handler) which is a completely different instance Hence, we are unable to get any call back for delegate methods of UICollectionViewDelegate/UIScrollView

    Feel free to post another answer if you are aware of any solution that allows giving the paging behaviour to the collection view while at the same time allowing to implementation of UIScrollView delegate methods...

    Resource of solution