UIKit standards
Views
For UI, we try to avoid Storyboards and XIBs as much as possible. Views should be generated and styled in code. This gives us better oversight of changes and layout during code review, avoids painful merge conflicts and automatic changes to storyboard files performed merely by opening them.
For autolayout it's fine to use Apple's implementation as it's become ergonomic enough with time. However, using SnapKit is also an acceptable solution.
As we're adding a lot of layout and styling related code this way, we separate UIViewController and UIView subclasses for our Views.
This means, for e.g. Login flow, we have two files in the same folder:
📁 Login
📄 LoginVC.swift
📄 LoginView.swift
...
View file contains UI, layout and styling code only - defining and adding subviews, adding and managing constraints and defining fonts and colours. It encapsulates all the noise that would else end up in the view controller. View Controller is responsible for handling actions, delegating to View Model and reacting to changes and dictating UI changes to the View when needed.
To install a UIView subclass in UIViewController, we generally define a proxy mainView
property so the concrete type matches and pass it to loadView()
method, e.g.:
class LoginVC: UIViewController {
private lazy var mainView = LoginView()
func loadView() {
view = mainView
}
}
If needed, minor tweaks to the snippet above are acceptable as long as the general principle is fulfilled.
Patterns
In DECODE, we use MVVM-C architecture. This means each screen consists of View, ViewController, ViewModel (defined by protocol) and combined with Coordinator. Coordinator can be one per screen or can contain a sensible number of related screens (representing one complete UI flow). Choice of how many screen one coordinator handles is left to developer's common sense - e.g. settings screen with associated sub-items are fine to be handled by one Coordinator, although particular use case may differ.
Those MVVM-C bundles should be self-contained, e.g. adding an existing flow to another part of the app should basically mean instantiate another instance of coordinator and start it. Deleting a feature means deleting coordinator and associate view (models) with as little as possible interventions elsewhere (apart from removing property and call to start()
method).
More details about the pattern can be found in the following blog posts:
Short Walk-Through: Implementing the MVVM-C pattern Taming the App Flow with Coordinators