The previous episode focused on the basics of compositional layouts. A key feature of compositional layouts is the simple and readable API. The code we wrote in the previous episode isn't complex or difficult to understand. Another nice bonus is that we didn't need to subclass the UICollectionViewCompositionalLayout class.

Adaptive Layouts

The user interface looks fine on iPhone, but it falls short on iPad. We want to take advantage of the larger screen of iPad. The compositional layout should update itself dynamically as the size of the collection view changes. The goal is to show two columns on iPad in portrait and three columns on iPad in landscape. The user interface should also update if the user resizes the application window. Compositional layouts make this almost trivial.

In the previous episode, we initialized a UICollectionViewCompositionalLayout instance by invoking the init(section:) initializer. The initializer accepts an instance of the NSCollectionLayoutSection class. This approach works fine for a static compositional layout. We need a dynamic solution.

The idea is simple. We need the ability to create an NSCollectionLayoutSection instance every time the size of the collection view changes. One option is to create a brand new UICollectionViewCompositionalLayout instance every time the size of the collection view changes and update the collectionViewLayout property of the collection view. This doesn't feel right, though. The solution should be more elegant and, fortunately, it is.

Updating the Collection Layout Section

In the previous episode, I mentioned that the UICollectionViewCompositionalLayout class offers two options to create a compositional layout. We explored the first option in the previous episode. The second option gives us more control and it is more dynamic.

Instead of passing an NSCollectionLayoutSection instance to the initializer, we pass a section provider to the initializer. A section provider is nothing more than a closure. The closure accepts two arguments, the section index and an object that conforms to the NSCollectionLayoutEnvironment protocol. The return value of the section provider is of type NSCollectionLayoutSection?.

private func createCollectionViewLayout() -> UICollectionViewLayout {
    return UICollectionViewCompositionalLayout { (section, environment) -> NSCollectionLayoutSection? in

    }
}

Let me explain how the API works. We no longer pass an NSCollectionLayoutSection instance to the initializer. We take a different approach. Every time the compositional layout needs an NSCollectionLayoutSection instance, the section provider is executed. The closure has access to the section index and an object conforming to the NSCollectionLayoutEnvironment protocol. The closure inspects the section index and the NSCollectionLayoutEnvironment object and, based on this information, creates and returns an NSCollectionLayoutSection instance. The compositional layout executes the section provider every time the collection view needs to respond to a change, for example, when the bounds of the collection view change.

The NSCollectionLayoutEnvironment object is a key element to create an adaptive user interface. It defines two properties, container of type NSCollectionLayoutContainer and traitCollection of type UITraitCollection. You are probably already familiar with trait collections and the UITraitCollection class. The NSCollectionLayoutContainer protocol is new. The protocol defines four properties that expose information about the container, contentInsets, contentSize, effectiveContentInsets, and effectiveContentSize. Let's use what we have learned to create a compositional layout that is adaptive.

Implementing the Section Provider

We can reuse most of the implementation of the createCollectionViewLayout() method. To create the UICollectionViewCompositionalLayout instance, we invoke the init(sectionProvider:) initializer, passing in a section provider. Notice that we return the result of the initializer from the createCollectionViewLayout() method.

private func createCollectionViewLayout() -> UICollectionViewLayout {
    return UICollectionViewCompositionalLayout { (section, environment) -> NSCollectionLayoutSection? in

    }
}

We use the NSCollectionLayoutEnvironment object to define the number of columns. We could use the traitCollection property of the NSCollectionLayoutEnvironment object, but that won't allow us to display two columns on iPad in portrait and three columns on iPad in landscape. The effective content size of the container defines the number of columns the collection view displays.

We declare a constant of type Int with name numberOfColumns and assign a self-executing closure to it. This is a technique I use quite a bit. In the closure, we use a switch statement and switch on the width of the container's effective content size. If the width of the container falls between 0 and 499 points, the collection view displays a single column. If the width of the container falls between 500 and 1024 points, the collection view displays two columns. If the width of the container is larger than 1024 points, the collection view displays three columns.

private func createCollectionViewLayout() -> UICollectionViewLayout {
    return UICollectionViewCompositionalLayout { (section, environment) -> NSCollectionLayoutSection? in
        // Calculate Number of Columns
        let numberOfColumns: Int = {
            switch environment.container.effectiveContentSize.width {
            case 0..<500:
                return 1
            case 501...1024:
                return 2
            default:
                return 3
            }
        }()
    }
}

The next step is creating an NSCollectionLayoutSection instance. We reuse most of the code we wrote in the previous episode. The only change we need to make relates to the creation of the NSCollectionLayoutGroup instance. We use a different class method to create the NSCollectionLayoutGroup instance. We invoke the horizontal(layoutSize:subitem:count:) method, passing in the size of the group, the item the group displays, and the number of items in the group.

private func createCollectionViewLayout() -> UICollectionViewLayout {
    return UICollectionViewCompositionalLayout { (section, environment) -> NSCollectionLayoutSection? in
        // Calculate Number of Columns
        let numberOfColumns: Int = {
            switch environment.container.effectiveContentSize.width {
            case 0..<500:
                return 1
            case 501...1024:
                return 2
            default:
                return 3
            }
        }()

        // Define Item Size
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200.0))

        // Create Item
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // Define Group Size
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200.0))

        // Create Group
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: numberOfColumns)

        // Create Section
        let section = NSCollectionLayoutSection(group: group)

        // Configure Section
        section.contentInsets = NSDirectionalEdgeInsets(top: 0.0, leading: 20.0, bottom: 0.0, trailing: 20.0)

        return section
    }
}

We return the NSCollectionLayoutSection instance from the section provider. I hope you agree that this isn't difficult to understand. We pass a section provider to the initializer of the UICollectionViewCompositionalLayout class. The section provider, a closure, is executed every time the compositional layout needs an NSCollectionLayoutSection instance for a section. In the section provider, the feed view controller inspects the NSCollectionLayoutEnvironment object, creates an NSCollectionLayoutSection instance, and returns it from the closure.

Build and Run

The code we wrote is similar to the code we wrote in the previous episode. The key difference is that the collection view adapts its layout to external events, for example, when the user rotates the device or when the user resizes the application window. Let me show you what the result looks like. The best way to demonstrate the adaptive user interface is by running the application in Split View mode.

Build and run the application. The feed view controller shows three columns in landscape. That is a good start.

Adaptive User Interfaces With Compositional Layouts

Swipe up from the bottom to bring up the dock. Press the Safari icon and drag it to the right side of the screen to enter Split View mode. The application window of the Cocoacasts client resizes automatically and the feed view controller also updates its user interface by showing two columns instead of three.

Adaptive User Interfaces With Compositional Layouts

If we give Safari more space, then the application window of the Cocoacasts client resizes automatically and the feed view controller shows a single column. The feed view controller shows three columns if we remove Safari.

Adaptive User Interfaces With Compositional Layouts

This example also illustrates what we learned in the previous episode about the relation between items, groups, and sections. Groups are an integral part of compositional layouts and the basic units of layout. Each group groups one or more items. On iPhone, a group contains a single item. On iPad, a group contains two items in portrait and three items in landscape.

Don't be confused by the use of the term columns in this episode. The numberOfColumns constant we used in the createCollectionViewLayout() method defines the number of items in each group. Because the compositional layout creates horizontal groups, this visually translates to columns. A compositional layout has no notion of columns.

What's Next?

With a few lines of code, we were able to transform a static compositional layout into a dynamic compositional layout that looks and feels great on iPhone and iPad. The code we wrote to implement an adaptive layout is easy to understand and not complicated.

Before we move on to the library tab, I want to add a title to the feed view controller. We do this by adding a section header to the only section of the collection view of the feed view controller.