Skip to content

View Controller and Its Life Cycle

A View Controller in UIKit is a fundamental component of iOS app development, responsible for managing a view hierarchy and handling user interactions. It is an instance of the UIViewController class, which provides the infrastructure for managing views, responding to user input, and coordinating with other parts of the app. This document explains the role of a view controller and details its life cycle, including key methods and best practices.

What is a View Controller?

A view controller manages a single view (or a hierarchy of views) and coordinates the presentation of content on the screen. It acts as a mediator between the app’s data (model) and the user interface (view), following the Model-View-Controller (MVC) design pattern.

Key responsibilities:

  • Managing a view hierarchy (via view property).
  • Handling user interactions (e.g., button taps, gestures).
  • Managing navigation and transitions (e.g., presenting or dismissing other view controllers).
  • Responding to system events (e.g., orientation changes, memory warnings).

View Controller Life Cycle

The life cycle of a view controller consists of a series of methods called by the system at specific points during its existence. Understanding these methods is crucial for initializing, configuring, and cleaning up resources appropriately.

Below is a detailed breakdown of the view controller life cycle methods, in the order they are typically called:

1. init(nibName:bundle:) or init(coder:)

  • Purpose: Initializes the view controller.
  • When Called: When the view controller is created programmatically (init(nibName:bundle:)) or from a storyboard/nib (init(coder:)).
  • Usage:
    • Set up initial properties or configurations.
    • Avoid heavy setup here; defer to later life cycle methods.
  • Example:
    swift
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        // Initialize properties
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        // Initialize properties for storyboard
    }

2. loadView()

  • Purpose: Creates or loads the view controller’s view hierarchy.
  • When Called: When the view property is accessed for the first time.
  • Usage:
    • Programmatically create the view hierarchy (do not call super.loadView() if overriding).
    • Avoid loading data or performing complex logic here.
  • Example:
    swift
    override func loadView() {
        view = UIView()
        view.backgroundColor = .white
        // Add subviews programmatically
        let label = UILabel()
        view.addSubview(label)
    }

3. viewDidLoad()

  • Purpose: Performs initial setup after the view is loaded into memory.
  • When Called: Once, after the view is loaded (from a nib, storyboard, or programmatically).
  • Usage:
    • Configure subviews, set up constraints, and initialize data.
    • Safe to access the view property here.
  • Example:
    swift
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        // Set up UI elements and constraints
        let button = UIButton(type: .system)
        button.setTitle("Tap Me", for: .normal)
        view.addSubview(button)
    }

4. viewWillAppear(_:)

  • Purpose: Prepares the view controller before it becomes visible.
  • When Called: Every time the view is about to appear (e.g., during navigation or when presenting modally).
  • Usage:
    • Update UI elements with dynamic data (e.g., refresh a label’s text).
    • Start animations or lightweight tasks.
  • Example:
    swift
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Update UI before appearing
        navigationController?.setNavigationBarHidden(false, animated: animated)
    }

5. viewDidAppear(_:)

  • Purpose: Handles tasks after the view is visible to the user.
  • When Called: Every time the view appears on screen.
  • Usage:
    • Start animations, timers, or network requests.
    • Perform tasks that require the view to be fully visible.
  • Example:
    swift
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Start an animation
        UIView.animate(withDuration: 0.5) {
            self.view.alpha = 1.0
        }
    }

6. viewWillLayoutSubviews()

  • Purpose: Prepares for the view’s subviews to be laid out.
  • When Called: Before the system lays out subviews (e.g., during orientation changes or frame updates).
  • Usage:
    • Adjust layout-related properties or constraints.
    • Avoid heavy computations; focus on layout tweaks.
  • Example:
    swift
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        // Adjust constraints if needed
    }

7. viewDidLayoutSubviews()

  • Purpose: Handles tasks after subviews are laid out.
  • When Called: After the system lays out subviews.
  • Usage:
    • Perform tasks that depend on final view bounds (e.g., positioning subviews programmatically).
  • Example:
    swift
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // Update subview positions based on final bounds
        print("View bounds: \(view.bounds)")
    }

8. viewWillDisappear(_:)

  • Purpose: Prepares the view controller before it is removed from the screen.
  • When Called: Every time the view is about to disappear (e.g., during navigation or dismissal).
  • Usage:
    • Save state, pause animations, or cancel ongoing tasks.
  • Example:
    swift
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Save data or pause tasks
    }

9. viewDidDisappear(_:)

  • Purpose: Cleans up after the view is no longer visible.
  • When Called: Every time the view disappears from the screen.
  • Usage:
    • Perform final cleanup, such as stopping timers or releasing resources.
  • Example:
    swift
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // Clean up resources
    }

10. deinit

  • Purpose: Cleans up when the view controller is deallocated.
  • When Called: When the view controller is removed from memory.
  • Usage:
    • Release resources, such as observers or timers.
    • Ensure no retain cycles exist (e.g., use weak/unowned references).
  • Example:
    swift
    deinit {
        // Remove observers or clean up
        print("ViewController deallocated")
    }

Life Cycle Diagram

Here’s a simplified flow of the view controller life cycle:

init(nibName:bundle:) or init(coder:)

    loadView()

    viewDidLoad()

    viewWillAppear(_:)

    viewWillLayoutSubviews()

    viewDidLayoutSubviews()

    viewDidAppear(_:)

    [View is visible; user interacts]

    viewWillDisappear(_:)

    viewDidDisappear(_:)

    deinit (when deallocated)

Best Practices

  • Minimize Work in loadView: Only create the view hierarchy; defer setup to viewDidLoad.
  • Use viewWillAppear and viewDidAppear for UI Updates: Refresh dynamic content or start animations in these methods.
  • Clean Up in viewWillDisappear or deinit: Stop timers, remove observers, or cancel network requests to prevent memory leaks.
  • Handle Orientation Changes: Use viewWillLayoutSubviews or viewDidLayoutSubviews for layout adjustments.
  • Avoid Retain Cycles: Use [weak self] in closures to prevent memory leaks.
  • Test Life Cycle Methods: Simulate navigation, rotation, and dismissal in the Xcode simulator to ensure correct behavior.

Common Use Cases

  • Data Loading: Fetch data in viewDidLoad or viewWillAppear (depending on whether data needs to refresh on each appearance).
  • Navigation Setup: Configure navigation bar or tab bar in viewWillAppear.
  • Animation: Start animations in viewDidAppear to ensure the view is visible.
  • Resource Cleanup: Cancel network requests or remove observers in viewWillDisappear or deinit.

Example: Complete View Controller

Here’s an example combining multiple life cycle methods:

swift
import UIKit

class ExampleViewController: UIViewController {
    private let label = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        // Set up label
        label.text = "Hello, UIKit!"
        label.textAlignment = .center
        view.addSubview(label)
        
        // Set constraints
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Update UI
        label.textColor = .systemBlue
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Start animation
        UIView.animate(withDuration: 1.0) {
            self.label.alpha = 1.0
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Reset animation
        label.alpha = 0.5
    }
    
    deinit {
        print("ExampleViewController deallocated")
    }
}

Troubleshooting

  • View Not Displaying: Ensure loadView or viewDidLoad correctly sets up the view hierarchy.
  • Constraints Breaking: Check for conflicting constraints or missing translatesAutoresizingMaskIntoConstraints = false.
  • Memory Leaks: Verify deinit is called; use Instruments to detect retain cycles.
  • Unexpected Behavior: Add print statements to life cycle methods to trace execution.

Resources

Released under the MIT License.