├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── CollectionView
│ ├── CollectionView+Coordinator.swift
│ ├── CollectionView+Initializers.swift
│ ├── CollectionView+ViewModifiers.swift
│ ├── CollectionView.swift
│ ├── Extensions
│ │ ├── ListLayoutAppearance+DefaultBackgroundColor.swift
│ │ └── OrderedDictionary+IndexPath.swift
│ ├── ResultBuilders
│ │ ├── Builder Inits.swift
│ │ └── BuilderTesting.swift
│ └── Testing
│ │ ├── HostingInitTest.swift
│ │ ├── Initializer Testing.swift
│ │ ├── ListInitTest.swift
│ │ └── StandardInitMultipleSelectTest.swift
└── PrivacyInfo.xcprivacy
└── Tests
└── CollectionViewTests
└── CollectionViewTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 | Package.resolved
10 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | builder:
3 | configs:
4 | - documentation_targets: [CollectionView]
5 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Edon Valdman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
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: "CollectionView",
8 | platforms: [
9 | .iOS(.v14),
10 | .macCatalyst(.v14),
11 | .tvOS(.v14),
12 | .visionOS(.v1)
13 | ],
14 | products: [
15 | // Products define the executables and libraries a package produces, making them visible to other packages.
16 | .library(
17 | name: "CollectionView",
18 | targets: ["CollectionView"])
19 | ],
20 | dependencies: [
21 | .package(
22 | url: "https://github.com/apple/swift-collections.git",
23 | .upToNextMajor(from: "1.0.0")
24 | ),
25 | .package(
26 | url: "https://github.com/edonv/CompositionalLayoutBuilder.git",
27 | .upToNextMajor(from: "0.1.0")
28 | )
29 | ],
30 | targets: [
31 | // Targets are the basic building blocks of a package, defining a module or a test suite.
32 | // Targets can depend on other targets in this package and products from dependencies.
33 | .target(
34 | name: "CollectionView",
35 | dependencies: [
36 | .product(name: "OrderedCollections", package: "swift-collections"),
37 | .product(name: "CompositionalLayoutBuilder", package: "CompositionalLayoutBuilder")
38 | ],
39 | resources: [.copy("../PrivacyInfo.xcprivacy")]
40 | ),
41 | .testTarget(
42 | name: "CollectionViewTests",
43 | dependencies: ["CollectionView"]),
44 | ]
45 | )
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CollectionView
2 |
3 | [](https://swiftpackageindex.com/edonv/CollectionView)
4 | [](https://swiftpackageindex.com/edonv/CollectionView)
5 |
6 | `CollectionView` is a SwiftUI wrapper of [`UICollectionView`](https://developer.apple.com/documentation/uikit/uicollectionview).
7 |
8 | Have you ever wanted to make an app solely in SwiftUI, but the processing tradeoff of using [`Grid`](https://developer.apple.com/documentation/swiftui/grid), [`Lazy_Stack`](https://developer.apple.com/documentation/swiftui/grouping-data-with-lazy-stack-views), and [`Lazy_Grid`](https://developer.apple.com/documentation/swiftui/layout-fundamentals#dynamically-arranging-views-in-two-dimensions) are too significant? Wish you could stick with SwiftUI but still get the processing power of [`UICollectionView`](https://developer.apple.com/documentation/uikit/uicollectionview)? Then try out CollectionView!
9 |
10 | It’s a SwiftUI wrapper for `UICollectionView` that exposes all [`UICollectionViewDelegate`](https://developer.apple.com/documentation/uikit/uicollectionviewdelegate)/[`UICollectionViewDataSourcePrefetching`](https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching) delegate functions (via view modifiers). Plus, on iOS 16+, you can utilize [`UIHostingConfiguration`](https://developer.apple.com/documentation/swiftui/uihostingconfiguration) to use SwiftUI views for the cells.
11 |
12 | Plus, by passing your data source as a `Binding`, it updates changes to the view using [`UICollectionViewDiffableDataSource`](https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource). This also means it doesn't fully reload the view on every change, but rather, they're reloaded internally by the `UICollectionView`.
13 |
14 | It’s still a work in progress (especially with testing everything + documentation), but please give it a try and send feedback my way!
15 |
16 | ## Usage
17 |
18 | - Make sure to use `.ignoresSafeArea()` if it's meant to be full-screen.
19 |
20 | ## To-Do's
21 |
22 | - [x] Implement `UICellConfigurationState`.
23 | - [ ] Add support for `NSCollectionView`.
24 | - [ ] Make sure `CollectionView` isn't updating itself more than necessary? or is it not because it's a `RepresentableView`?
25 | - [ ] Condense initializers somehow...
26 | - [ ] Add support for section snapshots.
27 | - [ ] Add support for expandable sections.
28 | - [ ] Add support for section headers/footers.
29 | - [ ] Refactor `cellRegistrationHandler` out of primary initializers and replace with a `UICollectionViewDiffableDataSource.CellProvider` closure to allow for more complicated configurations.
30 | - [ ] Finish documenting view modifiers.
31 | - [ ] Work on more concrete example for README/DocC articles.
32 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CollectionView+Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionView+Coordinator.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 9/26/23.
6 | //
7 |
8 | import UIKit
9 |
10 | extension CollectionView {
11 | public class Coordinator: NSObject, UICollectionViewDelegate, UICollectionViewDataSourcePrefetching {
12 | var parent: CollectionView
13 | var dataSource: DataSource!
14 |
15 | init(_ parent: CollectionView) {
16 | self.parent = parent
17 | }
18 |
19 | internal func setUpCollectionView(_ collectionView: UICollectionView) {
20 | let cellRegistration = parent.cellRegistration
21 | self.dataSource = .init(collectionView: collectionView) { collectionView, indexPath, item in
22 | collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
23 | }
24 | }
25 |
26 | // MARK: - UICollectionViewDelegate
27 |
28 |
29 |
30 | // MARK: Managing the selected cells
31 |
32 | public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
33 | parent.shouldSelectItemHandler?(collectionView, indexPath) ?? true
34 | }
35 |
36 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
37 | parent.didSelectItemHandler?(collectionView, indexPath)
38 | guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
39 | parent.selection.insert(item)
40 | }
41 |
42 | public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool {
43 | parent.shouldDeselectItemHandler?(collectionView, indexPath) ?? true
44 | }
45 |
46 | public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
47 | guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
48 | parent.selection.remove(item)
49 | }
50 |
51 | // MARK: Multiple Selection Interaction
52 |
53 | public func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
54 | parent.shouldBeginMultipleSelectionInteractionHandler?(collectionView, indexPath) ?? false
55 | }
56 |
57 | public func collectionView(_ collectionView: UICollectionView, didBeginMultipleSelectionInteractionAt indexPath: IndexPath) {
58 | parent.didBeginMultipleSelectionInteractionHandler?(collectionView, indexPath)
59 | }
60 |
61 | public func collectionViewDidEndMultipleSelectionInteraction(_ collectionView: UICollectionView) {
62 | parent.didEndMultipleSelectionInteractionHandler?()
63 | }
64 |
65 | // MARK: Managing cell highlighting
66 |
67 | public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
68 | parent.shouldHighlightItemHandler?(collectionView, indexPath) ?? true
69 | }
70 |
71 | public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
72 | parent.didHighlightItemHandler?(collectionView, indexPath)
73 | }
74 |
75 | public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
76 | parent.didUnhighlightItemHandler?(collectionView, indexPath)
77 | }
78 |
79 | // MARK: Tracking the addition and removal of views
80 |
81 | public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
82 | parent.willDisplayCellHandler?(cell, indexPath)
83 | }
84 |
85 | // public func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
86 | // <#code#>
87 | // }
88 |
89 | public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
90 | parent.didEndDisplayingCellHandler?(cell, indexPath)
91 | }
92 |
93 | // public func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) {
94 | // <#code#>
95 | // }
96 |
97 | // MARK: Managing context menus
98 |
99 | public func collectionView(_ collectionView: UICollectionView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
100 | parent.willDisplayContextMenu?(configuration, animator)
101 | }
102 |
103 | public func collectionView(_ collectionView: UICollectionView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
104 | parent.willEndContextMenuInteraction?(configuration, animator)
105 | }
106 |
107 | public func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
108 | parent.willPerformPreviewAction?(configuration, animator)
109 | }
110 |
111 | public func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
112 | parent.contextMenuConfigHandler?(indexPaths, point)
113 | }
114 |
115 | // TODO: implement collectionView(_:contextMenuConfiguration:highlightPreviewForItemAt:) and collectionView(_:contextMenuConfiguration:dismissalPreviewForItemAt:)
116 |
117 | // MARK: - UICollectionViewDataSourcePrefetching
118 |
119 | public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
120 | parent.prefetchItemsHandler?(indexPaths)
121 | }
122 |
123 | public func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
124 | parent.cancelPrefetchingHandler?(indexPaths)
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CollectionView+Initializers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionView+Initializers.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 9/26/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // MARK: - UIHostingConfiguration
11 |
12 | @available(iOS 16, macCatalyst 16, tvOS 16, visionOS 1, *)
13 | extension CollectionView where Cell == UICollectionViewCell {
14 | // MARK: - SwiftUI Cell, No Background, Multiple Select
15 |
16 | /// Creates a collection view that computes its cells using a SwiftUI view, also allowing users to select multiple items.
17 | ///
18 | /// - Note: If you'd like to allow multiple selection, but don't need to keep track of the selections, use `.constant([])` as input for `selection`.
19 | /// - Parameters:
20 | /// - data: The data for populating the list.
21 | /// - selection: A binding to a set that represents selected items.
22 | /// - layout: The layout object to use for organizing items.
23 | /// - cellContent: A view builder that creates the view for a single cell in the collection view. If using a list layout, it's possible to use [`.swipeActions`](https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)) on the content of this closure and it'll be bridged automatically.
24 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
25 | public init(
26 | _ data: Binding,
27 | selection: Binding>,
28 | layout: CollectionLayout,
29 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
30 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil
31 | ) where Content: View {
32 | self.init(data,
33 | selection: selection,
34 | layout: layout) { cell, indexPath, item in
35 | cell.configurationUpdateHandler = { cell, state in
36 | cell.contentConfiguration = UIHostingConfiguration {
37 | cellContent(indexPath, state, item)
38 | }
39 | cellConfigurationHandler?(cell, indexPath, state, item)
40 | }
41 | }
42 | }
43 |
44 | // MARK: - SwiftUI Cell, No Background, Single/No Select
45 |
46 | /// Creates a collection view that computes its cells using a SwiftUI view, optionally allowing users to select a single item.
47 | ///
48 | /// - Note: If you'd like to allow single selection, but don't need to keep track of the selection, use `.constant(nil)` as input for `selection`.
49 | /// - Parameters:
50 | /// - data: The data for populating the list.
51 | /// - selection: A binding to a selected value, if provided. Otherwise, no selection will be allowed.
52 | /// - layout: The layout object to use for organizing items.
53 | /// - cellContent: A view builder that creates the view for a single cell in the collection view. If using a list layout, it's possible to use [`.swipeActions`](https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)) on the content of this closure and it'll be bridged automatically.
54 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
55 | public init(
56 | _ data: Binding,
57 | selection: Binding- ? = nil,
58 | layout: CollectionLayout,
59 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
60 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil
61 | ) where Content: View {
62 | self.init(data,
63 | selection: selection,
64 | layout: layout) { cell, indexPath, item in
65 | cell.configurationUpdateHandler = { cell, state in
66 | cell.contentConfiguration = UIHostingConfiguration {
67 | cellContent(indexPath, state, item)
68 | }
69 | cellConfigurationHandler?(cell, indexPath, state, item)
70 | }
71 | }
72 | }
73 |
74 | // MARK: - SwiftUI Cell, Custom View Background, Multiple Select
75 |
76 | /// Creates a collection view that computes its cells and their backgrounds using SwiftUI views, also allowing users to select multiple items.
77 | ///
78 | /// - Note: If you'd like to allow multiple selection, but don't need to keep track of the selections, use `.constant([])` as input for `selection`.
79 | /// - Parameters:
80 | /// - data: The data for populating the list.
81 | /// - selection: A binding to a set that represents selected items.
82 | /// - layout: The layout object to use for organizing items.
83 | /// - cellContent: A view builder that creates the view for a single cell in the collection view. If using a list layout, it's possible to use [`.swipeActions`](https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)) on the content of this closure and it'll be bridged automatically.
84 | /// - cellBackground: The contents of the SwiftUI hierarchy to be shown inside the background of the cell.
85 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
86 | public init(
87 | _ data: Binding,
88 | selection: Binding>,
89 | layout: CollectionLayout,
90 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
91 | @ViewBuilder cellBackground: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Background,
92 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil
93 | ) where Content: View, Background: View {
94 | self.init(data,
95 | selection: selection,
96 | layout: layout) { cell, indexPath, item in
97 | cell.configurationUpdateHandler = { cell, state in
98 | cell.contentConfiguration = UIHostingConfiguration {
99 | cellContent(indexPath, state, item)
100 | }
101 | .background {
102 | cellBackground(indexPath, state, item)
103 | }
104 | cellConfigurationHandler?(cell, indexPath, state, item)
105 | }
106 | }
107 | }
108 |
109 | // MARK: - SwiftUI Cell, Custom View Background, Single/No Select
110 |
111 | /// Creates a collection view that computes its cells and their backgrounds using SwiftUI views, optionally allowing users to select a single item.
112 | ///
113 | /// - Note: If you'd like to allow single selection, but don't need to keep track of the selection, use `.constant(nil)` as input for `selection`.
114 | /// - Parameters:
115 | /// - data: The data for populating the list.
116 | /// - selection: A binding to a selected value, if provided. Otherwise, no selection will be allowed.
117 | /// - layout: The layout object to use for organizing items.
118 | /// - cellContent: A view builder that creates the view for a single cell in the collection view. If using a list layout, it's possible to use [`.swipeActions`](https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)) on the content of this closure and it'll be bridged automatically.
119 | /// - cellBackground: The contents of the SwiftUI hierarchy to be shown inside the background of the cell.
120 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
121 | public init(
122 | _ data: Binding,
123 | selection: Binding
- ? = nil,
124 | layout: CollectionLayout,
125 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
126 | @ViewBuilder cellBackground: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Background,
127 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil
128 | ) where Content: View, Background: View {
129 | self.init(data,
130 | selection: selection,
131 | layout: layout) { cell, indexPath, item in
132 | cell.configurationUpdateHandler = { cell, state in
133 | cell.contentConfiguration = UIHostingConfiguration {
134 | cellContent(indexPath, state, item)
135 | }
136 | .background {
137 | cellBackground(indexPath, state, item)
138 | }
139 | cellConfigurationHandler?(cell, indexPath, state, item)
140 | }
141 | }
142 | }
143 |
144 | // MARK: - SwiftUI Cell, ShapeStyle Background, Multiple Select
145 |
146 | /// Creates a collection view that computes its cells using SwiftUI views (and their backgrounds from a shape style), also allowing users to select multiple items.
147 | ///
148 | /// - Note: If you'd like to allow multiple selection, but don't need to keep track of the selections, use `.constant([])` as input for `selection`.
149 | /// - Parameters:
150 | /// - data: The data for populating the list.
151 | /// - selection: A binding to a set that represents selected items.
152 | /// - layout: The layout object to use for organizing items.
153 | /// - cellBackground: The shape style to be used as the background of the cell.
154 | /// - cellContent: A view builder that creates the view for a single cell in the collection view. If using a list layout, it's possible to use [`.swipeActions`](https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)) on the content of this closure and it'll be bridged automatically.
155 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
156 | public init(
157 | _ data: Binding,
158 | selection: Binding>,
159 | layout: CollectionLayout,
160 | cellBackground: S,
161 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
162 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil
163 | ) where Content: View, S: ShapeStyle {
164 | self.init(data,
165 | selection: selection,
166 | layout: layout) { cell, indexPath, item in
167 | cell.configurationUpdateHandler = { cell, state in
168 | cell.contentConfiguration = UIHostingConfiguration {
169 | cellContent(indexPath, state, item)
170 | }
171 | .background(cellBackground)
172 | cellConfigurationHandler?(cell, indexPath, state, item)
173 | }
174 | }
175 | }
176 |
177 | // MARK: - SwiftUI Cell, ShapeStyle Background, Single/No Select
178 |
179 | /// Creates a collection view that computes its cells using SwiftUI views (and their backgrounds from a shape style), optionally allowing users to select a single item.
180 | ///
181 | /// - Note: If you'd like to allow single selection, but don't need to keep track of the selection, use `.constant(nil)` as input for `selection`.
182 | /// - Parameters:
183 | /// - data: The data for populating the list.
184 | /// - selection: A binding to a selected value, if provided. Otherwise, no selection will be allowed.
185 | /// - layout: The layout object to use for organizing items.
186 | /// - cellBackground: The shape style to be used as the background of the cell.
187 | /// - cellContent: A view builder that creates the view for a single cell in the collection view. If using a list layout, it's possible to use [`.swipeActions`](https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)) on the content of this closure and it'll be bridged automatically.
188 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
189 | public init(
190 | _ data: Binding,
191 | selection: Binding
- ? = nil,
192 | layout: CollectionLayout,
193 | cellBackground: S,
194 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
195 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil
196 | ) where Content: View, S: ShapeStyle {
197 | self.init(data,
198 | selection: selection,
199 | layout: layout) { cell, indexPath, item in
200 | cell.configurationUpdateHandler = { cell, state in
201 | cell.contentConfiguration = UIHostingConfiguration {
202 | cellContent(indexPath, state, item)
203 | }
204 | .background(cellBackground)
205 | cellConfigurationHandler?(cell, indexPath, state, item)
206 | }
207 | }
208 | }
209 | }
210 |
211 | // MARK: - UICollectionLayoutListConfiguration
212 |
213 | extension CollectionView where CollectionLayout == UICollectionViewCompositionalLayout, Cell == UICollectionViewListCell {
214 |
215 | // MARK: - List Layout, List Cell, Multiple Select
216 |
217 | /// Creates a collection view with a list layout that allows users to select multiple items.
218 | ///
219 | /// - Important: The `state` parameter in the closures only take effect if used on iOS 15+.
220 | /// - Note: If you'd like to allow multiple selection, but don't need to keep track of the selections, use `.constant([])` as input for `selection`.
221 | /// - Parameters:
222 | /// - data: The data for populating the list.
223 | /// - selection: A binding to a set that represents selected items.
224 | /// - listAppearance: The overall appearance of the list.
225 | /// - contentConfiguration: A closure for creating a [`UIContentConfiguration`](https://developer.apple.com/documentation/uikit/uicontentconfiguration) for each item's cell.
226 | /// - backgroundConfiguration: An optional closure for creating a [`UIBackgroundConfiguration`](https://developer.apple.com/documentation/uikit/uibackgroundconfiguration) for each item's cell.
227 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
228 | /// - listConfigurationHandler: A closure for configuring the `UICollectionLayoutListConfiguration` of the layout.
229 | public init(
230 | _ data: Binding,
231 | selection: Binding>,
232 | listAppearance: UICollectionLayoutListConfiguration.Appearance,
233 | contentConfiguration: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> UIListContentConfiguration,
234 | backgroundConfiguration: ((IndexPath, _ state: UICellConfigurationState, _ item: Item) -> UIBackgroundConfiguration)?,
235 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil,
236 | listConfigurationHandler: ((_ config: inout UICollectionLayoutListConfiguration) -> Void)? = nil
237 | ) {
238 | var listConfig = UICollectionLayoutListConfiguration(appearance: listAppearance)
239 | listConfig.backgroundColor = .clear
240 | listConfigurationHandler?(&listConfig)
241 |
242 | self.init(data,
243 | selection: selection,
244 | layout: UICollectionViewCompositionalLayout.list(using: listConfig)) { cell, indexPath, item in
245 | if #available(iOS 15.0, *) {
246 | cell.configurationUpdateHandler = { cell, state in
247 | cell.contentConfiguration = contentConfiguration(indexPath, state, item)
248 | cell.backgroundConfiguration = backgroundConfiguration?(indexPath, state, item)
249 | cellConfigurationHandler?(cell as! Cell, indexPath, state, item)
250 | }
251 | } else {
252 | cell.contentConfiguration = contentConfiguration(indexPath, .init(traitCollection: .current), item)
253 | cell.backgroundConfiguration = backgroundConfiguration?(indexPath, .init(traitCollection: .current), item)
254 | cellConfigurationHandler?(cell, indexPath, .init(traitCollection: .current), item)
255 | }
256 | }
257 |
258 | self.collectionViewBackgroundColor = if #available(iOS 15.0, *) {
259 | Color(uiColor: listAppearance.defaultBackgroundColor)
260 | } else {
261 | Color(listAppearance.defaultBackgroundColor)
262 | }
263 | }
264 |
265 | // MARK: - List Layout, List Cell, Single/No Select
266 |
267 | /// Creates a collection view with a list layout that optionally allows users to select a single item.
268 | ///
269 | /// - Important: The `state` parameter in the closures only take effect if used on iOS 15+.
270 | /// - Note: If you'd like to allow single selection, but don't need to keep track of the selection, use `.constant(nil)` as input for `selection`.
271 | /// - Parameters:
272 | /// - data: The data for populating the list.
273 | /// - selection: A binding to a selected value, if provided. Otherwise, no selection will be allowed.
274 | /// - listAppearance: The overall appearance of the list.
275 | /// - contentConfiguration: A closure for creating a [`UIContentConfiguration`](https://developer.apple.com/documentation/uikit/uicontentconfiguration) for each item's cell.
276 | /// - backgroundConfiguration: An optional closure for creating a [`UIBackgroundConfiguration`](https://developer.apple.com/documentation/uikit/uibackgroundconfiguration) for each item's cell.
277 | /// - cellConfigurationHandler: An optional closure for configuring properties of each item's cell. See more here: ``CollectionView/CollectionView/cellConfigurationHandler``.
278 | /// - listConfigurationHandler: A closure for configuring the `UICollectionLayoutListConfiguration` of the layout.
279 | public init(
280 | _ data: Binding,
281 | selection: Binding
- ? = nil,
282 | listAppearance: UICollectionLayoutListConfiguration.Appearance,
283 | contentConfiguration: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> UIListContentConfiguration,
284 | backgroundConfiguration: ((IndexPath, _ state: UICellConfigurationState, _ item: Item) -> UIBackgroundConfiguration)?,
285 | cellConfigurationHandler: ((Cell, IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Void)? = nil,
286 | listConfigurationHandler: ((_ config: inout UICollectionLayoutListConfiguration) -> Void)? = nil
287 | ) {
288 | var listConfig = UICollectionLayoutListConfiguration(appearance: listAppearance)
289 | listConfig.backgroundColor = .clear
290 | listConfigurationHandler?(&listConfig)
291 |
292 | self.init(data,
293 | selection: selection,
294 | layout: UICollectionViewCompositionalLayout.list(using: listConfig)) { cell, indexPath, item in
295 | if #available(iOS 15.0, *) {
296 | cell.configurationUpdateHandler = { cell, state in
297 | cell.contentConfiguration = contentConfiguration(indexPath, state, item)
298 | cell.backgroundConfiguration = backgroundConfiguration?(indexPath, state, item)
299 | cellConfigurationHandler?(cell as! Cell, indexPath, state, item)
300 | }
301 | } else {
302 | cell.contentConfiguration = contentConfiguration(indexPath, .init(traitCollection: .current), item)
303 | cell.backgroundConfiguration = backgroundConfiguration?(indexPath, .init(traitCollection: .current), item)
304 | cellConfigurationHandler?(cell, indexPath, .init(traitCollection: .current), item)
305 | }
306 | }
307 |
308 | self.collectionViewBackgroundColor = if #available(iOS 15.0, *) {
309 | Color(uiColor: listAppearance.defaultBackgroundColor)
310 | } else {
311 | Color(listAppearance.defaultBackgroundColor)
312 | }
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CollectionView+ViewModifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionView+ViewModifiers.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 9/27/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension CollectionView {
11 | // MARK: - Misc Properties
12 |
13 | /// Sets the view’s background to a color.
14 | /// - Parameter color: An instance of a color that is drawn behind the modified view.
15 | /// - Returns: A view with the specified color drawn behind it.
16 | public func backgroundColor(
17 | _ color: UIColor
18 | ) -> CollectionView {
19 | var new = self
20 | new.collectionViewBackgroundColor = if #available(iOS 15.0, *) {
21 | Color(uiColor: color)
22 | } else {
23 | Color(color)
24 | }
25 | return new
26 | }
27 |
28 | /// Sets the view’s background to a color.
29 | /// - Parameter color: An instance of a color that is drawn behind the modified view.
30 | /// - Returns: A view with the specified color drawn behind it.
31 | public func backgroundColor(
32 | _ color: Color?
33 | ) -> CollectionView {
34 | var new = self
35 | new.collectionViewBackgroundColor = color
36 | return new
37 | }
38 |
39 | /// Sets the view’s background to a style.
40 | /// - Parameter style: An instance of a color that is drawn behind the modified view.
41 | /// - Returns: A view with the specified color drawn behind it.
42 | @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
43 | public func backgroundStyle
(
44 | _ style: S
45 | ) -> some View where S: ShapeStyle {
46 | self.backgroundView {
47 | Rectangle().fill(BackgroundStyle.background)
48 | }
49 | }
50 |
51 | /// Layers the given view behind this view.
52 | /// - Parameters:
53 | /// - background: The view to draw behind this view.
54 | /// - alignment: The alignment with a default value of [`center`](https://developer.apple.com/documentation/swiftui/alignment/center) that you use to position the background view.
55 | @available(iOS, deprecated: 14, obsoleted: 15)
56 | @available(macOS, deprecated: 11, obsoleted: 12)
57 | @available(macCatalyst, deprecated: 14, obsoleted: 15)
58 | @available(tvOS, deprecated: 14, obsoleted: 15)
59 | @available(watchOS, deprecated: 7, obsoleted: 8)
60 | public func backgroundView(
61 | _ background: Background,
62 | alignment: Alignment = .center
63 | ) -> some View where Background: View {
64 | var new = self
65 | new.collectionViewBackgroundColor = nil
66 | return new.background(background, alignment: alignment)
67 | }
68 |
69 | /// Layers the views that you specify behind this view.
70 | /// - Parameters:
71 | /// - alignment: The alignment that the modifier uses to position the implicit [`ZStack`](https://developer.apple.com/documentation/swiftui/zstack) that groups the background views. The default is [`center`](https://developer.apple.com/documentation/swiftui/alignment/center).
72 | /// - content: A [`ViewBuilder`](https://developer.apple.com/documentation/swiftui/viewbuilder) that you use to declare the views to draw behind this view, stacked in a cascading order from bottom to top. The last view that you list appears at the front of the stack.
73 | /// - Returns: A view that uses the specified content as a background.
74 | @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
75 | public func backgroundView(
76 | alignment: Alignment = .center,
77 | @ViewBuilder content: () -> V
78 | ) -> some View where V: View {
79 | var new = self
80 | new.collectionViewBackgroundColor = nil
81 | return new.background(alignment: alignment, content: content)
82 | }
83 |
84 | // MARK: - Delegate Callbacks
85 |
86 |
87 |
88 | // MARK: - Single Selection
89 |
90 | /// Adds an action to perform when a cell is selected.
91 | /// - Parameters:
92 | /// - handler: A closure to run when the cell is selected.
93 | /// - indexPath: The index path of the cell that was selected.
94 | /// - Returns: A view that runs the given action when a cell is selected.
95 | public func onSelect(
96 | _ handler: CollectionViewVoidCallback?
97 | ) -> CollectionView {
98 | var new = self
99 | new.didSelectItemHandler = handler
100 | return new
101 | }
102 |
103 | /// Adds closures for checking if a cell should be selected/deselected.
104 | ///
105 | /// When setting either item to `nil`, all items will be selected/deselected normally.
106 | /// - Parameters:
107 | /// - shouldSelectItem: A closure that should return `true` if the specified item should be selected.
108 | /// - shouldDeselectItem: A closure that should return `true` if the specified item should be deselected.
109 | public func itemSelection(
110 | shouldSelectItem: CollectionViewBoolCallback? = nil,
111 | shouldDeselectItem: CollectionViewBoolCallback? = nil
112 | ) -> CollectionView {
113 | var new = self
114 | new.shouldSelectItemHandler = shouldSelectItem
115 | new.shouldDeselectItemHandler = shouldDeselectItem
116 | return new
117 | }
118 |
119 | /// Sets if cell selection should be allowed.
120 | /// - Parameter allowed: Whether cells should be allowed to be selected.
121 | public func shouldAllowSelection(
122 | _ allowed: Bool
123 | ) -> CollectionView {
124 | var new = self
125 | new.shouldSelectItemHandler = { _, _ in allowed }
126 | return new
127 | }
128 |
129 | // MARK: - Multiple Selection
130 |
131 | public func shouldBeginMultipleSelectionInteraction(
132 | _ handler: CollectionViewBoolCallback?
133 | ) -> CollectionView {
134 | var new = self
135 | new.shouldBeginMultipleSelectionInteractionHandler = handler
136 | return new
137 | }
138 |
139 | public func shouldAllowTwoFingerMultipleSelectionInteraction(
140 | _ allowed: Bool
141 | ) -> CollectionView {
142 | var new = self
143 | new.shouldBeginMultipleSelectionInteractionHandler = { _, _ in allowed }
144 | return new
145 | }
146 |
147 | public func didBeginMultipleSelectionInteraction(
148 | _ handler: CollectionViewVoidCallback?
149 | ) -> CollectionView {
150 | var new = self
151 | new.didBeginMultipleSelectionInteractionHandler = handler
152 | return new
153 | }
154 |
155 | public func didEndMultipleSelectionInteraction(
156 | _ handler: (() -> Void)?
157 | ) -> CollectionView {
158 | var new = self
159 | new.didEndMultipleSelectionInteractionHandler = handler
160 | return new
161 | }
162 |
163 | // MARK: - Highlighting
164 |
165 | public func shouldHighlightItem(
166 | _ handler: CollectionViewBoolCallback?
167 | ) -> CollectionView {
168 | var new = self
169 | new.shouldHighlightItemHandler = handler
170 | return new
171 | }
172 |
173 | public func shouldAllowHighlighting(
174 | _ allowed: Bool
175 | ) -> CollectionView {
176 | var new = self
177 | new.shouldHighlightItemHandler = { _, _ in allowed }
178 | return new
179 | }
180 |
181 | public func didHighlightItem(
182 | _ handler: CollectionViewVoidCallback?
183 | ) -> CollectionView {
184 | var new = self
185 | new.didHighlightItemHandler = handler
186 | return new
187 | }
188 |
189 | public func didUnhighlightItem(
190 | _ handler: CollectionViewVoidCallback?
191 | ) -> CollectionView {
192 | var new = self
193 | new.didUnhighlightItemHandler = handler
194 | return new
195 | }
196 |
197 | // MARK: - Displaying Cells
198 |
199 | public func willDisplayCell(
200 | _ handler: ((_ cell: UICollectionViewCell, _ indexPath: IndexPath) -> Void)?
201 | ) -> CollectionView {
202 | var new = self
203 | new.willDisplayCellHandler = handler
204 | return new
205 | }
206 |
207 | public func didEndDisplayingCell(
208 | _ handler: ((_ cell: UICollectionViewCell, _ indexPath: IndexPath) -> Void)?
209 | ) -> CollectionView {
210 | var new = self
211 | new.didEndDisplayingCellHandler = handler
212 | return new
213 | }
214 |
215 | // MARK: - Context Menu
216 |
217 | public func willDisplayContextMenu(
218 | _ handler: ((_ configuration: UIContextMenuConfiguration, UIContextMenuInteractionAnimating?) -> Void)?
219 | ) -> CollectionView {
220 | var new = self
221 | new.willDisplayContextMenu = handler
222 | return new
223 | }
224 |
225 | public func willEndContextMenuInteraction(
226 | _ handler: ((_ configuration: UIContextMenuConfiguration, UIContextMenuInteractionAnimating?) -> Void)?
227 | ) -> CollectionView {
228 | var new = self
229 | new.willEndContextMenuInteraction = handler
230 | return new
231 | }
232 |
233 | public func onContextMenuPreviewTap(
234 | _ handler: ((_ configuration: UIContextMenuConfiguration, UIContextMenuInteractionAnimating?) -> Void)?
235 | ) -> CollectionView {
236 | var new = self
237 | new.willPerformPreviewAction = handler
238 | return new
239 | }
240 |
241 | public func contextMenu(
242 | menuIdentifier: NSCopying? = nil,
243 | swiftUIProvider previewProvider: @escaping (_ indexPaths: [IndexPath], _ point: CGPoint) -> Preview?,
244 | actionProvider: ((_ indexPaths: [IndexPath], _ point: CGPoint, [UIMenuElement]) -> UIMenu?)? = nil
245 | ) -> CollectionView {
246 | var new = self
247 | new.contextMenuConfigHandler = { indexPaths, point in
248 | UIContextMenuConfiguration(
249 | identifier: menuIdentifier,
250 | previewProvider: {
251 | guard let preview = previewProvider(indexPaths, point) else { return nil }
252 | return UIHostingController(rootView: preview)
253 | },
254 | actionProvider: { menuElements in
255 | actionProvider?(indexPaths, point, menuElements)
256 | }
257 | )
258 | }
259 | return new
260 | }
261 |
262 | public func contextMenu(
263 | menuIdentifier: NSCopying? = nil,
264 | uiKitProvider previewProvider: @escaping (_ indexPaths: [IndexPath], _ point: CGPoint) -> UIViewController?,
265 | actionProvider: ((_ indexPaths: [IndexPath], _ point: CGPoint, [UIMenuElement]) -> UIMenu?)? = nil
266 | ) -> CollectionView {
267 | var new = self
268 | new.contextMenuConfigHandler = { indexPaths, point in
269 | UIContextMenuConfiguration(
270 | identifier: menuIdentifier,
271 | previewProvider: {
272 | previewProvider(indexPaths, point)
273 | },
274 | actionProvider: { menuElements in
275 | actionProvider?(indexPaths, point, menuElements)
276 | }
277 | )
278 | }
279 | return new
280 | }
281 |
282 | // MARK: - Prefetching
283 |
284 | public func prefetching(
285 | prefetchItems: ((_ indexPaths: [IndexPath]) -> Void)? = nil,
286 | cancelPrefetchingItems: ((_ indexPaths: [IndexPath]) -> Void)? = nil
287 | ) -> CollectionView {
288 | var new = self
289 | new.prefetchItemsHandler = prefetchItems
290 | new.cancelPrefetchingHandler = cancelPrefetchingItems
291 | return new
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/Sources/CollectionView/CollectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionView.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 9/25/23.
6 | //
7 |
8 | import SwiftUI
9 | import OrderedCollections
10 |
11 | public struct CollectionView
12 | where Section: Sendable & Hashable, Item: Sendable & Hashable, Cell: UICollectionViewCell, CollectionLayout: UICollectionViewLayout {
13 |
14 | public typealias ItemCollection = OrderedDictionary
15 | public typealias CellRegistration = UICollectionView.CellRegistration
16 | typealias DataSource = UICollectionViewDiffableDataSource
17 | typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot
18 |
19 | private var singleSelection: Bool
20 | private var multipleSelection: Bool
21 |
22 | /// The data for populating the list.
23 | @Binding internal var data: ItemCollection
24 |
25 | /// A binding to a set that represents selected items.
26 | @Binding internal var selection: Set-
27 |
28 | /// The layout object to use for organizing items.
29 | internal var layout: CollectionLayout
30 |
31 | internal var cellRegistration: CellRegistration
32 | /// A closure for creating a [`UIContentConfiguration`](https://developer.apple.com/documentation/uikit/uicontentconfiguration) for each item's cell.
33 | // internal var contentConfiguration: (IndexPath, Item) -> ContentConfiguration
34 |
35 | /// An optional closure for creating a [`UIBackgroundConfiguration`](https://developer.apple.com/documentation/uikit/uibackgroundconfiguration) for each item's cell.
36 | // internal var backgroundConfiguration: ((IndexPath, Item) -> UIBackgroundConfiguration)?
37 | /// An optional closure for configuring properties of each item's cell.
38 | ///
39 | /// One possible use can be to set the cell's [`configurationUpdateHandler`] (https://developer.apple.com/documentation/uikit/uicollectionviewcell/3751733-configurationupdatehandler) property.
40 | // public private(set) var cellConfigurationHandler: ((Cell, IndexPath, Item) -> Void)?
41 |
42 | // MARK: - Standard Init, Multiple Select
43 |
44 | /// Creates a collection view that allows users to select multiple items.
45 | ///
46 | /// If you'd like to allow multiple selection, but don't need to keep track of the selections, use `.constant([])` as input for `selection`.
47 | /// - Parameters:
48 | /// - data: The data for populating the list.
49 | /// - selection: A binding to a set that represents selected items.
50 | /// - layout: The layout object to use for organizing items.
51 | /// - cellType: A subclass of `UICollectionViewCell` that the collection view should use. It defaults to `UICollectionViewCell`.
52 | /// - cellRegistrationHandler: A closure that handles the cell registration and configuration.
53 | public init(
54 | _ data: Binding,
55 | selection: Binding>,
56 | layout: CollectionLayout,
57 | cellType: Cell.Type = UICollectionViewCell.self,
58 | cellRegistrationHandler: @escaping CellRegistration.Handler
59 | ) {
60 | self._data = data
61 | self._selection = selection
62 | self.singleSelection = true
63 | self.multipleSelection = true
64 | self.layout = layout
65 | self.cellRegistration = .init(handler: cellRegistrationHandler)
66 | }
67 |
68 | // MARK: - Standard Init, Single/No Select
69 |
70 | /// Creates a collection view that optionally allows users to select a single item.
71 | ///
72 | /// If you'd like to allow single selection, but don't need to keep track of the selection, use `.constant(nil)` as input for `selection`.
73 | /// - Parameters:
74 | /// - data: The data for populating the list.
75 | /// - selection: A binding to a selected value, if provided. Otherwise, no selection will be allowed.
76 | /// - layout: The layout object to use for organizing items.
77 | /// - cellType: A subclass of `UICollectionViewCell` that the collection view should use. It defaults to `UICollectionViewCell`.
78 | /// - cellRegistrationHandler: A closure that handles the cell registration and configuration.
79 | public init(
80 | _ data: Binding,
81 | selection: Binding
- ? = nil,
82 | layout: CollectionLayout,
83 | cellType: Cell.Type = UICollectionViewCell.self,
84 | cellRegistrationHandler: @escaping CellRegistration.Handler
85 | ) {
86 | self._data = data
87 |
88 | if let selection {
89 | self._selection = .init(get: {
90 | if let item = selection.wrappedValue { [item] } else { [] }
91 | }, set: { selectedItems in
92 | selection.wrappedValue = selectedItems.first
93 | })
94 | } else {
95 | self._selection = .constant([])
96 | }
97 |
98 | self.singleSelection = selection != nil
99 | self.multipleSelection = false
100 | self.layout = layout
101 | self.cellRegistration = .init(handler: cellRegistrationHandler)
102 | }
103 |
104 | // MARK: - View Modifier Properties
105 |
106 | // MARK: Misc Properties
107 |
108 | internal var collectionViewBackgroundColor: Color? = nil
109 |
110 | // MARK: Callback Properties
111 |
112 | public typealias CollectionViewBoolCallback = (UICollectionView, _ indexPath: IndexPath) -> Bool
113 | public typealias CollectionViewVoidCallback = (UICollectionView, _ indexPath: IndexPath) -> Void
114 |
115 | // Single Selection
116 |
117 | internal var didSelectItemHandler: CollectionViewVoidCallback? = nil
118 | internal var shouldSelectItemHandler: CollectionViewBoolCallback? = nil
119 | internal var shouldDeselectItemHandler: CollectionViewBoolCallback? = nil
120 |
121 | // Multiple Selection
122 |
123 | internal var shouldBeginMultipleSelectionInteractionHandler: CollectionViewBoolCallback? = nil
124 | internal var didBeginMultipleSelectionInteractionHandler: CollectionViewVoidCallback? = nil
125 | internal var didEndMultipleSelectionInteractionHandler: (() -> Void)? = nil
126 |
127 | // Highlighting
128 |
129 | internal var shouldHighlightItemHandler: CollectionViewBoolCallback? = nil
130 | internal var didHighlightItemHandler: CollectionViewVoidCallback? = nil
131 | internal var didUnhighlightItemHandler: CollectionViewVoidCallback? = nil
132 |
133 | // Displaying Cells
134 |
135 | internal var willDisplayCellHandler: ((_ cell: UICollectionViewCell, _ indexPath: IndexPath) -> Void)? = nil
136 | internal var didEndDisplayingCellHandler: ((_ cell: UICollectionViewCell, _ indexPath: IndexPath) -> Void)? = nil
137 |
138 | // Context Menu
139 |
140 | internal var willDisplayContextMenu: ((_ configuration: UIContextMenuConfiguration, UIContextMenuInteractionAnimating?) -> Void)? = nil
141 | internal var willEndContextMenuInteraction: ((_ configuration: UIContextMenuConfiguration, UIContextMenuInteractionAnimating?) -> Void)? = nil
142 | internal var willPerformPreviewAction: ((_ configuration: UIContextMenuConfiguration, UIContextMenuInteractionAnimating?) -> Void)? = nil
143 | internal var contextMenuConfigHandler: ((_ indexPaths: [IndexPath], _ point: CGPoint) -> UIContextMenuConfiguration?)? = nil
144 |
145 | // Prefetch
146 |
147 | internal var prefetchItemsHandler: ((_ indexPaths: [IndexPath]) -> Void)? = nil
148 | internal var cancelPrefetchingHandler: ((_ indexPaths: [IndexPath]) -> Void)? = nil
149 | }
150 |
151 | // MARK: - Convenience Functions
152 |
153 | extension CollectionView {
154 | private func uiColor(from color: Color?, in context: Context) -> UIColor? {
155 | guard let color else { return nil }
156 | if #available(iOS 17.0, *) {
157 | return UIColor(cgColor: color.resolve(in: context.environment).cgColor)
158 | } else {
159 | return if let cgColor = color.cgColor {
160 | UIColor(cgColor: cgColor)
161 | } else {
162 | nil
163 | }
164 | }
165 | }
166 | }
167 |
168 | // MARK: - UIViewRepresentable
169 |
170 | extension CollectionView: UIViewRepresentable {
171 | public func makeUIView(context: Context) -> UICollectionView {
172 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
173 |
174 | context.coordinator.setUpCollectionView(collectionView)
175 |
176 | collectionView.delegate = context.coordinator
177 | collectionView.prefetchDataSource = context.coordinator
178 | collectionView.allowsSelection = singleSelection
179 | collectionView.allowsMultipleSelection = multipleSelection
180 |
181 | return collectionView
182 | }
183 |
184 | public func updateUIView(_ uiView: UICollectionView, context: Context) {
185 | // Updates background color
186 | if uiView.backgroundColor != uiColor(from: collectionViewBackgroundColor, in: context) {
187 | uiView.backgroundColor = uiColor(from: collectionViewBackgroundColor, in: context)
188 | }
189 |
190 | // Check if this calls after initial loading
191 | updateDataSource(context.coordinator)
192 |
193 | // Update selected cells (if selection is enabled)
194 | if singleSelection || multipleSelection {
195 | let newSelectedIndexPaths = Set(selection
196 | .compactMap { context.coordinator.dataSource.indexPath(for: $0) })
197 | let currentlySelectedIndexPaths = Set(uiView.indexPathsForSelectedItems ?? [])
198 |
199 | if newSelectedIndexPaths != currentlySelectedIndexPaths {
200 | let removed = currentlySelectedIndexPaths.subtracting(newSelectedIndexPaths)
201 | removed.forEach {
202 | uiView.deselectItem(at: $0, animated: true)
203 | }
204 |
205 | let added = newSelectedIndexPaths.subtracting(currentlySelectedIndexPaths)
206 | added.forEach {
207 | uiView.selectItem(at: $0, animated: true, scrollPosition: .centeredVertically)
208 | }
209 | }
210 | }
211 | }
212 |
213 | private func updateDataSource(_ coordinator: Coordinator) {
214 | if let dataSource = coordinator.dataSource {
215 | var snapshot: DataSourceSnapshot = .init()
216 |
217 | snapshot.appendSections(Array(data.keys))
218 | for (section, items) in data {
219 | snapshot.appendItems(items, toSection: section)
220 | }
221 |
222 | // Animate if there were already items added.
223 | dataSource.apply(snapshot, animatingDifferences: !dataSource.snapshot().itemIdentifiers.isEmpty)
224 | }
225 | }
226 |
227 | public func makeCoordinator() -> Coordinator {
228 | Coordinator(self)
229 | }
230 | }
231 |
232 | private struct TestView: View {
233 | enum Sections: Hashable {
234 | case main
235 | }
236 |
237 | @State
238 | var items: OrderedDictionary = [Sections.main: ["String 1", "String 2", "String 3", "String 4"]]
239 |
240 | var body: some View {
241 | NavigationView {
242 | CollectionView(
243 | $items,
244 | selection: .constant([]),
245 | listAppearance: .sidebar) { indexPath, state, string in
246 | if indexPath.item > 0 {
247 | var config = UIListContentConfiguration.sidebarCell()
248 | config.image = UIImage(systemName: "speaker.wave.3.fill")
249 | config.imageProperties.cornerRadius = 40
250 | config.text = string
251 | config.secondaryText = string
252 | return config
253 | } else {
254 | var config = UIListContentConfiguration.sidebarHeader()
255 | config.text = string
256 | return config
257 | }
258 | } backgroundConfiguration: { indexPath, state, _ in
259 | if indexPath.item > 0 {
260 | .listSidebarCell()
261 | } else {
262 | .listGroupedHeaderFooter()
263 | }
264 | } cellConfigurationHandler: { cell, _, state, _ in
265 |
266 | } listConfigurationHandler: { config in
267 | config.headerMode = .firstItemInSection
268 | // config.backgroundColor = .systemGroupedBackground
269 | }
270 | // .backgroundColor(.systemRed)
271 | .onSelect { _, indexPath in
272 | print(indexPath)
273 | }
274 | .ignoresSafeArea()
275 | .navigationTitle("Test")
276 | .toolbar {
277 | ToolbarItem(placement: .primaryAction) {
278 | Button("Test") {
279 | items[.main]?.append("Test \(items[.main]?.count ?? 0)")
280 | }
281 | }
282 | }
283 | }
284 | }
285 | }
286 |
287 | #Preview("SwiftUI") {
288 | TestView()
289 | }
290 |
--------------------------------------------------------------------------------
/Sources/CollectionView/Extensions/ListLayoutAppearance+DefaultBackgroundColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListLayoutAppearance+DefaultBackgroundColor.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 10/2/23.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UICollectionLayoutListConfiguration.Appearance {
11 | var defaultBackgroundColor: UIColor {
12 | switch self {
13 | case .plain, .sidebarPlain:
14 | .systemBackground
15 | case .grouped, .insetGrouped, .sidebar:
16 | .systemGroupedBackground
17 | @unknown default:
18 | .systemBackground
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/CollectionView/Extensions/OrderedDictionary+IndexPath.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OrderedDictionary+IndexPath.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 9/28/23.
6 | //
7 |
8 | import Foundation
9 | import OrderedCollections
10 |
11 | extension OrderedDictionary where Value: RandomAccessCollection, Value.Index == Int {
12 | public subscript(_ indexPath: IndexPath) -> Value.Element? {
13 | guard let sectionKey = key(for: indexPath.section),
14 | let value = self[sectionKey],
15 | value.count > indexPath.item else { return nil }
16 | return value[indexPath.item]
17 | }
18 | }
19 |
20 | extension OrderedDictionary {
21 | public func key(for index: Int) -> Key? {
22 | guard keys.count > index else { return nil }
23 | return keys[index]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/CollectionView/ResultBuilders/Builder Inits.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Builder Inits.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 2/24/24.
6 | //
7 |
8 | import SwiftUI
9 | import CompositionalLayoutBuilder
10 |
11 | public typealias CollectionViewLayoutHandler = () -> CompositionalLayout
12 |
13 | @available(iOS 16, macCatalyst 16, tvOS 16, visionOS 1, *)
14 | extension CollectionView where Cell == UICollectionViewCell, CollectionLayout == CompositionalLayout.Layout {
15 | public init(
16 | _ data: Binding,
17 | selection: Binding>,
18 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
19 | @CompositionalLayoutBuilder
20 | layout: @escaping CollectionViewLayoutHandler
21 | ) where Content: View {
22 | self.init(
23 | data,
24 | selection: selection,
25 | layout: layout().layout,
26 | cellContent: cellContent
27 | )
28 | }
29 |
30 | public init(
31 | _ data: Binding,
32 | selection: Binding
- ? = nil,
33 | @ViewBuilder cellContent: @escaping (IndexPath, _ state: UICellConfigurationState, _ item: Item) -> Content,
34 | @CompositionalLayoutBuilder
35 | layout: @escaping CollectionViewLayoutHandler
36 | ) where Content: View {
37 | self.init(
38 | data,
39 | selection: selection,
40 | layout: layout().layout,
41 | cellContent: cellContent
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/CollectionView/ResultBuilders/BuilderTesting.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuilderTesting.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 2/24/24.
6 | //
7 |
8 | import SwiftUI
9 | import CompositionalLayoutBuilder
10 | import OrderedCollections
11 |
12 | #if os(iOS)
13 | @available(iOS 16, *)
14 | private struct LayoutBuilderTest: View {
15 | @State
16 | var items: OrderedDictionary = .dummyData
17 |
18 | @State var selection: Testing.Item? = nil
19 |
20 | var body: some View {
21 | CollectionView($items, selection: $selection) { indexPath, state, item in
22 | VStack {
23 | Image(systemName: item.systemImageName)
24 | Text(item.title)
25 | Text(item.subtitle)
26 | }
27 | .font(.body)
28 | .foregroundStyle(state.isSelected ? .blue : .primary)
29 | .disabled(indexPath.item == 0)
30 | .swipeActions {
31 | Button(role: .destructive) {
32 | print("Test")
33 | } label: {
34 | Label("", systemImage: "trash")
35 | }
36 | Button(role: .destructive) {
37 | print("Test")
38 | } label: {
39 | Label("", systemImage: "trash")
40 | }
41 | .tint(.blue)
42 | }
43 | .swipeActions(edge: .leading) {
44 | Button(role: .destructive) {
45 | print("Test")
46 | } label: {
47 | Label("", systemImage: "trash")
48 | }
49 | Button(role: .destructive) {
50 | print("Test")
51 | } label: {
52 | Label("", systemImage: "trash")
53 | }
54 | .tint(.blue)
55 | }
56 | .padding()
57 | .background(.yellow)
58 | } layout: {
59 | CompositionalSection {
60 | CompositionalGroup(.horizontal, width: .fractionalWidth(1), height: .absolute(200)) {
61 | CompositionalGroup(.vertical, width: .fractionalWidth(0.75), height: .fractionalHeight(1)) {
62 | CompositionalItem(width: .fractionalWidth(1), height: .fractionalHeight(0.5))
63 | .repeating(count: 2)
64 | }
65 |
66 | CompositionalItem(width: .fractionalWidth(0.25), height: .fractionalHeight(1))
67 | }
68 | }
69 |
70 | CompositionalSection {
71 | CompositionalGroup(.horizontal, width: .fractionalWidth(1), height: .absolute(100)) {
72 | CompositionalItem(width: .fractionalWidth(0.5), height: .fractionalHeight(1))
73 | }
74 | }
75 | .orthogonalScrollingBehavior(.continuous)
76 | }
77 | // .backgroundStyle(.green)
78 | }
79 | }
80 |
81 | @available(iOS 16, *)
82 | #Preview {
83 | LayoutBuilderTest()
84 | }
85 | #endif
86 |
--------------------------------------------------------------------------------
/Sources/CollectionView/Testing/HostingInitTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HostingInitTest.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 10/1/23.
6 | //
7 |
8 | import SwiftUI
9 | import OrderedCollections
10 |
11 | @available(iOS 16, *)
12 | struct HostingInitTest: View {
13 | @State
14 | var items: OrderedDictionary = .dummyData
15 |
16 | @State var selection: Testing.Item? = nil
17 |
18 | var body: some View {
19 | CollectionView(
20 | $items,
21 | selection: $selection,
22 | layout: compositionalLayout(),
23 | cellBackground: LinearGradient(colors: [.blue, .green], startPoint: .topLeading, endPoint: .bottomTrailing)) { indexPath, state, item in
24 | VStack {
25 | Image(systemName: item.systemImageName)
26 | Text(item.title)
27 | Text(item.subtitle)
28 | }
29 | .font(state.isSelected ? .title : .body)
30 | .disabled(indexPath.item == 0)
31 | .swipeActions {
32 | Button(role: .destructive) {
33 | print("Test")
34 | } label: {
35 | Label("", systemImage: "trash")
36 | }
37 | Button(role: .destructive) {
38 | print("Test")
39 | } label: {
40 | Label("", systemImage: "trash")
41 | }
42 | .tint(.blue)
43 | }
44 | .swipeActions(edge: .leading) {
45 | Button(role: .destructive) {
46 | print("Test")
47 | } label: {
48 | Label("", systemImage: "trash")
49 | }
50 | Button(role: .destructive) {
51 | print("Test")
52 | } label: {
53 | Label("", systemImage: "trash")
54 | }
55 | .tint(.blue)
56 | }
57 | // } cellBackground: { _, state, _ in
58 | // Text("BACKGROUND")
59 | // .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
60 | // .background(state.isSwiped ? .orange : .yellow)
61 | } cellConfigurationHandler: { cell, indexPath, _, _ in
62 | if indexPath.item == 1 {
63 |
64 | }
65 | }
66 | .backgroundStyle(BackgroundStyle.background)
67 | // .itemSelection(shouldSelectItem: { _, indexPath in
68 | // indexPath.item != 0
69 | // })
70 | .ignoresSafeArea()
71 | .toolbar {
72 | ToolbarItem(placement: .primaryAction) {
73 | Button("Select First Item") {
74 | selection = items[.section1]?.first
75 | }
76 | }
77 | }
78 | }
79 |
80 | func listLayout() -> UICollectionViewLayout {
81 | return UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
82 | }
83 |
84 | func compositionalLayout() -> UICollectionViewCompositionalLayout {
85 | let layout = UICollectionViewCompositionalLayout {
86 | (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
87 |
88 | let leadingItem = NSCollectionLayoutItem(
89 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7),
90 | heightDimension: .fractionalHeight(1.0)))
91 | leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
92 |
93 | let trailingItem = NSCollectionLayoutItem(
94 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
95 | heightDimension: .fractionalHeight(0.3)))
96 | trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
97 | let trailingGroup = NSCollectionLayoutGroup.vertical(
98 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
99 | heightDimension: .fractionalHeight(1.0)),
100 | subitem: trailingItem, count: 2)
101 |
102 | let nestedGroup = NSCollectionLayoutGroup.horizontal(
103 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
104 | heightDimension: .fractionalHeight(0.4)),
105 | subitems: [leadingItem, trailingGroup])
106 | let section = NSCollectionLayoutSection(group: nestedGroup)
107 | section.orthogonalScrollingBehavior = .continuous
108 | return section
109 |
110 | }
111 | return layout
112 | }
113 | }
114 |
115 | @available(iOS 16, *)
116 | #Preview {
117 | NavigationView {
118 | HostingInitTest()
119 | .navigationTitle("Test")
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/CollectionView/Testing/Initializer Testing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Initializer Testing.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 10/1/23.
6 | //
7 |
8 | import Foundation
9 | import OrderedCollections
10 |
11 | enum Testing {
12 | enum Sections: String, Hashable {
13 | case section1
14 | case section2
15 | }
16 |
17 | struct Item: Hashable {
18 | var title: String
19 | var subtitle: String
20 | var systemImageName: String
21 | }
22 | }
23 |
24 | extension OrderedDictionary where Key == Testing.Sections, Value == [Testing.Item] {
25 | internal static var dummyData: Self {
26 | [
27 | .section1: [
28 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
29 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
30 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
31 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
32 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill")
33 | ],
34 | .section2: [
35 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
36 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "books.vertical.fill"),
37 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
38 | Testing.Item(title: String(UUID().uuidString.prefix(8)), subtitle: String(UUID().uuidString.prefix(8)), systemImageName: "trash.fill"),
39 | ]
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/CollectionView/Testing/ListInitTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListInitTest.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 10/2/23.
6 | //
7 |
8 | import SwiftUI
9 | import OrderedCollections
10 |
11 | struct ListInitTest: View {
12 | @State
13 | var items: OrderedDictionary = .dummyData
14 |
15 | @State var selection: Testing.Item? = nil
16 |
17 | var body: some View {
18 | CollectionView(
19 | $items,
20 | selection: $selection,
21 | listAppearance: .insetGrouped) { indexPath, state, item in
22 | let isHeader = indexPath.item == 0
23 | var contentConfig: UIListContentConfiguration
24 | if isHeader {
25 | contentConfig = UIListContentConfiguration.groupedHeader()
26 | } else {
27 | contentConfig = UIListContentConfiguration.valueCell()
28 | contentConfig.secondaryText = item.subtitle
29 | }
30 |
31 | contentConfig.text = item.title
32 | contentConfig.image = UIImage(systemName: item.systemImageName)
33 | contentConfig.secondaryTextProperties.color = .label
34 | contentConfig.imageProperties.cornerRadius = 10
35 |
36 |
37 | return contentConfig
38 | } backgroundConfiguration: { indexPath, state, item in
39 | if indexPath.item == 0 {
40 | UIBackgroundConfiguration.listGroupedHeaderFooter()
41 | } else {
42 | UIBackgroundConfiguration.listGroupedCell()
43 | }
44 | } cellConfigurationHandler: { cell, indexPath, state, _ in
45 | // if indexPath.item == 1 {
46 | cell.indentationLevel = indexPath.item
47 | // }
48 | } listConfigurationHandler: { config in
49 | config.headerMode = .firstItemInSection
50 | }
51 | .itemSelection(shouldSelectItem: { _, indexPath in
52 | indexPath.item != 0
53 | })
54 | .ignoresSafeArea()
55 | .toolbar {
56 | ToolbarItem(placement: .primaryAction) {
57 | Button("Select First Item") {
58 | selection = items[.section1]?[1]
59 | }
60 | }
61 | }
62 | }
63 |
64 | func listLayout() -> UICollectionViewLayout {
65 | return UICollectionViewCompositionalLayout.list(using: .init(appearance: .insetGrouped))
66 | }
67 | }
68 |
69 | @available(iOS 16, *)
70 | #Preview {
71 | NavigationStack {
72 | ListInitTest()
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/CollectionView/Testing/StandardInitMultipleSelectTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardInitMultipleSelectTest.swift
3 | //
4 | //
5 | // Created by Edon Valdman on 9/28/23.
6 | //
7 |
8 | import SwiftUI
9 | import OrderedCollections
10 |
11 | struct StandardInitMultipleSelectTest: View {
12 | @State
13 | var items: OrderedDictionary = .dummyData
14 |
15 | @State var selection: Set = []
16 |
17 | var body: some View {
18 | CollectionView(
19 | $items,
20 | selection: $selection,
21 | layout: layout()) { cell, indexPath, item in
22 | cell.contentConfiguration = {
23 | let isHeader = indexPath.item == 0
24 | var contentConfig: UIListContentConfiguration
25 | if isHeader {
26 | contentConfig = UIListContentConfiguration.groupedHeader()
27 | } else {
28 | contentConfig = UIListContentConfiguration.valueCell()
29 | contentConfig.secondaryText = item.subtitle
30 | }
31 |
32 | contentConfig.text = item.title
33 | contentConfig.image = UIImage(systemName: item.systemImageName)
34 | contentConfig.secondaryTextProperties.color = .label
35 | contentConfig.imageProperties.cornerRadius = 10
36 | contentConfig.imageProperties.preferredSymbolConfiguration = .init(font: .preferredFont(forTextStyle: .largeTitle), scale: .default)
37 | return contentConfig
38 | }()
39 |
40 | cell.backgroundConfiguration = {
41 | if indexPath.item == 0 {
42 | UIBackgroundConfiguration.listGroupedHeaderFooter()
43 | } else {
44 | UIBackgroundConfiguration.listGroupedCell()
45 | }
46 | }()
47 | }
48 | .itemSelection(shouldSelectItem: { _, indexPath in
49 | indexPath.item != 0
50 | })
51 | // .contextMenu(menuIdentifier: nil, swiftUIProvider: { indexPaths, point in
52 | // guard let firstIndexPath = indexPaths.first,
53 | // let item = self.items[firstIndexPath] else { return nil }
54 | //
55 | // return Text(item.title)
56 | // }, actionProvider: { indexPaths, point, _ in
57 | // return nil
58 | // })
59 | // .background(.green)
60 | // .backgroundColor(Color.yellow)
61 | .ignoresSafeArea()
62 | .toolbar {
63 | ToolbarItem(placement: .primaryAction) {
64 | Button("Remove Random") {
65 | guard let random = selection.randomElement() else { return }
66 | selection.remove(random)
67 | }
68 | }
69 | }
70 | // .onChange(of: selection) { newValue in
71 | // print(newValue.map(\.title))
72 | // }
73 | }
74 |
75 | func layout() -> UICollectionViewCompositionalLayout {
76 | var listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
77 | listConfig.headerMode = .firstItemInSection
78 | // listConfig.backgroundColor = .clear
79 | return .list(using: listConfig)
80 | }
81 | }
82 |
83 | @available(iOS 17.0, *)
84 | #Preview {
85 | NavigationView {
86 | StandardInitMultipleSelectTest()
87 | // .navigationTitle("Test")
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyCollectedDataTypes
6 |
7 | NSPrivacyTrackingDomains
8 |
9 | NSPrivacyTracking
10 |
11 | NSPrivacyAccessedAPITypes
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Tests/CollectionViewTests/CollectionViewTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | //@testable import CollectionView
3 |
4 | final class CollectionViewTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
|