9

I have a UICollectionView that has the following settings,

public CollectionView(CollectionLayout layout) : base(CGRect.Empty, layout)
{
    RegisterClassForCell(typeof(CollectionCell), CollectionCell.CellIdentifier);
    CollectionViewLayout = layout;
    ShowsHorizontalScrollIndicator = false;
    PagingEnabled = true;
}

The CollectionViewLayout is,

public CollectionLayout()
{
    ScrollDirection = UICollectionViewScrollDirection.Horizontal;
    MinimumInteritemSpacing = 0f;
    MinimumLineSpacing = 0f;
    ItemSize = new CGSize(UIScreen.MainScreen.Bounds.Width, 200f);
}

so the cells in the CollectionView are stretched so that a cell fills the CollectionView. The CollectionView is only horizontally scrollable.

Now I want to have a dotted page indicator instead of the scroll bar for the CollectionView. Is there anyway I can achieve this properly?

Rizan Zaky
  • 4,230
  • 3
  • 25
  • 39

4 Answers4

26

Perfectly correct approach for 2020:

Simply add a UIPageControl in storyboard.

Put it below (i.e., visible on top of) your collection view.

enter image description here

Link to it...

class YourVC: UIViewController, UICollectionViewDelegate,
         UICollectionViewDataSource,
         UICollectionViewDelegateFlowLayout {
    
    @IBOutlet var collectionView: UICollectionView!
    @IBOutlet var dots: UIPageControl!

Simply add a constraint centering it horizontally to the collection view, and add a constraint to align the bottoms.

That will give the standard positioning / spacing.

(Of course, you can place the dots anywhere you want, but that is the standard.)

enter image description here

Tip 1 - colors

Bizarrely the default colors for the dots are .. clear!

So set them to gray/black or whatever you wish:

enter image description here

Or you can do that in code:

override func viewDidLoad() {
    super.viewDidLoad()
    dots.pageIndicatorTintColor = .systemGray5
    dots.currentPageIndicatorTintColor = .yourCorporateColor
}

Next. In numberOfItemsInSection, add ...

func collectionView(_ collectionView: UICollectionView,
              numberOfItemsInSection section: Int) -> Int {
    
    let k = ... yourData.count, or whatever your count is
    
    dots.numberOfPages = k
    return k
}

Tip 2 - in fact, do NOT use the deceleration calls

Add this code:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    dots.currentPage = Int(
        (collectionView.contentOffset.x / collectionView.frame.width)
            .rounded(.toNearestOrAwayFromZero)
        )
    )
}

You simply set the page in "scrollViewDidScroll".

In fact

do not use scrollViewWillBeginDecelerating

do not use scrollViewDidEndDecelerating.

To see why: try it using either of those calls. Now skim quickly through many pages. Notice it does not work properly.

Simply use scrollViewDidScroll for the correct, perfect result, including initialization.

Tip 3 - do NOT use Int division - it completely changes the behavior and is totally wrong.

You will often see this example code:

// wrong, do not do this
dots.currentPage = Int(collectionView.contentOffset.x) /
                     Int(collectionView.frame.width)
// wrong, do not do this

That often-seen example code is completely wrong.

If you try that, it will result in the dots "jumping" in a non-standard way, as you skim through pages.

Best explanation is to try it and see.

For the usual, correct, Apple-style behavior as you scroll through or skim through the pages, the code is:

    dots.currentPage = Int(
        (collectionView.contentOffset.x / collectionView.frame.width)
        .rounded(.toNearestOrAwayFromZero)
    )

Final example...

enter image description here

ScottyBlades
  • 12,189
  • 5
  • 77
  • 85
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Last item isn't highlighted in the page control like this.. – Nicolai Harbo Apr 09 '21 at 09:02
  • cheers @NicolaiHarbo ! you may have made some little mistake. Double-check your `numberOfItemsInSection` is correct. – Fattie Apr 09 '21 at 09:29
  • Doesn’t “ .rounded(.toNearestOrAwayFromZero)” have no effect? – ScottyBlades Jul 02 '21 at 20:51
  • @ScottyBlades good question, that is specifically and definitely the one to use to get the "standard effect" (exactly per Apple). As I mention, it is difficult to explain the different physics behaviors but, be sure to try it with `.rounded.toNearestOrAwayFromZero` and then in comparison try some of the other .rounded options. Cheers ! – Fattie Jul 02 '21 at 21:35
  • ```print(0.rounded(.toNearestOrAwayFromZero)) // out 0.0 print(1.rounded(.toNearestOrAwayFromZero)) // out 1.0 print(2.rounded(.toNearestOrAwayFromZero)) // out 2.0``` It has no effect on an int. I think you used it on an int in your answer. – ScottyBlades Jul 02 '21 at 22:13
  • you may have missed a bracket, if I'm not mistaken Int is applied outside of the CGFloat calc, notice the brackets – Fattie Jul 03 '21 at 01:02
  • I am having a carousel-like effect on the Collection view, and the cells are a bit shifted. The `toNearestOrAwayFromZero` is not precise in my case. With rounding to `awayFromZero` everything works like a charm. – Cyril Cermak Jan 07 '23 at 11:41
  • hi @CyrilCermak , it would be difficult to comment on the details of that without completely investigating the project ... however I think the key is, as it states, **do not use int division**, you have to round correctly and precisely, however that's achieved in your case. It's that horrible issue where someone puts on the internet an incorrect answer, and literally for years, even decades, it is widely duplicated. Cheers – Fattie Jan 07 '23 at 16:32
  • Hi @Fattie, I totally agree, I only wanted to leave the comment for someone who might have a similar case like me. So that they can quickly fix their carousel dots by rounding awayFromZero. :) – Cyril Cermak Jan 07 '23 at 17:59
  • 1
    Solution works perfect, its also a really simply way to detect which cell is in view, a problem I've been trying to find an elegant solution for – Chris Feb 08 '23 at 07:52
2

Drag a UIPageControl above your collectionView and make an IBOutlet of UIPageControl in your ViewController.

Then Put the following code:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    pageControl.currentPage = Int(self.collectionView=.contentOffset.x)/ Int(self.collectionView.frame.width)
}
aBilal17
  • 2,974
  • 2
  • 17
  • 23
Deepak
  • 724
  • 4
  • 13
-1

Use UIPageControl. Link Below: UIPageControl

Then detect which cell you are in and write some code to change the "currentPage" of UIPageControl.

-1

I added a UIPageControl component on top of the CollectionView, setting its constraints to the bottom of the CollectionView. Then I got an instance of this UIPageControl component to a property of the CollectionSource of the UICollectionView.

I set the Pages and CurrentPage properties of the UIPageControl as follows,

public class CollectionSource : UICollectionViewSource
{
    private List<SourceDataModel> _dataSource;
    public UIPageControl PageControl { get; set; }

    // other code

    public override void DecelerationEnded(UIScrollView scrollView)
    {
        var index = (int)(Collection.ContentOffset.X / Collection.Frame.Width);
        PageControl.CurrentPage = index;
        // other code
    }

    public override nint GetItemsCount(UICollectionView collectionView, nint section)
    {
        PageControl.Pages = _dataSource.Count;
        return _dataSource.Count;
    }
}

Tutorial Followed: https://blog.learningtree.com/paging-with-collection-views-part-2/

Rizan Zaky
  • 4,230
  • 3
  • 25
  • 39
  • I think there may be some typos here, example "UICollectionViewDataSource", the function declarations are wrong etc. – Fattie Jun 17 '20 at 14:43