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.
- Programmatically create the view hierarchy (do not call
- 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 toviewDidLoad
. - Use
viewWillAppear
andviewDidAppear
for UI Updates: Refresh dynamic content or start animations in these methods. - Clean Up in
viewWillDisappear
ordeinit
: Stop timers, remove observers, or cancel network requests to prevent memory leaks. - Handle Orientation Changes: Use
viewWillLayoutSubviews
orviewDidLayoutSubviews
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
orviewWillAppear
(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
ordeinit
.
Example: Complete View Controller
Here’s an example combining multiple life cycle methods:
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
orviewDidLoad
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.