How to Use a Custom Font with Dynamic Text Sizes in iOS7

How to use a custom font with dynamic text sizes in iOS7

Behind the scenes of that API, apple has some sort of lookup table that returns a specific font family, size, and sometimes symbolic traits (like bold) that (e.g. UIFontTextStyleHeadline) and the user's preferred text size. The latter is a string pulled off of the sharedApplication like this:

[UIApplication sharedApplication].preferredContentSizeCategory;

(I logged out all the default sizes/fonts/traits for Helvetica-Neue for the various dynamic text sizes). We've since added handling for the accessibility sizes, which is important.

So all you really have to do is build a similar lookup table. Our designer created a simple spreadsheet for me:

Font size lookup table

Notice that we added a couple styles (caption 3 & 4) to have 8 instead of 6 to choose from.

Then you'll want to put it someplace convenient, like a category on UIFontDescriptor. You'll want your method to return a UIFontDescriptor like Apple's API, so that it's still easy to adjust with symbolic traits, etc.

My category looks like this:

UIFontDescriptor+AvenirNext.h

#import <UIKit/UIKit.h>

extern NSString *const ANUIFontTextStyleCaption3;

@interface UIFontDescriptor (AvenirNext)

+(UIFontDescriptor *)preferredAvenirNextFontDescriptorWithTextStyle:(NSString *)style;

@end

UIFontDescriptor+AvenirNext.m

#import "UIFontDescriptor+AvenirNext.h"

NSString *const ANUIFontTextStyleCaption3 = @"ANUIFontTextStyleCaption3";
NSString *const ANUIFontTextStyleCaption4 = @"ANUIFontTextStyleCaption4";

@implementation UIFontDescriptor (AvenirNext)
+(UIFontDescriptor *)preferredAvenirNextFontDescriptorWithTextStyle:(NSString *)style {
static dispatch_once_t onceToken;
static NSDictionary *fontSizeTable;
dispatch_once(&onceToken, ^{
fontSizeTable = @{
UIFontTextStyleHeadline: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @26,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @25,
UIContentSizeCategoryAccessibilityExtraLarge: @24,
UIContentSizeCategoryAccessibilityLarge: @24,
UIContentSizeCategoryAccessibilityMedium: @23,
UIContentSizeCategoryExtraExtraExtraLarge: @23,
UIContentSizeCategoryExtraExtraLarge: @22,
UIContentSizeCategoryExtraLarge: @21,
UIContentSizeCategoryLarge: @20,
UIContentSizeCategoryMedium: @19,
UIContentSizeCategorySmall: @18,
UIContentSizeCategoryExtraSmall: @17,},

UIFontTextStyleSubheadline: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @24,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @23,
UIContentSizeCategoryAccessibilityExtraLarge: @22,
UIContentSizeCategoryAccessibilityLarge: @22,
UIContentSizeCategoryAccessibilityMedium: @21,
UIContentSizeCategoryExtraExtraExtraLarge: @21,
UIContentSizeCategoryExtraExtraLarge: @20,
UIContentSizeCategoryExtraLarge: @19,
UIContentSizeCategoryLarge: @18,
UIContentSizeCategoryMedium: @17,
UIContentSizeCategorySmall: @16,
UIContentSizeCategoryExtraSmall: @15,},

UIFontTextStyleBody: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @21,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @20,
UIContentSizeCategoryAccessibilityExtraLarge: @19,
UIContentSizeCategoryAccessibilityLarge: @19,
UIContentSizeCategoryAccessibilityMedium: @18,
UIContentSizeCategoryExtraExtraExtraLarge: @18,
UIContentSizeCategoryExtraExtraLarge: @17,
UIContentSizeCategoryExtraLarge: @16,
UIContentSizeCategoryLarge: @15,
UIContentSizeCategoryMedium: @14,
UIContentSizeCategorySmall: @13,
UIContentSizeCategoryExtraSmall: @12,},

UIFontTextStyleCaption1: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @19,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @18,
UIContentSizeCategoryAccessibilityExtraLarge: @17,
UIContentSizeCategoryAccessibilityLarge: @17,
UIContentSizeCategoryAccessibilityMedium: @16,
UIContentSizeCategoryExtraExtraExtraLarge: @16,
UIContentSizeCategoryExtraExtraLarge: @16,
UIContentSizeCategoryExtraLarge: @15,
UIContentSizeCategoryLarge: @14,
UIContentSizeCategoryMedium: @13,
UIContentSizeCategorySmall: @12,
UIContentSizeCategoryExtraSmall: @12,},

UIFontTextStyleCaption2: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @18,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @17,
UIContentSizeCategoryAccessibilityExtraLarge: @16,
UIContentSizeCategoryAccessibilityLarge: @16,
UIContentSizeCategoryAccessibilityMedium: @15,
UIContentSizeCategoryExtraExtraExtraLarge: @15,
UIContentSizeCategoryExtraExtraLarge: @14,
UIContentSizeCategoryExtraLarge: @14,
UIContentSizeCategoryLarge: @13,
UIContentSizeCategoryMedium: @12,
UIContentSizeCategorySmall: @12,
UIContentSizeCategoryExtraSmall: @11,},

ANUIFontTextStyleCaption3: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @17,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @16,
UIContentSizeCategoryAccessibilityExtraLarge: @15,
UIContentSizeCategoryAccessibilityLarge: @15,
UIContentSizeCategoryAccessibilityMedium: @14,
UIContentSizeCategoryExtraExtraExtraLarge: @14,
UIContentSizeCategoryExtraExtraLarge: @13,
UIContentSizeCategoryExtraLarge: @12,
UIContentSizeCategoryLarge: @12,
UIContentSizeCategoryMedium: @12,
UIContentSizeCategorySmall: @11,
UIContentSizeCategoryExtraSmall: @10,},

UIFontTextStyleFootnote: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @16,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @15,
UIContentSizeCategoryAccessibilityExtraLarge: @14,
UIContentSizeCategoryAccessibilityLarge: @14,
UIContentSizeCategoryAccessibilityMedium: @13,
UIContentSizeCategoryExtraExtraExtraLarge: @13,
UIContentSizeCategoryExtraExtraLarge: @12,
UIContentSizeCategoryExtraLarge: @12,
UIContentSizeCategoryLarge: @11,
UIContentSizeCategoryMedium: @11,
UIContentSizeCategorySmall: @10,
UIContentSizeCategoryExtraSmall: @10,},

ANUIFontTextStyleCaption4: @{
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @15,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @14,
UIContentSizeCategoryAccessibilityExtraLarge: @13,
UIContentSizeCategoryAccessibilityLarge: @13,
UIContentSizeCategoryAccessibilityMedium: @12,
UIContentSizeCategoryExtraExtraExtraLarge: @12,
UIContentSizeCategoryExtraExtraLarge: @11,
UIContentSizeCategoryExtraLarge: @11,
UIContentSizeCategoryLarge: @10,
UIContentSizeCategoryMedium: @10,
UIContentSizeCategorySmall: @9,
UIContentSizeCategoryExtraSmall: @9,},
};
});


NSString *contentSize = [UIApplication sharedApplication].preferredContentSizeCategory;
return [UIFontDescriptor fontDescriptorWithName:[self preferredFontName] size:((NSNumber *)fontSizeTable[style][contentSize]).floatValue];
}
+(UIFontDescriptor *)preferredAvenirNextDemiBoldFontDescriptorWithTextStyle:(NSString *)style {
return [[self preferredAvenirNextFontDescriptorWithTextStyle:style] fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
}

+(UIFontDescriptor *)preferredAvenirNextBoldFontDescriptorWithTextStyle:(NSString *)style {
return [UIFontDescriptor fontDescriptorWithName:[self preferredBoldFontName] size:[self preferredAvenirNextFontDescriptorWithTextStyle:style].pointSize];
}

+(NSString *)preferredFontName {
return @"AvenirNext-Medium";
}
+(NSString *)preferredBoldFontName {
return @"AvenirNext-Bold";
}

@end

We chose to use the same base font AvenirNext-Medium, and then bold and such via symbolic traits, but you could get crazy and specify different weight variants on your font as part of your lookup table as well if you wanted, like AvenirNext-ExtraBold.

That's all there is to it! We use it like this:

[UIFont fontWithDescriptor:[UIFontDescriptor preferredAvenirNextFontDescriptorWithTextStyle:UIFontTextStyleHeadline] size: 0]

System Font with custom size for text style

Following Font function helps achieve custom font size relative to TextStyle:

static func custom(
_ name: String,
size: CGFloat,
relativeTo textStyle: Font.TextStyle
) -> Font

How to honor Dynamic Type Accessibility Sizes with a custom font in an iOS storyboard

Although you can't specify both a custom font and a preferred text style via Storyboard, it's not difficult to programmatically specify a dynamic type size for your custom font:

Swift:

let pointSize  = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFont‌​TextStyleTitle1).poi‌​ntSize
let customFont = UIFont(name: "Chalkboard SE", size: pointSize)

When you receive a UIContentSizeCategoryDidChangeNotification, use the same code to update your label's font.

Obj C:

 CGFloat pointSize = [[UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleHeadline] pointSize];
[titleLabel setFont:[UIFont fontWithName:@"Marker Felt" size:pointSize]];

A better way to use Dynamic Type with a custom font in Swift 3/iOS10


The problem with this method is that the text size won't change until the user goes back to the app, and then the user would see the old text size change to the new size, which is not ideal.

I share your thoughts that this would probably be a better UX, but I guess you are overthinking it a bit.

If you have a look at system provided Apps (e.g. Contacts) you will clearly see that the refresh is not done until the user goes back to the app, too.

By the way, you could refactor your code a bit for Swift 3:

extension UIFontDescriptor {

private struct SubStruct {
static var preferredFontName: String = "Avenir-medium"
}

static let fontSizeTable: [UIFontTextStyle: [UIContentSizeCategory: CGFloat]] = [
.headline: [
.accessibilityExtraExtraExtraLarge: 23,
.accessibilityExtraExtraLarge: 23,
.accessibilityExtraLarge: 23,
.accessibilityLarge: 23,
.accessibilityMedium: 23,
.extraExtraExtraLarge: 23,
.extraExtraLarge: 21,
.extraLarge: 19,
.large: 17,
.medium: 16,
.small: 15,
.extraSmall: 14
],
.subheadline: [
.accessibilityExtraExtraExtraLarge: 21,
.accessibilityExtraExtraLarge: 21,
.accessibilityExtraLarge: 21,
.accessibilityLarge: 21,
.accessibilityMedium: 21,
.extraExtraExtraLarge: 21,
.extraExtraLarge: 19,
.extraLarge: 17,
.large: 15,
.medium: 14,
.small: 13,
.extraSmall: 12
],
.body: [
.accessibilityExtraExtraExtraLarge: 53,
.accessibilityExtraExtraLarge: 47,
.accessibilityExtraLarge: 40,
.accessibilityLarge: 33,
.accessibilityMedium: 28,
.extraExtraExtraLarge: 23,
.extraExtraLarge: 21,
.extraLarge: 19,
.large: 17,
.medium: 16,
.small: 15,
.extraSmall: 14
],
.caption1: [
.accessibilityExtraExtraExtraLarge: 18,
.accessibilityExtraExtraLarge: 18,
.accessibilityExtraLarge: 18,
.accessibilityLarge: 18,
.accessibilityMedium: 18,
.extraExtraExtraLarge: 18,
.extraExtraLarge: 16,
.extraLarge: 14,
.large: 12,
.medium: 11,
.small: 11,
.extraSmall: 11
],
.caption2: [
.accessibilityExtraExtraExtraLarge: 17,
.accessibilityExtraExtraLarge: 17,
.accessibilityExtraLarge: 17,
.accessibilityLarge: 17,
.accessibilityMedium: 17,
.extraExtraExtraLarge: 17,
.extraExtraLarge: 15,
.extraLarge: 13,
.large: 11,
.medium: 11,
.small: 11,
.extraSmall: 11
],
.footnote: [
.accessibilityExtraExtraExtraLarge: 19,
.accessibilityExtraExtraLarge: 19,
.accessibilityExtraLarge: 19,
.accessibilityLarge: 19,
.accessibilityMedium: 19,
.extraExtraExtraLarge: 19,
.extraExtraLarge: 17,
.extraLarge: 15,
.large: 13,
.medium: 12,
.small: 12,
.extraSmall: 12
]
]

final class func preferredDescriptor(textStyle: UIFontTextStyle) -> UIFontDescriptor {
let contentSize = UIApplication.shared.preferredContentSizeCategory
let style = fontSizeTable[textStyle]!
return UIFontDescriptor(name: SubStruct.preferredFontName, size: style[contentSize]!)
}
}

No need to cast to NSDictionary or NSNumber and get the floatValue indirectly.

This way your call site can use the following, more readable code:

func userChangedTextSize(notification: NSNotification) {
label.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body), size: 0)

}

Edit: As I am working on the same right now, I improved the above (on SO commonly seen solution) to something way easier.

import UIKIt

extension UIFont {

private struct CustomFont {
static var fontFamily = "Avenir"
}

/// Returns a bold version of `self`
public var bolded: UIFont {
return fontDescriptor.withSymbolicTraits(.traitBold)
.map { UIFont(descriptor: $0, size: 0) } ?? self
}

/// Returns an italic version of `self`
public var italicized: UIFont {
return fontDescriptor.withSymbolicTraits(.traitItalic)
.map { UIFont(descriptor: $0, size: 0) } ?? self
}

/// Returns a scaled version of `self`
func scaled(scaleFactor: CGFloat) -> UIFont {
let newDescriptor = fontDescriptor.withSize(fontDescriptor.pointSize * scaleFactor)
return UIFont(descriptor: newDescriptor, size: 0)
}

class func preferredCustomFont(forTextStyle textStyle: UIFontTextStyle) -> UIFont {
// we are using the UIFontDescriptor which is less expensive than creating an intermediate UIFont
let systemFontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)

let customFontDescriptor = UIFontDescriptor.init(fontAttributes: [
UIFontDescriptorFamilyAttribute: CustomFont.fontFamily,
UIFontDescriptorSizeAttribute: systemFontDescriptor.pointSize // use the font size of the default dynamic font
])

// return font of new family with same size as the preferred system font
return UIFont(descriptor: customFontDescriptor, size: 0)
}

}

Usage

func userChangedTextSize(notification: NSNotification) {
label.font = UIFont.preferredCustomFont(forTextStyle: .headline)
// or in Bold / Italic:
// label.font = UIFont.preferredCustomFont(forTextStyle: .headline).bolded
// label.font = UIFont.preferredCustomFont(forTextStyle: .headline).italicized
}

WKWebView and Dynamic Type + Custom Fonts

I've finally managed to solve it, just add this chunk to the CSS

html {
font: -apple-system-body;
}

And leave body like this, with no font-size:

body {
font-family: 'MyCustomFont';
}

With just that, both things would work.

iOS7: Can we use other than Helvetica Neue fonts with Dynamic Type?

As far as I understood [UIFont preferredFontForTextStyle:] returns a font with fixed size for a particular font style regardless of text view default size. I expect that changing text size in Settings will change text sizes in my app by some delta instead of setting fixed value. As noted in Text Programming Guide for iOS,

The actual font used for the purpose described by a text style can vary based on a number of dynamic considerations, including the user’s content size category preference, which is represented by the UIApplication property preferredContentSizeCategory.

I noticed that property preferredContentSizeCategory changes in response to setting text size in Settings.

It’s also important to observe the UIContentSizeCategoryDidChangeNotification so that you can re-layout the text when the user changes the content size category. When your app receives that notification, it should send the invalidateIntrinsicContentSize message to views positioned by Auto Layout or send setNeedsLayout to user interface elements positioned manually. And it should invalidate preferred fonts or font descriptors and acquire new ones as needed.

So, my idea is to observe appropriate notification, calculate size delta based on preferredContentSizeCategory property and apply delta to text view's default font size (which was set in IB or programmatically).


PreferredFontLabel.h

@interface PreferredFontLabel : UILabel

@property (nonatomic) UIFontDescriptor *defaultFontDescriptor;

@end

PreferredFontLabel.m

#import "PreferredFontLabel.h"
#import "UIApplication+ContentSize.h"

@implementation PreferredFontLabel

- (id)init
{
self = [super init];
if (self) {
[self setup];
}
return self;
}

- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self setup];
}
return self;
}

- (void)setup
{
self.defaultFontDescriptor = self.font.fontDescriptor;

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(contentSizeCategoryDidChange)
name:UIContentSizeCategoryDidChangeNotification
object:nil];

[self contentSizeCategoryDidChange];
}

- (void)setDefaultFontDescriptor:(UIFontDescriptor *)defaultFontDescriptor
{
_defaultFontDescriptor = defaultFontDescriptor;

[self contentSizeCategoryDidChange];
}

- (void)contentSizeCategoryDidChange
{
CGFloat preferredSize = [self.defaultFontDescriptor.fontAttributes[UIFontDescriptorSizeAttribute] floatValue];
preferredSize += [UIApplication sharedApplication].contentSizeDelta;

self.font = [UIFont fontWithDescriptor:self.defaultFontDescriptor size:preferredSize];
[self invalidateIntrinsicContentSize];
}

- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
}

@end

UIApplication+ContentSize.h

@interface UIApplication (ContentSize)

@property (nonatomic, readonly) NSInteger contentSizeDelta;

@end

UIApplication+ContentSize.m

#import "UIApplication+ContentSize.h"

@implementation UIApplication (ContentSize)

- (NSInteger)contentSizeDelta
{
static NSArray *contentSizeCategories;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
contentSizeCategories = @[UIContentSizeCategoryExtraSmall,
UIContentSizeCategorySmall,
UIContentSizeCategoryMedium,
UIContentSizeCategoryLarge,
UIContentSizeCategoryExtraLarge,
UIContentSizeCategoryExtraExtraLarge,
UIContentSizeCategoryExtraExtraExtraLarge
UIContentSizeCategoryAccessibilityMedium,
UIContentSizeCategoryAccessibilityLarge,
UIContentSizeCategoryAccessibilityExtraLarge,
UIContentSizeCategoryAccessibilityExtraExtraLarge,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge];
});

// assume UIContentSizeCategoryLarge is default category
NSInteger contentSizeDelta = [contentSizeCategories indexOfObject:self.preferredContentSizeCategory];

if(contentSizeDelta != NSNotFound) {
contentSizeDelta -= [contentSizeCategories indexOfObject:UIContentSizeCategoryLarge];

return contentSizeDelta;
} else {
return 0;
}
}

@end

I added attributed string support, demo is available on GitHub

Keeping texts of different sizes aligned at top as the dynamic type size increases


Any ideas on how to keep them aligned at top even as the Dynamic Type size changes?

I never needed this kind of configuration but your question arose my curiosity. br />
(I keep the same context you provided: 3 Text elements with the same text styles/font and two of them having the same font size)

  • How can I align two Text elements on top with different font sizes? ‍br />
    I took a look at the font metrics to understand the returned values.

Sample Image
If these elements are aligned on top in a HStack view, it's the top edge that's concerned, not the letters nor the numbers inside.
So, I have to move vertically the biggest one to make its top content aligned to the smallest one.

To reach this goal, I used the alignmentGuide method that returns a view modified with respect to its horizontal alignment according to the computation performed in the method's closure by using the ascender and capHeight metrics difference.

  • How do I take into account the Dynamic Type feature in this implementation? br />
    The initial font sizes can be adapted to the user's preferences by using the dynamic property wrapper @ScaledMetrics that scales a numeric value./li>

I implement this information with Xcode 13.4 and iOS 15:/p>

import SwiftUI
import Foundation

struct ContentView: View {

@ScaledMetric(relativeTo: .footnote) var sizeMax: CGFloat=60
@ScaledMetric(relativeTo: .footnote) var sizeMin: CGFloat=24

var body: some View {
HStack(alignment: .top){
Text("$1&")
.font(Font.custom("Helvetica", size: 24,
relativeTo: .footnote))
.background(Color.blue)

Text("a2A")
.font(Font.custom("Helvetica", size: 60,
relativeTo: .footnote))
.alignmentGuide(VerticalAlignment.top){ d in
let customFontDescriptor = UIFontDescriptor.init(fontAttributes: [
UIFontDescriptor.AttributeName.family: "Helvetica",
UIFontDescriptor.AttributeName.textStyle:"footnote"
])

// Values for the font with min size.
let fontMin=UIFont(descriptor: customFontDescriptor,
size: sizeMin)
let ascMin = fontMin.ascender
let capHeightMin = fontMin.capHeight

// Values for the font with max size.
let fontMax=UIFont(descriptor: customFontDescriptor,
size: sizeMax)
let ascMax = fontMax.ascender
let capHeightMax = fontMax.capHeight

return abs((ascMax-capHeightMax)-(ascMin-capHeightMin))
}
.background(Color.gray)

Text("b3B")
.font(Font.custom("Helvetica", size: 24,
relativeTo: .footnote))
.background(Color.red)
}
}
}

Following this rationale, I get the following result to keep texts of different sizes aligned at top as the dynamic type size changes. br>Sample Image
Don't hesitate to adapt this code snippet that fits the example you provided and that's definitely not generic. /p>

Dynamic Font Required in iOS Apps?

Dynamic Type isn't required, no. Compliance with the HIG is a pretty loose requirement itself, considering the HIG is only a guideline.

Despite not being required, seriously consider incorporating it into your app. It's a great feature for accessibility and user-control. There are some great writeups on using Dynamic Type with fonts other than San Francisco, if that's a concern.

Shall I use my preferred font or dynamic font (system) in iOS development

System fonts are best suited for your needs. They can be changed with accessibility parameters, if user wishes to. Your custom ones can't. Moreover, it will have consistent look with the iOS, since you will be using default fonts. You still can configure it, using thin, light, bold weights etc.

I personally stick with system ones if possible.



Related Topics



Leave a reply



Submit