Kerning in iOS UITextView
Asked Answered
S

6

23

For what apparently is a 'bug,' UITextView does not seem to support kerning like other UIKit Elements. Is there a workaround to get kerning working?

To be clear, I'm talking about the spacing in between pairs of characters that are defined in the font file, not the general spacing between all characters. Using an NSAttributedString with the NSKernAttributeName will adjust the spacing between all characters, but the built in character pairs still don't work.

For Example:

enter image description here

Is there a workaround to fix this?

One simple workaround I have discovered is to add css styles to the UITextView which enable kerning. If I use the undocumented method setContentToHTMLString: I can inject the css to the secret webview within the text view.

NSString *css = @"*{text-rendering: optimizeLegibility;}";
NSString *html = [NSString stringWithFormat:@"<html><head><style>%@</style></head><body>Your HTML Text</body></html>", css];
[textView performSelector:@selector(setContentToHTMLString:) withObject:html];

This fixes the problem immediately; however, it seems very likely this will get the app rejected. Is there a safe way to sneak some css into the text view?

Other workarounds I have experimented with include:

Using a webview and the contenteditable property. This isn't ideal because webview does a bunch of extra stuff that gets in the way, for example the input accessory view which can't easily be hidden.

Subclassing a UITextView and rendering text manually with core text. This is more complex than I'd hoped because all the selections stuff needs to be reconfigured as well. Because of UITextView's private subclasses of UITextPosition and UITextRange this is a huge pain in the ass if not completely impossible.

Stockwell answered 5/11, 2012 at 19:48 Comment(5)
You could use NSAttributedStrings. Would only work if you can require iOS6 though.Gonocyte
Good call. I forgot to also mention that due to a 'bug' kerning does not work with an NSAttributedString in a UITextView.Stockwell
When you say it's a bug, does that mean you've opened a radar on it at bugreport.apple.com?Faitour
If so, can you post it so we can dupe?Faitour
What's the attributed string you are trying? I my experiment I was able to specify a negative value for the kerning value attribute to switch the letters together more: [attributedString addAttribute:NSKernAttributeName value:[NSNumber numberWithFloat:-10] range:NSMakeRange(0, 3)];Humboldt
L
37

You are right that your app would probably be rejected by using @selector(setContentToHTMLString:). You can however use a simple trick to construct the selector dynamically so that it will not be detected during validation.

NSString *css = @"*{text-rendering: optimizeLegibility;}";
NSString *html = [NSString stringWithFormat:@"<html><head><style>%@</style></head><body>Your HTML Text</body></html>", css];
@try {
    SEL setContentToHTMLString = NSSelectorFromString([@[@"set", @"Content", @"To", @"HTML", @"String", @":"] componentsJoinedByString:@""]);
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [textView performSelector:setContentToHTMLString withObject:html];
    #pragma clang diagnostic pop
}
@catch (NSException *exception) {
    // html+css could not be applied to text view, so no kerning
}

By wrapping the call inside a @try/@catch block, you also ensure that your app will not crash if the setContentToHTMLString: method is removed in a future version of iOS.

Using private APIs is generally not recommended, but in this case I think it totally makes sense to use a small hack vs rewriting UITextView with CoreText.

Leash answered 14/11, 2012 at 21:43 Comment(5)
Very clever. And sneaky. :-)Derma
Note that checking -respondsToSelector: is a better check for the method being removed than a @try/@catch block. The exception handler is more appropriate to counter you potentially passing in something the API doesn't handleDyke
Thanks @0xced, nice trick. I had assumed that in the app store review process they run the application in an environment which tracks all api calls so they can check even for dynamically generated messages. Can anyone confirm that this will sneak through?Stockwell
I have apps that shipped using this trick--but I used -respondsToSelector:, not a @try/@catchConoid
and in fact I didn't obfuscate the selector, I simply used NSSelectorFromString(selectorName)Conoid
H
3

After researching this a bit I found that the reason for the missing Kerning is that UITextView internally uses a UIWebDocumentView which has Kerning turned off by default.

Some infos about the inner workings of UITextView: http://www.cocoanetics.com/2012/12/uitextview-caught-with-trousers-down/

Unfortunately there is no sanctioned method to enable Kerning, I would definitely advise against using a hack using a camouflaged selector.

Here's my Radar, I suggest you dupe it: http://www.cocoanetics.com/2012/12/radar-uitextview-ignores-font-kerning/

In this I argue that when setting text on UITextView via setAttributedText we developers expect for Kerning to be on by default because that is how it would most closely match output of rendering the text via CoreText.

Humboldt answered 16/12, 2012 at 18:54 Comment(0)
H
1

Try this:

[textView_ setValue:@"hello, <b>world</b>" forKey:@"contentToHTMLString"];

It works in my test. I haven't tried it in the app store of course.

Heliochrome answered 15/11, 2012 at 6:36 Comment(0)
S
0

Have you taken a look here: https://github.com/AliSoftware/OHAttributedLabel? Looking at this project, it appears that there are several apps in the app store that are using markup (provided by this class). Perhaps you could either use this class or glean some implementation ideas from it. Perhaps your app as-is will pass muster.

Selfrespect answered 9/11, 2012 at 15:48 Comment(1)
Thanks Mark, I have taken a look at that project. I think it's relatively moot now that most of the UIKit elements support attributed strings. I also need multi-line text input, so I wasn't able to glean much from it.Stockwell
D
0

With iOS6, UITextView has a new attribute attributedText, to which you can assign an NSAttributedString. I have not tried it, but it would be interesting to see if you get the kerning you are after if you set this attribute of your text view rather than the text attribute. And it has the added benefit of being a public interface.

Derma answered 14/11, 2012 at 21:51 Comment(1)
That's true, NSAttributedString's work well for most text formatting, but as I mentioned in the question, due to what is presumably a bug, kerning is broken in UITextViews. While you can use the attributed string property to adjust the kerning for particular paris, there is no way to enable the font's built in kerning.Stockwell
I
-1

There’s no need to resort to private methods. You can easily fake it by dynamically changing the attributedText property of the text field each time the text changes by subscribing to the UITextFieldTextDidChangeNotification notification:

- (void)viewDidLoad {
    // your regular stuff goes here
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChangeNotification:) name:UITextFieldTextDidChangeNotification object:nil];
}


- (void)textDidChangeNotification:(NSNotification *)notification {
    // you can of course specify other attributes here, such as font and text color
    CGFloat kerning = -3.; // change accordingly to your desired value
    NSDictionary *attributes = @{
                                 NSKernAttributeName: @(kerning),
                                 };
    UITextField *textField = [notification object];
    textField.attributedText = [[NSAttributedString alloc] initWithString:textField.text attributes:attributes];
}
Intense answered 14/3, 2014 at 15:42 Comment(2)
This answer does not address the question which is about using the font's built in kerning, not about how to set kerning values for specific pairs. Setting the kern attribute to a number as you've suggested would adjust the tracking of the whole line of text.Stockwell
Good answer. I don't know why this was down voted but +1 always use non-private API and this worked clean. There may be down points with this but for me this worked well. Thank you!Sealer

© 2022 - 2024 — McMap. All rights reserved.