GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Resizing images to fit the screen using GeometryReader

Paul Hudson    @twostraws   

SwiftUI lets us create views with exact sizes like this:

Image(.example)
    .resizable()
    .scaledToFit()
    .frame(width: 300, height: 300)

All this works great if we want fixed-sized views, but very often you want images that automatically scale up to fill more of the screen in one or both dimensions. That is, rather than hard-coding a width of 300, what you really want to say is “make this image fill 80% of the width of the screen.”

One option is to use the containerRelativeFrame() modifier, which we covered back in project 8. But SwiftUI also gives us a dedicated type for this work called GeometryReader, and it’s remarkably powerful.

We’ll go into much more detail on GeometryReader shortly, but for now we’re going to use it for one job: to make sure our image fills some percentage of its container's width.

GeometryReader is a view just like the others we’ve used, except when we create it we’ll be handed a GeometryProxy object to use. This lets us query the environment: how big is the container? What position is our view? Are there any safe area insets? And so on.

In principle that seems simple enough, but in practice you need to use GeometryReader carefully because it automatically expands to take up available space in your layout, then positions its own content aligned to the top-left corner.

For example, we could make an image that’s 80% the width of the screen, with a fixed height of 300:

GeometryReader { proxy in
    Image(.example)
        .resizable()
        .scaledToFit()
        .frame(width: proxy.size.width * 0.8, height: 300)
}

You can even remove the height from the image, like this:

GeometryReader { proxy in
    Image(.example)
        .resizable()
        .scaledToFit()
        .frame(width: proxy.size.width * 0.8)
}

We’ve given SwiftUI enough information that it can automatically figure out the height: it knows the original width, it knows our target width, and it knows our content mode, so it understands how the target height of the image will be proportional to the target width.

Now, you're probably wondering how this is any different from using containerRelativeFrame(). Well, the problem is that containerRelativeFrame() has a very precise definition of what constitutes a "container": it might be the whole screen, it might be a NavigationStack, it might be a List or a ScrollView, and so on, but it won't consider a HStack or a VStack a container.

This causes problems when using views in stacks, because you can't easily subdivide them using containerRelativeFrame(). For example, the code below places two views in a HStack, with one being given a fixed width and the other using a container relative frame:

HStack {
    Text("IMPORTANT")
        .frame(width: 200)
        .background(.blue)

    Image(.example)
        .resizable()
        .scaledToFit()
        .containerRelativeFrame(.horizontal) { size, axis in
            size * 0.8
        }
}

That's not going to lay out well at all, because the containerRelativeFrame() will read the whole screen width for its size, meaning that image will be 80% the screen width despite 200 points of the screen being a text view.

On the other hand, using a GeometryReader will subdivide the space correctly:

GeometryReader { proxy in
    Image(.example)
        .resizable()
        .scaledToFit()
        .frame(width: proxy.size.width * 0.8)
}

Of course, that introduces a different problem: our image is now aligned to the top-left corner of the GeometryReader!

Fortunately, this is easily solved. If you ever want to center a view inside a GeometryReader, rather than aligning to the top-left corner, add a second frame that makes it fill the full space of the container, like this:

GeometryReader { proxy in
    Image(.example)
        .resizable()
        .scaledToFit()
        .frame(width: proxy.size.width * 0.8)
        .frame(width: proxy.size.width, height: proxy.size.height)
}
Hacking with Swift is sponsored by Essential Developer.

SPONSORED Transform your career with the iOS Lead Essentials. This Black Friday, unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a free crash course.

Save your spot

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.5/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.