4

I need to use the iOS's system font "Hiragino Sans W3" in my app, but no matter which size/style or the UILabel's dimensions I choose, the font's ascenders and descenders always appear clipped:

enter image description here

It seems that this can this fixed by creating a subclass of UILabel and overwriting method textRectForBounds:limitedToNumberOfLines: to return the "correct" value. So the following code...

- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines
{
    CGRect result = [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines];
    result = CGRectInset(result, 0, -5);
    return result;
}

...results in both the ascender and descenders not being clipped anymore:

enter image description here

I know it's also possible to adjust font ascender and descender positions using an external editor. But this is a system font, shouldn't it work correctly without any modifications? Is there something I'm missing here?

Thanks in advance for any answers.

DPR
  • 770
  • 1
  • 11
  • 29
  • 1
    I wish I had a better answer for you but all I can say is I was able to reproduce this and agree that it is unexpected. You may want to file a bug report with Apple. – allocate Jun 06 '17 at 05:00
  • So I ended up sending a bug report to Apple. It was eventually closed with the justification that this is "by design". According to Apple, the label will draw the tone marks (or any other ascenders/descenders/diacritics) if .clipsToBounds = false. But sizing the label will not take these into account, and this is the correct behaviour. Personally, this explanation doesn't make much sense to me... – DPR Aug 21 '18 at 09:51

3 Answers3

3

Swift version using method swizzling:

extension UIFont {

    static func swizzle() {
        method_exchangeImplementations(
                class_getInstanceMethod(self, #selector(getter: ascender))!,
                class_getInstanceMethod(self, #selector(getter: swizzledAscender))!
        )
        method_exchangeImplementations(
                class_getInstanceMethod(self, #selector(getter: lineHeight))!,
                class_getInstanceMethod(self, #selector(getter: swizzledLineHeight))!
        )
    }

    private var isHiragino: Bool {
        fontName.contains("Hiragino")
    }

    @objc private var swizzledAscender: CGFloat {
        if isHiragino {
            return self.swizzledAscender * 1.15
        } else {
            return self.swizzledAscender
        }
    }

    @objc private var swizzledLineHeight: CGFloat {
        if isHiragino {
            return self.swizzledLineHeight * 1.25
        } else {
            return self.swizzledLineHeight
        }
    }

}

Call UIFont.swizzle() once, for example in application(_:didFinishLaunchingWithOptions:).

H.D.
  • 1,133
  • 9
  • 9
2

I think this issue really boils down to the font itself ("Hiragino Sans"). Using a font editor it is possible to see that the glyphs go beyond the ascender and descender values, which is what iOS seems to assume as the vertical "bounding box" for displayed text.

For the lack of a better solution, I've been using a (pretty ugly) hack which modifies the values for UIFont read-only properties ascender and lineHeight.

File UIFont+Alignment.h:

#import <UIKit/UIKit.h>

@interface UIFont (Alignment)

- (void)ensureCorrectFontAlignment;

@end

File UIFont+Alignment.m:

#import "UIFont+Alignment.h"
#import <objc/runtime.h>
#import <objc/message.h>

static NSHashTable *adjustedFontsList;

@implementation UIFont (Alignment)

- (void)ensureCorrectFontAlignment
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        adjustedFontsList = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
    });

    @synchronized (adjustedFontsList) {

        if ([adjustedFontsList containsObject:self]) {
            return;
        }
        else if ([self.fontName containsString:@"Hiragino"]) {

            SEL originalAscenderSelector = @selector(ascender);
            Method originalAscenderMethod = class_getInstanceMethod([UIFont class], originalAscenderSelector);
            SEL originalLineHeightSelector = @selector(lineHeight);
            Method originalLineHeightMethod = class_getInstanceMethod([UIFont class], originalLineHeightSelector);

            id result = method_invoke(self, originalAscenderMethod, nil);
            CGFloat originalValue = [[result valueForKey:@"ascender"] floatValue];
            [result setValue:@(originalValue * 1.15) forKey:@"ascender"];

            result = method_invoke(self, originalLineHeightMethod, nil);
            originalValue = [[result valueForKey:@"lineHeight"] floatValue];
            [result setValue:@(originalValue * 1.25) forKey:@"lineHeight"];

            [adjustedFontsList addObject:self];
        }
    }
}

@end

To apply, just import the header and invoke [myUIFont ensureCorrectFontAlignment] after creating a new font instance.

DPR
  • 770
  • 1
  • 11
  • 29
1

@DPR 's answer helped me, but with it I had an error

Too many arguments to function call, expected 0, have 2 method_invoke

I replaced

id result = method_invoke(self, originalAscenderMethod, nil);

with

id (*function)(id, Method) = (id (*)(id, Method)) method_invoke; id result = function(self, originalAscenderMethod);

And

result = method_invoke(self, originalLineHeightMethod, nil);

with

result = function(self, originalLineHeightMethod);

Yeesha
  • 131
  • 1
  • 4