├── .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
--------------------------------------------------------------------------------