0

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()
        }
    }
Jack
  • 2,206
  • 3
  • 18
  • 25
  • Yes, LazyHGrid is probably what you want to use. Could you provide a version with LazyHGrid since you already tried it? It'll be easier to take it from there. – Vadim Belyaev May 17 '22 at 22:52

1 Answers1

0

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.

Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Thanks, but not quite what I'm looking for. It seems like it's fixed to 2 columns, so it looks bad on an iPad. The VGrid deals with this dynamism perfectly, I just wish there was a "layoutOrder: .byColumn|.byRow" property. – Jack May 19 '22 at 21:14
  • To change the number of columns, you simply change the divisor. Your question indicated 2. I updated the answer with a changeable amount of columns for you to play with. – Yrb May 19 '22 at 21:48
  • That's exactly what I'm trying to avoid. I don't want to specify the columns, I want the Grid to figure it out, that's what VGrid does. Otherwise I'll need to use a GeometryReader, and do all that computation that figures out how many columns will "fit". – Jack May 26 '22 at 15:28
  • There is still some mathematical rule to determine how to handle it. You can always determine. It just depends upon what you want to happen, and that is not clear – Yrb May 26 '22 at 17:36
  • To be clear, I want exactly what VGrid is doing. It knows how to fill the screen, no matter what the device is, and what the orientation is. This means the number of columns is dynamic, and the number of rows is dynamic. It figures it out. And the scrolling is vertical. The only difference i'm looking for is to lay out the items by column, rather than by row. But it doesn't seem like this is possible w/o using (as mentioned) a GeometryReader and do all that math. – Jack May 26 '22 at 19:29
  • You can't have it both ways. Either you use the `LazyVGrid` as stock, or you implement this solution. It isn't difficult, but you can't make the `LazyVGrid` into something it is not. – Yrb May 26 '22 at 20:52
  • It's not about having it "both ways", it's about whether VGrid supports something like "layoutOrder", which it looks it doesn't, or whether HGrid supports dynamic rows/cols, which it looks like it doesn't either. And "this solution" is not the answer. The answer is something that uses GeometryReader to figure out dynamically how many rows/cols there should be. – Jack May 27 '22 at 15:52
  • I have been asking you what "how many rows/cols there should be" means to you. And you can't say like `LazyVGrid` because they are specified differently. Do you want as many columns as will fit on the page, or what? You won't specify, so I can't update the answer. – Yrb May 27 '22 at 16:00
  • Sorry, I've been saying all along that I want dynamic rows/cols to fill the screen, just like what my initial code is doing with VGrid. If you run that code on iphones/ipads, rotate them, etc, you'll see what I mean. And I can say "like LazyVGrid", it doesn't matter whether it can do it or not, or whatever; it is exhibiting exactly the behavior I'm looking for w/respect to laying out the rows/cols, so it's a good way to describe things. – Jack May 27 '22 at 23:50
  • Sorry I was out for a while. But it doesn't look like the code itself got updated? – Jack Jun 09 '22 at 20:11