UIImage Aspect Fill when using drawInRect? - ios

I tried to draw scaleAspectFill like contents mode.
I found how to make sacelAspectFit using AVFoundation But I can't find scaleAspectFill.
if I draw horizontal image, I don't know x value
image.draw(in: CGRect(origin: CGPoint.init(x: ?, y: 0), size: CGSize(width: displayWidth*(image.size.width/image.size.height), height: displayWidth)))

Assuming you have an image called image, and you want to draw it inside a rectangle targetRect so that it fills the rect without being distorted, you can use the following code:
let aspect = image.size.width / image.size.height
let rect: CGRect
if targetRect.size.width / aspect > targetRect.size.height {
let height = targetRect.size.width / aspect
rect = CGRect(x: 0, y: (targetRect.size.height - height) / 2,
width: targetRect.size.width, height: height)
} else {
let width = targetRect.size.height * aspect
rect = CGRect(x: (targetRect.size.width - width) / 2, y: 0,
width: width, height: targetRect.size.height)
}
image.draw(in: rect)
Note: this doesn't clip the image, so it will draw outside the edges of the target rect. if you want to clip the image, call CGContextClipToRect(context, rect) before drawing.
Note also that the core graphics vertical axis is flipped, with zero starting in the bottom-left instead of top-left compared to UIGraphics, so you may need to flip the rect and clipping rect accordingly.

Related

Understanding CGRect coordinate system and ios drawing

I am really confused about how a CGRect is interpreted and drawn. I have the following routine to draw a rectangle:
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
ctx.cgContext.setFillColor(UIColor.red.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(1)
ctx.cgContext.addRect(rectangle)
ctx.cgContext.drawPath(using: .fillStroke)
Now I was expecting this to draw a rectangle at the origin of the screen with the width and height of 100 (pixels/points, not sure about this).
However, I can barely see the drawn rectangle. Most of it seem to lie outside the screen (on the left hand side).
Changing this to:
let rectangle = CGRect(x: 50, y: 50, width: 100, height: 100)
Still around half the rectangle is outside the screen.
Is the origin of this CGRect at the bottom right? I am not sure how this is all getting interpreted.
I thought if the origin is at the center of the rectangle, then surely the second call should show the whole rectangle?
[EDIT]
I am running this on iphone X running ios 14.4.
[EDIT]
So, the full drawing code is as follows. This is part of a View. So, in the end the view image is assigned to the image we draw on
func show(on frame: CGImage) {
// This is of dimension 480 x 640
let dstImageSize = CGSize(width: frame.width, height: frame.height)
let dstImageFormat = UIGraphicsImageRendererFormat()
dstImageFormat.scale = 1
let renderer = UIGraphicsImageRenderer(size: dstImageSize,
format: dstImageFormat)
let dstImage = renderer.image { rendererContext in
// Draw the current frame as the background for the new image.
draw(image: frame, in: rendererContext.cgContext)
// This is where I draw my rectangle
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
rendererContext.cgContext.setAlpha(0.3)
rendererContext.cgContext.setFillColor(UIColor.red.cgColor)
rendererContext.cgContext.setStrokeColor(UIColor.green.cgColor)
//rendererContext.cgContext.setLineWidth(1)
rendererContext.cgContext.addRect(rectangle)
rendererContext.cgContext.drawPath(using: .fill)
}
image = dstImage
}
So, from what I can tell, it should draw it on the context of the image and should not go out of bounds with the parameters I gave.
I mean, the context is that of the bitmap that has been initialized to 480 x 640. I am nt sure why this is shown out of bounds when I view it on the device. Should this bitmap/image not be shown correctly?

How to resize a UIImage without antialiasing?

I am developing an iOS board game. I am trying to give the board a kind of "texture".
What I did was I created this very small image (really small, be sure to look carefully):
And I passed this image to the UIColor.init(patternImage:) initializer to create a UIColor that is this image. I used this UIColor to fill some square UIBezierPaths, and the result looks like this:
All copies of that image lines up perfectly and they form many diagonal straight lines. So far so good.
Now on the iPad, the squares that I draw will be larger, and the borders of those squares will be larger too. I have successfully calculated what the stroke width and size of the squares should be, so that is not a problem.
However, since the squares are larger on an iPad, there will be more diagonal lines per square. I do not want that. I need to resize the very small image to a bigger one, and that the size depends on the stroke width of the squares. Specifically, the width of the resized image should be twice as much as the stroke width.
I wrote this extension to resize the image, adapted from this post:
extension UIImage {
func resized(toWidth newWidth: CGFloat) -> UIImage {
let scale = newWidth / size.width
let newHeight = size.height * scale
UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: newHeight), false, 0)
self.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
And called it like this:
// this is the code I used to draw a single square
let path = UIBezierPath(rect: CGRect(origin: point(for: Position(x, y)), size: CGSize(width: squareLength, height: squareLength)))
UIColor.black.setStroke()
path.lineWidth = strokeWidth
// this is the line that's important!
UIColor(patternImage: #imageLiteral(resourceName:
"texture").resized(toWidth: strokeWidth * 2)).setFill()
path.fill()
path.stroke()
Now the game board looks like this on an iPhone:
You might need to zoom in the webpage a bit to see what I mean. The board now looks extremely ugly. You can see the "borders" of each copy of the image. I don't want this. On an iPad though, the board looks fine. I suspect that this only happens when I downsize the image.
I figured that this might be due to the antialiasing that happens when I use the extension. I found this post and this post about removing antialiasing, but the former seems to be doing this in a image view while I am doing this in the draw(_:) method of my custom GameBoardView. The latter's solution seems to be exactly the same as what I am using.
How can I resize without antialiasing? Or on a higher level of abstraction, How can I make my board look pretty?
class Ruled: UIView {
override func draw(_ rect: CGRect) {
let T: CGFloat = 15 // desired thickness of lines
let G: CGFloat = 30 // desired gap between lines
let W = rect.size.width
let H = rect.size.height
guard let c = UIGraphicsGetCurrentContext() else { return }
c.setStrokeColor(UIColor.orange.cgColor)
c.setLineWidth(T)
var p = -(W > H ? W : H) - T
while p <= W {
c.move( to: CGPoint(x: p-T, y: -T) )
c.addLine( to: CGPoint(x: p+T+H, y: T+H) )
c.strokePath()
p += G + T + T
}
}
}
Enjoy.
Note that you would, obviously, clip that view.
If you want to have a number of them on the screen or in a pattern, just do that.
To clip to a given rectangle:
The class above simply draws it the "size of the UIView".
However, often, you want to draw a number of the "boxes" actually within the view, at different coordinates. (A good example is for a calendar).
Furthermore, this example explicitly draws "both stripes" rather than drawing one stripe over the background color:
func simpleStripes(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) {
let stripeWidth: CGFloat = 20.0 // whatever you want
let m = stripeWidth / 2.0
guard let c = UIGraphicsGetCurrentContext() else { return }
c.setLineWidth(stripeWidth)
let r = CGRect(x: x, y: y, width: width, height: height)
let longerSide = width > height ? width : height
c.saveGState()
c.clip(to: r)
var p = x - longerSide
while p <= x + width {
c.setStrokeColor(pale blue)
c.move( to: CGPoint(x: p-m, y: y-m) )
c.addLine( to: CGPoint(x: p+m+height, y: y+m+height) )
c.strokePath()
p += stripeWidth
c.setStrokeColor(pale gray)
c.move( to: CGPoint(x: p-m, y: y-m) )
c.addLine( to: CGPoint(x: p+m+height, y: y+m+height) )
c.strokePath()
p += stripeWidth
}
c.restoreGState()
}
extension UIImage {
func ResizeImage(targetSize: CGSize) -> UIImage
{
let size = self.size
let widthRatio = targetSize.width / self.size.width
let heightRatio = targetSize.height / self.size.height
// Figure out what our orientation is, and use that to form the rectangle
var newSize: CGSize
if(widthRatio > heightRatio) {
newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
} else {
newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
}
// This is the rect that we've calculated out and this is what is actually used below
let rect = CGRect(x: 0, y: 0, width: newSize.width,height: newSize.height)
// Actually do the resizing to the rect using the ImageContext stuff
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
self.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}

How do I use this circle drawing code in a UIView?

I am making an app including some breathing techniques for a client. What he wants is to have a circle in the middle. For breathing in it becomes bigger, for breathing out tinier. The thing is, that he would like to have a cool animated circle in the middle, not just a standard one. I showed him this picture from YouTube:
The code used in the video looks like this:
func drawRotatedSquares() {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 512, height: 512), false, 0)
let context = UIGraphicsGetCurrentContext()
context!.translateBy(x: 256, y: 256)
let rotations = 16
let amount = M_PI_2 / Double(rotations)
for i in 0 ..< rotations {
context!.rotate(by: CGFloat(amount))
//context!.addRect(context, CGRect(x: -128, y: -128, width: 256, height: 256))
context!.addRect(CGRect(x: -128, y: -128, width: 256, height: 256))
}
context!.setStrokeColor(UIColor.black as! CGColor)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageView.image = img
}
But if I run it, my simulator shows just a white screen. How do I get this circle into my Swift 3 app and how would the code look like? And is it possible not to show it in an ImageView but simply in a view?
Thank you very much!
Here is an implementation as a UIView subclass.
To set up:
Add this class to your Swift project.
Add a UIView to your Storyboard and change the class to Circle.
Add an outlet to your viewController
#IBOutlet var circle: Circle!
Change the value of multiplier to change the size of the circle.
circle.multiplier = 0.5 // 50% of size of view
class Circle: UIView {
var multiplier: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// Calculate size of square edge that fits into the view
let size = min(bounds.width, bounds.height) * multiplier / CGFloat(sqrt(2)) / 2
// Move origin to center of the view
context.translateBy(x: center.x, y: center.y)
// Create a path to draw a square
let path = UIBezierPath()
path.move(to: CGPoint(x: -size, y: -size))
path.addLine(to: CGPoint(x: -size, y: size))
path.addLine(to: CGPoint(x: size, y: size))
path.addLine(to: CGPoint(x: size, y: -size))
path.close()
UIColor.black.setStroke()
let rotations = 16
let amount = .pi / 2 / Double(rotations)
for _ in 0 ..< rotations {
// Rotate the context
context.rotate(by: CGFloat(amount))
// Draw a square
path.stroke()
}
}
}
Here it is running in a Playground:
You posted a singe method that generates a UIImage and installs it in an image view. If you don't have the image view on-screen then it won't show up.
If you create an image view in your view controller and connect an outlet to the image view then the above code should install the image view into your image and draw it on-screen.
You could rewrite the code you posted as the draw(_:) method of a custom subclass of UIView, in which case you'd get rid of the context setup and UIImage stuff, and simply draw in the current context. I suggest you search on UIView custom draw(_:) methods for more guidance.

Image blurred slightly when drawing in drawRect (Swift)

I am building a circle crop function in Swift. I pretty much have everything working however when I save the cropped photo it is slightly blurry:
Not sure if it is visible here or not but on my iPhone I can see a difference, slight blurring. I'm not zooming in more than the actual size of the image. I am using a UIScrollView with max zoom factor set to 1.0. The crop code is:
func didTapOk() {
let scale = scrollView.zoomScale
let newSize = CGSize(width: image!.size.width*scale, height: image!.size.height*scale)
let offset = scrollView.contentOffset
UIGraphicsBeginImageContext(CGSize(width: 240, height: 240))
let circlePath = UIBezierPath(ovalInRect: CGRect(x: 0, y: 0, width: 240, height: 240))
circlePath.addClip()
var sharpRect = CGRect(x: -offset.x, y: -offset.y, width: newSize.width, height: newSize.height)
sharpRect = CGRectIntegral(sharpRect)
image?.drawInRect(sharpRect)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIImageWriteToSavedPhotosAlbum(finalImage, nil, nil, nil)
}
Here I am trying to use CGRectIntegral to improve the result but it doesn't seem to make any difference. Any pointers on what I could do to improve this?
What's happening is your crop is blurring because it hasn't accounted for the scale of your screen (whether you're using a retina display etc).
Use UIGraphicsBeginImageContextWithOptions(CGSize(width: 240, height: 240), true, 0) to account for the retina screen. Check here for more details on how to use this function.
I suspect that, although the accepted answer above is working for you, it is only working due to the screen scale being the same as the image scale.
As you are not rendering your image from the screen itself (you're rendering from a UIImage to another UIImage), the screen scale should be irrelevant.
You should instead be passing in the scale of the image when you create your context, like so:
UIGraphicsBeginImageContextWithOptions(CGSize(width: 240, height: 240), false, image.scale)
Here's a way in Swift 3 to disable any interpolation.
UIGraphicsBeginImageContextWithOptions(imageSize, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()
context?.interpolationQuality = CGInterpolationQuality.none
Additionally, since this is often for pixel drawing purposes, you may want to add:
// Ensure the image is not drawn upside down
context?.saveGState()
context?.translateBy(x: 0.0, y: maxHeight)
context?.scaleBy(x: 1.0, y: -1.0)
// Make the drawing
context?.draw(yourImage, in: CGRect(x: 0, y: 0, width: maxWidth, height: maxHeight))
// Restore the GState of the context
context?.restoreGState()

When I draw on the image, the image enlarges

imageView is the photo. I created a copy of the photo called tempImageView and I tried to draw on the tempImageView. However, when I try to draw tempImageView, it enlarges to fill the entire screen.
func createTempImageView(){
tempImageView = UIImageView(frame:CGRect(x: 0, y: 0, width: imageView.image!.size.width, height: imageView.image!.size.height))
tempImageView.center = CGPoint(x: imageView.center.x, y: imageView.center.y)
view.insertSubview(tempImageView, aboveSubview: imageView)
}
Because I set imageView.contentMode = .ScaleAspectFit, the image on screen has a smaller width and height than the original image. However, the imageView.image!.size.width and imageView.image!.size.height refers the original image. Therefore, when I tried to draw on the tempImageView, the image enlarged because tempImageView refers to the original image, which is much larger than the image on screen.
To get the dimensions of the image on screen and to stop the image from enlarging, I did this:
func createTempImageView(){
let widthRatio = imageView.bounds.size.width / imageView.image!.size.width
let heightRatio = imageView.bounds.size.height / imageView.image!.size.height
let scale = min(widthRatio, heightRatio)
let imageWidth = scale * imageView.image!.size.width
let imageHeight = scale * imageView.image!.size.height
tempImageView = UIImageView(frame:CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight))
tempImageView.center = CGPoint(x: imageView.center.x, y: imageView.center.y)
tempImageView.backgroundColor = UIColor.clearColor()
view.insertSubview(tempImageView, aboveSubview: imageView)
}

Resources