Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Backporting Collection View Compositional Layouts /w notes

Backporting Collection View Compositional Layouts /w notes

What is Collection View Compositional Layouts?
What problem does it solve?
Anatomy: Compositional Layouts
Backporting Collection View Compositional Layouts

- iPlayground 2019 -

Kishikawa Katsumi

September 21, 2019
Tweet

More Decks by Kishikawa Katsumi

Other Decks in Programming

Transcript

  1. Backporting Collection View Compositional Layouts Kishikawa Katsumi Hi! I'm Katsumi

    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,
  2. Agenda • What is Collection View Compositional Layouts? • What

    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.
  3. What is Collection View Compositional Layouts? • New API introduced

    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.
  4. https://developer.apple.com/videos/play/wwdc2019/215/ WWDC 2019 In WWDC 2019, Apple says their own

    App Store app has been rearchitected using compositional layout.
  5. https://developer.apple.com/videos/play/wwdc2019/215/ WWDC 2019 Thanks to compositional layout, he says that

    the codebase can be significantly simplified. Maintainability has improved. Also, his aspect of the collection view has completely changed.
  6. Modern App UI is Complicated! • Various rich contents
 must

    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.
  7. How to make it? • Collection View or
 Stack View

    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?
  8. Problem: Building Custom Collection View Layouts Because building custom layouts

    has a lot of problems. People who have made or people tried to make it may know. What are problems?
  9. How to Build Custom Collection View Layouts • Subclass UICollectionViewLayout

    • 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?
  10. The problem Why difficult to build custom collection view layouts

    Unfortunately NO. I will explain what the problem with building custom layouts is and why I don't want to do it.
  11. Why Difficult to Build Custom Collection View Layouts? • Implement

    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.
  12. UICollectionViewLayout class UICollectionViewLayout { class var layoutAttributesClass: AnyClass class var

    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.
  13. Easy Simple Complex Collection View on Collection View Custom Layouts

    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.
  14. Easy Simple Complex Collection View on Collection View Custom Layouts

    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.
  15. Easy Simple Complex Collection View on Collection View Custom Layouts

    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.
  16. Anatomy: Compositional Layouts Up next, to show that my opinion

    is correct, let's see how the compositional layout works.
  17. App Store.app Let's go back to the App Store.app example.

    Please think; How long does it take time to develop these views? 2 weeks? One month? Using the compositional layout can be done in just 5, 6 hours.
  18. I will explain how to build a layout. Let's see

    these three kinds of components as examples.
  19. Layout, Section, Group, Item Anatomy: Compositional Layouts There are four

    important parts in the compositional layout, →
  20. Layout, Section, Group, Item Layout There is a layout. There

    is only one layout as the outermost container.
  21. Layout, Section, Group, Item Layout Section Group Item Items are

    the smallest units that make up a layout, →
  22. Layout, Section, Group, Item Layout Section Group Item Eventually, the

    position of the item maps to the cell position.
  23. Layout, Section, Group, Item Layout Section Group Item Like that,

    the compositional layout can produce any complex layout to compose these four parts. It's like just configure than implement.
  24. Section Let's try it out. Return to the example. A

    collection of components of the same kind can be represented as a section.
  25. Group There is only one group occupies the whole width

    of the section. The height is fixed.
  26. Group Item The group contains one item. The item occupies

    the whole width and height of the group.
  27. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let 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) The code that achieves this layout looks like this: →
  28. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let 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) The item I just explained is created programmatically.
  29. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let 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) Also a group is created in the similar way. The group contains the item.
  30. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let 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.
  31. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let 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) 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.
  32. let itemSize = ...Size(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) 100% 100% In

    this example, the width and height are specified as hundred % of the group. This means that this item will be of the same size as the group.
  33. let itemSize = ...Size(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1)) 50% 100% 50%

    100% If the width is set to 0.5, the item will be half the width of the group and two items will be placed in the group.
  34. let groupSize = ...Size(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)) 100% 50pt For

    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.
  35. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let 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) 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.
  36. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let item =

    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.
  37. let groupSize = ...Size(widthDimension: .fractionalWidth(0.92), heightDimension: .fractionalHeight(0.4)) 92% 40% The

    height is 40% of the container. Specify the width 92%, so that you can see the next cell a little to indicate that it can scrolled horizontally.
  38. let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let item =

    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.
  39. let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 3) Here,

    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.
  40. let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) -> NSCollectionLayoutSection? in

    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.
  41. Compositional layout determines the shape of the layout that is

    mapped to the cell, and the data flows in along the shape. Having such an image makes it easier to understand. →
  42. Once this image is clear, you can easily code more

    complex layout, for another example, the layout above is used in Photos.app. →
  43. Just place items in nested groups like this. If you

    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.
  44. As you have seen so far, the compositional layout represents

    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?
  45. Oh, I forgot to mention a very powerful feature for

    modern apps. In the current state, all cells are arranged vertically. The identity of a cool modern app was a nested scroll, right?
  46. ... let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension: .fractionalHeight(0.4)) let group

    = 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.
  47. Orthogonal Scrolling Behavior ... let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.92), heightDimension:

    .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, →
  48. No longer to place collection views on collection view, and

    write complex code to manage them. Simply handle simple collection view and data source. That's amazing!
  49. So far, we have seen creating an AppStore app's UI

    using Compositional Layout. Is that true for Apple says the code become simple and surprisingly easy to create?
  50. https://github.com/kishikawakatsumi/AppStore-Clone-CollectionViewCompositionalLayouts Yes! That's absolutely true. I actually tried it. Here

    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.
  51. Only iOS 13? I'm sure you all think. It doesn't

    matter now because it can only be used on iOS 13. So it will be usable in two years. Two years later, we will be migrated SwiftUI.
  52. IBPCollectionViewCompositionalLayout Please try IBPCollectionViewCompositionalLayout. The repository shows various complex layouts

    as sample code. You can see how easy to write any layout. And it works on iOS 12, 11, and 10.
  53. Under development ✅ Spacing ✅ Nested Groups ✅ Supplemental Views

    (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.
  54. For example, a layout has such complex headers and footers

    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.
  55. Drop-in Replacement - (instancetype)initWithSection:(IBPNSCollectionLayoutSection *)section { if (@available(iOS 13, *))

    { 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.
  56. UICollectionViewLayout class UICollectionViewLayout { ... func prepare() func layoutAttributesForElements(in rect:

    CGRect) -> [UICollectionViewLayoutAttributes]? func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> U func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICo func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool ... var collectionViewContentSize: CGSize { get } ... Then I gradually implemented a subclass of UICollectionViewLayout.
  57. UICollectionViewLayout class UICollectionViewLayout { ... func prepare() func layoutAttributesForElements(in rect:

    CGRect) -> [UICollectionViewLayoutAttributes]? func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> U func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICo func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool ... var collectionViewContentSize: CGSize { get } ... In shouldInvalidateLayout method, it returns a value whether invalidate previous layout and re-calculate the layout again.
  58. UICollectionViewLayout class UICollectionViewLayout { ... func prepare() func layoutAttributesForElements(in rect:

    CGRect) -> [UICollectionViewLayoutAttributes]? func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> U func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICo func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool ... var collectionViewContentSize: CGSize { get } ... In prepare method, calculate all positions of the cells.
  59. UICollectionViewLayout class UICollectionViewLayout { ... func prepare() func layoutAttributesForElements(in rect:

    CGRect) -> [UICollectionViewLayoutAttributes]? func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> U func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICo func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool ... var collectionViewContentSize: CGSize { get } ... layoutAttributesForItem method returns the position for cells on demand.
  60. The first challenge was the nested groups. It was overcome

    by implementing the layout calculation method in a recursive structure.
  61. Another challenge was orthogonal scrolling. As a result, it was

    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.
  62. Wrap up • 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) 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)
  63. No Documentation There is still no documentation. We have to

    code with only on header comments, WWDC session videos and sample code. The sample code available from my repository is surely a great help.
  64. 2019-09-20 07:14:13.535020+0900 ... [29591:20271214] [CompositionalLayout] Attempting to add contentInsets to

    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.
  65. References • Advances in Collection View Layout
 https://developer.apple.com/videos/play/wwdc2019/215/ • All

    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 :)