WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Some help with Compositional Layout and Diffable DataSource [iOS 13+/UIKit]

Forums > iOS

[iOS 13+/UIKit]

in my current project I have a UICollectionView with a diffable dataSource. I have it up and running with two sections, looking like this:

//   +--------------------------------------+
//   | +---------------------------------+  |
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  | SECTION 1 (featured)
//   | |                                 |  | 1 item, non scrollable
//   | |                                 |  | custom cell class
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  |
//   | |                                 |  |
//   | +---------------------------------+  |
//   +--------------------------------------+
//   |                                      |
//   |           +-------------+            |
//   |           |             |            |
//   |           |             |            |
//   |           |             |            |
//   |           |             |            |
//   |           +-------------+            | SECTION 2 (normal)
//   |                                      | 2 items, horizontal
//   |           +-------------+            | scrolling, 2 at a time
//   |           |             |            | custom cell class
//   |           |             |            |
//   |           |             |            |
//   |           |             |            |
//   |           +-------------+            |
//   +--------------------------------------+

I get this behaviour by sorting the items (Projects) on lastEdited (Date) and then appending the first item to the featured section and the rest to the normal section. However, whenever the number of items changes (adding, deleting, searching), the first item may no longer be the same and I have to redo the appointing of sections. This is where I get stuck, how and where should I best do that. Alternatively, all items may be in 1 section, but I still want render two sections, where the first section only has the first item, in that case how and where should I apply that? Got the feeling I am close, but oversee something very simpel...

Below is the relevant code:

 //
//  ProjectsViewController.swift
//

import UIKit

class ProjectsViewController: UIViewController {
    //MARK: - Types
    enum Section: CaseIterable {
        case featured, normal
    }

    //MARK: - Properties
    let projecstController = ProjectsController()
    typealias Project = ProjectsController.Project

    let searchBar = UISearchBar(frame: .zero)
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Project>?

    var isSearching: Bool = false

    //MARK: - ViewController Methods
    override func viewDidLoad() {
        super.viewDidLoad()
(...)
    }

    //MARK: - DataSource
    func createDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Project>(collectionView: collectionView) { (collectionView, indexPath, project) in
            if indexPath.section == 0 && !self.isSearching {
                return self.configure(FeaturedProjectCell.self, with: project, for: indexPath)
            } else {
                return self.configure(NormalProjectCell.self, with: project, for: indexPath)
            }
        }

        dataSource?.supplementaryViewProvider = { (collectionView, kind, indexPath) in
            guard let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeader.reuseidentifier, for: indexPath) as? SectionHeader else {
                fatalError("Could not dequeue sectionHeader: \(SectionHeader.reuseidentifier)")
            }

            sectionHeader.title.text = "Tap and hold to edit an existing Project!"
            sectionHeader.subtitle.text = "tap to view Project notes"
            return sectionHeader
        }
    }

    func updateData(on projects: [Project]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Project>()
        snapshot.appendSections([Section.featured, Section.normal])

        let projectsSorted = projects.sorted {$0.lastEdited > $1.lastEdited}
        for (index, project) in projectsSorted.enumerated() {
            if index == 0 {
                snapshot.appendItems([project], toSection: .featured)
            } else {
                snapshot.appendItems([project], toSection: .normal)
            }
        }
        //apply() is safe to call from a background queue!
        self.dataSource?.apply(snapshot, animatingDifferences: true)
    }

    func performQuery(filter: String?) {
        let projects = projecstController.filteredProjects(with: filter).sorted { $0.title < $1.title }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Project>()
        snapshot.appendSections([.normal])
        snapshot.appendItems(projects)
        dataSource?.apply(snapshot, animatingDifferences: true)
    }

    ///Configure any type of cell that conforms to selfConfiguringProjectCell!
    func configure<T: SelfConfiguringProjectCell>(_ cellType: T.Type, with project: Project, for indexPath: IndexPath) -> T {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
            fatalError("Unable to dequeue \(cellType)")
        }

        cell.configure(with: project)
        return cell
    }

    //MARK: - Actions
    @objc func addButtonTapped() {
        print("tapped")
    }

    //MARK: - UI & Layout
    private func configureViewController() {
     (...)
     }

    private func configureCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
        collectionView.delegate = self
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)

        collectionView.register(FeaturedProjectCell.self, forCellWithReuseIdentifier: FeaturedProjectCell.reuseIdentifier)
        collectionView.register(NormalProjectCell.self, forCellWithReuseIdentifier: NormalProjectCell.reuseIdentifier)
        collectionView.register(SectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SectionHeader.reuseidentifier)
    }

(...) 

    //MARK: - CompositionalLayout
    func createCompositionalLayout() -> UICollectionViewLayout {
        //this closure will be called whenever the layoutEnvironment changes! For example:
        //switching bewteen portrait and landscape, or
        let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) in
            if self.isSearching {
                return self.createSearchSection()
            }

        if sectionIndex == 0 && !self.isSearching {
                return self.createFeaturedSection()
            } else {
                if self.traitCollection.horizontalSizeClass == .compact {
                    // load slim view
                    print("compact")
                    return self.createNormalSection()
                } else {
                    // load wide view
                    print("wide")
                    return self.createNormalSectionIpad()
                }
            }
        }

        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.interSectionSpacing = 20
        layout.configuration = config

        return layout
    }

    func createFeaturedSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.93)) // full width & height of its parent (a group)
        let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
        layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20)

        let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(350))
        let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
        //let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitem: layoutItem, count: 2)

        let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
        layoutSection.orthogonalScrollingBehavior = .groupPaging
        let layoutSectionHeader = createSectionHeader()
        layoutSection.boundarySupplementaryItems = [layoutSectionHeader]

        return layoutSection
    }

    func createNormalSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))
        let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
        layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 10, trailing: 20)

        let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.40))
        let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])

        let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
        layoutSection.orthogonalScrollingBehavior = .groupPagingCentered

        return layoutSection
    }

    func createNormalSectionIpad() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(0.93)) // full width & height of its parent (a group)
        let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
        layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 5)

        let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(270))
        let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])

        let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
        layoutSection.orthogonalScrollingBehavior = .groupPagingCentered

        return layoutSection
    }

        func createSearchSection() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1))
        let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
        layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 5)

        let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))
        let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])

        let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
        layoutSection.orthogonalScrollingBehavior = .none

        return layoutSection
    }

    func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
        let sectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(80))
        return NSCollectionLayoutBoundarySupplementaryItem(layoutSize: sectionHeaderSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
    }

}

//MARK: - Ext CollectionView Delegate
extension ProjectsViewController: UICollectionViewDelegate  {

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let project = dataSource?.itemIdentifier(for: indexPath) else { return }
        print(project)
    }
}

//MARK: - Ext SearchBar
extension ProjectsViewController: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        if searchText.isEmpty {
            print("NO LONGER SEARCHING")
            isSearching = false
            searchBar.resignFirstResponder()
            return
        }
        print("SET SEARCHING TO TRUE")
        isSearching = true
        performQuery(filter: searchText)
    }

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        print("SET SEARCHING TO FALSE")
        isSearching = false
        searchBar.resignFirstResponder()
    }
//
//  ProjectsController.swift
//

import UIKit

class ProjectsController {
    //MARK: - Types
    struct Project: Codable, Hashable {
        let identifier: UUID
        let createDate: Date
        var lastEdited: Date
        var title: String
        var subTitle: String
        var iconImageName: String?

        init(title: String, subTitle: String = "") {
            self.title = title
            self.subTitle = subTitle
            self.identifier = UUID()
            self.createDate = Date()
            self.lastEdited = Date()
            self.iconImageName = nil
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(identifier)
        }

        static func == (lhs: Project, rhs: Project) -> Bool {
            return lhs.identifier == rhs.identifier
        }

        func contains(_ filter: String?) -> Bool {
            guard let filterText = filter else { return true }
            if filterText.isEmpty { return true }
            let lowercasedFilter = filterText.lowercased()
            return title.lowercased().contains(lowercasedFilter) || subTitle.lowercased().contains(lowercasedFilter)
        }
    }

    func filteredProjects(with filter: String? = nil, limit: Int? = nil) -> [Project] {
        let filtered = projects.filter { $0.contains(filter) }
        if let limit = limit {
            return Array(filtered.prefix(through: limit))
        } else {
            return filtered
        }
    }

    private lazy var projects: [Project] = {
        return generateInitialProjects()
    }()

    private func generateInitialProjects() -> [Project] {
            #warning("debug/development only")
            let initialProjects = [
                Project(title: "Dealing with Problems", subTitle: "Of any kind"),
                Project(title: "Just a household project"),
                Project(title: "Making notes on painting", subTitle: "Anything goes"),
                Project(title: "Tidy up cabling desk", subTitle: "Neat is good"),
                Project(title: "Study Swift"),
                Project(title: "Be a good boy", subTitle: "Good is the new you"),
                Project(title: "Going to the Run", subTitle: "Just like the Earrings"),
                Project(title: "Goo is nasty"),
                Project(title: "Gone with the wind", subTitle: "Not really")
            ]

            return initialProjects
        }

}

Note: I first want to get it working properly with the pre loaded test Projects and searching. After that moving forward with adding and deleting ...

3      

Anyone ?

3      

@emin  

Hey man! I just started digging into the diffable table and collectionViews myself. My example is not working yet, having somme issues connecting everything, but thank you for posting this code as i think it will help me understand this better.

In the meantime have you checked out this post by DonnyWals? From what i read there, i think he had similar issues near the end and posted a solution which MIGHT help. I see you already use UUIDs so it should be fine, but hope it sheds some light on your problem :)

I will look over this again after i get this down :)

3      

@Roblack. Thanks! Will check the article out! I have things working atm, allthough I am not sure it is the best way. For now I've moved on, but I will definitely revise that bit later on!

3      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.