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)
}
}