Matt Matt - 2 months ago 17
iOS Question

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.

Answer

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.

Comments