Im currently trying to scale an image using swift. This shouldnt be a difficult task, since i've implemented a scaling solution in C# in 30 mins - however, i've been stuck for 2 days now.
I've tried googling/crawling through stack posts but to no avail. The two main solutions i have seen people use are:
A function written in Swift to resize an NSImage proportionately
and
resizeNSImage.swift
An Obj C Implementation of the above link
So i would prefer to use the most efficient/least cpu intensive solution, which according to my research is option 2. Due to option 2 using NSImage.lockfocus() and NSImage.unlockFocus, the image will scale fine on non-retina Macs, but double the scaling size on retina macs. I know this is due to the pixel density of Retina macs, and is to be expected, but i need a scaling solution that ignores HiDPI specifications and just performs a normal scale operation.
This led me to do more research into option 1. It seems like a sound function, however it literally doesnt scale the input image, and then doubles the filesize as i save the returned image (presumably due to pixel density). I found another stack post with someone else having the exact same problem as i am, using the exact same implementation (found here). Of the two suggested answers, the first one doesnt work, and the second is the other implementation i've been trying to use.
If people could post Swift-ified answers, as opposed to Obj C, i'd appreciate it very much!
EDIT:
Here's a copy of my implementation of the first solution - I've divided it into 2 functions:
func getSizeProportions(oWidth: CGFloat, oHeight: CGFloat) -> NSSize {
var ratio:Float = 0.0
let imageWidth = Float(oWidth)
let imageHeight = Float(oHeight)
var maxWidth = Float(0)
var maxHeight = Float(600)
if ( maxWidth == 0 ) {
maxWidth = imageWidth
}
if(maxHeight == 0) {
maxHeight = imageHeight
}
// Get ratio (landscape or portrait)
if (imageWidth > imageHeight) {
// Landscape
ratio = maxWidth / imageWidth;
}
else {
// Portrait
ratio = maxHeight / imageHeight;
}
// Calculate new size based on the ratio
let newWidth = imageWidth * ratio
let newHeight = imageHeight * ratio
return NSMakeSize(CGFloat(newWidth), CGFloat(newHeight))
}
func resizeImage(image:NSImage) -> NSImage {
print("original: ", image.size.width, image.size.height )
// Cast the NSImage to a CGImage
var imageRect:CGRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)
// Create a new NSSize object with the newly calculated size
let newSize = NSSize(width: CGFloat(450), height: CGFloat(600))
//let newSize = getSizeProportions(oWidth: CGFloat(image.size.width), oHeight: CGFloat(image.size.height))
// Create NSImage from the CGImage using the new size
let imageWithNewSize = NSImage(cgImage: imageRef!, size: newSize)
print("scaled: ", imageWithNewSize.size.width, imageWithNewSize.size.height )
return NSImage(data: imageWithNewSize.tiffRepresentation!)!
}
EDIT 2:
As pointed out by Zneak: i need to save the returned image to disk - Using both implementations, my save function writes the file to disk successfully. Although i dont think my save function could be screwing with my current resizing implementation, i've attached it anyways just in case:
func saveAction(image: NSImage, url: URL) {
if let tiffdata = image.tiffRepresentation,
let bitmaprep = NSBitmapImageRep(data: tiffdata) {
let props = [NSImageCompressionFactor: Appearance.imageCompressionFactor]
if let bitmapData = NSBitmapImageRep.representationOfImageReps(in: [bitmaprep], using: .JPEG, properties: props) {
let path: NSString = "~/Desktop/out.jpg"
let resolvedPath = path.expandingTildeInPath
try! bitmapData.write(to: URL(fileURLWithPath: resolvedPath), options: [])
print("Your image has been saved to \(resolvedPath)")
}
}
To anyone else experiencing this problem - I ended up spending countless hours trying to find a way to do this, and ended up just getting the scaling factor of the screen (1 for normal macs, 2 for retina)... The code looks like this:
func getScaleFactor() -> CGFloat {
return NSScreen.main()!.backingScaleFactor
}
Then once you have the scale factor you either scale normally or half the dimensions for retina:
if (scaleFactor == 2) {
//halve size proportions for saving on Retina Macs
return NSMakeSize(CGFloat(oWidth*ratio)/2, CGFloat(oHeight*ratio)/2)
} else {
return NSMakeSize(CGFloat(oWidth*ratio), CGFloat(oHeight*ratio))
}
I've been working on making a view controller that will crop an image down to a specific size with some draggable control points and the background image outside of the crop zone dimmed.
For some reason whenever the image is cropped, it is grabbing the wrong reference. I've looked at just about every other post on this to deal with cropping.
Here is my setup for the Storyboard:
I've asked a few other people including a tutor and mentor from a course that I'm taking, but we all seem to be stumped.
I can select a frame by dragging the UL UR DL DR corners around the view controller like this:
But when I press the button and use the crop function I've written, I get something that is not the correct crop based on the framed selection.
I also get this error message during the cropping proceedure:
2016-09-07 23:36:38.962 ImageCropView[33133:1056024]
<UIView: 0x7f9cfa42c730; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x7f9cfa408400>>'s window
is not equal to <ImageCropView.CroppedImageViewController: 0x7f9cfa43f9b0>'s view's window!
The offending part of the code must be somewhere in one of the functions below.
Here is the cropping function:
func cropImage(image: UIImage, toRect rect: CGRect) -> UIImage {
func rad(deg: CGFloat) -> CGFloat {
return deg / 180.0 * CGFloat(M_PI)
}
// determine the orientation of the image and apply a transformation to the crop rectangle to shift it to the correct position
var rectTransform: CGAffineTransform
switch image.imageOrientation {
case .Left:
rectTransform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(rad(90)), 0, -image.size.height)
case .Right:
rectTransform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(rad(-90)), -image.size.width, 0)
case .Down:
rectTransform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(rad(-180)), -image.size.width, -image.size.height)
default:
rectTransform = CGAffineTransformIdentity
}
// adjust the transformation scale based on the image scale
rectTransform = CGAffineTransformScale(rectTransform, UIScreen.mainScreen().scale, UIScreen.mainScreen().scale)
// apply the transformation to the rect to create a new, shifted rect
let transformedCropSquare = CGRectApplyAffineTransform(rect, rectTransform)
// use the rect to crop the image
let imageRef = CGImageCreateWithImageInRect(image.CGImage, transformedCropSquare)
// create a new UIImage and set the scale and orientation appropriately
let result = UIImage(CGImage: imageRef!, scale: image.scale, orientation: image.imageOrientation)
return result
}
Here are the functions to set and translate the mask view
func setTopMask(){
let path = CGPathCreateWithRect(cropViewMask.frame, nil)
topMaskLayer.path = path
topImageView.layer.mask = topMaskLayer
}
func translateMask(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(self.view)
sender.view!.center = CGPointMake(sender.view!.center.x + translation.x, sender.view!.center.y + translation.y)
// print(sender.translationInView(self.view))
sender.setTranslation(CGPointZero, inView: self.view)
// print("panned mask")
if sender.state == .Ended {
printFrames()
}
}
func setCropMaskFrame() {
let x = ulCorner.center.x
let y = ulCorner.center.y
let width = urCorner.center.x - ulCorner.center.x
let height = blCorner.center.y - ulCorner.center.y
cropViewMask.frame = CGRectMake(x, y, width, height)
setTopMask()
}
I know this was long time ago...Just a thought, I ran into similar problem and what I found is that the frames for cropping are most probably correct. The problem lies in the actual size of the picture you're trying to crop. I solved the issue by aligning sizes of my view which holds the picture, with the actual picture size (in points). Then the cropping area cropped what was selected. I know this is probably not a solution, just sharing my experience, hope it helps to turn on some lightbulbs :)
I have a square (200X200) with a SKLabelNode in it. The label shows score and it my be reach a large number. I want fit the number in the square.
How can i change text size (or Size) of a SKLabelNode to fit it in a fixed size.
The size of the frame of the SKLabelNode can be compared against the given rectangle. If you scale the font in proportion to the sizes of the label's rectangle and the desired rectangle, you can determine the best font size to fill the space as much as possible. The last line conveniently moves the label to the center of the rectangle. (It may look off-center if the text is only short characters like lowercase letters or punctuation.)
Swift
func adjustLabelFontSizeToFitRect(labelNode:SKLabelNode, rect:CGRect) {
// Determine the font scaling factor that should let the label text fit in the given rectangle.
let scalingFactor = min(rect.width / labelNode.frame.width, rect.height / labelNode.frame.height)
// Change the fontSize.
labelNode.fontSize *= scalingFactor
// Optionally move the SKLabelNode to the center of the rectangle.
labelNode.position = CGPoint(x: rect.midX, y: rect.midY - labelNode.frame.height / 2.0)
}
Objective-C
-(void)adjustLabelFontSizeToFitRect:(SKLabelNode*)labelNode rect:(CGRect)rect {
// Determine the font scaling factor that should let the label text fit in the given rectangle.
double scalingFactor = MIN(rect.size.width / labelNode.frame.size.width, rect.size.height / labelNode.frame.size.height);
// Change the fontSize.
labelNode.fontSize *= scalingFactor;
// Optionally move the SKLabelNode to the center of the rectangle.
labelNode.position = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect) - labelNode.frame.size.height / 2.0);
}
I have written this for the width, but you can adapt it to the height to fit your CGRect. In the example, pg is a SKLabelNode initialized with the font you are using. Arguments are your String and the target width, and the result is the size you want to assign to your SKLabelNode. Of course, you can also put directly your SKLabelNode. If the size is too big, then the max size is 50, but that's personal.
func getTextSizeFromWidth(s:String, w:CGFloat)->CGFloat {
var result:CGFloat = 0;
var fits:Bool = false
pg!.text=s
if(s != ""){
while (!fits) {
result++;
pg!.fontSize=result
fits = pg!.frame.size.width > w;
}
result -= 1.0
}else{
result=0;
}
return min(result, CGFloat(50))
}
Edit: Actually, I just realized I had also written this:
extension SKLabelNode {
func fitToWidth(maxWidth:CGFloat){
while frame.size.width >= maxWidth {
fontSize-=1.0
}
}
func fitToHeight(maxHeight:CGFloat){
while frame.size.height >= maxHeight {
fontSize-=1.0
}
This expansion of mike663's answer worked for me, and gets there much quicker than going 1 pixel at a time.
// Find the right size by trial & error...
var testingSize: CGFloat = 0 // start from here
var currentStep: CGFloat = 16 // go up by this much. It will be reduced each time we overshoot.
var foundMatch = false
while !foundMatch {
var overShot = false
while !overShot {
testingSize += currentStep
labelNode.fontSize = testingSize
// How much bigger the text should be to perfectly fit in the given rectangle.
let scalingFactor = min(rect.width / labelNode.frame.width, rect.height / labelNode.frame.height)
if scalingFactor < 1 { overShot = true } // Never go over the space
else if scalingFactor < 1.01 { foundMatch = true } // Stop when over 99% of the space is filled
}
testingSize -= currentStep // go back to the last one that didn't overshoot
currentStep /= 4
}
labelNode.fontSize = testingSize // go back to the one we were happy with
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!
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)
}