An elegant way to organize communication between view and view controller
Tags: ios, swift, mobile, architecture
Published: 2023-02-08 21:06:16

## 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](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload) 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.

```swift
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.](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview) Here's what it looks like now:

```swift
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:

1. A lot of UI configuring code in `MyViewController` class
2. `view.label` is inaccessible.

### View encapsulation

A much better solution would be to [encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) 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`**
```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`**
```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:

1. 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.
2. 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`**
```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
    }
}
```

1. [Associated type](https://docs.swift.org/swift-book/LanguageGuide/Generics.html) for view's any type.
2. Non-optional view's reference to access any subview and other properties from view controller.
3. View loading method (more details will be described below).
4. 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`**
```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`**
```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`**
```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](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) 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`**
```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`**
```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
    }
}
```

1. You need to keep a reference to the [XIB file](https://developer.apple.com/documentation/uikit/uinib) to initialize the custom view with this file.
2. 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 class `MyNibContentView`, `MyNibContentView.xib` file must be created. _If you use single bundle you can pass nil to `bundle` parameter._
3. The [bundle](https://developer.apple.com/documentation/foundation/bundle) in which to search for the XIB file.
4. 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`**
```swift
// ...

extension ViewControllerInterface where ContentView: NibViewInterface {
    
    func loadContentView() -> ContentView {
        return ContentView.loadFromNib(owner: self, options: nil)
    }
}
```

**`NibViewController.swift`**
```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`**
```swift
final class MyNibViewController: NibViewController<MyNibContentView> {

    override func viewDidLoad() {
        super.viewDidLoad()
        contentView.label.text = "Hello, World!"
    }
}
```

**`MyNibContentView.swift`**
```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](https://github.com/prokhorovxo/ViewController/tree/main). It is published under the “Unlicense”, which allows you to do whatever you want with it.