Dynamically center 2 lines of text programmatically with roughly equal length per row - ios

My situation is that I have a line of text that can vary in length due to localization. This will need to be displayed on the screen such that each line is roughly of equal length, and is centered.
This is my very long line.
Should look like this
This is my
very long line.

So I took a crack at this and got something that works the way I want it now.
I take a localized string, set it to an empty label, and find out what it's size is. (The orange is just for illustrative purposes)
With the size of the label, I then divide it by 1.8 which gives me some buffer room to account for inconsistent word sizes (again, I don't know what will be here in advance). Finally, I multiply the height by 2.0, and set that as my new frame. Finally, I add it to the view.
This has held up with a few sample strings, though it would need to be revised to handle more than 2 lines (currently, not an issue).
let text = NSLocalizedString("This is my very long line of text.", comment: "")
let instructionLabel = UILabel()
instructionLabel.text = text
instructionLabel.textAlignment = .center
instructionLabel.backgroundColor = .orange
instructionLabel.numberOfLines = 0
let size = instructionLabel.intrinsicContentSize
let newSize = CGSize(width: size.width / 1.8, height: size.height * 2.0)
let rect = CGRect(x: 20, y: 100, width: newSize.width, height: newSize.height)
instructionLabel.frame = rect
view.addSubview(instructionLabel)
Which produces the following output:
And an even longer one:
Just for some variety, this is the second string above, but in Arabic:

You could do this to set alignment.
myLabel.textAlignment = .center
Also set the number of lines to 0. And if you want a specific width, set the preferredMaxLayoutWidth property like so:
myLabel.preferredMaxLayoutWidth = 80
myLabel.numberOfLines = 0

If you want it to work for arbitrary localizations (assuming languages that use spaces), you would need an algorithm that split the text on spaces and then loop through each combination of top and bottom text, measuring its width, to see what gave the most evenly distributed sizing. This feels like overkill.
Having done a fair amount of localization, the better bet is to manually insert \n characters in the .strings file to adjust breaks that aren't visually pleasing. Relying on a fixed width will work for many languages, but won't give you the flexibility you're looking for.

Related

Find width of string (swift)

How do I find the width of a string (CGFloat) given the font name and font size?
(The goal is to set the width of a UIView to be just wide enough to hold the string.)
I have two strings: one with "1" repeated 36 times, the other with "M" repeated 36 times. These both fill the width (359.0) of the screen (give or take a little for margins).
I am using using Courier 16, which is monospaced, so I expect the width of both strings to be equal (as they in fact do appear on the screen).
However, using https://stackoverflow.com/a/58782429/8635708 :
the width of the string with the "1"s is 257.34375
the width of the string with the "M"s is 492.1875.
The first is does not fill the screen, the other is way too long.
And using https://stackoverflow.com/a/58795998/8635708 :
the width of each string is 249.640625.
At least here, they are the same, but that value clearly does not fill the screen.
I think you could create a label and call label.sizeToFit():
let label = UILabel()
label.font = UIFont.init(name: "Courier", size: 16)
label.text = "mmmmmmmmmmmmmmmm"//"1111111111111111"
label.sizeToFit()
print("Width: \(label.frame.size.width)") //153.66666666666666 -> For both strings

String Fit to Fix UILabel Height

I followed a that told to use "TruncatingTail" not "WordWrapping" so I used this. But still not able to find the way to get the best optimal output as attached in image.
lbl.text = "1 Year\n=\n365 Opportunities"
lbl.numberOfLines = 0;
lbl.lineBreakMode = .byTruncatingTail;
lbl.adjustsFontSizeToFitWidth = true;
lbl.minimumScaleFactor = 0.1;
lbl.textAlignment = .center;
lbl.backgroundColor = .red
lbl.font = UIFont.init(name:"SourceCodePro-Regular", size: 80)
lbl.numberOfLines = 0;
lbl.lineBreakMode = .byWordWrapping;
I am building an App for Quotes, but I am stuck at this point. I have tried all various ways but not able to succeed. Please anyone can help so it will be a great help to me.
Thank you.
From the UILabel documentation for lineBreakMode:
Specifies what happens when a line is too long for the label’s bounds. Character wrap and word wrap are most commonly applied to multiline labels and determine the position of line breaks between consecutive lines. Select word wrap to place line breaks at word boundaries, or character wrap to insert line breaks in words. Truncate head, middle, and tail are usually applied for single-line labels, and describe the placement of an inserted ellipsis to represent the truncated text. Access this value at runtime with the lineBreakMode property.
Truncating the tail doesn't apply when you allow as many lines as required (numberOfLines = 0). Switch back to using byWordWrapping and your auto-shrink settings should do the rest.

How to make multi-line UILabel text fit within predefined width without wrapping mid-word

I have a UILabel carefully laid out in Interface Builder with proper height and width constraints. The number of lines is set to 4. The wrapping is set to word wrap. The text is "CHECKED". The font size is very large and thus it only fits "CHECKE" and the "D" is on the second line. Writing "Checked" instead of "CHECKED" lets the font shrink (as intended) so that the whole word fits. But (the text is user given and it can be expected that the user writes fully uppercase words) having uppercase words the label does not break it/shrink the font as expected.
Do you have a suggestion as to what I might have missed? Capitalising the words (thusly only having the first letter uppercase) does work, but is not what the client wants.
Updated question
The problem seems to be unrelated to having uppercase or lowercase text. My problem could be solved by an answer to the following question:
How to make (ideally with the help of only Interface Builder) the UILabel text shrink trying to fit full words within all available lines without wrapping the text mid-word?
If the text "CHECKED" is too wide for a label (with more than 1 line available) it should shrink the font size instead of breaking the "D" and wrapping the single letter to the next line.
If the text is "one CHECKED two" and the single word "CHECKED" is already too wide for a label (with more than 1 line available) it should break between all words and shrinking the font size so that "CHECKED" still fits the middle line.
Avoiding:
one
CHECKE
D two
Thank you very much!
Here is a UILabel subclass that will find the largest word in the labels text, use the boundingRect function of NSString to see how large that one word will be with the current font, and drop the font size until it fits the width.
class AutosizingMultilineLabel: UILabel {
override func layoutSubviews() {
super.layoutSubviews()
self.adjustFontToFitWidth()
}
func adjustFontToFitWidth() {
guard let currentFont = self.font else { return }
let minimumFontSize: CGFloat = floor(self.minimumScaleFactor * currentFont.pointSize)
var newFontSize = currentFont.pointSize
var theNewFont = currentFont
if let text = self.text, let longestWord = text.components(separatedBy: " ").max(by: {$1.count > $0.count})?.replacingOccurrences(of: "\n", with: "") {
let nsString = longestWord as NSString
while newFontSize > minimumFontSize {
theNewFont = currentFont.withSize(newFontSize)
let boundingRect = nsString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
options: NSStringDrawingOptions.usesLineFragmentOrigin,
attributes: [.font: theNewFont],
context: nil)
if ceil(boundingRect.size.width) <= self.bounds.size.width {
break
}
newFontSize -= 1
}
self.font = theNewFont
}
}
}
When the word is bigger than the line, word wrap doesn't work. If it doesn't fit on this line, it won't fit on the next line. (same word, same size, same line size). To make it fit, the label will start putting letters on the next line.
If you allow multiple lines on your label, the OS will try to fill the lines before adjusting the font size.
I think you're just running into a limitation on Autoshrink.
In Interface Builder:
add a new UILabel with Width: 230 and Height: 280
set the Font to System 44.0
set Line Break: Truncate Tail
set Autoshrink: Minimum Font Scale at 0.15
set the text of the label to test CHECKED lines
Now, drag the handle on the right edge of the label left and right... when it gets too narrow, the word CHECKED will break onto the next line.
Change CHECKED to checked and do the same thing. You should see the same behavior.
Now, try dragging the Bottom edge up and down. With either CHECKED or checked, you should see the Font Size auto shrink.
So... to do what you're trying to do, you might have to skip Autoshrink and instead do some code calculations.
Edit: further visual of what goes on...
Start with above values, but set the Height of the label to 170 - gives it just a little vertical padding.
Now, drag the left edge to make it narrower.
When you reach the end of the word CHECKED, and keep going, you will see the font shrink until it gets small enough that there is space for it to wrap to a 4th line.
I think you're going to need some code to get exactly what you need.

how much pixels dose a character takes in iOS?

I'm trying to automatically layout text on a UILabel view.
The text (such as "abcdefghij") contains ten characters. I want to display it in one single line.
I turned off the Size Class and Auto Layout for convenience, and added following codes to layout the text on the UILabel. It should be ten characters in one line, and the width of the UILabel is equal to the width of the device.
let screenWidth = UIScreen.mainScreen().bounds.width
labelView.frame = CGRect(x: labelView.frame.origin.x, y: labelView.frame.origin.y, width: screenWidth, height: labelView.frame.height)
let string = "abcdefghij"
let stringLength = CGFloat(string.characters.count)
let characterSize = keyboardView.font.pointSize
let characterSpacing = (screenWidth - characterSize * stringLength) / stringLength
let content = NSAttributedString(string: string, attributes: [NSKernAttributeName: characterSpacing])
keyboardView.attributedText = content
But it turns out like this. The width of string is not equal to the screen
I think, the only could be wrong here is the pointSize. It equals to 13.8 while I set the font size to 17.
I don't understand it.
Give me some hints, please.
Thanks for your attention. 😄
By using sizeWithAttributes and boundingRectWithSize(_:options:context:), I finally figured out how it works. But my origin purpose is fitting the 10 characters in one line. The code should calculate the space between the characters, and all the space is same size. Could you give me some advices?
This is what I want to make
Each character occupies different amount of space depending on the character, font and size of the font.
Hence, you can use boundingRectWithSize(_:options:context:) to predict size of the string at runtime, and then take action according to your requirements.

How to get UILabel to resize better?

I am trying to make my multiline UILabel as small as possible with given max-width, but the sizeToFit() and sizeThatFits(_,_) methods aren't giving the results I want.
Take a look at this image:
The red rectangle represents the width that I pass in sizeThatFits (while height being Max). Obviously, as you can see, this method does in fact return a size that fits, but it does not give me the smallest size possible, which I want.
Let's say I specified max-width: 300. This actual result is giving me a size of ca. 280*50.
As you can see in the image, the text is now written like this:
Here is some text that is supposed to
align nicely
What I want to achieve is this:
Here is some text that is
supposed to align nicely
This result would've had the same height, but a much smaller width, e.g 200*50
I realize that it's difficult to define "smallest size possible", as it could return this:
Here
is
some
text
that
...
Or even just a single letter per line. But given that sizeThatFits returns this, with a given width and height, why doesn't it return my wanted result, which is the same height, but with smaller width. Why isn't the smallest fit returned? Does a function like this exist?
Sti,
You can try this buddy :) I have been using it and seems to do a pretty good job :)
CGRect rect = [yourText boundingRectWithSize:CGSizeMake(maxWidth_you_Can_afford, CGFLOAT_MAX)
options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
attributes:#{NSFontAttributeName:[UIFont systemFontOfSize:16]}
context:nil];
The rect after executing will have the minimum width and height that will be required to render the text :)
Once you have the frame now you can set the same for your label and set the text as well :)
I have used it in chat bubble :) and it works well :)
Happy coding :)
I made my own solution, which works pretty well:
private func calculateWantedSize(label:UILabel)->CGSize{
var lastAcceptableWidth = label.bounds.width
let currentHeight = label.sizeThatFits(CGSize(width: label.bounds.width, height: CGFloat.max)).height
var tempHeight = currentHeight
while(tempHeight == currentHeight){
let newWidth = lastAcceptableWidth - 1
tempHeight = label.sizeThatFits(CGSize(width: newWidth, height: CGFloat.max)).height
if tempHeight == currentHeight{
lastAcceptableWidth = newWidth
}
}
return CGSize(width: lastAcceptableWidth, height: label.bounds.height)
}
Here I'm sending my label to a function which first calculates the current width and height of the label, then loops through width-1 and checks the resulting height until the height is changed. The height will change when the width is so low that it needs a new line. When this happens, I return the last valid width which still needed the same height as original.
This is perfect for my problem, using attributed text etc.
Of course, when using this, be sure to do some checking before calling. I don't know what will happen if you use this function when the label has a single word, or a single letter, or no text at all. I have only tested this for the cases I know will happen with this app, and it works well.

Resources