New Collection View APIs in iOS 13 - UICollectionView Compositional Layout
This blog post is based on the Session 215 - Advances in Collection View Layout from WWDC 2019. I would highly recommend to go through this video to learn basics about new collection view APIs and basics of Compositional layout on iOS.
In Session 215 of WWDC 2019, Apple introduced a brand new API for collection views to facilitate building complex layouts with minimal lines of code with ease of extension and maintenance. The thing I was most excited about was introduction of compositional layout.
At its core, Composite Layout is made up of four components going from bottom to top,
- Item
- Group
- Section
- Layout
Depending on how you leverage this structure, you can make layouts ranging from simple to most complex which would otherwise have been nearly difficult had it not been for third party APIs. In order to keep things simple, reduce the length of this post, and keep the topic focused, I am solely going to concentrate on how to make a custom layout using new APIs.
Here, I will assume that you already have set up
- Collection view (Programmatically or using Storyboards)
- Respective
dataSource
anddelegates
cellForRowAtIndexPath
datasource method or usingdiffable
data source (More info ondiffable
here)
Here, we will touch only on the subject of creating and assigning a UICollectionViewLayout
to the collection view,
collectionView.collectionViewLayout = createLayout()
All we're going to see is a different variations of createLayout()
method which will give us range of layout options,
- Simple layout
To begin with, we want to create a layout like this, this is similar to the one Apple demonstrated during the presentation,
This might have looked intimidating without composite layout, but turns out it's just few lines of code when new collection view APIs are used. Here we will make a division like this
- A leading item
- A trailing group containing two nested items
- A nested group containing both leading item and trailing group from previous steps
We will start by creating a layout for leading item. Since we want to maintain unequal widths for both parts, we will assign fractional width of 0.7 to leading item. (Which means whatever the width of parent container, our leading item will end up taking 70%
container width)
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1.0)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
Please note that we have also set contentInsets
with 5 pixels on each side and this is optional and depends what your design team wants
Now, we will start constructing trailing group with two items stacked vertically on each other. We want this trailing group to take 30% of total width of its parent container. (Because we already allocated 70% to leading group in the previous step) The group will have
- Two items vertically stacked
- Each item taking
100%
of its parent container width - Each item taking
50%
of its parent container height
let trailingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
// The following line could also be
//let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0)), subitems: [trailingItem])
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0)), subitem: trailingItem, count: 2)
Now all we have to do it to combine leadingItem
and trailingGroup
into another nested group. We will set the nested group dimensions relative to the full screen size,
- It should occupy
85%
of total screen width - It should occupy
40%
of total screen height
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.85), heightDimension: .fractionalHeight(0.4)), subitems: [leadingItem, trailingGroup])
We're almost done. All we have to do now is to create a section embedding this nested group inside and assigning contentInsets
if applicable,
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
If we combine all the previous steps, our createLayout()
function will look like this,
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1.0)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0)), subitem: trailingItem, count: 2)
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.85), heightDimension: .fractionalHeight(0.4)), subitems: [leadingItem, trailingGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
return section
}
return layout
}
// And somewhere in your file, assign this layout to the collection view (Assuming you've already set up your data sources)
collectionView.collectionViewLayout = createLayout()
Here is little graphical summary of how this code gets converted into the layout,
Now if you run your code, this layout will scroll vertically - Which is expected.
However, if you want it to scroll horizontally this is just one line change
// Possible values of orthogonalScrollingBehavior are
// .none, .continuous, .continuousGroupLeadingBoundary, .paging, .groupPaging, and .groupPagingCentered
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
Note: For all the subsequent examples, we will be using .continuousGroupLeadingBoundary
for the orthogonal scrolling behavior. However, the choice of scrolling behavior or lack of it depends on your use-case and there is no hard and fast rule which one you should be using
2. Vertical tri-section layout
Now we will move on to bit more complex layout I affectionately call Vertical tri-section layout
. To give you little idea about it, this is how it's supposed to look like,
Here's how our division will look like
- Vertically divide this layout in three sections
- First section takes
33%
of total container width and contain 3 items vertically stacked each taking33%
of its parent container width - Section section takes
33%
of its parent container width and occupies100%
of its height - Third section, just like the first, takes
33%
of total container width and contain 3 items vertically stacked each taking33%
of its parent container width
Let's start writing some code now,
Leading section
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.3)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)), subitem: leadingItem, count: 3)
Middle section
let middleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)))
middleItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
And finally, trailing section
let trailingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.3)))
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)), subitem: trailingItem, count: 3)
Nesting all of the together in the top level nestedGroup
and converting it to section
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.85), heightDimension: .fractionalHeight(0.5)), subitems: [leadingGroup, middleItem, trailingGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
If we combine all the previous snippets and put them inside createLayout()
method, it will look like this
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.3)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)), subitem: leadingItem, count: 3)
let middleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)))
middleItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.3)))
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)), subitem: trailingItem, count: 3)
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.85), heightDimension: .fractionalHeight(0.5)), subitems: [leadingGroup, middleItem, trailingGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return section
}
return layout
}
// And somewhere in your file, assign this layout to the collection view (Assuming you've already set up your data sources)
collectionView.collectionViewLayout = createLayout()
Here is little graphical summary of how this code gets converted into the layout,
3. Image carousel layout
As name suggests, we are going to create a simple layout to support image slider. This is much simpler than the earlier one since we only have one item and group to deal with. Subcomponents are as follows,
- An item taking
100%
of its parent container width and height - A group occupying
100%
of its parent container width and absolute height of150px
- A section encapsulating this group with
5px
padding on each side
First off, an item taking 100%
of its parent container width and height,
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 15.0)
Now, a group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(150.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
And finally, a section encapsulating this group,
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .paging
Please note how we set the orthogonalScrollingBehavior
to paging
. When this parameter is set, page size is equal to the collection view's bounds. It means, every time we swipe, it's going to display the next page.
Combining everything in our createLayout
method,
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 15.0)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(150.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .paging
return section
}
return layout
}
// And somewhere in your file, assign this layout to the collection view (Assuming you've already set up your data sources)
collectionView.collectionViewLayout = createLayout()
As a bonus, here's video of image slider in action,
Here's the demonstration of how source code gets mapped to image slider layout,
4. Tree layout
This layout is bit more complicated and my sincere apologies for misleading name. I couldn't come up with suitable name for this layout, but looks at the layout and decide by yourself which name would you like to assign to it.
Please bear with me. This is going to take little bit longer to explain all the subcomponents of this layout. But trust me, I will try to make it as simple as possible
This component contain,
- Nested group
1. Leading full group
1. Leading top item
2. Leading middle group
1. Leading middle item (Count: 2)
3. Leading bottom group
1. Leading bottom item (Count: 2)
2. Trailing full group
1. Trailing top group
1. Trailing top item (Count: 2)
2. Trailing middle group
1. Trailing middle item (Count: 2)
3. Trailing bottom item
So now we're done with the hierarchical structure. Let's start converting it into the actual Swift code piece-by-piece
Leading group
Leading top item
let leadingTopItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
leadingTopItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
Leading middle group
let leadingMiddleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
leadingMiddleItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let leadingMiddleGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: leadingMiddleItem, count: 2)
Leading bottom group
let leadingBottomItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
leadingBottomItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let leadingBottomGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: leadingBottomItem, count: 2)
Leading full group
let leadingFullGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(1.0)), subitems: [leadingTopItem, leadingMiddleGroup, leadingBottomGroup])
Trailing group
Trailing top group
let trailingTopItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
trailingTopItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingTopGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: trailingTopItem, count: 2)
Trailing middle group
let trailingMiddleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
trailingMiddleItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingMiddleGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: trailingMiddleItem, count: 2)
Trailing bottom item
let trailingBottomItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
trailingBottomItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
Trailing full group
let trailingFullGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(1.0)), subitems: [trailingTopGroup, trailingMiddleGroup, trailingBottomItem])
Nested Group and Full Section
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(0.9)), subitems: [leadingFullGroup, trailingFullGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
Now just like previous examples, we will combine the full code into createLayout
method, which will return the object of type UICollectionViewLayout
that will be directly applied to the UICollectionView
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
// Leading Top item
let leadingTopItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
leadingTopItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
// Leading Middle Group
let leadingMiddleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
leadingMiddleItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let leadingMiddleGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: leadingMiddleItem, count: 2)
// Leading Bottom Group
let leadingBottomItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
leadingBottomItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let leadingBottomGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: leadingBottomItem, count: 2)
// Trailing Top Item
let trailingTopItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
trailingTopItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingTopGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: trailingTopItem, count: 2)
// Trailing Middle Group
let trailingMiddleItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)))
trailingMiddleItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let trailingMiddleGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)), subitem: trailingMiddleItem, count: 2)
// Trailing Bottom Item
let trailingBottomItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
trailingBottomItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
// Leading Full Group
let leadingFullGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(1.0)), subitems: [leadingTopItem, leadingMiddleGroup, leadingBottomGroup])
// Trailing Full Group
let trailingFullGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(1.0)), subitems: [trailingTopGroup, trailingMiddleGroup, trailingBottomItem])
// Nested Group
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(0.9)), subitems: [leadingFullGroup, trailingFullGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return section
}
return layout
}
// And somewhere in your file, assign this layout to the collection view (Assuming you've already set up your data sources)
collectionView.collectionViewLayout = createLayout()
And here's the graphical demonstration of how the source code gets mapped to tree layout,
6. Square family layout
And this is the final and most complicated layout we will see how to construct using composite layout APIs. I got to admit, this was the most difficult layout for me and took the maximum time working out mathematics behind it. It involves bit of mathematics, but please bear with me. We will go through it step-by-step.
Here, our constraints are bit restrictive. We have big square appearing in the top left corner surrounded by more smaller squares. Here's how we will divide this structure into separate sub-components.
- Nested group
1. Left nested group
1. Top leading item
2. Middle leading group
1. Middle leading item (Count: 2)
3. Bottom leading group
1. Bottom leading item (Count: 2)
2. Right nested group
1. Right vertical item (Count: 4)
Top leading item
As evident from designs, we want top item to take 66.67%
of total width and 50%
of total height of its parent container. However, here we will create divisions like this - Left container vs. Right container
So our top leading item will be the part of left container group. This item will take 100%
of total parent width (In composite layout terminology, .fractionalWidth(1.0)
). Since we want height to match with width, we will set height to .fractionalWidth(1.0)
as well.
let topLeadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(1.0)))
topLeadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
Middle leading group
This group is a part of Left container as well - Appearing horizontally in the middle section. We will create individual Middle leading item to take 50%
of total parent container width and set the height to the same value.
Middle leading group will take 100%
of total parent width and only 50%
of total width as height (To maintain the "square" look of its child items)
let middleLeadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(0.5)))
middleLeadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let middleLeadingGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5)), subitems: [middleLeadingItem])
Bottom leading group
It will follow exact same structure as Middle leading group except that it will appear at the end in the left container
let bottomLeadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(0.5)))
bottomLeadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let bottomLeadingGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5)), subitems: [bottomLeadingItem])
Left nested group
Left nested group will take 2/3rd of total parent container width and 100% of its height
let leftNestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.6667), heightDimension: .fractionalHeight(1.0)), subitems: [topLeadingItem, middleLeadingGroup, bottomLeadingGroup])
Right vertical item (Count: 4)
Right vertical item is a part of right vertical group which ultimately is a part of right container. Since we want these items to be uniformly distributed, we will set the width to full parent container width, but height to only 25%
of total parent container height.
let rightVerticalItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)))
rightVerticalItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
Right vertical group
Right vertical group will take 1/3rd of total parent container width and 100% of its height
let rightNestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3334), heightDimension: .fractionalHeight(1.0)), subitems: [rightVerticalItem])
Nested Group
Now this is the final step. Here we will combine left and right nested group into top-level nested group to form the final layout. Here we will occupy 90%
of total container width. But for height we will need 1/3rd extra since layout aspect ratio is more like 1:1.34
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(1.20006)), subitems: [leftNestedGroup, rightNestedGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 100.0, leading: 35.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return section
And if we combine all the previous snippets to create a square family layout, it will look like this,
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let topLeadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(1.0)))
topLeadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let middleLeadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(0.5)))
middleLeadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let middleLeadingGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5)), subitems: [middleLeadingItem])
let bottomLeadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(0.5)))
bottomLeadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let bottomLeadingGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5)), subitems: [bottomLeadingItem])
let leftNestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.6667), heightDimension: .fractionalHeight(1.0)), subitems: [topLeadingItem, middleLeadingGroup, bottomLeadingGroup])
let rightVerticalItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.25)))
rightVerticalItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
let rightNestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3334), heightDimension: .fractionalHeight(1.0)), subitems: [rightVerticalItem])
// 0.9 * 0.6667 + 0.9 * 0.6667 = 1.20006 total height of the container view
let nestedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(1.20006)), subitems: [leftNestedGroup, rightNestedGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return section
}
return layout
}
And here's the graphical demonstration of how the source code gets mapped to square family layout,
And that should be all folks. Hope these example were useful to you. All these examples are available on GitHub to download with source code. You can just clone the repository and switch to the branch named composite-layout-examples
, to see all these examples in action
If you have further questions or need help with any design where composite layout may be useful, feel free to get in touch with me