Avoid messy view controller layout code with extensions

When creating view controller views programatically, without using any .xib or .storyboard files, there can be a significant amount of code that needs to be organised.

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 👀

Stay in the Loop

Subscribe to tapdev and never miss a post.

Total
0
Shares
Previous Post

6 ways to level up your skills as a Junior Swift / iOS Developer

Next Post
facades of buildings in modern town

Neatly organise complex views with a UIView subclass