First of all, there are many questions on StackOverflow, but none that fully answer this question.
The problem is mainly, but most likely not limited to, Thai and Arabic diacritics when rendered with a custom Latin-only font, using the text property of a UILabel. Which is also intrinsically sized in an auto-layout. I've already done everything Apple suggests, playing with the settings mentioned in their documentation, WWDC videos, as well as questions on StackOverflow (e.g. clipsToBounds = NO, etc.). Keep in mind, only my custom font setup clips in my scenario, not the iOS system font (.SF-UIDisplay), and not even the iOS system provided Helvetica or Helvetic Neue. The custom font has been checked and rechecked, and at this point the conclusion, iOS is the anomaly across all platforms, even macOS. To be even clearer, the same clipping behavior as the custom font can be seen with SF Pro, a font provided by Apple themselves here: https://developer.apple.com/fonts/
This question is about the most proper, least intrusive, and most complete way to do what is necessary to not clip diacritics. Meaning, how would you do this, ideally, from scratch.
All of my font research and test runs have led all those involved in this problem to believe that Apple has implemented special treatment specifically for their system fonts in UILabel, to avoid diacritic clipping. So making that an assumption, I'm also assuming the font is ok, and I'm looking for solutions that do not involve editing the font.
In my tries to use the font, the first thing to go wrong was vertical clipping of the ascender diacritics of Thai glyphs:
นื้ทั้มูHello
This means the glyphs of the font Thonburi when they cascade from the custom Latin-only font. The fix from this point, was to use a custom font only for Thai without any Latin characters, so it could be defined as the primary font, and cascade to the previously mentioned Latin-only custom font. After all this, the custom Thai font still has horizontal clipping issues on diacritics that come at the end of the text:
Worldฟล์
So now I am at a loss for anything further that font management puppetry can do (though still open to suggestions), and I am moving on to more code-centric fixes. I've seen quite a few questions and answers mentioning subclassing UILabel, but I'd like to know what this would look like that could accomplish what I've described.
I'd also like to know if just opting out of UILabel would be an option for anyone. Meaning would writing something from the ground up with TextKit be worth it to avoid all these bugs that seem to only plague iOS, and specifically UILabel.
At first I thought this was a problem with the framework but it's not, it's just a strict enforcement of a font's metrics. And in probably everything but web/app development, fonts are not rendered so strictly, which is why this problem rarely comes up. Fonts have a number of metrics that tell the program rendering it onto the screen how to render it, most importantly how to handle padding. And UILabel (and UITextField, and likely others) applies these metrics strictly. And the problem for us is that some fonts are very decorative and are often too thick or oblique to fit perfectly into the square canvas that each character must fit into (this is especially the case with accents, like umlauts). This isn't a problem outside of web/app development because when a character doesn't fit into its canvas, like a very thick, wide, and oblique W, the program just shows it anyway, and that's why a low-hanging g might spill into the line below it. But if that g was rendered in a single-line UILabel, because of how strict the font-metric enforcement is in iOS, that low-handing g is clipped.
Subclassing UILabel (in the case of UILabel) and overriding its intrinsicContentSize to add some extra padding is not a good idea, on further research. For one, it's kind of hacky, but more importantly, it produces constraint warnings in the debugger. The true fix, and the only acceptable fix AFAIK, is to edit the font's metrics.
Download a program like Glyphs (https://glyphsapp.com/), open the font, open the Font's Info, and in the Masters tab, give the font the proper ascender and descender values. To best understand how these values work, open the San Francisco font in the program and see how Apple did it (it's the font they made specifically for macOS and iOS development). As a side note, if you use this app, when you're editing the font's info, go into the Features tab as well, delete all of the features (using the minus icon in the lower left) and hit Update to let the program manage the font's features for you.
The last hurdle is clipping at the leading edge (not the top and bottom) which the ascender and descender metrics don't address. You can use the Glyphs program to edit the canvas size of individual characters to make sure they all fit but that changes the complexion of the font because it changes the character spacing too noticeably. For this problem, I think the best solution is to simply use attributed strings for your labels and text fields. And that's because attributed strings let you safely edit padding without hacking into intrinsic sizes. An example:
someLabel.attributedText = NSAttributedString(string: "Done", attributes: [NSAttributedString.Key.font: UIFont.blackItalic(size: 26), NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle.kItalicCenter])
For convenience, I extended NSMutableParagraphStyle since I use this font all over:
extension NSMutableParagraphStyle {
static var kItalicCenter: NSMutableParagraphStyle {
let s = NSMutableParagraphStyle()
s.alignment = .center
s.firstLineHeadIndent = 2
s.headIndent = 2
return s
}
}
This label will push the font forward a couple of points to prevent clipping.
I was trying to solve similar problem with diacritics in Arabic and found workaround:
I have a UITableViewCell with UILabel with arabic text, it's diacritics were cut sometimes
I overrided - (void)drawRect:(CGRect)frame to directly draw NSAttributedString on UITableViewCell
Also I decreased alpha self.arabicLabel.alpha = 0.1; to draw manually on top of label position, I still keep it to calculate cell's height
- (void)drawRect:(CGRect)frame {
[super drawRect:frame];
if (self.viewModel == nil) return;
NSAttributedString *string = [self.viewModel arabicStringWithTajweed];
CGRect originalRect = [self convertRect:self.arabicLabel.frame fromView:self.arabicLabel];
[string drawInRect:originalRect];
}
The core problem on iOS is font substitution. You are specifying a latin font, the font does not contain glyphs for the characters that will be rendered, the system uses a different font to draw the glyphs, but it is still measuring based on the original font.
Option 1, the most robust option, is to manually choose fonts that include glyphs for the characters you will render. When the font assigned to UILabel, or the attributed string it is rendering, contains all the glyphs that will be rendered, and that font has good metrics as most system fonts do, then nothing will be clipped.
Option 2, manually measure the string using glyph bounds. Make a subclass of UILabel and override textRectForBounds and possibly drawText. Measure the string with .usesDeviceMetrics. This is slower that measuring by font metrics and produces different results. For example, the strings "a" and "A" will measure differently.
Option 3, use baseline offset and line height multiple to make room for the diacritics that are being clipped. Choose or compute constant values for each font for each language, and apply those to the attributed string of the UILabel. This can compensate for the different in font metrics between the font you chose and the font that is actually rendering glyphs. We had localized strings with the worst case clipped characters for each language, and used those to compute the offset and height. Different fonts have different worst case clipping characters.
I have a UITextView set with some text with both chinese and latin characters.
As you can see, the lineSpace is different depending if there is a latin character or not in the line above. Obviously, I need to remove this difference and to equalize the lineSpace.
Here are the statements :
I work with an AttributedString
The font is different depending if the character is latin or chinese
The point above needs to stay true
The lineHeight is different between the 2 fonts
I almost sure than the reason of my problem comes from this last point but since the lineHeight of the font is not editable, I'm a bit stuck.
I already try to play with those properties
style.minimumLineHeight = mylineSpacing;
style.maximumLineHeight = mylineSpacing;
style.lineHeightMultiple = mylineSpacing;
style.lineSpacing = 0;
NSDictionary *attributes = #{
NSParagraphStyleAttributeName : style
};
It doesn't work. I'm thinking about using CoreText or even CoreGraphic to redraw all the characters one by one but maybe there is an easier solution. And if not, which Kit should I use ?
PS : I'm not chinese and I used random chinese text for this screenshot and I apologize to an eventual chinese stackoverflow user if my text looks stupid / no sense / or offending in any way.
I'm working on a view which uses TextKit framework to typeset text in columns like this:
I use my UIView's bounds with edge insets (black rectangle) to compute 10 CGRects which I then transform into NSTextContainers (red rectangles). In drawRect: I pass those to the NSLayoutManager which typesets and draws the glyphs for me.
My question is: How can I compute the number of columns required? I can draw a constant number of columns but with varying text lengths, I need to adjust the number of columns programmatically.
I found the method [NSLayoutManager glyphRangeForTextContainer:] which returns range of length 0 when the text container is to be left empty. Therefore, I could loop to create text containers and use this method to determine if more containers are needed. However, this method is said to be inefficient as it triggers the layout computation and I'm not happy running it in a loop perhaps hundreds of times over.
There has to be a better way!
Thanks for your answers, Pete.
Well, after some digging through the TextKit framework I've finally found the answer.
My code works in a loop like this:
while ([self needsMoreColumns]) {
[self addColumn];
}
...
- (BOOL)needsMoreColumns {
// Always create at least one column
if (self.layoutManager.textContainers.count == 0)
return YES;
// Find out the glyph range of the last column
NSRange range = [self.layoutManager glyphRangeForTextContainer:[self.layoutManager.textContainers lastObject]];
NSUInteger glyphs = [self.layoutManager numberOfGlyphs];
// Compare it with the number of glyphs
return range.location + range.length < glyphs;
}
I didn't include the method [self addColumn] as it's a no brainer. It simply uses the geometry of my layout and position of the last column (if any) to compute the CGRect of the next one. Then, it creates NSTextContainer with respective size and stores the origin property of the rectangle in a dedicated array for drawing purposes.
I've also discovered methods [NSLayoutManager firstUnlaidCharacterIndex] and [NSLayoutManager firstUnlaidGlyphIndex] but they don't seem to work as expected. After laying out three columns worth of text in only one column, they returned the length of the entire string and not the position of the first character which didn't fit into the first column. That's why I rather used the range-based approach.
That's all folks, be safe!
Pete.
I have some UILabels next to each other horizontally which will contain names.
I can't find any way of getting the text to be truncated at a character boundary - I've tried a line break mode with NSLineBreakByCharWrapping but the characters get chopped off in the middle
I've tried the other line break modes but can't get rid of the partial character (x in this particular example), I don't want part of a character displayed obviously as it looks no good.
Its not feasible to limit the output to a hard coded number of characters because iiiiiiii is a totally different width to wwwwwwww for example.
Also I don't want … appearing within the text because as its narrow then there would be too few characters left if part of the available space is consumed with ...
contactItem.name.frame = nameFrame;
contactItem.name.font = [UIFont systemFontOfSize:12];
contactItem.name.textColor = [UIColor whiteColor];
contactItem.name.textAlignment = NSTextAlignmentCenter;
contactItem.name.lineBreakMode = NSLineBreakByCharWrapping;
[self.scrollView addSubview: contactItem.name];
contactItem.name is the UILabel(s). nameFrame is getting horizontally incremented for each contactItem.
Maybe calculate text size yourself and react when it's too long?
Calculating UILabel Text Size
I don't know if this the simplest solution for this problem - but for sure it's the working solution.
I have a scenario where I will have to draw a text on a UIView. For which my code is
- (void)drawRect:(CGRect)iRect {
[super drawRect:iRect];
CGContextRef aContext = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(aContext, [[UIColor greenColor] CGColor]);
[#"s" drawInRect:CGRectMake(0, 0, iRect.size.width, iRect.size.height) withFont:[UIFont systemFontOfSize:500] lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
With this code I can achieve:
However, since I'm using a FontSize of 500, the text lineWidth is big.
I would like to reduce the lineWidth to 1px.
Please advice on how to achieve this.
Is there another method apart from the conventional drawInRect.
Will CoreText help me get the solution
Thanks.
EDIT
Using UIBezierPath for the Glyph, I was able to get the Path of the Character. But the path that is achieved is through the border of the character. hence, I get the text as:
Can this be made a single line ?
Even though it looks as if the letter S is drawn as a line of a certain width, that's not the case. If you look closer, then you'll see that the line width varies. It's in fact a complex graphical construct defined by it's outline. It's even more obvious if you use a font with serifs. As a consequence, there is no such thing as a text line width.
The best solution probably is to use a different font that has the look as if it was drawn using a narrow pen. I'm not sure if such a font is preinstalled on iOS.