Skip to content

Instantly share code, notes, and snippets.

@KazaiMazai
Last active August 19, 2024 10:03
Show Gist options
  • Save KazaiMazai/ca9e18f76e02ff9d17c99846ab8cea1c to your computer and use it in GitHub Desktop.
Save KazaiMazai/ca9e18f76e02ff9d17c99846ab8cea1c to your computer and use it in GitHub Desktop.
SwiftUI wrapper for UICollectionView
import UIKit
import SwiftUI
public protocol SectionProtocol: Hashable {
associatedtype Item: Hashable
var items: [Item] { get }
}
extension CollectionView {
public typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>
public typealias SnapShot = NSDiffableDataSourceSnapshot<Section, Item>
public typealias CellProvider = DataSource.CellProvider
public typealias SupplementaryViewProvider =
(SnapShot, UICollectionView, String, IndexPath) -> UICollectionReusableView?
public typealias CollectionViewProvider = () -> UICollectionView
public typealias CollectionViewUpdateCompleteHandler = (UICollectionView) -> Void
}
public struct CollectionView<Section, Item>
where
Section: SectionProtocol,
Section.Item == Item {
private let collectionViewProvider: CollectionViewProvider
private let cellProvider: CellProvider
private let supplementaryViewProvider: SupplementaryViewProvider?
private let updateCompleteHandler: CollectionViewUpdateCompleteHandler?
private var animateCollectionUpdates: Bool = true
private let sections: [Section]
public init(sections: [Section],
collectionViewProvider: @escaping CollectionViewProvider,
cellProvider: @escaping CellProvider,
supplementaryViewProvider: SupplementaryViewProvider? = nil,
updateCompleteHandler: CollectionViewUpdateCompleteHandler? = nil) {
self.collectionViewProvider = collectionViewProvider
self.cellProvider = cellProvider
self.sections = sections
self.supplementaryViewProvider = supplementaryViewProvider
self.updateCompleteHandler = updateCompleteHandler
}
}
extension CollectionView: UIViewRepresentable {
public func makeCoordinator() -> Coordinator {
Coordinator()
}
public func makeUIView(context: UIViewRepresentableContext<CollectionView>) -> UICollectionView {
let collectionView = collectionViewProvider()
let datasource = DataSource(collectionView: collectionView,
cellProvider: cellProvider)
datasource.setSupplementaryViewProvider(with: supplementaryViewProvider)
context.coordinator.datasource = datasource
return collectionView
}
public func updateUIView(_ uiView: UICollectionView,
context: UIViewRepresentableContext<CollectionView>) {
context.coordinator.applySnapshotInBackground(sections: sections,
animated: animateCollectionUpdates) {
updateCompleteHandler?(uiView)
}
}
}
extension UICollectionViewDiffableDataSource where SectionIdentifierType: SectionProtocol,
SectionIdentifierType.Item == ItemIdentifierType {
func setSupplementaryViewProvider(
with provider: CollectionView<SectionIdentifierType, ItemIdentifierType>.SupplementaryViewProvider?) {
guard let provider = provider else {
supplementaryViewProvider = nil
return
}
supplementaryViewProvider = { [weak self] (collecion, kind, idx) in
guard let self = self else {
return nil
}
return provider(self.snapshot(), collecion, kind, idx)
}
}
}
public extension CollectionView {
func animatingDifferences(_ animated: Bool) -> Self {
var selfCopy = self
selfCopy.animateCollectionUpdates = animated
return selfCopy
}
}
import UIKit
import SwiftUI
extension CollectionView {
public class Coordinator: NSObject {
var datasource: DataSource?
private let updateQueue = DispatchQueue(
label: "CollectionView.Coordinator.Update.Queue",
qos: .userInteractive)
func applySnapshotInBackground(sections: [Section],
animated: Bool,
complete: @escaping () -> Void) {
updateQueue.async { [weak self] in
guard let self = self else {
DispatchQueue.main.async {
complete()
}
return
}
self.applySnapshot(sections: sections, animated: animated) {
DispatchQueue.main.async {
complete()
}
}
}
}
}
}
extension CollectionView.Coordinator {
private func applySnapshot(sections: [Section],
animated: Bool,
complete: @escaping () -> Void) {
guard let datasource = self.datasource else {
complete()
return
}
var snapshot = CollectionView.SnapShot()
snapshot.appendSections(sections)
sections.forEach {
snapshot.appendItems($0.items, toSection: $0)
}
datasource.apply(snapshot, animatingDifferences: animated) {
complete()
}
}
}
public protocol ReusableView {
}
public extension ReusableView {
static var reuseIdentifier: String {
"\(self)"
}
}
public class HostingCollectionViewCell<Content: View>: UICollectionViewCell
where Content: ReusableView {
private var hostingController: UIHostingController<Content>?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public static var reuseIdentifier: String {
String(describing: Content.reuseIdentifier)
}
public func set(rootView: Content) {
guard let host = hostingController else {
let host = UIHostingController(rootView: rootView)
let parentController = resolveParentViewController()
parentController?.addChild(host)
addSubview(host.view)
host.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: host.view.leadingAnchor),
trailingAnchor.constraint(equalTo: host.view.trailingAnchor),
topAnchor.constraint(equalTo: host.view.topAnchor),
bottomAnchor.constraint(equalTo: host.view.bottomAnchor)
])
parentController.map { host.didMove(toParent: $0) }
return
}
host.rootView = rootView
host.view.invalidateIntrinsicContentSize()
}
deinit {
hostingController?.willMove(toParent: nil)
hostingController?.view.removeFromSuperview()
hostingController?.removeFromParent()
hostingController = nil
}
}
fileprivate extension UIView {
func resolveParentViewController() -> UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder?.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
@KazaiMazai
Copy link
Author

@KazaiMazai - I have been using your updated implementation of CollectionView and I am running into some issues. 1) The functions CollectionView.animateDifferences(...), .onUpdate(...), and .collectionViewDelegate(...) need to return selfCopy rather than self. 2) The UICollectionViewDelegate is held as a weak reference and is therefore nil upon returning from CollectionView.makeUIView(...). I have worked around these issues by modifying your code specifically for my use (not a general solution). If you would like to resolve the issues, I would be happy to give you feedback on the fix. Thanks for your efforts.

@jafienberg
Hey! Just noticed you reply that I missed a year ago. Thanks for pointing it out. Fixed the issues in the latest revision.

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