Search code examples
iosswiftuipageviewcontrolleruipagecontrol

Control "active" dot in UIPageControl


My designer is asking that I display 3 dots in a UIPageViewController for 10 views.

When the first 3 view controllers display, the 0th dot should be highlighted; when the next 4 view controllers display, the 1st dot should be highlighted; when the final 3 view controllers display, the 2nd dot should be highlighted.

So far I'm able to display 3 dots in the UIPageControl, but the indicator dot just rotates around indicating the n%3 position as active.

func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
    return 3
}

I haven't seen any documentation on how to control with UIPageControl index is the active index, so I'm not sure if this is something Apple wants you to be able to override.

If there is a way to accomplish what I'm trying to do, I'd appreciate the help.


Solution

  • It turns out that what I'm trying to accomplish can't be done with a UIPageViewController. By default the UIPageControl in this class cannot be overridden directly.

    Instead, I was able to use a combination of a UICollectionView (with a hack that allows it to resemble a UIPageViewController in its page changing effects) and a UIPageControl, as subviews to the same overarching UIViewController.

    class MyPageViewController : UIViewController {
        // MARK: subviews
        private var collectionView:UICollectionView!
        /// the collection layout controls the scrolling behavior of the collection view
        private var collectionLayout = MyLayout()
        private var pageControl = UIPageControl()
    
        let CollectionViewCellReuseIdentifer = "CollectionViewCellReuseIdentifier"
    
        // MARK: autolayout
        private var autolayoutConstraints:[NSLayoutConstraint] = [NSLayoutConstraint]()
    
    
    
        // MARK: constructors
        init() {
            super.init(nibName: nil, bundle: nil)
        }
    
    
    
        // MARK: UIViewController lifecycle methods
        override func viewDidLoad() {
            super.viewDidLoad()
            self.setupView()
        }
    
    
    
        /**
        Set up the collection view, page control, skip & log in buttons
        */
        func setupView() {
            self.setupCollectionView()
            self.setupPageControl()
    
            self.setupConstraints()
    
            self.view.addConstraints(self.autolayoutConstraints)
        }
    
        /**
        Set up the collection view
        */
        func setupCollectionView() {
            self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.collectionLayout)
            self.collectionView.translatesAutoresizingMaskIntoConstraints = false
            self.collectionView.registerClass(MyPageView.self, forCellWithReuseIdentifier: self.CollectionViewCellReuseIdentifer)
            self.collectionView.dataSource = self
            self.collectionView.delegate = self
            self.collectionView.backgroundColor = UIColor.whiteColor()
            self.collectionView.scrollEnabled = true
            self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
    
    
            self.collectionLayout.minimumInteritemSpacing = 1
            self.collectionLayout.minimumLineSpacing = 1
            self.collectionLayout.scrollDirection = .Horizontal
            self.collectionLayout.delegate = self
    
            self.view.addSubview(self.collectionView)
        }
    
        /**
        Set up view showing pagination dots for slideshow items
        */
        func setupPageControl() {
            self.pageControl.translatesAutoresizingMaskIntoConstraints = false
            self.pageControl.numberOfPages = 3
            self.pageControl.backgroundColor = UIColor.whiteColor()
    
            self.view.addSubview(self.pageControl)
        }
    
        func setupConstraints() {
            let views:[String:AnyObject] = [
                "collectionView" : self.collectionView,
                "pageControl" : self.pageControl,
            ]
    
            self.autolayoutConstraints.appendContentsOf(
                NSLayoutConstraint.constraintsWithVisualFormat(
                    "V:|[collectionView][pageControl]|",
                    options: .AlignAllCenterX,
                    metrics: nil,
                    views: views
                )
            )
    
            self.autolayoutConstraints.appendContentsOf(
                NSLayoutConstraint.constraintsWithVisualFormat(
                    "H:|[collectionView]|",
                    options: .AlignAllCenterY,
                    metrics: nil,
                    views: views
                )
            )
    
            self.autolayoutConstraints.appendContentsOf(
                NSLayoutConstraint.constraintsWithVisualFormat(
                    "H:|[pageControl]|",
                    options: NSLayoutFormatOptions(),
                    metrics: nil,
                    views: views
                )
            )
        }
    }
    
    extension MyPageViewController : MyPageViewControllerDelegate {
        func didSwitchToPage(imageIndex: Int) {
            if imageIndex < 3 {
                self.pageControl.currentPage = 0
            } else if imageIndex < 7 {
                self.pageControl.currentPage = 1
            } else {
                self.pageControl.currentPage = 2
            }
    
            self.pageControl.setNeedsDisplay()
        }
    }
    

    The layout class was derived from an answer my coworker found when researching a similar issue. http://karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/

    /**
    *  Delegate for slide interactions
    */
    protocol MyPageViewControllerDelegate {
        /**
        Triggered when a new page has been 'snapped' into place
    
        - parameter imageIndex: index of the image that has been snapped to
        */
        func didSwitchToPage(imageIndex: Int)
    
    }
    
    class MyLayout : UICollectionViewFlowLayout {
        var delegate:MyPageViewControllerDelegate?
    
        /*
        Allows different items in the collection to 'snap' onto the current screen section.
        Based off of http://karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/
        */
        override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    
            if let cv = self.collectionView {
    
                let cvBounds = cv.bounds
                let halfWidth = cvBounds.size.width * 0.5;
                let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth;
    
                if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) {
    
                    var candidateAttributes : UICollectionViewLayoutAttributes?
                    // the index of the image selected
                    var index:Int = 0
    
                    for attributes in attributesForVisibleCells {
    
                        // == Skip comparison with non-cell items (headers and footers) == //
                        if attributes.representedElementCategory != UICollectionElementCategory.Cell {
                            index++
                            continue
                        }
    
                        if let candAttrs = candidateAttributes {
    
                            let a = attributes.center.x - proposedContentOffsetCenterX
                            let b = candAttrs.center.x - proposedContentOffsetCenterX
    
                            if fabsf(Float(a)) < fabsf(Float(b)) {
                                candidateAttributes = attributes;
                            }
    
                        }
                        else { // == First time in the loop == //
    
                            candidateAttributes = attributes;
                            index++
                            continue;
                        }
    
                    }
    
                    // Beautification step , I don't know why it works!
                    if(proposedContentOffset.x == -(cv.contentInset.left)) {
                        return proposedContentOffset
                    }
    
                    if let delegate = self.delegate {
                        delegate.didSwitchToPage((candidateAttributes?.indexPath.row)!)
                    }
    
                    return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y)
    
                }
    
    
            }
    
            // fallback
            return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
        }
    }
    

    Note: I trimmed down the actual code I used and replaced a bunch of names to make them more appropriate for examples. I did not run this specific code and did not test for errors in my IDE. That being said, the approach behind the code is solid.