UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to move view code out of your view controllers

It's the fastest way to make view controllers simpler

Paul Hudson       @twostraws

Part 3 in a series of tutorials on fixing massive view controllers:

  1. How to use the coordinator pattern in iOS apps
  2. How to move data sources and delegates out of your view controllers
  3. How to move view code out of your 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.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

So, what should be in a view controller?

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:

  • Are responsible for view lifecycle events like viewDidLoad() , viewWillAppear(), and traitCollectionDidChange().
  • Have some @IBOutlets and @IBActions, although those actions should really just run a method elsewhere. Keep in mind that you can add outlets to your views if you want.
  • Shuttle data between my model and my view. This does not add any knowledge such as formatting data – it just binds values to their views and sends changes back from the user.

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.

Where next?

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.

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.3/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.