facades of buildings in modern town

Neatly organise complex views with a UIView subclass

Creating views programatically helps us avoid some of the pitfalls of Storyboards and Interface Builder. But what is the best way to organise complex view hierarchies?

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 🚀.

Stay in the Loop

Subscribe to tapdev and never miss a post.

Total
0
Shares
Previous Post

Avoid messy view controller layout code with extensions

Next Post
woman looking at the map

Convert array types using the map operator in Combine