I have an UIImageView with contentMode = .aspectFit. I have an image in imageView, which dimension is bigger than size of imageView. User can draw some lines and save them as sublayer. After that I need to save the edited image. But quality of saved image is worse than quality of image which I load.
What am I doing wrong? I tried to use transform, but it didn't work.
import UIKit
extension UIImageView {
var contentClippingRect: CGRect {
let imgViewSize = self.frame.size
let imgSize = self.image?.size ?? .zero
let scaleW = imgViewSize.width / imgSize.width
let scaleH = imgViewSize.height / imgSize.height
let aspect = fmin(scaleW, scaleH)
let width = imgSize.width * aspect
let height = imgSize.height * aspect
let imageRect = CGRect(x: (imgViewSize.width-width)/2 + self.frame.origin.x, y: (imgViewSize.height-height)/2 + self.frame.origin.y, width: width, height: height)
return imageRect
}
func asImage() -> UIImage {
let imageRect = self.contentClippingRect
let renderer = UIGraphicsImageRenderer(bounds: imageRect)
let renderedImage = renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
return renderedImage
}
}
You can use UIGraphicsImageRendererFormat().scale it will increase quality of the rendered image a bit.
import UIKit
extension UIImageView {
var contentClippingRect: CGRect {
let imgViewSize = self.frame.size
let imgSize = self.image?.size ?? .zero
let scaleW = imgViewSize.width / imgSize.width
let scaleH = imgViewSize.height / imgSize.height
let aspect = fmin(scaleW, scaleH)
let width = imgSize.width * aspect
let height = imgSize.height * aspect
let imageRect = CGRect(x: (imgViewSize.width-width)/2 + self.frame.origin.x, y: (imgViewSize.height-height)/2 + self.frame.origin.y, width: width, height: height)
return imageRect
}
func asImage() -> UIImage {
let imageRect = self.contentClippingRect
//add this
let format = UIGraphicsImageRendererFormat()
format.scale = 2
let renderer = UIGraphicsImageRenderer(bounds: imageRect, format: format)
let renderedImage = renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
return renderedImage
}
}
I have an image in a UIImageView:
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .red
because of .scaleAspectFit the image view has some red borders and thats OK:
User can added some UIView like label or images over the imageView.
In final step I used the following code to save edited image and user can share it or save it to photo library:
private func generateImage() -> UIImage? {
var finalImage: UIImage?
UIGraphicsBeginImageContextWithOptions(CGSize(width: imageView.frame.size.width, height: imageView.frame.size.height), true, 0)
imageView.drawHierarchy(in: CGRect(x: 0, y: 0, width: imageView.frame.size.width, height: imageView.frame.size.height), afterScreenUpdates: true)
finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
The problem is that the finalImage still has the red borders from imageView.
You can get CGRect of the UIImage displayed in the UIImageView in AspectFit content mode. Please create extension of UIImageView like this,
extension UIImageView {
var contentClippingRect: CGRect {
guard let image = image else { return bounds }
guard contentMode == .scaleAspectFit else { return bounds }
guard image.size.width > 0 && image.size.height > 0 else { return bounds }
let scale: CGFloat
if image.size.width > image.size.height {
scale = bounds.width / image.size.width
} else {
scale = bounds.height / image.size.height
}
let size = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let x = (bounds.width - size.width) / 2.0
let y = (bounds.height - size.height) / 2.0
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
}
You can now use imageView.contentClippingRect to read how read the position and size of the image inside.
You have to do minor changes in your method, call your function with appropriate bounds as contentClippingRect.
Let me know in case of any queries.
UPDATE
Please try this UIImageView+Extension, this might help you. It is in Objective-C code, convert it in Swift.
You can try this as well,
let image = #imageLiteral(resourceName: "Cat03")
let x: CGRect = AVMakeRect(aspectRatio: image.size, insideRect: imageView1.frame)
print(x)
Above code gives you size perfectly.
I am trying to crop this image, which is a SKSpriteNode:
I am trying to crop this image from the top, so that I maintain the bottom semi circle of this shape. For instance, it'd be cropped to this:
So I use these two methods to accomplish this task:
func recalculateScore() {
currentScore -= decreaseRate
let image = UIImage(cgImage: (vial.texture?.cgImage())!)
vial.texture = SKTexture(image: cropBottomImage(image: image))
}
func cropBottomImage(image: UIImage) -> UIImage {
let height = CGFloat(image.size.height / 3)
let rect = CGRect(x: 0, y: image.size.height - height, width: image.size.width, height: height)
return cropImage(image: image, toRect: rect)
}
func cropImage(image:UIImage, toRect rect:CGRect) -> UIImage {
let imageRef:CGImage = image.cgImage!.cropping(to: rect)!
let croppedImage:UIImage = UIImage(cgImage:imageRef)
return croppedImage
}
However, this leads to this result:
It is as if it was being compressed. I think my issue might be in this line:
let rect = CGRect(x: 0, y: image.size.height - height, width: image.size.width, height: height)
Does the CGRect coordinate of (0,0) lie within the top most left corner? I am a bit confused on what the x and y parameters for the CGRect mean?
Resize your sprite, what is happening is the cropped texture is stretching to fill the sprite, and since you only crop vertically, it will only stretch vertically
func recalculateScore() {
currentScore -= decreaseRate
let image = UIImage(cgImage: (vial.texture?.cgImage())!)
vial.texture = SKTexture(image: cropBottomImage(image: image))
vial.size = vial.texture.size
}
Im currently editing an UIImage with core graphics
The UIImage content mode is set to .aspectFill
When I draw on the image I want to keep the scale / ratio of the image.
Ive tried the following code but it returns .aspectFit values rather then .AspectFill
let imageSize = AVMakeRect(aspectRatio: (mainImageView.image?.size)!, insideRect: mainImageView.frame)
Any help would be greatly appreciated
Thomas
That function can only do an aspect fit.
Here is a function that will do an aspect fill:
func makeFillRect(aspectRatio: CGSize, insideRect: CGRect) -> CGRect {
let aspectRatioFraction = aspectRatio.width / aspectRatio.height
let insideRectFraction = insideRect.size.width / insideRect.size.height
let r: CGRect
if (aspectRatioFraction > insideRectFraction) {
let w = insideRect.size.height * aspectRatioFraction
r = CGRect(x: (insideRect.size.width - w)/2, y: 0, width: w, height: insideRect.size.height)
} else {
let h = insideRect.size.width / aspectRatioFraction
r = CGRect(x: 0, y: (insideRect.size.height - h)/2, width: insideRect.size.width, height: h)
}
return r
}
In my iPhone app, I need to provide the user with an ability to zoom/pan a large-ish image on the screen. This is quite simple: I use UIScrollView, set max/min scale factors and zooming/panning works as expected. Here's where things get interesting. The image is a dynamic one, received from a server. It can have any dimensions. When the image first loads, it's scaled down (if needed) to fit completely into the UIScrollView and is centered in the scroll view - the screenshot is below:
Because the proportions of the image are different from those of the scroll view, there's white space added above and below the image so that the image is centered. However when I start zooming the image, the actual image becomes large enough to fill the whole of the scrollview viewport, therefore white paddings at top/bottom are not needed anymore, however they remain there, as can be seen from this screenshot:
I believe this is due to the fact that the UIImageView containing the image is automatically sized to fill the whole of UIScrollView and when zoomed, it just grows proportionally. It has scale mode set to Aspect Fit. UIScrollView's delegate viewForZoomingInScrollView simply returns the image view.
I attempted to recalculate and re-set UIScrollView, contentSize and image view's size in scrollViewDidEndZooming method:
CGSize imgViewSize = imageView.frame.size;
CGSize imageSize = imageView.image.size;
CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}
scrollView.contentSize = realImgSize;
CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
imageView.frame = fr;
However this was only making things worse (with bounds still being there but panning not working in the vertical direction).
Is there any way to automatically reduce that whitespace as it becomes unneeded and then increment again during zoom-in? I suspect the work will need to be done in scrollViewDidEndZooming, but I'm not too sure what that code needs to be.
Awesome!
Thanks for the code :)
Just thought I'd add to this as I changed it slightly to improve the behaviour.
// make the change during scrollViewDidScroll instead of didEndScrolling...
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
CGSize imgViewSize = self.imageView.frame.size;
CGSize imageSize = self.imageView.image.size;
CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}
CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
self.imageView.frame = fr;
CGSize scrSize = scrollView.frame.size;
float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
// don't animate the change.
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
}
Here's my solution that works universally with any tab bar or navigation bar combination or w/o both, translucent or not.
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// The scroll view has zoomed, so you need to re-center the contents
CGSize scrollViewSize = [self scrollViewVisibleSize];
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
CGPoint imageCenter = CGPointMake(self.scrollView.contentSize.width/2.0,
self.scrollView.contentSize.height/2.0);
CGPoint scrollViewCenter = [self scrollViewCenter];
//if image is smaller than the scrollView visible size - fix the image center accordingly
if (self.scrollView.contentSize.width < scrollViewSize.width) {
imageCenter.x = scrollViewCenter.x;
}
if (self.scrollView.contentSize.height < scrollViewSize.height) {
imageCenter.y = scrollViewCenter.y;
}
self.imageView.center = imageCenter;
}
//return the scroll view center
- (CGPoint)scrollViewCenter {
CGSize scrollViewSize = [self scrollViewVisibleSize];
return CGPointMake(scrollViewSize.width/2.0, scrollViewSize.height/2.0);
}
// Return scrollview size without the area overlapping with tab and nav bar.
- (CGSize) scrollViewVisibleSize {
UIEdgeInsets contentInset = self.scrollView.contentInset;
CGSize scrollViewSize = CGRectStandardize(self.scrollView.bounds).size;
CGFloat width = scrollViewSize.width - contentInset.left - contentInset.right;
CGFloat height = scrollViewSize.height - contentInset.top - contentInset.bottom;
return CGSizeMake(width, height);
}
Swift 5:
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerScrollViewContents()
}
private var scrollViewVisibleSize: CGSize {
let contentInset = scrollView.contentInset
let scrollViewSize = scrollView.bounds.standardized.size
let width = scrollViewSize.width - contentInset.left - contentInset.right
let height = scrollViewSize.height - contentInset.top - contentInset.bottom
return CGSize(width:width, height:height)
}
private var scrollViewCenter: CGPoint {
let scrollViewSize = self.scrollViewVisibleSize()
return CGPoint(x: scrollViewSize.width / 2.0,
y: scrollViewSize.height / 2.0)
}
private func centerScrollViewContents() {
guard let image = imageView.image else {
return
}
let imgViewSize = imageView.frame.size
let imageSize = image.size
var realImgSize: CGSize
if imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height {
realImgSize = CGSize(width: imgViewSize.width,height: imgViewSize.width / imageSize.width * imageSize.height)
} else {
realImgSize = CGSize(width: imgViewSize.height / imageSize.height * imageSize.width, height: imgViewSize.height)
}
var frame = CGRect.zero
frame.size = realImgSize
imageView.frame = frame
let screenSize = scrollView.frame.size
let offx = screenSize.width > realImgSize.width ? (screenSize.width - realImgSize.width) / 2 : 0
let offy = screenSize.height > realImgSize.height ? (screenSize.height - realImgSize.height) / 2 : 0
scrollView.contentInset = UIEdgeInsets(top: offy,
left: offx,
bottom: offy,
right: offx)
// The scroll view has zoomed, so you need to re-center the contents
let scrollViewSize = scrollViewVisibleSize
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
var imageCenter = CGPoint(x: scrollView.contentSize.width / 2.0,
y: scrollView.contentSize.height / 2.0)
let center = scrollViewCenter
//if image is smaller than the scrollView visible size - fix the image center accordingly
if scrollView.contentSize.width < scrollViewSize.width {
imageCenter.x = center.x
}
if scrollView.contentSize.height < scrollViewSize.height {
imageCenter.y = center.y
}
imageView.center = imageCenter
}
Why it's better than anything else I could find on SO so far:
It doesn't read or modify the UIView frame property of the image view since a zoomed image view has a transform applied to it. See here what Apple says on how to move or adjust a view size when a non identity transform is applied.
Starting iOS 7 where translucency for bars was introduced the system will auto adjust the scroll view size, scroll content insets and scroll indicators offsets. Thus you should not modify these in your code as well.
FYI:
There're check boxes for toggling this behavior (which is set by default) in the Xcode interface builder. You can find it in the view controller attributes:
The full view controller's source code is published here.
Also you can download the whole Xcode project to see the scroll view constraints setup and play around with 3 different presets in the storyboard by moving the initial controller pointer to any the following paths:
View with both translucent tab and nav bars.
View with both opaque tab and nav bars.
View with no bars at all.
Every option works correctly with the same VC implementation.
I think I got it. The solution is to use the scrollViewDidEndZooming method of the delegate and in that method set contentInset based on the size of the image. Here's what the method looks like:
- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
CGSize imgViewSize = imageView.frame.size;
CGSize imageSize = imageView.image.size;
CGSize realImgSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
}
CGRect fr = CGRectMake(0, 0, 0, 0);
fr.size = realImgSize;
imageView.frame = fr;
CGSize scrSize = scrollView.frame.size;
float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.25];
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
[UIView commitAnimations];
}
Note that I'm using animation on setting the inset, otherwise the image jumps inside the scrollview when the insets are added. With animation it slides to the center. I'm using UIView beginAnimation and commitAnimation instead of animation block, because I need to have the app run on iphone 3.
Here is the swift 3 version of Genk's Answer
func scrollViewDidZoom(_ scrollView: UIScrollView){
let imgViewSize:CGSize! = self.imageView.frame.size;
let imageSize:CGSize! = self.imageView.image?.size;
var realImgSize : CGSize;
if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
realImgSize = CGSize(width: imgViewSize.width,height: imgViewSize.width / imageSize.width * imageSize.height);
}
else {
realImgSize = CGSize(width: imgViewSize.height / imageSize.height * imageSize.width, height: imgViewSize.height);
}
var fr:CGRect = CGRect.zero
fr.size = realImgSize;
self.imageView.frame = fr;
let scrSize:CGSize = scrollView.frame.size;
let offx:CGFloat = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
let offy:CGFloat = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
// The scroll view has zoomed, so you need to re-center the contents
let scrollViewSize:CGSize = self.scrollViewVisibleSize();
// First assume that image center coincides with the contents box center.
// This is correct when the image is bigger than scrollView due to zoom
var imageCenter:CGPoint = CGPoint(x: self.scrollView.contentSize.width/2.0, y:
self.scrollView.contentSize.height/2.0);
let scrollViewCenter:CGPoint = self.scrollViewCenter()
//if image is smaller than the scrollView visible size - fix the image center accordingly
if (self.scrollView.contentSize.width < scrollViewSize.width) {
imageCenter.x = scrollViewCenter.x;
}
if (self.scrollView.contentSize.height < scrollViewSize.height) {
imageCenter.y = scrollViewCenter.y;
}
self.imageView.center = imageCenter;
}
//return the scroll view center
func scrollViewCenter() -> CGPoint {
let scrollViewSize:CGSize = self.scrollViewVisibleSize()
return CGPoint(x: scrollViewSize.width/2.0, y: scrollViewSize.height/2.0);
}
// Return scrollview size without the area overlapping with tab and nav bar.
func scrollViewVisibleSize() -> CGSize{
let contentInset:UIEdgeInsets = self.scrollView.contentInset;
let scrollViewSize:CGSize = self.scrollView.bounds.standardized.size;
let width:CGFloat = scrollViewSize.width - contentInset.left - contentInset.right;
let height:CGFloat = scrollViewSize.height - contentInset.top - contentInset.bottom;
return CGSize(width:width, height:height);
}
Here is an extension tested on Swift 3.1. Just create a separate *.swift file and paste the code below:
import UIKit
extension UIScrollView {
func applyZoomToImageView() {
guard let imageView = delegate?.viewForZooming?(in: self) as? UIImageView else { return }
guard let image = imageView.image else { return }
guard imageView.frame.size.valid && image.size.valid else { return }
let size = image.size ~> imageView.frame.size
imageView.frame.size = size
self.contentInset = UIEdgeInsets(
x: self.frame.size.width ~> size.width,
y: self.frame.size.height ~> size.height
)
imageView.center = self.contentCenter
if self.contentSize.width < self.visibleSize.width {
imageView.center.x = self.visibleSize.center.x
}
if self.contentSize.height < self.visibleSize.height {
imageView.center.y = self.visibleSize.center.y
}
}
private var contentCenter: CGPoint {
return CGPoint(x: contentSize.width / 2, y: contentSize.height / 2)
}
private var visibleSize: CGSize {
let size: CGSize = bounds.standardized.size
return CGSize(
width: size.width - contentInset.left - contentInset.right,
height: size.height - contentInset.top - contentInset.bottom
)
}
}
fileprivate extension CGFloat {
static func ~>(lhs: CGFloat, rhs: CGFloat) -> CGFloat {
return lhs > rhs ? (lhs - rhs) / 2 : 0.0
}
}
fileprivate extension UIEdgeInsets {
init(x: CGFloat, y: CGFloat) {
self.bottom = y
self.left = x
self.right = x
self.top = y
}
}
fileprivate extension CGSize {
var valid: Bool {
return width > 0 && height > 0
}
var center: CGPoint {
return CGPoint(x: width / 2, y: height / 2)
}
static func ~>(lhs: CGSize, rhs: CGSize) -> CGSize {
switch lhs > rhs {
case true:
return CGSize(width: rhs.width, height: rhs.width / lhs.width * lhs.height)
default:
return CGSize(width: rhs.height / lhs.height * lhs.width, height: rhs.height)
}
}
static func >(lhs: CGSize, rhs: CGSize) -> Bool {
return lhs.width / lhs.height > rhs.width / rhs.height
}
}
The way to use:
extension YOUR_SCROLL_VIEW_DELEGATE: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return YOUR_IMAGE_VIEW
}
func scrollViewDidZoom(_ scrollView: UIScrollView){
scrollView.applyZoomToImageView()
}
}
Analogous to different answers based on setting contentInset, but shorter. Remember about setting scrollView.delegate.
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let offsetX = max((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0)
let offsetY = max((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0)
scrollView.contentInset = UIEdgeInsets(top: offsetY, left: offsetX, bottom: offsetY, right: offsetX);
}
If you want to take a look on a few different strategies, here is a place worth to look: github & post.