UIKit Component Styling in iOS

Abhishek Ravi 🇮🇳
8 min readApr 5, 2023

--

Objective

If you have done any web development then you are aware of CSS and how developer cry over it 😂. But, you know — Styling is very important for any front end application. Be it mobile app or any web app, you should always follow a consistent design for your UI & styling will help you in achieving this. If not, then you can ask your designer to make it consistent. In design, we should follow same font, consistent weight across the app, colour-theory should be same, padding, container view outside margin.

If you have worked on Android, they have pretty good convention to follow the fonts, colours, style — separate files for each.
But, in iOS people hardly follow any convention. How I got to know about this — Few years back, I was going through one of my friend Mukesh’s code and I saw this styling thing which he does in his project. I was like a Eureka moment for me.

Agenda

Here, I will try to copy Uber UI Design Theory for Login and Reset Password Module. We will try to create component which will be used across the app.

  1. Create a UITextField Component
  2. Create a UILabel Component
  3. Create UIButton Component
  4. Create UIFont Component
  5. Create UIColor Component

Here, I have just created component for these UIKit controls, you explore more.

And, I follow this convention of starting component name with App, you can follow any prefix, there is no hard and fast rule for this. Usually, people follow their company’s initials (only if, you’re less creative with your naming convention) 😄

AppFont

Any design theory starts with the font and yes, it’s a very integral part of UI, it sets the visual tone. Let’s dive into it.
Either your UI Designer will give you the font, or you something for yourself from Google Fonts. I have chose Montserrat for myself.

So, first you need to download the *.ttf file and then import in your project.

After importing the font file, you need to specify in your Info.plist file that, you’re using external font for this project—

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIAppFonts</key>
<array>
<string>Montserrat-Regular.ttf</string>
<string>Montserrat-Medium.ttf</string>
<string>Montserrat-Bold.ttf</string>
<string>Montserrat-Thin.ttf</string>
<string>Montserrat-Italic.ttf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>

Now, I will create a AppFont file with following code. You can structure as you wish, but our prime objective here will be —

This class will decide for the app that, what set of fonts & weight you are going to use. You can expose as many method as you want. Naming Convention is also upto you, I have followed mine.

import UIKit

extension UIFont {

private enum CustomFont: String {
case book = "Montserrat-Regular"
case italic = "Montserrat-Italic"
case thin = "Montserrat-Thin"
case medium = "Montserrat-Medium"
case bold = "Montserrat-Bold"
}

static func appFontRD(ofSize size: CGFloat, weight: UIFont.Weight = .regular) -> UIFont {
var font = UIFont(name: CustomFont.book.rawValue, size: size)
switch weight {
case .ultraLight:
font = UIFont(name: CustomFont.thin.rawValue, size: size)
case .thin:
font = UIFont(name: CustomFont.thin.rawValue, size: size)
case .light:
font = UIFont(name: CustomFont.thin.rawValue, size: size)
case .regular:
font = UIFont(name: CustomFont.book.rawValue, size: size)
case .medium:
font = UIFont(name: CustomFont.medium.rawValue, size: size)
case .semibold:
font = UIFont(name: CustomFont.medium.rawValue, size: size)
case .bold:
font = UIFont(name: CustomFont.bold.rawValue, size: size)
case .heavy:
font = UIFont(name: CustomFont.bold.rawValue, size: size)
case .black:
font = UIFont(name: CustomFont.bold.rawValue, size: size)
default:
font = UIFont(name: CustomFont.book.rawValue, size: size)
}
guard let retValue = font else {
return .systemFont(ofSize: size, weight: weight)
}
return retValue
}

static func boldAppFontRD(ofSize size: CGFloat) -> UIFont {
return self.appFontRD(ofSize: size, weight: .bold)
}

static func italicAppFontRD(ofSize size: CGFloat) -> UIFont {
guard let retValue = UIFont(name: CustomFont.italic.rawValue, size: size) else {
return .italicSystemFont(ofSize: size)
}
return retValue
}
}

class AppFont {

static let shared = AppFont()

public var headingSemibold: UIFont {
return UIFont.appFontRD(ofSize: 32, weight: UIFont.Weight.semibold)
}

public var headingRegular: UIFont {
return UIFont.appFontRD(ofSize: 32, weight: UIFont.Weight.regular)
}

public var headlineSemibold: UIFont {
return UIFont.appFontRD(ofSize: 24, weight: UIFont.Weight.semibold)
}

public var headlineRegular: UIFont {
return UIFont.appFontRD(ofSize: 24, weight: UIFont.Weight.regular)
}

public var titleSemibold: UIFont {
return UIFont.appFontRD(ofSize: 20, weight: UIFont.Weight.semibold)
}

public var titleRegular: UIFont {
return UIFont.appFontRD(ofSize: 20, weight: UIFont.Weight.regular)
}

public var subHeadingSemibold: UIFont {
return UIFont.appFontRD(ofSize: 16, weight: UIFont.Weight.semibold)
}

public var subHeadingRegular: UIFont {
return UIFont.appFontRD(ofSize: 16, weight: UIFont.Weight.regular)
}

public var subHeadingMedium: UIFont {
return UIFont.appFontRD(ofSize: 16, weight: UIFont.Weight.medium)
}

public var bodySemibold: UIFont {
return UIFont.appFontRD(ofSize: 14, weight: UIFont.Weight.semibold)
}

public var bodyRegular: UIFont {
return UIFont.appFontRD(ofSize: 14, weight: UIFont.Weight.regular)
}

public var captionSemibold: UIFont {
return UIFont.appFontRD(ofSize: 12, weight: UIFont.Weight.semibold)
}

public var captionRegular: UIFont {
return UIFont.appFontRD(ofSize: 12, weight: UIFont.Weight.regular)
}

public var bodyMedium: UIFont {
return UIFont.appFontRD(ofSize: 14, weight: UIFont.Weight.medium)
}

public var headlineMedium: UIFont {
return UIFont.appFontRD(ofSize: 32, weight: UIFont.Weight.medium)
}

public var subHeadlineMedium: UIFont {
return UIFont.appFontRD(ofSize: 24, weight: UIFont.Weight.medium)
}

public var bodyBook: UIFont {
return UIFont.appFontRD(ofSize: 16)
}

public var headlineBook: UIFont {
return UIFont.appFontRD(ofSize: 24)
}

public var subHeadlineBook: UIFont {
return UIFont.appFontRD(ofSize: 24)
}

public func custom(_ size:CGFloat ,_ weight: UIFont.Weight) -> UIFont {
return UIFont.appFontRD(ofSize: size, weight: weight)
}
}

And, when I am going to set my font to any UI Component, I will be like —

myLabel.font = AppFont.shared.headlineBook

🍀 Advantage — Say, in near future Product team or Designers have decided to change the font, then you will have a single set of class where you have to do the changes. Yes, you’re right — Single Responsibility Principle ✌️

AppLabel

Label is like a cell in human body, you are bound to have this component in your project, it’s better to create a UI component for this.

class AppLabel: UILabel {

override init(frame: CGRect) {
super.init(frame: frame)
stupUI()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
stupUI()
}

private func stupUI() {
self.font = AppFont.shared.bodyMedium
}

func setSize(of size:CGFloat, of weight: UIFont.Weight) {
self.font = AppFont.shared.custom(size, weight)
}

}

Here, I have just assigned the font for my label component, you can extend this class as per your need, be it colour, spacing or anything.
And, whenever you’re creating your UILabel, go to File Inspector and assign AppLabel to it.

AppButton

Button is also one of the essential UI component.I personally, believes button shouldn’t have more than four states. So, in my case I have made enum of the States — primary and secondary.

You can have as multiple state, like — active, inactive, disabled, default and loading.

For Loading state, I have exposed a isLoading property (I know, it be under the enum).

import UIKit

enum ButtonState {
case primary
case secondary
}

class AppButton: UIButton {

private let activityIndicator = UIActivityIndicatorView(style: .medium)
private var defaultStyle: ButtonState = .primary

var isLoading: Bool = false {
didSet {
updateView()
}
}

override init(frame: CGRect) {
super.init(frame: frame)
intialize()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
intialize()
}

private func intialize() {
self.titleLabel?.font = AppFont.shared.custom(16, .bold)
self.layer.cornerRadius = 2.0
self.setStyleState(self.defaultStyle)

// Indicator
addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
}

private func updateView() {
if isLoading {
activityIndicator.startAnimating()
self.titleLabel?.alpha = 0
self.titleLabel?.layer.opacity = 0
self.imageView?.alpha = 0
self.titleLabel?.isHidden = true
self.titleLabel?.textColor = UIColor.clear
isEnabled = false
} else {
activityIndicator.stopAnimating()
self.titleLabel?.alpha = 1
self.titleLabel?.layer.opacity = 1
self.imageView?.alpha = 1
self.titleLabel?.textColor = UIColor.white
self.titleLabel?.isHidden = false
isEnabled = true
}
}

func setStyleState(_ state: ButtonState) {
if state == .primary {
self.setTitleColor(UIColor.white, for: .normal)
self.backgroundColor = UIColor.black
self.layer.borderColor = UIColor.black.cgColor
self.layer.borderWidth = 1.5
self.defaultStyle = .primary
} else {
self.setTitleColor(UIColor.black, for: .normal)
self.backgroundColor = UIColor.white
self.defaultStyle = .secondary
self.layer.borderWidth = 1.5
self.layer.borderColor = UIColor.black.cgColor
}
}
}

So, both the primary and secondary button state look like this —

You’re free to extend the class as per your need. And, please do comment if you find any abusive code 😅

And, you just need to assign your UIButton with AppButton class in file inspector.

AppTextField

Here, I haven’t played enough with UITextField. I just assigned few UI properties like border colour and text colour.

import UIKit

class AppTextField: UITextField {

override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}

private func setupUI() {
let borderWidth = CGFloat(2.0)
self.layer.borderColor = UIColor.darkGray.cgColor
self.layer.frame = CGRect(x: 0, y: self.frame.size.height - borderWidth, width: self.frame.size.width, height: self.frame.size.height)
self.layer.borderWidth = borderWidth
self.textColor = UIColor.darkGray
self.backgroundColor = UIColor.white
self.font = AppFont.shared.bodyMedium
self.attributedPlaceholder = NSAttributedString(
string: self.placeholder ?? "",
attributes: [NSAttributedString.Key.foregroundColor: UIColor.darkGray]
)
self.layer.masksToBounds = true

}

override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(x: bounds.origin.x + 16, y: bounds.origin.y + 16, width: bounds.size.width - 20, height: bounds.size.height - 30 )
}

override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.textRect(forBounds: bounds)
}
}

And, Text Field will look something like this if you assign AppTextField —

AppColor

Yes, colours need to be under single class, you shouldn’t be allowed to consume colour directly using UIColor.black

This will also helps you in future code changes if re-designing your UI comes up. Usually, designer have their colour pallet for the UI design.

Yes, I have directly consumed UIColor.black or UIColor.white in my code snippet. My Bad, I have pushed the fix, you can checkout from the Github.

extension UIColor {
static let appBlack = UIColor.black
static let appWhite = UIColor.white
static let appDarkGray = UIColor.darkGray
}

🌎 Source Code

--

--