System font "Hiragino Sans" is show with clipped ascender and descenders
Asked Answered
C

3

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.

Cajun answered 5/6, 2017 at 17:25 Comment(2)
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.Deon
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...Cajun
C
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.

Cajun answered 6/6, 2017 at 18:43 Comment(0)
R
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:).

Remise answered 13/1, 2020 at 4:58 Comment(2)
You guys genius! For me, working with font BravuraText in SwiftUI, "swizzledAscender * 1.5, swizzledLineHeight * 1.6" makes perfect.Madelainemadeleine
@Madelainemadeleine thanks for linking me up with this.Bambibambie
C
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.

Cajun answered 6/6, 2017 at 18:43 Comment(0)
O
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);

Overcloud answered 21/8, 2018 at 5:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.