A better way to use Dynamic Type with a custom font in Swift 3/iOS10
Solution 1
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
}
Solution 2
Frederik's Winkelsdorf version fo code but tweaked to use two different Font Family (default one and bold) plus sample of usage.
MyFontExtension.swift
import UIKit
extension UIFontDescriptor {
private struct FontFamily {
static var preferredFontNameRegular: String = "Montserrat-Regular"
static var preferredFontNameBold: String = "Montserrat-Bold"
}
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: UIFont.TextStyle, styleBold: Bool = false) -> UIFontDescriptor {
let contentSize = UIApplication.shared.preferredContentSizeCategory
let fontFamily = styleBold ? FontFamily.preferredFontNameBold : FontFamily.preferredFontNameRegular
guard let style = fontSizeTable[textStyle], let size = style[contentSize] else {
return UIFontDescriptor(name: fontFamily, size: 16)
}
return UIFontDescriptor(name: fontFamily, size: size)
}
}
myViewController.swift
myBoldLabel.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body, styleBold: true), size: 0)
myNormalLabel.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body), size: 0)
Solution 3
Swift 4: Custom scaled Font supporting Accessibility (German BITV)
//UIFont+CustomScaledFont.swift
import UIKit
extension UIFont {
/// Scaled and styled version of any custom Font
///
/// - Parameters:
/// - name: Name of the Font
/// - textStyle: The text style i.e Body, Title, ...
/// - Returns: The scaled custom Font version with the given textStyle
static func scaledFont(name:String, textStyle: UIFont.TextStyle) -> UIFont {
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
guard let customFont = UIFont(name: name, size: fontDescriptor.pointSize) else {
fatalError("Failed to load the \(name) font.")
}
return UIFontMetrics.default.scaledFont(for: customFont)
}
}
After successfully adding custom fonts to your project...
Print available font names:
for family in UIFont.familyNames {
for name in UIFont.fontNames(forFamilyName: family) {
print(name)
}
}
Setup Example:
myLabel.text = "My scaled custom Font"
myLabel.font = UIFont.scaledFont(name: "MyCustomFontName-Bold", textStyle: .title1)
myLabel.adjustsFontForContentSizeCategory = true
Test with Accessibility Inspector (MacOS)
Solution 4
Fred answer but in Swift 5 (I tried to edit the original post but the edit queue is always full):
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: UIFont.TextStyle) -> 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: [
UIFontDescriptor.AttributeName.family: CustomFont.fontFamily,
UIFontDescriptor.AttributeName.size: 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)
}
}
Comments
-
ielyamani almost 2 years
I tried two ways:
Method 1:
label.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body) label.adjustsFontForContentSizeCategory = true
This works fine, even when the preferred text size is changed in the Settings, the text size changes automatically, even before when I go back to the app. But it only works with the system font (San Francisco).
Method 2:
To use a custom font, I add an extension to
UIFontDescriptor
://from this answer http://stackoverflow.com/a/35467158/2907715 extension UIFontDescriptor { private struct SubStruct { static var preferredFontName: String = "Avenir-medium" } static let fontSizeTable : NSDictionary = [ UIFontTextStyle.headline: [ UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 23, UIContentSizeCategory.accessibilityExtraExtraLarge: 23, UIContentSizeCategory.accessibilityExtraLarge: 23, UIContentSizeCategory.accessibilityLarge: 23, UIContentSizeCategory.accessibilityMedium: 23, UIContentSizeCategory.extraExtraExtraLarge: 23, UIContentSizeCategory.extraExtraLarge: 21, UIContentSizeCategory.extraLarge: 19, UIContentSizeCategory.large: 17, UIContentSizeCategory.medium: 16, UIContentSizeCategory.small: 15, UIContentSizeCategory.extraSmall: 14 ], UIFontTextStyle.subheadline: [ UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 21, UIContentSizeCategory.accessibilityExtraExtraLarge: 21, UIContentSizeCategory.accessibilityExtraLarge: 21, UIContentSizeCategory.accessibilityLarge: 21, UIContentSizeCategory.accessibilityMedium: 21, UIContentSizeCategory.extraExtraExtraLarge: 21, UIContentSizeCategory.extraExtraLarge: 19, UIContentSizeCategory.extraLarge: 17, UIContentSizeCategory.large: 15, UIContentSizeCategory.medium: 14, UIContentSizeCategory.small: 13, UIContentSizeCategory.extraSmall: 12 ], UIFontTextStyle.body: [ UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 53, UIContentSizeCategory.accessibilityExtraExtraLarge: 47, UIContentSizeCategory.accessibilityExtraLarge: 40, UIContentSizeCategory.accessibilityLarge: 33, UIContentSizeCategory.accessibilityMedium: 28, UIContentSizeCategory.extraExtraExtraLarge: 23, UIContentSizeCategory.extraExtraLarge: 21, UIContentSizeCategory.extraLarge: 19, UIContentSizeCategory.large: 17, UIContentSizeCategory.medium: 16, UIContentSizeCategory.small: 15, UIContentSizeCategory.extraSmall: 14 ], UIFontTextStyle.caption1: [ UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 18, UIContentSizeCategory.accessibilityExtraExtraLarge: 18, UIContentSizeCategory.accessibilityExtraLarge: 18, UIContentSizeCategory.accessibilityLarge: 18, UIContentSizeCategory.accessibilityMedium: 18, UIContentSizeCategory.extraExtraExtraLarge: 18, UIContentSizeCategory.extraExtraLarge: 16, UIContentSizeCategory.extraLarge: 14, UIContentSizeCategory.large: 12, UIContentSizeCategory.medium: 11, UIContentSizeCategory.small: 11, UIContentSizeCategory.extraSmall: 11 ], UIFontTextStyle.caption2: [ UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 17, UIContentSizeCategory.accessibilityExtraExtraLarge: 17, UIContentSizeCategory.accessibilityExtraLarge: 17, UIContentSizeCategory.accessibilityLarge: 17, UIContentSizeCategory.accessibilityMedium: 17, UIContentSizeCategory.extraExtraExtraLarge: 17, UIContentSizeCategory.extraExtraLarge: 15, UIContentSizeCategory.extraLarge: 13, UIContentSizeCategory.large: 11, UIContentSizeCategory.medium: 11, UIContentSizeCategory.small: 11, UIContentSizeCategory.extraSmall: 11 ], UIFontTextStyle.footnote: [ UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 19, UIContentSizeCategory.accessibilityExtraExtraLarge: 19, UIContentSizeCategory.accessibilityExtraLarge: 19, UIContentSizeCategory.accessibilityLarge: 19, UIContentSizeCategory.accessibilityMedium: 19, UIContentSizeCategory.extraExtraExtraLarge: 19, UIContentSizeCategory.extraExtraLarge: 17, UIContentSizeCategory.extraLarge: 15, UIContentSizeCategory.large: 13, UIContentSizeCategory.medium: 12, UIContentSizeCategory.small: 12, UIContentSizeCategory.extraSmall: 12 ], ] final class func preferredDescriptor(textStyle: String) -> UIFontDescriptor { let contentSize = UIApplication.shared.preferredContentSizeCategory let style = fontSizeTable[textStyle] as! NSDictionary return UIFontDescriptor(name: SubStruct.preferredFontName, size: CGFloat((style[contentSize] as! NSNumber).floatValue)) } }
and in
viewDidLoad()
:label.font = UIFont(descriptor: UIFontDescriptor.preferredDescriptor(textStyle: UIFontTextStyle.body.rawValue), size: 0) NotificationCenter.default.addObserver(self, selector:#selector(self.userChangedTextSize(notification:)), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
here is the
userChangedTextSize
function:func userChangedTextSize(notification: NSNotification) { label.font = UIFont(descriptor: UIFontDescriptor.preferredDescriptor(textStyle: UIFontTextStyle.body.rawValue), size: 0) }
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.
Could I have the best of both worlds: a custom font with a size that changes automatically in the background?
-
ielyamani about 7 yearsThis is definitely more romantic. The first version gives more control over the size to give text when the UIContentSizeCategory changes: you don't have to use the default sizes (11...53), Which could mess up the whole ui hierarchy and nothing would be readable on iPhone plus sizes. The Medium app reacts to UIContentSizeCategory changes in a subtle way: changing the font size by a smidgen and increasing the line height.
-
Frederik Winkelsdorf about 7 yearsThanks! Feel free to combine the solutions to have the best of both approaches. Agreed, the previous gives you subtle control over the sizes to provide good and readable layouts while still decreasing/increasing the type. As far as I can see the given code uses the defaults, so my approach would be a viable shortening for anyone who wants to stick to the defaults. Medium is a very good example for using custom sizes! I still wonder why
.Body
is increased up to 53 points by Apple.. This would break nearly every thinkable layout. -
ielyamani about 7 yearsBonmot can monitor content size category changes. But still the result isn't smooth enough. Even using
viewWillAppear
isn't enough because the app switcher keeps a snapshot of the app using the previous content size. Anyway, later on I'll look into animating the content size category change: e.g going fromlarge
toaccessibilityExtraExtraExtraLarge
would go through allUIContentSizeCategory
s in between, inspired by this article -
koen over 6 yearsI really like this approach - it also makes it very easy to have more than one custom font.
-
koen almost 6 yearsNote that for iOS 10 and higher, you don't have to respond to the
UIContentSizeCategoryDidChange
notification anymore, but can setadjustsFontForContentSizeCategory
for the label. Also supposed to work forUITextField
andUITextView
. -
zgjie about 5 yearsThis does not work if the app launch with non-default scale.
UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
will get the scaled value, and usescaledFont
method it will scaled again. -
Ben Shabat about 5 yearswhen ' userChangedTextSize ' is getting called? i put breakpoint there and it never get called for me.
-
Frederik Winkelsdorf about 5 years@BenShabat You likely missed adding the NotificationCenter
addObserver
inviewDidLoad
(see the initial question for details). Could you please check that? -
Ben Shabat about 5 years@FrederikA.Winkelsdorf i figured out how to work it out i had to use traitCollectionDidChange
-
Frederik Winkelsdorf about 5 yearsOk, glad to hear that you figured it out! My post likely needs a bit of an overhaul for alle the changes since early 2017..
-
Basil over 3 yearsI tried this solution it doesn't notice my custom font , it use the default font , I'm very sure I change 'fontFamily' to my custom font
-
Basil over 3 yearsThank you, this solution working great with me
-
Frederik Winkelsdorf over 3 years@Basil for Custom Fonts in iOS you'll always have to add them to the Info.plist first, you're likely missing them there. Thus said, this solution is a couple of years old now and I would tend to use other approaches available with newer iOS Versions now (using
scaledFont
). See this blog for more insights: useyourloaf.com/blog/using-a-custom-font-with-dynamic-type. -
blyscuit over 2 yearsUIFontMetrics is for iOS11 and above, the question asked about iOS10.