It's the fastest way to make view controllers simpler
Part 3 in a series of tutorials on fixing massive view controllers:
There are many good reasons for wanting to write your user interface in code rather than using Interface Builder: you might find it easier to use source control, you might find it a more expressive environment for writing complex Auto Layout constraints, or you might prefer to create interfaces by composing functions.
However, there’s a right way and a wrong way to do this, and too many apps make the wrong decision. You see, even though you should treat your view controllers as part of your view layer rather than your controller layer (the V in MVC rather than the C), they still ought to leave actual view code to UIView
and its many subclasses.
Although it should be self-evident that view code should be handled by UIView
or its subclasses, you’ll often see all sorts of view creation and configuration right inside the viewDidLoad()
method of view controllers, which is almost certainly the wrong place for it. That method is called when your view has finished loading, not when it’s time for you to start creating it.
Moving such code into a custom UIView
subclass will not only make your view controllers simpler, but it will also allow you to re-use your view code in different places as needed. Even better, once your view controllers are made smaller and simpler you can start to lean more heavily on view controller containment to make them reusable too, with the end result being more flexible code that’s loosely coupled too.
Rather than discuss all this in the abstract, let’s look at a specific example of how folks mix up views and view controllers. Cast your eyes over this monstrosity:
backgroundColor = UIColor(white: 0.9, alpha: 1)
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 10
view.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
stackView.axis = .vertical
let notice = UILabel()
notice.numberOfLines = 0
notice.text = "Your child has attempted to share the following photo from the camera:"
stackView.addArrangedSubview(notice)
let imageView = UIImageView(image: shareImage)
stackView.addArrangedSubview(imageView)
let prompt = UILabel()
prompt.numberOfLines = 0
prompt.text = "What do you want to do?"
stackView.addArrangedSubview(prompt)
for option in ["Always Allow", "Allow Once", "Deny", "Manage Settings"] {
let button = UIButton(type: .system)
button.setTitle(option, for: .normal)
stackView.addArrangedSubview(button)
}
That’s not even a complex user interface, but it’s the kind of thing you’ll see in viewDidLoad()
even though that’s a terrible place to put it.
All the code above – literally all of it – is view code, and needs to be treated as such. It is not controller code, and even with Apple’s muddled definition it is not view controller code either. It’s view code, and belongs in a subclass of UIView
.
This change is trivial to make: you copy all that code, paste it into a new subclass of UIView
called SharePromptView
, then change the class of your view controller’s view to your new subclass.
The final SharePromptView
class should look something like this:
class SharePromptView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubviews()
}
func createSubviews() {
// all the layout code from above
}
}
All UIView
subclasses must implement init(coder:)
, but as you’re creating your UI in code you will also need to add init(frame:)
. The createSubviews()
method is there to support both.
Thanks to that custom UIView
subclass you can now take a huge amount of code out of your view controller:
class ViewController: UIViewController {
var shareView = SharePromptView()
override func loadView() {
view = shareView
}
}
The loadView()
method is the correct place to load your view programmatically.
Note: having a dedicated shareView
property allows you to access any properties you declare inside SharePromptView
without having to keep casting view
.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
This is a question I get asked a lot. I’ve already talked about how navigation can be done using coordinators, and how to separate delegates and source sources from view controllers, so what should view controllers do?
In my own code, I strive to make view controllers as simple as possible, because I’ve seen first hand what happens when get grow out of control. That means they:
viewDidLoad()
, viewWillAppear()
, and traitCollectionDidChange()
.They may also handle model fetching and storage depending on what I’m doing; I’m pretty pragmatic about that.
As Dave DeLong puts it, “300-line view controllers or bust.” Nothing bad happens when you write line 301, and certainly I wouldn’t go refactoring a view controller into extensions just to get SwiftLint off my back, but it might be indicative that you’re overloading your view controller with responsibilities.
If you found this article interesting then you should definitely read my book Swift Design Patterns – it’s packed with similar tips for ways to simplify, streamline, and uncouple your components.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.