Text Wrapping not happening as it should in NSTextField Attributed String - swift

I am having an issue with text not wrapping correctly if there is a single quote, or macOS ASCII Extended Character #213 (shift+opt.+]) in a string.
Apple does not escape the media item title string when it is retrieved through the iTunesLibrary framework.
As you can see in the example below, the first string is exactly how it come from the iTunesLibrary using the framework API call. The second string is is the single quote is escaped, the third string is if I use macOS Extended ASCII Character code 213, and the fourth string is if I use a tilde. The tilde is not the right character to use in this situation, but it is the only one that correctly wraps the text in the cell.
I've been working on this for the past 6-8 hours to figure it out and I'm just throwing it out there to see if someone can help me.
ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.frame.size = NSSize(width: 616, height: 184)
// Strings
let string1 = "I Keep Forgettin' (Every Time You're Near)"
let string2 = "I Keep Forgettin\' (Every Time You're Near)"
let string3 = "I Keep Forgettin’ (Every Time You're Near)"
let string4 = "I Keep Forgettin` (Every Time You're Near)"
// Formatting
let foreground = NSColor.purple.cgColor
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.tabStops = .none
paragraphStyle.baseWritingDirection = .leftToRight
guard let font = NSFont(name: "Helvetica", size: 28.0) else { return }
// Labels
let label1 = NSTextField(frame: NSRect(x: 20, y: self.view.frame.minY+20, width: 144, height: 144))
label1.cell = VerticallyCenteredTextFieldCell()
label1.wantsLayer = true
label1.layer?.borderColor = NSColor.purple.cgColor
label1.layer?.borderWidth = 0.5
label1.layer?.backgroundColor = NSColor.lightGray.cgColor
label1.alphaValue = 1
var fontSize = bestFontSize(attributedString: NSAttributedString(string: string1, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label1.attributedStringValue = NSAttributedString(string: string1, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label1)
let label2 = NSTextField(frame: NSRect(x: 164, y: self.view.frame.minY+20, width: 144, height: 144))
label2.cell = VerticallyCenteredTextFieldCell()
label2.wantsLayer = true
label2.layer?.borderColor = NSColor.purple.cgColor
label2.layer?.borderWidth = 0.5
label2.layer?.backgroundColor = NSColor.lightGray.cgColor
label2.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string2, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label2.attributedStringValue = NSAttributedString(string: string2, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label2)
let label3 = NSTextField(frame: NSRect(x: 308, y: self.view.frame.minY+20, width: 144, height: 144))
label3.cell = VerticallyCenteredTextFieldCell()
label3.wantsLayer = true
label3.layer?.borderColor = NSColor.purple.cgColor
label3.layer?.borderWidth = 0.5
label3.layer?.backgroundColor = NSColor.lightGray.cgColor
label3.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string3, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label3.attributedStringValue = NSAttributedString(string: string3, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label3)
let label4 = NSTextField(frame: NSRect(x: 452, y: self.view.frame.minY+20, width: 144, height: 144))
label4.cell = VerticallyCenteredTextFieldCell()
label4.wantsLayer = true
label4.layer?.borderColor = NSColor.purple.cgColor
label4.layer?.borderWidth = 0.5
label4.layer?.backgroundColor = NSColor.lightGray.cgColor
label4.alphaValue = 1
fontSize = bestFontSize(attributedString: NSAttributedString(string: string4, attributes: [.font: font, .paragraphStyle: paragraphStyle]), size: CGSize(width: 136, height: 136))
label4.attributedStringValue = NSAttributedString(string: string4, attributes: [.font: font.withSize(fontSize), .foregroundColor: foreground, .paragraphStyle: paragraphStyle])
self.view.addSubview(label4)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func bestFontSize(attributedString: NSAttributedString, size: CGSize) -> CGFloat {
// Create a property to hold the font and size
var font: NSFont?
// Get the font information from the string attibutes
attributedString.enumerateAttribute(.font, in: NSRange(0..<attributedString.length)) { value, range, stop in
if let attrFont = value as? NSFont {
font = attrFont
}
}
if font == nil {
return 0
}
// Get any paragraph styling attributes
var paragraphStyle: NSMutableParagraphStyle?
attributedString.enumerateAttribute(.paragraphStyle, in: NSMakeRange(0, attributedString.length)) { value, range, stop in
if let style = value as? NSMutableParagraphStyle {
paragraphStyle = style
}
}
if paragraphStyle == nil {
return 0
}
// Create a sorted list of words from the string in descending order of length (chars) of the word
let fragment = attributedString.string.split(separator: " ").sorted() { $0.count > $1.count }
// Create a bounding box size that will be used to check the width of the largest word in the string
var width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
// Create a bounding box size that will be used to check the height of the string
var height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
while height >= size.height || width >= size.width {
guard let pointSize = font?.pointSize else {
return 0
}
font = font?.withSize(pointSize-0.25)
width = String(fragment[0]).boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: size.height), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).width.rounded(.up)
height = attributedString.string.boundingRect(with: CGSize(width: size.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: font!, .paragraphStyle: paragraphStyle!], context: nil).height.rounded(.up)
}
return font!.pointSize
}
}
VerticallyCenteredTextFieldCell.swift
import Cocoa
class VerticallyCenteredTextFieldCell: NSTextFieldCell {
// https://stackoverflow.com/questions/11775128/set-text-vertical-center-in-nstextfield/33788973 - Sayanti Mondal
func adjustedFrame(toVerticallyCenterText rect: NSRect) -> NSRect {
// super would normally draw from the top of the cell
var titleRect = super.titleRect(forBounds: rect)
let minimumHeight = self.cellSize(forBounds: rect).height
titleRect.origin.y += (titleRect.height - minimumHeight) / 2
titleRect.size.height = minimumHeight
return titleRect
}
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawInterior(withFrame: adjustedFrame(toVerticallyCenterText: cellFrame), in: controlView)
}
}
This is the result I get:
Anyone else get the same result running this?

Related

CIAttributedTextImageGenerator filter - text doesn't fit (NSAttributedString)

I use a Core Image filter CIAttributedTextImageGenerator to generate text as a CIImage. However, sometimes the text just doesn't fit into the resulted CIImage as you can see at the picture:
I tried to play with different key-values of NSAttributedString to make some padding around text but with no success:
func generateImageFromText(_ text: String, style: TextStyle) -> CIImage? {
let font = UIFont.init(name: style.fontName, size: style.fontSize)!
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = style.textAlignment
paragraphStyle.headIndent = 5.0
paragraphStyle.tailIndent = -5.0
paragraphStyle.firstLineHeadIndent = 0
let shadow = NSShadow()
if let shadowStyle = style.shadowStyle {
shadow.shadowColor = shadowStyle.color
shadow.shadowOffset = shadowStyle.offset
shadow.shadowBlurRadius = shadowStyle.blurRadius
}
var strokeColor = UIColor.clear
var strokeWidth: CGFloat = 0.0
if let strokeStyle = style.strokeStyle {
strokeColor = strokeStyle.color
strokeWidth = strokeStyle.width
}
let attributes: [NSAttributedString.Key: Any] = [
.baselineOffset: 50,
.font: font,
.foregroundColor: style.color,
.paragraphStyle: paragraphStyle,
.shadow: shadow,
.strokeColor: strokeColor,
.strokeWidth: strokeWidth
]
let attributedQuote = NSAttributedString(string: text, attributes: attributes)
let textGenerationFilter = CIFilter(name: "CIAttributedTextImageGenerator")!
textGenerationFilter.setValue(attributedQuote, forKey: "inputText")
textGenerationFilter.setValue(NSNumber(value: Double(1.0)), forKey: "inputScaleFactor")
guard let textImage = textGenerationFilter.outputImage else {
return nil
}
return textImage
}
Maybe there are some values of NSAttributedString that I miss which can help to fit in the text?

How can I properly pass an array in iOS PDFKit

I'm trying to in an [string] array for a PDF. Below is what I have so far. I am guess I need to do a foreach somewhere, but I'm not entirely sure.
I thought something like this might work, but it does not.
for entry in body {
let attributedText = NSAttributedString(
string: entry,
attributes: textAttributes
)
}
private func addBody(body: [String], pageRect: CGRect, textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
let attributedText = NSAttributedString(
string: body,
attributes: textAttributes
)
let textRect = CGRect(
x: 15,
y: bodyCG + 30,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
Adding some additional details. If I don't use [String] and just use String everything works fine. The PDF is generated, The concept I'm struggling to understand is how can I pass an array for the PDF.
var courseAttendees : [String] = ["name", "name", "name", "name"]
For Example, I want to pass courseAttendees and then loop through the array the names are just overlapped and shown below.
Final code.
private func addBody(body: [String], textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let textAttributes: [NSAttributedString.Key: Any] =
[NSAttributedString.Key.font: textFont]
// keep track of the y position on the page. You might need
// to set this globally as you have multiple pages
var currentYPos: CGFloat = bodyCG
// Loop through the array
for entry in body {
let attributedText = NSAttributedString(
string: "\(entry)",
attributes: textAttributes
)
// Update the currentYPos
currentYPos += 15
// Use the currentYPos in the textRect
let textRect = CGRect(
x: 15,
y: currentYPos,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
}
Based on your question and image, I am assuming the PDF creation works fine but the data is not rendered as desired.
I think the two questions to answer here are:
Where to loop through your array
How to keep track of the current y coordinate in the page which is responsible for the vertical positioning
Here are some additions I made to try and fix your issue, I have added comments to what I have changed
// Somewhere appropriate in your code
var courseAttendees : [String] = ["name1", "name2", "name3", "name4"]
// Call the function, 15 is just a random number for textTop,
// give it what you feel is appropriate
addBody(body: courseAttendees, textTop: 15)
// I have removed the pageRect parameter since you create it
// in the function
private func addBody(body: [String], textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
// keep track of the y position on the page. You might need
// to set this globally as you have multiple pages
var currentYPos: CGFloat = 0.0
// Loop through the array
for entry in body {
let attributedText = NSAttributedString(
string: entry,
attributes: textAttributes
)
// Update the currentYPos
currentYPos += bodyCG + 30
// Use the currentYPos in the textRect
let textRect = CGRect(
x: 15,
y: currentYPos,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
}
I have not tested the above so please give this a try and check if it solves your issue.
If not, comment and I will update this accordingly.

NSImage Getting Resized when I draw Text on it

I have the following code to draw a text over an NSImage.
But the resulting image is getting resized to smaller one when I save it to disk.
What i'm i doing wrong? Please advice
func drawText(image :NSImage) ->NSImage
{
let text = "Sample Text"
let font = NSFont.boldSystemFont(ofSize: 18)
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
let textStyle = NSMutableParagraphStyle.default().mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSFontAttributeName: font,
NSForegroundColorAttributeName: NSColor.white,
NSParagraphStyleAttributeName: textStyle
]
let im:NSImage = NSImage(size: image.size)
let rep:NSBitmapImageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace, bytesPerRow: 0, bitsPerPixel: 0)!
im.addRepresentation(rep)
im.lockFocus()
image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes)
im.unlockFocus()
return im
}
This is a different approach using a temporary NSView to draw the image and the text and cache the result in a new image (code is Swift 4). The benefit it you don't need to deal with pixels
class ImageView : NSView {
var image : NSImage
var text : String
init(image: NSImage, text: String)
{
self.image = image
self.text = text
super.init(frame: NSRect(origin: NSZeroPoint, size: image.size))
}
required init?(coder decoder: NSCoder) { fatalError() }
override func draw(_ dirtyRect: NSRect) {
let font = NSFont.boldSystemFont(ofSize: 18)
let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
image.draw(in: dirtyRect)
text.draw(in: textRect, withAttributes: [.font: font, .foregroundColor: NSColor.white])
}
var outputImage : NSImage {
let imageRep = bitmapImageRepForCachingDisplay(in: frame)!
cacheDisplay(in: frame, to:imageRep)
let tiffData = imageRep.tiffRepresentation!
return NSImage(data : tiffData)!
}
}
To use it, initialize a view
let image = ... // get some image
let view = ImageView(image: image, text: "Sample Text")
and get the new image
let imageWithText = view.outputImage
Note:
The paragraph style is not used at all, but if you want to create a mutable paragraph style just write
let textStyle = NSMutableParagraphStyle()
Mixed pixel vs point?
Depending on your screen 2x or 3x image is smaller 2 times or 3 times?
Here is more detailed info (scroll down to "Converting between pixels and points")
http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/
But keep in mind that:
NSImage is resolution aware and uses a HiDPI graphics context when you lockFocus on a system with retina screen.
The image dimensions you pass to your NSBitmapImageRep initializer are in points (not pixels). An 150.0 point-wide image therefore uses 300 horizontal pixels in a #2x context.
Source:
How to save PNG file from NSImage (retina issues)
Following simple app works for me. Enjoy ;)
import Cocoa
class ViewController: NSViewController {
func save(image:NSImage, imageURL:String, format:String) -> Bool
{
let bMImg = NSBitmapImageRep(data: (image.tiffRepresentation)!)
switch format {
case ".png":
let filepath = URL(fileURLWithPath: imageURL+".png")
let dataToSave = bMImg?.representation(using: NSBitmapImageRep.FileType.png, properties: [NSBitmapImageRep.PropertyKey.compressionFactor : 1])
do
{
try dataToSave?.write(to: filepath)
return true
} catch {
return false
}
default:
return false
}
}
func draw(text:String, image:NSImage) -> NSImage
{
let font = NSFont.boldSystemFont(ofSize: 18)
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSAttributedStringKey.font: font,
NSAttributedStringKey.foregroundColor: NSColor.white,
NSAttributedStringKey.paragraphStyle: textStyle
]
let im:NSImage = NSImage(size: image.size)
let rep:NSBitmapImageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSColorSpaceName.calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0)!
im.addRepresentation(rep)
im.lockFocus()
image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes)
im.unlockFocus()
return im
}
#IBAction func action(_ sender: NSButton) {
let dialog = NSOpenPanel();
dialog.title = "Choose a image...";
dialog.showsResizeIndicator = true;
dialog.showsHiddenFiles = false;
dialog.canChooseDirectories = true;
dialog.canCreateDirectories = true;
dialog.allowsMultipleSelection = false;
dialog.allowedFileTypes = ["png", "jpg"];
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
guard let url = dialog.url,
let imageCIImage = CIImage(contentsOf: url) else {
return
}
let rep: NSCIImageRep = NSCIImageRep(ciImage: imageCIImage)
let nsImage = NSImage(size: rep.size)
nsImage.addRepresentation(rep)
let imageWithText = draw(text:"ABC", image: nsImage)
if (save(image: imageWithText, imageURL: "imageWithText", format: ".png")) {
print("Success")
} else {
print("ERROR:Failed to save image")
}
} else {
// User clicked on "Cancel"
return
}
}
}

Swift boundingRect returns different values for same input on playground and actual project

I'm not perfectly sure why boundingRect returns incorrect value in some case such as when a text ends closed to its maximum width. It works fine on all other cases though.
Here's example.
func snap(_ x: CGFloat) -> CGFloat {
let scale = UIScreen.main.scale
return ceil(x * scale) / scale
}
func snap(_ point: CGPoint) -> CGPoint {
return CGPoint(x: snap(point.x), y: snap(point.y))
}
func snap(_ size: CGSize) -> CGSize {
return CGSize(width: snap(size.width), height: snap(size.height))
}
func snap(_ rect: CGRect) -> CGRect {
return CGRect(origin: snap(rect.origin), size: snap(rect.size))
}
extension String {
func boundingRect(with size: CGSize, attributes: [String: AnyObject]) -> CGRect {
let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
let rect = self.boundingRect(with: size, options: options, attributes: attributes, context: nil)
return snap(rect)
}
func size(fits size: CGSize, font: UIFont, maximumNumberOfLines: Int = 0) -> CGSize {
let attributes = [NSFontAttributeName: font]
var size = self.boundingRect(with: size, attributes: attributes).size
if maximumNumberOfLines > 0 {
size.height = min(size.height, CGFloat(maximumNumberOfLines) * font.lineHeight)
}
return size
}
func width(with font: UIFont, maximumNumberOfLines: Int = 0) -> CGFloat {
let size = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
return self.size(fits: size, font: font, maximumNumberOfLines: maximumNumberOfLines).width
}
func height(fits width: CGFloat, font: UIFont, maximumNumberOfLines: Int = 0) -> CGFloat {
let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
return self.size(fits: size, font: font, maximumNumberOfLines: maximumNumberOfLines).height
}
func height(fits width: CGFloat, attributes: [String:AnyObject]) -> CGFloat {
let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
return self.boundingRect(with: size, attributes: attributes).height
}
}
with above extension and methods, I ran below code in playground.
let str = "ありがとうございます。スマホ専用のページがなくても反映できるかはエンジンにご確認いたします。"
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.alignment = .left
let attributes = [
NSFontAttributeName: UIFont.systemFont(ofSize: 15),
NSParagraphStyleAttributeName: paragraphStyle
]
let height = str.height(fits: 320, attributes: attributes)
print("calculated height: ", height)
and it returned 54.
However, when I ran same code in simple project (single view controller, copied and pasted above into viewDidLoad: in ViewController, it returned 36 (missing one line height). Any idea why this two behave differently with same input?
I have run your code on multiple simulators for iOS 10.3 and they all give me the value of 54.0 for calculated height.
You didn't specify which version of iOS you are using, so perhaps that is significant.
Also, print the font that is used in your app and in your playground. For UIFont.systemFont(ofSize: 15) I get .SFUIText in both.

How to get the height of a UILabel in Swift?

I am a beginner in Swift and I am trying to get the height of a label.
The label has multiple lines of text. I want to know the total height it occupies on the screen.
Swift 4 with extension
extension UILabel{
public var requiredHeight: CGFloat {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: frame.width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.attributedText = attributedText
label.sizeToFit()
return label.frame.height
}
}
it's simple, just call
label.bounds.size.height
Updated for Swift 3
func estimatedHeightOfLabel(text: String) -> CGFloat {
let size = CGSize(width: view.frame.width - 16, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSFontAttributeName: UIFont.systemFont(ofSize: 10)]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
override func viewDidLoad() {
super.viewDidLoad()
guard let labelText = label1.text else { return }
let height = estimatedHeightOfLabel(text: labelText)
print(height)
}
Swift 5 ioS 13.2 tested 100%, best solution when the UILabel numberOfLines = 0
Note, result is rounded. Just remove ceil() if you don't want it.
If you want to get height -> give storyboard width of UILabel
If you want to get width -> give storyboard height of UILabel
let stringValue = ""//your label text
let width:CGFloat = 0//storybord width of UILabel
let height:CGFloat = 0//storyboard height of UILabel
let font = UIFont(name: "HelveticaNeue-Bold", size: 18)//font type and size
func getLableHeightRuntime() -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = stringValue.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
func getLabelWidthRuntime() -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = stringValue.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.width)
}
#iajmeri43's answer Updated for Swift 5
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedString.Key.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
To use it:
// 1. get the text from the label
guard let theLabelsText = myLabel.text else { return }
// 2. get the width of the view the label is in for example a cell
// Here I'm just stating that the cell is the same exact width of whatever the collection's width is which is usually based on the width of the view that collectionView is in
let widthOfCell = self.collectionView.frame.width
// 3. get the font that your using for the label. For this example the label's font is UIFont.systemFont(ofSize: 17)
let theLabelsFont = UIFont.systemFont(ofSize: 17)
// 4. Plug the 3 values from above into the function
let totalLabelHeight = estimatedLabelHeight(text: theLabelsText, width: widthOfCell, font: theLabelsFont)
// 5. Print out the label's height with decimal values eg. 95.46875
print(totalLabelHeight)
// 6. as #slashburn suggested in the comments, use the ceil() function to round out the totalLabelHeight
let ceilHeight = ceil(totalLabelHeight)
// 7. Print out the ceilHeight rounded off eg. 95.0
print(ceilHeight)