DEV Community

ArshTechPro
ArshTechPro

Posted on

Understanding View Lifecycle in UIKit - iOS

What is a View Lifecycle?

In iOS development with UIKit, the view lifecycle refers to the complete sequence of method calls that occur as a view controller goes through different stages of its existence - from initialization to destruction. Understanding this lifecycle is crucial for iOS developers because it determines when and how you should perform specific tasks like setting up UI elements, loading data, responding to user interactions, and cleaning up resources.

Think of the view lifecycle as the "birth-to-death" journey of a view controller. Just like how a person goes through different life stages (birth, childhood, adulthood, etc.), a view controller goes through predictable stages where the system calls specific methods at each phase.

The view lifecycle is important because:

  • It ensures your app's UI appears and behaves correctly
  • It helps manage memory efficiently
  • It provides the right timing for data loading and UI updates
  • It maintains proper app state during navigation and background transitions
  • It prevents premature view loading that can hurt performance

The Complete View Lifecycle Sequence

The UIViewController lifecycle involves multiple phases that UIKit manages automatically.

Phase 1: View Creation

1. loadView()

override func loadView() {
    // Don't call super when overriding!
    let customView = UIView()
    customView.backgroundColor = .systemBackground
    view = customView
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Only once during the entire lifecycle when the view controller's view property is nil and needs to be created
  • Automatically when using Storyboards/XIBs
  • When you first access the view property

What to do here:

  • Only override when creating views programmatically (no Storyboard/XIB)
  • Create your custom view hierarchy
  • Assign the root view to the view property
  • Never call super.loadView() when overriding
  • Never call this method directly

Important Notes:

  • If you are working with storyboards or nib files you do not have to do anything with this method and you can ignore it
  • This is where subclasses should create their custom view hierarchy if they aren't using a nib

2. viewDidLoad()

override func viewDidLoad() {
    super.viewDidLoad() // Always call this first!

    // One-time setup code here
    setupUI()
    configureNavigationBar()
    setupNotificationObservers()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Only once when the view controller's view hierarchy is loaded into memory
  • After loadView() completes its execution
  • Before the view appears on screen

What to do here:

  • One-time setup tasks
  • Configure UI elements (outlets are connected)
  • Set up delegates and data sources
  • Initialize objects used by the view controller
  • Add/remove subviews (when using Storyboards/XIBs)

Important Notes:

  • The view bounds are not yet established, so avoid layout-dependent code
  • All IBOutlets are connected and available

Phase 2: Appearance Cycle (Can happen multiple times)

3. viewWillAppear(_:)

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

    // Refresh data that might have changed
    refreshUserData()
    updateNavigationBar()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Every time the view is about to be added to the view hierarchy
  • Before any appearance animations start
  • View has bounds but orientation is not set yet

What to do here:

  • Refresh data that might have changed while away
  • Update UI based on current state
  • Prepare for the view to become visible
  • Start services that should be active when visible

4. viewIsAppearing(_:) - New in iOS 17 (Back-deployable to iOS 13)

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

    // Perfect place for layout-dependent setup
    configureViewsBasedOnTraits()
    setupLayoutConstraints()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Called after viewWillAppear but before viewDidAppear
  • After the view has been added to the hierarchy but is not yet onscreen
  • The view controller's view has been laid out so you can rely on its size and traits

What to do here:

  • Update UI based on final layout and traits
  • Configure constraints that depend on view size
  • Perfect timing for size-dependent configurations

5. viewWillLayoutSubviews()

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    // Called before Auto Layout runs
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Every time the frame changes (rotation, keyboard appearance, etc.)
  • Before Auto Layout processes constraints

6. viewDidLayoutSubviews()

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    // Called after Auto Layout runs

    // Now view bounds are final
    configureLayoutDependentElements()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • After Auto Layout runs and view bounds are final
  • Multiple times during the view's lifetime

What to do here:

  • Layout-dependent calculations
  • Update frames for views not using Auto Layout
  • Configure elements based on final bounds

7. viewDidAppear(_:)

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

    // Start active processes
    startLocationServices()
    beginVideoPlayback()
    startTimers()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Every time after the view has appeared on screen and is fully visible
  • After appearance animations complete

What to do here:

  • Start animations, and intensive processing
  • Begin location services or other sensors
  • Start video/audio playback
  • Register for notifications that should only be active when visible

Phase 3: Disappearance Cycle

8. viewWillDisappear(_:)

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

    // Prepare to leave
    saveUserInput()
    pauseVideoPlayback()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Every time before the view is about to disappear
  • Before disappearance animations start

What to do here:

  • Save user input or current state
  • Validate and persist data
  • Pause animations
  • Prepare for view to become inactive

9. viewDidDisappear(_:)

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

    // Clean up active processes
    stopLocationServices()
    invalidateTimers()
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • Every time after the view has disappeared from screen
  • After disappearance animations complete

What to do here:

  • Stop animations, and intensive processing
  • Pause video/audio playback
  • Clean up temporary resources
  • Unregister from notifications

Phase 4: Memory Management

10. deinit

deinit {
    // Final cleanup before object is deallocated
    NotificationCenter.default.removeObserver(self)
    // Release any remaining resources
}
Enter fullscreen mode Exit fullscreen mode

When it's called:

  • When the view controller is being deallocated from memory
  • After all strong references are removed

What to do here:

  • Remove observers
  • Release resources not handled by ARC
  • Final cleanup tasks

Complete Lifecycle Sequence Summary

Creation & First Appearance:

  1. loadView() - View creation
  2. viewDidLoad() - View loaded (once only)
  3. viewWillAppear(_:) - About to appear
  4. viewIsAppearing(_:) - Added to hierarchy, layout complete
  5. viewWillLayoutSubviews() - Before layout
  6. viewDidLayoutSubviews() - After layout
  7. viewDidAppear(_:) - Fully visible

Subsequent Appearances:

  • Steps 3-7 repeat each time the view appears

Disappearance:

  1. viewWillDisappear(_:) - About to disappear
  2. viewDidDisappear(_:) - No longer visible

Destruction:

  1. deinit - Object deallocation

Common Pitfalls and Best Practices

1. Always Call Super

override func viewDidLoad() {
    super.viewDidLoad() // Always call this first!
    // Your code here
}
Enter fullscreen mode Exit fullscreen mode

2. Never Access self.view in init()

// WRONG - This will trigger premature view loading!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    view.backgroundColor = .red // This triggers loadView() and viewDidLoad() too early!
}

// CORRECT - Initialize non-view properties only
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    // Only initialize properties that don't require the view
}
Enter fullscreen mode Exit fullscreen mode

3. Use the Right Method for Layout-Dependent Code

// WRONG - Bounds not yet established
override func viewDidLoad() {
    super.viewDidLoad()
    let center = view.center // This might be incorrect
}

// CORRECT - Use viewIsAppearing or viewDidLayoutSubviews
override func viewIsAppearing(_ animated: Bool) {
    super.viewIsAppearing(animated)
    let center = view.center // This is correct
}
Enter fullscreen mode Exit fullscreen mode

4. Don't Override loadView When Using Storyboards

// WRONG - Don't override when using Storyboards/XIBs
override func loadView() {
    super.loadView() // This breaks the loading process
    // Additional setup
}

// CORRECT - Use viewDidLoad for Storyboard-based setup
override func viewDidLoad() {
    super.viewDidLoad()
    // Setup code here
}
Enter fullscreen mode Exit fullscreen mode

5. Proper Memory Management

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

    // Clean up to prevent memory leaks and save battery
    refreshTimer?.invalidate()
    refreshTimer = nil

    // Stop expensive operations
    locationManager.stopUpdatingLocation()
    videoPlayer.pause()
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Complete Sequence: loadView()viewDidLoad()viewWillAppear(_:)viewIsAppearing(_:) → layout methods → viewDidAppear(_:) → disappearance cycle → deinit

  2. One-Time Methods: loadView(), viewDidLoad(), and deinit are called only once

  3. Repeating Methods: Appearance/disappearance methods are called every time the view appears/disappears

  4. Critical Rule: Never access self.view in init() - this causes premature view loading

  5. New in iOS 17: viewIsAppearing(_:) provides the perfect timing for layout-dependent setup

  6. Memory Safety: Always clean up resources in viewDidDisappear(_:) and deinit

  7. Call Super: Always call the superclass implementation in lifecycle methods

Understanding and properly implementing the view lifecycle ensures your iOS apps are performant, responsive, and provide excellent user experiences.

Top comments (1)

Collapse
 
arshtechpro profile image
ArshTechPro

Complete Sequence: loadView() → viewDidLoad() → viewWillAppear(:) → viewIsAppearing(:) → layout methods → viewDidAppear(_:) → disappearance cycle → deinit