I know that we can create a List in vertical SwiftUI like this,
struct ContentView : View {
var body: some View {
NavigationView {
List {
Text("Hello")
}
}
}
}
but is there any way that we could split the list in 2 or 3 or maybe more spans that covers the screen like a grid like we did in UICollectionView
Checkout ZStack based example here
Grid(0...100) { _ in
Rectangle()
.foregroundColor(.blue)
}
iOS 14
There is 2 new native Views that you can use:
LazyHGrid
LazyVGrid
With code or directly from the library:
The library contains a fully working sample code that you can test yourself.
You can create your customView like this to achieve UICollectionView behavior:-
struct ContentView : View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
ScrollView(showsHorizontalIndicator: true) {
HStack {
ForEach(0...10) {_ in
GridView()
}
}
}
List {
ForEach(0...5) {_ in
ListView()
}
}
Spacer()
}
}
}
struct ListView : View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello World!"/*#END_MENU_TOKEN#*/)
.color(.red)
}
}
struct GridView : View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Image("marker")
.renderingMode(.original)
.cornerRadius(5)
.frame(height: 200)
.border(Color.red)
Text("test")
}
}
}
Available for iOS/iPadOS 14 on Xcode 12. You can use LazyVGrid to load just what the user see into screen and not the whole list, List is lazy by default.
import SwiftUI
//MARK: - Adaptive
struct ContentView: View {
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum:100))]) {
ForEach(yourObjects) { object in
YourObjectView(item: object)
}
}
}
}
}
//MARK: - Custom Columns
struct ContentView: View {
var body: some View {
ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) {
ForEach(yourObjects) { object in
YourObjectView(item: object)
}
}
}
}
}
Don't forget replace the info objects with your info and YourObjectView with your customView.
🔴 SwiftUI’s LazyVGrid and LazyHGrid give us grid layouts with a fair amount of flexibility.
The simplest possible grid is made up of three things: your raw data, an array of GridItem describing the layout you want, and either a LazyVGrid or a LazyHGrid that brings together your data and your layout.
For example, this will create a vertical grid layout using cells that are 80 points in size:
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}
🔴 Using GridItem(.adaptive(minimum: 80)) means we want the grid to fit in as many items per row as possible, using a minimum size of 80 points each.
If you wanted to control the number of columns you can use .flexible() instead, which also lets you specify how big each item should be but now lets you control how many columns there are. For example, this creates five columns:
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}
🔴 A third option is to use fixed sizes. For example, this will make the first column be exactly 100 points wide, and allow the second column to fill up all the remaining space:
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.fixed(100)),
GridItem(.flexible()),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}
🔴 You can also use LazyHGrid to make a horizontally scrolling grid, which works in much the same way except it accepts rows in its initializer.
For example, we could create 10 side by side heading images that are horizontally scrolling like this:
struct ContentView: View {
let items = 1...50
let rows = [
GridItem(.fixed(50)),
GridItem(.fixed(50))
]
var body: some View {
ScrollView(.horizontal) {
LazyHGrid(rows: rows, alignment: .center) {
ForEach(items, id: \.self) { item in
Image(systemName: "\(item).circle.fill")
.font(.largeTitle)
}
}
.frame(height: 150)
}
}
}
🔴 As you can see, the code required to create horizontal and vertical grids is almost the same, changing just rows for columns.
If you’re required to support iOS 13 you won’t have access to LazyHGrid or LazyVGrid, so read below for an alternative…
SwiftUI gives us VStack for vertical layouts and HStack for horizontal layouts, but nothing that does both – nothing that can lay out views in a grid structure.
Fortunately we can write one ourselves by leveraging SwiftUI’s view builder system. This means writing a type that must be created using a row and column count, plus a closure it can run to retrieve the views for a given cell in the grid. Inside the body it can then loop over all the rows and columns and create cells inside VStack and HStack to make a grid, each time calling the view closure to ask what should be in the cell.
In code it looks like this:
struct GridStack<Content: View>: View {
let rows: Int
let columns: Int
let content: (Int, Int) -> Content
var body: some View {
VStack {
ForEach(0 ..< rows, id: \.self) { row in
HStack {
ForEach(0 ..< columns, id: \.self) { column in
content(row, column)
}
}
}
}
}
init(rows: Int, columns: Int, #ViewBuilder content: #escaping (Int, Int) -> Content) {
self.rows = rows
self.columns = columns
self.content = content
}
}
// An example view putting GridStack into practice.
struct ContentView: View {
var body: some View {
GridStack(rows: 4, columns: 4) { row, col in
Image(systemName: "\(row * 4 + col).circle")
Text("R\(row) C\(col)")
}
}
}
That creates a 4x4 grid with an image and text in each cell.
Happy Coding ;)
Related
How do you reduce the height of a View in List in SwiftUI for iOS 16?
I was using this method with defaultMinListRowHeight set to zero, but iOS 16 adds more adding than previous versions.
struct ContentView: View {
var numbers = 1...10
var body: some View {
List
{
ForEach(numbers, id: \.self) { number in
Text(String(number))
}
}
.environment(\.defaultMinListRowHeight, 0)
.padding()
}
}
Set your desired height to your Text and List will follow that height only.
var body: some View {
List
{
ForEach(numbers, id: \.self) { number in
Text(String(number))
.frame(height: 15)// Set your desired height
}
}
.environment(\.defaultMinListRowHeight, 0)
.padding()
}
I've got a problem : I am using a ForEach loop to generate a custom view in a view.
I want them to be align like this (I've made this by creating 3 HStack inside a VStack):
The expectation
But I use ForEach, as a consequence, I am restricted to only "1 Stack".
I'm getting something like this :
The problem
Here is the code that concerns only the ScrollView :
ScrollView(.vertical, showsIndicators: true) {
HStack{
ForEach(styles, id: \.id) { style in // styles is an array that stores ids.
MusicStyleTabView(style: style, selectedBtn: self.$selected)// This view is the "cell" in question.
}
}
}
So how can I align horizontally a VStack ?
this code is going to solve your problem.
struct ContentView: View {
let gridItems = Array(repeating: GridItem(.flexible(minimum: 60)), count: 2)
var body: some View {
ScrollView {
LazyVGrid(columns: gridItems) {
ForEach(styles, id: \.id) { style in
MusicStyleTabView(style: style , selectedBtn: self.$selected)
}
}
}
}
I have a Text() view inside a HStack inside of a ForEach inside of a VStack. The text can be a string of any length, and I have no control of that is put inside of it. The problem is that when you run the program, the views in the VStack overlap resulting in this jumbled mess
What I want to do is have a view that resizes its height based on the height of the multi line text view, so that the views never overlap, and always displays the entirety of the string.
Here is some code that generates the view in question:
struct ScrollingChatView: View {
#State var model: WatchModel
#State var messages: [DisplayableMessage] = []
var body: some View {
ScrollView {
if (!messages.isEmpty) {
LazyVStack {
ForEach(messages, id: \.sortTimestamp) { message in
CompactChatView(message: message)
}
}.padding()
} else {
Text("Getting Chat...").padding()
}
}.onReceive(model.chatDriver.publisher) { m in
self.messages = m
}
}
}
struct CompactChatView: View {
#State var message: DisplayableMessage
#State var stringMessage: String? = nil
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(message.displayAuthor)
.lineLimit(1)
.layoutPriority(1)
Group {
Text(getEmojiText(message))
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Text(message.displayTimestamp)
.font(.subheadline)
.foregroundColor(Color.gray)
.layoutPriority(1)
}.padding(.all, 6.0)
}
}
func getEmojiText(_ item: DisplayableMessage) -> String {
var fullMessage: String = ""
for m in item.displayMessage {
switch m {
case .text(let s):
fullMessage += s
case .emote(_):
print()
}
}
return fullMessage
}
}
I've tried removing .fixedSize(horizontal: false, vertical: true) from the text view, but it only makes the text cut off after one line, which is not what I want.
If you need more context, the entire project in located at: https://github.com/LiveTL/apple. We're looking at code in the macOS folder.
You may find it useful to instead use a List() with a trailing closure like this...
List(itemList) { item in
Text(item)
}
This should prevent the issue you are running into when trying to display messages. For more information on lists check this out: https://developer.apple.com/documentation/swiftui/list.
You can see an example of this at 25:11 here: https://developer.apple.com/videos/play/wwdc2019/216/
Xcode 12.3 | SwiftUI 2.0 | Swift 5.3
I have multiple HStack whose first element must have the same width, but I cannot know how to achieve this.
HStack {
Text("Label 1:")
Text("Random texts")
}
HStack {
Text("Label 2 Other Size:")
Text("Random texts")
Text("Additional text")
}
HStack {
Text("Label 3 Other Larger Size:")
Text("Random texts")
}
This will display this:
Label 1: Random Texts
Label 2 Other Size: Random Texts Additional Text
Label 3 Other Larger Size: Random Texts
And I want to display this without using VStacks, because each HStack is a List Row:
Label 1: Random Texts
Label 2 Other Size: Random Texts Additional Text
Label 3 Other Larger Size: Random Texts
[__________________________] [_____________...
same size
I tried using a #propertyWrapper that stores the GeometryProxy of each Label's background and calcs the max WrappedValuein order to set the .frame(maxWidth: value) of each Label, but without success, I cannot have working that propertyWrapper (I get a loop and crash).
struct SomeView: View {
#MaxWidth var maxWidth
var body: some View {Â
HStack {
Text("Label 1:")
.storeGeo($maxWidth)
.frame(maxWidth: _maxWidth.wrappedValue)
Text("Random texts")
}
HStack {
Text("Label 2 Larger Size:")
.storeGeo($maxWidth)
.frame(maxWidth: _maxWidth.wrappedValue)
// Continue as the example above...
...
...
You can pass a GeometryReader width into any child view by using a standard var (not #State var).
Try this code out, its using GeometryReader and a list of Identifiable items which are passed a "labelWidth" they all render the same width for all labels. Note: I used a VStack in the row so it looked better, didn't follow why a row can't have a VStack in it.
[![struct Item : Identifiable {
var id:UUID = UUID()
var label:String
var val:\[String\]
}
struct ItemRow: View {
var labelWidth:CGFloat = 0
var item:Item
var body: some View {
HStack(spacing:5) {
Text(item.label)
.frame(width:labelWidth, alignment: .leading)
VStack(alignment: .leading) {
ForEach(0 ..< item.val.indices.count) { idx in
Text(item.val\[idx\])
}
}
}
}
}
struct ContentView : View {
#State var items:\[Item\] = \[
Item(label:"Label 1:", val:\["Random texts"\]),
Item(label:"Label 2 Other Size:", val:\["Random texts","Additional text"\]),
Item(label:"Label 3 Other Larger Size:", val:\["Random texts", "Random texts and texts", "Random texts", "Random texts"\])
\]
var body: some View {
GeometryReader { geo in
let labelWidth = geo.size.width * 0.6 // set this however you want to calculate label width
List(items) { item in
ItemRow(labelWidth: labelWidth, item: item)
}
}
}
}][1]][1]
The easiest way would be to set a specific .frame(width:) for the first Text() within the HStack. You can then use .minimumScaleFactor() to resize the text within to avoid the words being cut off. Here's an example to get you started:
struct ContentView: View {
var body: some View {
VStack {
CustomRow(title: "Label 1:", otherContent: ["Random Texts"])
CustomRow(title: "Label 2 Other Size::", otherContent: ["Random Texts", "Additional Text",])
CustomRow(title: "Label 3 Other Larger Size:", otherContent: ["Random Texts"])
}
}
}
struct CustomRow: View {
let title: String
let otherContent: [String]
var body: some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
.lineLimit(1)
.minimumScaleFactor(0.5)
ForEach(otherContent, id: \.self) { item in
Text(item)
.lineLimit(1)
.minimumScaleFactor(0.1)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Result:
Balanced widths between views
Based on all responses I coded a simpler way to get this working with a modifier.
First, we have to add the necessary extensions with the view modifier and the width getter:
extension View {
func balanceWidth(store width: Binding<CGFloat>, alignment: HorizontalAlignment = .center) -> some View {
modifier(BalancedWidthGetter(width: width, alignment: alignment))
}
#ViewBuilder func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct BalancedWidthGetter: ViewModifier {
#Binding var width: CGFloat
var alignment: HorizontalAlignment
func body(content: Content) -> some View {
content
.background(
GeometryReader { geo in
Color.clear.frame(maxWidth: .infinity)
.onAppear {
if geo.size.width > width {
width = geo.size.width
}
}
}
)
.if(width != .zero) { $0.frame(width: width, alignment: .leading) }
}
}
Usage
With this, all the work is done. In order to get equal widths between views all we have to do is mark each view with the balancedWidth modifier and store the shared width value in a #State variable with initial value == .zero:
#State var width: CGFloat = 0 <-- Initial value MUST BE == .zero
SomeView()
.balanceWidth(store: $width)
AnotherViewRightAligned()
.balanceWidth(store: $width, alignment: .leading)
Sample
struct ContentView: View {
#State var width: CGFloat = 0
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Label 1")
.balanceWidth(store: $width, alignment: .leading)
Text("Textss")
}
HStack {
Text("Label 2: more text")
.balanceWidth(store: $width, alignment: .leading)
Text("Another random texts")
}
HStack {
Text("Label 3: another texts")
.balanceWidth(store: $width, alignment: .leading)
Text("Random texts")
}
}
.padding()
}
}
We can create more relationships between views and balance the widths between them separately by creating more than one #State variable.
I am trying to implement grid layout in collectionView. This is my current view
instead of 1 item per collection I would like to show 3 items
this is my productView
struct ProductSearchView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var model: SearchResultViewModel
var body: some View{
NavigationView {
List {
ForEach(0 ..< Global.productArry.count) { value in
Text(Global.productArry[value].name)
CollectionView(model: self.model, data: Global.productArry[value])
}
}.navigationBarTitle("Store")
}
}
}
and this is my collection view
struct CollectionView: View {
#ObservedObject var model: SearchResultViewModel
let data: Product
var body: some View {
VStack {
HStack {
Spacer()
AsyncImage(url: URL(string: self.data.productImageUrl)!, placeholder: Text("Loading ...")
).aspectRatio(contentMode: .fit)
Spacer()
}
VStack {
Spacer()
Text(self.data.name)
Spacer()
}
HStack {
Text("Aisle: \(self.data.location_zone)\(String(self.data.aisleNo))").bold()
Text("$\(String(self.data.productPrice))")
}
}.onAppear(perform:thisVal)
}
func thisVal (){
print(self.data.productImageUrl)
}
}
how can I implement a grid of three items ?
Use a LazyVGrid. The following example is very simple and easy to read and you can change it to anything you need.
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { _ in
Circle().scaledToFill()
}
}
.padding(.horizontal)
}
}
}
The code snippet below is what you need if you are using SwiftUI version 1 which came with Xcode 11. It is a custom implementation which simply use a combination of ScrollView, VStack and HStack as well as a little maths to calculate the frame size depending on the number of items.
import SwiftUI
struct GridView<Content, T>: View where Content: View {
// MARK: - Properties
var totalNumberOfColumns: Int
var numberRows: Int {
return (items.count - 1) / totalNumberOfColumns
}
var items: [T]
/// A parameter to store the content passed in the ViewBuilder.
let content: (_ calculatedWidth: CGFloat,_ type: T) -> Content
// MARK: - Init
init(columns: Int, items: [T], #ViewBuilder content: #escaping(_ calculatedWidth: CGFloat,_ type: T) -> Content) {
self.totalNumberOfColumns = columns
self.items = items
self.content = content
}
// MARK: - Helpers
/// A function which help checking if the item exist in the specified index to avoid index out of range error.
func elementFor(row: Int, column: Int) -> Int? {
let index:Int = row * self.totalNumberOfColumns + column
return index < items.count ? index : nil
}
// MARK: - Body
var body: some View {
GeometryReader { geometry in
ScrollView{
VStack {
ForEach(0...self.numberRows,id: \.self) { (row) in
HStack {
ForEach(0..<self.totalNumberOfColumns) { (column) in
Group {
if (self.elementFor(row: row, column: column) != nil) {
self.content(geometry.size.width / CGFloat(self.totalNumberOfColumns), self.items[self.elementFor(row: row, column: column)!])
.frame(width: geometry.size.width / CGFloat(self.totalNumberOfColumns), height: geometry.size.width / (CGFloat(self.totalNumberOfColumns)), alignment: .center)
}else {
Spacer()
}
}
}
}
}
}
}
}
}
}
Usage: It is generic ready to use with any view of your choice. For instance the Preview code below uses the system images.
struct GridView_Previews: PreviewProvider {
static var previews: some View {
GridView(columns: 3, items: ["doc.text.fill","paperclip.circle.fill", "globe","clear.fill","sun.min.fill", "cloud.rain.fill", "moon","power"], content: { gridWidth,item in
Image(systemName: item)
.frame(width: gridWidth, height: gridWidth, alignment: .center)
.border(Color.orange)
})
.padding(10)
}
}
Output
You can change the code to fit your needs for example paddings etc.
NOTE: Use this method only if you don't have lots of views as it is not very performant. If you have a lot of views to scroll I suggest you use the newer and simple build-in GridItem from Apple which was introduced this year at WWDC. But you will have to use Xcode 12 which currently is available as Beta version.
put for loop logic in HStack like givern below.
HStack {
ForEach(0..<3) { items in
Spacer()
AsyncImage(url: URL(string: self.data.productImageUrl)!, placeholder: Text("Loading ...")
).aspectRatio(contentMode: .fit)
Spacer()
}.padding(.bottom, 16)
}