Skip to main content

Swift View Controllers

Introduction

View controllers are one of the fundamental building blocks of iOS applications. They manage a portion of your app's user interface and coordinate the flow of information between your data model and the views that display that data. In iOS development using Swift, understanding view controllers is essential for creating well-structured, maintainable apps.

In this lesson, we'll explore what view controllers are, how they function within the iOS architecture, and how to implement them effectively in your Swift applications.

What Are View Controllers?

View controllers are instances of the UIViewController class or its subclasses. They serve as the intermediaries between your app's data model and its view objects. Every visible screen in an iOS app is managed by at least one view controller.

Key responsibilities of view controllers include:

  1. View Management: Loading, configuring, and displaying views
  2. Event Handling: Responding to user interactions
  3. Resource Management: Managing the life cycle of views and related resources
  4. Navigation: Coordinating transitions between different parts of your user interface
  5. Data Coordination: Moving data between your model objects and views

The View Controller Life Cycle

View controllers go through a specific sequence of events as they are loaded, appear, disappear, and are unloaded from memory. Understanding this life cycle is crucial for proper app development.

Here's a diagram of the main life cycle methods:

viewDidLoad() → viewWillAppear() → viewDidAppear() → viewWillDisappear() → viewDidDisappear() → viewDidUnload()

Let's examine each of these methods:

viewDidLoad()

This method is called after the view controller has loaded its view hierarchy into memory. This is a good place to perform one-time setup.

swift
override func viewDidLoad() {
super.viewDidLoad()

// Setup code that needs to run once
setupUserInterface()
loadInitialData()
}

viewWillAppear(_:)

Called just before the view controller's view is added to the view hierarchy and becomes visible.

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

// Prepare view before it becomes visible
updateUIWithLatestData()
startAnimations()
}

viewDidAppear(_:)

Called after the view controller's view has been added to the view hierarchy and is visible.

swift
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

// Code to execute after view is visible
startLocationUpdates()
analyticsService.logScreenView("Home Screen")
}

viewWillDisappear(_:)

Called just before the view controller's view is removed from the view hierarchy.

swift
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// Cleanup before view disappears
pauseTimers()
saveUserState()
}

viewDidDisappear(_:)

Called after the view controller's view has been removed from the view hierarchy.

swift
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)

// Cleanup after view is gone
stopLocationUpdates()
releaseExpensiveResources()
}

Creating a Basic View Controller

Let's create a simple view controller to display a welcome message and a button:

swift
import UIKit

class WelcomeViewController: UIViewController {

// MARK: - Properties
private let welcomeLabel = UILabel()
private let actionButton = UIButton(type: .system)

// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}

// MARK: - UI Setup
private func setupUI() {
// Configure view
view.backgroundColor = .white

// Configure welcome label
welcomeLabel.text = "Welcome to My App!"
welcomeLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
welcomeLabel.textAlignment = .center
welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(welcomeLabel)

// Configure action button
actionButton.setTitle("Get Started", for: .normal)
actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside)
actionButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(actionButton)

// Setup constraints
NSLayoutConstraint.activate([
welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
welcomeLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50),

actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
actionButton.topAnchor.constraint(equalTo: welcomeLabel.bottomAnchor, constant: 40)
])
}

// MARK: - Actions
@objc private func actionButtonTapped() {
print("Button tapped! User wants to get started.")
// Navigate to the next screen or perform an action
}
}

To use this view controller, you would typically set it as the initial view controller in your app's SceneDelegate or present it from another view controller:

swift
// In SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }

let window = UIWindow(windowScene: windowScene)
window.rootViewController = WelcomeViewController()
self.window = window
window.makeKeyAndVisible()
}

Types of View Controllers

iOS offers several specialized types of view controllers for different purposes:

Content View Controllers

These display content and are the most common type. Examples include:

  • UIViewController: The base class
  • UITableViewController: For displaying data in rows
  • UICollectionViewController: For displaying data in customizable layouts

Container View Controllers

These manage and coordinate multiple child view controllers:

  • UINavigationController: Manages a stack of view controllers
  • UITabBarController: Manages multiple view controllers with tab-based navigation
  • UISplitViewController: Manages master-detail interfaces (especially on iPad)

Let's see how to implement a simple UITableViewController:

swift
import UIKit

class FruitListViewController: UITableViewController {

// Data source
let fruits = ["Apple", "Banana", "Orange", "Strawberry", "Mango"]

override func viewDidLoad() {
super.viewDidLoad()
title = "Fruits"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "FruitCell")
}

// MARK: - Table View Data Source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FruitCell", for: indexPath)
cell.textLabel?.text = fruits[indexPath.row]
return cell
}

// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let selectedFruit = fruits[indexPath.row]
print("Selected fruit: \(selectedFruit)")

// Show an alert with the selected fruit
let alert = UIAlertController(
title: "Fruit Selected",
message: "You selected \(selectedFruit)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}

View Controller Communication

View controllers often need to communicate with each other. There are several patterns for this:

1. Direct Property Access

swift
// In the source view controller
let detailVC = DetailViewController()
detailVC.itemToDisplay = selectedItem
navigationController?.pushViewController(detailVC, animated: true)

2. Delegation Pattern

swift
// Define a protocol in the second view controller
protocol ItemSelectionDelegate: AnyObject {
func didSelectItem(_ item: Item)
}

// In the second view controller
class ItemSelectionViewController: UIViewController {
weak var delegate: ItemSelectionDelegate?

func userSelectedItem(_ item: Item) {
delegate?.didSelectItem(item)
dismiss(animated: true)
}
}

// In the first view controller
class MainViewController: UIViewController, ItemSelectionDelegate {
func showItemSelection() {
let selectionVC = ItemSelectionViewController()
selectionVC.delegate = self
present(selectionVC, animated: true)
}

// Delegate method
func didSelectItem(_ item: Item) {
// Handle the selected item
updateUI(with: item)
}
}

3. Closure-Based Callbacks

swift
class ProductViewController: UIViewController {
var onProductSelected: ((Product) -> Void)?

func selectButtonTapped() {
let selectedProduct = // get the product
onProductSelected?(selectedProduct)
dismiss(animated: true)
}
}

// Using the callback
let productVC = ProductViewController()
productVC.onProductSelected = { [weak self] product in
self?.handleSelectedProduct(product)
}
present(productVC, animated: true)

4. Notification Center

swift
// Post a notification
NotificationCenter.default.post(
name: Notification.Name("UserSelectedItem"),
object: nil,
userInfo: ["item": selectedItem]
)

// Observe a notification
NotificationCenter.default.addObserver(
self,
selector: #selector(handleItemSelection(_:)),
name: Notification.Name("UserSelectedItem"),
object: nil
)

@objc func handleItemSelection(_ notification: Notification) {
if let item = notification.userInfo?["item"] as? Item {
// Handle the selected item
}
}

Real-World Example: A Complete To-Do List App

Let's put everything together in a more complex example. Here's a simple to-do list app with two view controllers:

swift
// Task model
struct Task {
let id = UUID()
var title: String
var isCompleted: Bool = false
}

// Main task list view controller
class TaskListViewController: UITableViewController {
var tasks: [Task] = [
Task(title: "Learn Swift"),
Task(title: "Study View Controllers"),
Task(title: "Build a sample app")
]

override func viewDidLoad() {
super.viewDidLoad()
title = "My Tasks"

// Setup UI
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TaskCell")

// Add button to navigation bar
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .add,
target: self,
action: #selector(addTaskTapped)
)
}

@objc func addTaskTapped() {
let addTaskVC = AddTaskViewController()
addTaskVC.onTaskAdded = { [weak self] taskTitle in
let newTask = Task(title: taskTitle)
self?.tasks.append(newTask)
self?.tableView.reloadData()
}
let navController = UINavigationController(rootViewController: addTaskVC)
present(navController, animated: true)
}

// MARK: - Table View Data Source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}

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

cell.textLabel?.text = task.title
cell.accessoryType = task.isCompleted ? .checkmark : .none

return cell
}

// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)

// Toggle completion status
tasks[indexPath.row].isCompleted.toggle()
tableView.reloadRows(at: [indexPath], with: .automatic)
}

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
}

// Add task view controller
class AddTaskViewController: UIViewController {
private let taskTextField = UITextField()
var onTaskAdded: ((String) -> Void)?

override func viewDidLoad() {
super.viewDidLoad()
title = "Add New Task"
view.backgroundColor = .white

// Setup UI
setupTextField()
setupButtons()
}

private func setupTextField() {
taskTextField.placeholder = "Enter task title"
taskTextField.borderStyle = .roundedRect
taskTextField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(taskTextField)

NSLayoutConstraint.activate([
taskTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
taskTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
taskTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}

private func setupButtons() {
// Save button
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .save,
target: self,
action: #selector(saveTask)
)

// Cancel button
navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(cancel)
)
}

@objc private func saveTask() {
guard let taskTitle = taskTextField.text, !taskTitle.isEmpty else {
// Show an alert for empty task
let alert = UIAlertController(
title: "Error",
message: "Task title cannot be empty",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}

onTaskAdded?(taskTitle)
dismiss(animated: true)
}

@objc private func cancel() {
dismiss(animated: true)
}
}

To use this app, you would set up a navigation controller in your SceneDelegate:

swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }

let window = UIWindow(windowScene: windowScene)
let taskListVC = TaskListViewController()
let navigationController = UINavigationController(rootViewController: taskListVC)

window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
}

Best Practices for View Controllers

  1. Keep View Controllers Focused: Each view controller should have a single responsibility.

  2. Avoid Massive View Controllers: Extract code into helper classes, extensions, and separate components.

  3. Use Child View Controllers: Compose complex UIs from multiple smaller view controllers.

  4. Separate Data and Presentation Logic: Use view models or presenters to keep view controllers lighter.

  5. Prefer Composition Over Inheritance: Instead of subclassing view controllers, use composition patterns.

  6. Be Mindful of Memory Management: Properly handle memory in view controller life cycle methods.

  7. Use Storyboards and XIBs Wisely: Understand the trade-offs between programmatic UI and Interface Builder.

Summary

View controllers are a cornerstone of iOS development with Swift. They manage your app's user interface, handle user interactions, and coordinate the flow of data between your model and views. In this lesson, we covered:

  • The purpose and responsibilities of view controllers
  • The view controller life cycle
  • Creating basic view controllers
  • Different types of view controllers (content and container)
  • Communication between view controllers
  • A real-world task list app example
  • Best practices for working with view controllers

By mastering view controllers, you'll be well on your way to creating robust, well-structured iOS applications.

Additional Resources

Exercises

  1. Create a profile view controller that displays user information and allows editing through a second view controller.

  2. Implement a navigation-based app with at least three levels of navigation.

  3. Create a tab bar application with at least three different tabs, each containing different content.

  4. Implement a master-detail interface using UISplitViewController (challenging for iPad interfaces).

  5. Refactor the to-do list app to use proper architecture (like MVVM or MVP) to separate concerns.

By completing these exercises, you'll gain practical experience in working with view controllers in Swift.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)