Create Reusable Views in SwiftUI

Create Reusable Views in SwiftUI

A fitness summary repeats the same card layout for Walking and Cycling. Extract that layout into ActivityCard and pass each card its own title, duration and symbol.

Prerequisites: finish Getting Started and the Swift for Apps group. Open the verified FitnessCards Xcode project, choose an installed iPhone simulator, and press Command-R. No account, network service or private user data is needed.

Define the component’s inputs

ActivityCard conforms to View and has three immutable String inputs. Its body describes one card. Swift supplies the memberwise initializer used by the parent in this small struct.

struct ActivityCard: View {
    let title: String
    let duration: String
    let symbol: String
    // body appears in the complete source below.
}

This declaration is a teaching excerpt, not a complete standalone View: the complete source supplies body. Input properties are not locally owned interactive state. State and bindings follow in later lessons.

Create two instances from one definition

The parent passes different values to the same component. Reusing a view definition does not mean the two instances share one title or one duration.

ActivityCard(title: "Walking", duration: "30 minutes", symbol: "figure.walk")
ActivityCard(title: "Cycling", duration: "45 minutes", symbol: "bicycle")

If you change the card’s padding or typography, both instances gain that layout change. If you change only the second call’s duration, only the cycling card receives the new value.

Choose a useful component boundary

A card is a useful boundary because its layout and meaning repeat. Keep input names tied to that meaning. Avoid extracting every single Text into a component unless the abstraction provides a useful shared rule.

The decorative symbol is hidden from accessibility because the adjacent title already conveys the activity. The component uses flexible horizontal space rather than a hard-coded screen width. It can move to ActivityCard.swift later without changing its call sites; this small downloadable project keeps it in the same file for easy inspection.

Read and run the complete example

Replace ContentView.swift with this complete source in the supplied project. The app entry point already presents ContentView(). Excerpts above illustrate individual ideas; this file is the exact source used for the verified build.

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Activity").font(.largeTitle).bold()
            ActivityCard(title: "Walking", duration: "30 minutes", symbol: "figure.walk")
            ActivityCard(title: "Cycling", duration: "45 minutes", symbol: "bicycle")
        }
        .padding(24)
    }
}

struct ActivityCard: View {
    let title: String
    let duration: String
    let symbol: String

    var body: some View {
        HStack(spacing: 16) {
            Image(systemName: symbol).font(.title).foregroundStyle(.blue)
                .accessibilityHidden(true)
            VStack(alignment: .leading, spacing: 4) {
                Text(title).font(.headline)
                Text(duration).foregroundStyle(.secondary)
            }
            Spacer(minLength: 8)
        }
        .padding(16)
        .background(.blue.opacity(0.08), in: RoundedRectangle(cornerRadius: 16))
    }
}

#Preview { ContentView() }

The #Preview declaration supports the Xcode canvas. The screenshot below verifies a launched app, independently of whether your canvas is active.

Compare with the running app

Activity appears above two consistently styled cards: Walking with 30 minutes, and Cycling with 45 minutes, each with its own symbol.

This capture verifies the baseline on the named simulator. Also try a larger text size and another destination width; a baseline capture alone does not establish every accessibility or adaptive-layout case.

Activity appears above two consistently styled cards: Walking with 30 minutes, and Cycling with 45 minutes, each with its own symbol.
Actual iPhone 18 Pro simulator capture, iOS 27.0. Built from the complete source shown in this lesson.

If your result differs

Old content: save ContentView.swift, confirm the selected scheme and destination, and run again. Incomplete snippet: use the complete source file rather than replacing the whole file with one excerpt. Layout crowding: check text size, available width, fixed frames and padding before adding arbitrary offsets. Read the first build diagnostic if compilation fails.

Compose a third card without copying its implementation

A reusable view earns its abstraction when a meaningful repeated unit has independent input. The fitness row’s title, duration and symbol are data; its spacing, typography and background are shared presentation. Add Running by constructing another row rather than copying its HStack.

Notice the two different kinds of change: editing the component’s padding affects all rows, while editing one call’s duration affects only that instance. Keeping those decisions separate is a practical reason to extract the component before introducing more advanced ownership patterns.

Build a third independent activity row

Build a third independent activity row, captured from the corresponding built example
Actual iPhone 18 Pro simulator capture, iOS 27.0. This checkpoint has its own built source and project download.

Run this separate comparison checkpoint. Its complete source and screenshot show the differences discussed immediately above; the earlier baseline project remains available.

import SwiftUI

struct ContentView: View {
 var body: some View {
 VStack(alignment: .leading, spacing: 16) {
 Text("Weekly activity").font(.title).bold()
 ActivityRow(title: "Walking", duration: "30 minutes", symbol: "figure.walk")
 ActivityRow(title: "Cycling", duration: "45 minutes", symbol: "bicycle")
 ActivityRow(title: "Running", duration: "20 minutes", symbol: "figure.run")
 Text("One layout, three independent inputs.").font(.caption)
}.padding(24)
 }
}

struct ActivityRow: View {
 let title: String
 let duration: String
 let symbol: String
 var body: some View {
  HStack(spacing: 16) {
   Image(systemName: symbol).font(.title).foregroundStyle(.blue).accessibilityHidden(true)
   VStack(alignment: .leading, spacing: 4) {
    Text(title).font(.headline)
    Text(duration).foregroundStyle(.secondary)
   }
   Spacer(minLength: 8)
  }.padding(16).background(.blue.opacity(0.08), in: RoundedRectangle(cornerRadius: 16))
 }
}

#Preview { ContentView() }

Download this complete checkpoint project. The code above replaces ContentView.swift in this project; its app entry point is already included.

Try a focused change

Add a third card for Running lasting 20 minutes, using figure.run. Do not duplicate the HStack implementation.

Worked answer

ActivityCard(title: "Running", duration: "20 minutes", symbol: "figure.run")

The new call reuses the component. All three cards retain the same padding, typography and background. The verified baseline contains the first two cards. As the content grows, consider a scrolling container in the later list/layout lessons.

Check your understanding

Choose the explanation that matches this example.

How do you change spacing consistently for both cards?
Choose one answer. You can try again.

Answer and explanation

Change the shared ActivityCard body.

Both instances use the same view definition while receiving their own input values.

What you learned

You extracted a meaningful component with explicit inputs and reused it without copying its layout. Next in the curriculum: buttons and actions, state, bindings, and forms.

Apple documentation.

Reaching the recap and completing the quizzes are separate milestones.

    Next lesson: Buttons and Actions in SwiftUI →