Uicollectionview and Swiftui

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.

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()
}
}

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)
}
}

UICollectionView with SwiftUI + Drag and drop reordering possible?

Just use UICollectionViewDragDelegate and UICollectionViewDropDelegate to drag and drop cell views inside UICollectionView. It works perfectly. Here is the sample code...

struct ContentView: View {
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var body: some View {
GeometryReader { proxy in
GridView(self.numbers, proxy: proxy) { number in
Image("image\(number)")
.resizable()
.scaledToFill()
}
}
}
}
struct GridView<CellView: View>: UIViewRepresentable {
let cellView: (Int) -> CellView
let proxy: GeometryProxy
var numbers: [Int]
init(_ numbers: [Int], proxy: GeometryProxy, @ViewBuilder cellView: @escaping (Int) -> CellView) {
self.proxy = proxy
self.cellView = cellView
self.numbers = numbers
}
func makeUIView(context: Context) -> UICollectionView {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0

let collectionView = UICollectionView(frame: UIScreen.main.bounds, collectionViewLayout: layout)
collectionView.backgroundColor = .white
collectionView.register(GridCellView.self, forCellWithReuseIdentifier: "CELL")

collectionView.dragDelegate = context.coordinator //to drag cell view
collectionView.dropDelegate = context.coordinator //to drop cell view

collectionView.dragInteractionEnabled = true
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.contentInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
return collectionView
}
func updateUIView(_ uiView: UICollectionView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDragDelegate, UICollectionViewDropDelegate {
var parent: GridView
init(_ parent: GridView) {
self.parent = parent
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return parent.numbers.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL", for: indexPath) as! GridCellView
cell.backgroundColor = .clear
cell.cellView.rootView = AnyView(parent.cellView(parent.numbers[indexPath.row]).fixedSize())
return cell
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: ((parent.proxy.frame(in: .global).width - 8) / 3), height: ((parent.proxy.frame(in: .global).width - 8) / 3))
}

//Provides the initial set of items (if any) to drag.
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = self.parent.numbers[indexPath.row]
let itemProvider = NSItemProvider(object: String(item) as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}

//Tells your delegate that the position of the dragged data over the collection view changed.
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UICollectionViewDropProposal(operation: .forbidden)
}

//Tells your delegate to incorporate the drop data into the collection view.
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
var destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationIndexPath = IndexPath(item: row - 1, section: 0)
}
if coordinator.proposal.operation == .move {
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
}
private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
if let item = coordinator.items.first, let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates({
self.parent.numbers.remove(at: sourceIndexPath.item)
self.parent.numbers.insert(item.dragItem.localObject as! Int, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: nil)
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
}
}
}
class GridCellView: UICollectionViewCell {
public var cellView = UIHostingController(rootView: AnyView(EmptyView()))
public override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
contentView.addSubview(cellView.view)
cellView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cellView.view.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 5),
cellView.view.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -5),
cellView.view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
cellView.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
])
cellView.view.layer.masksToBounds = true
}
}

You can see the final result here https://media.giphy.com/media/UPuWLauQepwi5Q77PA/giphy.gif
Thanks. X_X

Creating a Grid/ UICollectionView with SwiftUI

The solution to this issue is with the data structure. You need a 2D Array of Listing.

Edit: I am adding the full code with array converter. I have not adjusted the styles to make it picture perfect. But I think everything works.

import SwiftUI

struct ContentView: View {
@ObservedObject var model = DataLoader()
var body: some View {
VStack {
List {
ForEach(self.model.data, id: \.self){ item in
HStack(spacing: 25) {
ForEach(item, id: \.self) { listing in
ListingView(listing: listing)
}
}
}
}
}.padding()
}
}

struct ListingView: View {
@State var listing: Listing
var body: some View {
VStack (alignment: .leading) {
VStack{
HStack(spacing: 5){
Image(listing.images[0]).resizable().scaledToFill()
.frame(height:70).frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(4)

Image(listing.images[1]).resizable()
.scaledToFill()
.frame(height:70).frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(4)
}.frame(minWidth: 0, maxWidth: .infinity)
HStack(spacing: 5){
Image(listing.images[2]).resizable()
.scaledToFill()
.frame(height:70).frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(4)

Image(listing.images[3]).resizable()
.scaledToFill()
.frame(height:70).frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(4)
}.frame(minWidth: 0, maxWidth: .infinity)
}.frame(minWidth: 0, maxWidth: .infinity)
Text(listing.tags + " • " + listing.location).font(.custom("Nolasco Sans", size: 14))
Text(listing.title).font(.custom("Google Sans", size: 16))

}
}
}

struct Listing: Hashable, Decodable {
var id: Int
var title: String
var location: String
var tags: String
var images: [String]
}

class DataLoader: ObservableObject {
@Published var data: [[Listing]] = [[Listing]]()
func loader() {
if let url = URL(string: "https://nikz.in/data.json") {
let req = URLRequest(url: url)
URLSession.shared.dataTask(with: req) { data, res, err in
if let data = data {
do {
let response = try JSONDecoder().decode([Listing].self, from: data)
DispatchQueue.main.async {
self.data = self.to2DArray(input: response, rows: 2)
}
return
} catch {
print("Unable to decode")
}
} else {
print("Data not loading")
}
}.resume()
}
}

func to2DArray<T>(input: [T], rows: Int) -> [[T]] {
let columns = Int(ceil(Double(input.count)/Double(rows)))
print(Double(input.count)/Double(rows))
var output = [[T]]()
for column in 0..<columns {
var temp: [T] = [T]()
for row in 0..<rows {
let id = (column*rows)+row
if id>input.count-1 { break }
temp.append(input[id])
}
output.append(temp)
}
return output
}

init() {
loader()
}
}

How to set a SwiftUI view as a cell to a CollectionView

This is a pure SwiftUI solution.
It will wrap what ever view you give it and give you the effect you want.
Let me know if it works for you.

struct WrappedGridView: View {
let views: [WrappedGridViewHolder]
var showsIndicators = false
var completion:(Int)->Void = {x in}
var body: some View {
GeometryReader { geometry in
ScrollView(showsIndicators: showsIndicators) {
self.generateContent(in: geometry)
}
}
}

init(views: [AnyView], showsIndicators: Bool = false, completion: @escaping (Int)->Void = {val in}) {
self.showsIndicators = showsIndicators
self.views = views.map { WrappedGridViewHolder(view: $0) }
self.completion = completion
}

private func generateContent(in g: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero

return
ZStack(alignment: .topLeading) {
ForEach(views) { item in
item
.padding(4)
.alignmentGuide(.leading) { d in
if (abs(width - d.width) > g.size.width) {
width = 0
height -= d.height
}
let result = width
if item == self.views.last {
width = 0
} else {
width -= d.width
}
return result
}
.alignmentGuide(.top) { d in
let result = height
if item == self.views.last {
height = 0
}
return result
}
.onTapGesture {
tapped(value: item)
}
}
}
.background(
GeometryReader { r in
Color
.clear
.preference(key: SizePreferenceKey.self, value: r.size)
}
)
}

func tapped(value: WrappedGridViewHolder) {
guard let index = views.firstIndex(of: value) else { assert(false, "This should never happen"); return }
completion(index)
}
}

struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value: inout Value, nextValue: () -> Value) {

}
}

extension WrappedGridView {
struct WrappedGridViewHolder: View, Identifiable, Equatable {
let id = UUID().uuidString
let view: AnyView
var body: some View {
view
}

static func == (lhs: WrappedGridViewHolder, rhs: WrappedGridViewHolder) -> Bool { lhs.id == rhs.id }
}
}


Related Topics



Leave a reply



Submit