I've tried to implement CollectionView in SwiftUI via UIViewRepresentable and use there SwiftUI views as reusable cells but cells doesn't appear at all. It seems that wrapping collection view and using compositional layout is not possible in SwiftUI, or maybe I am missing something.
I know I can implement this CollectionView completely in UIKit and just wrap all view controller but what if i want to use SwiftUI views as cells of UICollectionView?
When I am using UICollectionViewFlowLayout() views appear correctly
import SwiftUI
import UIKit
struct CollectionView<Section: Hashable & CaseIterable, Item: Hashable>: UIViewRepresentable {
// MARK: - Properties
let layout: UICollectionViewLayout
let sections: [Section]
let items: [Section: [Item]]
// MARK: - Actions
let snapshot: (() -> NSDiffableDataSourceSnapshot<Section, Item>)?
let content: (_ indexPath: IndexPath, _ item: Item) -> AnyView
// MARK: - Init
init(layout: UICollectionViewLayout,
sections: [Section],
items: [Section: [Item]],
snapshot: (() -> NSDiffableDataSourceSnapshot<Section, Item>)? = nil,
@ViewBuilder content: @escaping (_ indexPath: IndexPath, _ item: Item) -> AnyView) {
self.layout = layout
self.sections = sections
self.items = items
self.snapshot = snapshot
self.content = content
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func generateLayout() -> UICollectionViewLayout {
let itemHeightDimension = NSCollectionLayoutDimension.absolute(44)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeightDimension)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeightDimension)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
func makeUIView(context: Context) -> UICollectionView {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
collectionView.backgroundColor = .clear
collectionView.delegate = context.coordinator
collectionView.register(HostingControllerCollectionViewCell<AnyView>.self, forCellWithReuseIdentifier: HostingControllerCollectionViewCell<AnyView>.reuseIdentifier)
context.coordinator.dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: context.coordinator.cellProvider)
return collectionView
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
context.coordinator.dataSource.apply( (snapshot ?? reloadSnaphot)() , animatingDifferences: true)
}
func reloadSnaphot() -> NSDiffableDataSourceSnapshot<Section, Item> {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Array(items.keys))
items.forEach { (section, items) in
snapshot.appendItems(items, toSection: section)
}
return snapshot
}
class Coordinator: NSObject, UICollectionViewDelegate {
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! = nil
// MARK: - Properties
let parent: CollectionView
// MARK: - Init
init(_ parent: CollectionView) {
self.parent = parent
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Did select item at \(indexPath)")
}
// MARK: - Cell Provider
func cellProvider(collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell? {
print("Providing cell for \(indexPath)")
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HostingControllerCollectionViewCell<AnyView>.reuseIdentifier, for: indexPath) as? HostingControllerCollectionViewCell<AnyView> else {
fatalError("Coult not load cell!")
}
cell.host(parent.content(indexPath, item))
//cell.host(rootView: parent.content(indexPath, item))
return cell
}
}
}
class HostingControllerCollectionViewCell<Content: View> : UICollectionViewCell {
weak var controller: UIHostingController<Content>?
func host(_ view: Content, parent: UIViewController? = nil) {
if let controller = controller {
controller.rootView = view
controller.view.layoutIfNeeded()
} else {
let controller = UIHostingController(rootView: view)
self.controller = controller
controller.view.backgroundColor = .clear
layoutIfNeeded()
parent?.addChild(controller)
contentView.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor)
])
if let parent = parent {
controller.didMove(toParent: parent)
}
controller.view.layoutIfNeeded()
}
}
}