Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: Can't align image to the top of ZStack

Tags:

ios

swift

swiftui

I can't figure out how to align Image view on top of ZStack, by default views in SwiftUI are placed at the center of their parent, and we then use stacks to align them, I have the following piece of code:

struct ContentView: View {
    var body: some View {
        ZStack {
           Image("bgImage")
           Text("Hello, World!")
        }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.red) //this is for debugging purposes, to show the area of the ZStack
    }
}

How can I position the image to the top ?

like image 687
JAHelia Avatar asked Jan 24 '20 20:01

JAHelia


1 Answers

To tell the ZStack to align things a particular way within it, configure it with the alignment parameter:

ZStack(alignment: .top) {
    Color.clear
    Image(...)
    Text("Hello, World!")
}

(Color.clear expands to fill all available space, so this forces your ZStack to be as large as the enclosing view without needing to add a .frame().)

That will align everything at the top of course, which might not be what you want. You could fix that by making a nesting your ZStacks to align as you want them to:

ZStack{
    ZStack(alignment: .top) {
        Color.clear
        Image(...)        // This will be at the top
    }
    Text("Hello, World!") // This will be centered
}

That said, I'd probably use a .background for this example.

ZStack {
    Color.clear
    Text("Hello, World!")
}
.background(Image(...), alignment: .top)

And if you only have one view, you can get rid of the ZStack and use a frame instead:

Text("Hello, World!")
    .frame(maxHeight: .infinity)
    .background(Image(uiImage:#imageLiteral(resourceName: "image.jpg")), 
                alignment: .top)

Keep in mind that in this case the image will draw outside its frame. In many cases that's fine (and it's completely legal), but it can matter sometimes (for example, if you put this inside a stack). You can add .border(Color.green) to the end to see how that works.


This example really gets to the heart of SwiftUI layout, so it's worth understanding what's going on. This isn't a workaround or a trick, so you should get to the place where this feels very normal.

The top-level content view (the one that contains the ZStack) offers its entire space to the ZStack. A ZStack is always exactly the size that contains its contents, so first the ZStack needs to layout its children. It lays them out according to its alignment, and then sizes itself exactly to fit around them. So with top-alignment (but without Color.clear), the Image is at the top of the ZStack. The ZStack is just exactly the same size as the Image.

The top-level content view then places the ZStack in its center.

The way the ZStack lays out its children is similar to how the content view did. It offers all the space it was offered, and then the child-views decide their sizes. Views always decide their own sizes. The Image and Text are fixed-sized views, so they are just the size of their contents. But Color is a flexible-sized view. It accepts the entire space that the ZStack offered (which is the same space that the top-level content view offered) and returns that as its size. Since a ZStack must exactly contain its children, the ZStack is now the size of the top-level content view, and things behave as you expect, aligning at the top of the screen.

Let's compare to using .frame() as you originally did:

ZStack {
   Image("bgImage")
   Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack

First, I want to focus on your comment, because it's not correct. This is not the area of the ZStack. The ZStack is exactly the size of its contents (the Image and the Text). You put the background on the frame view, and the frame view is larger.

A key confusion people have is that they think .frame(...) changes the size of the View it's attached to. That's not correct at all. As before, a ZStack is always the size of its contents. .frame() creates a completely new view of the requested size. It then positions the wrapped view inside itself according to the frame's alignment. So in this example it works like this:

Top-level - Background - Frame - ZStack - { Image Text }

The top-level view offers all its space to the Background. Backgrounds are the size of what they contain, so it offers all of that space to the Frame. The Frame is flexible in both directions (due to the max fields), and so it ignores its child's size and chooses to be the size it was offered.

The Frame then offers all that space to the ZStack. The ZStack lays out its children, and returns its size as exactly the size that contains them.

The Frame then places the ZStack according to the Frame's alignment (.center, since that's the default). If you'd set the Frame's alignment to .top, then the ZStack would have been placed at the top of the frame (but the text would be centered in the ZStack not in the Frame).

It then reports to the Background that it is as large as the top-level view (since its flexible).

The Background then claims that same size to the top-level content view.

And finally, the top-level content view places the Background in its center.

like image 148
Rob Napier Avatar answered Nov 01 '22 15:11

Rob Napier