'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 |
// | |
// | |
// | |
// | |
// +------------------------------+
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):
Step 1 - I think Apple did a very bad job with naming, so I'm changing:
leadingItem
tosingleItem
trailingItem
todoubleItem
trailingGroup
todoubleVerticalGroup
containerGroup
torepeatingGroup
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):
Step 2 we can change the order easily, by swapping the order of the repeating group's subitems: [doubleVerticalGroup, singleItem])
:
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):
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:
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:
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:
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):
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 |