Motivation
There are numerous architectures available today for use when developing an iOS app. MVC, MVVM, MVP, VIPER, TCA, and others are examples. These are excellent and practical architectures that are actively used by developers. However, there is one nuance: the organization of view controller and view is frequently contentious. Where should the code for view configuration and setting constraints be kept? Is it better to put it in a separate class or directly in the view controller? How should they communicate?
In this article, I'd like to look at a variety of cases, ranging from the most heinous, in my opinion, to the solution, which I frequently employ in various projects with varying architectures.
Worst case
In the worst case, the code that relates to view configuration in some UIViewController
is located in the viewDidLoad()
method. This is completely wrong in terms of the view controller's lifecycle. viewDidLoad()
method is called after the controller's view is loaded into memory and it's not intended to place elements on the main view. Of course, there are exceptions here, for example, in viewDidLoad()
you can set text for a view's label.
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.textColor = .systemBlue
label.text = "Hello, World!"
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
A little better case
A better solution is to put the view and its subviews initialization code into the loadView()
method. This method loads or creates a view and assigns it to the view property. Here's what it looks like now:
final class MyViewController: UIViewController {
override func loadView() {
let view = UIView()
let label = UILabel()
// Configure label, add it as view's subview and setup its constraints...
self.view = view
}
}
But there is another problem here. What if we want to set different text to the label later on, in viewDidLoad()
method f.e.? Yes, we can store a reference to the label in a view controller's property. But imagine how many such properties there would be if we are talking about a UI with a lot of elements.
Let's move view out to solve these two problems:
- A lot of UI configuring code in
MyViewController
class -
view.label
is inaccessible.
View encapsulation
A much better solution would be to encapsulate the view in a separate class and set it as the view controller's view in the loadView()
method. Here's how it can be done:
MyView.swift
final class MyView: UIView {
let label = UILabel()
init() {
super.init(frame: .zero)
setupViewsAndConstraints()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViewsAndConstraints()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViewsAndConstraints()
}
private func setupViewsAndConstraints() {
// Configure label, add it as view's subview and setup its constraints...
}
}
Once MyView class has been created, let's set this view in the view controller:
MyViewController.swift
final class MyViewController: UIViewController {
// Reference to the view of our custom SomeView type
private var someView: SomeView? {
return view as? SomeView
}
override func loadView() {
self.view = SomeView()
}
override func viewDidLoad() {
super.viewDidLoad()
someView?.label.text = "New text"
}
}
We can now access the label in the view controller to change the text in it at any time. This is already a good result, but there is also space for improvement:
- Boilerplate coding. I mean, we should override
loadView()
method in each new view controller. And also we need to store a reference to the view of specific type as a view controller's property. - Because we have to safely type casting in the view type to access all subviews in a custom view, this view will be an optional type. Unfortunately, it isn't always convenient to work with optional types.
At this point, we came smoothly to a solution that my colleague and I developed several years ago. I use this solution to this day in combination with different architectures.
Implementation
First of all, let's declare a protocol that will describe the view controller's interface:
ViewControllerInterface.swift
protocol ViewControllerInterface: AnyObject {
// 1
associatedtype ContentView
// 2
var contentView: ContentView { get }
// 3
func loadContentView() -> ContentView
}
extension ViewControllerInterface where Self: UIViewController {
// 4
var contentView: ContentView {
return view as! ContentView
}
}
- Associated type for view's any type.
- Non-optional view's reference to access any subview and other properties from view controller.
- View loading method (more details will be described below).
- Default implementation for avoiding boilerplate code and type casting in future.
After ViewControllerInterface
protocol is described, let's implement a base generic class from which all view controllers will inherit in the future:
ViewController.swift
/// Base ViewController, in which view must be defined programmatically in the loadContentView() method
class ViewController<ContentView: UIView>: UIViewController, ViewControllerInterface {
// Empty initializer. Override this for custom initializing
init() {
super.init(nibName: nil, bundle: nil)
}
// We create our UI programmatically only, so we don't need this initialiser.
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Set view controller's view
final override func loadView() {
view = loadContentView()
}
// Method for determining your own view
func loadContentView() -> ContentView {
return ContentView()
}
}
Finally, it's time to refactor MyViewController
and MyView
using the base ViewController
class:
MyViewController.swift
final class MyViewController: ViewController<MyContentView> {
override func viewDidLoad() {
super.viewDidLoad()
// You can access view's properties if you use contentView property instead of view
contentView.label.text = "Hello, World!"
}
}
MyContentView.swift
final class MyContentView: UIView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViewsAndConstraints()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViewsAndConstraints()
}
private func setupViewsAndConstraints() {
// Configure label, add it as view's subview and setup its constraints...
}
}
Now compare the code at the beginning of this article with this one. It's much better, right? I have demonstrated the most basic case, but let's touch on a few more special cases...
Advanced usage of loadContentView() method
Suppose we are working with the MVVM architecture and we want to initialize MyContentView
with some parameters for the initial layout of the UI. These parameters are stored in the view model and we have to pass them to the view during initialization. No problem, we can just pass view model to the view in loadContentView()
method:
MyViewController.swift
final class MyViewController: ViewController<MyContentView> {
private let viewModel: MyViewModelInterface
init(viewModel: MyViewModelInterface) {
self.viewModel = viewModel
super.init()
}
override func loadContentView() -> MyContentView {
return MyContentView(viewModel: viewModel)
}
// ...
}
Custom UIView with XIB file
If you still prefer to layout your UI using Interface Builder, I suggest defining a protocol for such views. Let's write some additional code and create the NibViewInterface
protocol. This protocol will describe the interface for views that are connected with some XIB file:
NibViewInterface.swift
protocol NibViewInterface: AnyObject {
// 1
static var nib: UINib { get }
}
extension NibViewInterface where Self: UIView {
// 2
static var nib: UINib {
return UINib(nibName: String(describing: self), bundle: Self.bundle)
}
}
extension NibViewInterface {
// 3
static var bundle: Bundle {
return Bundle(for: Self.self)
}
// 4
static func loadFromNib(owner: Any? = nil, options: [UINib.OptionsKey: Any]? = nil) -> Self {
return nib.instantiate(withOwner: owner, options: options).first as! Self
}
}
- You need to keep a reference to the XIB file to initialize the custom view with this file.
- Default implementation of the
nib
property. Attention: The name of the XIB file has to be the same as the view class name. For example, for the classMyNibContentView
,MyNibContentView.xib
file must be created. If you use single bundle you can pass nil tobundle
parameter. - The bundle in which to search for the XIB file.
- Method to load a view from the XIB file.
Now let's add support for the view created in the XIB file to our ViewControllerInterface
. And also create a base generic class NibViewController
, which we will inherit in the future to use the view associated with the XIB file:
ViewControllerInterface.swift
// ...
extension ViewControllerInterface where ContentView: NibViewInterface {
func loadContentView() -> ContentView {
return ContentView.loadFromNib(owner: self, options: nil)
}
}
NibViewController.swift
/// Base ViewController, for which a view must be created in the XIB file
class NibViewController<ContentView: UIView & NibViewInterface>: ViewController<ContentView> {
final override func loadContentView() -> ContentView {
return ContentView.loadFromNib(owner: self, options: nil)
}
}
Great, now you can create MyNibViewController
, MyNibContentView
, and the XIB file MyNibContentView.xib
with the same name. Don't forget to link the outlets from the Interface Builder with the swift file:
MyNibViewController.swift
final class MyNibViewController: NibViewController<MyNibContentView> {
override func viewDidLoad() {
super.viewDidLoad()
contentView.label.text = "Hello, World!"
}
}
MyNibContentView.swift
final class MyNibContentView: UIView, NibViewInterface {
// This is outlet from Interface Builder
@IBOutlet weak var label: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// You can setup your view and subviews here
}
}
Thanks for reading!
You can find the source code in this repo. It is published under the “Unlicense”, which allows you to do whatever you want with it.
Top comments (0)