How to centralize fonts from Storyboard/Xib itself | Font Manager | Centralized Theme Management — Chapter #1

Khushabu Borole
14 min readSep 9, 2022

--

How to centralize system/custom fonts by applying them in Storyboards/Xibs?

Hello guys!! This is my first blog which I am very excited 😀 about (hope you are too). This blog will help you to solve a widespread and traditional problem always faced by us in the iOS development journey.

As we all know there is nothing to centralize our fonts in the overall app using our Interface Builders (IBs) i.e. storyboards/XIBs themselves. Traditionally, to centralize fonts or colors, we need to create IBOutlets for all UI Components to apply font programmatically so that, whenever our client gives us a requirement to change the font in the overall app, we could easily change it in a centralized place and it will apply to all UI components. (When I am saying UI Components it means the labels, buttons, text fields, text views, etc.)

Otherwise, you would waste the time by changing the fonts of every UI Component one by one in every storyboard & xib. But still, somehow I was not satisfied with this traditional practice 😐. And like me, I feel many of you might be trying or struggling with the same issue.

Hence, in my overall journey of iOS development (which is almost 7.5 years 😌), I kept trying to find a simplified way to centralize and manage the custom fonts for the overall app in different ways. And guess what??🤔 Finally, I did find a simplified way😃, with the help of which neither I need to create all outlets to follow the traditional way nor do I need to subclass the UI Components. In Fact, I can centralize fonts and colors from my IBs themselves.

Thanks 🤗 to swift which does have powerful features such as enums, extensions, and of course iOS 11 onwards, features of user-named colors (we can create color assets the same as image assets which can be selectable from storyboard/xib, We will learn this later in another chapter 😉).

So guys enough of the problem discussion, let’s start with the solution.

Note: Kindly go through or study the topics such as enum classes and custom font installation in case of difficulty in understanding my code.

In order to apply fonts from the interface builder, we need to create and finish our centralized theme font base programmatically.

Step 1: Font Manager Class

a. system font text styles

In interface builder, if you have noticed we can apply different text styles of the system font such as large title, title 1, title 2, and so on for UI components (refer to image (a)). But the problem is they are only for system font right? and also they are limited. Hence, for our custom fonts, we need to create our own text styles to apply to UI components & by creating our own we get the freedom to add multiple. Though, I will also tell you how to apply custom fonts using already present text styles later in step 3(🙂).

Starting with creating a swift file named FontManager.swift of course with having the class name the same i.e. FontManager. This class will hold enum classes or functions to handle our text styles, fonts, sizes, etc.

enum Styles: String {
case bigTitle = "bt" /// regular-60 (splash screen)
case largeHeadline = "lh" /// Bold-34 ()
case largeHeadline2 = "lh2" /// Bold-28 ()
case largeHeadline3 = "lh3" /// Bold-22 ()
case largeHeadline4 = "lh4" /// Bold-20 ()
case largeTitle = "lt" /// regular-34 (Show main amount or rewards or calculations)
case largeTitle2 = "lt2" /// regular-28
case largeTitle3 = "lt3" /// regular-22
case largeTitle4 = "lt4" /// regular-20
case title = "t" /// medium-17 (Navigation Titles)
case title2 = "t2" /// medium-15 (cell titles)
case subTitle = "st" /// medium-13 (other small titles)
case headline = "h" /// Semibold-17 (mostly for prices)
case headline2 = "h2" /// Semiboild-15 ()
case subHead = "sh" /// Semibold-13 (smaller size prices)
case subject = "s" /// regular-17 (use where need moderate attention)
case body = "b" /// regular-15 (for subtitles in cells)
case paragraph = "p" /// regular-13 (sub body kind of)
case note = "n" /// Semibold-11 (note)
case footNote = "f" /// regular-11 (less important labels, notes in overall app)
case caption = "c" /// medium-11 (tags or flags)
}

Here we have created an enum class Styles holding our custom text styles in FontManager. Each text style case will have an associated value i.e. rawValue as its own initials such as lt for “large title”, t for “title” & so on.. (why? that I will tell you later on in step 2…)

As we know, to define a Font of any UI component i.e. UIFont we call the UIFont’s init function as follows:

eg -> UIFont(name: “PassengerSans-Bold”, size: 14.0)

So, the UIFont init function has parameters such as fontName & size i.e. we will need font-family, font-face/font-weight, and font size for each style. Hence, Along with the Styles class, we will also need to create enum class Fonts for managing multiple font families & their font faces. Normally, I use 1 or 2 font families (2 are also very rare) and mostly only four font faces (bold, semibold, regular, and light) in my whole app (you can add or customize according to your requirements).

enum Fonts: String {
case font1 = "f1"
case font2 = "f2"

var name: String {
switch self {
case .font1:
return "Passenger Sans"
case .font2:
return "Aeonik"
}
}
}

Here we have 2 fonts i.e. 2 cases as font1 and font2. Again here, each font case will have an associated value i.e rawValue as initials of its own as shown in images f1, f2,… Also, the name is a variable returning font family name as string respectively using switch case.

Also, we will create one function in the enum class Fonts which will return the Font name according to the font-face/font-weight in order to pass to UIFont’s init function as follows:

func fontName(_ fontFace: UIFont.Weight) -> String {
switch self {
case .font1:
switch fontFace {
case .bold:
return "PassengerSans-Bold"
case .semibold:
return "PassengerSans-Semibold"
case .medium:
return "PassengerSans-Medium"
case .regular:
return "PassengerSans-Regular"
case .light:
return "PassengerSans-Light"
default:
return "PassengerSans-Regular"
}
case .font2:
switch fontFace {
case .bold:
return "Aeonik-Bold"
case .semibold:
return "Aeonik-Semibold"
case .medium:
return "Aeonik-Medium"
case .regular:
return "Aeonik-Regular"
case .light:
return "Aeonik-Light"
default:
return "Aeonik-Regular"
}
}
}

So, we have our custom-created text styles & multiple Custom Fonts along with their respective font names of required font faces.

Now, we need to understand that, each text style case in the enum class Styles will have a particular assigned UIFont.. so that, we can apply that style to respective UI components. (how? that also I will tell you later..⏳) Referring to image (a) I have also mentioned UIFonts for each style case in the comments whichever I wanted to assign them respectively. You can customize it according to your requirements.

Going further, we will create two functions in the enum class Styles, one to get font-face/font-weight and another one to get the font size for the respective style case as follows:

/// get font face acc to style - bold, semibold ...
func fontFace() -> UIFont.Weight {
switch self {
case .largeHeadline, .largeHeadline2, .largeHeadline3, .largeHeadline4:
return .bold
case .headline, .headline2, .subHead, .note:
return .semibold
case .title, .title2, .subTitle, .caption:
return .medium
case .bigTitle, .largeTitle, .largeTitle2, .largeTitle3, .largeTitle4:
return .regular
case .subject, .body, .paragraph, .footNote:
return .regular
}
}


/// get font size acc to style ...
func fontSize() -> CGFloat {
switch self {
case .bigTitle:
return 60.0
case .largeHeadline, .largeTitle:
return 34.0
case .largeHeadline2, .largeTitle2:
return 28.0
case .largeHeadline3, .largeTitle3:
return 22.0
case .largeHeadline4, .largeTitle4:
return 20.0
case .title, .headline, .subject:
return 17.0
case .title2, .headline2, .body:
return 15.0
case .subTitle, .subHead, .paragraph:
return 13.0
case .footNote, .caption, .note:
return 11.0
}
}

But the question is, how to apply our custom styles using an interface builder? I mean, there is no such provision in storyboards/xibs where we can input anything..🤔

Step 2: FontUIManager

(Interface Builders Storyboard/XIBs handling)

As per our Step 1 development, we basically require two inputs from IBs to decide the custom font as well as style to apply respective UI Components i.e. style and font family. In order to get inputs from IBs, we can use them as IBInspectable variables.

Note: Kindly go through or study IBDesignable / IBInspectable to understand further code

Currently, Apple doesn’t provide any provision as IBInspectable variables for selecting a value from the dropdown. Hence, we will get input those things in a string format as an initial of each, for example, lt for the “large title”, t for “title”, h for “header”, and so on… as I have mentioned in step 1. Also f1 for font1 f2 for font2, etc.

Where to create those IBInspectable input variables?

So starting with another chunk of programming, let’s create FontUIManager.swift file & let’s create an extension of UIFont class in the file (Why? That too we will discuss later😅).

You will also need to import UIKit in the file as the UIFont extension will be defined in this file.

import Foundation
import UIKit

extension UIFont {
static var defaultFontFamily: FontManager.Fonts = .font1
static var defaultStyle: FontManager.Styles = .body
}

Ok. 👐🏻 Now you might think 🤔 we can subclass UI Components to define IBInspectable variables and use them in IB. Of course, we can..👍🏻 but there is one better alternative we can use in which we need not subclass components as well as apply them to each one in IBs.

We can use extensions of each UI component such as UILabel, UIButton, UITextField, & UITextView whichever is applicable to apply fonts. … where we will create two IBInspectable variables named style and fontFamily in each extension class.

So, in the same swift file i.e. FontUIManager let’s create extension classes of each UI Component & both style & fontFamily IBInspectable variables in them. Let’s start with UI Component UILabel.

b. UILabele extension with both IBInspectable input variables — FontUIManager (part 2)

Yes, guys… I am coming to that point raising in your head… I agree entirely extensions cannot have stored properties. (Doing so, the compiler will show the error shown in the image (b).) And here I was very disappointed 😑 that time until I found one more way to accomplish my purpose finally😉👍🏻.

Note: Kindly go through or study the concept ObjectIdentifier to understand code

Using the concept of ObjectIdentifier, let’s create two variables in FontUIManager.swift file directly which will get mapped to the IBInspectable variables as follows:

fileprivate var kstyle: [ObjectIdentifier: String] = [:]
fileprivate var kfontFamily: [ObjectIdentifier: String] = [:]
c. ObjectIdentifier variables — FontUIManager (part 3)

Here, the values of key ObjectIdentifier is a datatype string as our IBInspectable variables are also of data type string. We will use these variables in each UI Component’s extensions to map IbInspectable variables style & fontFamily as follows:

extension UILabel {
@IBInspectable var style: String? {
get {
return kstyle[ObjectIdentifier(self)] ?? ""
}
set {
kstyle[ObjectIdentifier(self)] = newValue
self.setFont()
}
}

@IBInspectable private var fontFamily: String? {
get {
return kfontFamily[ObjectIdentifier(self)] ?? ""
}
set {
kfontFamily[ObjectIdentifier(self)] = newValue
self.setFont()
}
}

private func setFont() {
// code to set font (we will see in step 3)
}
}

Here, we have finished coding by defining the getter-setter using ObjectIdentifier variables as well as creating the setFont() method & called in the setter so that, whenever we set the value to the IBInspectable variable new font must be updated.

Now you can see the two inputs in the attribute inspector as follows:

j. Attribute inspector of UILabel — FontUIManager (part 5)

We can leave the fontFamily input blank while designing and can consider it some default value later font1 or font2 as per the requirement to save time. As I have shown for UILabel, create extension classes along with the same stuff for UIButton, UITextField & UITextView along with the setFont() method (How to implement? I will let you know in Step 3)

Step 3: Set Font to UI Components

Summarizing the current state, we have our own Styles, Fonts, Font-Faces, Sizes, and IB inputs in extensions.

Going ahead.. in order to apply fonts to UI Components in the setFont() method in extensions we have to create the functions which will return the UIFont according to the input style & fontFamily.

So again navigating to FontManager class, we will create the following functions in the enum class Styles, so my final Style enum class will be as follows:

enum Styles: String {
case bigTitle = "bt" /// regular-60 (splash screen)
case largeHeadline = "lh" /// Bold-34 ()
case largeHeadline2 = "lh2" /// Bold-28 ()
case largeHeadline3 = "lh3" /// Bold-22 ()
case largeHeadline4 = "lh4" /// Bold-20 ()
case largeTitle = "lt" /// regular-34 (Show main amount or rewards or calculations)
case largeTitle2 = "lt2" /// regular-28
case largeTitle3 = "lt3" /// regular-22
case largeTitle4 = "lt4" /// regular-20
case title = "t" /// medium-17 (Navigation Titles)
case title2 = "t2" /// medium-15 (cell titles)
case subTitle = "st" /// medium-13 (other small titles)
case headline = "h" /// Semibold-17 (mostly for prices)
case headline2 = "h2" /// Semiboild-15 ()
case subHead = "sh" /// Semibold-13 (smaller size prices)
case subject = "s" /// regular-17 (use where need moderate attention)
case body = "b" /// regular-15 (for subtitles in cells)
case paragraph = "p" /// regular-13 (sub body kind of)
case note = "n" /// Semibold-11 (note)
case footNote = "f" /// regular-11 (less important labels, notes in overall app)
case caption = "c" /// medium-11 (tags or flags)


/// get system Font according to style
func systemFont() -> UIFont {
let fontSize = self.fontSize() + Constants.Device.fontScalingFactor
return UIFont.systemFont(ofSize: fontSize, weight: self.fontFace())
}

/// get custom Font according to style
func customFont(_ font: Fonts) -> UIFont {
let fontName = font.fontName(self.fontFace())
let fontSize = self.fontSize() + font.fontSpecificScalingFactor + Constants.Device.fontScalingFactor
return UIFont(name: fontName, size: fontSize) ?? self.systemFont()
}

/// get font face acc to style - bold, semibold ...
func fontFace() -> UIFont.Weight {
switch self {
case .largeHeadline, .largeHeadline2, .largeHeadline3, .largeHeadline4:
return .bold
case .headline, .headline2, .subHead, .note:
return .semibold
case .title, .title2, .subTitle, .caption:
return .medium
case .bigTitle, .largeTitle, .largeTitle2, .largeTitle3, .largeTitle4:
return .regular
case .subject, .body, .paragraph, .footNote:
return .regular
}
}

/// get font size acc to style ...
func fontSize() -> CGFloat {
switch self {
case .bigTitle:
return 60.0
case .largeHeadline, .largeTitle:
return 34.0
case .largeHeadline2, .largeTitle2:
return 28.0
case .largeHeadline3, .largeTitle3:
return 22.0
case .largeHeadline4, .largeTitle4:
return 20.0
case .title, .headline, .subject:
return 17.0
case .title2, .headline2, .body:
return 15.0
case .subTitle, .subHead, .paragraph:
return 13.0
case .footNote, .caption, .note:
return 11.0
}
}
}

Here, we can see one function is to get the system font & another function is to get a custom font for the respective style.

Yes, I know two new terms in the above code block fontSpecificScalingFactor & fontScalingFactor. (As usual, I will tell you that later on.. in step 4.. 😜)

Now the question is, where to call these functions?

Navigating to FontUIManager class & UIFont extension, we will create 2 functions as follows:

extension UIFont {
static var defaultFontFamily: FontManager.Fonts = .font1
static var defaultStyle: FontManager.Styles = .body

/// custom init - custom textStyles
convenience init(style: FontManager.Styles, fontFamily: FontManager.Fonts? = nil) {
let customFont = style.customFont(fontFamily ?? Self.defaultFontFamily)
self.init(name: customFont.fontName, size: customFont.pointSize)!
}

/// Font UI manager
fileprivate static func font(style: String?, fontFamily: String?, currentFont: UIFont) -> UIFont {
// old functionality - kept for compatibility
guard let s = FontManager.Styles(rawValue: style ?? "") else {
return self.getFont(style: style, fontFamily: fontFamily, currentFont: currentFont)
}

// for applying custom fonts
return UIFont(style: s, fontFamily: FontManager.Fonts(rawValue: fontFamily ?? ""))

// for applying system font
// return s.systemFont()
}
}

As per the above code, one function is a convenience initializer with parameters style & fontFamily. We can use this extension function in the overall app to set font programmatically as follows:

label.font = UIFont(style: .body)

Also, in another method, we are passing parameters style & fontFamily as strings, so that we can call this function from UI Component extension class method setFont() (FYI..we already taking input of style & fontFamily as a string in extensions).

Coming to the commented line..which is self-explanatory in the image..😊

Finally, we have all the functions connecting each other & we are now ready to use the UIFont extension functions to apply fonts in the overall app. As of now, let’s apply to UILabels & then later on you can refer to that for other UI Components as follows:

private func setFont() {
self.font = UIFont.font(style: self.style, fontFamily: self.fontFamily, currentFont: self.font)
}

And we are done 😃👍🏻

Wait… Remember? I have mentioned that I will let you know how to use system text styles for custom fonts. So, what we can do is, apply any text style from the font attribute in IBs (refer to the image (a).). Then, we have to write one more convenience init() function in the UIFont extension as follows:

/// custom init - system's textStyles
convenience init(textStyle: UIFont.TextStyle) {
var style = FontManager.Styles.paragraph

switch textStyle {
case .largeTitle:
style = .largeHeadline
case .title1:
style = .largeTitle
case .title2:
style = .title
case .title3:
style = .subTitle
case .headline:
style = .headline
case .subheadline:
style = .subHead
case .body:
style = .body
case .callout:
style = .subject
case .footnote:
style = .footNote
case .caption1:
style = .caption
case .caption2:
style = .caption
default:
style = .paragraph
}

let customFont = style.customFont(Self.defaultFontFamily)
self.init(name: customFont.fontName, size: customFont.pointSize)!
}

Next, we have to change setFont() method as follows:

private func setFont() {
if let textStyle = self.font.fontDescriptor.object(forKey: UIFontDescriptor.AttributeName.textStyle) as? UIFont.TextStyle {
self.font = UIFont(textStyle: textStyle)
} else {
self.font = UIFont(textStyle: .body)
}
}

Now, the thing is where to call the setFont() method. Yes, If we are not setting our custom text styles setter won’t be called & setFont() won’t be called. Hence, we have to write one more method in UILabel extension class i.e. awakeFromNib(), and in that, we have to call setFont() as follows:

open override func awakeFromNib() {
super.awakeFromNib()
self.setFont()
}

So here was another method using the system’s text styles we can apply custom fonts or whatever we want... simple? Isn’t it?

Now we are done but still ending with one more small step (now what ? 🤨 .. remember? fontScaling factors?)

Step 4: FontScalingFactors

Firstly, let’s understand the purpose of creating font scaling factors. Some fonts can appear smaller or bigger than others in sizes, for example, Regular-16pts can look smaller in Passenger Sanse & other fonts, suppose Aeonik font may need half or a point more than Passenger Sanse like Regular-17. Hence, we will maintain a variable fontSpecificScalingFactor for the respective font family in the enum class Fonts in FontManager.swift & we will add this factor to the font size as shown in step 3 code block.

var fontSpecificScalingFactor: CGFloat {
switch self {
case .font1:
return 0.0
case .font2:
return 0.5
}
}

Secondly, let’s look at the variable fontScalingFactor mentioned in the same step 3 code block.

Note: There is no such issue if you skip adding fontScalingFactor variable if you are handling dynamic sizes through any other way.

This is the factor I am maintaining for different iPhone device screen sizes. I calculated it when my app gets launched & the whole enum class of device sizes & the computed value is saved in my Constants file & have been added to the font size as shown in the step 3 code block.

enum iPhoneDevices {
case iPhone4s
case iPhone5_5s_5c_SE
case iPhone6_6s_7_8
case iPhone6plus_6splus_7plus_8plus
case iPhoneX_Xs_11Pro_12Mini_13Mini
case iPhoneXSMax_XR_11_11ProMax
case iPhone12_12Pro_13_13Pro
case iPhone12ProMax_13ProMax
case unknown

init() {
switch (UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height) {
case (320, 480): // 320x480, 640x960|3.5"|320
self = .iPhone4s
case (320, 568): // 640x1136|4"|320
self = .iPhone5_5s_5c_SE
case (375, 667): // 750x1334|4.7"|375
self = .iPhone6_6s_7_8
case (414, 736): // 1242x2208|5.5"|414
self = .iPhone6plus_6splus_7plus_8plus
case (375, 812): // 1125x2436|5.8"|375
self = .iPhoneX_Xs_11Pro_12Mini_13Mini
case (414, 896): // iPhoneXR_11 = 828x1792|6.1"|414 , // iPhoneXsMax_11ProMax = 1242x2688|6.5"|414
self = .iPhoneXSMax_XR_11_11ProMax
case (390, 844):
self = .iPhone12_12Pro_13_13Pro
case (428, 926):
self = .iPhone12ProMax_13ProMax
default:
self = .unknown
}
}

var fontScalingFactor: CGFloat {
switch self {
case .iPhone4s:
return -4.0
case .iPhone5_5s_5c_SE:
return -3.0
case .iPhone6_6s_7_8, .iPhoneX_Xs_11Pro_12Mini_13Mini:
return -2.0
case .iPhone12_12Pro_13_13Pro:
return -1
case .iPhone6plus_6splus_7plus_8plus, .iPhoneXSMax_XR_11_11ProMax:
return 0
case .iPhone12ProMax_13ProMax:
return 1
case .unknown:
return 0
}
}
}

So, finally, we are done now…😃😎

THAT’S ALL FOLKS!! Thanks for reading!!

--

--