[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 ...