'UICollectionView compositional layout with orthogonal scrolling and a different size for last cell

We’re attempting to use UICollectionView compositional layout to describe a section layout, but it’s giving us some trouble.

What we’d like to achieve:

  • A total of 7 cells
  • 3 cells stacked, each 30% height and 100% width, with orthogonal scrolling to see the next 3
  • When the user reaches the last cell, it would be 100% height and 100% width of the container.

ASCII art and screenshots of the imperfect example are below.

Our starting point was Apple’s example code, specifically OrthogonalScrollingViewController, but what we’ve found is if we expand the widths there, the height settings seem to longer be respected.

The most basic version of a layout that we tried is this:

  func createLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout {
      (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
      
      // Small items at roughly 30% of the container height.
      let smallItem = NSCollectionLayoutItem(
        layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1.0),
          heightDimension: .fractionalHeight(0.3)
        )
      )

      // The large item should be 100% of the container width.
      let largeItem = NSCollectionLayoutItem(
        layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1.0),
          heightDimension: .fractionalHeight(1.0)
        )
      )

      // We want 3 items to appear stacked with orthogonal scrolling.
      let smallItemGroup = NSCollectionLayoutGroup.vertical(
        layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1.0),
          heightDimension: .fractionalHeight(1.0)
        ),
        subitem: smallItem,
        count: 3
      )
      
      let containerGroup = NSCollectionLayoutGroup.horizontal(
        layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1),
          heightDimension: .fractionalHeight(0.5)
        ),
        subitems: [smallItemGroup, largeItem]
      )
      
      let section = NSCollectionLayoutSection(group: containerGroup)
      section.orthogonalScrollingBehavior = .paging
      
      return section
    }
    
    return layout
  }

We’ve tried many variations of nested groups and using the .vertical and .horizontal initializers, but each of them has the same result: The last cell is always the same size as all the others.

  //   +------------------------------+                  These would be off-screen
  //   | +-------------------------+  |  +-------------------------+  +-------------------------+
  //   | |            1            |  |  |            4            |  |                         |
  //   | +-------------------------+  |  +-------------------------+  |                         |
  //   | +-------------------------+  |  +-------------------------+  |                         |
  //   | |            2            |  |  |            5            |  |            7            |
  //   | +-------------------------+  |  +-------------------------+  |                         |
  //   | +-------------------------+  |  +-------------------------+  |                         |
  //   | |            3            |  |  |            6            |  |                         |
  //   | +-------------------------+  |  +-------------------------+  +-------------------------+
  //   |                              |
  //   |                              |
  //   |                              |
  //   |     rest of the screen       |
  //   |                              |
  //   |                              |
  //   |                              |
  //   |                              |
  //   +------------------------------+

Page 1 with the first 3 cells Page 2 with the second 3 cells Page 3 with desired large cell

The last cell in the third screenshot, (0,6), is the one we want 100% height of the section.

Is there a way to achieve this layout where the last cell is 100% of the section height?



Solution 1:[1]

Couple notes...

First: if you will only have exactly 7 cells - no more, no less - this layout would be very easy to do with stack views in a scroll view.

Second: I have only taken a cursory look at Compositional / Orthogonal layouts, so it's very possible there is a much better solution than what follows. But, it may be worth looking at.

Third: you have probably already figured some of this out, but I'll go step-by-step for clarity.


So, using Apple's Implementing Modern Collection Views sample code, focusing on OrthogonalScrollingViewController (we'll change number of Sections to 1 and number of items to 7).

Using the original createLayout() code, we get this (as you're aware):

enter image description here

Step 1 - I think Apple did a very bad job with naming, so I'm changing:

  • leadingItem to singleItem
  • trailingItem to doubleItem
  • trailingGroup to doubleVerticalGroup
  • containerGroup to repeatingGroup

What this clarifies, to me, is that we've defined a vertical group of two items, and a horizontal group of two items (subitems: [singleItem, doubleVerticalGroup]), which will repeat (the result, of course, looks the same):

enter image description here

Step 2 we can change the order easily, by swapping the order of the repeating group's subitems: [doubleVerticalGroup, singleItem]):

enter image description here

Step 3 - I find it a bit confusing to have a single item and a vertical group of items, so I'll "embed" the single item in a vertical group all by itself:

// let's create a "singleVerticalGroup" - based on "doubleVerticalGroup"
let singleVerticalGroup = NSCollectionLayoutGroup.vertical(
    // width is percentage of "parent"
    // height is percentage of "parent"
    //  subitem is 1 singleItem (count: 1), arranged vertically
    // width of this group is what the singleItem width used to be
    layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
                                       heightDimension: .fractionalHeight(1.0)),
    subitem: singleItem, count: 1)
        

and change our repeatingGroup items to:

subitems: [doubleVerticalGroup, singleVerticalGroup])

No difference in appearance (yet):

enter image description here

Step 4 - Your layout wants "vertical / vertical / single" so let's change the repeatingGroup to:

subitems: [doubleVerticalGroup, doubleVerticalGroup, singleVerticalGroup])

And we're close to what we want:

enter image description here

Whoops! What happened to the "single" vertical group? Well, we had the the "double vertical group" element width set to 30% of its parent and the "single vertical group" element width set to 70% of its parent ... so now we have:

30%  30%  70%

Which, of course, doesn't fit into 100% ... so,

Step 5 - let's set the element widths to 1/3

let fWidth: CGFloat = 1.0 / 3.0

let singleVerticalGroup = NSCollectionLayoutGroup.vertical(
    // width of this group is now 1/3
    layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(fWidth),
                                       heightDimension: .fractionalHeight(1.0)),
    subitem: singleItem, count: 1)
        
let doubleVerticalGroup = NSCollectionLayoutGroup.vertical(
    // width of this group is now 1/3
    layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(fWidth),
                                       heightDimension: .fractionalHeight(1.0)),
    subitem: singleItem, count: 2)
        

and we're getting much closer:

enter image description here

Step 6 - your design wants 3 items stacked vertically, not 2, so let's name it tripleVerticalGroup and change count: 2 to count: 3:

let tripleVerticalGroup = NSCollectionLayoutGroup.vertical(
    //  subitem is 3 singleItems, arranged vertically
    layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(fWidth),
                                       heightDimension: .fractionalHeight(1.0)),
    subitem: singleItem, count: 3)
        

and our repeatingGroup items:

let repeatingGroup = NSCollectionLayoutGroup.horizontal(
    // width is percentage of "parent"
    // height is percentage of "parent"
    //  subitem is now 3 "groups" arranged horiztonally ...
    layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.85),
                                       heightDimension: .fractionalHeight(0.4)),
    subitems: [tripleVerticalGroup, tripleVerticalGroup, singleVerticalGroup])

and we get:

enter image description here

Step 7 - the last thing we need to do is adjust the layout so each element in our repeatingGroup fills the width of the view.

We note that the repeatingGroup layout size used:

widthDimension: .fractionalWidth(0.85)

so let's change that to 3 times the width of the view (its parent):

let repeatingGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(3.0),
                                       heightDimension: .fractionalHeight(0.4)),
    subitems: [tripleVerticalGroup, tripleVerticalGroup, singleVerticalGroup])

and the final output is (showing scrolling):

enter image description here enter image description here

enter image description here enter image description here


I created a GitHub repo here: https://github.com/DonMag/AppleModernCollectionViews with all of the above.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 DonMag