SwiftUI & Interface Design

Chapter 2
SwiftUI and the Declarative UI Model

Certificate in IOS Development
SwiftUI & Interface Design   •   Certificate

SwiftUI and the Declarative UI Model

Introduction

SwiftUI is Apple’s modern framework for building user interfaces across iOS, iPadOS, macOS, watchOS, and tvOS. It uses Swift code to describe what an interface should look like and how it should behave. This approach is called declarative user-interface development.

Earlier Apple frameworks, particularly UIKit, commonly use an imperative style. In an imperative system, the developer gives the interface a sequence of instructions: create a label, set its text, position it, respond to an event, and update it when data changes. In SwiftUI, the developer describes the desired interface for a particular state. SwiftUI then calculates and applies the necessary changes.

This difference is foundational. It affects how applications are structured, how state is managed, how screens are tested, and how developers reason about interface updates. SwiftUI does not eliminate programming logic; rather, it changes the level at which interface logic is expressed.

Learning Objectives

By the end of this chapter, you should be able to:

  • create and run a basic SwiftUI project in Xcode;
  • explain the declarative UI model and contrast it with imperative UI programming;
  • describe the SwiftUI app lifecycle and the role of scenes;
  • construct interfaces using views, containers, modifiers, and composition;
  • use previews effectively during development;
  • identify suitable locations for state and event-handling logic;
  • evaluate the strengths and limitations of SwiftUI in practical development.

1. Working with Xcode and a SwiftUI Project

Xcode is Apple’s integrated development environment for developing, testing, debugging, profiling, and distributing applications. A SwiftUI project normally contains Swift source files, an application entry point, asset catalogs, configuration files, and test targets.

To create a basic project:

  1. Open Xcode and choose Create a new Xcode project.
  2. Select an iOS App template.
  3. Choose Swift as the language and SwiftUI as the interface technology.
  4. Select an appropriate deployment target.
  5. Choose a project name and save location.
  6. Build and run the project using an iOS Simulator or a connected device.

A newly created SwiftUI application commonly contains an entry-point file similar to the following:

import SwiftUI

@main
struct StudyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

The @main attribute identifies the application’s entry point. The App protocol describes the application structure. Its body property returns one or more scenes. WindowGroup provides a scene capable of presenting the application’s main window or windows. ContentView() is the root view initially displayed within that scene.

A view may be defined as follows:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Welcome to SwiftUI")
            .font(.title)
            .padding()
    }
}

The body property is not a conventional stored property containing a permanent screen object. It is a description of the interface derived from the current inputs and state. SwiftUI may evaluate this description repeatedly as those inputs change.

Xcode’s main development tools

ToolPurpose
Project navigatorOrganises source files, assets, tests, and project settings
EditorDisplays and edits Swift source code and interface files
CanvasDisplays SwiftUI previews when available
SimulatorRuns the application in a simulated Apple device environment
Debug areaDisplays logs, errors, and debugging information
InspectorConfigures selected code, views, or project elements
Preview diagnosticsReports errors that prevent a preview from rendering

The simulator is valuable for rapid development, but it is not a complete substitute for physical-device testing. Performance, memory pressure, sensors, camera behaviour, accessibility hardware, and some system interactions may differ on a real device.

2. The Declarative UI Model

A declarative interface describes what should be present. An imperative interface describes how to create and modify it through a sequence of commands.

Consider a simple state variable named isLoggedIn. In a declarative design, the interface can be described as:

struct AccountView: View {
    let isLoggedIn: Bool

    var body: some View {
        Group {
            if isLoggedIn {
                Text("Account dashboard")
            } else {
                Text("Please sign in")
            }
        }
    }
}

The view states the relationship between application state and visible content. When isLoggedIn changes, SwiftUI recomputes the relevant description and updates the rendered interface.

In an imperative framework, equivalent logic might involve locating an existing label, changing its text, hiding or showing controls, and ensuring that all related visual properties are synchronised. The developer must manage the sequence and consistency of those mutations.

A conceptual rendering model

A useful simplified model is:

Interface description = f(application state, environment, inputs)

Here, f represents the view-building logic; application state includes values such as authentication status; environment includes system-provided context such as color scheme and accessibility settings; and inputs include properties passed into a view. This is a conceptual model rather than a public implementation formula. SwiftUI’s internal rendering, identity, layout, and update mechanisms are more sophisticated.

The important principle is that the visible interface is derived from current values. A view should not generally be treated as the authoritative storage location for application data.

Declarative and imperative comparison

ConcernDeclarative SwiftUIImperative UI approach
Primary descriptionDesired result for current stateSequence of mutations
UpdatesState changes cause reevaluationDeveloper explicitly updates objects
StructureNested view compositionView-controller or object hierarchy
ReuseSmall composable views and modifiersReusable classes, methods, or components
Typical riskIncorrect identity or state ownershipOut-of-sync visual objects
Reasoning styleState-to-interface relationshipEvent-to-mutation sequence

Declarative programming does not mean that execution order is irrelevant. Network requests, data validation, navigation, and side effects still require carefully controlled imperative code. The declarative boundary primarily concerns the description of the user interface.

3. The SwiftUI App Lifecycle and Scenes

The application lifecycle describes important phases in an application’s existence, such as launching, becoming active, moving into the background, and being terminated. In SwiftUI, the application is represented by a type conforming to App, while windows and their associated lifecycle contexts are represented by scenes.

A scene is a conceptual container for a user interface region managed by the system. A simple application normally uses WindowGroup:

@main
struct ReadingApp: App {
    var body: some Scene {
        WindowGroup {
            LibraryView()
        }
    }
}

Other scene types may be appropriate in particular platforms or use cases, including document-based windows and settings scenes. The exact availability and behaviour depend on the operating system and deployment target.

SwiftUI exposes lifecycle information through environment values. For example:

struct RootView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Library")
            .onChange(of: scenePhase) { _, newPhase in
                switch newPhase {
                case .active:
                    print("Scene is active")
                case .inactive:
                    print("Scene is inactive")
                case .background:
                    print("Scene is in the background")
                @unknown default:
                    break
                }
            }
    }
}

The scenePhase values commonly include:

  • active: the scene is visible and receiving normal interaction;
  • inactive: the scene is visible but temporarily not receiving normal interaction;
  • background: the scene is no longer visible to the user.

Lifecycle events are not guarantees that an application will receive unlimited time to save data or complete work. Important user data should be saved incrementally and through appropriate persistence mechanisms. A background transition can be a useful signal, but it should not be the only protection against data loss.

SwiftUI application lifecycle and scene relationship App @main entry point Scene WindowGroup Root View ContentView Scene phases Active Inactive Background Use phase changes as signals for appropriate work, not as a substitute for reliable persistence.
SwiftUI application lifecycle and scene relationship
Figure: SwiftUI application lifecycle and scene relationship

Figure: Relationship between the SwiftUI application, scenes, root views, and scene phases.

4. Views: The Basic Building Blocks

A SwiftUI view conforms to the View protocol and provides a body describing its content. Views may represent visible controls, layout containers, navigation structures, or invisible composition logic.

Common views include:

  • Text for displaying text;
  • Image for images and symbols;
  • Label for text paired with an icon;
  • Button for an explicit user action;
  • Toggle for an on/off choice;
  • TextField for editable text;
  • List for rows of data;
  • ScrollView for scrollable content;
  • NavigationStack for hierarchical navigation;
  • Form for settings and data-entry interfaces.

A view can receive values through stored properties:

struct GreetingView: View {
    let name: String

    var body: some View {
        Text("Hello, \(name)")
    }
}

This promotes predictable data flow. The view does not need to know where name originated; it only renders the value it receives.

5. Containers and Layout

Containers arrange or group views. The most frequently used stacks are:

  • VStack, which arranges children vertically;
  • HStack, which arranges children horizontally;
  • ZStack, which layers children along the depth axis;
  • Group, which groups views without adding a visual layout region;
  • ScrollView, which provides scrolling;
  • List, which presents structured rows and supports system behaviours.

For example:

struct ProfileView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Jordan Lee")
                .font(.title)
            Text("iOS learner")
                .foregroundStyle(.secondary)
            HStack {
                Image(systemName: "checkmark.circle.fill")
                Text("Course enrolled")
            }
        }
        .padding()
    }
}

SwiftUI layout is proposal-based. A parent proposes available dimensions to a child; the child chooses a suitable size; the parent positions the child. Developers should therefore avoid assuming that every view has a fixed frame. Flexible layouts, dynamic type, localization, and varying device sizes require interfaces that adapt rather than merely imitate a single screen measurement.

Spacer expands into available space, while frame, padding, and alignment influence layout. Excessive fixed frames can cause clipping or accessibility problems, particularly when text size increases.

6. Modifiers and the View-Builder Pattern

A modifier transforms or configures a view:

Text("Save")
    .font(.headline)
    .foregroundStyle(.white)
    .padding()
    .background(.blue)
    .clipShape(Capsule())

Modifier order matters. Padding applied before a background causes the background to include the padding; applying padding after the background produces a different visual result. A modifier often returns a new view description rather than mutating the original view object.

SwiftUI uses result builders, including the view builder used by many container closures. This allows multiple child views to be written in a natural syntax:

VStack {
    Text("First")
    Text("Second")
}

The apparent simplicity is supported by generic types and compiler transformations. As interfaces become complex, extracting subviews improves readability and compile-time performance.

Accessibility and semantic modifiers

Professional interface design requires more than visual appearance. SwiftUI provides modifiers and APIs for accessibility labels, hints, traits, contrast, dynamic type, and platform conventions. An icon-only button, for example, should have a meaningful accessibility label:

Button {
    deleteItem()
} label: {
    Image(systemName: "trash")
}
.accessibilityLabel("Delete item")

System symbols, semantic colours, adaptable text, and platform controls generally provide better support for users with different abilities than custom visual approximations.

7. Composition and Data Flow

Composition means building larger interfaces from smaller views. A well-composed view has a focused responsibility and communicates through explicit inputs and outputs.

struct TaskRow: View {
    let title: String
    let completed: Bool

    var body: some View {
        HStack {
            Image(systemName: completed ? "checkmark.circle.fill" : "circle")
            Text(title)
            Spacer()
        }
    }
}

A parent can compose several rows:

struct TaskListView: View {
    let tasks: [String]

    var body: some View {
        List(tasks, id: \.self) { task in
            TaskRow(title: task, completed: false)
        }
    }
}

For editable or changing data, SwiftUI provides property wrappers that express ownership and dependency relationships. The exact wrappers depend on the project’s architecture and Swift version, but the central questions remain:

  1. Who owns the data?
  2. Who may modify it?
  3. Which views depend on it?
  4. How should changes be propagated?

Common patterns include:

MechanismTypical role
@StateLocal, transient view-owned state
@BindingA child’s read/write connection to parent-owned state
@EnvironmentAccess to shared system or application environment values
Observable model typesShared or more durable application data
Plain let or var propertiesRead-only or value inputs passed into a view

Example of local state and a binding:

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Stepper("Adjust", value: $count)
        }
        .padding()
    }
}

$count represents a binding, allowing the Stepper to read and update the state owned by CounterView. State should be kept at the lowest level that can correctly own it, but not so low that unrelated views must duplicate or synchronise it.

8. Previews as a Development Practice

SwiftUI previews provide a rapid feedback loop. A preview can display a view without requiring the complete application flow:

#Preview {
    GreetingView(name: "Sam")
}

Previews are particularly useful for checking:

  • multiple content states, including empty and error states;
  • light and dark appearance;
  • different device sizes and orientations;
  • large accessibility text sizes;
  • localized or unusually long strings;
  • representative sample data.

A preview is not a replacement for unit tests, UI tests, simulator testing, or device testing. It may not reproduce all runtime services, permissions, network conditions, performance constraints, or lifecycle behaviour. Preview data should be deterministic and safe; it should not depend unnecessarily on a production database or live network service.

9. Worked Application: A Study-Session Screen

Consider a small educational application that displays a study topic and allows the learner to mark a session as complete.

Requirements

The screen should:

  • display a title and short explanation;
  • show whether the session is complete;
  • provide a completion button;
  • remain readable in different appearances and text sizes;
  • separate presentation from the action that changes state.

Implementation

import SwiftUI

struct StudySessionView: View {
    let topic: String
    let explanation: String
    @State private var isComplete = false

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Label("Study session", systemImage: "book.closed")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)

                Text(topic)
                    .font(.largeTitle.bold())

                Text(explanation)
                    .font(.body)

                statusView

                Button {
                    isComplete = true
                } label: {
                    Text(isComplete ? "Completed" : "Mark as complete")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
                .disabled(isComplete)
            }
            .padding()
        }
        .navigationTitle("Learning")
    }

    @ViewBuilder
    private var statusView: some View {
        if isComplete {
            Label("Session complete", systemImage: "checkmark.circle.fill")
                .foregroundStyle(.green)
        } else {
            Label("Not completed", systemImage: "circle")
                .foregroundStyle(.secondary)
        }
    }
}

#Preview {
    NavigationStack {
        StudySessionView(
            topic: "Declarative interfaces",
            explanation: "Describe the interface for the current state and let SwiftUI update the presentation."
        )
    }
}

Analysis

The view receives stable content through let properties and owns temporary interaction state through @State. The statusView computed property demonstrates composition within the same type. The button changes state; the body then describes the new interface. No code searches for a label and manually changes its colour or text.

This example is deliberately small. In a production application, completion would usually be recorded in a model or persistence layer rather than only in local view state. The example nevertheless illustrates the core declarative cycle: input and state change, the view description is reevaluated, and the rendered result reflects the new values.

10. Illustrative Case Study: Converting a UIKit Screen

Background

A fictional training provider has a UIKit-based lesson screen. It contains a title label, a description label, a completion button, and a status icon. Developers report that the screen sometimes displays a completed icon while the button still says “Mark complete.”

Problem

The screen has several imperative update paths. One method updates the button, another updates the icon, and a third responds to restored data. Because these methods are not always called together, the visual elements can become inconsistent.

Analysis

The underlying problem is not simply a missing line of code. The interface has multiple mutable representations of one fact: completion status. Each representation can diverge. A declarative design attempts to make one state value the source for all dependent presentation.

Approach and solution

A SwiftUI version stores or receives a single isComplete value. Both the status label and button content depend on that value. A persistence model may own the durable completion state, while the view observes it and sends user actions back through a clearly defined method or binding.

Outcome

The screen has a more direct relationship between state and presentation. Tests can provide true and false values and verify the resulting content. Preview configurations can show both states. The migration does not automatically solve every concern: navigation, persistence, accessibility, asynchronous work, and performance still require design decisions.

Learning points

  • A single source of truth reduces inconsistent visual states.
  • Declarative code improves local reasoning but does not remove the need for architecture.
  • Migration should focus on state ownership and behaviour, not only on rewriting syntax.
  • UIKit and SwiftUI can coexist, which permits gradual adoption where appropriate.

11. Challenges and Limitations

SwiftUI provides substantial benefits, but it has limitations and learning costs.

State and identity complexity

Incorrect state ownership can cause values to reset unexpectedly or become difficult to share. Dynamic collections also require stable identity so that SwiftUI can associate data with the correct view. Using unstable or inappropriate identifiers can produce animation and update errors.

Abstraction and compiler complexity

Deeply nested generic view expressions may produce slow compilation or difficult diagnostics. Extracting subviews, simplifying expressions, and using meaningful model types can improve maintainability.

Platform and operating-system differences

SwiftUI APIs evolve with system releases. Some features require newer deployment targets, and behaviour may vary across platforms. A professional developer must verify API availability and test on the deployment versions that the product supports.

Imperative integration

Applications still perform imperative tasks: importing files, calling services, managing transactions, scheduling work, and handling legacy frameworks. SwiftUI can host imperative components and can be hosted inside UIKit. The goal is not to reject imperative programming, but to place it at appropriate boundaries.

Performance

Declarative descriptions may be recomputed frequently. This is normally expected and does not mean that every visual element is recreated inefficiently. Nevertheless, expensive work should not be performed directly in view-building code. Data loading, image processing, and complex computation should use suitable models, tasks, caching, or background mechanisms.

12. Professional Best Practices

  1. Keep views focused. Extract a view when a section has a distinct responsibility or becomes difficult to read.
  2. Make data flow explicit. Prefer clear inputs, bindings, and observable models over hidden global state.
  3. Separate side effects. Network requests, persistence, and analytics should not be casually embedded in view construction.
  4. Design for accessibility. Test VoiceOver, Dynamic Type, colour contrast, and reduced-motion preferences.
  5. Use adaptive layout. Avoid unnecessary fixed dimensions and test long text and different orientations.
  6. Preview representative states. Include loading, empty, error, completed, and unusually long-content cases.
  7. Test on devices. Simulator results cannot represent every performance and hardware condition.
  8. Respect privacy. Do not expose personal data in preview fixtures, logs, screenshots, or analytics without a legitimate purpose and appropriate controls.
  9. Use source control. Xcode projects, Swift files, assets, and configuration changes should be reviewed and tracked.
  10. Choose architecture proportionately. A small screen does not require an elaborate framework, while a large product needs deliberate boundaries for models, services, navigation, and persistence.

Contemporary Direction

SwiftUI continues to develop as Apple expands support for observation, navigation, widgets, multiplatform interfaces, charts, animations, and accessibility. Modern projects increasingly combine SwiftUI with structured concurrency and observable model layers. Developers should learn concepts rather than memorise only one version of an API: state ownership, unidirectional data flow, composition, identity, layout, accessibility, and lifecycle reasoning remain useful as syntax evolves.

Artificial intelligence can assist with code explanation, scaffolding, and test generation, but generated code must be reviewed. It may use unavailable APIs, mishandle state ownership, omit accessibility, or introduce privacy risks. In professional development, AI assistance complements—not replaces—documentation, testing, design review, and human responsibility for application behaviour.

Chapter Summary

SwiftUI is a declarative framework in which views describe the interface appropriate for current state, inputs, and environment. Its application structure begins with an App entry point and one or more scenes, commonly a WindowGroup. Views are composed from controls, containers, and modifiers. Stacks, lists, forms, navigation containers, and scroll views provide common layout and interaction structures.

The declarative model reduces the need to synchronise many independently mutable visual objects. State changes cause dependent view descriptions to be reevaluated, although developers must still design reliable state ownership, identity, side-effect boundaries, and persistence. @State, @Binding, environment values, and observable models express different data-flow responsibilities.

Xcode previews provide rapid feedback for representative interface states but must be supplemented by tests, simulator checks, accessibility evaluation, and physical-device testing. SwiftUI is powerful but not a complete replacement for imperative programming; applications continue to require explicit logic for services, persistence, lifecycle work, and integration with other frameworks. Effective SwiftUI development combines declarative composition with disciplined software design.

Key Terms

Declarative UI: A user-interface approach that describes the desired result for current state.

Imperative UI: An approach that uses ordered commands to create and mutate interface objects.

SwiftUI: Apple’s declarative framework for building interfaces across Apple platforms.

Xcode: Apple’s integrated development environment for application development.

App protocol: The SwiftUI protocol that describes an application’s entry structure.

Scene: A system-managed user-interface context, such as a window or document space.

WindowGroup: A SwiftUI scene type commonly used for application windows.

View: A SwiftUI type that describes a piece of user-interface content or layout.

Modifier: A transformation or configuration applied to a view description.

Container: A view that groups, arranges, or manages other views.

Composition: Building a larger interface from smaller, focused views.

State: Data whose current value influences the interface or application behaviour.

Binding: A read-and-write connection to state owned elsewhere.

Environment: Context supplied through the view hierarchy or system.

Scene phase: The active, inactive, or background condition of a scene.

Result builder: A Swift language feature supporting structured declarations such as view content.

Preview: A design-time rendering of a SwiftUI view in Xcode.

Identity: The stable association between data and a particular view or item.

Unidirectional data flow: A design in which data moves predictably from owners to dependants and actions communicate changes back.

Accessibility: Designing interfaces usable by people with varied abilities and assistive technologies.

Review Questions

5 Short Answer Questions

  1. What is the principal difference between declarative and imperative UI programming?
  2. What role does the App protocol play in a SwiftUI application?
  3. What is a scene, and why is WindowGroup commonly used?
  4. Name three SwiftUI containers and describe their typical layout roles.
  5. Why should lifecycle transitions not be treated as the sole mechanism for saving data?

5 Long Answer Questions

  1. Explain how SwiftUI derives an interface from state, inputs, and environment. Contrast this process with imperative view mutation.
  2. Describe the relationship among an application entry point, scenes, root views, containers, and child views.
  3. Explain the purposes of @State and @Binding, and discuss how incorrect state ownership can create defects.
  4. Discuss how modifiers affect view composition and why modifier order can change the result.
  5. Evaluate the benefits and limitations of SwiftUI previews as part of a professional testing and design workflow.

3 Analytical / Application Questions

  1. A screen contains a status icon, a status message, and a button. Each is updated by a different method, and they occasionally disagree. Analyse the likely design problem and propose a SwiftUI state and composition strategy.
  2. Design the view hierarchy for a course screen containing a navigation title, scrollable lesson content, a progress indicator, and a completion action. Justify your choice of containers and state ownership.
  3. A product team wants to support small phones, tablets, Dynamic Type, dark mode, and VoiceOver. Develop a practical SwiftUI testing and preview plan that addresses layout, accessibility, lifecycle, and device limitations.

Multiple Choice Questions with Answers

Question 1

Which statement best describes SwiftUI’s declarative UI model?

A. The developer manually changes every visual object after each event.

B. The developer describes the interface appropriate for current state and inputs.

C. The interface can contain no event-handling code.

D. All layout decisions are made by the operating system without developer input.

Correct Answer: B. The developer describes the interface appropriate for current state and inputs.

Question 2

What is the usual purpose of the @main attribute on a SwiftUI App type?

A. It identifies the application entry point.

B. It creates the first database table.

C. It enables VoiceOver automatically.

D. It converts UIKit views into SwiftUI views.

Correct Answer: A. It identifies the application entry point.

Question 3

Which SwiftUI type is commonly used to provide an application’s main window scene?

A. VStack

B. PreviewProvider

C. WindowGroup

D. Binding

Correct Answer: C. WindowGroup

Question 4

Which scene phase indicates that a scene is no longer visible to the user?

A. active

B. focused

C. inactive

D. background

Correct Answer: D. background

Question 5

What is the typical role of @State?

A. To store local, view-owned transient state

B. To define a navigation stack automatically

C. To provide a network connection

D. To replace every model object in an application

Correct Answer: A. To store local, view-owned transient state

Question 6

What does a Binding provide?

A. A read-and-write connection to state owned elsewhere

B. A permanent copy of a view’s rendered pixels

C. A scene lifecycle event

D. A compiler setting for previews

Correct Answer: A. A read-and-write connection to state owned elsewhere

Question 7

Which container primarily arranges its children vertically?

A. HStack

B. VStack

C. ZStack

D. WindowGroup

Correct Answer: B. VStack

Question 8

Why can modifier order affect a SwiftUI interface?

A. Modifiers are applied as transformations, so their sequence can change layout and appearance.

B. Modifiers are ignored unless placed alphabetically.

C. Only the first modifier in a chain is evaluated.

D. Modifier order changes the application’s deployment target.

Correct Answer: A. Modifiers are applied as transformations, so their sequence can change layout and appearance.

Question 9

Which practice best supports accessible SwiftUI interfaces?

A. Use fixed frames for every text element.

B. Hide labels from assistive technologies.

C. Provide meaningful accessibility labels and support adaptable text.

D. Use colour as the only indicator of status.

Correct Answer: C. Provide meaningful accessibility labels and support adaptable text.

Question 10

Why are previews not a complete replacement for device testing?

A. Previews cannot display any SwiftUI view.

B. Previews may not reproduce all hardware, performance, permission, and lifecycle conditions.

C. Previews are available only after App Store submission.

D. Previews automatically modify production data.

Correct Answer: B. Previews may not reproduce all hardware, performance, permission, and lifecycle conditions.

SwiftUI & Interface Design
Chapter 2 of 14