How to center crop an image in SwiftUI - ios

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.

Related

How to make multiple images in a single row have same height an correct aspect ratio?

The following is the screenshots.
The left iPhone, where there are 3 images in a single row, are having same height and correct aspect ratio. It is implemented using UIKit.
The right iPhone, where there are 3 images in a single row, are having different height and correct aspect ratio. It is implemented using SwiftUI.
I want to use SwiftUI, to achieve
All 3 images are having same height.
All 3 images are having correct aspect ratio.
In UIKit, this is how I achieve so
Step 1: Use a horizontal stack view
let horizontalStackView = UIStackView()
horizontalStackView.axis = .horizontal
horizontalStackView.distribution = .fill
horizontalStackView.alignment = .fill
horizontalStackView.spacing = spacing
Step 2: Assign correct constraint to every UIImageView based on original image dimension
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
let multiplier = CGFloat((Double)(attachment.height) / (Double)(attachment.width))
imageView.heightAnchor.constraint(
equalTo: imageView.widthAnchor,
multiplier: multiplier).isActive = true
However, I am not sure how I can implement similar idea in SwiftUI. So far, this is how I achieve imperfect outcome at the right iPhone screenshot. They are having correct aspect ratio. But they are not having same height.
SwiftUI, incorrect outcome
struct ContentView: View {
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 2) {
Image("0").resizable().aspectRatio(contentMode: .fit)
Image("3").resizable().aspectRatio(contentMode: .fit)
Image("2").resizable().aspectRatio(contentMode: .fit)
}
Text("Hello, world!")
Spacer(minLength: 0)
}
Spacer(minLength: 0)
}
}
}
Do you have idea, how I can achieve the same outcome in SwiftUI, as I did in UIKit? Thanks.
Call frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) and specify maxHeight. This ways all images will have the same size.
define aspect ratio same for all images, like this:
HStack(spacing: 2) {
Image("0").resizable().aspectRatio(CGSize(width: 3, height: 3), contentMode: .fit)
Image("1").resizable().aspectRatio(CGSize(width: 3, height: 3),contentMode: .fit)
Image("2").resizable().aspectRatio(CGSize(width: 3, height: 3),contentMode: .fit)
}
you will get the desired output
This seems to match what you did on UIKit with similar results. That is, using .fill on the images, restricting the horizontal stack to a fixed size and then calculating the HStack's height based on the device's width multiplied by the aspect ratio of the desired HStack.
This is the original code with changes:
struct ContentView: View {
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 2) {
Image("0").resizable().aspectRatio(contentMode: .fill)
Image("3").resizable().aspectRatio(contentMode: .fill)
Image("2").resizable().aspectRatio(contentMode: .fill)
}
.fixedSize(horizontal: true, vertical: false)
.frame(height: UIScreen.main.bounds.size.width * (2.1 / 7))
Text("Hello, world!").padding(.leading)
Spacer(minLength: 0)
}
Spacer(minLength: 0)
}
}
}
The result for an iphone and an ipad:
The .aspectRatio doesn't appear to work on an HStack so I used frame() instead. The magic number is a guestimate using the image of your desired layout (height / width). To match the original image, .padding(.leading) was also added.

Constraint-based layout of images in a list in SwiftUI

I want to achieve the following constraint-based layout of images in a SwiftUI List:
pin left/right edges of each Image to the list margins (adapts to screen size)
dynamic height based on aspect ratio (I'd also be ok with fixed height)
maintain image aspect ratio, and content should fill the space
What I have tried and doesn't work (based on this article):
struct MyView: View {
#ObservedObject var viewModel: MyViewModel
let aspectRatio = CGSize(width: 345, height: 120)
var body: some View {
List {
ForEach(viewModel.items) { item in
GeometryReader { geo in
Image("test_image")
.resizable()
.aspectRatio(aspectRatio, contentMode: .fill)
.frame(width: geo.size.width)
.clipped()
}
}
}
}
}
The size I get from geo is (343, 32) on iPhone 11 Pro. Width makes sense but it's not letting the cells expand beyond a height of 32 for some reason. Any tips welcome because I'm really starting to miss auto layout constraints.
No need to use GeometryReader for something like this. For the fixed height, you can just supply a frame with height only. You also don't need to create your own let aspectRatio = CGSize(width: 345, height: 120) - if you leave it nil (by default) it should be fine.
Edit: Using padding instead of VStack with spacing
struct MyView: View {
var body: some View {
List {
ForEach(0..<10, id: \.self) { item in
Image("test_image")
.resizable()
.aspectRatio(contentMode: .fill) /// no need for custom aspect ratio
.frame(height: 120) /// fixed height of image
.clipped() /// stop image from overflowing
.padding(.vertical, 12) /// extra vertical padding
}
}
}
}
Result (with "test_image"):
However, this has a fixed height of 120, so the top and bottom of the images are cropped out. To fix this, you can just avoid frame and clipped altogether.
struct MyView: View {
var body: some View {
List {
ForEach(0..<10, id: \.self) { item in
Image("test_image")
.resizable()
.aspectRatio(contentMode: .fill) /// doesn't matter if it's fit or fill
.padding(.vertical, 12) /// extra vertical padding
}
}
}
}
Result:

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.

Clip image to square in SwiftUI

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

SwiftUI view like UIKit UIView, How to change the size of the image

Is there any view in swiftui that is similar to UIKit UIView
There is an EmptyView in swiftui does anyone know what this view do
Image("image")
.resizable()
.aspectRatio(3/2, contentMode: .fill)
This messes up the image is there any other way that i can resize the image
In SwiftUI, primitive views like Color, Text, Image etc act like UIView. To change a image size you just need to give it a frame and make it resizable() first.
struct ContentView : View {
var body: some View {
Image("your image name")
.resizable()
.frame(width: 300, height: 300)
}
}
Hope it will help.
In SwiftUI, to change the image size there is 2 ways
1) Make it resizable and give some frame to Image
Image("image name")
.resizable()
.frame(width: 10, height: 10, alignment: .center)
2) Make it resizable and scaleToFit property to Image
Image("image name")
.resizable()
.scaledToFit()
For more customization of Image please look at below link
https://developer.apple.com/documentation/swiftui/image/3269652-frame
I hope it will help you!!

Resources