How to Present a UIcollectionview in Swiftui with UIviewcontrollerrepresentable

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)

demo

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.

demo

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



Leave a reply



Submit