Clip image to square in SwiftUI

Answer based on the one by @ramzesenok but wrapped into a view modifier

Modifier:

struct FitToAspectRatio: ViewModifier {

    let aspectRatio: Double
    let contentMode: SwiftUI.ContentMode

    func body(content: Content) -> some View {
        Color.clear
            .aspectRatio(aspectRatio, contentMode: .fit)
            .overlay(
                content.aspectRatio(nil, contentMode: contentMode)
            )
            .clipShape(Rectangle())
    }

}

You can optionally also add an extension function for easy access

extension Image {
    func fitToAspect(_ aspectRatio: Double, contentMode: SwiftUI.ContentMode) -> some View {
        self.resizable()
            .scaledToFill()
            .modifier(FitToAspectRatio(aspectRatio: aspectRatio, contentMode: contentMode))
    }
}

and then simply

Image(...).fitToAspect(1, contentMode: .fill)

A ZStack will help solve this by allowing us to layer views without one effecting the layout of the other.

For the text:

.frame(minWidth: 0, maxWidth: .infinity) to expand the text horizontally to its parent's size

.frame(minHeight: 0, maxHeight: .infinity) is useful in other situations

As for the image:

.aspectRatio(contentMode: .fill) to make the image maintain its aspect ratio rather than squashing to the size of its frame.

.layoutPriority(-1) to de-prioritize laying out the image to prevent it from expanding its parent (the ZStack within the ForEach in our case).

The value for layoutPriority just needs to be lower than the parent views which will be set to 0 by default. We have to do this because SwiftUI will layout a child before its parent, and the parent has to deal with the child size unless we manually prioritize differently.

The .clipped() modifier uses the bounding frame to mask the view so you'll need to set it to clip any images that aren't already 1:1 aspect ratio.

    var body: some View {
        HStack {
            ForEach(0..<3, id: \.self) { index in
                ZStack {
                    Image(systemName: "doc.plaintext")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .layoutPriority(-1)
                    VStack {
                        Spacer()
                        Text("yes")
                            .frame(minWidth: 0, maxWidth: .infinity)
                            .background(Color.white)
                    }
                }
                .clipped()
                .aspectRatio(1, contentMode: .fit)
                .border(Color.red)
            }
        }
    }

Edit: While geometry readers are super useful I think they should be avoided whenever possible. It's cleaner to let SwiftUI do the work. This is my initial solution with a Geometry Reader that works just as well.

        HStack {
            ForEach(0..<3, id: \.self) { index in
                ZStack {
                    GeometryReader { proxy in
                        Image(systemName: "pencil")
                            .resizable()
                            .scaledToFill()
                            .frame(width: proxy.size.width)
                        VStack {
                            Spacer()
                            Text("yes")
                                .frame(width: proxy.size.width)
                                .background(Color.white)
                        }
                    }
                }
                .clipped()
                .aspectRatio(1, contentMode: .fit)
                .border(Color.red)
            }
        }


Here's another solution I found on Reddit and improved a bit:

Color.clear
    .aspectRatio(1, contentMode: .fit)
    .overlay(
        Image(imageName)
            .resizable()
            .scaledToFill()
        )
    .clipShape(Rectangle())

It is similar to Chads answer but differs in the way you put image relatively to the clear color (background vs overlay)

Bonus: to let it have circular shape just use .clipShape(Circle()) as the last modifier. Everything else stays unchanged


It works for me, but I don't know why cornerRadius is necessary...

import SwiftUI

struct ClippedImage: View {
    let imageName: String
    let width: CGFloat
    let height: CGFloat

    init(_ imageName: String, width: CGFloat, height: CGFloat) {
        self.imageName = imageName
        self.width = width
        self.height = height
    }
    var body: some View {
        ZStack {
            Image(imageName)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: width, height: height)
        }
        .cornerRadius(0) // Necessary for working
        .frame(width: width, height: height)
    }
}

struct ClippedImage_Previews: PreviewProvider {
    static var previews: some View {
        ClippedImage("dishLarge1", width: 100, height: 100)
    }
}

enter image description here

Tags:

Ios

Swiftui