View Controllers Explained: Ultimate Guide For iOS & Swift

Written by Reinder de Vries on May 22 2019 in App Development

View Controllers Explained: Ultimate Guide For iOS & Swift

View controllers are fundamental building blocks of your iOS app. They govern what happens on-screen, from User Interfaces to animation, from interaction to navigation, and the many steps in between.

In this article, you’ll learn everything you need to know about view controllers. It’ll help you build better apps and learn iOS development more effectively. Mastering view controllers is a crucial step in mastering iOS development!

Here’s what we’ll discuss:

  • What’s a view controller, and what are they used for?
  • How do view controllers and storyboards play well together?
  • The significance of the view controller lifecycle and hierarchy
  • Building navigation and interactivity with navigation controllers
  • Passing data between view controllers with segues
  • View controller outlets and actions, and why they’re useful
  • How you can avoid Massive View Controller

Ready? Let’s go.

  1. What’s A View Controller?
  2. Views, Storyboards, MVC And Controllers
  3. Connecting UIs To Code With View Controller Outlets
  4. Interactive UI With View Controller Actions
  5. Keeping Track: The View Controller Hierarchy
  6. “View Did Load” And The View Controller Lifecycle
  7. The Navigation Controller: Transitioning Between View Controllers
  8. View Controllers, Storyboards And Segues
  9. Avoiding Massive View Controller
  10. Further Reading

What’s A View Controller?

View controllers are fundamental building blocks for any iOS app. Every app has at least one view controller, but most apps have many more.

A view controller typically manages a single User Interface (UI) or “screen” in your app. It also manages interactions between the UI and the underlying data, better known as models.

Let’s take a look at the Swift code for a simple view controller:

class MainViewController: UIViewController
{
    @IBOutlet weak var textLabel:UILabel?

    override func viewDidLoad()
    {
        super.viewDidLoad()

        textLabel?.text = "Click the button..."
    }

    @IBAction func onButtonTap(sender: UIButton)
    {
        textLabel?.text = "Hello world!"
    }
}

View Controller iOS

What’s happening in the code?

  • We’ve created a class called MainViewController. It’s a subclass of UIViewController. The class has one property called textLabel of type UILabel?, an optional.
  • The property textLabel is an outlet. The @IBOutlet keyword means we can make an outlet connection with the label, in the UI, by using Interface Builder. This lets you change the label’s properties, like text, with code.
  • The superclass implementation of the function viewDidLoad() is overridden. This function is part of the view controller life-cycle, and its often the first “touch point” to customize a view controller. In viewDidLoad(), we’re changing the text label to say: “Click the button…”.
  • The function onButtonTap(sender:) is an action, as denoted by the @IBAction keyword. It’s connected to the on-screen button, by using Interface Builder, for the “Touch Up Inside” event. When you tap the button, the function is invoked, which will change the text on the label to “Hello world!”

We’ll discuss classes, outlets, actions, VC life-cycle, and much more, in this article. It may not be easy to grasp at first, and that’s OK. View controllers are definitely simple. Every view controller you’ll encounter in your iOS work is a riff on the example above; a riff on managing the UI!

Every view controller has a view property. As you’ll learn in the next section, view controllers are really controllers of the view. This view is a User Interface (UI) of your app. In code, you can access it with the view property of a UIViewController class.

Check out the different views that MainViewController has:

View hierarchy

See the tree-like hierarchy on the left? Like this:

  • UIView, the root view, with property view
    • UIButton, a subview, which is the on-screen button
    • UILabel, a subview, which is the on-screen label

Every UI element, that’s shown as part of the view controller, belongs in the view hierarchy. A view controller has one root view. This root view typically has many more subviews. Most complex UI elements also have subviews of their own, such as the label of the UIButton element, in the above screenshot.

We’re now starting to discover what a view controller really is. It’s a construct that controls the view. The Swift code that we use to bring a User Interface to life belongs in the view controller. But the view and the controller are also two distinct components: the UI and its governing code.

In the next section, we’ll discuss how a view controller fits within the larger architecture of your app.

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 13 apps with Swift 5 and Xcode 11.

Views, Storyboards, MVC And Controllers

In the previous section, we’ve discussed what view controllers are. We discovered that every view controller has a view property, which is a tree-like hierarchy of User Interface elements.

But, there’s much more to it! Take Storyboards and Model-View-Controller, for example. How do they fit into the picture?

First, we need to take a closer look at the Model-View-Controller architectural pattern. Model-View-Controller, or MVC for short, is a architectural best practice that separates components of your apps into three categories:

  1. Models: Models are wrappers of data, like entries in a database. Every entry can be represented with an instance of a Swift class – the model – with properties and their values.
  2. Views: Views are a representation of a User Interface, as we’ve discussed before. Views are standalone, but they can belong to a hierarchy too. A button is a view, but so is the root view of a view controller.
  3. Controllers: A controller is an intermediary between the view and the model. It moves data from the model to the view, and vice versa. Controllers also include “business logic”, like responding to interactions and managing transitions between view controllers.

The view controller is a mix between a view and a controller. Instead of separating Model, View and Controller, iOS’s take on MVC binds the view and the controller together in the concept of a view controller.

A good example is the UITableViewController class. By default, this class not only shows a table UI visually on screen, but it also manages the data for that table behind the scenes. It’s both a view and a controller; a view controller.

The dual role of the view controller is often criticized, and for good reason. It’s easy to dump a whole lot of code into a view controller. You’d let the view controller do everything in your app’s UI, from organizing the view, to networking, user interaction, and controlling data models. This often results in what’s called Massive View Controller, and we’ll discuss how to avoid it at the end of this article.

In practical iOS development, you don’t create your UIs manually with Swift code. It’s more productive to use XIBs or Storyboards with Interface Builder, to visually build UIs. Interface Builder also has helpful tools to create responsive layouts for different iPhone device models and screen sizes.

XIBs and Storyboards? Here’s what they are:

  • XIB: A XIB is an XML-based User Interface file. It contains information about one UI, including its views and their properties. A XIB is typically connected to a view or view controller class.
  • Storyboard: A Storyboard contains one or more User Interfaces, and they’re similar to XIBs. Storyboards define transitions, called segues, between view controllers.

View controller XIB example in Interface Builder

An important difference between XIBs and storyboards is that a storyboard typically contains more than one view controller. Storyboards also incorporate transitions between those view controllers. A XIB represents the UI information of one view or view controller class, whereas a storyboard represents many view controllers.

You can use XIBs for views and view controllers. The XIB itself just contains information about views and their attributes. A view controller can be represented in a XIB, but so can a UIButton subclass. You can find the class name of a component by using the Identify Inspector in Xcode.

An important concept in XIBs is that of the File’s Owner. The File’s Owner is the class that loads the XIB at runtime, such as a UIViewController subclass. It’s literally the class that “owns” the Interface Builder file. This connection is what binds a view controller to a XIB.

Always wondered what the difference between a XIB and a NIB is? A XIB is an Interface Builder file in editable XML format. A NIB is that same file in a deployment-ready binary format, stored in the app binary package. That’s why Interface Builder always mentions XIBs, and the iOS SDK’s classes and functions talk about “nib” and NIBs.

Connecting UIs To Code With View Controller Outlets

When discussing view controllers, a topic that inevitably comes up is that of outlets and actions. In short, outlets let you connect UI elements to properties of a view controller class. Actions connect UI events, such as a tap, to functions of the view controller class.

Imagine you’re building a view controller. You’ve added a label to the User Interface, with Interface Builder, and you want to dynamically change the text on the label. How can you do that with Swift code?

Here’s the outlet property that we defined earlier, in the MainViewController class:

@IBOutlet weak var textLabel:UILabel?

This is an instance property. Every MainViewController class instance has a property named textLabel, that you can use throughout the class. The @IBOutlet attribute is special here. It’ll “mark” the property textLabel in Interface Builder, indicating that Interface Builder can connect a UI element to this property.

In short, you’ll need two things to connect a UI element to your code:

  1. Outlet Property: The outlet property is the reference for the UI element in your view controller class. You code it with Swift, in Xcode.
  2. Outlet Connection: The outlet connection is what binds a UI element to the outlet property. You create the connection in Interface Builder.

Outlet properties, and their connected UI elements, always need to have the same type! In this example, the type of textLabel is UILabel, which is the same type as the label in the Storyboard.

View controller outlet connection

Here’s what’s happening in the above screenshot:

  • The Label UI element has been selected, and we’ve navigated to the Connections Inspector tab, on the right of Interface Builder.
  • We’re dragging a blue line from the small circle on the right of New Referencing Outlet to Main View Controller. A small dropdown list shows, in which we select the textView property (left highlight).
  • When the outlet connection has been made, we see that textLabel is connected to Main View Controller (right highlight).

And that’s all there is to it! We’ve made a connection between the textLabel property and the Label UI element.

You can use two approaches to make an outlet connection:

  • You can either first select the UI element, go to the Connections Inspector, and then drag from New Referencing Outlet to the View Controller item in the Document Outline.
  • Or, you can select the View Controller item, go to the Connections Inspector, and drag from the outlet property (below Outlets) directly to the UI element in the editor.

When you’re working with a XIB instead of a storyboard, the View Controller item is the File’s Owner. So, instead of dragging to the View Controller item, you drag to File’s Owner. They’re essentially the same thing: the view controller class that “owns” this Interface Builder file.

Before we move on to @IBAction, one last thing: Why is the property textLabel declared with weak var and why is it an optional? Like this:

@IBOutlet weak var textLabel:UILabel?

In the above code, we’ve declared the textLabel property with the weak attribute. Simply said, this means that the UI element and the view controller don’t have a strong connection between them. Neither of them holds “strongly” onto the other. You avoid a strong retain cycle – a memory leak – with this approach.

Because we’re using the weak attribute, the property must also be declared as an optional. After all, when the UILabel is deallocated, the property becomes nil.

Another reason for making outlets optional, is that outlet properties are nil before the viewDidLoad() function, in the view controller life-cycle, has been called (see below). Making the outlet an optional prevents you from accidentally referencing it when it’s nil.

Can you connect UI elements from a XIB or Storyboard to properties of a class, without using outlets? Yes! Although it’s cumbersome, you can use the viewWithTag(_:) function on any superview, such as a view controller’s view property, to find a view with a specific tag. You can set this tag in Interface Builder, by using the Attributes Inspector of a UI element. And of course, you can also traverse a superview’s subview property – an array of UIView objects – to find the view you’re looking for.

Interactive UI With View Controller Actions

OK, let’s move on to @IBAction. In the MainViewController example, we’ve marked a function with the @IBAction attribute. Like this:

@IBAction func onButtonTap(sender: UIButton)
{
    textLabel?.text = "Hello world!"
}

Outlets and actions are similar, because they connect a Storyboard or XIB with your view controller class. Just like outlets connect properties, actions connect functions.

Imagine you’ve added a button to a view controller. A function in the view controller should be invoked when the button is tapped. How do you connect that “tap” to the function? With @IBAction!

The first step is to mark the function with the @IBAction attribute, like this:

@IBAction func onButtonTap(sender: UIButton) { ...

In the above declaration, we’re using two practical conventions:

  • It’s recommended to give the function one parameter named sender, with the type of the UI element you want to connect with, such as UIButton. This parameter is automatically filled when the action is called, with a reference to the “sender” of the tap event. It’s super helpful for figuring out which button triggered an invocation, in case you’ve (deliberately) connected the action to multiple buttons.
  • The function name onButtonTap describes when this function is called, i.e. “on a button tap.” It’s helpful to carefully consider how you’re naming your actions, because they make your code clearer. Don’t just call them hello(), but instead choose a more descriptive name. You can name it specifically onButtonTouchUpInside(sender:), or just onButtonTap(sender:).

Connecting the action to an event is similar to creating an outlet connection. Here’s how:

  1. Select the UI element you want the action to connect to, such as a button
  2. Go to the Connections Inspector tab, on the right of Interface Builder
  3. Find the event you want to connect to, such as Touch Up Inside for a “tap” event
  4. Drag from the small circle on the right of Touch Up Inside to the View Controller item in the Document Outline, on the left of Interface Builder
  5. Finally, select the name of the function in the dropdown list that shows up

Just as before, you can create the same connection using a second approach:

  1. First, select the View Controller item and go to the Connections Inspector tab
  2. Find the name of the function, then drag from the small circle to the appropriate UI element in the editor
  3. Finally, select the right event from the dropdown list that shows up

When you’re working with a XIB instead of a storyboard, the View Controller item is the File’s Owner. So, instead of dragging to the View Controller item, you drag to File’s Owner. They’re essentially the same thing: the view controller class that “owns” this Interface Builder file.

It’s worth noting here that touch events on iOS use a component called UIResponder. In short, every UIResponder subclass can respond to user interaction, such as touch events.

The most common events you’ll want to tap into (pun intended) are:

  • Touch Up Inside, .touchUpInside, a touch-up event inside the UI element, i.e. when the finger has touched the screen and then lifts up again
  • Value Changed, .valueChanged, an event that’s fired when the value of a UI element changes, which is particularly useful for inputs and controls

Keep in mind that, when you’re working with more complex inputs, such as a UITextField or a UIPickerView instance, you’ll likely use delegate functions to respond to specific inputs and value changes.

Can you also connect functions to UI events without making use of @IBActions? Yes! You can use an approach called target-action, to connect for example a .touchUpEvent to a selector named onButtonTap(sender:).

Keeping Track: The View Controller Hierarchy

You can’t just show any view controller on screen, because view controllers need to be part of the view controller hierarchy. View controllers need to be organized in a tree-like structure, just like views and their subviews.

Every iOS app has a main window, and every window has one root view controller. This is the view controller that’s initially shown in the app window. Apps can have more than one window. Windows don’t have any visible content of their own, so the view controller’s view provides all the content for the initial UI of the app.

In the diagram below you can see how the root view controller provides the view for the app’s window.

View Controller Hierarchy 1

In Swift code, that workflow looks like this:

class AppDelegate: UIResponder, UIApplicationDelegate 
{
    var window:UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    {
        let mainViewController = MainViewController()
        mainViewController.title = "Main"

        let frame = UIScreen.main.bounds
        window = UIWindow(frame: frame)

        window!.rootViewController = mainViewController
        window!.makeKeyAndVisible()

        return true
    }
}

This is the application(_:didFinishLaunchingWithOptions:) function of the AppDelegate class. It’s the starting point for your app. In the function, you see how we first create a view controller, then create a window, and then assign the view controller as the rootViewController of the window.

If you’re using storyboards, the above process is done for you. You can set the “Is Initial View Controller” property on any view controller in the storyboard, which marks it as the view controller that’s initially shown in the app’s window.

Most apps use container view controllers, such as UITabBarController, UINavigationController, UISplitViewController and UIPageViewController. Container view controllers organize many view controllers in more manageable interfaces and components. The tab bar controller, for example, lets the user switch between view controllers by using buttons or “tabs” at the bottom of the UI.

View Controller Hierarchy 2 – Navigation Controller

In the above diagram, you can see the view controller hierarchy for a user interface with a UINavigationController. The navigation controller is the root view controller of the window. It has its own root view controller too, and a new view controller that’s pushed onto the navigation controller stack. This is the view controller’s view that’s visible in the app, at the “top” of the stack.

It’s important to note here that any view controller that’s visible on screen must be part of the view controller hierarchy. It doesn’t matter whether you’re presenting a view modally, or working with navigation, or using tabs – every view controller must be part of the view controller hierarchy!

This has two main reasons:

  1. When every view controller is accounted for in a hierarchy, iOS can support these view controllers with appropriate transitions, layout and interactions. A view controller that’s presented modally, for example, can indicate that it wants to take up the whole screen. The UIKit framework can facilitate this layout, because the view controller is part of the hierarchy.
  2. The hierarchy is also traversible – which is a good thing – either via properties like rootViewController or stacks like the viewControllers array. Because of this, you can always find a view controller by traversing the hierarchy at runtime.

And of course, keeping view controllers in a hierarchy also makes working with container view controllers much easier. It’s incredibly productive to just push and pop view controllers onto a navigation stack, for example. We’ll take a look at how navigation controllers work, later on.

“View Did Load” And The View Controller Lifecycle

Alright, let’s discuss the view controller lifecycle! It’s important you get to know how the lifecycle works, because it directly affects how you work with view controllers.

Like any lifecycle, the view controller lifecycle describes what happens in the “life” of a view controller. When it’s loaded, added to the view hierarchy, or removed again. A user interacts with your app, switching between tabs for example, and this triggers the transitions between different states in the lifecycle.

The functions of the view controller lifecycle are perfect for hooking into different customization points. For example, to change an outlet’s properties or to reload data from the network.

Here’s a visual representation of the view controller lifecycle:

View Controller Lifecycle

A view controller’s life begins when it’s initialized, just like any other class. After that, the view controller’s content view – its view property – is created from a storyboard or XIB. When that’s finished, the viewDidLoad() function is called.

The viewDidLoad() function is normally only called once. You override it in your view controller, because that view controller is a subclass of UIViewController. As such, the viewDidLoad() function can be called by iOS when the view has been loaded.

At this point, outlets are guaranteed to have valid values. So, the viewDidLoad() function is often the first touch point for customizing a view controller’s behaviour and User Interface. Within viewDidLoad(), and beyond, you can safely use outlets.

The viewDidLoad() function is not immediately called after a view controller is initialized. Instead, it’s called lazily, when the view property of the view controller is first accessed.

Once viewDidLoad() has been called, the view controller can transition between Appeared and Disappeared states. This involves the following 4 functions:

Will / Pre Did / Post
Appear viewWillAppear(_:) viewDidAppear(_:)
Disappear viewWillDisappear(_:) viewDidDisappear(_:)

Each of these functions has an unnamed parameter called animated of type Bool, that indicates if the view is added or removed with an animation. In general, you should always call the super function in your own overridden implementation, and first (in the function) for the Appear functions and last for the Disappear functions.

In short, the Appear functions are called before/after the view controller’s content view is added to the app’s view hierarchy. The Disappear functions are called before/after the view controller’s content view is removed from the app’s view hierarchy.

In everyday iOS development, these functions are used to hook into a view controller appearing or disappearing. You can do so before a view controller appears or disappears, with the “will” functions, or after, with the “did” functions. You could say, for example, that viewWillAppear() is called just before a view controller is shown on screen.

A common misconception is that these lifecycle functions are called when a view is hidden or obscured, but this isn’t true. When they’re are called is based on the removing or adding of a view controller’s content view to the view hierarchy.

Here’s what these functions do, once more:

  • viewDidLoad() is called when a view controller’s content view, i.e. the view property, is created and loaded from a storyboard or XIB. It’s the primary touch point for customizing a view controller with code.
  • viewWillAppear() is called just before a view controller’s content view is added to the app’s view hierarchy. Differently said, it’s called just before the view controller appears. This function is suitable for (small) operations that need to happen before a view controller is presented on screen.
  • viewDidAppear() is called just after a view controller’s content view is added to the app’s view hierarchy. This function is best used for operations that need to start as soon as a view controller is shown on screen, such fetching data.
  • viewWillDisappear() is called just before a view controller’s content view is removed from the app’s view hierarchy. Differently said, it’s called just before the view controller disappears. This function is suitable for cleaning up a view controller before it disappears, such as hiding the on-screen keyboard or triggering a save or commit operation.
  • viewDidDisappear() is called just after a view controller’s content view is removed from the app’s view hierarchy. This function is best used for teardown operations, such as removing memory-intensive data that can be recreated later.

What are good use cases for these view controller lifecycle functions? Let’s take a look:

  • viewDidLoad() is best used for view controller customization, such as setting the text property of an on-screen messageLabel
  • viewWillAppear() is best used for pre-appearance tasks, such as updating the UI with new information from back-end data models
  • viewDidAppear() is best used for post-appearance tasks, such as checking if a user is logged in, and presenting a LoginViewController
  • viewWillDisappear() is best used for pre-disappearance tasks, such as committing changes to the database and deregistering observers
  • viewDidDisappear() is best used for post-disappearance tasks, such as tearing down the view controller to save memory

Can you see these functions in action? Yes! Create a simple iOS project with one navigation controller or tab bar controller, and fill it up with a few view controller instances. Implement the view controller lifecycle functions, like viewWillAppear() and put a print("\(self) --- \(#function)") statement in there. Start the app, switch between view controllers, and see the flow of calls appear in the Console. Give it a try!

The Navigation Controller: Transitioning Between View Controllers

A topic that’s tangentially related to view controllers, is that of the navigation controller. If you’re building an app that consists of many view controllers, how do you navigate or transition from one to the other? That’s where the navigation controller comes in.

A navigation controller, an instance of class UINavigationController, is essentially a container for multiple view controllers. They’re organized on a stack, using an ordered array. You can navigate horizontally between view controllers, using an approach known as pushing and popping.

Navigation Controller

In the above example, you can see the following interactions:

  1. The app’s main view controller has started with an overview of different images. The user chooses to tap the “#dev” hashtag in the User Interface.
  2. A second view controller is pushed onto the navigation stack, which displays another view controller. The user chooses to tap on an image.
  3. A third view controller is pushed onto the navigation stack, which displays yet another view controller.

The view controllers in the above example are all part of one navigation controller. View controllers are pushed onto the navigation stack, based on user interaction. The navigation controller manages the navigation bar, navigation items, animations, transitions, etc. completely on its own.

When working with a navigation, you’ll typically also deal with the following components:

  • Navigation Bar: The central component in a navigation controller is its navigation bar. You can see it in the above example, at the top of the view controllers. This bar displays the title of a view controller, and you can also use it to go back to a previous view controller. iOS apps use them for many more things, such as primary buttons like Save or Edit.
  • Navigation Item: Every view controller that’s part of a navigation controller needs to have a navigation item. This item is used to display information about a view controller in the navigation bar. The navigation bar will, for example, use the title of the previous view controller to display its name next to the Back button in the navigation bar. You also use the navigation items to display secondary buttons in the navigation bar.
  • Toolbar: A navigation controller can optionally use a second bar at the bottom of the screen – the toolbar. You can add buttons to this toolbar, for secondary actions. Toolbars aren’t common in iOS apps anymore, except for custom keyboard inputs, because app UIs have gotten much more minimal.

In the above screenshots, you can also spot a tab bar, and tab bar controller. This is another container view for view controllers, which displays view controllers using separate switch buttons or “tabs”.

Navigation Controller View Hierarchy

Before you can work with a navigation controller, you’ll have to add one or more view controllers to it. Here’s an example in Swift:

let mainVC = MainViewController()
mainVC.title = "Main"

let navigationController = UINavigationController(rootViewController: mainVC)

window.rootViewController = navigationController

In the above code, an instance of MainViewController is initialized. It’s assigned as the root view controller of the newly created UINavigationController instance (which itself is the root view controller of the app window).

Now that an instance of MainViewController is embedded in a navigation controller, you can use its navigationController property to access its “owning” navigation controller. This navigationController property works out-of-the-box, if a view controller is embedded in a navigation controller – neat!

Now, consider that we want to navigate to a second view controller after the user interacts with the app. Here’s how you do that:

let detailVC = DetailViewController()
detailVC.title = "Detail UI"

navigationController?.pushViewController(detailVC, animated: true)

In the above code, we’re first creating a new instance of DetailViewController. Then, it’s pushed onto the navigation stack with the pushViewController(_:animated:) function call. This will animate a right-to-left transition, effectively navigating from the main view controller to the detail view controller.

What if we want to programmatically move back to the previous view controller? You can do that with the following code:

navigationController?.popViewController(animated: true)

You can also directly pop all the way to the root view controller, with:

navigationController?.popToRootViewController(animated: true)

This pushing and popping view controllers onto the stack is what makes a navigation controller work. It’s a navigation controller’s most important affordance, and a often used approach in practical iOS development.

Navigation controllers are powerful. Make sure you check out the UINavigationControllerDelegate and UINavigationBarDelegate protocols, as well as working with the left, middle and right navigation items. A common pattern is to embed a tab bar controller in a navigation controller, so you can use individual tabs but still “move away” from the tab bar in subsequent navigation.

View Controllers, Storyboards And Segues

View controllers and storyboards are related, so it’s important we discuss them. Storyboards deserve an article on their own, because storyboards are powerful and it’s an extensive topic. In this section, we’ll discuss the relationship between storyboards and view controllers.

In short, you can use storyboards to graphically lay out the user’s path – called flow – through your iOS app. Just like the storyboard of a movie, a storyboard in Xcode describes how we go from one scene to the next.

You’ll work with the following concepts in a storyboard:

  • Scene: A storyboard is made from a sequence of scenes. Every scene represents a view controller and its views.
  • Segue: Scenes are connected with segues. They are the transitions from one scene to the next; from one view controller to the next.

What’s confusing about storyboards, is that we’re already using the above principles in practical iOS development. If a scene is synonymous with a view controller, and a segue is the equivalent of a transition between view controllers, then what do we need storyboards for?

An advantage of using storyboards is organizing many view controllers in one Interface Builder file. But it’s more than that: storyboards also help you organize the “flow” of your app. You can see that flow visually, just like a movie storyboard helps you “see” the scenes and story of a movie.

A common practice in iOS development is passing data between view controllers. Storyboards can help you move data from one view controller to the next, by using segues. Because this moving of data is supported by a framework, like storyboards, you don’t have to design the flow of data from scratch. And that’s more productive.

Storyboards are also useful for prototyping apps. You can quickly mock an app in a storyboard, without writing lots of boilerplate code. This is especially useful for graphic designers, who can build an app with minimal effort – that actually runs on an iPhone or iPad.

Let’s go back to storyboards vs. view controllers. In the previous section, we’ve discussed how view controllers affect the navigation and interactions in your app. Most apps will need to use container view controllers, such as UINavigationController, to facilitate navigation and flow of the app.

Can we use storyboards to create similar functionality? Yes!

Storyboard Segue with View Controllers

Here’s what happens in the above screenshot:

  1. We’ve created a small app with two view controllers, a table view controller and a detail view controller. The TableViewController is wrapped in a UINavigationController, using the Editor -> Embed In -> Navigation Controller menu in Interface Builder.
  2. The table view controller shows a simple list of names. When the user taps any of these names, a detail view controller shows up. It displays which name is tapped.
  3. The detail view controller has one outlet, a label, which is connected to the nameLabel property. The view controller also has a name property, that’s used to fill the label with text.

Let’s first take a look at the code for DetailViewController.

class DetailViewController: UIViewController
{
    @IBOutlet weak var nameLabel:UILabel?

    var name = ""

    override func viewWillAppear(_ animated: Bool)
    {
        super.viewWillAppear(animated)

        nameLabel?.text = "You tapped \(name)"
    }
}

In the code, we’re using the viewWillAppear(_:) function to set the text on the label. As we’ve discussed before, the viewWillAppear(_:) function is called when the view controller’s content view is added to the view hierarchy – or basically when it’s shown on screen.

Then, the TableViewController class. Again, the code is incredibly simple:

let names = ["Ford Prefect", "Arthur Dent", "Zaphod Beeblebrox", "Trillian", "Deep Tought", "Marvin", "Eddie", "Slartibartfast"]

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return names.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

    cell.textLabel?.text = names[indexPath.row]

    return cell
}

In the above code, we’re using a simple array called names to fill up the table view. The table view has only one section, with the same number of rows as there are items in the names array. In the last function, we’re dequeueing a reusable table view cell and assign the appropriate name to the textLabel of the cell using indexPath.row.

Then, in the storyboard, we’ve connected the table view cell to the Show segue of the detail view controller. This will push the detail view controller onto the navigation stack when a table view cell is tapped by the user.

Let’s take a look at creating that segue in more detail:

Connect Segue in Storyboard

Here’s what happens in the above screenshot:

  • First, we’re Control-dragging from the table view cell to the detail view controller. This will create the segue between the cell and the destination view controller. It’s similar to creating an outlet or an action.
  • Then, in the gizmo that pops up, we’ll select Show for the selection segue. You can choose from many default segues, or provide your own custom implementation.
  • Finally, with the table view cell selected in the Document Outline, we can see the connection between the cell and the detail view controller in the Connections Inspector.

The one thing that’s missing is facilitating the segue with code, and passing on some data. So, in the TableViewController class we’ll override the superclass implementation of the prepare(for:sender:) function. Like this:

override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
    if  let viewController = segue.destination as? DetailViewController,
        let index = tableView.indexPathForSelectedRow?.row {

        viewController.name = names[index]
    }
}

The prepare(for:sender:) function is called before starting a segue, which gives us the opportunity to customize it. You can use the segue and sender parameters to find out more about the segue that’s about to take place, such as its destination.

In the code, we’re first using optional binding and type casting to test the type of segue.destination. If this is the segue we want to customize, we know that segue.destination must be of type DetailViewController. We’re also getting the selected index path from the table view.

And finally, inside the conditional, the name property of the detail view controller is set. This assigns the selected name from the table view to the detail view, which will pick it up in its viewWillAppear(_:) function to display the name that’s been tapped. Awesome!

Author’s Note: My personal preference is to use individual XIBs instead of storyboards. Storyboards don’t add much value over separate XIBs. A storyboard with 5+ view controllers quickly gets cluttered, and Interface Builder slows to a crawl. On top of that, segues obscure transitions and interactions between view controllers. With storyboards, view controller coordination “just works,” but it’s often not clear how exactly. This is a disadvantage for beginner iOS developers.

Avoiding Massive View Controller

We’re going to conclude this guide about view controllers by talking about an issue called Massive View Controller.

By now you know that the view controller is part of the Model-View-Controller architectural pattern. It merges the responsibility of the view and controller roles. A storyboard or XIB is used to set up a view and the view controller brings it to life.

Model-View-Controller is often half-jokingly called Massive View Controller, and with good reason. A common anti-pattern in practical iOS development is stuffing all your code in the view controller. After all, MVC dictates that we put the business logic and everything that pertains to the view into the view controller. In day-to-day coding, that means you end up with behemoths of view controllers. A 1000+ line in a view controller is, unfortunately, no exception.

The real reason we dump all the code in a view controller is not that we’re lazy or don’t care about architecture, but because it’s convenient. Most iOS apps are UI-intensive, so it’s only logical that you want to put your code as close as possible to the User Interface. The concept of a view controller doesn’t help. Making one entity responsible for both the view and the controller is a great way to confuse the roles of MVC.

So, how do you put your Massive View Controller on a diet? Let’s discuss a few ideas.

  • Use proper Object-Oriented Programming. An often overlooked part in making view controllers leaner and meaner, is properly applying the principles of Object-Oriented Programming. Are you writing networking code? Why don’t you put it in a separate controller, manager, facilitator, coordinator or helper class? This surely isn’t a sexy approach, but don’t throw the insights from the early days of software development overboard just yet.
  • Use proper design patterns. Delegation, observers, target-action, subclassing, extensions, facilitators, decorators, and even singletons – they have their uses! Invest some time into learning the basics of software design patterns and apply them in your code. You can solve many software development problems with a decent design pattern, including Massive VC. A good start are DRY, SOLID, Observer-Observable and Delegation.
  • Don’t make a view controller the delegate, too. A common pattern in iOS development is setting a component’s delegate property to self, and implementing the delegate protocol inside the current class. This easily leads to a huge view controller, because you keep tacking on new code. It’s smarter to assign different roles to separate classes, such as a table view‘s data source. The same goes for the many managers in the iOS SDK. Why don’t you put your CLLocationManager in a separate class, with a well-designed API?
  • Use extensions. It’s easy to overuse extensions, and it’s often nothing more than a band-aid – but then again, extensions help you split up the different responsibilities of one class into multiple components. If your view controller manages both a table view and a map view, for example, you can use extensions to separate these two concerns.

Perhaps the smartest best practice to avoid Massive View Controller, is to simply not put everything in the view controller! It sounds like a no-brainer, but you don’t have to refactor or debug code you never wrote in the first place. Whenever you add code in a view controller, ask yourself: “Does this code really belong here?”

Become a professional  iOS developer

Get started with iOS 13 and Swift 5

Sign up for our iOS development course Zero to App Store to learn iOS development with Swift 5, and start with your professional iOS career.

Further Reading

Pfew! We’ve come a long way. What you’ve read in this article will give you a good starting point to work with view controllers, and it’s hopefully given you some insight in one of the most important concepts in practical iOS development.

Here’s what we discussed:

  • What’s a view controller, and what are they used for?
  • How do view controllers and storyboards play well together?
  • The significance of the view controller lifecycle and hierarchy
  • Building navigation and interactivity with navigation controllers
  • Passing data between view controllers with segues
  • View controller outlets and actions, and why they’re useful
  • How you can avoid Massive View Controller

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.