dasdom

25. Aug 2020

Don't Put View Code Into Your View Controller

Sometimes I see code that sets up subviews in viewDidLoad() of a view controller. Be a good citizen and don't do that. If you have to add something to a view, do it in a UIView subclass.

Here is how I implement view and controller code when I'm not using Interface Builder.

The View

Let's build a login view:

import UIKit

class LoginView: UIView {

  private let usernameTextField: UITextField
  private let passwordTextField: UITextField
  let loginButton: UIButton
  
  override init(frame: CGRect) {
    
    usernameTextField = UITextField()
    usernameTextField.placeholder = NSLocalizedString("Username", comment: "")
    usernameTextField.borderStyle = .bezel
    
    passwordTextField = UITextField(frame: .zero)
    passwordTextField.isSecureTextEntry = true
    passwordTextField.placeholder = NSLocalizedString("Password", comment: "")
    passwordTextField.borderStyle = .bezel
    
    loginButton = UIButton(type: .system)
    loginButton.setTitle(NSLocalizedString("Login", comment: ""), for: .normal)
    loginButton.backgroundColor = .gray
    loginButton.tintColor = .white
    loginButton.layer.cornerRadius = 5
    
    let textFieldStackView = UIStackView(arrangedSubviews: [usernameTextField, passwordTextField])
    textFieldStackView.axis = .vertical
    textFieldStackView.spacing = 10
    textFieldStackView.distribution = .fillEqually
    
    let stackView = UIStackView(arrangedSubviews: [textFieldStackView, loginButton])
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.axis = .vertical
    stackView.spacing = 20
    
    super.init(frame: .zero)
    
    backgroundColor = .white
    
    addSubview(stackView)
    
    let layoutConstraints = [
      stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20),
      stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
      stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
      
      usernameTextField.heightAnchor.constraint(equalToConstant: 30),
      
      loginButton.heightAnchor.constraint(equalToConstant: 40),
    ]
    NSLayoutConstraint.activate(layoutConstraints)
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

extension LoginView {
  func username() -> String {
    return usernameTextField.text ?? "<empty>"
  }
  
  func password() -> String {
    return passwordTextField.text ?? "<empty>"
  }
}

This code defines two text fields and a button and puts it onto the view with the help of UIStackView.

The View Controller

Here comes the view controller:

import UIKit
import os

class LoginViewController: UIViewController {

  private var contentView: LoginView {
    return view as! LoginView
  }
  
  override func loadView() {
    let contentView = LoginView()
    contentView.loginButton.addTarget(self, action: #selector(login(_:)), for: .touchUpInside)
    view = contentView
  }
}

extension LoginViewController {
  @objc func login(_ sender: UIButton) {
    os_log(.info,
           log: .default,
           "username: %{public}@, password: %{private}@",
           contentView.username(), contentView.password())
  }
}

If you build your user interface in code you should override loadView(). Within loadView() you need to set the view property of the view controller. In this example we additionally add a target to the login button.

The so defined login screen looks like this:

You can find the sample code on github. In the sample project you can also find three other possibilities to define the layout and in addition the version using Interface Builder.

Let me know what you think on Twitter: @dasdom.

Tagged with: