Image with corner radius when alternating fill / fit

I have added an image inside a GeometryReader and I make the image to either scale to fit or scale to fill the GeometryReader depending on a boolean value (changed using a double tap on the image). The code bellow works fine.

My problem is that I cannot figure out how to use a corner radius on the image so that it is present in both fit/fill and follows the animation.

import SwiftUI

struct ContentView: View {

    @State var isFill = true

    var body: some View {
        VStack {

            Text("Before")
                .frame(maxWidth: .infinity)
                .padding()
                .background(.yellow)

            GeometryReader { geo in
                Image("rocket")
                    .resizable()
                    .aspectRatio(contentMode: isFill ? .fill : .fit)
                    .frame(width: geo.size.width, height: geo.size.height)
                    .clipped()
                    .cornerRadius(20)
                    .onTapGesture(count: 2) {
                        withAnimation() {
                            isFill.toggle()
                        }
                    }
            }
            .background(.white)

            Text("After")
                .frame(maxWidth: .infinity)
                .padding()
                .background(.yellow)

        }
        .padding()
        .background(.pink)
    }
}

#Preview {
    ContentView()
}

Here are 2 screenshots. The white behind the image is just a white color to visualize the frame of the GeometryReader.

0

When you apply the modifier .cornerRadius, it applies a clip shape that is a rounded rectangle. Actually, .cornerRadius is deprecated, so it’s better to apply the clip shape explicitly using .clipShape(RoundedRectangle(cornerRadius: 20)).

A clip shape is bounded by the frame of the view it is being applied to. In your example, you are applying a frame based on the size of the GeometryReader. This works for the scaled-to-fill version, because the image fills the frame. However, when scaled to fit, the rounded corners are being applied to empty space, so you don’t see them.

Quick fix

A quick way to fix is to clip the image before applying the frame, then clip again after applying the frame. Doing it this way, the first clip takes effect for the scaled-to-fit version, the second for scaled-to-fill:

GeometryReader { geo in
    Image("rocket")
        .resizable()
        .aspectRatio(contentMode: isFill ? .fill : .fit)
        .clipShape(RoundedRectangle(cornerRadius: 20))
        .frame(width: geo.size.width, height: geo.size.height)
        .clipShape(RoundedRectangle(cornerRadius: 20))
        .onTapGesture(count: 2) {
            withAnimation() {
                isFill.toggle()
            }
        }
}
.background(.white)

This works, but you will notice that the rounded corners “wander off” during the animation.

A better fix

A better way to fix is to apply the clip shape just once after constraining the size of the image to a frame of appropriate size. This way, the rounded corners remain rounded during the animation. However, it requires knowing the size of the image when it is scaled to fit.

  • One way to find the size of the image is to use a hidden version of it as a placeholder, then show the visible version as an overlay. An overlay automatically adopts the frame of the underlying view.

  • The same technique can in fact be used for scaled-to-fill, using a greedy view (such as a Color) to form the footprint. This way, a GeometryReader is not needed.

You might think that a scaled-to-fill version of the image could be used to define the footprint for the second case too. However, when scaled to fill, the image overflows the visible area, so its frame is too big. A Color works better.

Putting it together

  • A ZStack can be used to switch between the two footprints, depending on the flag.
  • The image is shown as an overlay over the ZStack (that is, over the footprint).
  • The clip shape is then applied after the overlay.

A working version is shown below. Use the ZStack to replace the GeometryReader in your original example:

ZStack {
    if isFill {
        Color.clear
    } else {
        Image("rocket")
            .resizable()
            .scaledToFit()
            .hidden()
    }
}
.overlay {
    Image("rocket")
        .resizable()
        .aspectRatio(contentMode: isFill ? .fill : .fit)
}
.clipShape(RoundedRectangle(cornerRadius: 20))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.white)
.onTapGesture(count: 2) {
    withAnimation() {
        isFill.toggle()
    }
}

1

The easiest answer, given your code and setup, accounting also maybe for some UX principles (the image is originally with straight corners fitting into a container with rounded corners), is simply to round the corners of the image container (in this case, your GeometryReader):

struct ImageCornerRadius: View {

    @State var isFill = true

    var body: some View {
        VStack {

            Text("Before")
                .frame(maxWidth: .infinity)
                .padding()
                .background(.yellow)

            GeometryReader { geo in
                Image("rocket")
                    .resizable()
                    .aspectRatio(contentMode: isFill ? .fill : .fit)
                    .frame(width: geo.size.width, height: geo.size.height)
                    //.clipped() // <- not needed
                    // .cornerRadius(20) // <- not neeeded (deprecated)
                    .onTapGesture(count: 2) {
                        withAnimation() {
                            isFill.toggle()
                        }
                    }
            }
            .background(.white)
            .clipShape(RoundedRectangle(cornerRadius: 20)) // <- clip it in RoundedRectangle


            Text("After")
                .frame(maxWidth: .infinity)
                .padding()
                .background(.yellow)

        }
        .padding()
        .background(.pink)
    }
}

#Preview {
    ImageCornerRadius()
}

Or, you can simply not use GeometryReader and use a Color view, which also stretches to fill the available space:

struct ImageCornerRadius: View {
    
    @State var isFill = true
    
    var body: some View {
        VStack {
            
            Text("Before")
                .frame(maxWidth: .infinity)
                .padding()
                .background(.yellow)
            
            Color.white // <- replaces GeometryReader
                .overlay {
                    Image("rocket")
                        .resizable()
                        .aspectRatio(contentMode: isFill ? .fill : .fit)
                        .onTapGesture(count: 2) {
                            withAnimation() {
                                isFill.toggle()
                            }
                        }
                }
                .clipShape(RoundedRectangle(cornerRadius: 20)) // <- clip it in RoundedRectangle

            
            
            Text("After")
                .frame(maxWidth: .infinity)
                .padding()
                .background(.yellow)
            
        }
        .padding()
        .background(.pink)
    }
}

#Preview {
    ImageCornerRadius()
}

The result is exactly the same with either approach.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật