SwiftUI ZStack alignment works strangely - ios

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)
}
}

Related

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:

View centered in superview with view on top of it in SwiftUI

I'm trying to achieve something that is quite easy in UIKit - one view that is always in in the center (image) and the second view (text) is on top of it with some spacing between two views. I tried many different approaches (mainly using alignmentGuide but nothing worked as I'd like).
code:
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack {
Text("Test")
.padding([.bottom], 20) // I want to define spacing between two views
Image(systemName: "circle")
.resizable()
.alignmentGuide(VerticalAlignment.center, computeValue: { value in
value[VerticalAlignment.center] + value.height
})
.frame(width: 20, height: 20)
}
}
.frame(width: 100, height: 100)
result:
As you can see image is not perfectly centered and it actually depends on the padding value of the Text. Is there any way to force vertical and horizontal alignment to be centered in the superview and layout second view without affecting centered view?
I think the “correct” way to do this is to define a custom alignment:
extension VerticalAlignment {
static var custom: VerticalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
return .init(CustomAlignment.self)
}
}
Then, tell your ZStack to use the custom alignment, and use alignmentGuide to explicitly set the custom alignment on your circle:
PlaygroundPage.current.setLiveView(
ZStack(alignment: .init(horizontal: .center, vertical: .custom)) {
Color.white
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
VStack {
Text("Test")
Circle()
.stroke(Color.white)
.frame(width: 20, height: 20)
.alignmentGuide(.custom, computeValue: { $0.height / 2 })
}
}
.frame(width: 300, height: 300)
)
Result:
You can center the Image by moving it to ZStack. Then apply .alignmentGuide to the Text:
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
Text("Test")
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + $0.height }
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
}
.frame(width: 100, height: 100)
}
}
Note that as you specify the width/height of the Image explicitly:
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
you can specify the .alignmentGuide explicitly as well:
.alignmentGuide(VerticalAlignment.center) { $0[.bottom] + 50 }
Here is possible alternate, using automatic space consuming feature
Tested with Xcode 12 / iOS 14
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color.red)
VStack(spacing: 0) {
Color.clear
.overlay(
Text("Test").padding([.bottom], 10),
alignment: .bottom)
Image(systemName: "circle")
.resizable()
.frame(width: 20, height: 20)
Color.clear
}
}
.frame(width: 100, height: 100)
}
}
Note: before I used Spacer() for such purpose but with Swift 2.0 it appears spacer becomes always just a spacer, ie. nothing can be attached to it - maybe bug.

How To Position Views Relative To Their Top Left Corner In SwiftUI

How do I position views relative to their top left corner in swiftUI? The "position" modifier moves the views relative to their center coordinates. So .position(x: 0, y: 0) places a views center coordinate in the top left of the screen.
I want to place a views top left coordinate in the top left of the screen, How do I do this?
struct HomeView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Top Text")
.font(.system(size: 20))
.fontWeight(.medium)
Text("Bottom Text")
.font(.system(size: 12))
.fontWeight(.regular)
}
.position(x: 0, y: 0)
}
}
Currently, the left half of my view is cut off the screen, how do I prevent this? Also, how do I align relative to the safe area?
I miss UIKit 😪
#Asperi 's answer will solve the problem. But, I think we should use Spacer() rather than Color.clear and ZStack.
Spacer is specifically designed for these scenarios and makes the code easier to understand.
struct HomeView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Top Text")
.font(.system(size: 20))
.fontWeight(.medium)
Text("Bottom Text")
.font(.system(size: 12))
.fontWeight(.regular)
Spacer()
}
Spacer()
}
}
}
SwiftUI layout system is different from UIKit.
It asks each child view to calculate its own size based on the bounds of its parent view. Next, asks each parent to position its children within its own bounds.
https://www.hackingwithswift.com/books/ios-swiftui/how-layout-works-in-swiftui
If I correctly understand your goal the .position is not appropriate instrument for it. SwiftUI layout works better without hardcoding.
Here is possible solution. Tested with Xcode 11.4 / iOS 13.4
struct HomeView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color.clear
VStack(alignment: .leading) {
Text("Top Text")
.font(.system(size: 20))
.fontWeight(.medium)
Text("Bottom Text")
.font(.system(size: 12))
.fontWeight(.regular)
}
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
You can use:
offset(x: CGFloat = 0, y: CGFloat = 0) -> some View
So you will be relative to current object position.
https://developer.apple.com/documentation/swiftui/gaugestyleconfiguration/currentvaluelabel-swift.struct/offset(x:y:)

Alignment changes after adding a GeometryReader

I had a layout that essentially looked like this:
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ZStack {
Text("Centered")
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
.background(Color.red)
}
Group {
GeometryReader { geometry in // This GeometryReader is causing issues.
VStack {
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
}
When this is rendered, both Text elements are rendered in the center of the screen. The second text element's container takes up the entire width of the screen, which is intended. If I remove the problematic GeometryReader, then the text is properly rendered at the bottom of the screen, but obviously the frame is not set to the entire width of the screen. Why is this happening?
By default SwiftUI containers tight to content, but GeometryReader consumes maximum of available space. So if to remove second GeometryReader the VStack just wraps internal Text.
If it is still needed to keep second GeometryReader (to read width) and put text to the bottom, the simplest approach would be to add Spacer as below
Group {
GeometryReader { geometry in
VStack {
Spacer()
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
Alternate approach of how to stick view at bottom you can find in my answer in this post Position view bottom without using a spacer
How about this?
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader {geometry in
Text("Centered")
.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.red)
}
WidthReader {w in
Text("I want this at the bottom").frame(width: w)
}
}
}
}
struct WidthReader<Content: View>: View {
let widthContent: (CGFloat) -> Content
#State private var width: CGFloat = 0
#State private var height: CGFloat = 0
var body: some View {
GeometryReader {g in
widthContent(width).background(
GeometryReader {g1 in
Spacer().onAppear {height = g1.size.height}.onChange(of: g1.size.height, perform: {height = $0})
}
).onAppear {width = g.size.width}.onChange(of: g.size.width, perform: {width = $0})
}.frame(height: height)
}
}
The easiest way is to add the .fixedSize() modifier to your Stack.

How do I do this simple layout in SwiftUI?

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.

Resources