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
WHAT I'VE TRIED
EXAMPLES
/* short */
___________
⚫️⚫️|
🔴⚪️|
/* tall */
___________
⚫️|
⚫️|
⚫️|
/* short */
___________
⚫️⚫️|
⚫️⚫️|
/* tall */
___________ ___________
⚫️⚫️| ⚫️⚫️| // NOTE: I *could* constrain the height in this case
🔴⚪️| ⚫️⚫️| // to achieve the 2x2 grid but would rather not
🔴⚪️|
/* short */
___________
⚫️⚫️⚫️|
🔴⚫️⚪️|
/* tall */
___________
⚫️⚫️|
⚫️⚫️|
🔴⚪️|
/* short */
___________
⚫️⚫️⚫️|
⚫️⚫️⚫️|
/* tall */
___________
⚫️⚫️|
⚫️⚫️|
⚫️⚫️|
/* 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)
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)
}
}
}())
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))