Swift: Retain floating point decimal in autolayout - swift

this is a follow on question from my post here. I am attempting to fix my heightConstraint of imageView dynamically depending on the size of my image, and then add a drawing layer over it. However, I noticed that the imageView size loses precision due to the division of floating point and this resulted in a blurred image when I start to draw on my image, ie the placement of the bitmap is not exactly at the position overlaying my original image.
My code as such, with the print statements to observe the decimal places.
func setupViews() {
view.backgroundColor = .black
view.addSubview(canvasImageView)
canvasImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
canvasImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
canvasImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
let aspectRatio = getImageAspectRatio(image: image)
let screenWidth = UIScreen.main.bounds.width
let height = CGFloat(1.0) / CGFloat(aspectRatio) * CGFloat(screenWidth)
canvasImageView.heightAnchor.constraint(equalToConstant: height).isActive = true
print("ImageSize", image.size.height)
print("Aspect Ratio image at start:", aspectRatio)
print("Calculated height:", height)
DispatchQueue.main.async {
print("Aspect Ratio imageView at start:", self.getAspectRatio(frame: self.canvasImageView.frame))
print("ImageViewSize", self.canvasImageView.frame.height)
}
}
My print statements are here:
ImageSize 3000.0 2002.0
Aspect Ratio image at start: 1.4985014985015
Calculated height: 213.546666666667
Aspect Ratio imageView at start: 1.49532710280374
ImageViewSize 320.0 214.0
As you can see, my attempt to do the heightAnchor.constraint(equalToConstant: height) actually loses the precision and resulting in a lost of resolution. Is there a way to eliminate this? An image of the outcome is here.

Related

How do you get the aspect fit size of a uiimage in a uimageview?

When print(uiimage.size) is called it only gives the width and the height of the original image before it was scaled up or down. Is there anyway to get the dimensions of the aspect fitted image?
Actually, there is a function in AVFoundation that can calculate this for you:
import AVFoundation
let fitRect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
now fitRect.size is the size inside the imageView bounds by maintaining the original aspect ratio.
You're going to need to calculate the resulting image size in Points yourself*.
* It turns out you don't. See Alladinian's answer. I'm going to
leave this answer here to explain what the library function is doing.
Here's the math:
let imageAspectRatio = image.size.width / image.size.height
let viewAspectRatio = imageView.frame.width / imageView.frame.height
var fitWidth: CGFloat // scaled width in points
var fitHeight: CGFloat // scaled height in points
var offsetX: CGFloat // horizontal gap between image and frame
var offsetY: CGFloat // vertical gap between image and frame
if imageAspectRatio <= viewAspectRatio {
// Image is narrower than view so with aspectFit, it will touch
// the top and bottom of the view, but not the sides
fitHeight = imageView.frame.height
fitWidth = fitHeight * imageAspectRatio
offsetY = 0
offsetX = (imageView.frame.width - fitWidth) / 2
} else {
// Image is wider than view so with aspectFit, it will touch
// the sides of the view but not the top and bottom
fitWidth = imageView.frame.width
fitHeight = fitWidth / imageAspectRatio
offsetX = 0
offsetY = (imageView.frame.height - fitHeight) / 2
}
Explanation:
It helps to draw the pictures. Draw a rectangle that represents the
imageView. Then draw a rectangle that is narrow but extends from the
top of the image view to the bottom. That is the first case. Then draw
one where the image is short but extends to the two side of the image
view. That is the second case. At that point, we know one of the
dimensions. The other is just that value multiplied or divided by the
image's aspect ratio because we know that the .aspectFit keeps the
image's original aspect ratio.
A note about frame vs. bounds. The frame is in the coordinate system of the view's superview. The bounds are in the coordinate system of the view itself. I chose to use the frame in this example, because the OP was interested in how far to move the imageView in it's superview's coordinates. For a standard imageView that has not been rotated or scaled further, the width and height of the frame will match the width and height of the bounds. Things get interesting though when a rotation is applied to an imageView. The frame expands to show the whole imageView, but the bounds remain the same.

Setting UIImageView Width, Height, Aspect Ratio & Constraints X, Y

I've set a UIImageView in my view controller and assigned constraints x & Y which works I'm trying to set the image width, height & aspect ratio but it doesn't seem to be working.
I've tried many answers already on here but can seem to get it working.
import UIKit
class GuestLoginViewController: UIViewController {
let headerImage = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
setHeaderImage()
}
func setHeaderImage() {
view.addSubview(headerImage)
headerImage.frame = CGRect(x: 0, y: 0, width: super.view.frame.width, height: 95)
headerImage.translatesAutoresizingMaskIntoConstraints = false
headerImage.mask?.contentMode = UIView.ContentMode.redraw
headerImage.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
headerImage.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
headerImage.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
headerImage.image = UIImage(named: "header")
view.sendSubviewToBack(headerImage)
}
}
As soon as you set headerImage.translatesAutoresizingMaskIntoConstraints = false, the frame is ignored. You need to set some constraint to establish the height of your UIImageView. Unfortunately, the image contents does not affect the height of the UIImageView.
Set either:
an absolute height constraint
an offset from the bottom of the superView
height relative to the width with a multiplier (an "aspect ratio constraint")
Based on my comment, you turned off headerImage.translatesAutoresizingMaskIntoConstraints = false and it worked.
This gives you extra constraints (your 3 plus the 4 that are generated from the frame), but luckily they aren't conflicting.
Instead, I would suggest you leave the translatesAutoresizingMaskIntoConstraints set to false and set a constraint for the height:
headerImage.heightAnchor.constraint(equalToConstant: 95).isActive = true

Rounded corners of an image

I'm experiencing something very weird. I have a custom UICollectionViewCell that has a UIImageView. Now, I'm setting the cornerRadius of the UIImageView to be rounded and it works on some images, but for others, it produces results like this:
You can clearly see the top and the bottom of the image are not rounded and I'm not sure what causes this. I assume it has to do with the width/height of the image, but I cant figure it out.
I'm using this tiny extension on UIImageView to set the rounded images.
extension UIImageView {
func makeRounded() {
let radius = self.bounds.height / 2.0
self.layer.cornerRadius = radius
self.layer.masksToBounds = true
self.clipsToBounds = true
}
}
I tried changing the self.bounds.height to self.bounds.width, no change. I tried using self.frame.height, self.frame.size.height, self.frame.width, all produces the same result.
It has nothing to with cornerRadius, your code (makeRounded()) should make it rounded.
The issue is related to the contentMode of the image view. If you tried to add a background color to it, you should be able to understand exactly what's the issue, so let's say set the backgroundColor as .red, you should see a red circle containing the image in the middle.
To fix it, you should choose a content mode that will fill the whole image view, such as .scaleToFill or .scaleAspectFill:
extension UIImageView {
func makeRounded() {
let radius = self.bounds.height / 2.0
self.layer.cornerRadius = radius
self.layer.masksToBounds = true
self.clipsToBounds = true
// add this one:
self.contentMode = .scaleAspectFill
}
}

Zoom/Scroll through parts of image in a scrollview

I have an image which I divide into pages, such that each page shows a zoomed rectangle of the image. I think I should be able to do that with a UIImageView in a ScrollView, such that next page zooms the view to a given point. However I can't seem to get it to work.
This is the code for loading the image and setting the zoom on the first part (i.e. the first page) into scrollview:
scrollView.isPagingEnabled = true
scrollView.contentSize = CGSize(width: 1280, height: 1920)
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self as UIScrollViewDelegate
let image = UIImage(named: imageName)
let imageView = UIImageView(image: image!)
imageView.frame.origin.x = 0
scrollView.addSubview(imageView)
scrollView.zoom(toPoint: CGPoint(x:800,y:800), scale: 1, animated: false)
The image is obviously much larger than the size of the scrollview, which is 375/309.
I'm probably missing a lot here, or maybe there's a completely different way of achieving this.
the zoom function is borrowed from https://gist.github.com/TimOliver/71be0a8048af4bd86ede.
Thanks,
Z.
It seems like you'll want to set the content offset rather than zooming to a point. Try:
scrollView.contentOffset = CGPoint(x:800,y:800)

Crop NSImage (macOS) Square without distorting

I am using the code below to resize an image in Swift on macOS. This is working but if the image is not square to begin with, the resizing squashes the image.
How can I resize the image but draw it in the center and keeping the ratio, preventing the squashing if the image is not square to begin with?
func resize(image: NSImage, w: CGFloat, h: CGFloat) -> NSImage {
var destSize = NSMakeSize(CGFloat(w), CGFloat(h))
var newImage = NSImage(size: destSize)
newImage.lockFocus()
image.draw(in: NSMakeRect(0, 0, destSize.width, destSize.height), from: NSMakeRect(0, 0, image.size.width, image.size.height), operation: NSCompositingOperation.sourceOver, fraction: CGFloat(1))
newImage.unlockFocus()
newImage.size = destSize
return NSImage(data: newImage.tiffRepresentation!)!
}
Thank you
The following line of code is the problem:
image.draw(in: NSMakeRect(0, 0, destSize.width, destSize.height), from: NSMakeRect(0, 0, image.size.width, image.size.height), operation: NSCompositingOperation.sourceOver, fraction: CGFloat(1))
You are drawing from the whole rectangle of the source into the whole rectangle of the destination. This will therefore scale the image to fit the destination, whereas you want to maintain aspect ratio. You need to decide how you want the final image to appear and adjust the source or destination rectangles accordingly.
For example, if you want to scale the result so that the whole image appears, you need to adjust either the width or height of the destination rectangle so they're in the same aspect ratio as the source.
Alternatively, if you want to crop the result, you need to adjust the width or height of the source rectangle, once again maintaining the same aspect ratio. You will also have to adjust the origin of the source rectangle if you want to crop (for example) the top and bottom.
Some of the other functions like draw(in:from:operation:fraction:) handle scaling for you, so depending on your needs, they may be better.
Assuming that you want to fit the entire source image into the destination image space(or size), maintaining the aspect ratio of the original image and adding some white space where the image doesn't fit into the destination.
There are three situations to consider.
The aspect ratio of the source and destination are the same.
No problem, just resize.
The aspect ratio destination is wider than the source
Height is the driving value (because it is smaller).
Figure out the height ratio from source to destination.
Use that ratio to calculate the destination width from the source width.
The aspect ratio destination is taller than the source
Width is the driving value.
Figure out the width ratio from source to destination.
Use that ratio to calculate the destination height from the source height.
I created this function to calculate the size you need.
func calcNewSize(source: NSSize, destination: NSSize) -> NSSize {
let widthRatio: Float = Float(destination.width) / Float(source.width)
let heightRatio: Float = Float(destination.height) / Float(source.height)
var newSize = NSSize()
print("widthRatio \(widthRatio) heightRatio \(heightRatio)")
if widthRatio == heightRatio {
print("use same ratio")
newSize = destination
}
else if widthRatio > heightRatio {
print("use height ratio")
newSize.height = source.height * CGFloat(heightRatio)
newSize.width = source.width * CGFloat(heightRatio)
}
else {
print("use width ratio")
newSize.height = source.height * CGFloat(widthRatio)
newSize.width = source.width * CGFloat(widthRatio)
}
return newSize
}