So what is the best way to do this without ending up with a bloated viewDidLoad function? By using extensions, we can extract this logic away from viewDidLoad and place it elsewhere. Below are 3 techniques to keep everything neat and tidy.
A separate file to handle view creation and layout
In this first example, a new file is created using an extension to both initialise and layout each view component. This leaves the view controller free of any view creation or layout code.
// OptionAViewController.swift
class OptionAViewController: UIViewController {
var emailTextField: UITextField!
var passwordTextField: UITextField!
var confirmTextField: UITextField!
var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
buildUI()
}
}
// OptionAViewController+UI.swift
extension OptionAViewController {
func buildUI() {
view.backgroundColor = .systemBackground
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 12
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let stackGap: CGFloat = 32
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: stackGap),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: stackGap),
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -stackGap)
])
emailTextField = UITextField()
emailTextField.borderStyle = .roundedRect
emailTextField.placeholder = "Enter your email"
stackView.addArrangedSubview(emailTextField)
passwordTextField = UITextField()
passwordTextField.borderStyle = .roundedRect
passwordTextField.placeholder = "Enter your password"
passwordTextField.isSecureTextEntry = true
stackView.addArrangedSubview(passwordTextField)
confirmTextField = UITextField()
confirmTextField.borderStyle = .roundedRect
confirmTextField.placeholder = "Confirm your password"
confirmTextField.isSecureTextEntry = true
stackView.addArrangedSubview(confirmTextField)
loginButton = UIButton()
loginButton.translatesAutoresizingMaskIntoConstraints = false
loginButton.setTitle("LOGIN (A)", for: .normal)
loginButton.setTitleColor(.systemBlue, for: .normal)
view.addSubview(loginButton)
NSLayoutConstraint.activate([
loginButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 44),
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
Whilst this results in minimal view code in the view controller, there are two things to be aware of. Firstly, each view declared in the view controller has to be made public so that an extension in another file can access and initialise them. This also means other parts of the system could set these view properties to new values which isn’t ideal. If those views were made private, Xcode would show the error “’someView’ is inaccessible due to ‘private’ protection level”
. Secondly, the call to layout the views declared in the extension must also be public, meaning any other part of the system could call this method.
A separate file to handle view layout only
This example is similar to the first, except that this time each view is declared and initialised in the view controller. The views are created using closure style syntax which keeps the configuration of each view outside of viewDidLoad. The extension now handles layout of each view only.
// OptionBViewController.swift
class OptionBViewController: UIViewController {
private(set) var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 12
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
private(set) var emailTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = "Enter your email"
textField.keyboardType = .emailAddress
return textField
}()
private(set) var passwordTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = "Enter your password"
textField.isSecureTextEntry = true
return textField
}()
private(set) var confirmTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
textField.placeholder = "Confirm your password"
textField.isSecureTextEntry = true
return textField
}()
private(set) var loginButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("LOGIN (B)", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
layoutUI()
}
}
// OptionBViewController+Layout.swift
extension OptionBViewController {
func layoutUI() {
view.addSubview(stackView)
let stackGap: CGFloat = 32
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: stackGap),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: stackGap),
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -stackGap)
])
stackView.addArrangedSubview(emailTextField)
stackView.addArrangedSubview(passwordTextField)
stackView.addArrangedSubview(confirmTextField)
view.addSubview(loginButton)
NSLayoutConstraint.activate([
loginButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 44),
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
The view properties are declared as private(set) to stop other parts of the system from initialising them to new values. They are still publicly accessible however, so other parts of the system could manipulate them.
An extension within the same file as the view controller
By creating the extension in the same file, we can now make all view properties private, along with the method to layout each view. This is a great solution if you want to protect the views and the layout method from being accessed outside of the view controller. It keeps the viewDidLoad method free from bloat. However, all view creation and layout code is now in one file.
// OptionCViewController.swift
class OptionCViewController: UIViewController {
private var emailTextField: UITextField!
private var passwordTextField: UITextField!
private var confirmTextField: UITextField!
private var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
buildUI()
}
}
extension OptionCViewController {
private func buildUI() {
view.backgroundColor = .systemBackground
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 12
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let stackGap: CGFloat = 32
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: stackGap),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: stackGap),
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -stackGap)
])
emailTextField = UITextField()
emailTextField.borderStyle = .roundedRect
emailTextField.placeholder = "Enter your email"
stackView.addArrangedSubview(emailTextField)
passwordTextField = UITextField()
passwordTextField.borderStyle = .roundedRect
passwordTextField.placeholder = "Enter your password"
passwordTextField.isSecureTextEntry = true
stackView.addArrangedSubview(passwordTextField)
confirmTextField = UITextField()
confirmTextField.borderStyle = .roundedRect
confirmTextField.placeholder = "Confirm your password"
confirmTextField.isSecureTextEntry = true
stackView.addArrangedSubview(confirmTextField)
loginButton = UIButton()
loginButton.translatesAutoresizingMaskIntoConstraints = false
loginButton.setTitle("LOGIN (C)", for: .normal)
loginButton.setTitleColor(.systemBlue, for: .normal)
view.addSubview(loginButton)
NSLayoutConstraint.activate([
loginButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 44),
loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
Conclusion
Each method described above solves the problem of messy view controller code when views are created programatically, using extensions on the view controller in a variety of ways. You can pick and choose which best suits your needs. Head to GitHub to download the code for this article.
However, there is a trade off between access to your view controllers from the outside world, and the separation of view controller and view code. In the next post, we’ll look at an alternative method without using extensions, that separates view controller and view code into separate files whilst also keeping the view properties and layout method private! Subscribe below to stay tuned 👀