Compositional Layout on iOS 13. The Basics

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, UITableViewor 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-sizingcell 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
    }

Nested Groups Scrolling


? ! , , .


// ...
section.orthogonalScrollingBehavior = .continuous
// ...



iOS 13 UICollectionView, Compositional Layout .
. .


:



All Articles