SwiftUI LazyHGrid space between dynamic rows - swiftui

Could you suggest how to have a dynamic number of rows in a grid.
I have 5 words (these could be different width). The idea is to show them next to each other and if there is no enough space -> move to next row
I'm currently using this
var rows: [GridItem] = [.init(.adaptive(minimum: 60))]
LazyHGrid(rows: rows, spacing: 2)
but the space between rows is too big.
Any idea how to make it is smaller?
Thank you

Add spacing in grid item
var rows: [GridItem] = [.init(.adaptive(minimum: 60), spacing: 2)]

Related

SwiftUI: LazyVGrid with three columns, corner cells should have rounded corners

I am creating a LazyVStack with tiles arranged in three columns which works fine.
var columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, pinnedViews: .sectionHeaders) {
ForEach(viewModel.items, id: \.self) { item in
ItemCellView(itemId: item.item_id, itemText: item.text)
}
}
}
}
Now I would like to manipulate the ItemCellViews which are in the four corners of the list to get rounded corners. For this purpose I need to know the row and column number. But how?
Thanks

Grid layout that puts things in column order

I have the code below which works great. It displays words in alphabetical order in a nice grid. And it works great on different devices, iphone, ipad. Eg, when rotating an ipad from portrait to landscape, I get more columns. It fills the screen no matter what device/orientation, and to see anything missing I scroll vertically. All good.
However, the one issue I'd like to solve is I'd like the items to be displayed in column order. Right now they are displayed in row order, first row1, then row2, etc, but I want to do it by column. First populate col1, then go to col2, etc.
I understand that LazyHGrid does populate in this order but I can't seem to get something that works (eg, I end up with all words in one row). Ideas?
struct ContentView: View {
func getWords() -> [String] {
var retval: [String] = []
let alpha = "abcdefghijklmnopqrstuvwxyz"
for _ in 0...500 {
let length = Int.random(in: 4...16)
retval.append( String(alpha.shuffled().prefix(length)).capitalized )
}
return retval.sorted()
}
func getColumns() -> [GridItem] {
return [GridItem(.adaptive(minimum: 150))]
}
var body: some View {
ScrollView() {
LazyVGrid(columns: getColumns(), alignment: .leading) {
ForEach(getWords(), id: \.self) { word in
Text(word)
}
}.padding()
}
}
}
EDIT: This is a version with the HGrid, but it just displays everything in one row. I don't want to have to specify the number of rows/columns, I really want things to work exactly like the VGrid version, except for the col vs row layout.
var body: some View {
ScrollView() {
LazyHGrid(rows: getColumns(), alignment: .top) {
ForEach(getWords(), id: \.self) { word in
Text(word)
}
}.padding()
}
}
The LazyHStack is the way to go, and while you do have to specify the number of rows, you don't have to hard code that number. I had to alter your MRE a bit as you do have to have the words initialized before you hit the LazyHGrid(), so your calling the function in the ForEach won't work. In a working app, you would have some variable already initialized to use, so this should not be a problem. So, an example of your view would be this:
struct ContentView: View {
#State var words: [String] = []
// The GridItem has to be .flexible, or odd numbers of words will add an extra column
let row = GridItem(.flexible(), alignment: .leading)
#State var numberOfColumns = 2.0
var body: some View {
VStack{
Stepper("Columns") {
numberOfColumns += 1
} onDecrement: {
if numberOfColumns > 2 {
numberOfColumns -= 1
}
}
ScrollView() {
// The parameter for rows becomes an array that you create on the fly,
// repeating the row for half the words rounded to give an extra line
// for an odd number of words.
LazyHGrid(rows: Array(repeating: row, count: Int((Double(words.count) / numberOfColumns).rounded()))) {
ForEach(words, id: \.self) { word in
Text(word)
}
}.padding()
}
}
.onAppear {
// Didn't want to deal with a static func, so just set the words here.
words = getWords()
}
}
func getWords() -> [String] {
var retval: [String] = []
let alpha = "abcdefghijklmnopqrstuvwxyz"
for _ in 0...500 {
let length = Int.random(in: 4...16)
retval.append( String(alpha.shuffled().prefix(length)).capitalized )
}
return retval.sorted()
}
}
Edit:
I believe this is what you are looking for. The following code will set the columns as above, and automatically compute the number of columns based off of the width of the word. It will also recompute the number of columns upon rotation, or changing of the list of words. I built in some ability to play with the view to see how it works. This was simply a math problem. The PreferenceKeys just give the numbers you need for the computations.
Of course, the PreferenceKeys use GeometryReaders to determine these sizes, but there is no other way to get this information. It is very likely that behind the scenes, LazyVGrid and LazyHGrid are also using GeometryReader.

LazyVGrid where every item takes only the place it needs

I just wondered how you could make a LazyVGrid where every item takes only the place it needs, not less and not more.
I know about .flexible() but the problem is: My Items are different sized, that means I don't know how many of them will fit in a row.
Do you got any ideas?
Thanks for your help!
Boothosh
EDIT:
LazyVGrid(columns: [GridItem(.flexible())]) {
Color.blue
.frame(width: 200, height: 200)
Color.blue
.frame(width: 100, height: 100)
Color.blue
.frame(width: 100, height: 100)
}
This is a example what Im talking about. I want to achieve that this items are placed with evenly space around them. (Not below each other, like they are now)
You just need to specify what you want by using variables.
Try this :
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
// The number of grid Items here represent the number of columns you will
//see. so you can specify how many items in a row thee are .
// 2 grid Items = 2 items in a row
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)
}
}

SwiftUI LazyGrid dynamic column width

How can I make the Grid item width dynamic so that it takes the width of the text?
Using the code below the text is truncated, I would like all the text to be displayed without the truncation, taking into account the variable text lengths.
struct ContentView: View {
let data = ["O Menino","The Boy", "The Girl", "A Menina","Mae","Mother"]
let layout = [
GridItem(.adaptive(minimum:50))
]
var body: some View {
ScrollView{
LazyVGrid(columns: layout, spacing: 20){
ForEach(data, id: \.self){ item in
VStack{
Text(item).lineLimit(1)
}.background(Color.red)
}
}
}
}
}
It is VGrid, it grows vertically filling columns. In your case it is only one column.
If you want to fit all those content in screen, you'd need to increase number of grid columns, like
let data = ["O Menino","The Boy", "The Girl", "A Menina","Mae","Mother"]
let layout = Array(repeating: GridItem(.adaptive(minimum:50)), count: 4)

SwiftUI - Resizable List height that dependent on an element count

I have some troubles with dynamically changing List height that dependent on elements count.
I tried this solution but it didn't work.
List {
ForEach(searchService.searchResult, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
}
}.frame(height: CGFloat(searchService.searchResult.count * 20))
TL;DR
This is not how the designers of SwiftUI want you to use lists. Either you will have to come up with a hacky solution that will probably break in the future (see below), or use something other than a list.
Background
SwiftUI tends to have two types of Views
Those designed to be easily modifiable and composable, providing unlimited customizability for a unique look and feel.
Those designed to provide a standard, consistent feel to some type of interaction, regardless of what app they are used in.
An example of type 1 would be Text. You can change font size, weight, typeface, color, background, padding, etc. It is designed for you to modify it.
An example of type 2 would be List. You are not in direct control of row height, you can't change the padding around views, you can't tell it to show only so many rows, etc. They don't want it to be very customizable, because then each app's lists would behave differently, defeating the purpose of a standard control.
List is designed to fill the entire parent View with as many rows as possible, even if they are empty or only partially on screen (and scroll if there are too many to show at once).
Your issue
The problem you are having comes about because the size of the List does not affect the size of its rows in any way. SwiftUI doesn't care if there are too many or too few rows to fit in your preferred size; it will happily size its rows according to content, even if it means they don't all show or there are empty rows shown.
If you really need rows to resize according to the size of their parent, you should use a VStack. If it needs to scroll, you will need to wrap the VStack in a ScrollView.
Hacky solution
If you still insist on using a list, you will have to do something like the following:
struct ContentView: View {
#State private var textHeight: Double = 20
let listRowPadding: Double = 5 // This is a guess
let listRowMinHeight: Double = 45 // This is a guess
var listRowHeight: Double {
max(listRowMinHeight, textHeight + 2 * listRowPadding)
}
var strings: [String] = ["One", "Two", "Three"]
var body: some View {
VStack {
HStack {
Text(String(format: "%2.0f", textHeight as Double))
Slider(value: $textHeight, in: 20...60)
}
VStack(spacing: 0) {
Color.red
List {
ForEach(strings, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
.frame(height: CGFloat(self.textHeight))
.background(Color(white: 0.5))
}
}
// Comment out the following line to see how List is expected to work
.frame(height: CGFloat(strings.count) * CGFloat(self.listRowHeight))
Color.red
}.layoutPriority(1)
}
}
}
The slider is there to show how the list row heights change with the height of their child view. You would have to manually pick listRowPadding and listRowMinHeight to get the appearance that best matches your expectation. If Apple ever changes how a List looks (changes padding, minimum row heights, etc.) you will have to remember to come back and adjust these values manually.
Self size List:
If you want a List to show it's content all at once, It means you don't need the recycling feature (the key feature of the list), So all you need is to not using a List! Instead, you can use ForEach directly, then it will size itself based on it's content:
ForEach(searchService.searchResult, id: \.self) { item in
VStack(alignment: .leading) {
Text(item).font(.custom("Avenir Next Regular", size: 12))
Divider()
}.padding(.horizontal, 8)
}
You can change all sizes and spacings according to your needs
Note that You can use LazyVStack from iOS 14 to make it lazy-load and boost its performance.
Starting from iOS 14 you can use LazyVStack instead of List.
List seems to span entire parent view height independent of rows height or count.
LazyVStack {
ForEach(searchService.searchResult, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
}
}.frame(height:
Other solution is to set .frame(height: ) on List based on rowCount*rowHeight or other GeometryReader -> geometry.size.height
SwiftUi has evolved. Here's a plain and simple answer for SwiftUI 3: https://stackoverflow.com/a/65769005/4514671