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:
16 Answers
iOS 14 and XCode 12
SwiftUI for iOS 14 brings a new and nativ grid view that is easy to use, its called LazyVGrid:
You can start with defining an array of GridItem's. GridItems are used to specify layout properties for each column. In this case all GridItems are flexible.
LazyVGrid takes an array of GridItem's 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)
}
}
}
}
}
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.
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)
}
}
}
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:
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..
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)
}
}
}