I've been reviewing the documentation for UITextField and its options are much more limited than the UITextView.
I've run into a situation where I need to handle truncating the text by forcing the bounding box to be bigger so no truncation exists.
I cannot just use .adjustsFontSizeToFitWidth because this event is only allowed when the text box is at screen width. The truncation is happening when it's not full screen length.
Right now when the user types, I log each keystroke and make sure the UITextFields box expands to fit the text. However if I use a large font, the text is getting cut off still:
"THIS IS LARGE TEXT GETTING CUT O..."
Currently I log each keystroke and run this code to size it:
func adjustFrameWidthToFitText()
{
var size = sizeThatFits(CGSizeMake(CGFloat.max,height))
frame = CGRectMake(frame.origin.x, frame.origin.y, size.width + 7, frame.height)
}
However I still get the ... cut off in some places. Is there anyway to tell if the text is being truncated and override the behaviour causing said truncation?
(Solutions in Swift & Obj-c welcome!)
Based on the answer below I tried:
func adjustFrameWidthToFitText()
{
var fontSize = font.pointSize
var atr = [NSFontAttributeName:font]
var textSize = NSString(string: text).sizeWithAttributes(atr)
frame = CGRectMake(frame.origin.x, frame.origin.y, textSize.width, frame.height)
}
But there is still truncation
extension String {
func sizeWithAttributes(atr: NSDictionary) -> CGSize {
return NSString(string: self).sizeWithAttributes(atr)
}
}
let size = textView.text.sizeWithAttributes([NSFontAttribute:textView.font])
This will return the exact size for the string including '\n' characters.
The you can use the size however you want.
ADDED
Also when I was using CATextLayer I had to add this to the attributes to get the rows, havent tested on UITextView or field though:
let style = NSMutableParagraphStyle()
style.lineHeightMultiple = 1.05
..., NSParagraphStyleAttributeName:style])
After much digging I found the root cause, and a shortcut.
Shortcut first:
func adjustFrameWidthToFitText()
{
var size = intrinsicContentSize()
frame = CGRectMake(frame.origin.x, frame.origin.y, size.width, frame.height)
}
This gives me the size of the textfield.
The problem with my code was I was calculating the resize event BEFORE the character was added to the text. So my bounds were always being calculated before the latest keystroke was added to the string.
When I changed my logic
public func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
adjustFrameWidthToFitText()
return true
}
To
textFieldDidEndEditing(_:)
{
adjustFrameWidthToFitText()
}
It works.
Voila!
Related
I'm using a custom font and somehow the rendering screws up the line height, potentially because of misconfigured descent or leading (?), so that g's and j's are cut off in the last line of the rendered text. I think it might be a problem with this particular font, because Sketch is also exposing similar issues with the font in question, but I feel like I don't understand quite enough about typographic measurements or fonts. I found this Apple documentation page on Typographic Concepts quite insightful.
I looked into the font itself with the test version of FontLab, which I have used for the first time btw - so I've little clue really what I'm looking at. It does seem like the g is going below the configured descent, which seems to be what the last line is. (?) (See: Character view in FontLab, showing the descend of the g)
Via lineSpacing I could adjust the distance between just the lines itself to fix this in the first few lines. I know iOS 14 is going to bring a way to modify the leading of a Text in SwiftUI. But I need to target iOS 13, so that doesn't help.
I've also tried SwiftUI's Text, a normal UILabel.text and UILabel.attributedText with a customized paragraph style, but nothing I adjust there seems to mitigate the problem.
The view is not even clipping. Just adding padding to the frame does not help at all. It increases the distance, but the g's and j's are still cut.
What can I do? Subclass UILabel and overwrite the intrinsicContentSize to add some extra space, when there is a g and j in the last line? That feels a) dirty and b) given that padding didn't help, it might not fix the problem?
Is the font itself the problem here? Can I patch the font somehow without making it worse?
Is there any way to modify the leading or the descend height of the font, when I use lower level APIs? Seems like I could go down to CoreText, as CTFontCreateCopyWithAttributes(_:_:_:_:) is a candidate, if I could just modify via attributes the leading, line space or the descend? Can or monkey-patch / swizzle things without shooting myself in the knee? Should I just file a radar a feedback?
You need to use NSAttributedString instead of String to control the line spacing of UILabel. Here is sample code
let style = NSMutableParagraphStyle()
style.lineSpacing = 20
let string = NSMutableAttributedString(string: "The quick brown fox jumps over the lazy dog, The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog")
string.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, string.length))
let label = UILabel(frame: CGRect(x: 20, y: 100, width: 300, height: 500))
label.attributedText = string
label.numberOfLines = 0
self.view.addSubview(label)
Out put
I know what you are asking as I have faced the same issues with custom fonts. I am going to offer two solutions. In my own project I went the way of your suggestion in overriding intrinsicContentSize and adding a padding multiplier for height and width. In my case the fonts were user facing so I had a struct that held all the relevant information. FYI Chalkduster is in the system and clips. I also believe that this is all due to the font file itself.
Solution 1:
Example:
struct UserFont{
var name : String
var displayName : String
var widthMultiplier : CGFloat
var heightMultiplier : CGFloat
}
Then in my UILabel I have it subclassed to use both of these metrics
#IBDesignable
class MultiplierUILabel: UILabel {
#IBInspectable var widthPaddingMultiplier : CGFloat = 1
#IBInspectable var heightPaddingMultiplier : CGFloat = 1
override var intrinsicContentSize: CGSize{
return CGSize(width: super.intrinsicContentSize.width * widthPaddingMultiplier, height: super.intrinsicContentSize.height * heightPaddingMultiplier)
}
}
This to me was the simplest implementation as I found the font and multiplier scale accordingly.
Solution 2:
You might be able to get the draw to occur slightly higher by measuring the glyph bounds and adjusting the origin y. For example this fixes the clipping on Chalkduster font that is included in the system.
#IBDesignable
class PaddingUILabel: UILabel {
override func drawText(in rect:CGRect) {
//hello
guard let labelText = text else { return super.drawText(in: rect) }
//just some breathing room
let info = boundsForAttrString(str: labelText, font: self.font!, kern: .leastNormalMagnitude)
let glyph = info.glyph
var newRect = rect
let glyphPadding = -(glyph.origin.y)
if glyphPadding - info.descent > 1 && info.descent != 0{
newRect.origin.y -= glyphPadding/2
}else{
if info.descent != 0{
newRect.origin.y += (info.descent - glyphPadding)/2
}
}
super.drawText(in: newRect)
}
func boundsForAttrString(str:String,font:UIFont,kern:CGFloat)->(glyph:CGRect,descent:CGFloat){
let attr = NSAttributedString(string: str, attributes: [.font:font,.kern:kern])
let line = CTLineCreateWithAttributedString(attr)
var ascent : CGFloat = 0
var descent : CGFloat = 0
var leading : CGFloat = 0
CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
let glyph = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds).integral
return (glyph,leading != 0 ? descent : 0)
}
}
Result of Solution 2:
System
PaddingUILabel using glyph bounds
I have a multiple UILabels. When the device size changes the width and height of the labels changes, so I scale down the text so it fits inside the label. What I want to do is get the font size from one label, and set it as the font size of the other labels, so that everything fits inside the labels, but are also the same size.
This is my code for rescaling the text:
Name_Label.numberOfLines = 1
Name_Label.adjustsFontSizeToFitWidth = true
Name_Label.lineBreakMode = NSLineBreakMode.ByClipping
I can get the point size as other answers have suggested:
Year_Label.font.fontWithSize(Name_Label.font.pointSize)
But this does not work, and comments suggest that this does not return the font size. So how do I get the font size of a label that has just been scaled down, and use that to set the other labels font size?
UPDATE: When I print the following code the output is the same, however in the simulator the size is different.
Year_Label.font = Name_Label.font
You can use this class to fit labels size. Just tell that your label is this class. There you as well can set if it should check horizontally or vertically.
https://github.com/VolodymyrKhmil/BBBLibs/tree/master/BBBAutoresizedFontLabel
Hope it helps.
After doing more research, I found a function in Swift 3 that worked. However, to change the size you have to set the Minimum Font Scale (Which I forgot to do). Below is the code in Swift 2 that works:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
Name_Label.numberOfLines = 1
Name_Label.adjustsFontSizeToFitWidth = true
Name_Label.lineBreakMode = NSLineBreakMode.ByClipping
func adjustedFontSizeForLabel(label: UILabel) -> CGFloat {
let text: NSMutableAttributedString = NSMutableAttributedString(attributedString: label.attributedText!)
text.setAttributes([NSFontAttributeName: label.font], range: NSMakeRange(0, text.length))
let context: NSStringDrawingContext = NSStringDrawingContext()
context.minimumScaleFactor = label.minimumScaleFactor
text.boundingRectWithSize(label.frame.size, options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: context)
let adjustedFontSize: CGFloat = label.font.pointSize * context.actualScaleFactor
return adjustedFontSize
}
Name_Label.font = Name_Label.font.fontWithSize(adjustedFontSizeForLabel(Name_Label))
Year_Label.font = Year_Label.font.fontWithSize(adjustedFontSizeForLabel(Name_Label))
House_Label.font = House_Label.font.fontWithSize(adjustedFontSizeForLabel(Name_Label))
}
Link to source: https://stackoverflow.com/a/38568278/6421669
I have multiple uitextfields setup and they are all connected with IBOutlets. I have one textfield that is a password and I have the 'Secure text Entry' selected. when I have this check I get this
Any ideas why this happens? If i deselect the secured entry the textfield rises fine depending on the size of the password with no ellipsis dots.
It does not matter how long the password is. Same thing.
If i don't have the security text selected it works fine
Any idea why? It can be a width issue because it does autosize. But why does the 'secure text entry' cause the issue?
I have faced the same problem. I think it is a bug of UITextfield. It calculates size for the text but not for the secure text(dots). I have the problem when the text includes slim characters like 1, l etc...
As workaround I have subclassed the UITextfield class and overridden intrinsicContentSize function. You might need to adjust letter spacing. I couldn't find how to get it dynamically depending on font.
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
if !self.isSecureTextEntry {
return size
}
var width = size.width
if let font = self.font,
let charCount = self.text?.count {
width = "•".size(withAttributes: [NSAttributedString.Key.font : font]).width * CGFloat(charCount)
width += (CGFloat(charCount)+1) * 4.5 // this magic number is for letter spacing
}
return CGSize(width: width, height: size.height)
}
I'm working with a UITextView and I want to make it so that once the user has filled the UITextView (you make it in storyboard, and these dimensions the user is not allowed to type outside of) the user cannot type anymore text. Basically, whats happening now is even if it looks like it's filled and I keep typing its like a never-ending text box which you can't see. What I assume is the dimensions you make it in storyboard is the only space you see text in.
Can someone help me?
http://www.prntscr.com/671n1u
You can use the UITextViewDelegate shouldChangeTextInRange: method to limit the text entry to the height of the text view:
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
// Combine the new text with the old
let combinedText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
// Create attributed version of the text
let attributedText = NSMutableAttributedString(string: combinedText)
attributedText.addAttribute(NSFontAttributeName, value: textView.font, range: NSMakeRange(0, attributedText.length))
// Get the padding of the text container
let padding = textView.textContainer.lineFragmentPadding
// Create a bounding rect size by subtracting the padding
// from both sides and allowing for unlimited length
let boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFloat.max)
// Get the bounding rect of the attributed text in the
// given frame
let boundingRect = attributedText.boundingRectWithSize(boundingSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, context: nil)
// Compare the boundingRect plus the top and bottom padding
// to the text view height; if the new bounding height would be
// less than or equal to the text view height, append the text
if (boundingRect.size.height + padding * 2 <= textView.frame.size.height){
return true
}
else {
return false
}
}
I have a UItextfieldthat holds a person's middle name. I only want it to display the first initial, which it does, but i want it to hold their entire name. It's only large enough to show the one initial, but it adds that ellipses (...) after the letter.Is it possible to remove those when a uitextfield overflows? I haven't found anything online regarding someone with the same issue.
Thankyou for your help
I do see this truncation when the text field resigns first responder. I fixed this by setting the lineBreakMode in the NSParagraphStyle attribute to .byClipping. I happened to be using a subclass of UITextField so I overrode resignFirstResponder() to do this. My textField starts out empty so there is no attributedString to start with in viewDidLoad.
override func resignFirstResponder() -> Bool {
guard let newAttributedText = (attributedText?.mutableCopy() as? NSMutableAttributedString) else {
return super.resignFirstResponder()
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byClipping
newAttributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedText?.length ?? 0))
attributedText = newAttributedText
return super.resignFirstResponder()
}
This might not work if you set the text in code. In that case, you might want to set the lineBreak mode in a common function that you call from both the override of resignFirstResponder() and after setting the text in code. You could make a set(text: String?) function and call the common function from there.
A UITextField shouldn't be truncating the text (because you can usually scroll/select that UI element).
A UILabel will truncate by default, you can set it to clip instead.