[SWIFT] Are you still exhausted with CollectionView? CompositionalLayout starting from iOS12 ~ From implementation to design ~

Apparently Hello, this year can not be easily held a year-end party is sad TOSH. Today, I will be in charge of the 17th day of the ZOZO Technologies Advent Calendar!

Introduction

By the way, every iOS engineer uses CollectionView. However, as you all feel, the UI of apps is becoming more and more complicated than it is.

The following example

AbemaTV RakutenNBA UberEats AppleStore
IMG_9967.PNG IMG_9966.PNG IMG_9965.jpg IMG_9964.PNG

From an engineer's point of view, it is difficult to implement these by first putting the whole in TableView, putting the CollectionView in the Cell, and then Layout the whole so that it becomes Horizontal in it. And above all, it is difficult to nest the contents more and more. Apple is also aware of that, and at WWDC19, we have announced a new Compositional Layout. However, it is compatible with iOS 13 or later. For business, the option to turn off iOS 12 is quite difficult, and in the end, it will be implemented by brute force. Good news for all of you! Our technical advisor, Mr. Kishikawa, has created a library that allows you to use CompositionLayout on iOS12! https://github.com/kishikawakatsumi/IBPCollectionViewCompositionalLayout So, in this article, I would like to introduce what kind of design should be created to make it easier to use in actual operation ~

By the way, CookPad Introduction is the method to stop the banner used in the App Store in the center, but it is quite difficult. .. ..

Premise

--People who use Collection View on a regular basis --People looking for the best design --People who are new to Compositional Layout

Basic concept of Compositiona Layout

Please refer to here for details. https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts

Especially, I think that the image of putting UIcollectionViewCell on a custom item is good.

Implementation method

Now let's actually implement it.

Settings for each section

First, create a Protocol for the section.

Section.swift


protocol Section {
    //Number of items in each section
    var numberOfItems: Int { get }
    //What to do when an item in each section is tapped
    //If you set it in the closure, you can add processing from the VC side.
    var didSelectItem: ((Int) -> Void)? { get }
    //Here, we will actually build the layout
    func layoutSection() -> NSCollectionLayoutSection
    //Set the Cell to be used as an Item here.
    func configureCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell
}

Initial setting on the VC side

On the VC side, it is an image that uses the Section array created earlier.

ViewController.swift


import UIKit

final class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.delegate = self
        collectionView.dataSource = self
    }

    @IBOutlet weak var collectionView: UICollectionView!

    //Make the layout of the section set here
    private var collectionViewLayout: UICollectionViewLayout {
        let sections = self.sections
        let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) -> NSCollectionLayoutSection? in
            return sections[sectionIndex].layoutSection()
        }
        return layout
    }

    private var sections: [Section] = [] {
        didSet {
            //Update the layout when sections are updated
            collectionView.collectionViewLayout = collectionViewLayout
            collectionView.reloadData()
        }
    }
}

//dataSource settings
extension ViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return sections.count
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return sections[section].numberOfItems
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return sections[indexPath.section].configureCell(collectionView: collectionView, indexPath: indexPath)
    }
}

//delegate settings
extension ViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let didSelectItem = sections[indexPath.section].didSelectItem else {
            return
        }
        didSelectItem(indexPath.row)
    }
}

Actually create a section

This time, we will proceed on the premise that the cell for Item has been created in advance.

ItemsSection.swift


//Inherit Section
struct ItemsSection: Section {
    var didSelectItem: ((Int) -> Void)?

    private var items: [Items] = []
    var numberOfItems: Int {
        self.items.count
    }

    func layoutSection() -> NSCollectionLayoutSection {

        //Layout settings for Item
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        //Layout settings for group
        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(UIScreen.main.bounds.width - 40), heightDimension: .absolute(184))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        //Layout settings for Section
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 10
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)
        //Set whether to stop scrolling or not here
        section.orthogonalScrollingBehavior = .groupPaging

        return section
    }

    func configureCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ItemCollectionViewCell.self), for: indexPath) as! ItemCollectionViewCell
       //Set the Cell here

        return cell
    }
}

extension BannerSection {
    //If you need an Initializer, it's a good idea to cut it out into an Extension.
    init() {
    }
}

It's a good idea to create one section struct for each section.

Actually add to the VC side

Now let's add the Section we created earlier.

ViewController.swift


func viewDidLoad() {
    ~~abridgement~~

    collectionView.register(ItemCollectionViewCell.self,
                            forCellWithReuseIdentifier: String(describing: ItemCollectionViewCell.self))

    var itemsSection = ItemsSection()
    itemsSection.didSelectItem = { index in
        //Here, the process at the time of cell selection is performed.
    }
    sections.append(itemsSection)
}

You have now created a CollectionView that sucrose sideways and stops in the center.

If you use these well, you can also create a layout like the image below! (Sorry, I don't have time and it will be the image in the figure Sweat) The numbers in [] are [Section number, row number]. The important merit of this is that you can manage everything on one CollectionView without nesting like CollectionView in TableView! Even with complex layouts, you can design in a manageable way!

bonus

I want to add Header and Footer to each section! I think there are many people who say that. There are two main ways to create a Header and Footer.

  1. Set Header and Footer for Section
  2. Add Sections that will be Header and Footer above and below the Section.

I think these methods have advantages and disadvantages, but if you want to add complex touch events to Header and Footer, method 2 is good, especially if you do not use touch events or tap to make an accordion. If you only want to process, I think method 1 is easier.

How to implement method 1

Add another method to the previous Section.

Section.swift


Protocol Section {
    ~~abridgement~~

    //UICollectionReusableView if you don't use Header or Footer()return it
    func configureHeaderFooter(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
}

Next, let's add the newly added method to ItemsSection!

ItemsSection.swift


struct ItemsSection: Section {
    func layoutSection() -> NSCollectionLayoutSection {
           ~~abridgement~~
            // header
            let headerSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .estimated(95))
            let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerSize,
                elementKind: "section-header-element-kind",
                alignment: .top)

            // footer
            let footerSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .estimated(45))
            let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: footerSize,
                elementKind: "section-footer-element-kind",
                alignment: .bottom)

            // header,Add footer
            section.boundarySupplementaryItems = [sectionHeader, sectionFooter]
          ~~abridgement~~
    }

    func configureHeaderFooter(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        //The character string here is fixed
        switch kind {
        // header
        case "section-header-element-kind":
            let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: ItemHeaderCell.self), for: indexPath) as! ItemHeaderCell
            //Header setup
            return header
        // footer
        case "section-footer-element-kind":
            let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: ItemFooterCell.self), for: indexPath) as! ItemFooterCell
        //Footer setup
        return footer
        default:
            //UICollectionReusableView if Header and Footer are not set()return it
            return UICollectionReusableView()
        }
    }
}

Finally, the VC side

ViewController.swift


final class ViewController: UIViewController {
    override func viewDidLoad() {
    ~~abridgement~~
        collectionView.register(itemHeaderCell.self, forSupplementaryViewOfKind: "section-header-element-kind", withReuseIdentifier: String(describing: itemHeaderCell.self))
        collectionView.register(itemFooterCell.self, forSupplementaryViewOfKind: "section-footer-element-kind", withReuseIdentifier: String(describing: itemFooterCell.self))
    ~~abridgement~~
    }
}

extension ViewController: UIViewControllerDataSource {
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let view = sections[indexPath.section].configureHeaderFooter(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath)
        //If you want to add a tap gesture to the view, do it here
        return view
    }
}

Summary

With this kind of feeling, if you implement it separately for ViewController and Section, it will be a pretty good design, isn't it? If you use the above-mentioned library, you can use CompositionalLayout from iOS12, so please start using it little by little ~ By the way, you can easily migrate even after the end of support for iOS 12.

This time, I used a normal DataSource, but at the same time as the CompositionLayout, a DiffrableDataSource has also appeared, so if you combine it with this, you can make another design? I will write about DiffrableDataSource again in the next article! Well then!

Recommended Posts

Are you still exhausted with CollectionView? CompositionalLayout starting from iOS12 ~ From implementation to design ~
Starting with iOS 14, text fields are automatically adjusted to be hidden on the keyboard.
CarPlay starting with iOS14
You are required to use winpty with docker exec [Windows]
Calculate age from birthday with 4 lines of Swift ~ How old are you now? To you who became ~