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!
Related
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
Real odd one to get stuck on but weirdly I am.
You you have a imageView containing a image. You size that imageView down and then tell it to use UIViewContentModeScaleAspectFit. so your imageView might be 300 by 200 but your scaled image within could be 300 by 118 or 228 by 200 because its aspectfit.
How on earth do you get the size of the actual image?
imageView.image.size is the size of the original image.
imageview.frame is the frame of the imageview not the contained image.
imageview.contentstretch does not work either
I have written a quick category on UIImageView to achieve that:
(.h)
#interface UIImageView (additions)
- (CGSize)imageScale;
#end
(.m)
#implementation UIImageView (additions)
- (CGSize)imageScale {
CGFloat sx = self.frame.size.width / self.image.size.width;
CGFloat sy = self.frame.size.height / self.image.size.height;
CGFloat s = 1.0;
switch (self.contentMode) {
case UIViewContentModeScaleAspectFit:
s = fminf(sx, sy);
return CGSizeMake(s, s);
break;
case UIViewContentModeScaleAspectFill:
s = fmaxf(sx, sy);
return CGSizeMake(s, s);
break;
case UIViewContentModeScaleToFill:
return CGSizeMake(sx, sy);
default:
return CGSizeMake(s, s);
}
}
#end
Multiply the original image size by the given scale, and you'll get your actual displayed image size.
well .. you have to do some math. you have the size of the original image and you have the size of the image view.. so you can calculate whether it will be resized by the height or the width.
if you image view is 300x200 and the image is 1024x768 you can calculate which is the limiting factor
300/1024 = 0.29296875
200/768 = 0.260416667
so the height is the limiting factor ... the image will be:
267x200
The Swift version of the accepted answer:
extension UIImageView {
var imageScale: CGSize {
let sx = Double(self.frame.size.width / self.image!.size.width)
let sy = Double(self.frame.size.height / self.image!.size.height)
var s = 1.0
switch (self.contentMode) {
case .scaleAspectFit:
s = fmin(sx, sy)
return CGSize (width: s, height: s)
case .scaleAspectFill:
s = fmax(sx, sy)
return CGSize(width:s, height:s)
case .scaleToFill:
return CGSize(width:sx, height:sy)
default:
return CGSize(width:s, height:s)
}
}
}
Just adding a little security on Individual11 code.
Calling imageScale on an imageView without an actual image would crash.
extension UIImageView {
var imageScale: CGSize {
if let image = self.image {
let sx = Double(self.frame.size.width / image.size.width)
let sy = Double(self.frame.size.height / image.size.height)
var s = 1.0
switch (self.contentMode) {
case .scaleAspectFit:
s = fmin(sx, sy)
return CGSize (width: s, height: s)
case .scaleAspectFill:
s = fmax(sx, sy)
return CGSize(width:s, height:s)
case .scaleToFill:
return CGSize(width:sx, height:sy)
default:
return CGSize(width:s, height:s)
}
}
return CGSize.zero
}
}
how to find what text is visible in a scrollable, non-ediable UITextView?
for example i may need to show next paragraph, then i want to find the current visible text range and use it to calculate the appropriate range and use scrollRangeToVisible: to scroll the text view
I find another solution here.
It's a better way to solve this problem in my eyes.
https://stackoverflow.com/a/9283311/889892
Since UITextView is a subclass of UIScrollView, its bounds property reflects the visible part of its coordinate system. So something like this should work:
-(NSRange)visibleRangeOfTextView:(UITextView *)textView {
CGRect bounds = textView.bounds;
UITextPosition *start = [textView characterRangeAtPoint:bounds.origin].start;
UITextPosition *end = [textView characterRangeAtPoint:CGPointMake(CGRectGetMaxX(bounds), CGRectGetMaxY(bounds))].end;
return NSMakeRange([textView offsetFromPosition:textView.beginningOfDocument toPosition:start],
[textView offsetFromPosition:start toPosition:end]);
}
This assumes a top-to-bottom, left-to-right text layout. If you want to make it work for other layout directions, you will have to work harder. :)
The way i would do it is to compute all the sizes of each paragraph. With sizeWithFont:constrainedToSize:lineBreakMode:
you will then be able to work out which paragraph is visible, from the [textView contentOffset].
to scroll, dont use scrollRangeToVisible, just use setContentOffset: The CGPoint y parameter for this should either be the sum of all the height sizes to the next paragraph, or just add the textView.frame.size.height, if that is closer than the beginning of the next paragraph.
This make sense?
in answer to comment requst code bellow (untested):
CGFloat paragraphOffset[MAX_PARAGRAPHS];
CGSize constraint = CGSizeMake(widthOfTextView, 999999 /*arbitrarily large number*/);
NSInteger paragraphNo = 0;
CGFloat offset = 0;
for (NSString* paragraph in paragraphs) {
paragraphOffset[paragraphNo++] = offset;
CGSize paragraphSize = [paragraph sizeWithFont:textView.font constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
offset += paragraphSize.height;
}
// find visible paragraph
NSInteger visibleParagraph = 0;
while (paragraphOffset[visibleParagraph++] < textView.contentOffset.y);
// scroll to paragraph 6
[textView setContentOffset:CGPointMake(0, paragraphOffset[6]) animated:YES];
If you want a Swift solution I use this:
Swift 2
public extension UITextView {
public var visibleRange: NSRange? {
if let start = closestPositionToPoint(contentOffset) {
if let end = characterRangeAtPoint(CGPointMake(contentOffset.x + CGRectGetMaxX(bounds), contentOffset.y + CGRectGetMaxY(bounds)))?.end {
return NSMakeRange(offsetFromPosition(beginningOfDocument, toPosition: start), offsetFromPosition(start, toPosition: end))
}
}
return nil
}
}
Swift 3
public extension UITextView {
public var visibleRange: NSRange? {
guard let start = closestPosition(to: contentOffset),
end = characterRange(at: CGPoint(x: contentOffset.x + bounds.maxX,
y: contentOffset.y + bounds.maxY))?.end
else { return nil }
return NSMakeRange(offset(from: beginningOfDocument, to: start), offset(from: start, to: end))
}
}
Swift 3.0/3.1 solution based on "Noodle of Death" answer.
public extension UITextView {
public var visibleRange: NSRange? {
if let start = closestPosition(to: contentOffset) {
if let end = characterRange(at: CGPoint(x: contentOffset.x + bounds.maxX, y: contentOffset.y + bounds.maxY))?.end {
return NSMakeRange(offset(from: beginningOfDocument, to: start), offset(from: start, to: end))
}
}
return nil
}
}
One option you have is to use a UIWebView instead of a UITextView. You can then use anchors and javascript to scroll to the appropriate places in the text. You can probably insert the anchors programmatically at the start of each paragraph to make this easier.
In the code below, CTFramesetterSuggestFrameSizeWithConstraints sometimes returns a CGSize with a height that is not big enough to contain all the text that is being passed into it. I did look at this answer. But in my case the width of the text box needs to be constant. Is there any other/better way to figure out the correct height for my attributed string? Thanks!
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);
CGSize tmpSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), NULL, CGSizeMake(self.view.bounds.size.width, CGFLOAT_MAX), NULL);
CGSize textBoxSize = CGSizeMake((int)tmpSize.width + 1, (int)tmpSize.height + 1);
CTFramesetterSuggestFrameSizeWithConstraints works correctly. The reason that you get a height that is too short is because of the leading in the default paragraph style attached to attributed strings. If you don't attach a paragraph style to the string then CoreText returns the height needed to render the text, but with no space between the lines. This took me forever to figure out. Nothing in the documentation spells it out. I just happened to notice that my heights were short by an amount equal to (number of lines x expected leading). To get the height result you expect you can use code like the following:
NSString *text = #"This\nis\nsome\nmulti-line\nsample\ntext."
UIFont *uiFont = [UIFont fontWithName:#"Helvetica" size:17.0];
CTFontRef ctFont = CTFontCreateWithName((CFStringRef) uiFont.fontName, uiFont.pointSize, NULL);
// When you create an attributed string the default paragraph style has a leading
// of 0.0. Create a paragraph style that will set the line adjustment equal to
// the leading value of the font.
CGFloat leading = uiFont.lineHeight - uiFont.ascender + uiFont.descender;
CTParagraphStyleSetting paragraphSettings[1] = { kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof (CGFloat), &leading };
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphSettings, 1);
CFRange textRange = CFRangeMake(0, text.length);
// Create an empty mutable string big enough to hold our test
CFMutableAttributedStringRef string = CFAttributedStringCreateMutable(kCFAllocatorDefault, text.length);
// Inject our text into it
CFAttributedStringReplaceString(string, CFRangeMake(0, 0), (CFStringRef) text);
// Apply our font and line spacing attributes over the span
CFAttributedStringSetAttribute(string, textRange, kCTFontAttributeName, ctFont);
CFAttributedStringSetAttribute(string, textRange, kCTParagraphStyleAttributeName, paragraphStyle);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(string);
CFRange fitRange;
CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, textRange, NULL, bounds, &fitRange);
CFRelease(framesetter);
CFRelease(string);
CTFramesetterSuggestFrameSizeWithConstraints() is broken. I filed a bug on this a while back. Your alternative is to use CTFramesetterCreateFrame() with a path that is sufficiently high. Then you can measure the rect of the CTFrame that you get back. Note that you cannot use CGFLOAT_MAX for the height, as CoreText uses a flipped coordinate system from the iPhone and will locate its text at the "top" of the box. This means that if you use CGFLOAT_MAX, you won't have enough precision to actually tell the height of the box. I recommend using something like 10,000 as your height, as that's 10x taller than the screen itself and yet gives enough precision for the resulting rectangle. If you need to lay out even taller text, you can do this multiple times for each section of text (you can ask CTFrameRef for the range in the original string that it was able to lay out).
Thanks to Chris DeSalvo for the excellent answer! Finally ending 16 hours of debugging. I had a little trouble figuring out the Swift 3 syntax. So sharing the Swift 3 version of setting the paragraph style.
let leading = uiFont.lineHeight - uiFont.ascender + uiFont.descender
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = leading
mutableAttributedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: textRange)
Working on this problem and following a lot of different answers from several posters, I had implemented a solution for the all mighty problem of correct text size, for me CTFramesetterSuggestFrameSizeWithConstraints is not working properly so we need to use CTFramesetterCreateFrame and then measure the size for that frame, (this is a UIFont extension) this is swift 100%
References
CTFramesetterSuggestFrameSizeWithConstraints sometimes returns incorrect size?
How to get the real height of text drawn on a CTFrame
Using CFArrayGetValueAtIndex in Swift with UnsafePointer (AUPreset)
func sizeOfString (string: String, constrainedToWidth width: Double) -> CGSize {
let attributes = [NSAttributedString.Key.font:self]
let attString = NSAttributedString(string: string,attributes: attributes)
let framesetter = CTFramesetterCreateWithAttributedString(attString)
let frame = CTFramesetterCreateFrame(framesetter,CFRange(location: 0,length: 0),CGPath.init(rect: CGRect(x: 0, y: 0, width: width, height: 10000), transform: nil),nil)
return UIFont.measure(frame: frame)
}
Then we measure our CTFrame
static func measure(frame:CTFrame) ->CGSize {
let lines = CTFrameGetLines(frame)
let numOflines = CFArrayGetCount(lines)
var maxWidth : Double = 0
for index in 0..<numOflines {
let line : CTLine = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), to: CTLine.self)
var ascent : CGFloat = 0
var descent : CGFloat = 0
var leading : CGFloat = 0
let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
if(width > maxWidth) {
maxWidth = width
}
}
var ascent : CGFloat = 0
var descent : CGFloat = 0
var leading : CGFloat = 0
CTLineGetTypographicBounds(unsafeBitCast(CFArrayGetValueAtIndex(lines, 0), to: CTLine.self), &ascent, &descent, &leading)
let firstLineHeight = ascent + descent + leading
CTLineGetTypographicBounds(unsafeBitCast(CFArrayGetValueAtIndex(lines, numOflines - 1), to: CTLine.self), &ascent, &descent, &leading)
let lastLineHeight = ascent + descent + leading
var firstLineOrigin : CGPoint = CGPoint(x: 0, y: 0)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 1), &firstLineOrigin);
var lastLineOrigin : CGPoint = CGPoint(x: 0, y: 0)
CTFrameGetLineOrigins(frame, CFRangeMake(numOflines - 1, 1), &lastLineOrigin);
let textHeight = abs(firstLineOrigin.y - lastLineOrigin.y) + firstLineHeight + lastLineHeight
return CGSize(width: maxWidth, height: Double(textHeight))
}
This was driving me batty and none of the solutions above worked for me to calculate layout of long strings over multiple pages and return range values with completely truncated-out lines. After reading the API notes, the parameter in CTFramesetterSuggestFrameSizeWithConstraints for 'stringRange' is thus:
'The string range to which the frame size will apply. The string range is a range over the string that was used to create the framesetter. If the length portion of the range is set to 0, then the framesetter will continue to add lines until it runs out of text or space.'
After setting the string range to 'CFRangeMake(currentLocation, 0)' instead of 'CFRangeMake(currentLocation, string.length)', it all works perfectly.
After weeks of trying everything, any combination possible, I made a break through and found something that works. This issue seems to be more prominent on macOS than on iOS, but still appears on both.
What worked for me was to use a CATextLayer instead of a NSTextField (on macOS) or a UILabel (on iOS).
And using boundingRect(with:options:context:) instead of CTFramesetterSuggestFrameSizeWithConstraints. Even though in theory the latter should be more lower level than the former, and I was assuming would be more precise, the game changer turns out to be NSString.DrawingOptions.usesDeviceMetrics.
The frame size suggested fits like a charm.
Example:
let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
with: .init(width: maxWidth,
height: .greatestFiniteMagnitude),
options: [
.usesFontLeading,
.usesLineFragmentOrigin,
.usesDeviceMetrics])
let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString
Then you can add the CATextLayer to any NSView/UIView.
macOS
let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)
iOS
let view = UIView()
view.layer.addSublayer(textLayer)
I finally find out.. When uses CTFramesetterSuggestFrameSizeWithConstraints returning CGSize to draw text, the size is considered not big enough(sometimes) for the whole text, so last line is not drew.. I just add 1 to size.height for return, it appears to be right now.
Something like this:
suggestedSize = CGSizeMake(suggestedSize.width, suggestedSize.height + 1);
I'm creating a custom UISlider to test out some interface ideas. Mostly based around making the thumb image larger.
I found out how to do that, like so:
UIImage *thumb = [UIImage imageNamed:#"newThumbImage_64px.png"];
[self.slider setThumbImage:thumb forState:UIControlStateNormal];
[self.slider setThumbImage:thumb forState:UIControlStateHighlighted];
[thumb release];
To calculate a related value I need to know where the center point of the thumb image falls when it's being manipulated. And the point should be in it's superview's coordinates.
Looking at the UISlider docs, I didn't see any property that tracked this.
Is there some easy way to calculate this or can it be derived from some existing value(s)?
This will return the correct X position of center of thumb image of UISlider in view coordinates:
- (float)xPositionFromSliderValue:(UISlider *)aSlider {
float sliderRange = aSlider.frame.size.width - aSlider.currentThumbImage.size.width;
float sliderOrigin = aSlider.frame.origin.x + (aSlider.currentThumbImage.size.width / 2.0);
float sliderValueToPixels = (((aSlider.value - aSlider.minimumValue)/(aSlider.maximumValue - aSlider.minimumValue)) * sliderRange) + sliderOrigin;
return sliderValueToPixels;
}
Put it in your view controller and use it like this: (assumes property named slider)
float x = [self xPositionFromSliderValue:self.slider];
I tried this after reading the above suggestion -
yourLabel = [[UILabel alloc]initWithFrame:....];
//Call this method on Slider value change event
-(void)sliderValueChanged{
CGRect trackRect = [self.slider trackRectForBounds:self.slider.bounds];
CGRect thumbRect = [self.slider thumbRectForBounds:self.slider.bounds
trackRect:trackRect
value:self.slider.value];
yourLabel.center = CGPointMake(thumbRect.origin.x + self.slider.frame.origin.x, self.slider.frame.origin.y - 20);
}
For Swift version
func sliderValueChanged() -> Void {
let trackRect = self.slider.trackRect(forBounds: self.slider.bounds)
let thumbRect = self.slider.thumbRect(forBounds: self.slider.bounds, trackRect: trackRect, value: self.slider.value)
yourLabel.center = CGPoint(x: thumbRect.origin.x + self.slider.frame.origin.x + 30, y: self.slider.frame.origin.y - 60)
}
I could get most accurate value by using this snippet.
Swift 3
extension UISlider {
var thumbCenterX: CGFloat {
let trackRect = self.trackRect(forBounds: frame)
let thumbRect = self.thumbRect(forBounds: bounds, trackRect: trackRect, value: value)
return thumbRect.midX
}
}
I would like to know why none of you provide the simplest answer which consist in reading the manual. You can compute all these values accurately and also MAKING SURE THEY STAY THAT WAY, by simply using the methods:
- (CGRect)trackRectForBounds:(CGRect)bounds
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value
which you can easily find in the developer documentation.
If thumb image changes and you want to change how it's positioned, you subclass and override these methods. The first one gives you the rectangle in which the thumb can move the second one the position of the thumb itself.
It's better to use -[UIView convertRect:fromView:] method instead. It's cleaner and easier without any complicated calculations:
- (IBAction)scrub:(UISlider *)sender
{
CGRect _thumbRect = [sender thumbRectForBounds:sender.bounds
trackRect:[sender trackRectForBounds:sender.bounds]
value:sender.value];
CGRect thumbRect = [self.view convertRect:_thumbRect fromView:sender];
// Use the rect to display a popover (pre iOS 8 code)
[self.popover dismissPopoverAnimated:NO];
self.popover = [[UIPopoverController alloc] initWithContentViewController:[UIViewController new]];
[self.popover presentPopoverFromRect:thumbRect inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionDown|UIPopoverArrowDirectionUp animated:YES];
}
I approached it by first mapping the UISlider's value interval in percents and then taking the same percent of the slider's size minus the percent of the thumb's size, a value to which I added half of the thumb's size to obtain its center.
- (float)mapValueInIntervalInPercents: (float)value min: (float)minimum max: (float)maximum
{
return (100 / (maximum - minimum)) * value -
(100 * minimum)/(maximum - minimum);
}
- (float)xPositionFromSliderValue:(UISlider *)aSlider
{
float percent = [self mapValueInIntervalInPercents: aSlider.value
min: aSlider.minimumValue
max: aSlider.maximumValue] / 100.0;
return percent * aSlider.frame.size.width -
percent * aSlider.currentThumbImage.size.width +
aSlider.currentThumbImage.size.width / 2;
}
Swift 3.0
Please refer if you like.
import UIKit
extension UISlider {
var trackBounds: CGRect {
return trackRect(forBounds: bounds)
}
var trackFrame: CGRect {
guard let superView = superview else { return CGRect.zero }
return self.convert(trackBounds, to: superView)
}
var thumbBounds: CGRect {
return thumbRect(forBounds: frame, trackRect: trackBounds, value: value)
}
var thumbFrame: CGRect {
return thumbRect(forBounds: bounds, trackRect: trackFrame, value: value)
}
}
AFter a little playing with IB and a 1px wide thumb image, the position of the thumb is exactly where you'd expect it:
UIImage *thumb = [UIImage imageNamed:#"newThumbImage_64px.png"];
CGRect sliderFrame = self.slider.frame;
CGFloat x = sliderFrame.origin.x + slideFrame.size.width * slider.value + thumb.size.width / 2;
CGFloat y = sliderFrame.origin.y + sliderFrame.size.height / 2;
return CGPointMake(x, y);
Here is a Swift 2.2 solution, I created an extension for it. I have only tried this with the default image.
import UIKit
extension UISlider {
var thumbImageCenterX: CGFloat {
let trackRect = trackRectForBounds(bounds)
let thumbRect = thumbRectForBounds(bounds, trackRect: trackRect, value: value)
return thumbRect.origin.x + thumbRect.width / 2 - frame.size.width / 2
}
}
Above solution is useful when UISlider is horizontal. In a recent project,we need to use UISlider with angle. So I need to get both x and y position. Using below to calculate the x,y axis:
- (CGPoint)xyPositionFromSliderValue:(UISlider *)aSlider WithAngle:(double)aangle{
//aangle means the dextrorotation angle compare to horizontal.
float xOrigin = 0.0;
float yOrigin = 0.0;
float xValueToaXis=0.0;
float yValueToaXis=0.0;
float sliderRange = slider_width-aSlider.currentThumbImage.size.width;
xOrigin = aSlider.frame.origin.x+slider_width*fabs(cos(aangle/180.0*M_PI));
yOrigin = aSlider.frame.origin.y;
xValueToaXis = xOrigin + ((((((aSlider.value-aSlider.minimumValue)/(aSlider.maximumValue-aSlider.minimumValue)) * sliderRange))+(aSlider.currentThumbImage.size.width / 2.0))*cos(aangle/180.0*M_PI)) ;
yValueToaXis = yOrigin + ((((((aSlider.value-aSlider.minimumValue)/(aSlider.maximumValue-aSlider.minimumValue)) * sliderRange))+(aSlider.currentThumbImage.size.width / 2.0))*sin(aangle/180.0*M_PI));
CGPoint xyPoint=CGPointMake(xValueToaXis, yValueToaXis);
return xyPoint;
}
Besides, can I Create a Ranger Slider based on UISlider? Thanks.
This will work for the UISlider being placed anywhere on the screen. Most of the other solutions will only work when the UISlider is aligned with the left edge of the screen. Note, I used frame rather than bounds for the thumbRect, to achieve that. And I show two variations, based on using frame or bounds for the trackRect
extension UISlider {
//this version will return the x coordinate in relation to the UISlider frame
var thumbCenterX: CGFloat {
return thumbRect(forBounds: frame, trackRect: trackRect(forBounds: bounds), value: value).midX
}
//this version will return the x coordinate in relation to the UISlider's containing view
var thumbCenterX: CGFloat {
return thumbRect(forBounds: frame, trackRect: trackRect(forBounds: frame), value: value).midX
}
}
step 1 :get View for detect position (use same extension top commet of# Ovi Bortas)
#IBOutlet weak var sliderView: UIView!
step 2 : set label frame for add sub view
func setLabelThumb(slider:UISlider,value:Float){
slider.value = value
let label = UILabel(frame: CGRect(x: slider.thumbCenterX - 20, y: slider.frame.origin.y - 25, width: 50, height: 30))
label.font = UIFont.systemFont(ofSize: 10.0)
label.textColor = UIColor.red
label.textAlignment = .center
label.text = "\(value) kg."
sliderView.addSubview(label)
}