Skip to content

Instantly share code, notes, and snippets.

@KazaiMazai
Last active May 21, 2023 02:49
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
}
}
@hoangnam714
Copy link

Can you give me an example of your use of this Collection view?

@KazaiMazai
Copy link
Author

Can you give me an example of your use of this Collection view?

Howdy! You can consider something like this:

struct FancyItem: Hashable {
    let id: Int
    let title: String
}

struct FancySection: SectionProtocol {
    let id: Int
    let items: [FancyItem]
}



struct FancyView: View {
    let sections:  [
        FancySection(
            id: 0, 
            items: [
                FancyItem(id: 0, title: "foo")
            ]
         )
    ]

    var body: some View {
        CollectionView<FancySection, FancyItem >(
           sections: sections,
           collectionViewProvider:  { 
               let cv = UICollectionView(...) //instantiate and setup collection view here. Layout and delegate if needed, register cells, etc.
               return cv
           },
           cellProvider: {
                ///cell for row at indexpath-like routines from collection view datatasource API here.
           }
       )
    }
}

BTW, I also recommend to have a look at the post with more details of the above here

Ping me if you need some more help, I will provide a more detailed example

@davidjunker
Copy link

Could you please provide a more detailed example of how to use the Collection View?

@KazaiMazai
Copy link
Author

Could you please provide a more detailed example of how to use the Collection View?

I think I can prepare something but I'm quite busy so it won't be very fast

@KazaiMazai
Copy link
Author

Can you give me an example of your use of this Collection view?
Here is an updated version:

https://gist.github.com/KazaiMazai/d9458293c0ef2006bb39958dff624f08

As well as the updated post with some comments:

https://kazaimazai.com/taking-uicollectionview-to-swiftui/

@jafienberg
Copy link

@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.

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