Finding NSFont Size to Fit a particular width - swift

I'm using the following code to find an NSFont size to fit a Width using a while loop.Is there an inbuilt function to make the computation easier?
let ratio = image.size.width/referenceimagesize!.width
let fcsize=fontsize.width*ratio
if(fontsize.width<fcsize)
{
while(fontsize.width==fcsize)
{
var newheight:CGFloat = 0;
font = NSFont(name: font!.fontName, size: font!.pointSize+1)
var fontAttributes = [NSAttributedStringKey.font: font]
fontsize = (text as NSString).size(withAttributes: fontAttributes)
}
}

No, there's no inbuilt function, but why do you use while(fontsize.width==fcsize)? Shouldn't the comparison be <? Try something like this:
func findFontSize(text: String, frameWidth: CGFloat, font: NSFont) -> CGFloat {
var textSize = text.size(withAttributes: [.font: font])
var newPointSize = font.pointSize
while textSize.width < frameWidth {
newPointSize += 1
let newFont = NSFont(name: font.fontName, size: newPointSize)!
textSize = text.size(withAttributes: [.font: newFont])
}
return newPointSize
}

Related

CIAttributedTextImageGenerator filter - text doesn't fit (NSAttributedString)

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

Vision – Face recognition performed but correct coordinates cannot be obtained

Swift 5, Xcode 11, iOS 13.0.
In the code below, we will get the face from the image 'test', recognize the left eye, and display 'num' at its coordinates.
However, the coordinates of the nose are not displayed at the correct position.
I'm in trouble because I don't know the solution. I would be grateful if you could tell me.
import Vision
import Foundation
import SwiftUI
struct ContentView: View {
#ObservedObject var faceGet = test()
#State var uiimage : UIImage? = nil
var body: some View {
VStack{
if uiimage != nil {
Image(uiImage: uiimage!).resizable().scaledToFit()
}
Divider()
Button(action: {
self.uiimage = self.faceGet.faceCheck()
}){
Text("Tap image to see result")
}
}
}
}
class test :ObservableObject{
private var originalImage = UIImage(named: "test3")
func faceCheck() -> UIImage?{
var drawnImage : UIImage? = originalImage
let request = VNDetectFaceLandmarksRequest { (request, error) in
for observation in request.results as! [VNFaceObservation] {
if let landmark = observation.landmarks?.nose{
for i in 0...landmark.pointCount - 1 {
drawnImage = self.drawText(
image: drawnImage!,
point: landmark.pointsInImage(imageSize: self.originalImage!.size) [i] ,
num: i)
}
}
}
}
if let cgImage = self.originalImage?.cgImage {
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
try? handler.perform([request])
}
return drawnImage
}
func drawText(image :UIImage , point:CGPoint , num:Int ) ->UIImage
{
let text = num.description
var newImage : UIImage? = nil
let fontSize = image.size.height / image.size.width * 10
let font = UIFont.boldSystemFont(ofSize: fontSize)
let textWidth = CGFloat(round(text.widthOfString(usingFont: font)))
let textHeight = CGFloat(round(text.heightOfString(usingFont: font)))
let imageRect = CGRect(origin: .zero, size: image.size)
UIGraphicsBeginImageContext(image.size);
image.draw(in: imageRect)
let rePoint :CGPoint = CGPoint(x:imageRect.maxX - CGFloat(round(point.x)),
y:imageRect.maxY - CGFloat(round(point.y)))
let textRect = CGRect(origin: rePoint, size: CGSize(width: textWidth , height: textHeight ))
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: UIColor.red,
NSAttributedString.Key.paragraphStyle: textStyle
]
text.draw(in: textRect, withAttributes: textFontAttributes)
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext()
return newImage!
}
}
extension String {
public func widthOfString(usingFont font: UIFont) -> CGFloat {
let attributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: attributes)
return size.width
}
public func heightOfString(usingFont font: UIFont) -> CGFloat {
let attributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: attributes)
return size.height
}
}

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

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

How to get the height of a UILabel in Swift?

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

Resize font size in UITextView

Is there a way to shrink the font-size in a UITextView if there is too much text? Similar to the UILabel?
The problem with the accepted answer is that you have to guess the number of characters (the string's length) needed to fill the field, and that differs from font to font. Something like this, a category on UITextView, should work.
#import "UITextView+Size.h"
#define kMaxFieldHeight 1000
#implementation UITextView (Size)
-(BOOL)sizeFontToFitMinSize:(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 = [self.text sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
while (stringSize.height >= self.frame.size.height) {
if (fontSize <= aMinFontSize) // it just won't fit, ever
return NO;
fontSize -= 1.0;
self.font = [self.font fontWithSize:fontSize];
tallerSize = CGSizeMake(self.frame.size.width-fudgeFactor,kMaxFieldHeight);
stringSize = [self.text sizeWithFont:self.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
}
return YES;
}
#end
Try this:
NSInteger lengthThreshold = 200;
if( [ textView.text length ] > lengthThreshold ) {
NSInteger newSize = ... //calculate new size based on length
[ textView setFont: [ UIFont systemFontOfSize: newSize ]];
}
Swift 4 implementation inspired by #Jane Sales's answer.
When calculating available width and height we must also take into consideration possible vertical and horizontal margins (textContainerInset and textContainer.lineFragmentPadding).
Here's a better explanation of how margins work on UITextView: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/TextUILayer/Tasks/SetTextMargins.html
If the text view can resize, then we must also force a layout so we can calculate the font size based on biggest possible text view size. In this case only height is considered (layouts only if required text height is bigger than original available height).
import UIKit
extension UITextView {
func adjustFontToFitText(minimumScale: CGFloat) {
guard let font = font else {
return
}
let scale = max(0.0, min(1.0, minimumScale))
let minimumFontSize = font.pointSize * scale
adjustFontToFitText(minimumFontSize: minimumFontSize)
}
func adjustFontToFitText(minimumFontSize: CGFloat) {
guard let font = font, minimumFontSize > 0.0 else {
return
}
let minimumSize = floor(minimumFontSize)
var fontSize = font.pointSize
let availableWidth = bounds.width - (textContainerInset.left + textContainerInset.right) - (2 * textContainer.lineFragmentPadding)
var availableHeight = bounds.height - (textContainerInset.top + textContainerInset.bottom)
let boundingSize = CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)
var height = text.boundingRect(with: boundingSize, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil).height
if height > availableHeight {
// If text view can vertically resize than we want to get the maximum possible height
sizeToFit()
layoutIfNeeded()
availableHeight = bounds.height - (textContainerInset.top + textContainerInset.bottom)
}
while height >= availableHeight {
guard fontSize > minimumSize else {
break
}
fontSize -= 1.0
let newFont = font.withSize(fontSize)
height = text.boundingRect(with: boundingSize, options: .usesLineFragmentOrigin, attributes: [.font: newFont], context: nil).height
}
self.font = font.withSize(fontSize)
}
}