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)
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
What Is Difference Between Urlwithstring and Fileurlwithpath of Nsurl
How to Properly Group a List Fetched from Coredata by Date
Flutter on iOS: Fatal Error: Module 'Cloud_Firestore' Not Found
Corebluetooth: What Is the Lifetime of Unique Uuids
Ckquery from Private Zone Returns Only First 100 Ckrecords from in Cloudkit
Apple Llvm 6.0 Error: Clang Failed with Exit Code -1
iPhone Storage in Tmp Directory
Simple Low-Latency Audio Playback in iOS Swift
Invalid Image Path - No Image Found at the Path. Cfbundleicons Xcode 5
How to Execute Some Code After a Segue Is Done
Uisearchcontroller Persisting After Segue
Why Would a 'Scheduledtimer' Fire Properly When Setup Outside a Block, But Not Within a Block
Custom Back Indicator Image and iOS 11
How to Enumerate All Nodes in a Sprite Kit Scene
How to Convert an Pffile to an Uiimage with Swift