Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to modify the position of items in a LazyVGrid/LazyHGrid (SwiftUI 5.5)?

I'm relatively new to Swift and have a grid layout question: I'm currently using a LazyHGrid with adaptive grid items and a single column to flexibly layout items without a predetermined column count. I determine the number of columns shown by setting a maxWidth the grid layout with a maxWidth based on the number of items to be displayed

What I'm trying to accomplish in each of the scenarios below (with a solution that would scale up too) is essentially to adjust the positions in the examples below by shifting items out of position "🔴" and into position "⚪️", eliminating the "floating orphan" item not hugging the edge. The "⚫️" positions are already correct.

CONSTRAINTS

  • I want to minimize the width that this grid uses, but not all rows can accommodate the taller stacks (so setting a bunch of brittle rules seems like a poor solution) and I'm not interested in setting a fixed row item height to remove the need for a dynamic solution
  • I'd like to avoid creating grid manually with nested HStacks and VStacks and writing a bunch of extra logic to manage the contents of each row and column given the height of the parent row (as retrieved via GeometryReader).

WHAT I'VE TRIED

  • I looked in the Lazy_Grid docs to see if there was a view modifier that would accomplish this -- didn't come across one (though it's entirely possible I've missed it!)
  • I thought maybe flipping the layoutDirection (from LTR to RTL) might do it, but no such luck
  • I shifted around the alignment values of the repeated gridItem and lazyHGrid container, but the floating orphan remained

EXAMPLES

  • For three items:
/* short */
___________
      ⚫️⚫️|
      🔴⚪️|

/* tall */
___________
        ⚫️|
        ⚫️|
        ⚫️|
  • For four items:
/* short */
___________
      ⚫️⚫️|
      ⚫️⚫️|

/* tall */
___________    ___________
      ⚫️⚫️|          ⚫️⚫️| // NOTE: I *could* constrain the height in this case
      🔴⚪️|          ⚫️⚫️| // to achieve the 2x2 grid but would rather not
      🔴⚪️|
  • For five items:
/* short */
___________
    ⚫️⚫️⚫️|
    🔴⚫️⚪️|

/* tall */
___________
      ⚫️⚫️|
      ⚫️⚫️|
      🔴⚪️|
  • For six items:
/* short */
___________
    ⚫️⚫️⚫️|
    ⚫️⚫️⚫️|

/* tall */
___________
      ⚫️⚫️|
      ⚫️⚫️|
      ⚫️⚫️|
  • For seven items:
/* short */
___________
  ⚫️⚫️⚫️⚫️|
  🔴⚫️⚫️⚪️|

/* tall */
___________
      ⚫️⚫️|
      ⚫️⚫️|
      ⚫️⚫️|
      🔴⚪️|
let items = filterItems()
let iconCount = CGFloat(items.count)
let iconSize: CGFloat = 18
let spacing: CGFloat = 2

// NOTE: `iconCount / 2` because the min-height of the parent 
// only fits two items. The current item count is 7 and it's
// unlikely that it'd ever do more than double.

let cols = CGFloat(iconCount / 2).rounded(.up)
let maxWidth = calcWidth(for: cols, iconSize: iconSize, spacing: spacing)
let minWidth = cols < 2 ? maxWidth : calcWidth(for: cols - 1, iconSize: iconSize, spacing: spacing)

LazyHGrid(
  rows: Array(
    repeating: GridItem(
      .adaptive(minimum: iconSize, maximum: iconSize),
      spacing: spacing,
      alignment: .center
    ),
    count: 1
  ),
  alignment: .center,
  spacing: spacing
) {
  ForEach(0..<items.count, id: \.self) { n in

    ...item views...

  }
}
.frame(minWidth: minWidth, maxWidth: maxWidth)

My brain is stuck on finding a Swift concept similar to CSS's FlexBox justify-content: flex-end; align-items: flex-start but it feels to me there should be a more Flexy/Swifty solution for this that I'm just missing?

(The grid is nested within an HStack currently, and shoved to the top-trailing corner of it's parent with a spacer, effectively accomplishing the align-items: flex-start portion of the Flexbox solution mentioned above asd as shown in the illustrations above)

like image 544
Kate Sowles Avatar asked Sep 14 '25 07:09

Kate Sowles


1 Answers

Edit: Layout direction works for me

You mentioned this didn't work in your post, but I just tested setting layout direction to right-to-left, and it works. All the end items are aligned to the right. Maybe the specific OS you're testing on has a layout bug. The full playground snippet (tested on Xcode 13):

import UIKit
import PlaygroundSupport
import SwiftUI
import Foundation

let iconCount: CGFloat = 4
let iconSize: CGFloat = 18
let spacing: CGFloat = 2
let cols: CGFloat = 2
let maxWidth = calcWidth(for: cols, iconSize: iconSize, spacing: spacing)
let minWidth = cols < 2 ? maxWidth : calcWidth(for: cols - 1, iconSize: iconSize, spacing: spacing)

func calcWidth(for cols: CGFloat, iconSize: CGFloat, spacing: CGFloat) -> CGFloat {
    return iconSize * cols + spacing * cols
}

PlaygroundPage.current.liveView = UIHostingController(rootView: {
    TabView {
        HStack {
            LazyHGrid(
              rows: Array(
                repeating: GridItem(
                  .adaptive(minimum: iconSize, maximum: iconSize),
                  spacing: spacing,
                  alignment: .center
                ),
                count: 1
              ),
              alignment: .center,
              spacing: spacing
            ) {
              ForEach(0..<Int(iconCount), id: \.self) { n in
                  Text("B")
                      .frame(width: iconSize, height: iconSize)
                      .border(.red)
              }
            }
            .frame(minWidth: minWidth, maxWidth: maxWidth)
            .background(Color.green)
            .frame(height: iconSize * 3 + spacing * 2)
            .environment(\.layoutDirection, .rightToLeft)
            // I also tried this, but it isn't needed
            //.flipsForRightToLeftLayoutDirection(true)
        }
    }
}())

enter image description here

Original answer:

This is not the ideal solution, but one option is to use 3D rotation to flip the layout on the Y axis. I used a simple text view in Playgrounds as an example:

LazyHGrid(
  rows: Array(
    repeating: GridItem(
      .adaptive(minimum: iconSize, maximum: iconSize),
      spacing: spacing,
      alignment: .center
    ),
    count: 1
  ),
  alignment: .center,
  spacing: spacing
) {
  ForEach(0..<Int(iconCount), id: \.self) { n in
      Text("B")
          .frame(width: iconSize, height: iconSize)
          .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
  }
}
.frame(minWidth: minWidth, maxWidth: maxWidth)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))

enter image description here

like image 60
akaffe Avatar answered Sep 17 '25 03:09

akaffe