DEV Community

Dariusz Rybicki for EL Passion

Posted on

Custom pagination in UIScrollView

There are various ways of implementing custom pagination in UIScrollView (or UITableView or UICollectionView). Below you can find very simple and effective one.

Example

Below you can see real-life example, taken from E-commerce Today's deals interaction, iOS demo.

Example
Preview

Implementation

We will use ScrollPageController class to control UIScrollView paging and update UIPageControl:

struct ScrollPageController {

    /// Computes page offset from page offsets array for given scroll offset and velocity
    ///
    /// - Parameters:
    ///   - offset: current scroll offset
    ///   - velocity: current scroll velocity
    ///   - pageOffsets: page offsets array
    /// - Returns: target page offset from array or nil if no page offets provided
    func pageOffset(for offset: CGFloat, velocity: CGFloat, in pageOffsets: [CGFloat]) -> CGFloat? {
        let pages = pageOffsets.enumerated().reduce([Int: CGFloat]()) {
            var dict = $0
            dict[$1.0] = $1.1
            return dict
        }
        guard let page = pages.min(by: { abs($0.1 - offset) < abs($1.1 - offset) }) else {
            return nil
        }
        if abs(velocity) < 0.2 {
            return page.value
        }
        if velocity < 0 {
            return pages[pageOffsets.index(before: page.key)] ?? page.value
        }
        return pages[pageOffsets.index(after: page.key)] ?? page.value
    }

    /// Cumputes page fraction from page offsets array for given scroll offset
    ///
    /// - Parameters:
    ///   - offset: current scroll offset
    ///   - pageOffsets: page offsets array
    /// - Returns: current page fraction in range from 0 to number of pages or nil if no page offets provided
    func pageFraction(for offset: CGFloat, in pageOffsets: [CGFloat]) -> CGFloat? {
        let pages = pageOffsets.sorted().enumerated()
        if let index = pages.first(where: { $0.1 == offset })?.0 {
            return CGFloat(index)
        }
        guard let nextOffset = pages.first(where: { $0.1 >= offset })?.1 else {
            return pages.map { $0.0 }.last.map { CGFloat($0) }
        }
        guard let (prevIdx, prevOffset) = pages.reversed().first(where: { $0.1 <= offset }) else {
            return pages.map { $0.0 }.first.map { CGFloat($0) }
        }
        return CGFloat(prevIdx) + (offset - prevOffset) / (nextOffset - prevOffset)
    }

}

Enter fullscreen mode Exit fullscreen mode

If you are looking for a drop-in solution for your project, check out attached ScrollPageController.swift file and its tests in ScrollPageControllerSpec.swift.

How to use it

It's quite simple:

class ExampleViewController: UIViewController, UIScrollViewDelegate {

    // to make pagination working, implement this delegate function:

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if let pageOffset = ScrollPageController().pageOffset(
            for: scrollView.contentOffset.x, 
            velocity: velocity.x, 
            in: pageOffsets(in: scrollView)
        ) {
            targetContentOffset.pointee.x = pageOffset
        }
    }

    // if you have a page control you would like to update when scrolling:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if let pageFraction = ScrollPageController().pageFraction(
            for: scrollView.contentOffset.x, 
            in: pageOffsets(in: scrollView)
        ) {
            let pageControl: UIPageControl = // your page controll
            pageControl.currentPage = Int(round(pageFraction))
        }
    }

    private func pageOffsets(in scrollView: UIScrollView) -> [CGFloat] {
        // return page offsets, see bellow for an example
    }

}
Enter fullscreen mode Exit fullscreen mode

If you want to page by scroll view frame width:

    private func pageOffsets(in scrollView: UIScrollView) -> [CGFloat] {
        let pageWidth = scrollView.bounds.width
                        - scrollView.adjustedContentInset.left
                        - scrollView.adjustedContentInset.right
        let numberOfPages = Int(ceil(scrollView.contentSize.width / pageWidth))
        return (0..<numberOfPages).map { CGFloat($0) * pageWidth - scrollView.adjustedContentInset.left }
    }
Enter fullscreen mode Exit fullscreen mode

If you want to page by scroll view subviews:

    private func pageOffsets(in scrollView: UIScrollView) -> [CGFloat] {
        return scrollView.subviews
                         .compactMap { $0 as? PageView }
                         .map { $0.frame.minX - scrollView.adjustedContentInset.left }
    }
Enter fullscreen mode Exit fullscreen mode

If you prefer real-life example, check out E-commerce Today's deals interaction, iOS demo implementation.

License

Copyright © 2019 EL Passion

Top comments (0)