Created
October 9, 2021 06:44
-
-
Save takiguri/4b13ebb6c3a9d2f76d86380afb6a0875 to your computer and use it in GitHub Desktop.
UICollectionViewCompositionalLayout example: BookingsController
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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