LAST CHANCE: Save 50% on all my Swift books and bundles! >>

Help with Navigation Bar over top of view

Forums > SwiftUI

I can't seem to get past this issue, no matter how much I play with self.edgesForExtendedLayout. I can't figure out where to put it, if that is the solution.

I have two problems that you can see in the image. The image of the turtle is actually starting way up underneath the Navigation Bar and the name of the turtle is for some reason over top of the turtle. There is nothing in my code, I don't think, that would make any of this happen, and most of the research snds me in the direction of edges and safe areas, but I am not eperienced enough to figure it out.

  ScrollView {
      VStack {
          Image(pet.profileImage)
              .resizable()
              .aspectRatio(contentMode: .fill)
              .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 2)
              .background(Color.red)
          Text(pet.petName).foregroundColor(Color.red)
      }
  }

Screenshot

   

This is actually a pretty cool example of how SwiftUI lays out and draws its views. The culprit actually is in your code, it's the frame modifier on your Image. Here's what's happening:

The Explanation

  • You create an image using the Image(_:) initializer. When you do this, SwiftUI takes the original size of the image, and uses that as the size of the Image view. In the code samples below I'll only show the code for the Image, but it's still wrapped in the ScrollView and VStack as you had it (+ a NavigationView).
Image("turtle")

Plain Image

  • Then you make it resizable. This causes the image to stretch to fill up all the available space. This makes sense if you put it in a fixed-size container, such as if you made it the root view of your app, then it would stretch to fill the whole screen (the space available to it). ScrollView adapts its size to fit its children so I'm not a hundred percent sure how the Image gets its size here, but either way, it's safe to say this isn't what you want.
Image("turtle")
    .resizable()

Resizable Image

  • Next you append an aspectRatio modifier. This forces the Image to keep it's width and height proportional no matter the size, which prevents it from streching in one direction and looking all weird. Here it's important to dive into the two options:
    • ContentMode.fill grabs the fixed constraints of the parent, picks the larger one, and scales the image such that it fully covers all of the space of the parent, even if that means that some portion of that image will be outside of the parent. The reason I say fixed constraints is views like ScrollView, which do not have a fixed height, and therefore the Image will only consider the ScrollView's width when determining its own size.
    • ContentMode.fit grabs the fixed constraints of the parent, picks the smaller one, and scales the image such that it remains proportionally correct, but fits fully within the parent, even if it means there will be some empty space either next to or above and below the image.
Image("turtle")
    .resizable()
    .aspectRatio(contentMode: .fill)

Aspect Ratio Image

  • Lastly, you add a frame modifier. I think what you're aiming to do here is to give the image a fixed size, but it makes it go weird.
Image("turtle")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 2)

Framed Image

The issue is, that the frame modifier only changes the views bounding box, but doesn't actually force it to clip itself. Translated to English, that means that when SwiftUI looks at the image when it tries to figure out where to place it on the screen and where to put other stuff (like that text), it looks at the size defined by the frame. But the image can actually draw itself outside of that frame as well, thanks to the aspectRatio(contentMode: fill) modifier. In this case, it extends past its own frame to the top and bottom, leaking behind the nav bar and the text. I can actually illustrate this a few different ways:

  • To see the issue, you can place an overlay over the image. This overlay draws in the frame of the Image, not in its content, so it shows you the actual area SwiftUI thinks the image is using.
Image("turtle")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 2)
    .overlay(Color.red)

Overlay Image

The Solutions

So, how do we fix this? The easiest fix is to just remove the frame modifier and let the image take up all the space it wants, but that's probably not what you're looking for. One alternative is to use the aspectRatio modifier to make the Image fit the available space rather than fill it, which scales the image down so it fits the height of the container but leaves white bands on the sides.

Image("turtle")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 2)

Fitted Image

The other alternative, and probably what you want, is to add a clipped modifier to the image, which forces it to only draw within its frame and not outside.

Image("turtle")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 2)
    .clipped()

Clipped Image

Hope that helped :)

1      

That was awesome, and expalined so much of what was going wrong for me.

If there any way to know how much of the image view is under the navbar and just, say, pad it down? What I really want to do set the size of a view and have a photo, portrait or landscape, fill that view and crop out the rest.

I have that now, it's just positioned incorrectly, or at least not where I want it.

Clipped, does that, kind of. But the center point of the photo is too high and the photo getes clipped off center.

   

If the image is offset from the centre then applying an offset before the .clipped statement may help


Image("turtle")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .offset(x: <some value>, y: <some value>)
    .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width / 2)
    .clipped()

   

Is there a way to know programmaticaly how far under the navbar it is?

I am assuming any magic number I find that would on one device will probably be different on another device size.

   

Straightforward padding usually probably works best is most cases, such .padding(10).

For anything more intricate or complicated, then use GeometryReader.

   

Click here to save 50% on all my books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.