Clip image to square in SwiftUI - ios

I am trying to put multiple cells next to each other where each cell consists of an image and a text below. The cell itself should be a square and the image should be scaled to fill the remaining space (cutting a part of the image).
First I tried just making the image square and the text below.
Now my problem is, that I don't know how to properly achieve that in SwiftUI. I can get it to work, when using this code:
VStack {
Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 200, height: 200, alignment: .center)
.clipped()
Text(recipe.name)
}
The problem is, that I have to specify a fixed frame size. What I want is a way to make a cell, that keeps an aspect ratio of 1:1 and is resizable, so I can fit a dynamic amount of them on a screen next to each other.
I also tried using
VStack {
Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.clipped()
Text(recipe.name)
}
which gives me square images, that scale dynamically. But the problem is, that the image now gets stretched to fill the square and not scaled to fill it.
My next idea was to clip it to a square shape. For that I first tried to clip it into a circle shape (because apparently there is not square shape):
VStack {
Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
Text(recipe.name)
}
But for some odd reason, it didn't really clip the image but instead kept the remaining space...
So am I not seeing something or is the only option to clip an image square the frame modifier?
EDIT
To clarify: I don't care about the text as much, as about the whole cell (or if it's simpler the image) being square, without having to specify its size via .frame and without the non-square original image being stretched to make it fit.
So the perfect solution would be that the VStack is square but getting a square image would also be okay. It should look like Image 1, but without having to use the .frame modifier.

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

This is the answer that worked for me:
VStack {
Color.clear
.aspectRatio(1, contentMode: .fit)
.background(Image(uiImage: recipe.image!)
.resizable()
.aspectRatio(contentMode: .fill))
.clipped()
Text(recipe.name)
}
I use clear color, then set the aspect ratio using fit. That makes the container square. Then I added a background of the image set it to fill. To top it off, I add clipped so the background doesn't spill over the edges of the square.

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)

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

GeometryReader for frame
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(spacing: 1) {
ForEach(recipes) { recipe in
GalleryView(width: geometry.size.width, recipe: recipe)
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
Overlapping gestures
If you have gesture actions and images close each other, add .contentShape() modifier to assign the tappable area.
Width and height get from GeometryReader of the previous View.
struct GalleryView: View {
var width: CGFloat
var recipe: Recipe
private enum C {
static let goldenRatio = CGFloat(0.67)
}
var body: some View {
VStack(spacing: 1) {
Image(uiImage: recipe.image)
.resizable()
.scaledToFill()
.frame(width: width, height: height, alignment: .center)
.clipped()
.contentShape(Rectangle())
Text(recipe.name)
}
.frame(height: width * C.goldenRatio + 1)
.frame(width: width * C.goldenRatio / 2)
}
}

Related

How to Align an image to the center of a screen using SwiftUI?

I'm trying to align an image to the center of the screen in Swift/SwiftUI.
GeometryReader { geometry in
HStack {
Spacer()
Color.white
.ignoresSafeArea()
Image("1Pzpe")
.frame(width: geometry.size.width-3, height: geometry.size.width-3, alignment: .center)
Spacer()
}
enter image description here
Instead of being centered, my image goes a little more to the right. Any help would be greatly appreciated :)
Using this modifier will let you center images
extension Image {
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
}
You probably don't need GeometryReader for this case
You are putting a Color view aside the image so it would never be centered
you probably just need a ZStack if you want the image over the color
ZStack{
Color.white
.ignoresSafeArea()
Image(systemName: "iphone")
}
to center a image in the view you only need to put the image there also you don't need GeometryReader to fill the image just use .resizable() .scaledToFit() and .padding(3)
ZStack{
Color.white
.ignoresSafeArea()
Image(systemName: "square.fill")
.resizable()
.scaledToFit()
.padding(3)
.foregroundColor(.red)
}

How can I clip/mask HStack content to behave like a ScrollView where content is shown inside HStack?

I have a custom TabView (Snap Carousel) where I scroll content and snap to the TabView Content. The content is basically a HStack x times larger in width than the TabView where x is also the number of pages. I am getting the desired scroll behaviour but when I clip the MenuTabView, I lose the contents outside the visible area, on scrolling it is empty.
HStack(spacing : 0) {
Image("image1")
.aspectRatio(2.8, contentMode: .fit)
.frame(width: proxy.size.width)
Image("image2")
.aspectRatio(2.8, contentMode: .fit)
.frame(width: proxy.size.width)
}
.frame(maxWidth: proxy.size.width, alignment: .leading)
.clipped()
proxy is the GeometryProxy.
struct MenuTabView<Content: View> : View {
var body: some View {
GeometryReader { proxy in
ZStack {
content()
.offset(x: (CGFloat(index) * -proxy.size.width) + self.offset)
.gesture(
DragGesture()
........
}
}
}
The second Image is the second page, when I clip using .clipped() it scrolls but its not visible. How can I create a behaviour similar to ScrollView
Try to move lines
.frame(maxWidth: proxy.size.width, alignment: .leading)
.clipped()
after the .offset operator

How to center crop an image in SwiftUI

I'm new to SwiftUI. I guess everyone is at this point. I've been an app developer for about 6 years now and I feel stupid asking this question on StackOverflow. But I looked everywhere. How do I center crop an image in an ImageView in SwiftUI?
I know there's an option to change the aspect ratio but I only see fit and fill. I just want the imageView to centerCrop(android term) the image. Does anybody know?
Android's ImageView.ScaleType documentation describes CENTER_CROP as:
CENTER_CROP
Scale the image uniformly (maintain the image's aspect ratio) so that
both dimensions (width and height) of the image will be equal to or
larger than the corresponding dimension of the view (minus padding).
The image is then centered in the view.
This is essentially what Aspect Fill Scaling (aka .scaledToFill()) does, except (surprisingly) Aspect Fill doesn't clip the parts that fall outside of the frame.
By making the image .resizable, and applying .scaledToFill(). the image will be scaled proportionally to fill its available frame leaving off the top and bottom or sides as necessary. .clipped() then removes the parts of the image outside of the frame.
Image("myImage")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200, alignment: .center)
.clipped()
To make this more convenient, I created this extension of Image:
extension Image {
func centerCropped() -> some View {
GeometryReader { geo in
self
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
}
}
}
To use the Image extension, just put it in a file in your project (a name like image-centercropped.swift will work nicely). Then just add .centerCropped() to any image you want to be center cropped.
Image("apolloimage").centerCropped()
It uses GeometryReader to figure out its frame so that it can crop the image correctly, which means you don't have to specify the frame to get proper clipping. You are free to size the image however you like using an explicit frame, or by just adding padding() and Spacer() to keep it nicely placed relative to other user interface items.
For example: If you want an image to fill the screen of the phone:
struct ContentView: View {
var body: some View {
Image("apolloimage")
.centerCropped()
.edgesIgnoringSafeArea(.all)
}
}
will nicely show the center of the image by scaling the image to show either the full height or the full width of the image and cropping the parts the hang over on the other dimension.
Demonstration:
Here's a demo that shows how the image is centered and cropped as the image grows. In this demo, the frame width is a constant 360 while the frame height varies from 50 to 700 as the slider advances to the right. At the beginning when the frame is short, the tops and bottoms of the image are cropped. As the frame exceeds the aspectRatio of the original image, the resulting image is centered but cropped on the left and right sides.
struct ContentView: View {
#State private var frameheight: CGFloat = 50
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("apolloimage")
.resizable()
.scaledToFill()
.frame(width: 360, height: self.frameheight)
.clipped()
Spacer()
Slider(value: self.$frameheight, in: 50...700)
.padding(.horizontal, 20)
}
}
}
or an equivalent test using .centerCropped():
struct ContentView: View {
#State private var frameheight: CGFloat = 50
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("apolloimage")
.centerCropped()
.frame(width: 360, height: self.frameheight)
Spacer()
Slider(value: self.$frameheight, in: 50...700)
.padding(.horizontal, 20)
}
}
}
Alternate Solution
Another way to make a center cropped image is to make the image an .overlay() of Color.clear. This allows Color.clear to establish the clipping bounds.
Color.clear
.overlay(
Image("apolloimage")
.resizable()
.scaledToFill()
)
.clipped()
and the corresponding extension to Image looks like this:
extension Image {
func centerCropped() -> some View {
Color.clear
.overlay(
self
.resizable()
.scaledToFill()
)
.clipped()
}
}
I was able to crop the square centre of image for view like iPhone Photo app.
extension Image {
func centerSquareCropped() -> some View {
GeometryReader { geo in
let length = geo.size.width > geo.size.height ? geo.size.height : geo.size.width
self
.resizable()
.scaledToFill()
.frame(width: length, height: length, alignment: .center)
.clipped()
}
}
}
I was originally using #vacawama's answer for this which uses the GeometryReader but discovered that actually this isn't necessary.
(I'm writing this with Xcode 13 and running in iOS15 if that makes any difference?)
Using this is sufficient...
Image(uiImage: image) // insert your own image here :D
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
I'm using this as the label parameter of a Button inside a List so the whole thing is like...
Section("Photo") {
Button {
// the action
} label: {
if let image = viewStore.imagePickerState.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.clipped()
} else {
PersonAvatarButton()
}
}
}
.aspectRatio(1, contentMode: .fill)
.listRowBackground(Color.gray)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
I haven't defined the frame anywhere for this, just the aspect ratio of the Section.
What I end up with is a square button with rounded corners and the photo all the way to the edge. Resized to fill but not squashed in any way. And the button size is determined by the screen size.
So I don't have any concrete sizes in my code.

iOS SwiftUI: prevent resizable() animation

I have a cell with an image.
The image is downloaded from the internet, and I would like to have a fade opacity transition when the image is downloaded.
This is easily accomplished with the couple of modifiers:
.transition(.opacity)
.animation(.default)
But because my image has also the .resizable() modifier I also get a scaling animation that I really don't want.
How can I prevent this?
struct GridCellView: View {
#ObservedObject var model: CellViewModel
var body: some View {
GeometryReader { proxy in
Image(uiImage: self.model.image)
.resizable()
.transition(.opacity)
.animation(.default)
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.width)
.aspectRatio(1/1, contentMode: .fit)
.clipped()
.animation(nil)
}
}
}
Use .animation(nil) right after property/ies which animations you want to disable, like below
var body: some View {
GeometryReader { proxy in
Image(uiImage: self.model.image)
.resizable()
.animation(nil) // << disables animation
.transition(.opacity)
.animation(.default) // << enables animation
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.width)
.aspectRatio(1/1, contentMode: .fit)
.clipped()
}
}
I would add a #State private var isDownloaded = false to your struct. Then when the download is finished use the ternary operator to animate the opacity. Something like this.
Image(uiImage: self.model.image)
.resizable()
.opacity(isDownloaded ? 1 : 0)
.animation(.default)

GeometryReader in SwiftUI ScrollView causes weird behaviour and random offset

Im trying to create a ScrollView that in turn contains a GeometryReader (Explicitly not the other way around). The GeometryReader should contain an (image) with a fixed width and height. However when doing this, the ScrollView pushes the view "above" the screen, it seems like it's applying some random vertical offset.
var body: some View {
ScrollView {
GeometryReader { geometry in
Image("hp")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: 400)
.clipped()
}
}
}
When removing the GeometryReader and just inserting the image it works fine.
Can somebody help?
Edit: Found a solution. I achieved the desired behaviour by telling the image that it should be as high and wide as the inner geometry reader. Then i'm setting the inner geometry reader
var body: some View {
GeometryReader { outerGeometry in
ScrollView {
VStack {
GeometryReader { innerGeometry in
Image("hp")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: innerGeometry.size.width,
height: innerGeometry.size.height)
.offset(y: innerGeometry.frame(in: .global).minY/7)
.clipped()
}
.frame(width: outerGeometry.size.width, height: 300)
}
}
}
}
Just try to put ScrollView inside GeometryReader and not vice versa:
// ...
var body: some View {
GeometryReader { geometry in
ScrollView {
Image("hp")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: 400)
.clipped()
}
}
}
// ...
and the result should be:
GeometryReader, if placed as you did, provides the size not of ScrollView itself, but of "Content View" in which image is placed. "Content View" of ScrollView is bigger than screen size, to allow spring effect at the edges, when one scrolls more than a scrolling document size.
The correct usage is in #Александр_Грабовский's answer.

Resources