Skip to content

Instantly share code, notes, and snippets.

@KazaiMazai
Last active November 11, 2024 02:27
Show Gist options
  • Save KazaiMazai/d9458293c0ef2006bb39958dff624f08 to your computer and use it in GitHub Desktop.
Save KazaiMazai/d9458293c0ef2006bb39958dff624f08 to your computer and use it in GitHub Desktop.
Better SwiftUI wrapper for UICollectionView
import SwiftUI
extension CollectionView {
typealias UIKitCollectionView = CollectionViewWithDataSource<SectionIdentifierType, ItemIdentifierType>
typealias DataSource = UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
typealias Snapshot = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>
typealias UpdateCompletion = () -> Void
}
struct CollectionView<SectionIdentifierType, ItemIdentifierType>
where
SectionIdentifierType: Hashable & Sendable,
ItemIdentifierType: Hashable & Sendable {
private let snapshot: Snapshot
private let configuration: ((UICollectionView) -> Void)
private let cellProvider: DataSource.CellProvider
private let supplementaryViewProvider: DataSource.SupplementaryViewProvider?
private let collectionViewLayout: () -> UICollectionViewLayout
private(set) var collectionViewDelegate: (() -> UICollectionViewDelegate)?
private(set) var animatingDifferences: Bool = true
private(set) var updateCallBack: UpdateCompletion?
init(snapshot: Snapshot,
collectionViewLayout: @escaping () -> UICollectionViewLayout,
configuration: @escaping ((UICollectionView) -> Void) = { _ in },
cellProvider: @escaping DataSource.CellProvider,
supplementaryViewProvider: DataSource.SupplementaryViewProvider? = nil) {
self.snapshot = snapshot
self.configuration = configuration
self.cellProvider = cellProvider
self.supplementaryViewProvider = supplementaryViewProvider
self.collectionViewLayout = collectionViewLayout
}
}
extension CollectionView: UIViewRepresentable {
final class Coordinator {
var delegate: UICollectionViewDelegate?
init(delegate: UICollectionViewDelegate?) {
self.delegate = delegate
}
}
func makeCoordinator() -> Coordinator {
Coordinator(delegate: collectionViewDelegate?())
}
func makeUIView(context: Context) -> UIKitCollectionView {
let collectionView = UIKitCollectionView(
frame: .zero,
collectionViewLayout: collectionViewLayout(),
collectionViewConfiguration: configuration,
cellProvider: cellProvider,
supplementaryViewProvider: supplementaryViewProvider
)
collectionView.delegate = context.coordinator.delegate
return collectionView
}
func updateUIView(_ uiView: UIKitCollectionView,
context: Context) {
uiView.apply(
snapshot,
animatingDifferences: animatingDifferences,
completion: updateCallBack
)
}
}
extension CollectionView {
func animateDifferences(_ animate: Bool) -> Self {
var selfCopy = self
selfCopy.animatingDifferences = animate
return selfCopy
}
func onUpdate(_ perform: (() -> Void)?) -> Self {
var selfCopy = self
selfCopy.updateCallBack = perform
return selfCopy
}
func collectionViewDelegate(_ makeDelegate: @escaping (() -> UICollectionViewDelegate)) -> Self {
var selfCopy = self
selfCopy.collectionViewDelegate = makeDelegate
return selfCopy
}
}
final class CollectionViewDelegateProxy: NSObject, UICollectionViewDelegate {
let didScroll: (UIScrollView) -> Void
let didSelect: (UICollectionView, IndexPath) -> Void
init(didScroll: @escaping (UIScrollView) -> Void,
didSelect: @escaping (UICollectionView, IndexPath) -> Void) {
self.didScroll = didScroll
self.didSelect = didSelect
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
didScroll(scrollView)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
didSelect(collectionView, indexPath)
}
}
import SwiftUI
final class CollectionViewWithDataSource<SectionIdentifierType, ItemIdentifierType>: UICollectionView
where
SectionIdentifierType: Hashable & Sendable,
ItemIdentifierType: Hashable & Sendable {
typealias DataSource = UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
typealias Snapshot = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>
private let cellProvider: DataSource.CellProvider
private let updateQueue: DispatchQueue = DispatchQueue(
label: "com.collectionview.update",
qos: .userInteractive)
private lazy var collectionDataSource: DataSource = {
DataSource(
collectionView: self,
cellProvider: cellProvider
)
}()
init(frame: CGRect,
collectionViewLayout: UICollectionViewLayout,
collectionViewConfiguration: ((UICollectionView) -> Void),
cellProvider: @escaping DataSource.CellProvider,
supplementaryViewProvider: DataSource.SupplementaryViewProvider?) {
self.cellProvider = cellProvider
super.init(frame: frame, collectionViewLayout: collectionViewLayout)
collectionViewConfiguration(self)
collectionDataSource.supplementaryViewProvider = supplementaryViewProvider
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(_ snapshot: Snapshot,
animatingDifferences: Bool = true,
completion: (() -> Void)? = nil) {
updateQueue.async { [weak self] in
self?.collectionDataSource.apply(
snapshot,
animatingDifferences: animatingDifferences,
completion: completion
)
}
}
}
extension UICollectionView.CellRegistration {
static func hosting<Content: View, Item>(
content: @escaping (IndexPath, Item) -> Content) -> UICollectionView.CellRegistration<UICollectionViewCell, Item> {
UICollectionView.CellRegistration { cell, indexPath, item in
cell.contentConfiguration = UIHostingConfiguration {
content(indexPath, item)
}
}
}
}
import SwiftUI
struct ContentView: View {
typealias Item = Int
typealias Section = Int
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
@State var snapshot: Snapshot = {
var initialSnapshot = Snapshot()
initialSnapshot.appendSections([0])
return initialSnapshot
}()
var body: some View {
ZStack(alignment: .bottom) {
CollectionView(
snapshot: snapshot,
collectionViewLayout: collectionViewLayout,
configuration: collectionViewConfiguration,
cellProvider: cellProvider,
supplementaryViewProvider: supplementaryProvider
)
.padding()
Button(
action: {
let itemsCount = snapshot.numberOfItems(inSection: 0)
snapshot.appendItems([itemsCount + 1], toSection: 0)
}, label: {
Text("Add More Items")
}
)
}
}
}
extension ContentView {
func collectionViewLayout() -> UICollectionViewLayout {
UICollectionViewFlowLayout()
}
func collectionViewConfiguration(_ collectionView: UICollectionView) {
collectionView.register(
UICollectionViewCell.self,
forCellWithReuseIdentifier: "CellReuseId"
)
collectionView.register(
UICollectionReusableView.self,
forSupplementaryViewOfKind: "KindOfHeader",
withReuseIdentifier: "SupplementaryReuseId"
)
}
func cellProvider(_ collectionView: UICollectionView,
indexPath: IndexPath,
item: Item) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "CellReuseId",
for: indexPath
)
cell.backgroundColor = .red
return cell
}
func supplementaryProvider(_ collectionView: UICollectionView,
elementKind: String,
indexPath: IndexPath) -> UICollectionReusableView {
collectionView.dequeueReusableSupplementaryView(
ofKind: elementKind,
withReuseIdentifier: "SupplementaryReuseId",
for: indexPath
)
}
}
import SwiftUI
struct ContentView: View {
typealias Item = Int
typealias Section = Int
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
@State var snapshot: Snapshot = {
var initialSnapshot = Snapshot()
initialSnapshot.appendSections([0])
return initialSnapshot
}()
var body: some View {
ZStack(alignment: .bottom) {
CollectionView(
snapshot: snapshot,
collectionViewLayout: collectionViewLayout,
cellProvider: cellProviderWithRegistration
)
.collectionViewDelegate {
CollectionViewDelegateProxy(didSelect: { collection, index in
appendItemToCollection()
})
}
.padding()
Button(
action: {
appendItemToCollection()
}, label: {
Text("Add More Items")
}
)
}
}
let cellRegistration: UICollectionView.CellRegistration = .hosting { (idx: IndexPath, item: Item) in
Text("\(item)")
}
func appendItemToCollection() {
let itemsCount = snapshot.numberOfItems(inSection: 0)
snapshot.appendItems([itemsCount], toSection: 0)
}
}
extension ContentView {
func collectionViewLayout() -> UICollectionViewLayout {
UICollectionViewFlowLayout()
}
func cellProviderWithRegistration(_ collectionView: UICollectionView,
indexPath: IndexPath,
item: Item) -> UICollectionViewCell {
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration,
for: indexPath,
item: item
)
}
}
@mrciezas
Copy link

Do you have an example to use the delegate please?

@KazaiMazai
Copy link
Author

Do you have an example to use the delegate please?

Hey! I've added a delegate example and fixed a few critical issues. Check this out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment