SwiftUI divider taking up too much space [duplicate] - swift

Using SwiftUI, I created a VStack, which contains some fixed elements and a list element. The reason is, that the user should only scroll the area under the fixed elements. Now I see a space between the second fixed element and the list. I don't know where this space is coming from and want to get rid of it, but have no idea, how. The area is marked in red.
struct DashboardView : View, CoreDataInjected {
var body: some View {
GeometryReader { geometry in
VStack {
ScopeSelectorView().frame(maxWidth: .infinity).background(ColorPalette.gray)
BalanceNumberView().frame(maxWidth: .infinity)
List {
DashboardNavigationView(
height: geometry.size.height - ScopeSelectorView.height - BalanceNumberView.height
).frame(maxWidth: .infinity).listRowInsets(.zero)
}
}
}.background(Color.red).edgesIgnoringSafeArea(.all)
}
}

Since you didn't pass a spacing argument to VStack, it is picking a default spacing based on context. If you want no spacing, pass 0 explicitly.
VStack(spacing: 0) {
// content here
}

I use this,
.padding(.top, -8)
More detail here,
VStack(spacing: 0) {
List { ...
}
VStack{ ... }.padding(.top, -8)
}

Separately
You can use offset modifier on any view to make it looks different for each content separately:
VStack {
Circle()
Circle().offset(x: 0, y: -20)
Circle().offset(x: 0, y: 40)
}
Note that it could be negative in both directions.
All at once
Also VStack and HStack have an argument called spacing and you can set it to 0 or any other number you need to apply it to all elements.
VStack(spacing: 0) {
Circle()
Circle()
}
Note that is could be negative if needed.

Related

How do I prevent SwiftUI View from taking up any space?

I’m pretty new to SwiftUI and am working on this little project. I want to place an image either on top of or between lines, depending on the position variable.
struct ContentView: View {
#State var position = 5
var body: some View {
VStack(spacing: 20){
ForEach(1...15, id: \.self){i in
ZStack{
if i%2 != 0{
Rectangle()
.frame(height: 4)
.foregroundColor(.white)
}
if i == position{
Circle()
.frame(height: 30)
.foregroundColor(.white)
}
}
}
}
}
}
This is the result ContentViewImage:-
If i is odd, we create a line. If i equals position, we create a circle on top of the line or if we didn’t create a line the circle will be drawn between the other lines.
My problem with this is that the lines don’t stay still when I change the value of position.* This is because the circle takes up space and pushes the lines away from it. The lines above and below the circle gets pushed away more when the circle is between two lines which causes the lines to kind of go back and forth as I change from between to on top of lines.
How would I go about fixing this?
There is two issues here: non-constant height of row (because row with circle and w/o circle have different heights) and conditional layout (absent rectangles gives different layout).
Here is a possible solution. Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
#State var position = 4
var body: some View {
VStack(spacing: 20){
ForEach(1...15, id: \.self){i in
ZStack {
Rectangle()
.frame(height: 4)
.foregroundColor(i%2 == 0 ? .clear : .white) // << here !!
if i == position{
Circle()
.foregroundColor(.white)
.frame(height: 30)
}
}.frame(height: 4) // << here !!
}
}
}
}

SwiftUI layout in VStack where one child is offered a maxHeight but can use less than that height

I'm trying to build a layout inside a VStack that contains two children. The first child should take up all available space unused by the second child. The second child has a preferred size based on its own contents. I'd like to limit the height of the second child to a maximum height, but it should be able to take less than the maximum (when its own contents cannot make use of all the height). This should all be responsive to the root view size, which is the parent of the VStack (because the device can rotate).
My attempt uses the .frame(maxHeight: n) modifier, which seems to unconditionally takes up the entire n points of height, even when the view being modified doesn't use it. This results in whitespace rendered above and below the VStack's second child. This problem is shown in the Portrait preview below - the hasIdealSizeView only has a height of 57.6pts, but the frame that wraps that view has a height of 75pts.
import SwiftUI
struct StackWithOneLimitedHeightChild: View {
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
hasIdealSizeView
.frame(maxHeight: geometry.size.height / 4)
}
}
}
var fullyExpandingView: some View {
Rectangle()
.fill(Color.blue)
}
var hasIdealSizeView: some View {
HStack {
Rectangle()
.aspectRatio(5/3, contentMode: .fit)
Rectangle()
.aspectRatio(5/3, contentMode: .fit)
}
// the following modifier just prints out the resulting height of this view in the layout
.overlay(alignment: .center) {
GeometryReader { geometry in
Text("Height: \(geometry.size.height)")
.font(.system(size: 12.0))
.foregroundColor(.red)
}
}
}
}
struct StackWithOneLimitedHeightChild_Previews: PreviewProvider {
static var previews: some View {
Group {
StackWithOneLimitedHeightChild()
.previewDisplayName("Portrait")
.previewLayout(PreviewLayout.fixed(width: 200, height: 300))
StackWithOneLimitedHeightChild()
.previewDisplayName("Landscape")
.previewLayout(PreviewLayout.fixed(width: 300, height: 180))
}
}
}
This observed result is consistent with how the .frame(maxHeight: n) modifier is described in the docs and online blog posts (the flow chart here is extremely helpful). Nonetheless, I can't seem to find another way to build this type of layout.
Related question: what are the expected use cases for .frame(maxHeight: n)? It seems to do the opposite of what I'd expect by unconditionally wrapping the view in a frame that is at least n points in height. It seems no different than .frame(height: n), using an explicit value for the offered height.
The behavior of .minHeight in this example is strange and far from intuitive. But I found a solution using a slightly different route:
This defines the minHeight for the expanding view (to get the desired layout in portrait mode), but adds a .layoutPriority to the second, making it define itself first and then give the remaining space to the upper view.
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
.frame(minHeight: geometry.size.height / 4 * 3)
hasIdealSizeView
.layoutPriority(1)
}
}
}
There's probably a really short way to go about this but in the meantime here is what I did.
Firstly I created a struct for your hasIdealSizeView and I made it return a GeometryProxy, and with that i could return the height of the HStack, in this case, the same height you were printing on to the Text View. then with that I used the return proxy to check if the height is greater than the maximum, and if it is, i set it to the maximum, otherwise, set the height to nil, which basically allows the native SwiftUI flexible height:
//
// ContentView.swift
// Test
//
// Created by Denzel Anderson on 3/16/22.
//
import SwiftUI
struct StackWithOneLimitedHeightChild: View {
#State var viewHeight: CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
fullyExpandingView
.overlay(Text("\(viewHeight)"))
// GeometryReader { geo in
hasIdealSizeView { proxy in
viewHeight = proxy.size.height
}
.frame(height: viewHeight > geometry.size.height / 4 ? geometry.size.height / 4:nil)
}
.background(Color.green)
}
}
var fullyExpandingView: some View {
Rectangle()
.fill(Color.blue)
}
}
struct hasIdealSizeView: View {
var height: (GeometryProxy)->()
var body: some View {
HStack {
Rectangle()
.fill(.white)
.aspectRatio(5/3, contentMode: .fit)
Rectangle()
.fill(.white)
.aspectRatio(5/3, contentMode: .fit)
}
// the following modifier just prints out the resulting height of this view in the layout
.overlay(alignment: .center) {
GeometryReader { geometry in
Text("Height: \(geometry.size.height)")
.font(.system(size: 12.0))
.foregroundColor(.red)
.onAppear {
height(geometry)
}
}
}
}
}

How to remove bottom default padding from List view in SwiftUI

I'm trying to remove the bottom padding on a List view when another view is being defined after it so that the List view is sitting on top of the other one. Not overlaying. There seems to be some extra space created from the List and I'm not sure how to remove that to achieve what I want. Here's what I have.
List {
ForEach(0...2, id: \.self) { color in
Text("\(color)")
}.listRowInsets(EdgeInsets())
}
Rectangle().fill(Color.red)
Here's what that looks like. You can see that there is a gap between row 2 and the red rectangle. What I'd like is for the boundaries of those two views to be adjacent to each other.
EDIT: I've tried embedding the code in a VStack but that doesn't remove the white gap we see below.
Looking at this answer, it seems like wrapping the list in a VStack would remove the gap but it’s not from what we can see here?
VStack(spacing: 0) {
VStack(spacing: 0) {
List {
ForEach(0...0, id: \.self) { number in
Text("1")
.listRowBackground(Color.blue)
Text("2")
}.listRowBackground(Color.gray)
}
}
VStack(spacing: 0) {
HStack(spacing: 0) {
Rectangle()
.fill(Color.red)
Rectangle()
.fill(Color.red)
}
HStack(spacing: 0) {
Rectangle()
.fill(Color.red)
Rectangle()
.fill(Color.red)
}
}
.aspectRatio(1.0, contentMode: .fill)
VStack {
List {
Text("1")
.listRowBackground(Color.purple)
Text("2")
.listRowBackground(Color.pink)
Text("3")
.listRowBackground(Color.blue)
Text("4")
.listRowBackground(Color.orange)
}
}
}
I've tried multiple variations of VStacks with spacing equals 0 and haven't found how to remove that gap that's created by the List view. If there are more rows added, it begins to fill up but I'm trying to just have two rows with no white gap sit above the red rectangle.
We can see the List view highlighted below includes that gap.

Is there a way to eliminate outside edge of padding in SwiftUI?

So I've noticed when using padding() it creates an outside space/edge outside a view frame. Is there a way to eliminate that outside edge?
Here is .padding(.top, 0)
Here is .padding(.top, 1) the extra outside edge has appeared
Here is .padding(.top, 10) the outside edge remains on any padding above 1px from what I see.
import SwiftUI
struct TestView: View {
var body: some View {
VStack {
Text("")
.frame(width: 300, height: 20)
.background(Color(.black))
.cornerRadius(10)
Text("")
.frame(width: 300, height: 20)
.background(Color(.black))
.cornerRadius(10)
.padding(.top, 0)//here you can change the 0 to 1
}
}
}
The reason this happens is because the VStack automatically has its own spacing. Replace:
VStack {
/* ... */
}
with:
VStack(spacing: 0) {
/* ... */
}
This removes padding between each view within the VStack. I am assuming SwiftUI makes an assumption that if you want 0 padding, you want them touching, otherwise you likely want padding from within its own space within the VStack.

How to use GeometryReader within a LazyVGrid

I'm building a grid with cards which have an image view at the top and some text at the bottom. Here is the swift UI code for the component:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
This component outputs the following layout:
This Looks great, but I want to add a Geometry reader into the Card component in order to scale the top image view according to the width of the enclosing grid column. As far as I know, that code should look like the following:
struct Main: View {
var body: some View {
ScrollView {
LazyVGrid(columns: .init(repeating: .init(.flexible()), count: 2)) {
ForEach(0..<6) { _ in
ZStack {
Rectangle()
.foregroundColor(Color(UIColor.random))
VStack {
GeometryReader { geometry in
Rectangle()
.frame(minHeight: 72)
Text(ipsum)
.fixedSize(horizontal: false, vertical: true)
.padding()
}
}
}.clipShape(RoundedRectangle(cornerRadius: 10))
}
}.padding()
}.frame(width: 400, height: 600)
}
}
The trouble is that this renders as the following:
As you can see, I'm not even trying to use the GeometryReader, I've just added it. If I add the geometry reader at the top level, It will render the grid correctly, however this is not of great use to me because I plan to abstract the components into other View conforming structs. Additionally, GeometryReader seems to be contextually useful, and it wouldn't make sense to do a bunch of math to cut the width value in half and then make my calculations from there considering the geometry would be from the top level (full width).
Am I using geometry reader incorrectly? My understanding is that it can be used anywhere in the component tree, not just at the top level.
Thanks for taking a look!
I had the same problem as you, but I've worked it out. Here's some key point.
If you set GeometryReader inside LazyVGrid and Foreach, according to SwiftUI layout rule, GeometryReader will get the suggested size (may be just 10 point). More importantly, No matter what subview inside GeometryReader, it wouldn't affect the size of GeometryReader and GeometryReader's parent view.
For this reason, your view appears as a long strip of black. You can control height by setting GeometryReader { subView }.frame(some size),
Generally, we need two GeometryReader to implement this. The first one can get size and do some Computing operations, then pass to second one.
(Since my original code contains Chinese, it may be hard for you to read, so I can only give a simple structure for you.)
GeometryReader { firstGeo in
LazyVGrid(columns: rows) {
ForEach(dataList) { data in
GeometryReader { secondGeo in
// subview
}
.frame(width: widthYouWantSubViewGet)
}
}
}
I just started to learn swift for a week. There may be some mistakes in my understanding. You are welcome to help correct it.