Replicating the AppStore’s CollectionViewLayout (orthogonal) in Swift 5

Building a simple bi-directional scrolling UICollectionView

In a precedent post I scratched the surface of the freshly released CollectionView Compositional Layout API to get simpler and more complex CollectionViewLayout.
The today's challenge is to replicate this orthogonal scrolling CollectionView layout that you might have seen in your iDevice's AppStore ("Apps" or "Games" tab).

Basically we have some featured content in wide cards, with horizontal scroll, the app list that scrolls vertically, just like a regular TableView, and some basic blocks of content, maybe headers or footers with text.

Alt Text

So let's start by building our cells :

  • the "featured cell" card, having the "card" look, rounded corner, shadow and stuff with a large title and a smaller label ;
  • an "app cell", that contains an app icon, title and some category ;
  • and a "text cell" : just a basic cell with a multi-line label.

I am not going to detail this part as it's not the point of this post but you can check the code, at the end of the article.

Alt Text

Right now the ViewController is empty, I'm going to add a simple function that will return a UICollectionViewLayout that we will use in the CollectionView initializer.

    func makeLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            //
        }
        return layout
    }

Now you know, the UICollectionViewCompositionalLayout block should return a NSCollectionLayoutSection. And here we have 3 section : the one with the features card, the one with the app list and the one with our text cell.

The horizontal scrolling sections' Layout

        let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)))
        item.contentInsets = NSDirectionalEdgeInsets = NSDirectionalEdgeInsets(top: 0.0, leading: 12.0, bottom: 0.0, trailing: 12.0)
        let group = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalHeight(0.25)), subitem: item, count: 1)
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 16.0, leading: 0.0, bottom: 16.0, trailing: 0.0)
        section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
        return section

Let's detail every line of this sample of code, but first, try to keep in mind what is an item, a group and a section, as seen on my previous post about compositional layout :

  • I first declare our NSCollectionLayoutItem, with it's size, saying it's going to take 100% of the parent's width and height.
  • I give this item the insets I want.
  • Then, I initialize a group, a NSCollectionLayoutGroup, with the size I want each cell to take horizontally. In this case : 90% of the total collectionView width, 25% of the collectionView height.
    (I want 90% so users can see there is more on the left so they want to scroll 😇)
  • I finally create my section with my group.
  • Add some insets.
  • This continuousGroupLeadingBoundary allows the vertical scrolling, but we can put .paging or any OrthogonalScrollingBehavior.

That's all it takes for the Featured Card's section. We'll put all that in an isolated piece of function that we'll call something like buildHorizontalSectionLayout() and simply return it in our block like so :

    func makeLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            if section == 0 {
                return self.buildHorizontalSectionLayout()
            }
        }
        return layout
    }

The vertical scrolling sections' Layout

Now for the vertical scrolling, we'll simply mimic the tableView behaviour with self sizing cells. We want full with rows with an estimated height, adjustable according it's content's constraints.

        let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)))
        item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),  heightDimension: .estimated(70))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 12, bottom: 0, trailing: 12)
  • An Item that takes 100% of width/height of it's superview.
  • A group that will have an estimated height, say 70, and 1 item per group.
  • Finally the section with the group and I specified insets.

I will also encapsulate this piece of code in a buildVerticalSectionLayout func, so now we now can update our makeLayout func :

    func makeLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            if section == 0 {
                return self.buildHorizontalSectionLayout()
            } else {
                return self.buildVerticalSectionLayout()
            }
        }
        return layout
    }

The viewController will be pretty basic, and I am not going to detail the code. Basically just add a CollectionView, init with a frame and a layout (calling the makeLayout func), place it with constraints, register your cells, and set the delegate and dataSource.

    lazy var collectionView: UICollectionView = {
        let collectionView: UICollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.makeLayout())
        collectionView.backgroundColor = UIColor.white
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "cell")
        collectionView.register(Featured.self, forCellWithReuseIdentifier: "featured")
        collectionView.register(Text.self, forCellWithReuseIdentifier: "text")
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()

And there you go !
Here is the final result :

Alt Text

Bonus : you can easily build an horizontal scrolling vertical list like so
Alt Text
by mixing the two layout examples shown above. Just use a vertical NSCollectionLayoutGroup and set an orthogonalScrollingBehavior (in the gif, it's .groupPaging). Don't forget to specify how many items you want in your group (how many "app cell" in a paged block) in the parameter count of you NSCollectionLayoutGroup.

Bonus 2 : You can use compositional Layouts with iOS 12 using IBPCollectionViewCompositionalLayout 😋

Hope you enjoyed this quick walkthrough of UICollectionViewCompositionalLayout. Of course you can download the code.
Play with it, tweak it and make something great! ☺️

Happy coding!

Étiquettes :

Leave a Comment