UICollectionView and SwiftUI?
Asked Answered
B

17

65

How to create grid of square items (for example like in iOS Photo Library) with SwiftUI?

I tried this approach but it doesn't work:

var body: some View {
    List(cellModels) { _ in
        Color.orange.frame(width: 100, height: 100)
    }
}

List still has UITableView style:

enter image description here

Belicia answered 5/6, 2019 at 18:43 Comment(1)
It is similar to what you are looking for averyvine.com/blog/programming/2019/06/07/…Dexter
N
37

iOS 18 Update:

The ForEach.init(subviewOf:content:) and Group.init(subviewsOf:transform:) seem to provide the right solution. I highly encourage you to watch this short WWDC video. It highlights all of these new APIs.


iOS 14 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.


Before iOS 14:

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

Neckpiece answered 7/6, 2019 at 7:10 Comment(3)
Beware of the bad performance of Lazy*Stack when used with many rows.Fletcherfletcherism
LazyVGrid for example will not match UICollectionView performance. Very bad performance as of now at least.Jaipur
That's true. very bad performanceSinge
I
45

iOS 14 and Xcode 12

SwiftUI for iOS 14 brings a new and nativ grid view that is easy to use called LazyVGrid: https://developer.apple.com/documentation/swiftui/lazyvgrid

You can start with defining an array of GridItems. GridItems are used to specify layout properties for each column. In this case all GridItems are flexible.

LazyVGrid takes an array of GridItems as its parameter and displays the containing views according to the defined GridItems.

import SwiftUI

struct ContentView: View {
    
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns) {
                ForEach(0...100, id: \.self) { _ in
                    Color.orange.frame(width: 100, height: 100)
                }
            }
        }
    }
}

LazyVGrid in use

Incorporeity answered 24/6, 2020 at 20:44 Comment(3)
In iOS 15 The performance of LazyVGrid is abysmal (even for the simplest example) compared to a UICollectionView doesn't scale with the number of items. It is quite ridiculous that they have WWDC lectures on avoiding hitches and they don't even fix performance issues in their own components.Teresiateresina
I’m running LazyVGrid and performance is bad on iPad as explained here #74249865. UICollectionView with much more complex UI runs like butter. Totally agree with comment above.Jaipur
The performance of Android Jetpack is also abysmal when items are more then a few hunderts. Seems to be an algorithmic problem of declarative gui language at the moment. So stay with UICollectionView and RecyclerViewFeathery
N
37

iOS 18 Update:

The ForEach.init(subviewOf:content:) and Group.init(subviewsOf:transform:) seem to provide the right solution. I highly encourage you to watch this short WWDC video. It highlights all of these new APIs.


iOS 14 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.


Before iOS 14:

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

Neckpiece answered 7/6, 2019 at 7:10 Comment(3)
Beware of the bad performance of Lazy*Stack when used with many rows.Fletcherfletcherism
LazyVGrid for example will not match UICollectionView performance. Very bad performance as of now at least.Jaipur
That's true. very bad performanceSinge
M
29

QGrid is a small library I've created that uses the same approach as SwiftUI's List view, by computing its cells on demand from an underlying collection of identified data:

In its simplest form, QGrid can be used with just this 1 line of code within the body of your View, assuming you already have a custom cell view:

struct PeopleView: View {
  var body: some View {
    QGrid(Storage.people, columns: 3) { GridCell(person: $0) }
  }
}   

struct GridCell: View {
  var person: Person
  var body: some View {
    VStack() {
      Image(person.imageName).resizable().scaledToFit()
      Text(person.firstName).font(.headline).color(.white)
      Text(person.lastName).font(.headline).color(.white)
    }
  }
}

enter image description here


You can also customize the default layout configuration:

struct PeopleView: View {
  var body: some View {
    QGrid(Storage.people,
          columns: 3,
          columnsInLandscape: 4,
          vSpacing: 50,
          hSpacing: 20,
          vPadding: 100,
          hPadding: 20) { person in
            GridCell(person: person)
    }
  }
} 

Please refer to demo GIF and test app within GitHub repo:

https://github.com/Q-Mobile/QGrid

Mcginley answered 19/7, 2019 at 19:8 Comment(9)
I've not tested your library yet, but it looks impressive. Awesome work. I assume this is not horizontally scroll-able, right?Apologetics
@Imthath : This feature is on the Roadmap / TODO list ;-) Shouldn't take too long, so if you'd like, feel free to contribute!Mcginley
Mac target won't compile unfortunately due to trying to use UIDevice.Nitrogenous
@GOR: Please retry with the latest version (v.0.1.3)Mcginley
I can't seem to find how to add the action on item tap. Kind of a bummer.Delaunay
I use this library and I'm thankful for the creators. The comment before mentioned action on tap item. just pass a "Button" or any view with "tapGesture" attached to it. QGrid(viewModel.colorSelectionNames, columns: columnCount) { colorName in Circle() .onTapGesture { self.viewModel.selectedColorName = colorName} }Sterigma
DUUUDE! You saved my life! this is incredible work!Coburg
If I want to select one of them in Collection, how do I know which one is selected, the number of selected oneSchwa
This is fantastic, thank you @KarolKulesza!!Rally
G
25

Thinking in SwiftUI, there is a easy way :

struct MyGridView : View {
var body: some View {
    List() {
        ForEach(0..<8) { _ in
            HStack {
                ForEach(0..<3) { _ in
                    Image("orange_color")
                        .resizable()
                        .scaledToFit()
                }
            }
        }
    }
}

}

SwiftUI enough if you want,you need forgot such as UIColectionView sometimes..

enter image description here

Gravitt answered 18/6, 2019 at 3:30 Comment(5)
Problem with this approach is that it won’t adapt columns according to screen width. Has anyone figured out a solution?Megrims
By calculating UIScreen.main.bounds.width / (itemSize + spacing), you get the number of items that fit in one row. (In the case that all of your horizontal spacings are the same) By putting this in a computed property, you can use it easily in the code example above.Glycoside
The problem with this solution is the fact that it doesn't work for collection views with a horizontal orientation. I.e., it works, but in that case, we lose reusability at all.Fastening
@ManeManero Remove your image frame and set .edgesIgnoringSafeArea(.all)Embellishment
poor performance too. does not scale.Jaipur
U
11

XCode 11.0

After looking for a while I decided that I wanted all the convenience and performance form the UICollectionView. So I implemented the UIViewRepresentable protocol.

This example does not implement the DataSource and has a dummy data: [Int] field on the collection view. You would use a @Bindable var data: [YourData] on the AlbumGridView to automatically reload your view when the data changes.

AlbumGridView can then be used like any other view inside SwiftUI.

Code

class AlbumPrivateCell: UICollectionViewCell {
    private static let reuseId = "AlbumPrivateCell"

    static func registerWithCollectionView(collectionView: UICollectionView) {
        collectionView.register(AlbumPrivateCell.self, forCellWithReuseIdentifier: reuseId)
    }

    static func getReusedCellFrom(collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> AlbumPrivateCell{
        return collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) as! AlbumPrivateCell
    }

    var albumView: UILabel = {
        let label = UILabel()
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(self.albumView)

        albumView.translatesAutoresizingMaskIntoConstraints = false

        albumView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        albumView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
        albumView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
        albumView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

    required init?(coder: NSCoder) {
        fatalError("init?(coder: NSCoder) has not been implemented")
    }
}

struct AlbumGridView: UIViewRepresentable {
    var data = [1,2,3,4,5,6,7,8,9]

    func makeUIView(context: Context) -> UICollectionView {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        collectionView.backgroundColor = .blue
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.dataSource = context.coordinator
        collectionView.delegate = context.coordinator

        AlbumPrivateCell.registerWithCollectionView(collectionView: collectionView)
        return collectionView
    }

    func updateUIView(_ uiView: UICollectionView, context: Context) {
        //
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
        private let parent: AlbumGridView

        init(_ albumGridView: AlbumGridView) {
            self.parent = albumGridView
        }

        // MARK: UICollectionViewDataSource

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            self.parent.data.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let albumCell = AlbumPrivateCell.getReusedCellFrom(collectionView: collectionView, cellForItemAt: indexPath)
            albumCell.backgroundColor = .red

            return albumCell
        }

        // MARK: UICollectionViewDelegateFlowLayout

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            let width = collectionView.frame.width / 3
            return CGSize(width: width, height: width)
        }
    }
}

Screenshot

AlbumGridView Preview

Unto answered 27/9, 2019 at 21:26 Comment(6)
Please use SwiftUI LazyGridView instead of this for >= iOS 14Unto
cellForItemAt method not call for me if i adding AlbumGridView from my view.Splash
@GauravThummar I would heavily looking into Lazy views introduced in WWDC 2020 instead of trying this collection view stuff.Unto
Thanks philipp it works for me once i added it into main view n give specific frame.Splash
@Unto LazyVGrid's performance is terrible for large number of items even in iOS 15Teresiateresina
Don't use LazyVGrid... terrible performance.Jaipur
A
11

We've developed a swift package that provides a fully featured CollectionView for use in SwiftUI.

Find it here: https://github.com/apptekstudios/ASCollectionView

It's designed to be easy to use, but can also make full use of the new UICollectionViewCompositionalLayout for more complex layouts. It supports auto-sizing of cells.

To achieve a grid view you could use it as follows:

import SwiftUI
import ASCollectionView

struct ExampleView: View {
    @State var dataExample = (0 ..< 21).map { $0 }

    var body: some View
    {
        ASCollectionView(data: dataExample, dataID: \.self) { item, _ in
            Color.blue
                .overlay(Text("\(item)"))
        }
        .layout {
            .grid(layoutMode: .adaptive(withMinItemSize: 100),
                  itemSpacing: 5,
                  lineSpacing: 5,
                  itemSize: .absolute(50))
        }
    }
}

See the demo project for examples of far more complex layouts.

Portrait Landscape

Artel answered 31/10, 2019 at 6:18 Comment(3)
That's a piece of an incredible work you've got here.Luddite
This is just crazy impressive.Cosine
This package is super buggy...Teresiateresina
A
7

I've been tackling this problem myself, and by using the source posted above by @Anjali as a base, a well as @phillip, (the work of Avery Vine), I've wrapped a UICollectionView that is functional...ish? It'll display and update a grid as needed. I haven't tried the more customizable views or any other things, but for now, I think it'll do.

I commented my code below, hope it's useful to someone!

First, the wrapper.

struct UIKitCollectionView: UIViewRepresentable {
    typealias UIViewType = UICollectionView

    //This is where the magic happens! This binding allows the UI to update.
    @Binding var snapshot: NSDiffableDataSourceSnapshot<DataSection, DataObject>

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: UIViewRepresentableContext<UIKitCollectionView>) -> UICollectionView {

        //Create and configure your layout flow seperately
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.sectionInsets = UIEdgeInsets(top: 25, left: 0, bottom: 25, right: 0)


        //And create the UICollection View
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)

        //Create your cells seperately, and populate as needed.
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "customCell")

        //And set your datasource - referenced from Avery
        let dataSource = UICollectionViewDiffableDataSource<DataSection, DataObject>(collectionView: collectionView) { (collectionView, indexPath, object) -> UICollectionViewCell? in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customCell", for: indexPath)
            //Do cell customization here
            if object.id.uuidString.contains("D") {
                cell.backgroundColor = .red
            } else {
                cell.backgroundColor = .green
            }


            return cell
        }

        context.coordinator.dataSource = dataSource

        populate(load: [DataObject(), DataObject()], dataSource: dataSource)
        return collectionView
    }

    func populate(load: [DataObject], dataSource: UICollectionViewDiffableDataSource<DataSection, DataObject>) {
        //Load the 'empty' state here!
        //Or any default data. You also don't even have to call this function - I just thought it might be useful, and Avery uses it in their example.

        snapshot.appendItems(load)
        dataSource.apply(snapshot, animatingDifferences: true) {
            //Whatever other actions you need to do here.
        }
    }


    func updateUIView(_ uiView: UICollectionView, context: UIViewRepresentableContext<UIKitCollectionView>) {
        let dataSource = context.coordinator.dataSource
        //This is where updates happen - when snapshot is changed, this function is called automatically.

        dataSource?.apply(snapshot, animatingDifferences: true, completion: {
            //Any other things you need to do here.
        })

    }

    class Coordinator: NSObject {
        var parent: UIKitCollectionView
        var dataSource: UICollectionViewDiffableDataSource<DataSection, DataObject>?
        var snapshot = NSDiffableDataSourceSnapshot<DataSection, DataObject>()

        init(_ collectionView: UIKitCollectionView) {
            self.parent = collectionView
        }
    }
}

Now, the DataProvider class will allow us to access that bindable snapshot and update the UI when we want it to. This class is essential to the collection view updating properly. The models DataSection and DataObject are of the same structure as the one provided by Avery Vine - so if you need those, look there.

class DataProvider: ObservableObject { //This HAS to be an ObservableObject, or our UpdateUIView function won't fire!
    var data = [DataObject]()

    @Published var snapshot : NSDiffableDataSourceSnapshot<DataSection, DataObject> = {
        //Set all of your sections here, or at least your main section.
        var snap = NSDiffableDataSourceSnapshot<DataSection, DataObject>()
        snap.appendSections([.main, .second])
        return snap
        }() {
        didSet {
            self.data = self.snapshot.itemIdentifiers
            //I set the 'data' to be equal to the snapshot here, in the event I just want a list of the data. Not necessary.
        }
    }

    //Create any snapshot editing functions here! You can also simply call snapshot functions directly, append, delete, but I have this addItem function to prevent an exception crash.
    func addItems(items: [DataObject], to section: DataSection) {
        if snapshot.sectionIdentifiers.contains(section) {
            snapshot.appendItems(items, toSection: section)
        } else {
            snapshot.appendSections([section])
            snapshot.appendItems(items, toSection: section)
        }
    }
}

And now, the CollectionView, which is going to display our new collection. I made a simple VStack with some buttons so you can see it in action.

struct CollectionView: View {
    @ObservedObject var dataProvider = DataProvider()

    var body: some View {
        VStack {
            UIKitCollectionView(snapshot: $dataProvider.snapshot)
            Button("Add a box") {
                self.dataProvider.addItems(items: [DataObject(), DataObject()], to: .main)
            }

            Button("Append a Box in Section Two") {
                self.dataProvider.addItems(items: [DataObject(), DataObject()], to: .second)
            }

            Button("Remove all Boxes in Section Two") {
                self.dataProvider.snapshot.deleteSections([.second])
            }
        }
    }
}

struct CollectionView_Previews: PreviewProvider {
    static var previews: some View {
        CollectionView()
    }
}

And just for those visual referencers (ye, this is running in the Xcode Preview window):

UICollectionView meets SwiftUI

Altercate answered 16/10, 2019 at 0:52 Comment(0)
E
7

UPDATE: This answer is related to iOS 13. For iOS 14 we have LazyGrids + a lot more stuff and following this answer will not be helpful.

For making a CollectionView without using UIKit, first of all we need an array extension. the array extension will help us chunk our array which we want to make a TableView around. Below is the code for the extension, + 3 examples. To a-little-bit-further understand how this extension works, take a look at this site, which i copied the extension from : https://www.hackingwithswift.com/example-code/language/how-to-split-an-array-into-chunks

    extension Array {
    func chunked(into size: Int) -> [[Element]] {
        return stride(from: 0, to: count, by: size).map {
            Array(self[$0 ..< Swift.min($0 + size, count)])
        }
    }
}

let exampleArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

print(exampleArray.chunked(into: 2)) // prints [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11, 12]]

print(exampleArray.chunked(into: 3)) // prints [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]

print(exampleArray.chunked(into: 5)) // prints [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12]]

Now lets make our SwiftUI view:

struct TestView: View {
    
    let arrayOfInterest = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].chunked(into: 4)
    // = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16], [17, 18]] 
    
    var body: some View {
        
        return VStack {
            
            ScrollView {
                
                VStack(spacing: 16) {
                    
                    ForEach(self.arrayOfInterest.indices, id:\.self) { idx in
                        
                        HStack {
                            
                            ForEach(self.arrayOfInterest[idx].indices, id:\.self) { index in
                                
                                HStack {
                                    
                                    Spacer()
                                    Text("\(self.arrayOfInterest[idx][index])")
                                        .font(.system(size: 50))
                                    .padding(4)
                                        .background(Color.blue)
                                        .cornerRadius(8)
                                    
                                    Spacer()
                                    
                                }
                                
                            }
                            
                        }
                        
                    }
                    
                }
                
            }
            
        }
        
    }
    
}


struct TestView_Preview : PreviewProvider {
    
    static var previews: some View {
            TestView()
        }
    
}

Image of Preview of the code above

Explanation:

First of all we need to make it clear how many columns do we need and put that number into our chunked extension. In my example, we have an array(arrayOfInterest) of numbers from 1 to 18 which we want to show in our view, and i decided that i want my view to have 4 columns, so i chunked it into 4(so 4 is number of our columns).

To make a CollectionView, the most obvious thing is that our CollectionView is a LIST of items, so it should be in a list to make it easily scrollable (NO, DO NOT DO THAT! use a ScrollView instead. i've seen weird behaviours while those 2 foreachs are in a list). after the ScrollView we have 2 ForEach s, the first one enables us to loop as many Rows as needed, while the second one helps us make the columns.

I know i didn't explain the code perfectly, but i'm sure it is worth sharing with you so can make you table views easier. This Image is an early example of a real app i'm making, and it looks nothing short of CollectionView, so you can be sure that this approach works well.

QUESTION: whats the point of having an array and trying to let swift make those indices for foreach?
its simple! if you have an array which defines its values/number-of-values in runtime, e.g. you are getting the numbers from a web api and that api tells you how many numbers are in your array, then you'll need to use some approach like this and let swift take care of indices of foreachs.

UPDATE:

More Info, reading these is optional.

LIST VS SCROLLVIEW: as some of you may not know, list works a little bit different from a scroll view. when you create a scrollview, it always calculates whole the ScrollView, then shows it to us. but list doesnt do that, when using lists, swift automatically calculates only a few of the list's components which are needed to show the current view, and when you scroll down to the bottom of the list, it only replaces the old values which are being scrolled out, with the new values of those which are at the bottom of the screen, with. so in general, list is always lighter, and can be much much faster when you are working with a heavy view, because it doesn't calculate all of your view at the beginning, and only calculates necessary things, while ScrollView doesn't.

WHY DID YOU SAY WE SHOULD USE SCROLLVIEW INSTEAD OF LIST? as i said before, there are some interactions with list that you probably dont like. for example when creating a list, every row is tappable, which is fine, but what is not fine is that ONLY the whole row is tappable! that means you cant set a tap action for the left side of a row, and a different one for the right side! this is just one of the weird interactions of a List() this either needs some knowledge i dont have! or is a big xcode-ios issue, or maybe its just fine and as intended! what i think is that its an apple issue and i hope it'll get fixed till at most the next WWDC. (UPDATE: and it of course got fixed with introduction of all the stuff like LazyGrids for iOS14-SwiftUI)

ANY WAYS TO OVERCOME THIS PROBLEM? as far as i know, the only way is to use UIKit. I've tried many many ways with SwiftUI, and although i've found out that you can get help from ActionSheet and ContextMenu to make lists better in terms of options when you tap them, i was unable to get the optimal intended functionality out of a SwiftUI List. so from my POV, SwiftUI devs can only wait for now.

Experiment answered 17/12, 2019 at 15:37 Comment(0)
E
3

Checkout ZStack based example here

Grid(0...100) { _ in
    Rectangle()
        .foregroundColor(.blue)
}

enter image description here

Emphysema answered 26/8, 2019 at 21:5 Comment(0)
N
3

Tired of finding many complicated solutions or Github libraries, I have decided to do my own, easy and beautiful Mathematical solution.

  1. Think you have an array of items var items : [ITEM] = [...YOUR_ITEMS...]
  2. You want to display an grid of Nx2

When N is the number of ROWS and 2 is the number of COLUMNS

  1. To show all items you need to use two ForEach statements, one for columns and one for rows.

Into both ForEach: (i) current index of ROWS, and (j) current index of COLUMNS

  1. Display the current item in the index [(i * 2) + j]
  2. Now let's go to the code:

Note: Xcode 11.3.1

var items : [ITEM] = [...YOUR_ITEMS...]
var body: some View {
VStack{
    // items.count/2 represent the number of rows
    ForEach(0..< items.count/2){ i in
        HStack(alignment: .center,spacing: 20){
            //2 columns 
            ForEach(0..<2){ j in
               //Show your custom view here
               // [(i*2) + j] represent the index of the current item 
                ProductThumbnailView(product: self.items[(i*2) + j])
            }
        }
        }.padding(.horizontal)
    Spacer()
   }
}
Nolin answered 26/3, 2020 at 5:31 Comment(2)
You can't use it with a lot of objects :) Memory issue. Only List has reusable items insideVerve
im a newbie, I can understand how a large m x n grid could exhaust memory, but this is the type of solution im pondering to scroll into a grid m x n where m,n range from 1..64. with a scroll & zoom. I'm thinking just mapping pixels.Nicaea
C
2

Try using a VStack and HStack

var body: some View {
    GeometryReader { geometry in
        VStack {
            ForEach(1...3) {_ in
                HStack {
                    Color.orange.frame(width: 100, height: 100)
                    Color.orange.frame(width: 100, height: 100)
                    Color.orange.frame(width: 100, height: 100)
                }.frame(width: geometry.size.width, height: 100)
            }
        }
    }
}

You can wrap in a ScrollView if you want scrolling

Courtnay answered 5/6, 2019 at 18:52 Comment(11)
Only problem is this isnt dynamically updating based on the width of the display....Distillery
Yes in order to adjust to the size of the display we need to use GeometryReader. I have adjusted my code to build a 3x3 grid based on the size of the display.Courtnay
But it this is not a list but stack - without reusability. What if I have 10000 items?Belicia
Then you make it dynamic by iterating over those items in the ForEach loop. And like I said you can wrap the vertical or horizontal stacks with a ScrollView if they do not all fit on screen.Courtnay
This answer has nothing to do with a CollectionView. Cells will not be reused and it has terrible performanceOvertake
Reusing is not an issue here - it’s a declarative code, so here you’re just creating super-lightweight structs conforming to a View type. Reusing of the actuall presentation instances is handled under the hood without you being involved. That’s the beauty of SwiftUI 😊 Although the question about best practices for collection views in SwiftUI remains valid I think. There seems to be no equivalent of UICollectionViewLayout here and that was the thing that was solving the problem elegantly in the UIKit.Neckpiece
@Overtake The original question was on how to create a grid of square items and thus I gave one option. It said nothing of CollectionView and nor was there any statement of performance.Courtnay
@MaciekCzarnik good answer, but it's not quite the same as a CollectionView as HStacks and VStacks can't handle arbitrary dynamic layout; e.g., flow layout.Ott
So does the SwiftUI version of ScrollView handle the reusability or not? If we need to present 1000 items, will they all be in memory all the time?Merganser
Any idea how to make it dynamic ?Branton
@MaciekCzarnik I would agree. SwiftUI seems to be useful for doing quick, responsive layouts and arrangement of semi-dynamic content. Anything requiring more complex logic or performance, it would be best used wrapped UIKit components.Courtnay
D
2

I think you can use scrollview like this

struct MovieItemView : View {
    var body: some View {
        VStack {
            Image("sky")
                .resizable()
                .frame(width: 150, height: 200)
            VStack {
                Text("Movie Title")
                    .font(.headline)
                    .fontWeight(.bold)
                Text("Category")
                    .font(.subheadline)
            }
        }
    }
}

struct MoviesView : View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10){
            Text("Now Playing")
                .font(.title)
                .padding(.leading)
            ScrollView {
                HStack(spacing: 10) {
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                    MovieItemView()
                }
            }
            .padding(.leading, 20)
        }
    }
}
Derna answered 9/6, 2019 at 15:51 Comment(0)
A
2

I’ve written a small component called 📱GridStack that makes a grid that adjusts to the available width. Even when that changes dynamically like when you rotate an iPad.

https://github.com/pietropizzi/GridStack

The essentials of that implementation are similar to what others have replied here (so HStacks inside a VStack) with the difference that it figures out the width depending on the available width and a configuration you pass it.

  • With minCellWidth you define the smallest width you want your item in the grid should have
  • With spacing you define the space between the items in the grid.

e.g.

GridStack(
    minCellWidth: 320,
    spacing: 15,
    numItems: yourItems.count
) { index, cellWidth in
    YourItemView(item: yourItems[index]).frame(width: cellWidth)
}
Asbury answered 9/7, 2019 at 7:46 Comment(2)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewHernardo
@DerkJanSpeelman I updated the answer to include the essential parts inline. CheersAsbury
E
2

Since I'm not using Catalina Beta, I wrote here my code you can run on Xcode 11 (Mojave) as a playground to take advantage of run-time compile and Preview

Basically when u look for a grid approach u should take in mind that SwiftUI child View get ideal size parameter from parent view so they can auto-adapt based on their own content, this behavior can be overridden (do not confuse with swift Override directive) by forcing view to a specific size via .frame(...) method.

In my opinion this make View behavior stable as well as the Apple SwiftUI framework has been correctly tested.

import PlaygroundSupport
import SwiftUI

struct ContentView: View {

    var body: some View {

        VStack {
            ForEach(0..<5) { _ in
                HStack(spacing: 0) {
                    ForEach(0..<5) { _ in
                        Button(action: {}) {
                            Text("Ok")
                        }
                        .frame(minWidth: nil, idealWidth: nil, maxWidth: .infinity, minHeight: nil, idealHeight: nil, maxHeight: .infinity, alignment: .center)
                        .border(Color.red)
                    }
                }
            }
        }
    }
}

let contentView = ContentView()
PlaygroundPage.current.liveView = UIHostingController(rootView: contentView)
Elongate answered 2/10, 2019 at 15:15 Comment(0)
B
1

Although the next WWDC is right around the corner, we're still missing a collection view in SwiftUI. I have tried to provide a decent example on how to create your own SwiftUI collection view using UIViewControllerRepresentable and Combine. I opted for creating my own collection view instead of using open source libraries as either I felt they were missing some key features or were bloated with too many things. In my example, I have used UICollectionViewDiffableDataSource and UICollectionViewCompositionalLayout to create the collection view. It supports both pullToRefresh and pagination functionality.

The full implementation can be found in: https://github.com/shabib87/SwiftUICollectionView.

Bluh answered 9/6, 2020 at 2:22 Comment(0)
C
0

Based on Will's answer i wrapped it all up in a SwiftUI ScrollView. So you can achieve horizontal (in this case) or vertical scrolling.

It's also uses GeometryReader so it is possible to calculate with the screensize.

GeometryReader{ geometry in
 .....
 Rectangle()
    .fill(Color.blue)
    .frame(width: geometry.size.width/6, height: geometry.size.width/6, alignment: .center)
 }

Here is the a working example:

import SwiftUI

struct MaterialView: View {

  var body: some View {

    GeometryReader{ geometry in

      ScrollView(Axis.Set.horizontal, showsIndicators: true) {
        ForEach(0..<2) { _ in
          HStack {
            ForEach(0..<30) { index in
              ZStack{
                Rectangle()
                  .fill(Color.blue)
                  .frame(width: geometry.size.width/6, height: geometry.size.width/6, alignment: .center)

                Text("\(index)")
              }
            }
          }.background(Color.red)
        }
      }.background(Color.black)
    }

  }
}

struct MaterialView_Previews: PreviewProvider {
  static var previews: some View {
    MaterialView()
  }
}

enter image description here

Carroll answered 1/10, 2019 at 19:31 Comment(0)
K
0

Updated Feb, 2024

For those who are new to SwiftUI and needs UICollectionView alternative like me:

  1. If your requirement has dynamic number of items in your Grid view then Use LazyVStack(vertical grid) with ScrollView and LazyHStack with ScrollView(.horizontal) for horizontal scrollable grid. Available from iOS 14.0+. Reference and resource: https://developer.apple.com/documentation/swiftui/lazyvgrid

  2. If your view has fixed number of items then use Grid. It is available from iOS 16.0+. Reference and resource: https://developer.apple.com/documentation/swiftui/grid

Khoury answered 13/2, 2024 at 16:4 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.