Get height of UIView based on specific width before showing view - ios

I have a UIView which I have designed in Interface Builder. It basically consists out of a header image and some text (UILabel) below. The view is being shown modally with a custom Transition and doesn't fill the whole screen.
There is like a 20 pixels margin on the left and right and 40 px on the top. The UILabel gets filled with some text that's coming from the web. What I want do do, is to find (or should I say predict) the height of the whole view for a specific width. How can I do that?

I had similar problem, but instead of calculating font size and image height you can use this UIView extension that will autosize your view with max width:
extension UIView {
func autosize(maxWidth: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
let dummyContainerView = UIView(frame: CGRect(x: 0, y: 0, width: maxWidth, height: 10000000))
dummyContainerView.addSubview(self)
dummyContainerView.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
dummyContainerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0).isActive = true
dummyContainerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0).isActive = true
setNeedsLayout()
layoutIfNeeded()
removeFromSuperview()
frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
translatesAutoresizingMaskIntoConstraints = true
}
}
Using this approuch you don't have to worry about your content inside the view.
To use it:
let customView: CustomView = ... //create your view
... // configure all data on your view, e.g. labels with correct text
customView.autosize(maxWidth: 150) // resize your view
view.addSubview(customView) // add your view to any view

You need to have both the picture and the label before calculating the aspected size.
I guess you should use something like this (maybe adding the vertical inter-distance between the imageView and the Label to the sum, and maybe removing the lateral margins from the width):
objective C :
- (CGFloat)preferredHeightFromWidth:(CGFloat)width text:(NSString *)text font:(UIFont *)font image:(UIImage *)image
{
// Calculate label height
CGFloat labelHeight = [text
boundingRectWithSize:CGSizeMake(width, 10000)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:font}
context:[[NSStringDrawingContext alloc] init]
].size.height;
// Calculate image height
CGFloat ratio = image.size.height/ image.size.width;
CGFloat imageHeight = (ratio * width);
// Do the sum
return labelHeight + imageHeight;
}
Swift:
func preferredHeight(width: CGFloat, text: NSString, font: UIFont, image: UIImage) -> CGFloat {
// Calculate Label Height
let labelRect = text.boundingRect(
with: CGSize.init(width: width, height: 10000),
options: .usesLineFragmentOrigin,
attributes: [NSFontAttributeName : font],
context: NSStringDrawingContext())
let labelHeight = labelRect.height
// Calculate Image Height
let ratio = image.size.height / image.size.width
let imageHeight = ratio / width
// Calculate Total Height
let height = labelHeight + imageHeight
// Return Height Value
return height
}
(Thanks to Christopher Hannah for swift version)

Here is the same answer as Alberto, but I have changed it into Swift 3.
func preferredHeight(width: CGFloat, text: NSString, font: UIFont, image: UIImage) -> CGFloat {
// Calculate Label Height
let labelRect = text.boundingRect(
with: CGSize.init(width: width, height: 10000),
options: .usesLineFragmentOrigin,
attributes: [NSFontAttributeName : font],
context: NSStringDrawingContext())
let labelHeight = labelRect.height
// Calculate Image Height
let ratio = image.size.height / image.size.width
let imageHeight = ratio / width
// Calculate Total Height
let height = labelHeight + imageHeight
// Return Height Value
return height
}

Related

Auto-sizing a UILabel without setting an explicit height

How do I get a multi-line label to size itself? I don't want to set an explicit height for it but I do need to place it in view.
The way my app is built, we explicitly set frames and origins rather than using NSLayoutConstraints. It's a mature app so this isn't up for discussion.
I'd like to be able to give my UILabel an origin and a width and let it figure its own height out.
How can I do this? This is my playground code:
let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 180))
view.backgroundColor = .white
let l = UILabel()
l.text = "this is a really long label that should wrap around and stuff. it should maybe wrap 2 or three times i dunno"
l.textColor = .black
l.lineBreakMode = .byWordWrapping
l.numberOfLines = 0
l.textAlignment = .center
l.sizeToFit()
let margin: CGFloat = 60
view
view.addSubview(l)
l.frame = CGRect(x: margin, y: 0, width: view.bounds.width - (margin * 2), height: 100)
// I don't want to do this ^^
This may do what you want...
As requested, you want to set the .origin and .width of a UILabel and have it set its own .height based on the text.
class ZackLabel: UILabel {
override public func layoutSubviews() {
super.layoutSubviews()
let h = sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
self.frame.size.height = h.height
}
}
class ViewController: UIViewController {
var testLabel: ZackLabel!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// instantiate a 300 x 180 UIView at 20, 80
let myView = UIView(frame: CGRect(x: 20, y: 80, width: 300, height: 180))
myView.backgroundColor = .white
// instantiate a ZackLabel
testLabel = ZackLabel()
testLabel.text = "this is a really long label that should wrap around and stuff. it should maybe wrap 2 or three times i dunno"
testLabel.textColor = .black
testLabel.lineBreakMode = .byWordWrapping
testLabel.numberOfLines = 0
testLabel.textAlignment = .center
// set background color so we can see its frame
testLabel.backgroundColor = .cyan
let margin: CGFloat = 60
// set label's origin
testLabel.frame.origin = CGPoint(x: margin, y: 0)
// set label's width (label will set its own height)
testLabel.frame.size.width = myView.bounds.width - margin * 2
// add the view
view.addSubview(myView)
// add the label to the view
myView.addSubview(testLabel)
// add a tap recognizer so we can change the label's text at run-time
let rec = UITapGestureRecognizer(target: self, action: #selector(tapFunc(_:)))
view.addGestureRecognizer(rec)
}
#objc func tapFunc(_ sender: UITapGestureRecognizer) -> Void {
testLabel.text = "This is dynamic text being set."
}
}
Result (on an iPhone 8):
and, after tapping on the (yellow) view, dynamically changing the text:
label.sizeThatFits(CGSize(width: <your required width>, height: CGFloat.greatestFiniteMagnitude))
This returns the labels needed size, growing infinitely in height, but fitted to your required width. I've occasionally noticed minor inaccuracies with this function (rounding error?), so I tend to bump the width and height by 1 just to be safe.
UILabel comes with an intrinsic size that should be calculated based on the text and the label's .font property. You may need to add a margin to it...
var height = l.intrinsicContentSize.height
height += margin
l.frame = CGRect(x: margin, y: 0, width: view.bounds.width - (margin * 2), height: height)
Failing that, maybe you can try something like:
let size = CGSize(width: view.bounds.width - (margin * 2), height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
var estimatedFrame = CGRect()
if let font = l.font {
estimatedFrame = NSString(string: l.text).boundingRect(with: size, options: options, attributes: [NSAttributedString.Key.font: font], context: nil)
}
//if you need a margin:
estimatedFrame.height += margin
l.frame = estimatedFrame
Give your UILabel as a UIScrollview or UITableView cell subview.
Then you setup UILabel leading, tralling, top, bottom constrain.
If you give UITableview then set table view hight auto dynamic. If you give UIScrollview
just set UILabel bottom constrain priority low

Understanding UIScrollView contentSize.height

I'm kind of confused by this. I have a very simple setup. I have a UIScrollView that is constrained to the superview of a ViewController with 0,0,0,0. And then a UILabel that is constrained to the UIScrollView with 8,8,8,8.
I have a String extension to get the height of the label with a specific font.
extension String {
func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
return boundingBox.height
}
}
In viewDidLoad, I set up my label. (UILabel is set for number of lines to 0 and word wrap).
let text = "some really long text" // actually really long text in my sample
let width = view.frame.width
let font: UIFont! = UIFont(name: "HelveticaNeue-UltraLight", size: 42)
let height = text.heightWithConstrainedWidth(width: width, font: font)
print("height first: \(height)") // 1080
Then I set my scrollView.contentSize to that size
scrollView.contentSize = CGSize(width: view.frame.width, height)
print("new height: \(scrollView.contentSize.height)") // 1080
But in scrollViewDidScroll, if I print out the height there
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("scrollView.contentSize.height: \(scrollView.contentSize.height)") // 3440
}
I was wondering why in scrollViewDidScroll the height of the scrollView's contentSize is 3440 and not 1080. It's 1080 after I put in the text for the label and set up the content size.
I would think if the size is 3440, I would see that after I set the scrollView's contentSize and it wouldn't change in the scrollViewDidScroll delegate method. Is this because I'm using Auto Layout and Auto Layout doesn't calculate the size of the contentSize until you start scrolling or something?

How to make a UIImageView have a square size based on a given bounds/container

So I am trying to create a simple UIImageView to make it have a square frame/size with CGSize. Based on a given bounds. So for example if the bounds container is the width & height of the screen then. The function should resize the UIImageView to fit like a perfect square base on those bounds on the screen.
Code:
let myImageView = UIImageView()
myImageView.frame.origin.y = (self.view?.frame.height)! * 0.0
myImageView.frame.origin.x = (self.view?.frame.width)! * 0.0
myImageView.backgroundColor = UIColor.blue
self.view?.insertSubview(myImageView, at: 0)
//("self.view" is the ViewController view that is the same size as the devices screen)
MakeSquare(view: myImageView, boundsOf: self.view)
func MakeSquare(view passedview: UIImageView, boundsOf container: UIView) {
let ratio = container.frame.size.width / container.frame.size.height
if container.frame.width > container.frame.height {
let newHeight = container.frame.width / ratio
passedview.frame.size = CGSize(width: container.frame.width, height: newHeight)
} else{
let newWidth = container.frame.height * ratio
passedview.frame.size = CGSize(width: newWidth, height: container.frame.height)
}
}
The problem is its giving me back the same bounds/size of the container & not changed
Note: I really have know idea how to pull this off, but wanted to see if its possible. My function comes from a question here. That takes a UIImage and resizes its parent view to make the picture square.
This should do it (and will centre the image view in the containing view):
func makeSquare(view passedView: UIImageView, boundsOf container: UIView) {
let minSize = min(container.bounds.maxX, container.bounds.maxY)
passedView.bounds = CGRect(x: container.bounds.midX - minSize / 2.0,
y: container.bounds.midY - minSize / 2.0,
width: minSize, height: minSize)
}

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)

Swift How to calculate one line text height from its font

I ran into an issue where I needed to animate translating a label vertically the same distance of a textField's text height. In most cases just textField.bounds.heigt but if the textField's height is bigger than the text height it will not be any good for me. So I need to know:
How to calculate the line height of the string text from its UIFont?
Regarding the duplicate:
There's a little bit different of what I need. that answer(which I've referenced in my answer) get the total height depending on 1) the string 2) the width 3) the font. What I needed is one line height dpending only on the font.
UIFont has a property lineHeight:
if let font = _textView.font {
let height = font.lineHeight
}
where font is your font
I have been searching for a way to do that and find this answer where it has a String extension to calculate the size for the string and a given font. I have modified it to do what I want (get the line height of text written using a font.):
extension UIFont {
func calculateHeight(text: String, width: CGFloat) -> CGFloat {
let constraintRect = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
let boundingBox = text.boundingRect(with: constraintRect,
options: NSStringDrawingOptions.usesLineFragmentOrigin,
attributes: [NSAttributedStringKey.font: self],
context: nil)
return boundingBox.height
}
}
I hope this helps someone looking for it. (may be myself in the future).
I've used this String extension in the past to draw some text as opposed to creating a UILabel somewhere. I don't like the fact that I can't seem to get the real height of the specific text I want to draw (not every string contains capital letters or characters with descenders, etc.) I've used a couple of enums for horizontal and vertical alignment around the given point. Open to ideas on the vertical height.
public func draw(at pt: CGPoint,
font: UIFont? = UIFont.systemFont(ofSize: 12),
color: UIColor? = .black,
align: HorizontalAlignment? = .Center,
vAlign: VerticalAlignment? = .Middle)
{
let attributes: [NSAttributedString.Key : Any] = [.font: font!,
.foregroundColor: color!]
let size = self.boundingRect(with: CGSize(width: 0, height: 0),
options: [ .usesFontLeading ],
attributes: [ .font: font! ],
context: nil).size
var x = pt.x
var y = pt.y
if align == .Center {
x -= (size.width / 2)
} else if align == .Right {
x -= size.width
}
if vAlign == .Middle {
y -= (size.height / 2)
} else if vAlign == .Bottom {
y -= size.height
}
let rect = CGRect(x: x, y: y, width: size.width, height: size.height)
draw(in: rect, withAttributes: attributes)
}

Resources