Positioning Views with GeometryReader in SwiftUI - ios

I have a set of child views (GridView1 and GridView2) within a scrollview. The child views are within a VStack since I need them one below the other. I have a GeometryReader wrapping the child views and each of the child views have a GeometryReader inside them. I need the GeometryReader inside the child view to determine the width of the column spacing I need in a LazyVGrid.
The problem is that, using a GeometryReader inside the Child views causes the two child views stack on top of each other. I have tried to set the frame size of the child view but that limits the vertical scrolling and is not giving me the right result.
I would appreciate any help in resolving this problem.
ContentView:
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
VStack(spacing: 20) {
GridView1()
GridView2()
}//.frame(minHeight: 0, maxHeight: .infinity)
}
}
.padding()
}
}
GridView1:
struct GridView1: View {
var body: some View {
GeometryReader { g in
let maxwidth = g.size.width/6 > 100 ? g.size.width/6 : 100
let columns = Array(repeating: GridItem(.flexible(minimum: 100, maximum: maxwidth), spacing: 0), count: 6)
ScrollView(.horizontal) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 10, pinnedViews: [.sectionHeaders]) {
Section(header: Text("Grid 1").font(.title)) {
ForEach(0...200, id:\.self) { index in
Text("\(index)").frame(maxWidth: .infinity)
}
}
}
}
.background(Color.blue)
}
}
}
GridView2 (GridView1 and GridView2 are essentially the same thing)
struct GridView2: View {
var body: some View {
GeometryReader { g in
let maxwidth = g.size.width/10 > 100 ? g.size.width/10 : 100
let columns = Array(repeating: GridItem(.flexible(minimum: 100, maximum: maxwidth), spacing: 0), count: 10)
ScrollView(.horizontal) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 20, pinnedViews: [.sectionHeaders]) {
Section(header: Text("Grid 2").font(.title)) {
ForEach(1000...1200, id:\.self) { index in
ZStack {
Text("\(index)")
}
.frame(maxWidth: .infinity)
.background(Color.green)
}
}//.frame(minHeight: 0, maxHeight: .infinity)
}
}//.frame(minHeight: 1000, maxHeight: .infinity)
//.frame(maxWidth: .infinity, maxHeight: .infinity)
//.background(Color.red)
}
}
}
here is what I am expecting:
Here is what I am currently getting. As you can see below, the child views are bunching up at the top.

Instead of use internal GeometryReader that confuses external ScrollView, pass width from top GeometryReader into sub-views grids.
Here is worked solution. Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
VStack(spacing: 20) {
GridView1(width: geo.size.width)
GridView2(width: geo.size.width)
}
}
}
.padding()
}
}
struct GridView1: View {
let width: CGFloat
var body: some View {
let maxwidth = width/6 > 100 ? width/6 : 100
let columns = Array(repeating: GridItem(.flexible(minimum: 100, maximum: maxwidth), spacing: 0), count: 6)
ScrollView(.horizontal) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 10, pinnedViews: [.sectionHeaders]) {
Section(header: Text("Grid 1").font(.title)) {
ForEach(0...200, id:\.self) { index in
Text("\(index)").frame(maxWidth: .infinity)
}
}
}
}
.background(Color.blue)
}
}
struct GridView2: View {
let width: CGFloat
var body: some View {
let maxwidth = width/10 > 100 ? width/10 : 100
let columns = Array(repeating: GridItem(.flexible(minimum: 100, maximum: maxwidth), spacing: 0), count: 10)
ScrollView(.horizontal) {
LazyVGrid(columns: columns, alignment: .leading, spacing: 20, pinnedViews: [.sectionHeaders]) {
Section(header: Text("Grid 2").font(.title)) {
ForEach(1000...1200, id:\.self) { index in
ZStack {
Text("\(index)")
}
.frame(maxWidth: .infinity)
.background(Color.green)
}
}
}
}
}
}

Related

SwiftUI: Expand LazyVGrid cells to max height of each row

I'm using a LazyVGrid in a ScrollView to display cells in either a 1 (portrait) or 2 (landscape) column layout. However, the height of shorter cells in a row does not expand to match the taller cell in the same row and looks pretty terrible.
How can I ensure the height is always the same for every cell in a row? Obviously I don't want a fixed height for every cell. (To be clear, I want "Church - Eastbound" to be as tall as "Church & Market" and "West Portal" to be as tall as "Forest Hill".
ScrollView(.vertical) {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 400))],
alignment: .leading,
spacing: 16
) {
ForEach(sharedFavorites.favoriteStops.indices, id: \.self) { index in
let favorite = sharedFavorites.favoriteStops[index]
NavigationLink(
destination: SingleStationView(
station: favorite.station,
direction: favorite.direction
)
) {
BoardRow(favorite: favorite, stop: favorite.observableStop)
.padding()
.background(Color(.secondarySystemGroupedBackground))
.cornerRadius(10)
.frame(maxHeight: .infinity)
}
}
}
}
Screenshot:
I tried .frame(maxHeight: .infinity) on both the BoardRow view and the inner contents of BoardView (which is just a normal VStack). It didn't work.
You were really close — just needed to put the .frame(maxHeight:) before .background.
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 160))],
alignment: .leading,
spacing: 16
) {
ForEach([0, 1, 2, 5, 6, 3], id: \.self) { index in
BoardCell(index: index)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Color(.secondarySystemGroupedBackground)) /// `background` goes after the frame!
.cornerRadius(10)
}
}
.padding(20)
}
.background(Color(.secondaryLabel))
}
}
/// I recreated your view for testing purposes
struct BoardCell: View {
var index: Int
var body: some View {
VStack(alignment: .leading) {
Text("Powell (Westbound)")
ForEach(0 ..< index) { _ in
HStack {
Circle()
.fill(.green)
.frame(width: 30, height: 30)
Text("4")
Text("19")
Text("17")
}
}
}
}
}
Result:

How to size subviews in HStack by percentage without GeometryReader (SwiftUI)?

I've got a simple HStack with subviews inside. How can I tell the first subview to be 60% the size of the HStack without using a GeometryReader?
struct ContentView: View {
var body: some View {
HStack {
Color.red.opacity(0.3)
Color.brown.opacity(0.4)
Color.yellow.opacity(0.6)
}
}
}
The code above makes each subview the same size. But I want the first one to be 60% regardless of it's content. In this example, it is a color, but it could be anything.
The HStack is dynamic in size.
Edit: Why no GeometryReader?
When I want to place multiple of those HStacks inside a ScrollView, they overlap, because the GeometryReader's height is only 10 Point. As mentioned above, the Color views could be anything, so I used VStacks with cells in it that have dynamic heights.
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
ProblematicView()
ProblematicView()
}
}
}
struct ProblematicView: View {
var body: some View {
GeometryReader { geo in
HStack(alignment: .top) {
VStack {
Rectangle().frame(height: 20)
Rectangle().frame(height: 30)
Rectangle().frame(height: 20)
Rectangle().frame(height: 40)
Rectangle().frame(height: 20)
}
.foregroundColor(.red.opacity(0.3))
.frame(width: geo.size.width * 0.6)
.overlay(Text("60%").font(.largeTitle))
VStack {
Rectangle().frame(height: 10)
Rectangle().frame(height: 30)
Rectangle().frame(height: 20)
}
.foregroundColor(.brown.opacity(0.4))
.overlay(Text("20%").font(.largeTitle))
VStack {
Rectangle().frame(height: 5)
Rectangle().frame(height: 10)
Rectangle().frame(height: 24)
Rectangle().frame(height: 10)
Rectangle().frame(height: 17)
Rectangle().frame(height: 13)
Rectangle().frame(height: 10)
}
.foregroundColor(.yellow.opacity(0.6))
.overlay(Text("20%").font(.largeTitle))
}
}
.border(.blue, width: 3.0)
}
}
As you can see, the GeometryReader's frame is too small in height. It should be as high as the HStack. That causes the views to overlap.
I don't know the exact reason (might be a bug in GeometryReader), but placing the GeometryReader outside the ScrollView, and passing down its width makes your code behave as you expect.
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ScrollView {
ProblematicView(geoWidth: geo.size.width)
ProblematicView(geoWidth: geo.size.width)
}
}
.border(.blue, width: 3.0)
}
}
struct ProblematicView: View {
let geoWidth: CGFloat
var body: some View {
// same code, but using geoWidth to compute the relative width
Result:
You can set by .frame & UIScreen.main.bounds.size.width * (your width ratio) calculation.
Example
struct ContentView: View {
var body: some View {
HStack {
Color.red.opacity(0.3)
.frame(width: UIScreen.main.bounds.size.width * 0.6, height: nil)
Color.purple.opacity(0.4)
.frame(width: UIScreen.main.bounds.size.width * 0.2, height: nil)
Color.yellow.opacity(0.6)
.frame(width: UIScreen.main.bounds.size.width * 0.2, height: nil)
}
}
}
Using GeometryReader
struct ContentView: View {
var body: some View {
GeometryReader { geo in
HStack {
Color.red.opacity(0.3)
.frame(width: geo.size.width * 0.6, height: nil)
Color.brown.opacity(0.4)
.frame(width: geo.size.width * 0.2, height: nil)
Color.yellow.opacity(0.6)
.frame(width: geo.size.width * 0.2, height: nil)
}
}
}
}

Custom collapsible view

I'm trying to create a custom collapsible view. The code works but in Collapsible<Content: View> the VStack has strange behavior: the elements overlap when the component is closed.
To note this, try disable clipped() as shown in the image.
Is it a bug or something so stupid that I am not noticing?
Thanks in advance
FIXED CODE:
struct Collapsible<Content: View>: View {
var label: String
var content: () -> Content
init(label: String, #ViewBuilder _ content: #escaping () -> Content) {
self.label = label
self.content = content
}
#State private var collapsed: Bool = true
var body: some View {
VStack(spacing: 0) {
Button(action: {
withAnimation(.easeInOut) {
self.collapsed.toggle()
}
}, label: {
HStack {
Text(label)
Spacer(minLength: 0)
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding()
.background(Color.white.opacity(0.1))
}
)
.buttonStyle(PlainButtonStyle())
self.content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none, alignment: .top) // <- added `alignment` here
.clipped() // Comment to see the overlap
.animation(.easeOut)
.transition(.slide)
}
}
}
struct CollapsibleDemoView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Collapsible(label: "Collapsible") {
Text("Content")
.padding()
.background(Color.red)
}
Spacer(minLength: 0)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
The .frame modifier has a parameter alignment which defaults to center which results in the behaviour you're seeing: the layout bounds are set to zero height but the content is rendered vertically centered beyond the bounds (if not clipped). You can fix this by adding the alignment:
.frame(maxHeight: 0, alignment: .top)

How to set relative width in a HStack embedded in a ForEach in SwiftUI?

I wanted to create a list (without using List view) of attributes. Each attribute is a HStack which contains two texts, name and value. I want the name text to have always 30% of the width of the whole HStack and the value text to use the rest of the horizontal space. The height of each attribute depends on the content.
I try to accomplish it by having a following view:
struct FatherList: View {
let attributes: Attributes
init(_ attributes: Attributes) {
self.attributes = attributes
}
var body: some View {
VStack(spacing: CGFloat.spacing.medium) {
ForEach(
attributes,
id: \.name,
content: ChildView.init
)
}
}
}
which contains the following ChildView:
struct ChildView: View {
let listItem: Product.Attribute
init(_ attribute: Product.Attribute) {
self.attribute = attribute
}
var body: some View {
GeometryReader { geometry in
HStack(alignment: .top, spacing: 0) {
Text(attribute.name)
.bold()
.frame(width: 0.3 * geometry.size.width)
.background(Color.yellow)
Text(attribute.value)
}
.fixedSize(horizontal: false, vertical: true)
.background(Color.red)
}
}
}
And the result I get is this:
The child views overlap which is not what I want, I want the child views to expand and follow each other. I am using geometryReader to accomplish the relative width that I described above. What am I doing wrong?
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
Note: ViewHeightKey is taken from this another my solution
struct ChildView: View {
let attribute: Attribute
#State private var fitHeight = CGFloat.zero
var body: some View {
GeometryReader { geometry in
HStack(alignment: .top, spacing: 0) {
Text(self.attribute.name)
.bold()
.frame(width: 0.3 * geometry.size.width, alignment: .leading)
.background(Color.yellow)
Text(self.attribute.value)
.fixedSize(horizontal: false, vertical: true)
.frame(width: 0.7 * geometry.size.width, alignment: .leading)
}
.background(Color.red)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) { self.fitHeight = $0 }
.frame(height: fitHeight)
}
}

SwiftUI HStack with GeometryReader and paddings

In my iOS App i want to place two views of the same width so that they fill the entire width of the parent view.
For this I use GeometryReader and it broke auto layout. But auto layout does not work and the height of this view is not calculated automatically. Height of TestView is not determined, so i cant add frame size manually...
Here's what it should look like (what i expect TestView):
This is what it looks like when I put a view on a list (CurrenciesView):
TestView.swift
struct TestView: View {
var body: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1\n Test second name 2")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: geometry.size.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: geometry.size.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
}
}
}
}
CurrenciesView.swift
struct CurrenciesView: View {
#State private var items: [Str] = (0..<5).map { i in
return Str(title: "Struct #\(i)")
}
var body: some View {
NavigationView {
List {
Section(header:
TestView().listRowInsets(EdgeInsets())
) {
ForEach(items) { item in
Text("asd")
}
}.clipped()
}
.navigationBarTitle("Section Name")
.navigationBarItems(trailing: EditButton())
}
}
}
You can create a custom PreferenceKey and a view that calculates it:
struct ViewSizeKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct ViewGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: ViewSizeKey.self, value: geometry.size)
}
}
}
Then, you can use them in your views. Note that you need to use #Binding in the TestView and #State private var headerSize in the parent view. Otherwise the parent view won't be refreshed and the List won't re-calculate the header size properly.
struct CurrenciesView: View {
#State private var items: [String] = (0 ..< 5).map(String.init)
#State private var headerSize: CGSize = .zero
var body: some View {
NavigationView {
List {
Section(header:
TestView(viewSize: $headerSize)
) {
ForEach(items, id: \.self) {
Text($0)
}
}.clipped()
}
.navigationBarTitle("Section Name")
.navigationBarItems(trailing: EditButton())
}
}
}
struct TestView: View {
#Binding var viewSize: CGSize
var body: some View {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1\n Test second name 2")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: viewSize.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
VStack(alignment: .leading, spacing: 0.0) {
Text("Name 1")
.font(.system(size: 18))
.fontWeight(.bold)
HStack {
Text("123")
Text(" + 5")
}
}
.padding(.horizontal, 12.0)
.padding(.vertical, 9.0)
.frame(width: viewSize.width / 2)
.background(RoundedRectangle(cornerRadius: 8.0)
.foregroundColor(Color.blue
.opacity(0.2)))
}
.frame(maxWidth: .infinity)
.background(ViewGeometry()) // calculate the view size
.onPreferenceChange(ViewSizeKey.self) {
viewSize = $0 // assign the size to `viewSize`
}
}
}

Resources