Good afternoon,
In practice, iOS, the developer often faces the task of displaying a large amount of information in the form of a list or as a collection, as a rule, UITableView
or are great for this UICollectionView
. Also often encountered is the task of realizing a screen, which is a combination of a list and a collection.
In this article, we will consider what new features iOS 13 brought to implement this task.

Introduction
Imagine the task came to you to realize a screen on which information can scroll both vertically and horizontally, each section has its own layout, for example, in the AppStore application .

How will you implement this?
Before iOS 13, most likely you would do it like that
- The table in each cell of which there is a collection
- A collection with a different collection in each cell
- Collection, with its custom layout
- Your custom heir
UIScrollView
All these solutions contain a number of problems.
- It is difficult to implement and maintain.
- Hard to get good performance
- It's hard to make an animation
- Difficult to make
self-sizing
cell support - Difficult to do simultaneous support for iPad and iPhone
- The solution is tailored only for a specific scenario.
The developers were spinning as best they could and it was so until iOS 12.
Compositional layout
iOS 13 Apple , — Compositional Layout, 3- — , .
— , , , , , , , .

Compositional Layout — , , .
— , , - . , .
— grid-, flow- , .
— , , .
Compositional Layout. 4 :
NSCollectionLayoutSize
— ;NSCollectionLayoutItem
— ;NSCollectionLayoutGroup
— , ;NSCollectionLayoutSection
— ;UICollectionViewCompositionalLayout
— .
UICollectionViewCompositionalLayout
.
class UICollectionViewCompositionalLayout : UICollectionViewLayout { ... }
UICollectionViewCompositionalLayout
UICollectionViewLayout
, UICollectionView
.
List Layout
. storyboard-, UIViewController
, viewDidLoad()
UICollectionView
init(frame:, layout:)
UICollectionViewCompositionalLayout
. , UICollectionViewCompositionalLayout
.
, . , , , .

.
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
private func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
}
itemSize
, , NSCollectionLayoutDimension
. .
fractionalWidth(_:)
, , 0 1 , , 0.5
— () ;fractionalHeight(_: )
, , 0 1 , , 0.5
— () ;absolute(_: )
, , , 44.0
;estimated(_:)
, , .
itemSize
widthDimension = .fractionalWidth(1.0)
, heightDimension = .fractionalHeight(1.0)
, , . .
, widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44)
, , , 44.0
.
:
horizontal(layoutSize: , subitem: , count: )
, , ;horizontal(layoutSize:, subitems: )
, , , ;vertical(layoutSize:, subitem:, count:)
, , ;vertical(layoutSize:, subitems: )
, , , ;custom(layoutSize:, itemProvider: )
, , .
, .horizontal
.vertical
, , , .custom
, , .
NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
, , .
NSCollectionLayoutSection(group: group), . Compositional Layout
.
, , . ?
Grid Layout

, ? , , , .
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0/4.0),
heightDimension: .fractionalHeight(1.0))
, 4 , .

Two Column Layout
, , , , , widthDimension
.

.
private func createLayout() -> UICollectionViewLayout {
let spacing: CGFloat = 10
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
section.interGroupSpacing = spacing
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
group.interItemSpacing = .fixed(spacing)
, section.interGroupSpacing = spacing
, contentInset
section.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
. spacing
.
, interItemSpacing
, interGroupSpacing
, NSCollectionLayoutSpacing
.
class NSCollectionLayoutSpacing : NSObject, NSCopying {
class func fixed(_ fixedSpacing: CGFloat) -> Self // i.e. ==
class func flexible(_ flexibleSpacing: CGFloat) -> Self // i.e. >=
// ...
}
func fixed(_ fixedSpacing: )
, ;func flexible(_ flexibleSpacing:)
, , , ;
Inset Items Grid Layout
, , ? , , . , 5 , , 1.0/5.0 , . , 1.0/5.0 . , .
, .contentInsets
NSCollectionLayoutItem
.

.
private func createLayout() -> UICollectionViewLayout {
let spacing: CGFloat = 10
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.2),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(0.2))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
Distinct Sections Layout
, .
UICollectionViewCompositionalLayout
, UICollectionViewCompositionalLayoutSectionProvider
.
typealias UICollectionViewCompositionalLayoutSectionProvider = (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection?
class UICollectionViewCompositionalLayout : UICollectionViewLayout {
init(section: NSCollectionLayoutSection)
init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider)
}
UICollectionViewCompositionalLayoutSectionProvider
2 , , , .
3 , , grid- 3- , grid-, 5- .

.
enum Section: Int, CaseIterable {
case list
case grid3
case grid5
var columnCount: Int {
switch self {
case .list:
return 1
case .grid3:
return 3
case .grid5:
return 5
}
}
}
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
let columns = sectionKind.columnCount
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 2, leading: 2, bottom: 2, trailing: 2)
let groupHeight = columns == 1 ?
NSCollectionLayoutDimension.absolute(44) :
NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
}
Adaptive Sections Layout
, NSCollectionLayoutEnvironment
, traitCollection
.
protocol NSCollectionLayoutEnvironment : NSObjectProtocol {
var container: NSCollectionLayoutContainer { get }
var traitCollection: UITraitCollection { get }
}
ompact size-class , , , , .
traitCollection.verticalSizeClass
layoutEnvironment
.
container.effectiveContentSize
layoutEnvironment
.

.

.
enum Section: Int, CaseIterable {
case list
case grid3
case grid5
func columnCount(for width: CGFloat) -> Int {
let wideMode = width > 800
switch self {
case .list:
return wideMode ? 2 : 1
case .grid3:
return wideMode ? 6 : 3
case .grid5:
return wideMode ? 10 : 5
}
}
}
private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }
let columns = sectionKind.columnCount(for: layoutEnvironment.container.effectiveContentSize.width)
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 2, leading: 2, bottom: 2, trailing: 2)
let groupHeight = layoutEnvironment.traitCollection.verticalSizeClass == .compact ?
NSCollectionLayoutDimension.absolute(44) :
NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
}
Nested Groups
class NSCollectionLayoutGroup : NSCollectionLayoutItem, NSCopying { ... }
NSCollectionLayoutGroup
, , NSCollectionLayoutItem
, , , .

.
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: 10, leading: 10, bottom: 10, trailing: 10)
let trailingItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3)))
trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
heightDimension: .fractionalHeight(1.0)),
subitem: trailingItem, count: 2)
let bottomNestedGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.6)),
subitems: [leadingItem, trailingGroup])
let topItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3)))
topItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let nestedGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.4)),
subitems: [topItem, bottomNestedGroup])
let section = NSCollectionLayoutSection(group: nestedGroup)
return section
}
return layout
}
? ! , , .
section.orthogonalScrollingBehavior = .continuous

iOS 13 UICollectionView
, Compositional Layout .
. .
: