Emoji + Custom fonts != Love

  • Articles
  • iOS
  • Mobile

So you’ve just added a fancy custom font to your application and you’re really happy with how it looks. You’ve created your awesome app, finished all the layouts and UI elements, and then based on some user input (entering a title for something, or a comment on a photo, or whatever your app does) you get some Emoji’s inside your perfectly sized UILabels.

They’ve come to mess up your Feng Shui, all their little yellow heads are cut off, they overlap each other if there are two or more lines of text, a lot of nasty stuff. Oh the horror!

emoji1

You make yourself a fresh cup of coffee and think about how you can fix this. Should you go back into the storyboards and code and start increasing the height of every UILabel that could possible contain emojis? But that probably won’t fix the overlap from the multiple lines of text. Should you just give up on using your fancy custom font and go back to Helvetica? Never!

What you could do is try to change the font size of just those emoji characters. To do that would mean to use NSAttributedString instead of just NSString but it’s way better and quicker than the other ideas. We’ll create a category over NSString that will allow us to get a NSAttributedString with lower font sizes for the emoji characters.

We first need to find out where the emojis are inside the NSString. For that we’ll create a function that will return a NSArray that contains the NSRange for the emojis:

- (NSArray *)rangesForEmojis
{
    __block NSMutableArray *ranges = [[NSMutableArray alloc] init];
    [self enumerateSubstringsInRange:NSMakeRange(0, [self length]) options:NSStringEnumerationByComposedCharacterSequences usingBlock:
     ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
         BOOL isEmoji = NO;
         const unichar hs = [substring characterAtIndex:0];
         // surrogate pair
         if (0xd800 <= hs && hs <= 0xdbff) {
             if (substring.length > 1) {
                 const unichar ls = [substring characterAtIndex:1];
                 const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
                 if (0x1d000 <= uc && uc <= 0x1f77f) {
                     isEmoji = YES;
                 }
             }
         } else if (substring.length > 1) {
             const unichar ls = [substring characterAtIndex:1];
             if (ls == 0x20e3) {
                 isEmoji = YES;
             }
             
         } else {
             // non surrogate
             if (0x2100 <= hs && hs <= 0x27ff) {
                 isEmoji = YES;
             } else if (0x2B05 <= hs && hs <= 0x2b07) {
                 isEmoji = YES;
             } else if (0x2934 <= hs && hs <= 0x2935) {
                 isEmoji = YES;
             } else if (0x3297 <= hs && hs <= 0x3299) {
                 isEmoji = YES;
             } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) {
                 isEmoji = YES;
             }
         }
         
         if (isEmoji) {
             [ranges addObject:[NSValue valueWithRange:substringRange]];
         }
     }];
    
    return ranges;
}

Then we create another function that will use these NSRanges and create the NSAttributedString and set the font attribute for the emojis to a font smaller by 3 points:

- (NSAttributedString *)attributedStringWithFixedEmojisForFont:(UIFont *)font
{
    NSMutableAttributedString *attributedStringWithFixedEmojis = [[NSMutableAttributedString alloc] init];
    
    [attributedStringWithFixedEmojis appendAttributedString:[[NSAttributedString alloc] initWithString:self attributes:@{NSFontAttributeName : font}]];
    
    NSArray *emojiRanges = [self rangesForEmojis];
    if (emojiRanges && emojiRanges.count > 0) {
        for (NSValue *emojiRangeValue in emojiRanges) {
            NSRange emojiRange = [emojiRangeValue rangeValue];

            [attributedStringWithFixedEmojis setAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:(font.pointSize - 3)]}
                                                     range:emojiRange];
        }
    }
    
    return attributedStringWithFixedEmojis;
}

So all you need to do now is instead of calling:

self.chatTitleLabel.text = chat.name;

we’ll call:

self.chatTitleLabel.attributedText = [chat.name attributedStringWithFixedEmojisForFont:self.chatTitleLabel.font];

emoji2

But that’s not all! If you go and try this now you might see that sometimes the text will start to shrink progressively, each time your UITableView is refreshed or each time you set a different text to the UILabel.

It might seem crazy at first but apparently when you set the attributedText property on an UILabel, it will change the entire UILabel’s font to that of the first character in your NSAttributedString. And because we’re using the UILabel’s own font size to calculate the font size of the emojis, it ends up shrinking more and more. So if you start with a font size of 15, because we set the emojis to have a font lower by 3 points, the next time you set the attributedText property your UILabel might have a font of 12. Then the next time it will have a font of 9, and so on.

emoji3

emoji4

emoji5

emoji6

 

To fix this we’re going to have to make sure that the first character in the NSAttributedString will have the correct font size, but what can we use and still have the string look exactly the same? After a quick search, we’ve found an Unicode character called “ZERO WIDTH SPACE” with a code of “\u200B”.

We can insert this character as the first character in the NSAttributedString that we generate. We’ll also have to move the emojis NSRange location over by 1, because we inserted that character and everything got shifted to the right.
So our function inside the NSString category will look like this:

- (NSAttributedString *)attributedStringWithFixedEmojisForFont:(UIFont *)font
{
    NSMutableAttributedString *attributedStringWithFixedEmojis = [[NSMutableAttributedString alloc] init];
    
    [attributedStringWithFixedEmojis appendAttributedString:[[NSAttributedString alloc] initWithString:@"\u200B" attributes:@{NSFontAttributeName : font}]];
    

    [attributedStringWithFixedEmojis appendAttributedString:[[NSAttributedString alloc] initWithString:self attributes:@{NSFontAttributeName : font}]];
    
    NSArray *emojiRanges = [self rangesForEmojis];
    if (emojiRanges && emojiRanges.count > 0) {
        for (NSValue *emojiRangeValue in emojiRanges) {
            NSRange emojiRange = [emojiRangeValue rangeValue];
            // This needs to be +1 because we just added that "zero width space" at the front
            // So all the ranges in the initial string are off by 1 character
            emojiRange.location += 1;
            [attributedStringWithFixedEmojis setAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:(font.pointSize - 3)]}
                                                     range:emojiRange];
        }
    }
    
    return attributedStringWithFixedEmojis;
}

Ok. Now we’re done, it should all look fine now. But what if we’re too lazy to go through all the code and replace all of our label.text = someString; with label.text = [someString attributedStringWithFixedEmojisForFont:someFont];

We can create a category over UILabel and every time the setText function is called, we’ll actually call our setAttributedText instead. Let’s swizzle that shizzle! 😎

#import "NSString+FixEmoji.h"
#import <objc/runtime.h>

@implementation UILabel (EmojiFix)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(setText:);
        SEL swizzledSelector = @selector(xxx_setText:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)xxx_setText:(NSString *)text
{
    // This won't cause an infinite loop
    // Because of the swizzle above, if we call [self xxx_setText:] it actually calls the initial normal setText
    
    if ([text rangesForEmojis].count == 0) {
        [self xxx_setText:text];
    } else {
        [self setAttributedText:[text attributedStringWithFixedEmojisForFont:self.font]];
    }
}

@end

 

Author:

Răzvan Chichirău