├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Sources └── CompositionalLayoutableSection │ ├── CompositionalLayoutDataSource.swift │ ├── CompositionalLayoutDelegate.swift │ ├── CompositionalLayoutProvider.swift │ ├── CompositionalLayoutableDataSourcePrefetching.swift │ ├── CompositionalLayoutableSection.swift │ ├── UICollectioView+register.swift │ └── UICollectioView+supplementaryView.swift ├── Tests └── CompositionalLayoutableSectionTests │ └── CompositionalLayoutableSectionTests.swift └── assets ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── video.gif └── video.mov /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CompositionalLayoutableSection", 8 | platforms: [.iOS(.v15)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "CompositionalLayoutableSection", 13 | targets: ["CompositionalLayoutableSection"]) 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "CompositionalLayoutableSection", 24 | dependencies: []), 25 | .testTarget( 26 | name: "CompositionalLayoutableSectionTests", 27 | dependencies: ["CompositionalLayoutableSection"]) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CompositionalLayoutableSection 2 | 3 | 4 | https://github.com/ahmed-yamany/CompositionalLayoutableSection/assets/58072774/b2be01ab-008b-4fc1-89a2-b9cdc4a1847e 5 | 6 | CompositionalLayoutableSection is a Swift library that simplifies the implementation of compositional layouts for UICollectionViews on iOS 13.0 and later. This library introduces a set of protocols and a base class to help you create organized and abstracted sections for your collection view. 7 | 8 | It allows for better code organization and reusability, making it easier to maintain and switch between different sections within the same collection view. 9 | 10 | 11 | ## Table of Contents 12 | - Introduction 13 | - Protocols 14 | - CompositionalLayoutableSection 15 | - Usage 16 | 17 | ## Introduction 18 | UICollectionViews with compositional layouts offer a flexible and powerful way to create complex UI interfaces. CompositionalLayoutableSection provides a convenient framework to work with compositional layouts in a more structured manner. 19 | 20 | ## Protocols 21 | CompositionalLayoutableSection introduces three protocols: 22 | ### CompositionalLayoutableSectionDataSource 23 | 24 | ```swift 25 | public protocol CompositionalLayoutableSectionDataSource: UICollectionViewDataSource { 26 | associatedtype ItemsType 27 | var items: [ItemsType] { get set } 28 | 29 | func update(_ collectionView: UICollectionView, withItems items: [ItemsType]) 30 | } 31 | ``` 32 | This protocol defines the data source for a section in the compositional layout. It requires you to specify the data type for the section's items and allows you to update the items and trigger a collection view data reload. 33 | 34 | ### CompositionalLayoutableSectionLayout 35 | ```swift 36 | @available(iOS 13.0, *) 37 | @objc public protocol CompositionalLayoutableSectionLayout { 38 | func sectionLayout(at index: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 39 | } 40 | ``` 41 | This protocol defines the layout for a section. It enables you to return a NSCollectionLayoutSection based on the index and layout environment. 42 | 43 | ### CompositionalLayoutableSectionDelegate 44 | ```swift 45 | @available(iOS 13.0, *) 46 | @objc public protocol CompositionalLayoutableSectionDelegate: UICollectionViewDelegate { 47 | @objc func registerCell(_ collectionView: UICollectionView) 48 | @objc optional func registerSupplementaryView(_ collectionView: UICollectionView) 49 | @objc optional func registerDecorationView(_ layout: UICollectionViewCompositionalLayout) 50 | } 51 | ``` 52 | ### CompositionalLayoutProvider 53 | ```swift 54 | @available(iOS 13.0, *) 55 | public protocol CompositionalLayoutProvider { 56 | var compositionalLayoutSections: [CompositionalLayoutableSection] { get set } 57 | } 58 | ``` 59 | The `CompositionalLayoutProvider` protocol defines the requirements for providing Sections data for the `UICollectionView` 60 | 61 | 62 | ## CompositionalLayoutableSection 63 | CompositionalLayoutableSection is an open class that acts as a container for a section's data source, layout, and delegate. You can create objects that inherit from this class to define the properties for each section in your collection view. 64 | 65 | ## Usage 66 | Here's an example of how you can use CompositionalLayoutableSection to manage sections in your UICollectionView: 67 | 68 | ```swift 69 | import UIKit 70 | import CompositionalLayoutableSection 71 | 72 | // A custom section for displaying Products in a collection view. 73 | class ProductsCollectionViewSection: CompositionalLayoutableSection { 74 | typealias ItemsType = Product 75 | typealias CellType = ProductCollectionViewCell 76 | // 77 | var items: [ItemsType] = [] 78 | override init() { 79 | super.init() 80 | delegate = self 81 | dataSource = self 82 | layout = self 83 | } 84 | } 85 | // MARK: - Products CollectionView Section Data Source 86 | // 87 | extension ProductsCollectionViewSection: CompositionalLayoutableSectionDataSource { 88 | /// 89 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 90 | return items.count 91 | } 92 | /// cell For Item At 93 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 94 | let cell = collectionView.dequeueReusableCell(CellType.self, for: indexPath) 95 | let product = items[indexPath.item] 96 | cell.configure(with: product) 97 | return cell 98 | } 99 | } 100 | // MARK: - Products CollectionView Section Layout 101 | extension ProductsCollectionViewSection: CompositionalLayoutableSectionLayout { 102 | var spacing: CGFloat { 20 } // The spacing between items in the section. 103 | var width: CGFloat { 156 } // The width of each item in the section. 104 | var height: CGFloat { 242 } // The height of each item in the section. 105 | /// - Returns: The layout for an item within the group. 106 | var itemLayoutInGroup: NSCollectionLayoutItem { 107 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) 108 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 109 | return item 110 | } 111 | /// - Returns: The layout for a group within the section. 112 | var groupLayoutInSection: NSCollectionLayoutGroup { 113 | let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(width), heightDimension: .absolute(height)) 114 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [itemLayoutInGroup]) 115 | return group 116 | } 117 | /// Defines the layout for the entire section, including groups and supplementary views. 118 | func sectionLayout(at index: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { 119 | let section = NSCollectionLayoutSection(group: groupLayoutInSection) 120 | // 121 | section.orthogonalScrollingBehavior = .continuous 122 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: spacing, bottom: spacing, trailing: spacing) 123 | section.interGroupSpacing = spacing 124 | // 125 | return section 126 | } 127 | } 128 | // MARK: - Products CollectionView Section Delegate 129 | extension ProductsCollectionViewSection: CompositionalLayoutableSectionDelegate { 130 | /// Registers the cell type with the given collection view. 131 | func registerCell(_ collectionView: UICollectionView) { 132 | collectionView.registerFromNib(CellType.self) 133 | } 134 | } 135 | ``` 136 | 137 | ```swift 138 | class MyViewController: UICollectionViewController, CompositionalLayoutProvider { 139 | var compositionalLayoutSections: [CompositionalLayoutableSection] = [] 140 | // 141 | private(set) lazy var delegate = CompositionalLayoutDelegate(provider: self) 142 | private(set) lazy var datasource = CompositionalLayoutDataSource(provider: self) 143 | // 144 | override func viewDidLoad() { 145 | super.viewDidLoad() 146 | collectionView.delegate = delegate 147 | collectionView.dataSource = datasource 148 | // Create and configure a section 149 | let section = ProductsCollectionViewSection() 150 | let items = [Product(), Product(), Product()] 151 | section.update(collectionView, withItems: items) 152 | // Add the section to the compositionalLayoutSections 153 | compositionalLayoutSections.append(section) 154 | // 155 | // this must be called after adding all sections to compositionalLayoutSections 156 | collectionView.updateCollectionViewCompositionalLayout(for: self) 157 | } 158 | } 159 | ``` 160 | 161 | ## Requirements 162 | iOS 13.0 or later 163 | Swift 5.0 or later 164 | ## Author 165 | [Ahmed Yamany](https://www.linkedin.com/in/ahmed-yamany/) 166 | 167 | ## Other Resources 168 | ### Xcode file Template for CompositionalLayoutableSection 169 | [https://github.com/ahmed-yamany/Xcode-File-Templates](https://github.com/ahmed-yamany/Xcode-File-Templates) 170 | 171 | ### Mega Mall 172 | personal project that video recorded from 173 | [https://github.com/ahmed-yamany/Mega-Mall](https://github.com/ahmed-yamany/Mega-Mall) 174 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/CompositionalLayoutDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutDataSource.swift 3 | // iCinema 4 | // 5 | // Created by Ahmed Yamany on 07/04/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol CompositionalLayoutDataSourceConfiguration: AnyObject { 11 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView 12 | } 13 | 14 | open class CompositionalLayoutDataSource: NSObject, UICollectionViewDataSource { 15 | 16 | public weak var provider: (any CompositionalLayoutProvider)? 17 | public weak var configuration: (any CompositionalLayoutDataSourceConfiguration)? 18 | 19 | public init(provider: any CompositionalLayoutProvider, configuration: (any CompositionalLayoutDataSourceConfiguration)? = nil) { 20 | self.provider = provider 21 | self.configuration = configuration 22 | } 23 | 24 | func dataSource(at indexPath: IndexPath) -> (any UICompositionalLayoutableSectionDataSource)? { 25 | provider?.dataSource(at: indexPath) 26 | } 27 | 28 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 29 | return self.provider?.compositionalLayoutSections.count ?? 0 30 | } 31 | 32 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 33 | dataSource(at: .init(item: 0, section: section))?.collectionView(collectionView, numberOfItemsInSection: section) ?? 0 34 | } 35 | 36 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 37 | dataSource(at: indexPath)?.collectionView(collectionView, cellForItemAt: indexPath) ?? UICollectionViewCell() 38 | } 39 | 40 | public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 41 | if indexPath.item == 9223372036854775807, let configuration = configuration { 42 | return configuration.collectionView(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) 43 | } else { 44 | return dataSource(at: indexPath)?.collectionView?(collectionView,viewForSupplementaryElementOfKind: kind,at: indexPath) ?? UICollectionReusableView() 45 | } 46 | } 47 | 48 | public func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool { 49 | dataSource(at: indexPath)?.collectionView?(collectionView, canMoveItemAt: indexPath) ?? false 50 | } 51 | 52 | public func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 53 | dataSource(at: sourceIndexPath)?.collectionView?(collectionView, moveItemAt: sourceIndexPath, to: destinationIndexPath) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/CompositionalLayoutDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutDelegate.swift 3 | // iCinema 4 | // 5 | // Created by Ahmed Yamany on 07/04/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @available(iOS 13.0, *) 11 | open class CompositionalLayoutDelegate: NSObject, UICollectionViewDelegate { 12 | public weak var provider: (any CompositionalLayoutProvider)? 13 | 14 | public init(provider: any CompositionalLayoutProvider) { 15 | self.provider = provider 16 | } 17 | 18 | private func delegate(at indexPath: IndexPath) -> UICompositionalLayoutableSectionDelegate? { 19 | provider?.delegate(at: indexPath) 20 | } 21 | 22 | private func delegates() -> [UICompositionalLayoutableSectionDelegate?] { 23 | provider?.compositionalLayoutSections.map { $0.delegate } ?? [] 24 | } 25 | 26 | public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { 27 | delegate(at: indexPath)?.collectionView?(collectionView, shouldHighlightItemAt: indexPath) ?? true 28 | } 29 | 30 | public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) { 31 | delegate(at: indexPath)?.collectionView?(collectionView, didHighlightItemAt: indexPath) 32 | } 33 | 34 | public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) { 35 | delegate(at: indexPath)?.collectionView?(collectionView, didUnhighlightItemAt: indexPath) 36 | } 37 | 38 | public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 39 | delegate(at: indexPath)?.collectionView?(collectionView, shouldSelectItemAt: indexPath) ?? true 40 | } 41 | 42 | public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { 43 | delegate(at: indexPath)?.collectionView?(collectionView, shouldDeselectItemAt: indexPath) ?? true 44 | } 45 | 46 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 47 | delegate(at: indexPath)?.collectionView?(collectionView, didSelectItemAt: indexPath) 48 | } 49 | 50 | public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 51 | delegate(at: indexPath)?.collectionView?(collectionView, didDeselectItemAt: indexPath) 52 | } 53 | @available(iOS 16.0, *) 54 | public func collectionView(_ collectionView: UICollectionView, canPerformPrimaryActionForItemAt indexPath: IndexPath) -> Bool { 55 | delegate(at: indexPath)?.collectionView?(collectionView, canPerformPrimaryActionForItemAt: indexPath) ?? true 56 | } 57 | 58 | @available(iOS 16.0, *) 59 | public func collectionView(_ collectionView: UICollectionView, performPrimaryActionForItemAt indexPath: IndexPath) { 60 | delegate(at: indexPath)?.collectionView?(collectionView, performPrimaryActionForItemAt: indexPath) 61 | } 62 | 63 | public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 64 | delegate(at: indexPath)?.collectionView?(collectionView, willDisplay: cell, forItemAt: indexPath) 65 | } 66 | 67 | public func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) { 68 | delegate(at: indexPath)?.collectionView?(collectionView, willDisplaySupplementaryView: view, forElementKind: elementKind, at: indexPath) 69 | } 70 | 71 | public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 72 | delegate(at: indexPath)?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath) 73 | } 74 | 75 | public func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) { 76 | delegate(at: indexPath)?.collectionView?(collectionView, didEndDisplayingSupplementaryView: view, forElementOfKind: elementKind, at: indexPath) 77 | } 78 | 79 | public func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 80 | delegate(at: indexPath)?.collectionView?(collectionView, contextMenuConfigurationForItemAt: indexPath, point: point) 81 | } 82 | 83 | @available(iOS 16.0, *) 84 | public func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? { 85 | guard let indexPath = indexPaths.first else { return nil } 86 | return delegate(at: indexPath)?.collectionView?(collectionView, contextMenuConfigurationForItemsAt: indexPaths, point: point) 87 | } 88 | 89 | @available(iOS 16.0, *) 90 | public func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, dismissalPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? { 91 | delegate(at: indexPath)?.collectionView?(collectionView, contextMenuConfiguration: configuration, dismissalPreviewForItemAt: indexPath) 92 | } 93 | 94 | @available(iOS 16.0, *) 95 | public func collectionView(_ collectionView: UICollectionView, contextMenuConfiguration configuration: UIContextMenuConfiguration, highlightPreviewForItemAt indexPath: IndexPath) -> UITargetedPreview? { 96 | delegate(at: indexPath)?.collectionView?(collectionView, contextMenuConfiguration: configuration, highlightPreviewForItemAt: indexPath) 97 | } 98 | 99 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 100 | delegates().forEach { $0?.scrollViewDidScroll?(scrollView) } 101 | } 102 | 103 | public func scrollViewDidZoom(_ scrollView: UIScrollView) { 104 | delegates().forEach { $0?.scrollViewDidZoom?(scrollView) } 105 | } 106 | 107 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 108 | delegates().forEach { $0?.scrollViewWillBeginDragging?(scrollView) } 109 | } 110 | 111 | public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 112 | delegates().forEach { $0?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) } 113 | } 114 | 115 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 116 | delegates().forEach { $0?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) } 117 | 118 | } 119 | 120 | public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 121 | delegates().forEach { $0?.scrollViewWillBeginDecelerating?(scrollView) } 122 | } 123 | 124 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 125 | delegates().forEach { $0?.scrollViewDidEndDecelerating?(scrollView) } 126 | } 127 | 128 | 129 | public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 130 | delegates().forEach { $0?.scrollViewDidEndScrollingAnimation?(scrollView) } 131 | } 132 | 133 | public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { 134 | delegates().forEach { $0?.scrollViewWillBeginZooming?(scrollView, with: view) } 135 | } 136 | 137 | public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { 138 | delegates().forEach { $0?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) } 139 | } 140 | 141 | public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { 142 | delegates().forEach { $0?.scrollViewDidScrollToTop?(scrollView) } 143 | } 144 | 145 | public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { 146 | delegates().forEach { $0?.scrollViewDidChangeAdjustedContentInset?(scrollView) } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/CompositionalLayoutProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewCompositionalLayoutProvider.swift 3 | // iCinema 4 | // 5 | // Created by Ahmed Yamany on 07/04/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol CompositionalLayoutProvider: AnyObject { 11 | var compositionalLayoutSections: [CompositionalLayoutableSection] { get set } 12 | } 13 | 14 | extension CompositionalLayoutProvider { 15 | /** 16 | - Note: Before calling `updateCompositionalLayout(for`, 17 | ensure that the sections for the compositional layout have been appended to the `compositionalLayoutSections` 18 | property of the `CompositionalLayoutProvider`. 19 | Calling this method before configuring the sections may lead to unexpected behavior. 20 | */ 21 | public func updateCompositionalLayout(for collectionView: UICollectionView, configurations: UICollectionViewCompositionalLayoutConfiguration? = nil) { 22 | updateSections(with: collectionView) 23 | registerSupplementaryViews(for: collectionView) 24 | registerCells(for: collectionView) 25 | collectionView.collectionViewLayout = collectionViewCompositionalLayout(configurations: configurations) 26 | } 27 | 28 | private func updateSections(with collectionView: UICollectionView) { 29 | for index in compositionalLayoutSections.indices { 30 | let section = compositionalLayoutSections[index] 31 | section.index = index 32 | section.collectionView = collectionView 33 | } 34 | } 35 | 36 | private func registerCells(for collectionView: UICollectionView) { 37 | compositionalLayoutSections.forEach { $0.delegate?.registerCell(in: collectionView) } 38 | } 39 | 40 | private func registerSupplementaryViews(for collectionView: UICollectionView) { 41 | compositionalLayoutSections.forEach { $0.delegate?.registerSupplementaryView?(in: collectionView) } 42 | } 43 | 44 | private func registerDecorationViews(for layout: UICollectionViewCompositionalLayout) { 45 | compositionalLayoutSections.forEach { $0.delegate?.registerDecorationView?(in: layout) } 46 | } 47 | 48 | public func getCompositionalLayoutableSection(at indexPath: IndexPath) -> CompositionalLayoutableSection? { 49 | return compositionalLayoutSections[safe: indexPath.section] 50 | } 51 | 52 | public func dataSource(at indexPath: IndexPath) -> (any UICompositionalLayoutableSectionDataSource)? { 53 | getCompositionalLayoutableSection(at: indexPath)?.dataSource 54 | } 55 | 56 | public func prefetchDataSource(at indexPath: IndexPath) -> (any UICompositionalLayoutableSectionDataSourcePrefetching)? { 57 | getCompositionalLayoutableSection(at: indexPath)?.prefetchDataSource 58 | } 59 | 60 | public func delegate(at indexPath: IndexPath) -> (any UICompositionalLayoutableSectionDelegate)? { 61 | getCompositionalLayoutableSection(at: indexPath)?.delegate 62 | } 63 | 64 | public func sectionLayout(at sectionIndex: Int) -> (any UICompositionalLayoutableSectionLayout)? { 65 | getCompositionalLayoutableSection(at: IndexPath(row: 0, section: sectionIndex))?.sectionLayout 66 | } 67 | 68 | public func collectionViewCompositionalLayout(configurations: UICollectionViewCompositionalLayoutConfiguration? = nil) -> UICollectionViewCompositionalLayout { 69 | let layout = UICollectionViewCompositionalLayout(sectionProvider: provider) 70 | 71 | if let configurations { 72 | layout.configuration = configurations 73 | } 74 | 75 | registerDecorationViews(for: layout) 76 | 77 | return layout 78 | } 79 | 80 | private var provider: UICollectionViewCompositionalLayoutSectionProvider { 81 | { [weak self] sectionIndex, layoutEnvironment in 82 | guard let self = self else { return nil } 83 | return sectionLayout(at: sectionIndex)?.sectionLayout(at: sectionIndex, layoutEnvironment: layoutEnvironment) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/CompositionalLayoutableDataSourcePrefetching.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutableDataSourcePrefetching.swift 3 | // 4 | // 5 | // Created by Ahmed Yamany on 07/05/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | open class CompositionalLayoutDataSourcePrefetching: NSObject, UICollectionViewDataSourcePrefetching { 11 | public weak var provider: (any CompositionalLayoutProvider)? 12 | 13 | public init(provider: any CompositionalLayoutProvider) { 14 | self.provider = provider 15 | } 16 | 17 | func prefetchDataSource(at indexPath: IndexPath) -> (any UICollectionViewDataSourcePrefetching)? { 18 | provider?.prefetchDataSource(at: indexPath) 19 | } 20 | 21 | public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { 22 | guard let firstIndex = indexPaths[safe: 0] else { 23 | return 24 | } 25 | prefetchDataSource(at: firstIndex)?.collectionView(collectionView, prefetchItemsAt: indexPaths) 26 | } 27 | 28 | public func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { 29 | guard let firstIndex = indexPaths[safe: 0] else { 30 | return 31 | } 32 | prefetchDataSource(at: firstIndex)?.collectionView?(collectionView, cancelPrefetchingForItemsAt: indexPaths) 33 | } 34 | } 35 | 36 | extension Collection { 37 | subscript(safe index: Index) -> Element? { 38 | indices.contains(index) ? self[index] : nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/CompositionalLayoutableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewModelSectionType.swift 3 | // iCinema 4 | // 5 | // Created by Ahmed Yamany on 05/02/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol UICompositionalLayoutableSectionDataSource: UICollectionViewDataSource {} 11 | 12 | public protocol UICompositionalLayoutableSectionDataSourcePrefetching: UICollectionViewDataSourcePrefetching { } 13 | 14 | public protocol UICompositionalLayoutableSectionLayout: AnyObject { 15 | func sectionLayout(at index: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 16 | } 17 | 18 | @objc public protocol UICompositionalLayoutableSectionDelegate: UICollectionViewDelegate { 19 | @objc func registerCell(in collectionView: UICollectionView) 20 | @objc optional func registerSupplementaryView(in collectionView: UICollectionView) 21 | @objc optional func registerDecorationView(in layout: UICollectionViewCompositionalLayout) 22 | } 23 | 24 | /* 25 | - This class defines a four optional properties that hold objects 26 | conforming to the three protocols a section in the compositional layout should implement 27 | 28 | - Using this can lead to better organization and abstraction of your code, 29 | as well as making it easier to reuse and maintain. 30 | 31 | - You can create multiple objects Inherets from this class 32 | and switch between them to show different sections in the same collection view, 33 | */ 34 | open class CompositionalLayoutableSection: NSObject { 35 | open weak var dataSource: (any UICompositionalLayoutableSectionDataSource)? 36 | open weak var prefetchDataSource: (any UICompositionalLayoutableSectionDataSourcePrefetching)? 37 | open weak var sectionLayout: (any UICompositionalLayoutableSectionLayout)? 38 | open weak var delegate: (any UICompositionalLayoutableSectionDelegate)? 39 | 40 | public weak var collectionView: UICollectionView? 41 | public var index: Int? 42 | 43 | public func reloadData() { 44 | guard let collectionView else { 45 | debugPrint("couldn't reload data because collectionView is nil") 46 | return 47 | } 48 | 49 | guard let index else { 50 | debugPrint("couldn't reload data because section index couldn't be recoginzed") 51 | return 52 | } 53 | 54 | collectionView.reloadSections(IndexSet(integer: index)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/UICollectioView+register.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Ahmed Yamany on 26/10/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol IdentifiableView: Identifiable {} 11 | 12 | public extension IdentifiableView where Self: UIView { 13 | static var identifier: String { 14 | return String(describing: self) 15 | } 16 | } 17 | 18 | public extension UICollectionView { 19 | /// Register a cell class with the collection view and associate it with a reuse identifier 20 | func register(_ class: T.Type) where T: IdentifiableView { 21 | register(T.self, forCellWithReuseIdentifier: T.identifier) 22 | } 23 | 24 | /// Register a cell class from nib file with the collection view and associate it with a reuse identifier 25 | func registerFromNib(_ class: T.Type) where T: IdentifiableView { 26 | register(UINib(nibName: T.identifier, bundle: nil), forCellWithReuseIdentifier: T.identifier) 27 | } 28 | 29 | /// Dequeue a reusable cell of a specified class from the collection view 30 | func dequeueReusableCell(_ class: T.Type, for indexPath: IndexPath) -> T where T: IdentifiableView { 31 | guard let cell = dequeueReusableCell(withReuseIdentifier: T.identifier, for: indexPath) as? T else { 32 | fatalError("Couldn't find UICollectionViewCell for \(T.identifier), make sure the cell is registered with collection view") 33 | } 34 | return cell 35 | } 36 | } 37 | 38 | extension UICollectionReusableView: IdentifiableView { } 39 | -------------------------------------------------------------------------------- /Sources/CompositionalLayoutableSection/UICollectioView+supplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectioView+supplementaryView.swift 3 | // 4 | // 5 | // Created by Ahmed Yamany on 27/10/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @available(iOS 13.0, *) 11 | public extension UICollectionView { 12 | /** 13 | Registers a supplementary view class for a specific kind in the collection view. 14 | 15 | This method simplifies the process of registering a reusable supplementary view class with the collection view. 16 | It associates the provided class type with the given supplementary view kind 17 | and uses the class's identifier as the reuse identifier. 18 | 19 | - Parameters: 20 | - class: The class type of the supplementary view to register. This class must conform to `IdentifiableView`. 21 | - kind: The kind of supplementary view to associate with the class. 22 | */ 23 | func register(_ class: T.Type, supplementaryViewOfKind kind: String) where T: IdentifiableView { 24 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.identifier) 25 | } 26 | 27 | /// Register a UICollectionReusableView class from nib file with the collection view and associate it with a reuse identifier 28 | func registerFromNib(_ class: T.Type, supplementaryViewOfKind kind: String) where T: IdentifiableView { 29 | register(UINib(nibName: T.identifier, bundle: .main), forSupplementaryViewOfKind: kind, withReuseIdentifier: T.identifier) 30 | } 31 | 32 | /** 33 | Dequeues a reusable supplementary view of the specified class for a given kind and index path. 34 | 35 | This method simplifies the process of dequeuing a reusable supplementary view from the collection view 36 | . It dequeues a view of the provided class type for the specified supplementary view kind and index path. 37 | It's important that the class type is registered with the collection view using the 38 | `register(_:supplementaryViewOfKind:)` method before calling this method. 39 | 40 | - Parameters: 41 | - class: The class type of the supplementary view to dequeue. This class must conform to `IdentifiableView`. 42 | - kind: The kind of supplementary view being requested. 43 | - indexPath: The index path specifying the location of the supplementary view in the collection view. 44 | - Returns: A dequeued instance of the specified supplementary view class. 45 | - Precondition: The class type must be registered with the collection view using 46 | `register(_:supplementaryViewOfKind:)` before calling this method. 47 | */ 48 | func dequeueReusableSupplementaryView(_ class: T.Type, 49 | ofKind kind: String, 50 | for indexPath: IndexPath) -> T where T: IdentifiableView { 51 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, 52 | withReuseIdentifier: T.identifier, 53 | for: indexPath) as? T else { 54 | fatalError("Couldn't find UICollectionReusableView for \(T.identifier), make sure the view is registered with collection view") 55 | } 56 | return view 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/CompositionalLayoutableSectionTests/CompositionalLayoutableSectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CompositionalLayoutableSection 3 | 4 | final class CompositionalLayoutableSectionTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/1.png -------------------------------------------------------------------------------- /assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/2.png -------------------------------------------------------------------------------- /assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/3.png -------------------------------------------------------------------------------- /assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/4.png -------------------------------------------------------------------------------- /assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/5.png -------------------------------------------------------------------------------- /assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/6.png -------------------------------------------------------------------------------- /assets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/7.png -------------------------------------------------------------------------------- /assets/video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/video.gif -------------------------------------------------------------------------------- /assets/video.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmed-yamany/CompositionalLayoutableSection/2d0f744b4f7398ca32533a2001a6224b125be04b/assets/video.mov --------------------------------------------------------------------------------