In the previous article, we looked at how to avoid messy view controller view layout code, using extensions. We removed all of the view creation and layout code from the view controllers viewDidLoad method using a variety of techniques, all using an extension on the view controller.
Today, we’ll look at how we can encapsulate all of the view creation, configuration and layout within a custom UIView subclass which can be assigned to the view controllers view property. Not only does this keep the view controller clear of any view setup code, it also means the view is now reusable! 🥳
The custom view below has a number of subviews exposed as private(set) to the view controller. They are initialised using closure style syntax to keep the setup and configuration of each view component close to the declaration of the property.
class CustomView: UIView {
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", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemBackground
layoutUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func layoutUI() {
addSubview(stackView)
let stackGap: CGFloat = 32
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: stackGap),
stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: stackGap),
stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -stackGap)
])
stackView.addArrangedSubview(emailTextField)
stackView.addArrangedSubview(passwordTextField)
stackView.addArrangedSubview(confirmTextField)
addSubview(loginButton)
NSLayoutConstraint.activate([
loginButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 44),
loginButton.centerXAnchor.constraint(equalTo: centerXAnchor)
])
}
}
With all of this code neatly tucked away in the UIView subclass, the view controller then simply assigns an instance of this custom view to it’s view property in the loadView method. A reference to the custom view is stored, so that the subviews can be accessed within the view controller.
class CustomViewController: UIViewController {
var customView: CustomView!
override func loadView() {
customView = CustomView()
view = customView
}
override func viewDidLoad() {
super.viewDidLoad()
// access UI via i.e. customView.passwordTextField
}
}
We can take the implementation of the view controller one step further, taking inspiration from this excellent article on view controllers without XIBs by Pádraig. Here, we create a base class for all of our view controllers which takes the custom view class as a generic type. This base class then takes care of the boiler plate code used to initialise the custom view in loadView, and provide a variable through which to access the custom view property.
class XiblessViewController<V: UIView>: UIViewController {
var contentView: V {
return view as! V
}
override func loadView() {
view = V(frame: .zero)
}
init() {
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class GenericViewController: XiblessViewController<CustomView> {
override func viewDidLoad() {
super.viewDidLoad()
// access UI here using contentView.
}
}
Thanks for reading 🙏. You can grab the code for this article from GitHub. Subscribe below to get the latest articles straight to your inbox 🚀.