What is Collection View Compositional Layouts?
What problem does it solve?
Anatomy: Compositional Layouts
Backporting Collection View Compositional Layouts
Kishikawa, an iOS engineer. Today, I'm excited to talk to you about collection view compositional layouts. It is a new API introduced iOS 13. It is an entirely new mechanism for building custom layouts for collection views. It's very powerful and flexible. You can easily make any layout without bugs. It's amazing. It just grabs my heart when I watched a session about it at WWDC. But this wonderful API is less known than I expected,
problem does it solve? • Anatomy: Compositional Layouts • Backporting Collection View Compositional Layouts So today, I'm going to share what the collection view compositional layout is. And I will talk about what problems that the old ways have so far, and how the new API will solve them. Then, I will explain how it works. And I wanted to use this wonderful API in production right away, so I ported it to iOS12 and older versions. So finally, I'm going to share some technique to backport it to earlier iOS 12. Let's get started.
by iOS 13 • Extremely easy to build custom complex collection view layouts • Powerful features to beat complex layouts for modern apps Collection view compositional layout is a new API introduced by iOS 13. It makes extremely easy to build custom layouts for collection views. It's a completely different approach, and it's easy to use and hard to mistake. Also, it has powerful features that make it easy to achieve complex behaviors that a modern app requires.
be displayed • Various sizes of content are on a scroll view • Nested scrolling The App Store app has a typical modern app UI. The layout of this app is very complicated. Various sizes of content should be displayed, on a scroll view. Also, in most cases, nested scrolls are required. It looks cool when the scrolls are nested vertically and horizontally. It is making people think that the app is a cool modern app. So nesting scroll views orthogonally are often required to make your app look very modern and cool.
with Scroll View? • Collection View on Collection View • Collection View Custom Layouts So how do we make a such UI? There are multiple solutions. For example, using multiple collection views? Stack view and scroll view? Or build custom collection view layouts? Each solution has trade-offs. Collection view on collection view is much simpler than building custom layouts. However, data management and reusing cells are dramatically more complicated. Building custom layouts seems right way, but I never want to do. Why?
• Override methods as needed. What should I do to build custom collection view layouts? Subclassing UICollectionViewLayout. Then override methods as needed. That's it. It's a piece of cake! Right?
everything yourself • I don't know where to start • Error prone • Too flexible, overkill To create a custom layout, subclass UICollectionViewLayout. But UICollectionViewLayout class is a very abstract class. So we have to implement everything ourselves. UICollectionViewLayout doesn't help anything to calculate the layout. There is just an empty method to override. You have to do all the layout calculations and prepare yourself.
invalidationContextClass: AnyClass func prepare() func layoutAttributesForElements(in rect: CGRect) func layoutAttributesForItem(at indexPath: IndexPath) func layoutAttributesForSupplementaryView(ofKind: String, at: IndexPath) func layoutAttributesForDecorationView(ofKind: String, at: IndexPath) func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) func invalidationContext(forBoundsChange newBounds: CGRect) func shouldInvalidateLayout(forPreferredLayoutAttributes:, withOriginalAttributes:) func invalidationContext(forPreferredLayoutAttributes:, withOriginalAttributes:) func targetContentOffset(forProposedContentOffset: CGPoint, withScrollingVelocity: CGPoint) func targetContentOffset(forProposedContentOffset: CGPoint) var collectionViewContentSize: CGSize ... The code here is an example of the UICollectionViewLayout interface. It has many methods. Most people don't know where to start, what we should do. We don't know the right way, and it is error-prone. So we are forced to trial and error many times. Therefore, an implementation cost becomes too high. I'm pretty sure that is not what we wanted.
Hard Flow Layout Layout Layout This figure very roughly shows the implementation costs of each solution. Also, the layout complexity that each solution can produce. So which way do we use? Collection View Flow layout can be used easily. But the range it covers is not big. It is difficult to build a complex layout using only that.
Hard Flow Layout Implementation Layout Layout Other solution, the way of nesting collection views, called the collection view on collection view, can be used to build complex layouts. So most people tend to choose a way to nest collection views. Because another solution, building custom layouts are too early to use for us. However, it should become much more difficult; managing data sources and each scroll offset, UI event handling, and reusing cells become much more difficult.
Hard Flow Layout Compositional Layouts Implementation Layout Layout Then the compositional layout appears this position! You can easily create any complex layout and easy to use. I even think that all views can be built with the compositional layouts.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) The code that achieves this layout looks like this: →
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) The item I just explained is created programmatically.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) Also a group is created in the similar way. The group contains the item.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) Section has the group.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) When creating an item or group specify the size for them. We can use the relative value to specify the size from the container, which provides great flexibility.
the group, the width specified the same as the item fractionalWidth 1, so it is the same as the section width. On the other hand, the height is specified as absolute 50; then it will be 50 points height regardless of the size of the container.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) Basically this is all. You will find it very simple. You may think that using flow layout is easier or prefer nested collection views. Please wait. A compositional layout can produce any layout just by composing them. In other words, no matter how complex the layout you create, the code complexity will not increase any more. This means this is the upper limit for code complexity. So there are few bugs. Because it is just configuring not implement, we don't need to write any complex code.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension: .fractionalHeight(0.4)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) The code is very similar to the previous example. Only the size setting for the group are slightly different.
NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension: .fractionalHeight(0.3)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 3) let section = NSCollectionLayoutSection(group: group) There are multiple ways to specify the size when a group contains multiple items.
specify the number of items in the group so that the size is automatically determined equally. This method is useful when the number of items that the group contains is fixed. This is because the framework will automatically calculate the size that will fit exactly. We don't have to think about how much size to specify to fit into the group.
switch sectionIndex { case 0: ... return section case 1: ... return section ... } } Compose Them into Layout All that remains is to composing them. Return each section object correspond to each section index.
create such a layout yourself by subclassing UICollectionViewlLayout or flow layout. You have to write complex code that interacts closely with the data to determine the size of each cell for each index.
any complicated layout with a composed of four parts. Therefore the code complexity does not increase. You can keep your code simple. We no longer need to implement UICollectionViewLayout. That's so cool! You think so too, right?
= NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [itemGroup]) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPagingCentered The compositional layout automatically generates nested scrolling by setting just one property.
.fractionalHeight(0.4)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [itemGroup]) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .groupPagingCentered orthogonalScrollingBehavior. When this property is set, →
is a clone of the AppStore sample app I made. This is just only a layout, but I was able to make it in 5, 6 hours. This project is published on GitHub, so you can see how the code simple and easy.
(e.g. Section Header/Footers) ✅ Pinned Section Header/Footers ✅ Orthogonal Scrolling Behavior ✅ Estimated Size (Autosizing) ✅ Custom Group Item (Absolute Positions) ✅ Drop-in replacement ❌ RTL Support It is still under development, not fully supported functionality and lots of bugs left. But it's very helpful even now. AppStore clone app I mentioned works perfectly on earlier iOS 12.
won't render correctly. But do we need to be able to create such a layout? I have no experience to make such a layout so far. Since I can create an App Store app layout, it can probably cover ninety-percent of use cases, I guess.
{ return [[NSClassFromString(@"UICollectionViewCompositionalLayout") alloc] initWithSection:section]; } else { IBPUICollectionViewCompositionalLayoutConfiguration *configuration = [IBPUICollectionViewCompositionalLayoutConfiguration defaultConfiguration]; return [self initWithSection:section configuration:configuration]; } } The most interesting part of this library is that it provides a complete drop-in replacement. It provides emulated implementations on iOS 12 and below, but on iOS 13 and above, it automatically switches to the original implementation. In other words, there is nothing to do when you would omit support for iOS 12 in the future. No problem if you forget to remove this library because this library does nothing on iOS 13.
surprisingly easy. It works just by creating a new collection view internally and passing a layout that changed the scroll direction. This implementation is smart without using black magic such as method swizzling, so please read the code if you are interested.
No need to implement collection view layout subclass • Like just editing configurations • Keep your code simple regardless layout become complex • You can use it now! (without iOS 13) Collection View Compositional Layouts is incredible! No need to implement collection view layout subclass Like just editing configurations Keep your code simple regardless layout become complex You can use it now! (without iOS 13)
an item's dimension along an estimated axis. This layout axis insets will be ignored (however, any non-estimated axis insets will be applied). Please consider using the item's edgeSpacing or the containing group's interItemSpacing instead. The behavior of this API is not documented at all, but I found there are some settings that do not work well. When stepping on such a trap, you may see a warning at the console. You can see the setting that does not work.
you need to know about UICollectionViewCompositionalLayout https://medium.com/flawless-app-stories/all-what-you-need-to-know-about- uicollectionviewcompositionallayout-f3b2f590bdbe • Using CollectionView Compositional Layouts in Swift 5 https://dev.to/kevinmaarek/using-collectionview-compositional-layouts-in-swift-5-1nan • Move your cells left to right, up and down on iOS 13 — Part 1 https://medium.com/shopback-engineering/move-your-cells-left-to-right-up-and-down-on- ios-13-part-1-1a5e010f48f9 You’d like to learn more about the concepts we’ve discussed here today, I strongly encourage you to check out the Advances in Collection View Layout session from this year’s WWDC. I hope this talk will help you develop your app in the future. It’s been an absolute pleasure speaking with you! Thank you for your time. I hope you enjoy the rest of your conference :)