How to present a UICollectionView in SwiftUI with UIViewControllerRepresentable
Here is minimal runnable demo. (Note: Cell have to be registered if all is done programmatically)
class MyCell: UICollectionViewCell {
}
struct CollectionView: UIViewRepresentable {
func makeUIView(context: Context) -> UICollectionView {
let view = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
view.backgroundColor = UIColor.clear
view.dataSource = context.coordinator
view.register(MyCell.self, forCellWithReuseIdentifier: "myCell")
return view
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCell
cell.backgroundColor = UIColor.red
return cell
}
}
}
struct TestUICollectionView_Previews: PreviewProvider {
static var previews: some View {
CollectionView()
}
}
UICollectionView and SwiftUI?
One of the possible solutions is to wrap your UICollectionView
into UIViewRepresentable
. See Combining and Creating Views SwiftUI Tutorial, where they wrap the MKMapView
as an example.
By now there isn’t an equivalent of UICollectionView
in the SwiftUI and there’s no plan for it yet. See a discussion under that tweet.
To get more details check the Integrating SwiftUI WWDC video (~8:08).
Update:
Since iOS 14 (beta) we can use Lazy*Stack
to at least achieve the performance of the collection view in the SwiftUI. When it comes to the layout of cells I think we still have to manage it manually on a per-row/per-column basis.
UIViewRepresentable UICollectionView How do you scroll to a certain point when view appears?
You could use ViewControllerRepresentable
and take advantage of the fact that view controllers can override the viewWillAppear
/viewDidAppear
methods where you can write the code that scrolls.
For this, subclass UICollectionViewController
, and move all the collection view related logic there:
class MyCollectionViewController: UICollectionViewController {
var vm = CollectionViewModel()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = UIColor.clear
collectionView.register(ListCell.self, forCellWithReuseIdentifier: "listCell")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let scrollTo = IndexPath(row: 25, section: 0)
collectionView.scrollToItem(at: scrollTo, at: .top, animated: false)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return vm.items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as! ListCell
let item = vm.items[indexPath.row]
cell.item = item
return cell
}
}
With the above in mind, the SwiftUI code simplifies to something like this:
struct CollectionViewRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MyCollectionViewController {
.init(collectionViewLayout: UICollectionViewFlowLayout())
}
func updateUIViewController(_ vc: MyCollectionViewController, context: Context) {
}
}
SwiftUI wrapping UICollectionView and use compositional layout
I've rewritten it into UIViewControllerRepresentable and now it works ok
struct CollectionView<Section: Hashable & CaseIterable, Item: Hashable>: UIViewControllerRepresentable {
// 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 makeUIViewController(context: Context) -> CollectionViewController<Section, Item> {
let controller = CollectionViewController<Section, Item>()
controller.layout = self.layout
controller.snapshotForCurrentState = {
if let snapshot = self.snapshot {
return snapshot()
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(self.sections)
self.sections.forEach { section in
snapshot.appendItems(self.items[section]!, toSection: section)
}
return snapshot
}
controller.content = content
controller.collectionView.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: CollectionViewController<Section, Item>, context: Context) {
uiViewController.updateUI()
}
class Coordinator: NSObject, UICollectionViewDelegate {
// 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)")
}
}
}
struct CollectionView_Previews: PreviewProvider {
enum Section: CaseIterable {
case features
case categories
}
enum Item: Hashable {
case feature(feature: Feature)
case category(category: Category)
}
class Feature: Hashable{
let id: String
let title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: Feature, rhs: Feature) -> Bool {
lhs.id == rhs.id
}
}
class Category: Hashable {
let id: String
let title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: Category, rhs: Category) -> Bool {
lhs.id == rhs.id
}
}
static let items: [Section: [Item]] = {
return [
.features : [
.feature(feature: Feature(id: "1", title: "Feature 1")),
.feature(feature: Feature(id: "2", title: "Feature 2")),
.feature(feature: Feature(id: "3", title: "Feature 3"))
],
.categories : [
.category(category: Category(id: "1", title: "Category 1")),
.category(category: Category(id: "2", title: "Category 2")),
.category(category: Category(id: "3", title: "Category 3"))
]
]
}()
static var previews: some View {
func generateLayout() -> UICollectionViewLayout {
let itemHeightDimension = NSCollectionLayoutDimension.absolute(44)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: itemHeightDimension)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: itemHeightDimension)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
return CollectionView(layout: generateLayout(), sections: [.features], items: items, snapshot: {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases)
items.forEach { (section, items) in
snapshot.appendItems(items, toSection: section)
}
return snapshot
}) { (indexPath, item) -> AnyView in
switch item {
case .feature(let item):
return AnyView(Text("Feature \(item.title)"))
case .category(let item):
return AnyView(Text("Category \(item.title)"))
}
}
}
}
class CollectionViewController<Section, Item>: UIViewController
where Section : Hashable & CaseIterable, Item : Hashable {
var layout: UICollectionViewLayout! = nil
var snapshotForCurrentState: (() -> NSDiffableDataSourceSnapshot<Section, Item>)! = nil
var content: ((_ indexPath: IndexPath, _ item: Item) -> AnyView)! = nil
lazy var dataSource: UICollectionViewDiffableDataSource<Section, Item> = {
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: cellProvider)
return dataSource
}()
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
collectionView.backgroundColor = .clear
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
configureDataSource()
}
}
extension CollectionViewController {
private func configureCollectionView() {
view.addSubview(collectionView)
collectionView.register(HostingControllerCollectionViewCell<AnyView>.self, forCellWithReuseIdentifier: HostingControllerCollectionViewCell<AnyView>.reuseIdentifier)
}
private func configureDataSource() {
// load initial data
let snapshot : NSDiffableDataSourceSnapshot<Section, Item> = snapshotForCurrentState()
dataSource.apply(snapshot, animatingDifferences: false)
}
private 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(content(indexPath, item))
return cell
}
}
extension CollectionViewController {
func updateUI() {
let snapshot : NSDiffableDataSourceSnapshot<Section, Item> = snapshotForCurrentState()
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Presenting UIDocumentInteractionController with UIViewControllerRepresentable in SwiftUI
Here is possible approach to integrate UIDocumentInteractionController
for usage from SwiftUI view.
Full-module code. Tested with Xcode 11.2 / iOS 13.2
import SwiftUI
import UIKit
struct DocumentPreview: UIViewControllerRepresentable {
private var isActive: Binding<Bool>
private let viewController = UIViewController()
private let docController: UIDocumentInteractionController
init(_ isActive: Binding<Bool>, url: URL) {
self.isActive = isActive
self.docController = UIDocumentInteractionController(url: url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPreview>) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<DocumentPreview>) {
if self.isActive.wrappedValue && docController.delegate == nil { // to not show twice
docController.delegate = context.coordinator
self.docController.presentPreview(animated: true)
}
}
func makeCoordinator() -> Coordintor {
return Coordintor(owner: self)
}
final class Coordintor: NSObject, UIDocumentInteractionControllerDelegate { // works as delegate
let owner: DocumentPreview
init(owner: DocumentPreview) {
self.owner = owner
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return owner.viewController
}
func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
controller.delegate = nil // done, so unlink self
owner.isActive.wrappedValue = false // notify external about done
}
}
}
// Demo of possible usage
struct DemoPDFPreview: View {
@State private var showPreview = false // state activating preview
var body: some View {
VStack {
Button("Show Preview") { self.showPreview = true }
.background(DocumentPreview($showPreview, // no matter where it is, because no content
url: Bundle.main.url(forResource: "example", withExtension: "pdf")!))
}
}
}
struct DemoPDFPreview_Previews: PreviewProvider {
static var previews: some View {
DemoPDFPreview()
}
}
Passing Data from SwiftUI View to UIViewController
UIViewControllerRepresentable
is not a proxy, but wrapper, so everything should be transferred manually, like
struct HomeViewControllerRepresentation: UIViewControllerRepresentable {
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<HomeViewControllerRepresentation>) -> HomeViewController {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "homeViewController") as! HomeViewController
controller.message = self.message
return controller
}
func updateUIViewController(_ uiViewController: HomeViewController, context: UIViewControllerRepresentableContext<HomeViewControllerRepresentation>) {
}
}
Related Topics
Access Class Property from Instance
Fbsdksharephoto Not Sharing Link Alongside Photo Using Swift
Cmlogitem Timestamp: Why So Complicated
Swift Display Image UIimageview
Pass Type to Generic Function and Compare
How to Create an iOS Liveview in Xcode 8/Swift 3
Difference When Declaring Swift Protocol Using Inheritance from Another Protocol or Using Where Self
How to Detect Hash Changes in Wkwebview
Swiftui: What Are The Differences Between Image and UIimage
How to Write to a Variable from Within The Firebase Getdocument Function (Swift)
When How to Start Submitting Apps to The iOS App Store Written Using The Swift Programming Language
iOS 16 Swiftui List Background
How to Access The Firebase Topics a User Is Subscribed To
Swift Combine How Set<Anycancellable> Works
Get Path of a File in a Data Set Located in Assets.Xcassets
Negative Arrayslice: Index Is Out of Range
How to Check If Optional Is Not Nil and Property Is True in One Expression