I cannot say for sure because it is hard to understand fully what some of the variables like vv
etc.
However, I believe the main issues for getting choppy results could be:
- There is something wrong with how you are calculating the font for the current frame
- It seems you are only adjusting the font when the gesture has
ended
whereas I think it would be useful even when it is being changed
One method would be to follow these steps:
- Get the new bounds (mainly width and height) of your view
- Give your label a maximum font size
- Keep reducing the font till you get a font that fits the new size of the view
This will be easy and should work if the text is not too large
After doing some research myself as this was an interesting problem, I came across some elegant solutions to find the optimal font size using binary search
So here is how I implemented it:
1. Custom UILabel subclass called FlexiFontLabel
The logic is explained in the comments
class FlexiFontLabel: UILabel
{
// Boundary of minimum and maximum
private let maxFontSize = CGFloat(500)
private let minFontSize = CGFloat(5)
// Margin of error is needed in binary search
// so we can return when reach close enough
private let marginOFError = CGFloat(0.5)
// layoutSubviews() will get called while updating
// font size so we want to lock adjustments if
// processing is already in progress
private var isUpdatingFontSize = false
// Once this is set to true, the label should only
// only support multiple lines rather than one
var doesAdjustFontSizeToFitFrame = false
{
didSet
{
if doesAdjustFontSizeToFitFrame
{
numberOfLines = 0
}
}
}
// Adjusting the frame of the label automatically calls this
override func layoutSubviews()
{
super.layoutSubviews()
// Make sure the label is set to auto adjust the font
// and it is not currently processing the font size
if doesAdjustFontSizeToFitFrame
&& !isUpdatingFontSize
{
adjustFontSizeIfRequired()
}
}
/// Adjusts the font size to fit the label's frame using binary search
private func adjustFontSizeIfRequired()
{
guard let currentText = text,
var currentFont = font else
{
print("failed")
return
}
// Lock function from being called from layout subviews
isUpdatingFontSize = true
// Set max and min font sizes
var currentMaxFontSize = maxFontSize
var currentMinFontSize = minFontSize
while true
{
// Binary search between min and max
let midFontSize = (currentMaxFontSize + currentMinFontSize) / 2;
// Exit if approached minFontSize enough
if (midFontSize - currentMinFontSize <= marginOFError)
{
// Set min font size and exit because we reached
// the biggest font size that fits
currentFont = UIFont(name: currentFont.fontName,
size: currentMinFontSize)!
break;
}
else
{
// Set the current font size to the midpoint
currentFont = UIFont(name: currentFont.fontName,
size: midFontSize)!
}
// Configure an attributed string which can be used to find an
// appropriate rectangle for a font size using its boundingRect
// function
let attribute = [NSAttributedString.Key.font: currentFont]
let attributedString = NSAttributedString(string: currentText,
attributes: attribute)
let options: NSStringDrawingOptions = [.usesLineFragmentOrigin,
.usesFontLeading]
// Get a bounding box with the width of the current label and
// an unlimited height
let constrainedSize = CGSize(width: frame.width,
height: CGFloat.greatestFiniteMagnitude)
// Get the appropriate rectangle for the text using the current
// midpoint font
let newRect = attributedString.boundingRect(with: constrainedSize,
options: options,
context: nil)
// Get the current area of the new rect and the current
// label's bounds
let newArea = newRect.width * newRect.height
let currentArea = bounds.width * bounds.height
// See if the new frame is lesser than the current label's area
if newArea < currentArea
{
// The best font size is in the bigger half
currentMinFontSize = midFontSize + 1
}
else
{
// The best font size is in the smaller half
currentMaxFontSize = midFontSize - 1
}
}
// set the font of the current label
font = currentFont
// Open label to be adjusted again
isUpdatingFontSize = false
}
}
2. How to use the label
It is like setting up any UILabel with one small addition
let label = FlexiFontLabel()
/// Do all your set up like frames, colors
// alignments etc
// This opens the label to auto adjust itself
label.doesAdjustFontSizeToFitFrame = true
3. Pan Gesture Implementation
Nothing interesting happens here as all the adjustment happens in the UILabel subclass, however I just want to show you what I did for better understanding
@objc
func scaleGesture(recognizer: UIPanGestureRecognizer)
{
let location = recognizer.location(in: view)
// This is my logic to prevent the bottom right anchor going
// below a certain threshold, you can ignore it as it has
// nothing to do with your solution
if location.x >= minX && location.y >= minY
{
// Update the position of the anchor
circle.center = location
// Calculate the new frame for the label
var newLabelFrame = label.frame
let newWidth = location.x - originalLabelFrame.origin.x
let newHeight = location.y - originalLabelFrame.origin.y
newLabelFrame.size = CGSize(width: newWidth,
height: newHeight)
// After updating the label, layoutSubviews() will be called
// automatically which will update the font size
label.frame = newLabelFrame
}
}
This gives me the following result which is quite smooth when changing the font size according to the label frame
