Find rect/position of words in NSTextField - swift

I have a NSTextField with a sentence in it. I want to find a rect (ideally) or position for each word in the sentence to do things in those positions (outside of the NSTextField).
Doing this in a NSTextView/UITextView seems achievable with NSLayoutManager.boundingRectForGlyphRange, but without the NSLayoutManager that NSTextView (and UITextView) have it seems a bit more challenging.
What is the best way for me to find the position of a given word in a NSTextField?

It requires one barely-documented bit of magic and another completely undocumented bit. Here's Objective-C code. Don't have it handy in Swift, sorry.
NSRect textBounds = [textField.cell titleRectForBounds:textField.bounds];
NSTextContainer* textContainer = [[NSTextContainer alloc] init];
NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
NSTextStorage* textStorage = [[NSTextStorage alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
textContainer.lineFragmentPadding = 2;
layoutManager.typesetterBehavior = NSTypesetterBehavior_10_2_WithCompatibility;
textContainer.containerSize = textBounds.size;
[textStorage beginEditing];
textStorage.attributedString = textField.attributedStringValue;
[textStorage endEditing];
NSUInteger count;
NSRectArray rects = [layoutManager rectArrayForCharacterRange:matchRange
withinSelectedCharacterRange:matchRange
inTextContainer:textContainer
rectCount:&count];
for (NSUInteger i = 0; i < count; i++)
{
NSRect rect = NSOffsetRect(rects[i], textBounds.origin.x, textBounds.origin.y);
rect = [textField convertRect:rect toView:self];
// do something with rect
}
The typesetterBehavior is documented here. The lineFragmentPadding was determined empirically.
Depending on exactly what you're planning to do with the rectangles, you may wish to pass { NSNotFound, 0 } as the selected character range.
For efficiency, you generally want to keep the text objects around instead of instantiating them every time. You just set the text container's containerSize and the text storage's attributedString to the appropriate values each time.

According the Ken Thomases answer. I made a adaptation for Swift 4.2
guard let textFieldCell = textField.cell,
let textFieldCellBounds = textFieldCell.controlView?.bounds else{
return
}
let textBounds = textFieldCell.titleRect(forBounds: textFieldCellBounds)
let textContainer = NSTextContainer()
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage()
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
layoutManager.typesetterBehavior = NSLayoutManager.TypesetterBehavior.behavior_10_2_WithCompatibility
textContainer.containerSize = textBounds.size
textStorage.beginEditing()
textStorage.setAttributedString(textFieldCell.attributedStringValue)
textStorage.endEditing()
let rangeCharacters = (textFieldCell.stringValue as NSString).range(of: "string")
var count = 0
let rects: NSRectArray = layoutManager.rectArray(forCharacterRange: rangeCharacters,
withinSelectedCharacterRange: rangeCharacters,
in: textContainer,
rectCount: &count)!
for i in 0...count {
var rect = NSOffsetRect(rects[i], textBounds.origin.x, textBounds.origin.y)
rect = textField.convert(rect, to: self.view)
// do something with rect
}
All Credits to Ken Thomases

Related

Is this a UITextView transparency bug?

This issue came up in relation to a problem I had yesterday for which I should be able to create a workaround. As I investigated further, I found that it occurs more broadly than I originally thought. I had previously only noticed it in displayed text that included at least one newline character, but that's not the case below.
The problem seems to result from using the NSLayoutManager's boundingRect method to obtain (among other things) individual character widths and then using those widths to set characters' UITextView frame width properties. Doing so apparently causes the setting of the text view's backgroundColor to UIColor.clear to be ignored (i.e., the background becomes opaque). The Playground code below reproduces the problem, shown in red text, and shows the workaround of using a constant for widths, in black. The tighter the kerning, the more pronounced the effect.
Is this a bug? Or is it a quirk due to something else?
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.bounds = CGRect(x: -100, y: -100, width: 200, height: 200)
view.backgroundColor = .white
let str = "..T.V.W.Y.."
let strStorage = NSTextStorage(string: str)
let layoutManager = NSLayoutManager()
strStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: view.bounds.size)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
let strArray = Array(str)
struct CharInfo {
var char: Character
var origin: CGPoint?
var size: CGSize?
}
var charInfoArray = [CharInfo]()
for index in 0..<str.count {
charInfoArray.append(CharInfo.init(char: strArray[index], origin: nil, size: nil))
let charRange = NSMakeRange(index, 1)
let charRect = layoutManager.boundingRect(forGlyphRange: charRange, in: textContainer)
charInfoArray[index].origin = charRect.origin
charInfoArray[index].size = charRect.size
}
for charInfo in charInfoArray {
let textView0 = UITextView()
textView0.backgroundColor = UIColor.clear // Ignored in this case!!
textView0.text = String(charInfo.char)
textView0.textContainerInset = UIEdgeInsets.zero
let size0 = charInfo.size!
textView0.frame = CGRect(origin: charInfo.origin!, size: size0)
textView0.textContainer.lineFragmentPadding = CGFloat(0.0)
textView0.textColor = UIColor.red
view.addSubview(textView0)
let textView1 = UITextView()
textView1.backgroundColor = UIColor.clear // Required
textView1.text = String(charInfo.char)
textView1.textContainerInset = UIEdgeInsets.zero
var size1 = charInfo.size!
size1.width = 20 // But changing .height has no effect on opacity
textView1.frame = CGRect(origin: charInfo.origin!, size: size1)
textView1.frame = textView1.frame.offsetBy(dx: 0, dy: 20)
textView1.textContainer.lineFragmentPadding = CGFloat(0.0)
textView1.textColor = UIColor.black
view.addSubview(textView1)
}
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
This does seem to be a bug, but it's with NSLayoutManager's instance method boundingRect(forGlyphRange:in:). It only looks like it could be a transparency change.
According to Apple's documentation, boundingRect(forGlyphRange:in:) is supposed to "[return] a single bounding rectangle (in container coordinates) enclosing all glyphs and other marks drawn in the given text container for the given glyph range, including glyphs that draw outside their line fragment rectangles and text attributes such as underlining." But that's not what it's doing.
In this case, the width of each boundingRect gets reduced by the amount that the next glyph was shifted to the left, due to kerning. You can test this, for example, using str = "ToT" and adding print(size0.width) right after it is set. You'll get this:
6.0 // "T"; should have been 7.330078125
6.673828125 // "o"
7.330078125 // "T"
Until this bug is fixed, a workaround would be to calculate glyph size for each character in isolation.

Find Touched characters in UILabel with attributedString without using TextStorage

I've been investigating ways to find hashtags/mentions/... in UILabel.
All Libs out there are just doing fine, but when it comes to RTL languages like Persian and texts getting too long in UITableView the scrolling becomes lagging and Consumes CPU up to 50% each time this row is visited. I figured out that all these libs are using textStorage somehow, and by removing it and using a cache method the lag is all gone and list scrolls smoothly, but the problem is that without textStorage its not possible (I can't find a way) to find which character is touched by the user. so if I solve this, my problem with RTL languages is solved, so here's the Question: we've got a UIlabel and a CGPoint how to find characters touched?
currently using this modified method by ContextLabel Library:
fileprivate func linkResult(at location: CGPoint) -> LinkResult? {
var fractionOfDistance: CGFloat = 0.0
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &fractionOfDistance)
if characterIndex <= textStorage?.length {
if let linkResults = contextLabelData?.linkResults {
for linkResult in linkResults {
let rangeLocation = linkResult.range.location
let rangeLength = linkResult.range.length
let rect = self.boundingRect(forCharacterRange: linkResult.range)
if(rect?.contains(location))!
{
return linkResult
}
}
}
}
return nil
}
which boundingRect is
func boundingRect(forCharacterRange range: NSRange) -> CGRect? {
guard let attributedText = attributedText else { return nil }
let textStorage = NSTextStorage(attributedString: attributedText)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size:CGSize(width:self.bounds.size.width, height: CGFloat.greatestFiniteMagnitude))
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
and linkResults are those hashtags, mentions, url... that are previously found and now its going to search through them , this always returns nil when textStorage is removed
UPDATE: its working now with current methods for English Text no matter how long but not for RTL yet

Can't get UITextField to autoshrink text

I have a UITextField on a table view cell, and when it's text becomes too long I would like the font size to decrease. I want to make it very clear that I am talking about a UITextField, not a UILabel or a UITextView. The reason I say this is because I have seen this question pop up several times and the answers were all based on UILabel instead of UITextField. For example, someone asked "I can't get my UITextField to autoshrink" and the answer was "make sure it's numberOfLines is set to 1". To the best of my knowledge, a UITextField does not even have that property and is a single line control.
I have tried:
in IB setting the font to system 14.0, minFontSize to 7 and checking the "adjust to fit" box
in code in tableView:cellForRowAtIndexPath:
ptCell.name.font = [UIFont systemFontOfSize: 14.0];
ptCell.name.adjustsFontSizeToFitWidth = YES;
ptCell.name.minimumFontSize = 7.0;
but neither of these have worked. By that I mean that instead of the text shrinking it truncates the tail.
Does anyone know what I am missing? Presumably this should work because I have seen other questions complaining that it is doing that when the user does not want it to.
I had the same problem. The UITextField stoped shrinking the text when it was too long, but instead it resized itself and grew outside its 'bounds'.
The solution that helped me was to set width constraint on given UITextField. After that it did not grew anymore, instead the text inside got smaller as intended.
(Of course you have to set minFontSize and check the "adjust to fit" box in storyboard.)
I know it's kind of a late, but if anyone else will find this question via google as I did...it might just help.
I used the answer posted by #Purva as a starting point to come up with this method that gives the required font size starting at the configured font size, and not to drop below the configured minimum font size. Whereas #Purva tested for the height of the text I required the width to fit. This method can be put in either a category or a subclass of UITextField. I have it in a subclass which also captures the UITextFieldTextDidChangeNotification. From this notification handler I call the new method and resize the font if required. I also call it when I am assigning new text to the textfield to make sure it will fit by subclassing - (void)setText: (NSString*)text.
In each case, when I call it I am using the following code:
CGFloat requiredFontSize = self.requiredFontSize;
if( self.font.pointSize != requiredFontSize )
{
self.font = [self.font fontWithSize: requiredFontSize];
}
Here is the new method:
- (CGFloat)requiredFontSize
{
const CGRect textBounds = [self textRectForBounds: self.frame];
const CGFloat maxWidth = textBounds.size.width;
if( _originalFontSize == -1 ) _originalFontSize = self.font.pointSize;
UIFont* font = self.font;
CGFloat fontSize = _originalFontSize;
BOOL found = NO;
do
{
if( font.pointSize != fontSize )
{
font = [font fontWithSize: fontSize];
}
CGSize size = [self.text sizeWithFont: font];
if( size.width <= maxWidth )
{
found = YES;
break;
}
fontSize -= 1.0;
if( fontSize < self.minimumFontSize )
{
fontSize = self.minimumFontSize;
break;
}
} while( TRUE );
return( fontSize );
}
I think you want to set the adjustsFontSizeToFitWidth property for your UITextField to YES, and specify a minimumFontSize. Works for me, but I add the UITextField programmatically - IB issue?
Swift 3.0 way to resize the font to fit:
let startFontSize = 14.0
let minFontSize = 7.0
func resizeFont() {
guard let font = self.font, let text = self.text else {
return
}
let textBounds = self.textRect(forBounds: self.bounds)
let maxWidth = textBounds.size.width
for fontSize in stride(from: startFontSize, through: minFontSize, by: -0.5) {
let size = (text as NSString).size(attributes: [NSFontAttributeName: font.withSize(CGFloat(fontSize))])
self.font = font.withSize(CGFloat(fontSize))
if size.width <= maxWidth {
break
}
}
}
Try this its working for me (swift 3.0)
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// bla bla ... anything inside method you want to do
// reset font size of textfield
textField.font = UIFont.systemFont(ofSize: CGFloat(15.0))
var widthOfText: CGFloat = textField.text!.size(attributes: [NSFontAttributeName: textField.font!]).width
var widthOfFrame: CGFloat = textField.frame.size.width
// decrease font size until it fits (25 is constant that works for me)
while (widthOfFrame - 25) < widthOfText {
let fontSize: CGFloat = textField.font!.pointSize
textField.font = textField.font?.withSize(CGFloat(fontSize - 0.5))
widthOfText = (textField.text?.size(attributes: [NSFontAttributeName: textField.font!]).width)!
widthOfFrame = textField.frame.size.width
}
return true
}
-(BOOL)sizeFontToFit:(NSString*)aString minSize:(float)aMinFontSize maxSize:(float)aMaxFontSize
{
float fudgeFactor = 16.0;
float fontSize = aMaxFontSize;
self.font = [self.font fontWithSize:fontSize];
CGSize tallerSize = CGSizeMake(self.frame.size.width-fudgeFactor,kMaxFieldHeight);
CGSize stringSize = [aString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
while (stringSize.height >= self.frame.size.height)
{
if (fontSize <= aMinFontSize) // it just won't fit
return NO;
fontSize -= 1.0;
self.font = [self.font fontWithSize:fontSize];
tallerSize = CGSizeMake(self.frame.size.width-fudgeFactor,kMaxFieldHeight);
stringSize = [aString sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
}
return YES;
}
I had this same problem with UITextFields created in StoryBoard in Xcode 5.0.2, running iOS 7.0.4. I figured out that the .minimumFontSize property could not be changed by StoryBoard, or viewDidLoad, or viewWillAppear, but that it could be changed in viewDidAppear. So the following code solved my problem:
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.myTextField.minimumFontSize = 4.0;
[self.myTextField setNeedsLayout];
[self.myTextField layoutIfNeeded];
}
My humble solution for this problem was this.... (find the proper size of font on my own - to fit the size of frame) I made it inside delegate method of shouldChangeCharactersInRange:replacementString:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
// bla bla ... anything inside method you want to do
// reset font size of textfield
textField.font = [FONT_PROXY fontNormalOfSizeNormal];
CGFloat widthOfText = [textField.text sizeWithAttributes:#{NSFontAttributeName:textField.font}].width;
CGFloat widthOfFrame = textField.frame.size.width;
// decrease font size until it fits (25 is constant that works for me)
while((widthOfFrame - 25) < widthOfText){
CGFloat fontSize = textField.font.pointSize;
textField.font = [textField.font fontWithSize:fontSize - 0.5f];
widthOfText = [textField.text sizeWithAttributes:#{NSFontAttributeName:textField.font}].width;
widthOfFrame = textField.frame.size.width;
}
}
Tested with XCode8 and Swift 3.0.1
func getTextfield(view: UIView) -> [UITextField] {
var results = [UITextField]()
for subview in view.subviews as [UIView] {
if let textField = subview as? UITextField {
results += [textField]
} else {
results += getTextfield(view: subview)
}
}
return results
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let allTextFields = getTextfield(view: self.view)
for textField in allTextFields
{
textField.font = UIFont.boldSystemFont(ofSize: 14)
var widthOfText: CGFloat = textField.text!.size(attributes: [NSFontAttributeName: textField.font!]).width
var widthOfFrame: CGFloat = textField.frame.size.width
while (widthOfFrame - 15) < widthOfText { // try here to find the value that fits your needs
let fontSize: CGFloat = textField.font!.pointSize
textField.font = textField.font?.withSize(CGFloat(fontSize - 0.5))
widthOfText = (textField.text?.size(attributes: [NSFontAttributeName: textField.font!]).width)!
widthOfFrame = textField.frame.size.width
}
}
}
I used viewDidLayoutSubviews to get all textfields and autoshrink the text.
Maybe someone will need this code.
For me it works perfect.
The Code is from# Marek Manduch on this site and from
#Kunal Kumar on this site:
How to get all the textfields from a view in swift
The catch here is that minimumFontSize is a ratio and should be between 0 and 1 :)
f.adjustsFontSizeToFitWidth = true
f.minimumFontSize = 0.5
works perfectly :)
Can be solved by selecting Line Breaks = Clip in IB

Find out number of lines for a string with a font in a rect?

I want to find out how many lines a string will have when set to a font inside a rect. Is there a way to do this?
I have this code, but i'm trying to make it process that little bit faster, as it's being used a lot in a scroll view:
CGSize maximumSize = CGSizeMake(MESSAGE_FRAME.size.width - 16, 1000000);
CGSize expectedSize = [message sizeWithFont:[UIFont fontWithName:#"Crimson" size:15]
constrainedToSize:maximumSize
lineBreakMode:UILineBreakModeWordWrap];
return expectedSize.height;
I know this returns line height:
NSLog(#"%f", [[UIFont fontWithName:#"Crimson" size:15] lineHeight]);
So if I can multiply that by another value, it might prove to be faster.
I can't think of an alternative, and I doubt that there is one which will be much more efficient (after all, what possible magic could get to a line count and avoid the word wrap computation?).
Anyway, rather than hurt our brains shaving cycles off the computation, consider whether it needs to be done over and over while the view scrolls. My guess is no.
I think the way to speed this up is to do the traditional size computation and cache the result.
I don't know a different way but I have a small suggestion that I did in a similar case in a table view. If your data are stored somewhere (in my case I receive text from a network and store it in a database), I calculate the size using this way only the first time the text is received and then store this answer. When loading the data I put the sizes in an array so I can retrieve the values fast.
Just a suggestion, I hope it helps you.
For iOS7 and above, the officially sanctioned way is to use TextKit:
func numberOfLinesForString(string: String, size: CGSize, font: UIFont) -> Int {
let textStorage = NSTextStorage(string: string, attributes: [NSFontAttributeName: font])
let textContainer = NSTextContainer(size: size)
textContainer.lineBreakMode = .ByWordWrapping
textContainer.maximumNumberOfLines = 0
textContainer.lineFragmentPadding = 0
let layoutManager = NSLayoutManager()
layoutManager.textStorage = textStorage
layoutManager.addTextContainer(textContainer)
var numberOfLines = 0
var index = 0
var lineRange : NSRange = NSMakeRange(0, 0)
for (; index < layoutManager.numberOfGlyphs; numberOfLines++) {
layoutManager.lineFragmentRectForGlyphAtIndex(index, effectiveRange: &lineRange)
index = NSMaxRange(lineRange)
}
return numberOfLines
}

How to set font size to fill UILabel height?

I've seen a bunch of examples for changing the size of a UILabel.
Here's what I'd like to do:
Change the font size so that the text will be as large as possible within the new height.
Any clues?
I had the very same problem and, thanks to this thread and Joel's algorithm, I could fix it. :-)
Below is my code in Swift. I'm in iOS 8 + Autolayout.
Problem:
User inputs expenses:
When users tap the 'check' button, a menu appears from bottom, pushing everything to the top of the screen (shrinking stuff, including the label):
After the fix:
Which is exactly what the designer had in mind... :)
I subclassed UILabel and overrode layoutSubviews. Then each time the UILabel gets its size changed, the font size is recalculated:
//
// LabelWithAdaptiveTextHeight.swift
// 123
//
// Created by https://github.com/backslash-f on 12/19/14.
//
/*
Designed with single-line UILabels in mind, this subclass 'resizes' the label's text (it changes the label's font size)
everytime its size (frame) is changed. This 'fits' the text to the new height, avoiding undesired text cropping.
Kudos to this Stack Overflow thread: bit.ly/setFontSizeToFillUILabelHeight
*/
import Foundation
import UIKit
class LabelWithAdaptiveTextHeight: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
font = fontToFitHeight()
}
// Returns an UIFont that fits the new label's height.
private func fontToFitHeight() -> UIFont {
var minFontSize: CGFloat = DISPLAY_FONT_MINIMUM // CGFloat 18
var maxFontSize: CGFloat = DISPLAY_FONT_BIG // CGFloat 67
var fontSizeAverage: CGFloat = 0
var textAndLabelHeightDiff: CGFloat = 0
while (minFontSize <= maxFontSize) {
fontSizeAverage = minFontSize + (maxFontSize - minFontSize) / 2
// Abort if text happens to be nil
guard text?.characters.count > 0 else {
break
}
if let labelText: NSString = text {
let labelHeight = frame.size.height
let testStringHeight = labelText.sizeWithAttributes(
[NSFontAttributeName: font.fontWithSize(fontSizeAverage)]
).height
textAndLabelHeightDiff = labelHeight - testStringHeight
if (fontSizeAverage == minFontSize || fontSizeAverage == maxFontSize) {
if (textAndLabelHeightDiff < 0) {
return font.fontWithSize(fontSizeAverage - 1)
}
return font.fontWithSize(fontSizeAverage)
}
if (textAndLabelHeightDiff < 0) {
maxFontSize = fontSizeAverage - 1
} else if (textAndLabelHeightDiff > 0) {
minFontSize = fontSizeAverage + 1
} else {
return font.fontWithSize(fontSizeAverage)
}
}
}
return font.fontWithSize(fontSizeAverage)
}
}
Refer to this Pastebin for execution logs (println() of each
iteration).
There is a simpler solution. Just add below lines and magically, the label adjusts its font size to fit the height of the label too:
SWIFT 3:
label.minimumScaleFactor = 0.1 //or whatever suits your need
label.adjustsFontSizeToFitWidth = true
label.lineBreakMode = .byClipping
label.numberOfLines = 0
Here's how I did it, since DGund's answer didn't work for me, it fit the width, but I wanted it to fit the height.
+ (UIFont *)findAdaptiveFontWithName:(NSString *)fontName forUILabelSize:(CGSize)labelSize withMinimumSize:(NSInteger)minSize
{
UIFont *tempFont = nil;
NSString *testString = #"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
NSInteger tempMin = minSize;
NSInteger tempMax = 256;
NSInteger mid = 0;
NSInteger difference = 0;
while (tempMin <= tempMax) {
mid = tempMin + (tempMax - tempMin) / 2;
tempFont = [UIFont fontWithName:fontName size:mid];
difference = labelSize.height - [testString sizeWithFont:tempFont].height;
if (mid == tempMin || mid == tempMax) {
if (difference < 0) {
return [UIFont fontWithName:fontName size:(mid - 1)];
}
return [UIFont fontWithName:fontName size:mid];
}
if (difference < 0) {
tempMax = mid - 1;
} else if (difference > 0) {
tempMin = mid + 1;
} else {
return [UIFont fontWithName:fontName size:mid];
}
}
return [UIFont fontWithName:fontName size:mid];
}
This will take a font name, a size (it doesn't have to be a UILabel, theoretically, but I always used it with a UILabel), and a minimum size (you could also use a max size, just replace the 256 with the max size parameter). This will essentially test every font size between the minimum and maximum font sizes and return the one that is at or just underneath the target height.
Usage is self explanatory, but looks like this:
self.myLabel.font = [self findAdaptiveFontWithName:#"HelveticaNeue-UltraLight" forUILabelSize:self.myLabel.frame.size withMinimumSize:30];
You can also make this a class method category on UIFont (which is what I did).
EDIT: On suggestion, I removed the for loop and spent a little time making it more efficient with a Binary Search routine. I did several checks to make absolutely sure that the font will end up fitting within the label. In initial testing it appears to work.
Edit: Check out Joel Fischer's great answer to programmatically obtain the correct size!
You can set the font to automatically fill the size of a label, and optionally not go below a minimum font size. Just set adjustsFontSizeToFitWidth to YES. Check out the UILabel Class Reference if you need more information.
Although the boolean is called "adjustsFontSizeToFitWidth," it really means the largest size for the height of the label, that will stay on one line of the label (or however many lines you specify).
to adapt the text according to the height of my label I have adapt Joel method to swift
func optimisedfindAdaptiveFontWithName(fontName:String, label:UILabel!, minSize:CGFloat,maxSize:CGFloat) -> UIFont!
{
var tempFont:UIFont
var tempHeight:CGFloat
var tempMax:CGFloat = maxSize
var tempMin:CGFloat = minSize
while (ceil(tempMin) != ceil(tempMax)){
let testedSize = (tempMax + tempMin) / 2
tempFont = UIFont(name:fontName, size:testedSize)
let attributedString = NSAttributedString(string: label.text!, attributes: [NSFontAttributeName : tempFont])
let textFrame = attributedString.boundingRectWithSize(CGSize(width: label.bounds.size.width, height: CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin , context: nil)
let difference = label.frame.height - textFrame.height
println("\(tempMin)-\(tempMax) - tested : \(testedSize) --> difference : \(difference)")
if(difference > 0){
tempMin = testedSize
}else{
tempMax = testedSize
}
}
//returning the size -1 (to have enought space right and left)
return UIFont(name: fontName, size: tempMin - 1)
}
and I use it this way :
myLabel.font = optimisedfindAdaptiveFontWithName("Helvetica", label: myLabel, minSize: 10, maxSize: 38)
println("\(myLabel.font)")
Good news,
Performing a binary search is completely unnecessary!
You need only iterate (a couple of times) using a ratio search.
guess = guess * ( desiredHeight / guessHeight )
Here's a full total IBDesignable solution.
Note: when working with designers or typographers, you will need to set the tracking / stretching for fonts. (It's absurd Apple do not include this.) StyledLabel also includes tracking / stretching.
StyledLabel.swift
It sets tracking, stretching, AND it sets the point size to match the view frame height on all devices.
In storyboard: just make the frame of the UILabel, the height you want the text to be - end of story!
// the call fontToFitHeight FINDS THE POINT SIZE TO "FILL TO HEIGHT".
// Just use autolayout to make the frame THE ACTUAL HEIGHT
// you want the type ON ANY DEVICE
// ADDITIONALLY you can set:
// the tracking (that's the overall amount of space between all letters)
// and streching (actually squeeze or stretch the letters horizontally)
// Note: tracking and stretching IS SHOWN IN STORYBOARD LIVE
// WTT crazyrems http://stackoverflow.com/a/37300130/294884
import UIKit
#IBDesignable
class StyledLabel: UILabel
{
#IBInspectable var tracking:CGFloat = 0.8
// values between about 0.7 to 1.3. one means normal.
#IBInspectable var stretching:CGFloat = -0.1
// values between about -.5 to .5. zero means normal.
override func awakeFromNib()
{
tweak()
}
override func prepareForInterfaceBuilder()
{
tweak()
}
override func layoutSubviews()
{
super.layoutSubviews()
font = fontToFitHeight()
}
private func fontToFitHeight() -> UIFont
{
/* Apple have failed to include a basic thing needed in handling text: fitting the text to the height. Here's the simplest and fastest way to do that:
guess = guess * ( desiredHeight / guessHeight )
That's really all there is to it. The rest of the code in this routine is safeguards. Further, the routine iterates a couple of times, which is harmless, to take care of any theoretical bizarre nonlinear sizing issues with strange typefaces. */
guard text?.characters.count > 0 else { return font }
let desiredHeight:CGFloat = frame.size.height
guard desiredHeight>1 else { return font }
var guess:CGFloat
var guessHeight:CGFloat
print("searching for... ", desiredHeight)
guess = font.pointSize
if (guess>1&&guess<1000) { guess = 50 }
guessHeight = sizeIf(guess)
if (guessHeight==desiredHeight)
{
print("fluke, exact match within float math limits, up front")
return font.fontWithSize(guess)
}
var iterations:Int = 4
/* It is incredibly unlikely you would need more than four iterations, "two" would rarely be needed. You could imagine some very strange glyph handling where the relationship is non-linear (or something weird): That is the only theoretical reason you'd ever need more than one or two iterations. Note that when you watch the output of the iterations, you'll sometimes/often see same or identical values for the result: this is correct and expected in a float iteration. */
while(iterations>0)
{
guess = guess * ( desiredHeight / guessHeight )
guessHeight = sizeIf(guess)
if (guessHeight==desiredHeight)
{
print("unbelievable fluke, exact match within float math limits while iterating")
return font.fontWithSize(guess)
}
iterations -= 1
}
print("done. Shame Apple doesn't do this for us!")
return font.fontWithSize(guess)
}
private func sizeIf(pointSizeToTry:CGFloat)->(CGFloat)
{
let s:CGFloat = text!.sizeWithAttributes(
[NSFontAttributeName: font.fontWithSize(pointSizeToTry)] )
.height
print("guessing .. ", pointSizeToTry, " .. " , s)
return s
}
private func tweak()
{
let ats = NSMutableAttributedString(string: self.text!)
let rg = NSRange(location: 0, length: self.text!.characters.count)
ats.addAttribute(
NSKernAttributeName, value:CGFloat(tracking), range:rg )
ats.addAttribute(
NSExpansionAttributeName, value:CGFloat(stretching), range:rg )
self.attributedText = ats
}
}
One line called in viewWillAppear does the trick:
testLabel.font = testLabel.font.fontWithSize(testLabel.frame.height * 2/3)
In storyboard, I set all of my label heights relative to the overall height of the view, and this allows the font size to scale dynamically with them.
Notice that the font size is actually 2/3 the height of the label. If the font you are using has tails that dip below the line (as in y, g, q, p, or j), you will want to make the font size a ratio of the label height so that those tails aren't chopped off. 2/3 works well for Helvetica Neue, but try other ratios depending on the font you're using. For fonts without tails, numbers, or all-caps text, a 1:1 ratio may suffice.
Based on #Conaaando's great answer, I've updated it to a version with IBDesignable parameters included, which makes it possible to edit it throughout the Interface builder:
And the code:
//
// TIFFitToHeightLabel.swift
//
import Foundation
import UIKit
#IBDesignable class TIFFitToHeightLabel: UILabel {
#IBInspectable var minFontSize:CGFloat = 12 {
didSet {
font = fontToFitHeight()
}
}
#IBInspectable var maxFontSize:CGFloat = 30 {
didSet {
font = fontToFitHeight()
}
}
override func layoutSubviews() {
super.layoutSubviews()
font = fontToFitHeight()
}
// Returns an UIFont that fits the new label's height.
private func fontToFitHeight() -> UIFont {
var minFontSize: CGFloat = self.minFontSize
var maxFontSize: CGFloat = self.maxFontSize
var fontSizeAverage: CGFloat = 0
var textAndLabelHeightDiff: CGFloat = 0
while (minFontSize <= maxFontSize) {
fontSizeAverage = minFontSize + (maxFontSize - minFontSize) / 2
if let labelText: NSString = text {
let labelHeight = frame.size.height
let testStringHeight = labelText.sizeWithAttributes(
[NSFontAttributeName: font.fontWithSize(fontSizeAverage)]
).height
textAndLabelHeightDiff = labelHeight - testStringHeight
if (fontSizeAverage == minFontSize || fontSizeAverage == maxFontSize) {
if (textAndLabelHeightDiff < 0) {
return font.fontWithSize(fontSizeAverage - 1)
}
return font.fontWithSize(fontSizeAverage)
}
if (textAndLabelHeightDiff < 0) {
maxFontSize = fontSizeAverage - 1
} else if (textAndLabelHeightDiff > 0) {
minFontSize = fontSizeAverage + 1
} else {
return font.fontWithSize(fontSizeAverage)
}
}
}
return font.fontWithSize(fontSizeAverage)
}
}
This borrows heavily from Joel Fischer's answer. His answer takes into account label height only -- I've made some changes to take into account label width as well (given an input string), which I wanted:
typedef enum
{
kDimensionHeight,
kDimensionWidth,
} DimensionType;
#implementation UIFont (AdaptiveFont)
+ (UIFont *)_adaptiveFontWithName:(NSString *)fontName minSize:(NSInteger)minSize labelDimension:(CGFloat)labelDimension testString:(NSString *)testString dimension:(DimensionType)dimension
{
UIFont *tempFont = nil;
NSInteger tempMin = minSize;
NSInteger tempMax = 256;
NSInteger mid = 0;
NSInteger difference = 0;
CGFloat testStringDimension = 0.0;
while (tempMin <= tempMax) {
#autoreleasepool {
mid = tempMin + (tempMax - tempMin) / 2;
tempFont = [UIFont fontWithName:fontName size:mid];
// determine dimension to test
if (dimension == kDimensionHeight) {
testStringDimension = [testString sizeWithFont:tempFont].height;
} else {
testStringDimension = [testString sizeWithFont:tempFont].width;
}
difference = labelDimension - testStringDimension;
if (mid == tempMin || mid == tempMax) {
if (difference < 0) {
return [UIFont fontWithName:fontName size:(mid - 1)];
}
return [UIFont fontWithName:fontName size:mid];
}
if (difference < 0) {
tempMax = mid - 1;
} else if (difference > 0) {
tempMin = mid + 1;
} else {
return [UIFont fontWithName:fontName size:mid];
}
}
}
return [UIFont fontWithName:fontName size:mid];
}
+ (UIFont *)adaptiveFontWithName:(NSString *)fontName minSize:(NSInteger)minSize labelSize:(CGSize)labelSize string:(NSString *)string
{
UIFont *adaptiveFont = nil;
NSString *testString = nil;
// get font, given a max height
testString = #"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
UIFont *fontConstrainingHeight = [UIFont _adaptiveFontWithName:fontName minSize:minSize labelDimension:labelSize.height testString:testString dimension:kDimensionHeight];
CGSize boundsConstrainingHeight = [string sizeWithFont:fontConstrainingHeight];
CGSize boundsConstrainingWidth = CGSizeZero;
// if WIDTH is fine (while constraining HEIGHT), return that font
if (boundsConstrainingHeight.width <= labelSize.width) {
adaptiveFont = fontConstrainingHeight;
} else {
// get font, given a max width
// i.e., fontConstrainingWidth
testString = string;
adaptiveFont = [UIFont _adaptiveFontWithName:fontName minSize:minSize labelDimension:labelSize.width testString:testString dimension:kDimensionWidth];
// TEST comparison
boundsConstrainingWidth = [string sizeWithFont:adaptiveFont];
}
return adaptiveFont;
}
Combining answers by #DGund and #Kashif, here's a simple IB solution:
This fits text by height as low as you specify in Autoshrink parameter.
There is a much simpler way to do it. Just calculate point per pixel of the screen and multiply it to the height of your label, and you'll get the desiered font size.
Here are custom methods for this. Choose whatever you want.
TYPE 1. Hardoded single-line version:
- (CGFloat) fontSizeFromHeight:(CGFloat)height
{
return ceilf(height * (10.0 / [#"Tg" sizeWithAttributes:#{NSFontAttributeName:[UIFont systemFontOfSize:10.0]}].height));
}
TYPE 2. Cleaner version:
- (CGFloat)fontSizeFromHeight:(CGFloat)height
{
static CGFloat const testFontSize = 12.0;
static NSString * const testText = #"TestString";
UIFont *testFont = [UIFont systemFontOfSize:testFontSize];
CGFloat pixelHeight = [testText sizeWithAttributes:#{NSFontAttributeName:testFont}].height;
CGFloat pointPerPixel = testFontSize / pixelHeight;
CGFloat desiredFontSize = ceilf(height * pointPerPixel);
return desiredFontSize;
}
Usage examples:
myLabel.font = [UIFont systemFontOfSize:[self fontSizeFromHeight:myLabel.frame.size.height]];
myLabel.font = [myLabel.font fontWithSize:[self fontSizeFromHeight:myLabel.frame.size.height]];
Expanding on #Joe Blow's answer, here is an Objective-C category UILabel+FitToHeight which allows you to easily import and toggle a adjustsFontSizeToFitHeight much like you can already adjustsFontSizeToFitWidth.
UILabel+FitToHeight.h
#import <UIKit/UIKit.h>
#interface UILabel (FitToHeight)
#property (nonatomic, assign) BOOL adjustsFontSizeToFitHeight;
#end
UILabel+FitToHeight.m
#import "UILabel+FitToHeight.h"
#import <objc/runtime.h>
#implementation UILabel (FitToHeight)
-(BOOL)adjustsFontSizeToFitHeight {
NSNumber *number = objc_getAssociatedObject(self, #selector(adjustsFontSizeToFitHeight));
return [number boolValue];
}
-(void)setAdjustsFontSizeToFitHeight:(BOOL)adjustsFontSizeToFitHeight {
NSNumber *number = [NSNumber numberWithBool:adjustsFontSizeToFitHeight];
objc_setAssociatedObject(self, #selector(adjustsFontSizeToFitHeight), number, OBJC_ASSOCIATION_ASSIGN);
}
-(UIFont *)fontToFitHeight {
float desiredHeight = [self frame].size.height;
float guess;
float guessHeight;
guess = [[self font] pointSize];
guessHeight = [self sizeIf:guess];
if(guessHeight == desiredHeight) {
return [[self font] fontWithSize:guess];
}
int attempts = 4;
while(attempts > 0) {
guess = guess * (desiredHeight / guessHeight);
guessHeight = [self sizeIf:guess];
if(guessHeight == desiredHeight) {
return [[self font] fontWithSize:guess];
}
attempts--;
}
return [[self font] fontWithSize:guess];
}
-(float)sizeIf:(float)sizeToTry {
CGSize size = [[self text] sizeWithAttributes:#{ NSFontAttributeName : [[self font] fontWithSize:sizeToTry] }];
return size.height;
}
-(void)layoutSubviews {
[super layoutSubviews];
if([self adjustsFontSizeToFitHeight]) {
[self setFont:[self fontToFitHeight]];
}
}
Import as you would any other category...
#import "UILabel+FitToHeight.h"
and use as follows...
UILabel *titleLabel = [[UILabel alloc] init];
[titleLabel setAdjustsFontSizeToFitHeight:YES];
[titleLabel setAdjustsFontSizeToFitWidth:YES];
It's worth noting that this still works with [titleLabel setAdjustsFontSizeToFitWidth:YES]; so the using the two in conjunction is entirely possible.
SWIFT variation:
I managed to do it with an extension. Works fine, min font size is 5.
I subtract 10 from the height, so I leave a "margin" also, but you can delete it or modify it.
extension UILabel {
//Finds and sets a font size that matches the height of the frame.
//Use in case the font size is epic huge and you need to resize it.
func resizeToFitHeight(){
var currentfontSize = font.pointSize
let minFontsize = CGFloat(5)
let constrainedSize = CGSizeMake(frame.width, CGFloat.max)
while (currentfontSize >= minFontsize){
let newFont = font.fontWithSize(currentfontSize)
let attributedText: NSAttributedString = NSAttributedString(string: text!, attributes: [NSFontAttributeName: newFont])
let rect: CGRect = attributedText.boundingRectWithSize(constrainedSize, options: .UsesLineFragmentOrigin, context: nil)
let size: CGSize = rect.size
if (size.height < frame.height - 10) {
font = newFont
break;
}
currentfontSize--
}
//In case the text is too long, we still show something... ;)
if (currentfontSize == minFontsize){
font = font.fontWithSize(currentfontSize)
}
}
}
Building off of Joel Fisher's epic answer but written as a Swift 4 extension:
extension String {
/// Attempts to return the font specified by name of the appropriate point
/// size for this string to fit within a particular container size and
/// constrained to a lower and upper bound point size.
/// - parameter name: of the font.
/// - parameter containerSize: that this string should fit inside.
/// - parameter lowerBound: minimum allowable point size of this font.
/// - parameter upperBound: maximum allowable point size of this font.
/// - returns: the font specified by name of the appropriate point
/// size for this string to fit within a particular container size and
/// constrained to a lower and upper bound point size; `nil` if no such
/// font exists.
public func font(named name: String,
toFit containerSize: CGSize,
noSmallerThan lowerBound: CGFloat = 1.0,
noLargerThan upperBound: CGFloat = 256.0) -> UIFont? {
let lowerBound = lowerBound > upperBound ? upperBound : lowerBound
let mid = lowerBound + (upperBound - lowerBound) / 2
guard let tempFont = UIFont(name: name, size: mid) else { return nil }
let difference = containerSize.height -
self.size(withAttributes:
[NSAttributedStringKey.font : tempFont]).height
if mid == lowerBound || mid == upperBound {
return UIFont(name: name, size: difference < 0 ? mid - 1 : mid)
}
return difference < 0 ? font(named: name,
toFit: containerSize,
noSmallerThan: mid,
noLargerThan: mid - 1) :
(difference > 0 ? font(named: name,
toFit: containerSize,
noSmallerThan: mid,
noLargerThan: mid - 1) :
UIFont(name: name, size: mid))
}
/// Returns the system font of the appropriate point size for this string
/// to fit within a particular container size and constrained to a lower
/// and upper bound point size.
/// - parameter containerSize: that this string should fit inside.
/// - parameter lowerBound: minimum allowable point size of this font.
/// - parameter upperBound: maximum allowable point size of this font.
/// - returns: the system font of the appropriate point size for this string
/// to fit within a particular container size and constrained to a lower
/// and upper bound point size.
public func systemFont(toFit containerSize: CGSize,
noSmallerThan lowerBound: CGFloat = 1.0,
noLargerThan upperBound: CGFloat = 256.0) -> UIFont {
let lowerBound = lowerBound > upperBound ? upperBound : lowerBound
let mid = lowerBound + (upperBound - lowerBound) / 2
let tempFont = UIFont.systemFont(ofSize: mid)
let difference = containerSize.height -
self.size(withAttributes:
[NSAttributedStringKey.font : tempFont]).height
if mid == lowerBound || mid == upperBound {
return UIFont.systemFont(ofSize: difference < 0 ? mid - 1 : mid)
}
return difference < 0 ? systemFont(toFit: containerSize,
noSmallerThan: mid,
noLargerThan: mid - 1) :
(difference > 0 ? systemFont(toFit: containerSize,
noSmallerThan: mid,
noLargerThan: mid - 1) :
UIFont.systemFont(ofSize: mid))
}
}
Usage:
let font = "Test string".font(named: "Courier New",
toFit: CGSize(width: 150.0, height: 30.0),
noSmallerThan: 12.0,
noLargerThan: 20.0)
let sysfont = "Test string".systemFont(toFit: CGSize(width: 150.0, height: 30.0),
noSmallerThan: 12.0,
noLargerThan: 20.0)
For UILabels that resize proportionally for larger/smaller devices:
Most effective solution for me has been to set the font's point-size to some ratio of the label's height +/- an adjustment factor. Assuming use of auto-layout constraints, position it's y vertical-center aligned to the bottom of the superview, multiplied by a ratio. Similarly in IB, constrain label's width to a proportion of screen's width.
Optionally, you may lock in the label's height/width ratio with an aspect constraint, however this may cause clipping if you don't get the font's point-size calculation right. The only reason to lock aspect ratio is if other controls/views' positions are relative to this label. However I highly recommend placing such controls/views relative to the superview's height/width so that they are not dependent on this label.
I understand this isn't exactly an encapsulated solution, but it has consistently caused me the least amount of grief. The only other solution that came close made use of while loops, however in my case I couldn't deal with the delays they imposed for upon every layout/refresh system call.
My apologies, if I have missed something here in all the text.
I followed #Crazyrems suggestions for autoshrinking the label's font. This does scale the font based on width as others have observed.
Then I just set 'Lines' to 0 in the UILabel's font section of Xcode. In code, that should be numberOfLines. That's all.
Credit goes to #Mikrasya, who hinted on this solution in one of the comments above.
Tested on Xcode 7.3 and iOS 9.3.2.
Forgive me if I am wrong but everything mentioned here is unnecessary. Set your font again just after the change with a new fontSize of yourLabel.height
You can also check for a conditional comparison between these values (yourLabel.height and fontSize) to prevent unnecessary updates.
All you need to do is:
[yourLabel setFont:[UIFont fontWithName:#"*your fontname*" size:yourLabel.frame.size.height]];
I made a macro to do this for you
///Scales FontSize up (or down) until the text fits within the height of the label, will not auto-update, must be called any time text is updated. Label Frame must be set prior to calling
#define scaleFontSizeToFillHeight(__label) {\
__label.font = [UIFont fontWithName:__label.font.fontName size:__label.frame.size.height*2.0f];\
UIFont *__currentFont = __label.font;\
CGFloat __originalFontSize = __currentFont.pointSize;\
CGSize __currentSize = [__label.text sizeWithAttributes:#{NSFontAttributeName : __currentFont}];\
while (__currentSize.height > __label.frame.size.height && __currentFont.pointSize > (__originalFontSize * __label.minimumScaleFactor)) {\
__currentFont = [__currentFont fontWithSize:__currentFont.pointSize - 1];\
__currentSize = [__label.text sizeWithAttributes:#{NSFontAttributeName : __currentFont}];\
}\
__label.font = __currentFont;\
}
The accepted answer has a bug in it. The variable distance must be a float, or it can return a font size that is too big. Also, the use of "- (CGSize)sizeWithFont:(UIFont *)font;" is deprecated. Here's the code with these 2 issues fixed.
+ (UIFont *)findAdaptiveFontWithName:(NSString *)fontName forUILabelSize:(float)maxHeight withMaxFontSize:(int)maxFontSize
{
UIFont *tempFont = nil;
NSString *testString = #"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
NSInteger tempMin = 0;
NSInteger tempMax = maxFontSize;
NSInteger mid = 0;
float difference = 0;
while (tempMin <= tempMax) {
mid = tempMin + (tempMax - tempMin) / 2;
tempFont = [UIFont fontWithName:fontName size:mid];
UILabel* dummyLabel = [[UILabel alloc] initWithFrame:CGRectZero];
dummyLabel.text = testString;
dummyLabel.font = tempFont;
[dummyLabel sizeToFit];
difference = maxHeight - dummyLabel.bounds.size.height;
if (mid == tempMin || mid == tempMax) {
if (difference < 0) {
return [UIFont fontWithName:fontName size:(mid - 1)];
}
return [UIFont fontWithName:fontName size:mid];
}
if (difference < 0) {
tempMax = mid - 1;
} else if (difference > 0) {
tempMin = mid + 1;
} else {
return [UIFont fontWithName:fontName size:mid];
}
}
return [UIFont fontWithName:fontName size:mid];
}
This seemed to work for me, I've subclassed UILabel and in the layoutSubviews i've checked for the actual height and adjusted the font size accordingly.
import UIKit
class HeightAdjustableLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
if frame.height < font.pointSize + 2 {
font = font.withSize(frame.height - 2)
}
}
}
Yeah, go to interface builder, (your .xib file) and go to the third tab from the right in the attributes inspector and you may set the size of the font there!