Introduction

Welcome to Chapter 5! In the previous chapters, you’ve learned how to set up your development environment and build basic user interfaces using both UIKit and SwiftUI. Now, it’s time to bring your app to life by understanding how it behaves from launch to termination, how users move between different screens, and how data flows between these screens.

Understanding the app lifecycle is crucial for building robust applications that manage resources efficiently and respond correctly to system events, like incoming calls or backgrounding. Navigation is the backbone of any multi-screen app, defining the user’s journey. Finally, mastering basic data flow is essential for ensuring your app’s different parts can communicate and share information effectively.

By the end of this chapter, you’ll have a solid grasp of these foundational concepts, enabling you to build more interactive and professional iOS applications. Get ready to make your apps smarter and more dynamic!

The iOS App Lifecycle

Every iOS application goes through a series of states from the moment it’s launched until it’s finally terminated. This journey is known as the App Lifecycle. Understanding these states and how your app transitions between them is vital for managing resources, saving user data, and providing a smooth user experience.

Imagine your app as a person:

  • Not Running: The app isn’t launched or was terminated. (The person is asleep.)
  • Inactive: The app is running but not receiving events. This often happens briefly when a phone call comes in or the user pulls down the Notification Center. (The person is awake but paused.)
  • Active: The app is running, in the foreground, and fully interacting with the user. This is the normal operating state. (The person is actively working or playing.)
  • Background: The app is no longer in the foreground but is still executing code. It might be performing a short task, playing audio, or fetching data. (The person is in another room, doing something quietly.)
  • Suspended: The app is in the background but no longer executing code. The system can terminate suspended apps at any time to free up memory. (The person has left the house, and their work is paused.)

AppDelegate and SceneDelegate

For modern iOS applications (targeting iOS 13 and later), the responsibilities of managing the app’s lifecycle are split between two key objects: AppDelegate and SceneDelegate.

  • AppDelegate: Primarily handles application-level events, such as app launch, termination, push notification registration, and responding to memory warnings. It’s the entry point for your app’s process.
  • SceneDelegate: Manages the lifecycle of a scene, which represents a single instance of your app’s UI. This is particularly important for iPad apps that support multiple windows (scenes) of the same application. For iPhone apps, you typically have one scene. The SceneDelegate handles events like a scene becoming active, entering the background, or being disconnected.

Let’s visualize the common transitions for a single-scene iPhone app:

flowchart TD A[Not Running] --> B{Launch App} B -->|\1| C[SceneDelegate: sceneWillConnect] C --> D[SceneDelegate: sceneWillEnterForeground] D --> E[SceneDelegate: sceneDidBecomeActive] E -->|\1| F[Active] F -->|\1| G[SceneDelegate: sceneWillResignActive] G --> H[SceneDelegate: sceneDidEnterBackground] H --> I[Background] I -->|\1| J[Suspended] J -->|\1| A H -->|\1| A F -->|\1| K[Inactive] K -->|\1| F F -->|\1| L[SceneDelegate: sceneWillTerminate] L --> A

What happens when you launch an app:

  1. The AppDelegate’s application(_:didFinishLaunchingWithOptions:) method is called. This is your app’s initial setup point.
  2. The system asks the AppDelegate to create a new UISceneSession.
  3. The SceneDelegate’s scene(_:willConnectTo:options:) method is called. This is where you configure the initial UI for your scene, typically by setting up the window and its rootViewController.
  4. The SceneDelegate’s sceneWillEnterForeground(_:) is called.
  5. Finally, sceneDidBecomeActive(_:) is called, and your app is ready for user interaction.

Hands-on: Observing the App Lifecycle

Let’s open Xcode and create a new project to see these lifecycle methods in action.

  1. Open Xcode 16.x (or later, as of 2026).
  2. Choose “Create a new Xcode project”.
  3. Select the “iOS” tab, then “App” and click “Next”.
  4. Configure your project:
    • Product Name: LifecycleDemo
    • Interface: Storyboard (We’ll start with UIKit for lifecycle, then touch SwiftUI navigation).
    • Language: Swift
  5. Click “Next” and save your project.

Now, let’s add some print statements to our SceneDelegate.swift file.

Open SceneDelegate.swift. You’ll find several commented-out or empty methods. Let’s fill them in.

// SceneDelegate.swift

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        print("SceneDelegate: scene willConnectTo session")
    }

    // Called when the scene has moved from an inactive state to an active state.
    // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    func sceneDidBecomeActive(_ scene: UIScene) {
        print("SceneDelegate: sceneDidBecomeActive")
    }

    // Called when the scene will move from an active state to an inactive state.
    // This may occur due to temporary interruptions (ex. an incoming phone call).
    func sceneWillResignActive(_ scene: UIScene) {
        print("SceneDelegate: sceneWillResignActive")
    }

    // Called as the scene transitions from the background to the foreground.
    // Use this method to undo the changes made on entering the background.
    func sceneWillEnterForeground(_ scene: UIScene) {
        print("SceneDelegate: sceneWillEnterForeground")
    }

    // Called as the scene transitions from the foreground to the background.
    // Use this method to save data, release shared resources, and store enough scene-specific state information
    // to restore the scene back to its current state.
    func sceneDidEnterBackground(_ scene: UIScene) {
        print("SceneDelegate: sceneDidEnterBackground")
    }

    // Called when the scene is being released by the system.
    // This occurs shortly after the scene enters the background, or when its session is discarded.
    // Free any resources that were specific to the discarded scene, as they will not return.
    func sceneDidDisconnect(_ scene: UIScene) {
        print("SceneDelegate: sceneDidDisconnect (Scene is being released)")
    }
}

Explanation: We’ve added print statements to five key UISceneDelegate methods. Each method is called at a specific point in the scene’s lifecycle, providing you with opportunities to perform actions like saving data, pausing animations, or refreshing content.

Now, run your app on a simulator (Cmd+R). Observe the Xcode console. You should see output similar to this:

SceneDelegate: scene willConnectTo session
SceneDelegate: sceneWillEnterForeground
SceneDelegate: sceneDidBecomeActive

Try this:

  1. While the app is running (and active), press the Home button on the simulator (or Cmd+Shift+H).
    • Observe the console: You should see sceneWillResignActive followed by sceneDidEnterBackground.
  2. Tap the app icon on the simulator’s home screen to bring it back to the foreground.
    • Observe the console: You should see sceneWillEnterForeground followed by sceneDidBecomeActive.
  3. Terminate the app: In Xcode, click the Stop button (square icon) next to the run button.
    • Observe the console: You should see sceneDidDisconnect (though sometimes this might not fire if the app is forcibly terminated by Xcode, but it would for a user swipe-to-kill).

This hands-on exercise gives you a concrete understanding of how your app transitions between states.

Once your app is running, users need a way to move between different screens or views. iOS provides several standard navigation patterns to facilitate this.

UIKit Navigation

UIKit, Apple’s older but still widely used UI framework, offers robust navigation controllers.

1. Navigation Controller (UINavigationController)

This is the most common navigation pattern. It manages a stack of view controllers. When you push a new view controller onto the stack, it appears on top. When you pop a view controller, it’s removed, and the previous one reappears. Think of it like a deck of cards.

  • Push: Add a new card to the top.
  • Pop: Remove the top card, revealing the one beneath.

UINavigationController automatically provides a navigation bar at the top, which can display a title, back button, and custom bar button items.

2. Tab Bar Controller (UITabBarController)

A UITabBarController allows you to switch between different, distinct sections of your app. Each tab typically leads to its own navigation stack. Think of it like switching between different apps on your phone or different sections in a web browser.

3. Modal Presentation

Sometimes you need to present a view controller temporarily, perhaps to gather information or show a warning, without changing the main navigation flow. This is called modal presentation. The presented view controller typically covers a portion or the entirety of the presenting view controller. It must be explicitly dismissed.

SwiftUI Navigation (Modern Approach)

SwiftUI, Apple’s declarative UI framework, has evolved its navigation patterns significantly.

Introduced in iOS 16, NavigationStack is the modern and preferred way to manage hierarchical navigation in SwiftUI. It works similarly to UINavigationController by managing a stack of views, but it’s fully declarative.

You use NavigationLink within a NavigationStack to trigger transitions.

// Example of NavigationStack in SwiftUI (Conceptual)
struct ContentView: View {
    var body: some View {
        NavigationStack { // Manages the navigation stack
            VStack {
                Text("Welcome to the Home Screen!")
                NavigationLink("Go to Detail View") { // Triggers navigation
                    DetailView() // The destination view
                }
            }
            .navigationTitle("Home") // Sets title for the current view
        }
    }
}

struct DetailView: View {
    var body: some View {
        Text("You are in the Detail View!")
            .navigationTitle("Detail")
    }
}

Important Note: Before iOS 16, NavigationView was used. However, NavigationView is now deprecated and NavigationStack should be used for all new projects targeting iOS 16 and above. For apps needing to support older iOS versions (e.g., iOS 13-15), you might still encounter NavigationView or need to use conditional compilation.

Basic Data Flow: Passing Data Between Views

An app often needs to pass information from one screen to another. For example, a list of items might need to pass the selected item’s ID to a detail screen.

UIKit Data Flow: Property Injection

The most straightforward way to pass data forward (from a presenting view controller to a presented one) in UIKit is through property injection. This means setting a public property on the destination view controller before presenting it.

Let’s illustrate this with our LifecycleDemo project.

Step-by-Step: UIKit Navigation and Data Passing

First, we’ll set up two simple UIViewControllers.

  1. Open Main.storyboard:

    • You’ll see a ViewController already there (let’s call it ViewControllerA).
    • Drag another View Controller from the Object Library (bottom right, or Cmd+Shift+L) onto the canvas. Let’s call this ViewControllerB.
    • Select ViewControllerB. In the Identity Inspector (right panel, third tab), set its Storyboard ID to ViewControllerBIdentifier. This allows us to instantiate it programmatically.
    • Also, in the Attributes Inspector (right panel, fourth tab), change its Background to something distinct, like light gray, so we can clearly see the transition.
  2. Embed ViewControllerA in a Navigation Controller:

    • Select ViewControllerA in the storyboard.
    • Go to Editor -> Embed In -> Navigation Controller.
    • You’ll now see a Navigation Controller scene preceding ViewControllerA. This is crucial for push/pop navigation.
  3. Add UI Elements to ViewControllerA:

    • Select ViewControllerA.
    • Drag a UILabel onto it. Set its text to “Welcome to View A!”.
    • Drag a UIButton onto it. Set its text to “Go to View B”.
    • Center both horizontally.
  4. Create a new Swift file for ViewControllerB:

    • Right-click on your project folder in the Project Navigator (left panel).
    • Choose New File... -> Cocoa Touch Class.
    • Class: ViewControllerB
    • Subclass of: UIViewController
    • Language: Swift
    • Click Next and Create.
  5. Connect ViewControllerB to its Storyboard scene:

    • Go back to Main.storyboard.
    • Select ViewControllerB.
    • In the Identity Inspector, set its Class to ViewControllerB.
  6. Add a Label to ViewControllerB to display passed data:

    • Select ViewControllerB in the storyboard.
    • Drag a UILabel onto it. Set its text to “Data from A: “.
    • Center it horizontally.
    • Open ViewControllerB.swift side-by-side with the storyboard (Assistant Editor).
    • Ctrl-drag from the UILabel in ViewControllerB to the ViewControllerB.swift file, just below the class declaration.
    • Create an IBOutlet named dataLabel.
  7. Add a property to ViewControllerB to receive data:

    • In ViewControllerB.swift, add a property to hold the data.
    // ViewControllerB.swift
    
    import UIKit
    
    class ViewControllerB: UIViewController {
    
        @IBOutlet weak var dataLabel: UILabel!
        var receivedData: String? // This property will hold the data passed from ViewControllerA
    
        override func viewDidLoad() {
            super.viewDidLoad()
            title = "View B" // Set the title for the navigation bar
    
            // Update the label with the received data
            if let data = receivedData {
                dataLabel.text = "Data from A: \(data)"
            } else {
                dataLabel.text = "No data received."
            }
        }
    }
    
  8. Implement Navigation and Data Passing in ViewControllerA:

    • Open ViewController.swift (which is your ViewControllerA).
    • Open Main.storyboard side-by-side.
    • Ctrl-drag from the “Go to View B” button in ViewControllerA to the ViewController.swift file.
    • Create an IBAction named goToViewBButtonTapped.
    // ViewController.swift
    
    import UIKit
    
    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            title = "View A" // Set the title for the navigation bar
        }
    
        @IBAction func goToViewBButtonTapped(_ sender: UIButton) {
            // 1. Get a reference to the storyboard
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
    
            // 2. Instantiate ViewControllerB using its Storyboard ID
            // We use `instantiateViewController(withIdentifier:)` and cast it.
            // It's important to use `guard let` for safety.
            guard let viewControllerB = storyboard.instantiateViewController(withIdentifier: "ViewControllerBIdentifier") as? ViewControllerB else {
                print("Error: Could not instantiate ViewControllerB from storyboard.")
                return
            }
    
            // 3. Pass data to ViewControllerB's 'receivedData' property
            viewControllerB.receivedData = "Hello from View A!"
    
            // 4. Push ViewControllerB onto the navigation stack
            navigationController?.pushViewController(viewControllerB, animated: true)
        }
    }
    

Explanation:

  1. We get a reference to our Main.storyboard.
  2. We use instantiateViewController(withIdentifier:) to create an instance of ViewControllerB. The identifier must match the Storyboard ID we set earlier. We then downcast it to ViewControllerB so we can access its specific properties.
  3. We set the receivedData property of viewControllerB to our desired string. This is the property injection step.
  4. Finally, we use navigationController?.pushViewController(viewControllerB, animated: true) to push ViewControllerB onto the navigation stack. The animated: true parameter provides a smooth sliding transition.

Run your app!

  • You should see “Welcome to View A!” and a “Go to View B” button.
  • Tap “Go to View B”.
  • ViewControllerB should slide in from the right, display “Data from A: Hello from View A!”, and have a “Back” button automatically provided by the UINavigationController.
  • Tap “Back” to return to ViewControllerA.

SwiftUI Data Flow: @State and @Binding (Brief Introduction)

In SwiftUI, data flow is handled declaratively using property wrappers. For basic data passing between a parent view and a child view, @State and @Binding are fundamental.

  • @State: Used to manage simple, local state within a single view. When a @State variable changes, SwiftUI automatically re-renders the view.
  • @Binding: Allows a child view to read and write a value owned by a parent view. The child doesn’t own the data; it just has a “binding” or a two-way connection to it.

Let’s quickly see how this looks conceptually.

// Example of @State and @Binding in SwiftUI (Conceptual)

import SwiftUI

// Parent View: Owns the data
struct ParentView: View {
    @State private var message: String = "Hello from Parent!" // Data owned by ParentView

    var body: some View {
        VStack {
            Text("Parent Message: \(message)")
            // ChildView receives a binding to 'message'
            ChildView(childMessage: $message)
        }
    }
}

// Child View: Receives a binding to modify parent's data
struct ChildView: View {
    @Binding var childMessage: String // Declared as a Binding

    var body: some View {
        VStack {
            TextField("Enter message", text: $childMessage) // Modifies parent's message
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            Text("Child View displaying: \(childMessage)")
        }
    }
}

// To preview this, you'd set up a new SwiftUI project or add this to an existing one
// and set ContentView to ParentView in the App struct.

Explanation:

  • ParentView uses @State for message, indicating it owns this piece of data.
  • ChildView declares childMessage with @Binding, meaning it expects to receive a reference to an external @State variable.
  • When ParentView initializes ChildView, it passes $message (the binding syntax) to childMessage.
  • Any changes made to childMessage within ChildView (e.g., via the TextField) will directly update message in ParentView, and both views will re-render.

This is a powerful and elegant way to manage data flow in SwiftUI, especially for simple parent-child relationships. We’ll dive much deeper into SwiftUI state management in future chapters!

Mini-Challenge: Extend the UIKit Navigation

It’s your turn to apply what you’ve learned!

Challenge: In your LifecycleDemo project (the UIKit one):

  1. Create a third UIViewController (let’s call it ViewControllerC).
  2. Give ViewControllerC a distinct background color and a UILabel to display some received data.
  3. Add a public property to ViewControllerC to receive an Int value.
  4. From ViewControllerB, add a new button that, when tapped, navigates to ViewControllerC.
  5. When navigating from ViewControllerB to ViewControllerC, pass an Int value (e.g., 42) to ViewControllerC.
  6. Ensure ViewControllerC displays this Int value in its label.
  7. Verify that you can navigate back from ViewControllerC to ViewControllerB, and then from ViewControllerB to ViewControllerA.

Hint:

  • Remember to set the Storyboard ID for ViewControllerC in the Identity Inspector.
  • In ViewControllerB, you’ll follow a similar pattern to how you navigated from ViewControllerA to ViewControllerB.
  • Don’t forget to set title property in viewDidLoad for ViewControllerC to make the navigation bar look good.

What to observe/learn:

  • You should be comfortable creating new view controllers, setting them up in the storyboard, and connecting them to Swift files.
  • You’ll reinforce the concept of property injection for data passing.
  • You’ll see how UINavigationController manages a deeper stack of views.

Common Pitfalls & Troubleshooting

  1. “Application windows are expected to have a root view controller at the end of application launch”: This error typically means you haven’t correctly set the window.rootViewController in your SceneDelegate (or AppDelegate for older apps). For storyboard-based apps, ensure your initial view controller has “Is Initial View Controller” checked in the Attributes Inspector.
  2. navigationController is nil: If you’re trying to call navigationController?.pushViewController(...) and it’s not working, it’s likely because the UIViewController you’re calling it from is not embedded in a UINavigationController. Remember to go to Editor -> Embed In -> Navigation Controller in the storyboard.
  3. Forgetting Storyboard ID / Mismatching Identifier: When instantiating a view controller from a storyboard using instantiateViewController(withIdentifier:), if the Storyboard ID in the Identity Inspector doesn’t exactly match the string you provide in code, the instantiation will fail, often returning nil.
  4. Not Handling Optional Data: When passing data via properties (e.g., receivedData: String?), always remember that the property is an optional. Ensure you safely unwrap it (e.g., using if let or guard let) before trying to use its value to prevent crashes.
  5. Confusing AppDelegate and SceneDelegate: Remember AppDelegate for app-level events and SceneDelegate for UI-related scene events (like foreground/background for a specific window). For most single-window iPhone apps, you’ll spend more time in SceneDelegate for UI state changes.

Summary

Phew, you’ve covered a lot of ground in this chapter! Let’s recap the key takeaways:

  • App Lifecycle: iOS apps transition through states like Not Running, Inactive, Active, Background, and Suspended. Understanding these states is crucial for efficient resource management.
  • SceneDelegate: For modern iOS apps (iOS 13+), the SceneDelegate is responsible for managing the lifecycle of your app’s UI scenes, handling events like activation, backgrounding, and disconnection.
  • UIKit Navigation:
    • UINavigationController manages a stack of view controllers for hierarchical navigation (push/pop).
    • UITabBarController allows switching between distinct app sections.
    • Modal presentations show temporary view controllers.
  • SwiftUI Navigation:
    • NavigationStack (iOS 16+) is the modern, declarative way for hierarchical navigation, replacing the deprecated NavigationView.
    • NavigationLink is used within a NavigationStack to trigger transitions.
  • Basic Data Flow (Passing Data Forward):
    • In UIKit, property injection (setting a public property on the destination view controller before presenting it) is a common method.
    • In SwiftUI, @State manages local view state, and @Binding creates a two-way connection for a child view to modify a parent’s data.

You now have a solid foundation for how iOS apps operate, how users move through your app, and how views can share information. This knowledge is fundamental for building any interactive and multi-screen application.

What’s Next? In the upcoming chapters, we’ll build upon this understanding to explore more advanced state management techniques, delve into networking to fetch data from the internet, and learn about various data persistence options to save information locally within your app. Get ready to build even more dynamic and data-driven experiences!

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.