How do I do this simple layout in SwiftUI? - ios

I need a MainView that is centered on the screen, and a flexible HeaderView that takes up the remaining space between the MainView and the top of the screen (see below). How do I accomplish this in SwiftUI?
Starter code:
struct TestView: View {
var body: some View {
ZStack {
//Center Line
Divider()
VStack {
Text("HeaderView")
.border(Color.orange, width: 6)
Text("MainView")
.frame(width: 400, height: 200, alignment: .center)
.border(Color.red, width: 6)
}
}
}
}

VStack {
// Let the HeaderView expand to whatever is available, in both directions
Text("HeaderView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.border(Color.orange, width: 6)
Text("MainView")
.frame(maxWidth: .infinity) // Added to allow this view to expand horizontally
.frame(height: 200) // alignment is not needed here.
.border(Color.red, width: 6)
// And then add a Spacer() at the end that also has flexible height
Spacer()
.frame(maxHeight: .infinity)
}
Since Main is fixed in height, it will get its requested height first. Then since Header and Spacer are equally prioritized and flexible, they will each get half of what remains, causing Main to be centered vertically.

Related

Get elements to fill width screen in LazyHStack

I have a LazyHstack in a horizontal scrollview :
struct HorizontalSpotsList: View {
var spots: [Spot]
var body: some View {
ScrollView(.horizontal){
LazyHStack(alignment: .center, spacing: 0){
if (!spots.isEmpty){
ForEach(spots.prefix(upTo: 3) , id: \.self) { spot in
Text("Test").frame(maxWidth: .infinity)
.background(RoundedRectangle(cornerRadius: 25)
.fill(Color.white)).border(Color.blue)
}
}
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).border(Color.red)
}.frame(width: .infinity, height: 130).border(Color.green)
}
}
How can i get my lazyHStack elements to fill all the width screen ?
My LazyHstack doesn't seems to fill the width, i tried to apply .frame(width: .infinity) or .frame(maxWidth: .infinity) to different combinaisons of my scrollView / LazyHstack / Text element but it doesn't seems to work.
You could use Geometry Reader. You would have to apply a height however.
GeometryReader { geo in
ScrollView {
LazyHStack {
content
}.frame(width: geo.size.width, height: #)
}
}
Try adding Spacer() in between the Text elements.

Adjusting the frame of a SwiftUI view with a DragGesture resizes it in all dimensions

I'm trying to implement a simple view in SwiftUI that can be resized via a drag handle placed on its lower-left corner. The resizable view's dimensions are set via two #State variables controlling the width and height. These variables are mutated within a DragGesture's onChanged handler captured by the drag handle.
It's sort of working, but not in the way I expect. When I perform a drag gesture on the drag handle, the view gets resized in all dimensions. I want the resizing to work in the same way as a desktop window; dragging shouldn't resize the view in all directions.
Below is a recording from the iOS simulator showing the incorrect behaviour:
I understand that the centre of a SwiftUI view is considered its origin, and that's probably what's causing this behaviour. I'm not sure how to work around this.
Here's my code:
import SwiftUI
struct ContentView: View {
// Initialise to a size proportional to the screen dimensions.
#State private var width = UIScreen.main.bounds.size.width / 3.5
#State private var height = UIScreen.main.bounds.size.height / 1.5
var body: some View {
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .center)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
}
Edit: I was too liberal in what I removed for the minimal example above. The following snippet adds a position constraint on the ZStack, which is used in another gesture (not shown) so the view can be dragged around.
I'm guessing this is problematic because position specifies relative coordinates from the centre of the ZStack to the parent VStack, and as the ZStack is resized, it's being "moved" to maintain the position constraint.
import SwiftUI
struct ContentView: View {
// Initialise to a size proportional to the screen dimensions.
#State private var width = UIScreen.main.bounds.size.width / 3.5
#State private var height = UIScreen.main.bounds.size.height / 1.5
var body: some View {
VStack {
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height)
.border(.red, width: 5)
.background(.yellow)
.padding()
// This position constraint causes the ZStack to grow/shrink in all dimensions
// when resized.
.position(x: 400, y: 400)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
You're right that the issue is caused by your view being center-aligned.
To fix this, you can wrap your view in a VStack with a different alignment applied, e.g. .topLeading if you want it to align to the top-left.
You also have to make sure this VStack is taking up the available space of the view. Otherwise, it will shrink to the size of your resizable box, causing the view to stay center-aligned. You can achieve this with .frame(maxWidth: .infinity, maxHeight: .infinity).
TL;DR
Place your resizable box in a VStack with a .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) modifier.
VStack { // <-- Wrapping VStack with alignment modifier
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .topLeading)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Edit
With an additional position modifier on the ZStack, try offsetting the x and y values by half of the width and height to position relative to the top-left origin of the ZStack:
.position(x: 400 + width / 2, y: 400 + height / 2)

SwiftUI ZStack alignment works strangely

I created very simple views with SwiftUI including ZStack.
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Text("aaa")
.frame(width: 50, height: 50)
.font(.system(size: 20))
}
.frame(width: 142.0, height: 142.0)
.background(.pink)
}
}
I expected that the Text("aaa") would appear in the top leading of pink square.
But the result was this.
More strangely, it works well if I add 'Color.clear' to ZStack like this.
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color.clear
Text("aaa")
.frame(width: 50, height: 50)
.font(.system(size: 20))
}
.frame(width: 142.0, height: 142.0)
.background(.pink)
}
}
And the result it this.
I cannot understand this situation. SwiftUI is totally crazy.
Does anybody know about this?
The elements of the ZStack are laid out and aligned based on the size of the largest child, not the frame of the ZStack itself.
You should think of the "content area" of the stack being seperate to the frame of the stack.
When you add a Color.clear, its default frame is unbounded (the height and width are infinity), so when it is added to the ZStack, it will grow the "content area" to the maximum possible size.
This makes the content area of the stack the same size as the ZStacks frame.
You can achieve the same result (in a clearer way) by using a Spacer() with explicit infinite bounds.
This will ensure the children always fill the same available to them.
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Spacer()
.frame(maxWidth: .infinity, maxHeight: .infinity)
Text("aaa")
.frame(width: 50, height: 50)
.font(.system(size: 20))
}
.frame(width: 142.0, height: 142.0)
.background(.pink)
}
}

how to disable ScrollView clipping content in SwiftUI

I need that red rectangle should be visible until reach to the leading or trailing edge of the device.
So the problem is scrollview cliping red boxes (hiding) as move beyond the scrollview container size.
code:
struct test: View {
var body: some View {
ScrollView(.horizontal,showsIndicators:false) {
LazyHStack{
ForEach(0...4,id:\.self){i in
Rectangle()
.fill(Color.red)
.frame(width: 60, height: 50, alignment: .center)
}
}.padding(.horizontal, 4)
}.background(Color.yellow)
.frame(width: 68, height: 60, alignment: .trailing)
}
}
Results:
Expected:(I also produce this result by setting ScrollView full width and adding padding (.padding(.horizontal, UIScreen.main.bounds.width/2 )) to LazyHStack)
But it is a hack by adding space at start and end, and problem remain unresolved, that is clipping of ScrollView content
I'm not exactly sure what the problem is, but you probably want the padding to be on the Text and not the ScrollView. Correct me if this isn't what you are looking for.
struct test: View {
var body: some View {
ScrollView(.horizontal) {
Text("Hello, World!")
.frame(width: UIScreen.main.bounds.width, alignment: .leading)
.padding(.horizontal, 40)
}
.background(Color.yellow)
}
}
Result:
It seems to be working for me when I remove the line: .frame(width: UIScreen.main.bounds.width, alignment: .leading).
On an iPhone XR, I'm seeing a horizontal ScrollView with a yellow background starting at 40 offset from leading and ending at 40 offset from trailing. Is this what you're trying to achieve?
Also, I'm pretty sure UIScreen.main.bounds.width is going to return the width of the device, which will be a problem if you want your text to take up 80 pixels less than that value (since your ScrollView has 40 padding either side).
if I understand correct from your answers, this is the result you want:
struct test: View {
var body: some View {
GeometryReader { geo in
VStack {
Spacer()
HStack {
ScrollView(.horizontal,showsIndicators:false) {
LazyHStack{
ForEach(0...4,id:\.self){i in
Rectangle()
.fill(Color.red)
.frame(width: 60, height: 50, alignment: .center)
}
}
.padding(.horizontal,40)
}
.background(Color.yellow)
.frame(width: geo.size.width, height: 60, alignment: .center)
}
Spacer()
}
}
}
}
Result:

SwiftUI. How to set shrink behavior for Spacer?

How to set default spacing between rgb views (100pt) if their container (VStack) not conflicts with bottom black view.(like iPhone 11 Pro Max). BUT shrinks if there is no space for 100p height.(like iPhone SE on the screenshot)
My code:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 0) {
Rectangle()
.foregroundColor(.red)
.frame(height: 100)
Spacer()
.frame(minHeight: 10, maxHeight: 100)
Rectangle()
.foregroundColor(.green)
.frame(height: 100)
Spacer()
.frame(minHeight: 10, maxHeight: 100)
Rectangle()
.foregroundColor(.blue)
.frame(height: 100)
}
Spacer()
.frame(minHeight: 10, maxHeight: 600)
Rectangle() // keyboard
.frame(height: 200)
}
}
}
So the problem is:
Spacers with maxHeight: 100 have height = 10 (not 100) on iPhone 11 Pro Max. (BUT space between black view and VStack allows it)
How to make behavior I explained?
You need to use idealHeight alongside with .fixedSize modifier for Spacers:
Spacer()
.frame(minHeight: 10, idealHeight: 100, maxHeight: 600)
.fixedSize()
Use Spacer(minLength: 10) for the last spacer.
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 0) {
Rectangle()
.foregroundColor(.red)
.frame(height: 100)
Spacer()
.frame(minHeight: 10, maxHeight: 100)
Rectangle()
.foregroundColor(.green)
.frame(height: 100)
Spacer()
.frame(minHeight: 10, maxHeight: 100)
Rectangle()
.foregroundColor(.blue)
.frame(height: 100)
}
Spacer(minLength: 10)
Rectangle() // keyboard
.frame(height: 200)
}
}
}
The problem in your code is that when you wrap a Spacer inside a frame like Spacer().frame(minHeight: 10, maxHeight: 600), it is first considered as a frame, then a Spacer inside that frame. And a frame has equal default layout priority as other views. So the parent will propose it the same amount of space as the inner VStack. By removing the frame modifier, the Spacer has the least layout priority, so the inner VStack will take as much space as possible except the minimum 10 points claimed by the spacer and 200 points for the rectangle.
I ran into a similar problem...
This fixed it for me:
Spacer().frame(minWidth: 0)

Resources