Skip to content

Instantly share code, notes, and snippets.

@takiguri
Created October 9, 2021 06:44
Show Gist options
  • Save takiguri/4b13ebb6c3a9d2f76d86380afb6a0875 to your computer and use it in GitHub Desktop.
Save takiguri/4b13ebb6c3a9d2f76d86380afb6a0875 to your computer and use it in GitHub Desktop.
UICollectionViewCompositionalLayout example: BookingsController
import Foundation
import UIKit
import Rswift
import RxCocoa
import RxSwift
import StatefulCollectionView
class BookingsController: ViewController {
var viewModel: BookingsViewModelProtocol!
@IBOutlet private(set) var statefulCollection: StatefulCollectionView!
}
// MARK: - Lifecycle
extension BookingsController {
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: false)
}
}
// MARK: - Setup
private extension BookingsController {
func setup() {
setupCollectionView()
}
func setupCollectionView() {
setupCellsAndSupplementaryViewsForReuse()
statefulCollection.statefulDelegate = self
statefulCollection.dataSource = self
statefulCollection.delegate = self
statefulCollection.canPullToRefresh = true
setupCollectionViewLayout()
statefulCollection.triggerInitialLoad()
}
func setupCellsAndSupplementaryViewsForReuse() {
statefulCollection.innerCollection.register(R.nib.headingLabelCell)
statefulCollection.innerCollection.register(R.nib.yourBookingCell)
statefulCollection.innerCollection.register(R.nib.bookingTypeCell)
statefulCollection.innerCollection.register(R.nib.recommendedBookableCell)
statefulCollection.innerCollection.register(R.nib.bookableCell)
statefulCollection.innerCollection.register(
R.nib.yourBookingsHeaderView,
forSupplementaryViewOfKind: SupplementaryElementKind.sectionHeader.rawValue
)
}
func setupCollectionViewLayout() {
let layout = createLayout()
statefulCollection.innerCollection.setCollectionViewLayout(layout, animated: false)
}
}
// MARK: - Bindings
private extension BookingsController {
func bind() {
}
}
// MARK: - Router
private extension BookingsController {
// func presentSomeController() {
// let vc = R.storyboard.someController.SomeController()!
// vc.viewModel = SomeViewModel()
// navigationController?.pushViewController(vc, animated: true)
// }
}
// MARK: - Actions
private extension BookingsController {
// @IBAction
// func someButtonTapped(_ sender: Any) {
// viewModel.someFunction2(
// param1: 0,
// param2: "",
// onSuccess: handleSomeSuccess(),
// onError: handleError()
// )
// }
}
// MARK: - Event Handlers
private extension BookingsController {
// func handleSomeSuccess() -> VoidResult {
// return { [weak self] in
// guard let s = self else { return }
// // TODO: Do something here
// }
// }
}
// MARK: - Helpers
private extension BookingsController {
func createLayout() -> UICollectionViewCompositionalLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 16
return UICollectionViewCompositionalLayout(
sectionProvider: sectionProvider(),
configuration: config
)
}
func sectionProvider() -> (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? {
return { sectionIndex, layoutEnvironment -> NSCollectionLayoutSection? in
guard let sectionKind = SectionKind(rawValue: sectionIndex) else {
fatalError("Section index \(sectionIndex) is unsupported")
}
let item = NSCollectionLayoutItem(layoutSize: sectionKind.itemSize)
item.edgeSpacing = sectionKind.itemEdgeSpacing
let group = sectionKind.group(layoutSize: sectionKind.groupSize, subitem: item)
group.contentInsets = sectionKind.groupContentInsets
group.interItemSpacing = sectionKind.groupInterItemSpacing
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = sectionKind.orthogonalScrollingBehavior()
if let sectionHeaderSize = sectionKind.sectionHeaderSize {
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: sectionHeaderSize,
elementKind: SupplementaryElementKind.sectionHeader.rawValue,
alignment: .top
)
section.boundarySupplementaryItems = [sectionHeader]
}
section.contentInsets = sectionKind.sectionContentInsets
section.interGroupSpacing = sectionKind.sectionInterGroupSpacing
return section
}
}
}
// MARK: - StatefulCollectionDelegate
extension BookingsController: StatefulCollectionViewDelegate {
func statefulCollection(
_ collectionView: StatefulCollectionView,
didCompleteInitialLoad completion: @escaping LoadCompletion
) {
statefulCollection(collectionView, didCompletePullToRefresh: completion)
}
func statefulCollection(
_ collectionView: StatefulCollectionView,
didCompletePullToRefresh completion: @escaping LoadCompletion
) {
completion(false, nil)
}
}
// MARK: - UICollectionViewDataSource
extension BookingsController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return SectionKind.allCases.count
}
func collectionView(
_ collectionView: UICollectionView, numberOfItemsInSection section: Int
) -> Int {
guard let sectionKind = SectionKind(rawValue: section) else { return .zero }
switch sectionKind {
case .headingLargeTitle:
return 1
case .yourBookings:
return 1
case .bookingTypes:
return 3
case .recommended:
return 2
case .bookablesList:
return 12
}
}
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
guard let sectionKind = SectionKind(rawValue: indexPath.section) else {
return UICollectionViewCell()
}
switch sectionKind {
case .headingLargeTitle:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: R.reuseIdentifier.headingLabelCell,
for: indexPath
)!
return cell
case .yourBookings:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: R.reuseIdentifier.yourBookingCell,
for: indexPath
)!
return cell
case .bookingTypes:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: R.reuseIdentifier.bookingTypeCell,
for: indexPath
)!
return cell
case .recommended:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: R.reuseIdentifier.recommendedBookableCell,
for: indexPath
)!
return cell
case .bookablesList:
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: R.reuseIdentifier.bookableCell,
for: indexPath
)!
return cell
}
}
func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath
) -> UICollectionReusableView {
guard let sectionKind = SectionKind(rawValue: indexPath.section) else {
return UICollectionReusableView()
}
switch sectionKind {
case .yourBookings:
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: SupplementaryElementKind.sectionHeader.rawValue,
withReuseIdentifier: R.reuseIdentifier.yourBookingsHeaderView,
for: indexPath
)!
return header
default:
return UICollectionReusableView()
}
}
}
// MARK: - UICollectionViewDelegate {
extension BookingsController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: false)
}
}
// MARK: - BookingsController.SectionKind
extension BookingsController {
/// Apple UIKit should have created this within the UIKit framework...
enum SupplementaryElementKind: String, CaseIterable {
case badge = "badge-element-kind"
case background = "background-element-kind"
case sectionHeader = "section-header-element-kind"
case sectionFooter = "section-footer-element-kind"
case layoutHeader = "layout-header-element-kind"
case layoutFooter = "layout-footer-element-kind"
}
enum SectionKind: Int, CaseIterable {
case headingLargeTitle = 0, yourBookings, bookingTypes, recommended, bookablesList
/// Values are acquired from design spec
var itemSize: NSCollectionLayoutSize {
switch self {
case .headingLargeTitle:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)
)
case .yourBookings:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(104)
)
case .bookingTypes:
return NSCollectionLayoutSize(
widthDimension: .estimated(50),
heightDimension: .absolute(32)
)
case .recommended:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(152)
)
case .bookablesList:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .absolute(248)
)
}
}
var itemEdgeSpacing: NSCollectionLayoutEdgeSpacing? {
switch self {
case .bookingTypes:
return NSCollectionLayoutEdgeSpacing(
leading: .fixed(16),
top: .fixed(.zero),
trailing: .fixed(16),
bottom: .fixed(.zero)
)
default:
return nil
}
}
var groupSize: NSCollectionLayoutSize {
switch self {
case .headingLargeTitle:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)
)
case .yourBookings:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(104)
)
case .bookingTypes:
return NSCollectionLayoutSize(
widthDimension: .estimated(64),
heightDimension: .absolute(32)
)
case .recommended:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.80),
heightDimension: .absolute(152)
)
case .bookablesList:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(248)
)
}
}
var groupContentInsets: NSDirectionalEdgeInsets {
switch self {
case .headingLargeTitle, .yourBookings, .bookablesList:
return NSDirectionalEdgeInsets(
top: .zero,
leading: 16,
bottom: .zero,
trailing: 16
)
case .bookingTypes, .recommended:
return NSDirectionalEdgeInsets(
top: .zero,
leading: .zero,
bottom: .zero,
trailing: .zero
)
}
}
var groupInterItemSpacing: NSCollectionLayoutSpacing? {
switch self {
case .bookablesList:
return .fixed(8)
default:
return nil
}
}
func group(
layoutSize: NSCollectionLayoutSize,
subitem: NSCollectionLayoutItem
) -> NSCollectionLayoutGroup {
switch self {
case .headingLargeTitle:
return NSCollectionLayoutGroup.horizontal(
layoutSize: layoutSize,
subitems: [subitem]
)
case .yourBookings:
return NSCollectionLayoutGroup.horizontal(
layoutSize: layoutSize,
subitems: [subitem]
)
case .bookingTypes:
return NSCollectionLayoutGroup.vertical(
layoutSize: layoutSize,
subitems: [subitem]
)
case .recommended:
return NSCollectionLayoutGroup.vertical(
layoutSize: layoutSize,
subitems: [subitem]
)
case .bookablesList:
return NSCollectionLayoutGroup.horizontal(
layoutSize: layoutSize,
subitem: subitem,
count: 2
)
}
}
var sectionHeaderSize: NSCollectionLayoutSize? {
switch self {
case .yourBookings:
return NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44)
)
case .headingLargeTitle, .bookingTypes, .recommended, .bookablesList:
return nil
}
}
func orthogonalScrollingBehavior() -> UICollectionLayoutSectionOrthogonalScrollingBehavior {
switch self {
case .headingLargeTitle, .yourBookings, .bookablesList:
return .none
case .bookingTypes:
return .continuous
case .recommended:
return .groupPaging
}
}
var sectionContentInsets: NSDirectionalEdgeInsets {
switch self {
case .headingLargeTitle:
return NSDirectionalEdgeInsets(
top: 32,
leading: .zero,
bottom: .zero,
trailing: .zero
)
case .recommended:
return NSDirectionalEdgeInsets(
top: .zero,
leading: 16,
bottom: .zero,
trailing: 16
)
case .bookablesList:
return NSDirectionalEdgeInsets(
top: .zero,
leading: .zero,
bottom: 16,
trailing: .zero
)
default:
return .zero
}
}
var sectionInterGroupSpacing: CGFloat {
switch self {
case .recommended:
return 16
default:
return .zero
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment