UIImage aspect fit and align to top - ios

It looks like aspect fit aligns the image to the bottom of the frame by default. Is there a way to override the alignment while keeping aspect fit intact?
** EDIT **
This question predates auto layout. In fact, auto layout was being revealed in WWDC 2012 the same week this question was asked

In short, you cannot do this with a UIImageView.
One solution is to subclass a UIView containing an UIImageView and change its frame according to image size. For example, you can find one version here.

Set the UIImageView's bottom layout constraint priority to lowest (i.e. 250) and it will handle it for you.

The way to do this is to modify the contentsRect of the UIImageView layer. The following code from my project (sub class of UIImageView) assumes scaleToFill and offsets the image such that it aligns top, bottom, left or right instead of the default center alignment. For aspectFit is would be a similar solution.
typedef NS_OPTIONS(NSUInteger, AHTImageAlignmentMode) {
AHTImageAlignmentModeCenter = 0,
AHTImageAlignmentModeLeft = 1 << 0,
AHTImageAlignmentModeRight = 1 << 1,
AHTImageAlignmentModeTop = 1 << 2,
AHTImageAlignmentModeBottom = 1 << 3,
AHTImageAlignmentModeDefault = AHTImageAlignmentModeCenter,
};
- (void)updateImageViewContentsRect {
CGRect imageViewContentsRect = CGRectMake(0, 0, 1, 1);
if (self.image.size.height > 0 && self.bounds.size.height > 0) {
CGRect imageViewBounds = self.bounds;
CGSize imageSize = self.image.size;
CGFloat imageViewFactor = imageViewBounds.size.width / imageViewBounds.size.height;
CGFloat imageFactor = imageSize.width / imageSize.height;
if (imageFactor > imageViewFactor) {
//Image is wider than the view, so height will match
CGFloat scaledImageWidth = imageViewBounds.size.height * imageFactor;
CGFloat xOffset = 0.0;
if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeLeft)) {
xOffset = -(scaledImageWidth - imageViewBounds.size.width) / 2;
} else if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeRight)) {
xOffset = (scaledImageWidth - imageViewBounds.size.width) / 2;
}
imageViewContentsRect.origin.x = (xOffset / scaledImageWidth);
} else if (imageFactor < imageViewFactor) {
CGFloat scaledImageHeight = imageViewBounds.size.width / imageFactor;
CGFloat yOffset = 0.0;
if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeTop)) {
yOffset = -(scaledImageHeight - imageViewBounds.size.height) / 2;
} else if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeBottom)) {
yOffset = (scaledImageHeight - imageViewBounds.size.height) / 2;
}
imageViewContentsRect.origin.y = (yOffset / scaledImageHeight);
}
}
self.layer.contentsRect = imageViewContentsRect;
}
Swift version
class AlignmentImageView: UIImageView {
enum HorizontalAlignment {
case left, center, right
}
enum VerticalAlignment {
case top, center, bottom
}
// MARK: Properties
var horizontalAlignment: HorizontalAlignment = .center
var verticalAlignment: VerticalAlignment = .center
// MARK: Overrides
override var image: UIImage? {
didSet {
updateContentsRect()
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateContentsRect()
}
// MARK: Content layout
private func updateContentsRect() {
var contentsRect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
guard let imageSize = image?.size else {
layer.contentsRect = contentsRect
return
}
let viewBounds = bounds
let imageViewFactor = viewBounds.size.width / viewBounds.size.height
let imageFactor = imageSize.width / imageSize.height
if imageFactor > imageViewFactor {
// Image is wider than the view, so height will match
let scaledImageWidth = viewBounds.size.height * imageFactor
var xOffset: CGFloat = 0.0
if case .left = horizontalAlignment {
xOffset = -(scaledImageWidth - viewBounds.size.width) / 2
}
else if case .right = horizontalAlignment {
xOffset = (scaledImageWidth - viewBounds.size.width) / 2
}
contentsRect.origin.x = xOffset / scaledImageWidth
}
else {
let scaledImageHeight = viewBounds.size.width / imageFactor
var yOffset: CGFloat = 0.0
if case .top = verticalAlignment {
yOffset = -(scaledImageHeight - viewBounds.size.height) / 2
}
else if case .bottom = verticalAlignment {
yOffset = (scaledImageHeight - viewBounds.size.height) / 2
}
contentsRect.origin.y = yOffset / scaledImageHeight
}
layer.contentsRect = contentsRect
}
}

this will make the image fill the width and occupy only the height it needs to fit the image (widthly talking)
swift 4.2:
let image = UIImage(named: "my_image")!
let ratio = image.size.width / image.size.height
cardImageView.widthAnchor
.constraint(equalTo: cardImageView.heightAnchor, multiplier: ratio).isActive = true

I had similar problem.
Simplest way was to create own subclass of UIImageView. I add for subclass 3 properties so now it can be use easly without knowing internal implementation:
#property (nonatomic) LDImageVerticalAlignment imageVerticalAlignment;
#property (nonatomic) LDImageHorizontalAlignment imageHorizontalAlignment;
#property (nonatomic) LDImageContentMode imageContentMode;
You can check it here:
https://github.com/LucasssD/LDAlignmentImageView

Add the Aspect Ratio constraint with your image proportions.
Do not pin UIImageView to bottom.
If you want to change the UIImage dynamically remember to update aspect ratio constraint.

I solved this natively in Interface Builder by setting a constraint on the height of the UIImageView, since the image would always be 'pushed' up when the image was larger than the screen size.
More specifically, I set the UIImageView to be the same height as the View it is in (via height constraint), then positioned the UIImageView with spacing constraints in IB. This results in the UIImageView having an 'Aspect Fit' which still respects the top spacing constraint I set in IB.

If you are able to subclass UIImageView, then you can just override the image var.
override var image: UIImage? {
didSet {
self.sizeToFit()
}
}
In Objective-C you can do the same thing by overriding the setter.

Related

Building a circular facepile of profile pictures in Swift: how to have the last photo tucked under the first?

I am trying to build a UIView that has a few UIImageViews arranged in a circular, overlapping manner (see image below). Let's say we have N images. Drawing out the first N - 1 is easy, just use sin/cos functions to arrange the centers of the UIImageViews around a circle. The problem is with the last image that seemingly has two z-index values! I know this is possible since kik messenger has similar group profile photos.
The best idea I have come up so far is taking the last image, split into something like "top half" and "bottom half" and assign different z-values for each. This seems doable when the image is the left-most one, but what happens if the image is the top most? In this case, I would need to split left and right instead of top and bottom.
Because of this problem, it's probably not top, left, or right, but more like a split across some imaginary axis from the center of the overall facepile through the center of the UIImageView. How would I do that?!
Below Code Will Layout UIImageView's in Circle
You would need to import SDWebImage and provide some image URLs to run the code below.
import Foundation
import UIKit
import SDWebImage
class EventDetailsFacepileView: UIView {
static let dimension: CGFloat = 66.0
static let radius: CGFloat = dimension / 1.68
private var profilePicViews: [UIImageView] = []
var profilePicURLs: [URL] = [] {
didSet {
updateView()
}
}
func updateView() {
self.profilePicViews = profilePicURLs.map({ (profilePic) -> UIImageView in
let imageView = UIImageView()
imageView.sd_setImage(with: profilePic)
imageView.roundImage(imageDimension: EventDetailsFacepileView.dimension, showsBorder: true)
imageView.sd_imageTransition = .fade
return imageView
})
self.profilePicViews.forEach { (imageView) in
self.addSubview(imageView)
}
self.setNeedsLayout()
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 2
}
override func layoutSubviews() {
super.layoutSubviews()
let xOffset: CGFloat = 0
let yOffset: CGFloat = 0
let center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
let radius: CGFloat = EventDetailsFacepileView.radius
let angleStep: CGFloat = 2 * CGFloat(Double.pi) / CGFloat(profilePicViews.count)
var count = 0
for profilePicView in profilePicViews {
let xPos = center.x + CGFloat(cosf(Float(angleStep) * Float(count))) * (radius - xOffset)
let yPos = center.y + CGFloat(sinf(Float(angleStep) * Float(count))) * (radius - yOffset)
profilePicView.frame = CGRect(origin: CGPoint(x: xPos, y: yPos),
size: CGSize(width: EventDetailsFacepileView.dimension, height: EventDetailsFacepileView.dimension))
count += 1
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let requiredSize = EventDetailsFacepileView.dimension + EventDetailsFacepileView.radius
return CGSize(width: requiredSize,
height: requiredSize)
}
}
I don't think you'll have much success trying to split images to get over/under z-indexes.
One approach is to use masks to make it appear that the image views are overlapped.
The general idea would be:
subclass UIImageView
in layoutSubviews()
apply cornerRadius to layer to make the image round
get a rect from the "overlapping view"
convert that rect to local coordinates
expand that rect by the desired width of the "outline"
get an oval path from that rect
combine it with a path from self
apply it as a mask layer
Here is an example....
I was not entirely sure what your sizing calculations were doing... trying to use your EventDetailsFacepileView as-is gave me small images in the lower-right corner of the view?
So, I modified your EventDetailsFacepileView in a couple ways:
uses local images named "pro1" through "pro5" (you should be able to replace with your SDWebImage)
uses auto-layout constraints instead of explicit frames
uses MyOverlapImageView class to handle the masking
Code - no #IBOutlet connections, so just set a blank view controller to OverlapTestViewController:
class OverlapTestViewController: UIViewController {
let facePileView = MyFacePileView()
override func viewDidLoad() {
super.viewDidLoad()
facePileView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(facePileView)
facePileView.dimension = 120
let sz = facePileView.sizeThatFits(.zero)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
facePileView.widthAnchor.constraint(equalToConstant: sz.width),
facePileView.heightAnchor.constraint(equalTo: facePileView.widthAnchor),
facePileView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
facePileView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
facePileView.profilePicNames = [
"pro1", "pro2", "pro3", "pro4", "pro5"
]
}
}
class MyFacePileView: UIView {
var dimension: CGFloat = 66.0
lazy var radius: CGFloat = dimension / 1.68
private var profilePicViews: [MyOverlapImageView] = []
var profilePicNames: [String] = [] {
didSet {
updateView()
}
}
func updateView() {
self.profilePicViews = profilePicNames.map({ (profilePic) -> MyOverlapImageView in
let imageView = MyOverlapImageView()
if let img = UIImage(named: profilePic) {
imageView.image = img
}
return imageView
})
// add MyOverlapImageViews to self
// and set width / height constraints
self.profilePicViews.forEach { (imageView) in
self.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: dimension).isActive = true
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
}
// start at "12 o'clock"
var curAngle: CGFloat = .pi * 1.5
// angle increment
let incAngle: CGFloat = ( 360.0 / CGFloat(self.profilePicViews.count) ) * .pi / 180.0
// calculate position for each image view
// set center constraints
self.profilePicViews.forEach { imgView in
let xPos = cos(curAngle) * radius
let yPos = sin(curAngle) * radius
imgView.centerXAnchor.constraint(equalTo: centerXAnchor, constant: xPos).isActive = true
imgView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: yPos).isActive = true
curAngle += incAngle
}
// set "overlapView" property for each image view
let n = self.profilePicViews.count
for i in (1..<n).reversed() {
self.profilePicViews[i].overlapView = self.profilePicViews[i-1]
}
self.profilePicViews[0].overlapView = self.profilePicViews[n - 1]
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 2
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let requiredSize = dimension * 2.0 + radius / 2.0
return CGSize(width: requiredSize,
height: requiredSize)
}
}
class MyOverlapImageView: UIImageView {
// reference to the view that is overlapping me
weak var overlapView: MyOverlapImageView?
// width of "outline"
var outlineWidth: CGFloat = 6
override func layoutSubviews() {
super.layoutSubviews()
// make image round
layer.cornerRadius = bounds.size.width * 0.5
layer.masksToBounds = true
let mask = CAShapeLayer()
if let v = overlapView {
// get bounds from overlapView
// converted to self
// inset by outlineWidth (negative numbers will make it grow)
let maskRect = v.convert(v.bounds, to: self).insetBy(dx: -outlineWidth, dy: -outlineWidth)
// oval path from mask rect
let path = UIBezierPath(ovalIn: maskRect)
// path from self bounds
let clipPath = UIBezierPath(rect: bounds)
// append paths
clipPath.append(path)
mask.path = clipPath.cgPath
mask.fillRule = .evenOdd
// apply mask
layer.mask = mask
}
}
}
Result:
(I grabbed random images by searching google for sample profile pictures)

How to resize UIImageView based on UIImage's size/ratio in Swift 3?

I have a UIImageView and the user is able to download UIImages in various formats. The issue is that I need the UIImageView to resize based on the given Image's ratio.
Currently, I'm using Aspect fit, but the UIImageView remains empty on big parts of itself. I would like to have the UIImageView resize itself based on its content. E.g if the pic is 1:1, 4:3, 6:2, 16:9...
Help is very appreciated.
As requested, that is what I want:
I have had an UIImageView that was square, loaded with an Image in 16:7 or whatever, and the UIImageView resized to fit the size of the Image...
I spent many hours trying to find a solution to the same problem you're having and this is the only solution that worked for me (Swift 4, Xcode 9.2):
class ScaledHeightImageView: UIImageView {
override var intrinsicContentSize: CGSize {
if let myImage = self.image {
let myImageWidth = myImage.size.width
let myImageHeight = myImage.size.height
let myViewWidth = self.frame.size.width
let ratio = myViewWidth/myImageWidth
let scaledHeight = myImageHeight * ratio
return CGSize(width: myViewWidth, height: scaledHeight)
}
return CGSize(width: -1.0, height: -1.0)
}
}
Add the class to the project and set the UIImageView to the custom class ScaledHeightImageView. The image view's content mode is Aspect Fit.
My problem is the same as the one stated in this post. Inside my prototype TableViewCell's ContentView, I have a vertical StackView constrained to each edge. Inside the StackView there was a Label, ImageView and another Label. Having the ImageView set to AspectFit was not enough. The image would be the proper size and proportions but the ImageView didn't wrap the actual image leaving a bunch of extra space between the image and label (just like in the image above). The ImageView height seemed to match height of the original image rather than the height of the resized image (after aspectFit did it's job). Other solutions I found didn't completely resolve the problem for various reasons. I hope this helps someone.
I spent many hours on this, and I finally got a solution that worked for me (Swift 3):
in IB, I set UIImageView's 'Content Mode' to 'Aspect Fit'
in IB, I set UIImageView's width constraint to be equal to whatever you want (in my case, the view's width)
in IB, I set UIImageView's height constraint to be equal to 0, and create a referencing outlet for it (say, constraintHeight)
Then, when I need to display the image, I simply write the following (sampled from answers above):
let ratio = image.size.width / image.size.height
let newHeight = myImageView.frame.width / ratio
constraintHeight.constant = newHeight
view.layoutIfNeeded()
Basically, this ensures that the image fills the UIImageView's width and forces the UIImageView's height to be equal to the image's height after it scaled
It looks like you want to resize an ImageView according to the image ratio and the container view's size, here is the example in Swift (Sorry,the former answer with a bug, I fixed it):
let containerView = UIView(frame: CGRect(x:0,y:0,width:320,height:500))
let imageView = UIImageView()
if let image = UIImage(named: "a_image") {
let ratio = image.size.width / image.size.height
if containerView.frame.width > containerView.frame.height {
let newHeight = containerView.frame.width / ratio
imageView.frame.size = CGSize(width: containerView.frame.width, height: newHeight)
}
else{
let newWidth = containerView.frame.height * ratio
imageView.frame.size = CGSize(width: newWidth, height: containerView.frame.height)
}
}
SWIFT 5
This is what I have done in my project:
Place an ImageView in ViewController and create an outlet in viewDidLoad() named imageView.
override func viewDidLoad() {
super.viewDidLoad()
var image = UIImage(contentsOfFile: "yourFilePath")!
var aspectR: CGFloat = 0.0
aspectR = image.size.width/image.size.height
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = image
imageView.contentMode = .scaleAspectFit
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor),
imageView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1/aspectR)
])
}
The last 3 lines of NSLayoutConstraint.activate array ensures that the image width stays within the bounds of the container view and the height stays in proportion to width (i.e. the aspect ratio is maintained and height of imageView is shrunk to minimum required value).
View Controller in Interface Builder: Main.storyboard
Snapshot of UIImageView in running app: appSnapshot
The solution I used is based on olearyj234's solution, but makes having no image take up essentially no space (or more specifically the minimum iOS will accept). It also uses ceil to avoid problems which can occur with non-integer values when UIImageView's are embedded in things like scrolling cells.
class FixedWidthAspectFitImageView: UIImageView
{
override var intrinsicContentSize: CGSize
{
// VALIDATE ELSE RETURN
// frameSizeWidth
let frameSizeWidth = self.frame.size.width
// image
// ⓘ In testing on iOS 12.1.4 heights of 1.0 and 0.5 were respected, but 0.1 and 0.0 led intrinsicContentSize to be ignored.
guard let image = self.image else
{
return CGSize(width: frameSizeWidth, height: 1.0)
}
// MAIN
let returnHeight = ceil(image.size.height * (frameSizeWidth / image.size.width))
return CGSize(width: frameSizeWidth, height: returnHeight)
}
}
The solution is also based on olearyj234's solution, but I think this will help more people.
#IBDesignable
class DynamicImageView: UIImageView {
#IBInspectable var fixedWidth: CGFloat = 0 {
didSet {
invalidateIntrinsicContentSize()
}
}
#IBInspectable var fixedHeight: CGFloat = 0 {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
var size = CGSize.zero
if fixedWidth > 0 && fixedHeight > 0 { // 宽高固定
size.width = fixedWidth
size.height = fixedHeight
} else if fixedWidth <= 0 && fixedHeight > 0 { // 固定高度动态宽度
size.height = fixedHeight
if let image = self.image {
let ratio = fixedHeight / image.size.height
size.width = image.size.width * ratio
}
} else if fixedWidth > 0 && fixedHeight <= 0 { // 固定宽度动态高度
size.width = fixedWidth
if let image = self.image {
let ratio = fixedWidth / image.size.width
size.height = image.size.height * ratio
}
} else { // 动态宽高
size = image?.size ?? .zero
}
return size
}
}
A lot of the answers here are using the frame when calculating the intrinsicContentSize. The docs discourage this:
This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.
I've found wanting the UIImageView height to be dynamically set according to:
the aspect ratio of the image
a fixed width
to be a common problem, I provide a possible solution below.
Solution
I think this is best solved by adding an NSLayoutConstraint to the UIImageView which constrains the widthAnchor and heightAnchor (or vice versa) such that the multiplier matches the aspect ratio of the image. I have created a UIImageView subclass that does exactly this:
import UIKit
/// `AdjustableImageView` is a `UIImageView` which should have a fixed width or height.
/// It will add an `NSLayoutConstraint` such that it's width/height (aspect) ratio matches the
/// `image` width/height ratio.
class AdjustableImageView: UIImageView {
/// `NSLayoutConstraint` constraining `heightAnchor` relative to the `widthAnchor`
/// with the same `multiplier` as the inverse of the `image` aspect ratio, where aspect
/// ratio is defined width/height.
private var aspectRatioConstraint: NSLayoutConstraint?
/// Override `image` setting constraint if necessary on set
override var image: UIImage? {
didSet {
updateAspectRatioConstraint()
}
}
// MARK: - Init
override init(image: UIImage?) {
super.init(image: image)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// MARK: - Setup
/// Shared initializer code
private func setup() {
// Set default `contentMode`
contentMode = .scaleAspectFill
// Update constraints
updateAspectRatioConstraint()
}
// MARK: - Resize
/// De-active `aspectRatioConstraint` and re-active if conditions are met
private func updateAspectRatioConstraint() {
// De-active old constraint
aspectRatioConstraint?.isActive = false
// Check that we have an image
guard let image = image else { return }
// `image` dimensions
let imageWidth = image.size.width
let imageHeight = image.size.height
// `image` aspectRatio
guard imageWidth > 0 else { return }
let aspectRatio = imageHeight / imageWidth
guard aspectRatio > 0 else { return }
// Create a new constraint
aspectRatioConstraint = heightAnchor.constraint(
equalTo: widthAnchor,
multiplier: aspectRatio
)
// Activate new constraint
aspectRatioConstraint?.isActive = true
}
}
In case the Content mode is set aspectFit or aspectFill the answer would vary:
extension UIImageView {
var intrinsicScaledContentSize: CGSize? {
switch contentMode {
case .scaleAspectFit:
// aspect fit
if let image = self.image {
let imageWidth = image.size.width
let imageHeight = image.size.height
let viewWidth = self.frame.size.width
let ratio = viewWidth/imageWidth
let scaledHeight = imageHeight * ratio
return CGSize(width: viewWidth, height: scaledHeight)
}
case .scaleAspectFill:
// aspect fill
if let image = self.image {
let imageWidth = image.size.width
let imageHeight = image.size.height
let viewHeight = self.frame.size.width
let ratio = viewHeight/imageHeight
let scaledWidth = imageWidth * ratio
return CGSize(width: scaledWidth, height: imageHeight)
}
default: return self.bounds.size
}
return nil
}
}
Set your imageView to aspectFit, that will resize the image to not exceed your imageView's frame.
You can get the size of your UIImage of your imageView with logic from this question - basically just get the height and width of the UIImage.
Calculate the ratio and set the width/height of the imageView to fit you screen.
There is also a similar question to your that you might get you answer from.
I modified #user8969729 's solution to replace the "fixed" width/height with "max", thus more like #JoshuaHart's solution. Handle the maxWidth == 0 / maxHeight == 0 case as you wish, since I always had both set I just quickly ignored that case.
public class AdjustsViewBoundsImageView: UIImageView {
/// The maximum width that you want this imageView to grow to.
#objc dynamic var maxWidth: CGFloat = 0 {
didSet {
invalidateIntrinsicContentSize()
}
}
/// The maximum height that you want this imageView to grow to.
#objc dynamic var maxHeight: CGFloat = 0 {
didSet {
invalidateIntrinsicContentSize()
}
}
private var maxAspectRatio: CGFloat { return maxWidth / maxHeight }
override public var intrinsicContentSize: CGSize {
guard let classImage = self.image else { return super.intrinsicContentSize }
if maxHeight == 0 || maxWidth == 0 {
return super.intrinsicContentSize
}
let imageWidth = classImage.size.width
let imageHeight = classImage.size.height
let aspectRatio = imageWidth / imageHeight
// Width is greater than height, return max width image and new height.
if imageWidth > imageHeight {
let newHeight = maxWidth/aspectRatio
return CGSize(width: maxWidth, height: newHeight)
}
// Height is greater than width, return max height and new width.
if imageHeight > imageWidth {
// If the aspect ratio is larger than our max ratio, then using max width
// will be hit before max height.
if aspectRatio > maxAspectRatio {
let newHeight = maxWidth/aspectRatio
return CGSize(width: maxWidth, height: newHeight)
}
let newWidth = maxHeight * aspectRatio
return CGSize(width: newWidth, height: maxHeight)
}
// Square image, return the lesser of max width and height.
let squareMinimumValue = min(maxWidth, maxHeight)
return CGSize(width: squareMinimumValue, height: squareMinimumValue)
}
}
If you want scale UIImageView by width and height - use this class:
import UIKit
class AutoSizeImageView: UIImageView {
#IBInspectable var maxSize: CGFloat = 100
// MARK: Methods
func updateSize() {
let newSize = getSize()
snp.remakeConstraints { make in
make.width.equalTo(newSize.width)
make.height.equalTo(newSize.height)
}
}
private func getSize() -> CGSize {
guard let image = image else { return .zero }
if image.size.width == image.size.height { return CGSize(width: maxSize, height: maxSize) }
if image.size.width > image.size.height {
let widthRatio = maxSize / image.size.width
let scaledHeight = image.size.height * widthRatio
return CGSize(width: maxSize, height: scaledHeight)
}
let heightRatio = maxSize / image.size.height
let scaledWidth = image.size.width * heightRatio
return CGSize(width: scaledWidth, height: maxSize)
}
}
Call it like this:
#IBOutlet weak var imageView: AutoSizeImageView!
imageView.image = image
imageView.updateSize()
Please note I've used SnapKit to manage constraints:
snp.remakeConstraints { make in
make.width.equalTo(newSize.width)
make.height.equalTo(newSize.height)
}
Change solution for Merricat.
Hi. Use your solution in collection view cell, make onboarding. First launch and scroll not not give right height. I add this -
contentView.layoutIfNeeded()
if let image = UIImage(named: "\(data.imageName)") {
let ratio = image.size.width / image.size.height
let newHeight = imageView.frame.width / ratio
imageView.image = image
imageHeightConstraint.priority = .defaultHigh
imageHeightConstraint.constant = newHeight
contentView.layoutIfNeeded()
}
SWIFT 5 CLASS
This can easily be converted to use IBOutlets if desired. My use-case involved programmatically adding imageViews. This is very reliable. Just create a new file in your project and add the code below.
import UIKit
/// Resizeable Image View that takes a max height and max width
/// Will resize the imageView to best fit for the aspect ratio of the image,
/// With the given space provided.
public class ResizeableImageView: UIImageView {
private var widthConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
// MARK: - INITIALIZERS:
public override init(image: UIImage?) {
super.init(image: image)
}
/// Given the max width and height, resizes the imageView to fit the image.
/// - IMPORTANT: This subclass adds a height and width constraint.
/// - Parameters:
/// - image: (UIImage?) The image to add to the imageView.
/// - maxWidth: (CGFloat) The max width you would like the imageView to grow to.
/// - maxHeight: (CGFloat) The max height you would like the imageView to grow to.
convenience init(image: UIImage?, maxWidth: CGFloat, maxHeight: CGFloat) {
self.init(image: image)
widthConstraint = constrain(width: maxWidth)
heightConstraint = constrain(height: maxHeight)
}
#available (*, unavailable) required internal init?(coder aDecoder: NSCoder) { nil }
// MARK: - VARIABLES:
/// The maximum width that you want this imageView to grow to.
private var maxWidth: CGFloat {
get { widthConstraint?.constant ?? 0 }
set { widthConstraint?.constant = newValue }
}
/// The maximum height that you want this imageView to grow to.
private var maxHeight: CGFloat {
get { heightConstraint?.constant ?? 0 }
set { heightConstraint?.constant = newValue }
}
private var maxAspectRatio: CGFloat { maxWidth / maxHeight }
override public var intrinsicContentSize: CGSize {
guard let classImage = self.image else { return frame.size }
let imageWidth = classImage.size.width
let imageHeight = classImage.size.height
let aspectRatio = imageWidth / imageHeight
// Width is greater than height, return max width image and new height.
if imageWidth > imageHeight {
let newHeight = maxWidth/aspectRatio
self.widthConstraint?.constant = maxWidth
self.heightConstraint?.constant = newHeight
return CGSize(width: maxWidth, height: newHeight)
}
// Height is greater than width, return max height and new width.
if imageHeight > imageWidth {
// If the aspect ratio is larger than our max ratio, then using max width
// will be hit before max height.
if aspectRatio > maxAspectRatio {
let newHeight = maxWidth/aspectRatio
self.widthConstraint?.constant = maxWidth
self.heightConstraint?.constant = newHeight
return CGSize(width: maxWidth, height: newHeight)
}
let newWidth = maxHeight * aspectRatio
self.widthConstraint?.constant = newWidth
self.heightConstraint?.constant = maxHeight
return CGSize(width: newWidth, height: maxHeight)
}
// Square image, return the lesser of max width and height.
let squareMinimumValue = min(maxWidth, maxHeight)
self.widthConstraint?.constant = squareMinimumValue
self.heightConstraint?.constant = squareMinimumValue
return CGSize(width: squareMinimumValue, height: squareMinimumValue)
}
}
Example Usage:
let imageView = ResizeableImageView(image: image, maxWidth: 250, maxHeight: 250)

How to restrict shifting of a pinch zoomed image to the image borders?

I set up a scrollView in storyboard with a single imageView using auto layout as shown here.
In addition I enabled pinch zooming for this imageView as described in the docs.
The imageView has 4 constraints of 0 to the sides of the scollView, and it has equal width and height to the main view, which has the scrollView as subview.
The imageView has aspectFit scaling mode, so in the unzoomed state, the image will extend fully from left to right, or from top to bottom (or both).
In the following example, it extends from top to bottom, leaving some background visible left and right:
I can pinch zoom the image so that it is larger than the screen:
I can shift the zoomed image up or down, but the movement stops before the background becomes visible on top or bottom. This is want I want.
However, I can shift the zoomed image left or right so that the background can be seen:
How can I avoid shifting the image so far (here, to the right). Like with top or bottom, it should stop moving at the border, so that no background becomes visible.
Not ideal solution but I hope it will help:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
[self adjustScrollViewInsets];
}
- (void)adjustScrollViewInsets {
CGFloat imageWidth = self.imageView.image.size.width;
CGFloat imageHeight = self.imageView.image.size.height;
CGFloat aspect = imageWidth / imageHeight;
CGSize imageViewSize = self.imageView.frame.size;
if (imageViewSize.width / aspect <= imageViewSize.height) {
[self adjustVerticalInsetsWithImageHeight:(imageViewSize.width / aspect)];
} else {
[self adjustHorizontalInsetsWithImageWidth:(imageViewSize.height * aspect)];
}
}
- (void)adjustHorizontalInsetsWithImageWidth:(CGFloat)width {
CGFloat horizontalInset = (self.scrollView.contentSize.width - width) / 2;
if (width < self.scrollView.frame.size.width) {
horizontalInset = horizontalInset - (self.scrollView.frame.size.width - width) / 2;
}
[self.scrollView setContentInset:UIEdgeInsetsMake(0, -horizontalInset, 0, -horizontalInset)];
}
- (void)adjustVerticalInsetsWithImageHeight:(CGFloat)height {
CGFloat verticalInset = (self.scrollView.contentSize.height - height) / 2;
if (height < self.scrollView.frame.size.height) {
verticalInset = verticalInset - (self.scrollView.frame.size.height - height) / 2;
}
[self.scrollView setContentInset:UIEdgeInsetsMake(-verticalInset, 0, -verticalInset, 0)];
}
If you have troubles, check out this repo.
I was having the same problem and implemented in Swift 4.0 as given below and it worked for me.
Swift 4.0:
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
adjustScrollViewInsets()
}
func adjustScrollViewInsets() {
let imageWidth = self.imageView.image?.size.width
let imageHeight = self.imageView.image?.size.height
let aspect = imageWidth! / imageHeight!
let imageViewSize = self.imageView.frame.size
if imageViewSize.width / aspect <= imageViewSize.height {
adjustVerticalInsetsWithImageHeight(height: imageViewSize.width / 2)
} else {
adjustHorizontalInsetsWithImageWidth(width: imageViewSize.height / 2)
}
}
func adjustHorizontalInsetsWithImageWidth(width: CGFloat) {
var horizontalInset = (self.scrollView.contentSize.width - width) / 2
if width < self.scrollView.contentSize.width {
horizontalInset = horizontalInset - (self.scrollView.frame.size.width - width) / 2
}
self.scrollView.contentInset = UIEdgeInsetsMake(0, -horizontalInset, 0, -horizontalInset)
}
func adjustVerticalInsetsWithImageHeight(height: CGFloat) {
var verticalInset = (self.scrollView.contentSize.height - height) / 2
if height < self.scrollView.frame.size.height {
verticalInset = verticalInset - (self.scrollView.frame.size.height - height) / 2
}
self.scrollView.contentInset = UIEdgeInsetsMake(-verticalInset, 0, -verticalInset, 0)
}

Swift round up for respondsToSelector() and sizeWithFont()

I am translating the following Objective-C code into Swift:
- (void)layoutSubviews
{
CGSize valueLabelSize = CGSizeZero;
if ([self.valueLabel.text respondsToSelector:#selector(sizeWithAttributes:)])
{
valueLabelSize = [self.valueLabel.text sizeWithAttributes:#{NSFontAttributeName:self.valueLabel.font}];
}
else
{
valueLabelSize = [self.valueLabel.text sizeWithFont:self.valueLabel.font];
}
CGFloat xOffset = ceil((self.bounds.size.width - valueLabelSize.width) * 0.5);
self.valueLabel.frame = CGRectMake(xOffset, ceil(self.bounds.size.height * 0.5) - ceil(valueLabelSize.height * 0.5), valueLabelSize.width, valueLabelSize.height);
}
This code basically loads dynamically an UILabel in the middle of the UIView respecting the relative font size and the padding of the view. This is the code I currently have:
override func layoutSubviews()
{
var valueSize = CGSizeZero
if valueLabel.text != nil {
if ((self.valueLabel.text! as NSString).respondsToSelector(Selector("sizeWithAttributes:"))){
valueSize = (self.valueLabel.text! as NSString).sizeWithAttributes([NSFontAttributeName:self.valueLabel.font])
}
}
let xOffSet = ceil((self.bounds.size.width - valueSize.width) * 0.5)
valueLabel.frame = CGRectMake(xOffSet, ceil(self.bounds.size.height * 0.5) - ceil(valueSize.height * 0.5), valueSize.width, valueSize.height)
}
This is not working correctly cause the label appears in the left-top corner of the view. I believe there is some round up in swift for not using the respondsToSelector() method but do not know how. I also want to avoid using sizeWithFont() cause it's deprecated.
Any help will be very welcomed!
I finally made it work using the superview property:
override func layoutSubviews() {
super.layoutSubviews()
var valueSize = CGSizeZero
if let textValue = valueLabel.text {
valueSize = textValue.sizeWithAttributes([NSFontAttributeName:self.valueLabel.font])
}
var unitSize = CGSizeZero
if let textUnit = unitLabel.text {
unitSize = textUnit.sizeWithAttributes([NSFontAttributeName:self.unitLabel.font])
}
let xOffset = ceil((superview!.bounds.size.width - (valueSize.width + unitSize.width)) * 0.5)
valueLabel.frame = CGRectMake(xOffset, ceil(superview!.bounds.size.height * 0.5) - ceil(valueSize.height * 0.5), valueSize.width, valueSize.height)
unitLabel.frame = CGRectMake(CGRectGetMaxX(valueLabel.frame), ceil(superview!.bounds.size.height * 0.5) - ceil(unitSize.height * 0.5) + iMinistryValueInformationViewPadding, unitSize.width, unitSize.height)
}
Thanks anyway for the help!
Have you tried using label.sizeToFit() to set the correct label size to fit your content?
For example:
class CustomView : UIView {
lazy var textLabel: UILabel = {
let label = UILabel()
// Setup your label's text attributes here.
// e.g. font size
self.addSubview(label)
return label
}()
override func layoutSubviews() {
super.layoutSubviews()
// By this point the `text` for the label and all attributes
// that could affect the text size need to be set.
textLabel.sizeToFit()
textLabel.center = CGPoint(x: frame.width / 2, y: frame.height / 2)
}
}
The result:
Hope that helps!

Apply CGAffineTransformScale to UIView makes the layer border go bigger too

I have a UIView, inside it I have a UIImageView. I have a UIPinchGestureRecognizer added to the UIVIew to handle the pinch and zoom and make the UIView grow with the UIImageView altogether.
My UIView has a border. I added the border this way:
self.layer.borderColor = [UIColor blackColor].CGColor;
self.layer.borderWidth = 1.0f;
self.layer.cornerRadius = 8.0f;
And the problem I'm having is I can't find a way of making my UIView bigger while keeping the same width of the border. When pinching and zooming the border gets thicker.
This is my UIPinchGestureRecognizer handler:
- (void)scale:(UIPanGestureRecognizer *)sender{
if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);
CGAffineTransform currentTransform = self.transform;
CGAffineTransform newTransform = CGAffineTransformScale(currentTransform, scale, scale);
_lastScale = [(UIPinchGestureRecognizer*)sender scale];
[self setTransform:newTransform];
}
Thanks a lot!!
I've been googling around A LOT and found this:
self.layer.borderWidth = 2.0f / scaleFactor;
Sadly is not working for me... It makes sense but not working.
Also I read the solution about adding an other view in the back and making the front view to have an offset in the position so the back view is shown and looks like a border. That's not an option because I need my image to view transparent.
I want to recreate what Aviary does in their app. You can scale up and down an "sticker" and the border always stays the same size.
I was trying to do this effect in this way as well in Swift 4.2. Ultimately I could not do it successfully using CGAffine Effects. I ultimately had to update the constraints -- as I was using anchors.
Here is the code I ultimately started using. SelectedView is the view that should be scaled.
Keep in mind that this does not break other CGAffine effects. I'm still using those for rotation.
#objc private func scaleSelectedView(_ sender: UIPinchGestureRecognizer) {
guard let selectedView = selectedView else {
return
}
var heightDifference: CGFloat?
var widthDifference: CGFloat?
// increase width and height according to scale
selectedView.constraints.forEach { constraint in
guard
let view = constraint.firstItem as? UIView,
view == selectedView
else {
return
}
switch constraint.firstAttribute {
case .width:
let previousWidth = constraint.constant
constraint.constant = constraint.constant * sender.scale
widthDifference = constraint.constant - previousWidth
case .height:
let previousHeight = constraint.constant
constraint.constant = constraint.constant * sender.scale
heightDifference = constraint.constant - previousHeight
default:
return
}
}
// adjust leading and top anchors to keep view centered
selectedView.superview?.constraints.forEach { constraint in
guard
let view = constraint.firstItem as? UIView,
view == selectedView
else {
return
}
switch constraint.firstAttribute {
case .leading:
guard let widthDifference = widthDifference else {
return
}
constraint.constant = constraint.constant - widthDifference / 2
case .top:
guard let heightDifference = heightDifference else {
return
}
constraint.constant = constraint.constant - heightDifference / 2
default:
return
}
}
// reset scale after applying in order to keep scaling linear rather than exponential
sender.scale = 1.0
}
Notes: width and height anchors are on the view itself. Top and Leading anchors are on the superview -- hence the two forEach blocks.
I had to figure out a way of keeping my borders the same width during transforms (also creating stickers), and needed the solution to work with CGAffineTransform because my stickers rotate (and you cannot easily use frame-based solutions if you're doing rotations).
My approach was to change the layer.borderWidth in response to the transform changing, like this. Works for me.
class MyView : UIView {
let borderWidth = CGFloat(2)
override var transform: CGAffineTransform {
didSet {
self.setBorder()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.setBorder()
}
fileprivate func setBorder() {
self.layer.borderWidth = self.borderWidth / transform.scale
}
}
extension CGAffineTransform {
var scale: CGFloat {
return sqrt((a * a + c * c))
}
}
while setting CGAffineTransformScale, you wont be able to control the border-width separately.
The solution would be to use another view say borderView as subview in above view hierarchy than the view to be scaled (with backgroundColor as clearcolor)whose SIZE should be changed according to scale factor of other view.
Apply your border-width to the borderView keeping the desired borderwidth.
Best of luck!
Switch from CGAffineTransform to view's frame.
Sample code for pinching a view with constant border width:
#interface ViewController ()
#property (nonatomic, weak) IBOutlet UIView *testView;
#end
#implementation ViewController {
CGFloat _lastScale;
CGRect _sourceRect;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.testView.backgroundColor = [UIColor yellowColor];
self.testView.layer.borderColor = [UIColor blackColor].CGColor;
self.testView.layer.borderWidth = 1.0f;
self.testView.layer.cornerRadius = 8.0f;
_sourceRect = self.testView.frame;
_lastScale = 1.0;
}
- (IBAction)scale:(UIPanGestureRecognizer *)sender{
if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);
CGPoint center = self.testView.center;
CGRect newBounds = CGRectMake(0, 0, _sourceRect.size.width * scale, _sourceRect.size.height * scale);
self.testView.bounds = newBounds;
self.testView.layer.transform = CATransform3DMakeRotation(M_PI * (scale - 1), 0, 0, 1);
}
#end
UPD:
I have checked Aviary app: scale by frame, rotate by CGAffineTransform. You may need to implement some additional logic to make it work.
UPD:
Use bounds and play with rotation
Without setting the transform, scale the size of view by changing it's bounds. This solution works for me.
You can add a method in the view class you want to scale, sample code:
- (void)scale:(CGFloat)scale {
self.bounds = CGRectMake(0, 0, CGRectGetWidth(self.bounds) * scale, CGRectGetHeight(self.bounds) * scale);
}

Resources