I'm using TextStyles in my app to support dynamic fonts. I have the challenge to change the Font for each TextStyle. So for example the TextStyle.body should be MyAwesomeBODYFont and the TextStyle.headline should be MyAwesomeHeadlineFont. And this for the entire app. Setting the font for the whole app won't work because I need several Fonts for the different styles.
Is it possible to override these TextStyles somehow with custom fonts for the entire app and not for each label separately?
What I tried:
Setting the font for the appearance proxy of UILabel in general works fine:
let labelAppearance = UILabel.appearance()
let fontMetrics = UIFontMetrics(forTextStyle: .body)
labelAppearance.font = fontMetrics.scaledFont(for: myAwesomeBodyFont)
But this overrides all labels not matter what TextStyle they use.
After that I tried to check for the TextStyle but it crashes with a nil pointer exception for the UILabel.appearance().font or does not even go into the if-block.
let labelAppearance = UILabel.appearance()
if let textStyle = labelAppearance.font.fontDescriptor.object(forKey: UIFontDescriptor.AttributeName.textStyle) as? UIFont.TextStyle {
// this would be the place to check for the TextStyle and use the corresponding font
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
labelAppearance.font = fontMetrics.scaledFont(for: mayAwesomeBodyFont)
}
Because the appearance of UILabel does not have a font set.
You cannot directly "set" a custom font for Text Styles.
You can get font size for text style and then could use custom family.
let systemDynamicFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
let size = systemDynamicFontDescriptor.pointSize
let font = UIFont(name: MyAwesomeBODYFont, size: size)
For iOS 11+ There is scaledFont()
You can make this font variable static and could use it everywhere in app.
You could check it out this solution too: https://stackoverflow.com/a/42235227/4846167
I ended up creating a subclass of UILabel and let all my labels inherit from it. This way you can set the class in InterfaceBuilder and/or create the custom class in code.
This is the DynamicCustomFontLabel class:
import UIKit
class DynamicCustomFontLabel: UILabel {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
initCustomFont()
}
override init(frame: CGRect) {
super.init(frame: frame)
initCustomFont()
}
private func initCustomFont() {
if let textStyle = font.fontDescriptor.object(forKey: UIFontDescriptor.AttributeName.textStyle) as? UIFont.TextStyle {
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
var customFont: UIFont?
switch textStyle {
case .body:
customFont = UIFont(name: "MyAwesomeBODYFont", size: 21)
case .headline:
customFont = UIFont(name: "MyAwesomeHeadlineFont", size: 48)
// all other cases...
default:
return
}
guard let font = customFont else {
fatalError("Failed to load a custom font! Make sure the font file is included in the project and the font is added to the Info.plist.")
}
self.font = fontMetrics.scaledFont(for: font)
}
}
}
Related
I'm using a UICollectionView with compositional layout that contains UICollectionViewListCells. Each cell contains several UILabels. I set the dynamic type with custom font weight with
extension UIFont {
static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont {
let metrics = UIFontMetrics(forTextStyle: style)
let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
let font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight)
return metrics.scaledFont(for: font)
}
}
source: https://mackarous.com/dev/2018/12/4/dynamic-type-at-any-font-weight
I then change the font size:
When I scroll through my UICollectionView, some cells' font size remain unchanged while most are correctly resized (everything works perfectly when using something simpler like UIFont.preferredFont(forTextStyle: .subheadline). What is the best approach to getting the proper font size to render in response to the change in preferred content size?
Additionally I'm creating an attributed string which contains SF Symbols https://stackoverflow.com/a/58341241 and setting the size of the symbol with
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
When I change the font size, the image doesn't dynamically scale. Both of these issues go away when I restart the app. Is there any code I can execute when UIContentSizeCategory.didChangeNotification posts, or other approaches to tackling these two issues?
EDIT
I was able to solve the first part of the question by using the approach laid out here https://spin.atomicobject.com/2018/02/02/swift-scaled-font-bold-italic/.
extension UIFont {
func withTraits(traits:UIFontDescriptor.SymbolicTraits) -> UIFont {
let descriptor = fontDescriptor.withSymbolicTraits(traits)
return UIFont(descriptor: descriptor!, size: 0) //size 0 means keep the size as it is
}
func bold() -> UIFont {
return withTraits(traits: .traitBold)
}
}
let boldFont = UIFont.preferredFont(forTextStyle: .headline).bold()
I'm still stuck on how to get the sf symbol UIImage in the attributed string to resize when the preferred content size changes.
To get the attributed string to resize when the preferred content size changes.
func attributedStringDynamicTypeExample() {
let font = UIFont(name: "Times New Roman", size: 20)!
let fontMetrics = UIFontMetrics(forTextStyle: .title3)
let scaledFont = fontMetrics.scaledFont(for: font)
let attachment = NSTextAttachment()
attachment.image = UIImage(named: "BigPoppa")
attachment.adjustsImageSizeForAccessibilityContentSizeCategory = true
let attributes: [NSAttributedString.Key: Any] = [.font: scaledFont]
let attributedText = NSMutableAttributedString(string: "Your text here!", attributes: attributes)
attributedText.append(NSAttributedString(attachment: attachment))
label.adjustsFontForContentSizeCategory = true
label.attributedText = attributedText
}
As far as the best approach to getting the proper font size to render in response to the change in preferred content size I prefer overriding the traitCollectionDidChange like so:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection?.preferredContentSizeCategory
!= traitCollection.preferredContentSizeCategory
else { return }
collectionView.collectionViewLayout.invalidateLayout()
}
But using the notification should be just as effective:
NotificationCenter.default.addObserver(self,
selector: #selector(sizeCategoryDidChange),
name: UIContentSizeCategory.didChangeNotification,
object: nil)
#objc private func sizeCategoryDidChange() {
collectionView.collectionViewLayout.invalidateLayout()
}
I have a method for creating an auto-scaling font based on Dynamic Type that looks like so:
extension UIFont {
public static func getAutoScalingFont(_ fontName: String, _ textStyle: UIFont.TextStyle) -> UIFont {
// getFontSize pulls from a map of UIFont.TextStyle and UIFont.Weight to determine the appropriate point size
let size = getFontSize(forTextStyle: textStyle)
guard let font = UIFont(name: fontName.rawValue, size: size) else {
return UIFont.systemFont(ofSize: size)
}
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
let traitCollection = UITraitCollection(preferredContentSizeCategory: UIApplication.shared.preferredContentSizeCategory)
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle, compatibleWith: traitCollection)
return fontMetrics.scaledFont(for: font, maximumPointSize: fontDescriptor.pointSize)
}
}
This seems to work great; when I change the Text Size slider value in the phone Settings, the font scales as necessary.
I'm now trying to add the logic for a minimum UIContentSizeCategory. That is, if the user sets their Text Size value to be less than my specified minimum size category, the font should scale as if they've selected the minimum value.
Here's my attempt:
extension UIFont {
// This variable represents the minimum size category I want to support; that is, if the user
// chooses a size category smaller than .large, fonts should be scaled to the .large size
private static let minimumSupportedContentSize: UIContentSizeCategory = .large
public static func getAutoScalingFont(_ fontName: String, _ textStyle: UIFont.TextStyle) -> UIFont {
let size = getFontSize(forTextStyle: textStyle)
guard let font = UIFont(name: fontName.rawValue, size: size) else {
return UIFont.systemFont(ofSize: size)
}
// I've extended UIContentSizeCategory to adhere to Comparable so this works fine
let contentSize = max(UIApplication.shared.preferredContentSizeCategory, minimumSupportedContentSize)
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
let traitCollection = UITraitCollection(preferredContentSizeCategory: contentSize)
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle, compatibleWith: traitCollection)
return fontMetrics.scaledFont(for: font, maximumPointSize: fontDescriptor.pointSize)
}
}
Via logs I'm able to tell that, as expected, the contentSize I pass into the UITraitCollection initializer is never a value smaller than .large. However, it seems like the value passed to that initializer represents a maximum content size category. That is, if I init the trait collection like so:
let traitCollection = UITraitCollection(preferredContentSizeCategory: .large)
the font will re-scale for all UIContentSizeCategory's smaller than .large but will not re-scale for any categories larger than .large.
Does anyone know how to accomplish setting a minimum UIContentSizeCategory?
Although we have minimumContentSizeCategory and maximumContentSizeCategory supported from iOS 15, we still need the older way in a few scenarios. For example, these 2 properties doesn't work when we need to support dynamic text styles in NSAttributedString.
Here is how I did it the older way,
Use UIApplication.shared.preferredContentSizeCategory to decide which preferredContentSizeCategory to use with UITraitCollection
Example:
func getPreferredFont(textStyle: UIFont.TextStyle, weight: UIFont.Weight? = nil) -> UIFont {
let preferredContentSizeCategory: UIContentSizeCategory
switch UIApplication.shared.preferredContentSizeCategory {
case .extraSmall, .small, .medium:
preferredContentSizeCategory = .large
case .accessibilityExtraLarge, .accessibilityExtraExtraLarge, .accessibilityExtraExtraExtraLarge:
preferredContentSizeCategory = .accessibilityLarge
default:
preferredContentSizeCategory = UIApplication.shared.preferredContentSizeCategory
}
let traitCollection = UITraitCollection(preferredContentSizeCategory: preferredContentSizeCategory)
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle, compatibleWith: traitCollection)
let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
let font: UIFont
if let weight = weight {
font = UIFont.systemFont(ofSize: fontDescriptor.pointSize, weight: weight)
} else {
font = UIFont.systemFont(ofSize: fontDescriptor.pointSize)
}
return fontMetrics.scaledFont(for: font, maximumPointSize: fontDescriptor.pointSize, compatibleWith: traitCollection)
}
Starting from iOS 15 you can set limits on the minimum and maximum sizes of dynamic type:
// UIKit
view.minimumContentSizeCategory = .medium
view.maximumContentSizeCategory = .accessibilityExtraLarge
// SwiftUI
ContentView()
.dynamicTypeSize(.medium ... .accessibility3) // No smaller than medium, no bigger than accessibility3
When you set one or both of these properties on a view it limits the dynamic type size for that view and any of its subviews.
Since these are properties of UIView they are also available on subclasses like UILabel and UITextView. This gives you fine grained control to limit an individual text element without affecting other text:
label.minimumContentSizeCategory = .large
Here’s a quote from the Apple engineer in the WWDC session:
Please do not use this API to unduly limit text size. These settings
serve an extremely important function, and it’s paramount that your
app’s functionality is all available, and everything is legible, to
people using the highest text size setting.
In SwiftUI you get all these handy little Font convenience accessors like Font.caption, Font.title, Font.body, etc..
e.g.
VStack {
Text("Some Title").font(Font.title)
Text("Some Caption Text").font(Font.caption)
}
They all specify different font styles for the default Helvetica font family. I'd like to use these very helpful convenience accessors without ever using Helvetica in my app. Can I change the default font family? Or do I constantly have to apply custom fonts like e.g.:
Text("Custom Text").font(Font.custom("SourceSansPro-Regular", size: 14.0)
You can override the system font if you want.
extension Font {
/// Create a font with the large title text style.
public static var largeTitle: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .largeTitle).pointSize)
}
/// Create a font with the title text style.
public static var title: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .title1).pointSize)
}
/// Create a font with the headline text style.
public static var headline: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .headline).pointSize)
}
/// Create a font with the subheadline text style.
public static var subheadline: Font {
return Font.custom("OpenSans-Light", size: UIFont.preferredFont(forTextStyle: .subheadline).pointSize)
}
/// Create a font with the body text style.
public static var body: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .body).pointSize)
}
/// Create a font with the callout text style.
public static var callout: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .callout).pointSize)
}
/// Create a font with the footnote text style.
public static var footnote: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .footnote).pointSize)
}
/// Create a font with the caption text style.
public static var caption: Font {
return Font.custom("OpenSans-Regular", size: UIFont.preferredFont(forTextStyle: .caption1).pointSize)
}
public static func system(size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) -> Font {
var font = "OpenSans-Regular"
switch weight {
case .bold: font = "OpenSans-Bold"
case .heavy: font = "OpenSans-ExtraBold"
case .light: font = "OpenSans-Light"
case .medium: font = "OpenSans-Regular"
case .semibold: font = "OpenSans-SemiBold"
case .thin: font = "OpenSans-Light"
case .ultraLight: font = "OpenSans-Light"
default: break
}
return Font.custom(font, size: size)
}
}
My current approach is to just recreate my own font factory:
struct MyFont {
static let title = Font.custom("SourceSansPro-Bold", size: 24.0)
static let body = Font.custom("SourceSansPro-Regular", size: 12.0)
}
and then use e.g. MyFont.title in place of Font.title
This is my approach:
struct Fonts {
static func oswaldRegular(size:CGFloat) -> Font{
return Font.custom("Oswald-Regular", size: size)
}
static func oswaldLight(size:CGFloat) -> Font{
return Font.custom("Oswald-Light", size: size)
}
static func oswaldBold(size:CGFloat) -> Font{
return Font.custom("Oswald-Bold", size: size)
}
}
Here's an approach that combines some of these methods, specifically Justin's approach and Brady Murphy's Medium Article. The idea is to create a font manager that can be incorporated into a Font extension to override the default. I wish there were a compiler directive that could skip the Font extension for Previews. This will still break previews.
This func used once will grab the actual font names:
// put the following into the .onAppear of a top level view or App.init
func getCustomFontNames() {
// get each of the font families
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
// print array of names
print("Family: \(family) Font names: \(names)")
}
}
Font manager that can create a default but also allows for multiple fonts by calling the font family struct directly.
typealias FMgr = FontManager
struct FontManager {
// dynamic font sizes
struct dynamicSize {
public static var largeTitle : CGFloat = UIFont.preferredFont(forTextStyle: .largeTitle).pointSize
public static var title : CGFloat = UIFont.preferredFont(forTextStyle: .title1).pointSize
// repeat for all the dynamic sizes
}
// App Supplied Fonts
struct Quicksand {
static let familyRoot = "Quicksand"
// weights
static let heavy = bold
static let bold = "\(familyRoot)-Bold"
static let semibold = "\(familyRoot)-SemiBold"
static let medium = regular
static let regular = "\(familyRoot)-Regular"
static let thin = light
static let light = "\(familyRoot)-Light"
static let ultralight = light
// dynamic sizes
static let largeTitle : Font = Font.custom(FMgr.Quicksand.bold, size: FMgr.dynamicSize.largeTitle)
// repeat for other sizes
}
// structs for other fonts
}
extension Font {
public static var largeTitle = FMgr.Quicksand.largeTitle
// repeat for the rest of the dynamic sizes
}
I need to change the font of all the UILables, UITextViews, UITextFields, UIButtons (title), in the app based on the content of the text or title. How can I do this globally?.
I did try to create an extension for each and make the changes in layoutSubviews(), but this only worked for UILabels.
extension UILabel {
override open func layoutSubviews() {
super.layoutSubviews()
let text = self.text ?? ""
var fontName = THEME_FONT_NAME
if (!text.isAlphanumeric) {
self.font = UIFont.systemFont(ofSize: self.font.pointSize, weight: fontWeight)
return
}
self.font = UIFont(name: fontName, size: self.font.pointSize)
}}
Are you using firebase, then specify fonts in remote config. fetch those fonts and create some utility class with static property font. e.g
class Utils{
var font: UIFont = UIFont("fontFetched") ?? default font
}
like:
label.text.font = font
and use it wherever you implement fields and buttons
I'm trying to change the font throughout an iOS app in Swift. I'm currently using:
var size:CGFloat = 16
UILabel.appearance().font = UIFont(name: "Gudea", size: size)
Which changes every UILabel in my app to be the correct font. However, all labels are changed to size 16 despite different sizes assigned in the Main.Storyboard.
Is there a way to assign a font to every UILabel without changing the sizes assigned in Main.Storyboard?
or, perhaps,
Is there a better way to change the font used throughout the app?
Thanks!
My solution was to extend UIFont and set my labels programatically.
UIFontExtension.swift:
extension UIFont {
class func myFontName() -> String { return "Gudea" }
class func myBoldFontName() -> String { return "Gudea-Bold" }
class func myNormalFont() -> UIFont {
return UIFont(name: UIFont.myFontName(), size: 12)!
}
class func mySmallBoldFont() -> UIFont {
return UIFont(name: UIFont.myBoldFontName(), size: 10)!
}
}
MyViewController.swift:
myLabel.font = UIFont.myNormalFont()
mySmallLabel.font = UIFont.mySmallBoldFont()