Skip to content

SwiftUI Practice

SwiftUI Practice

5 exercises covering view composition, state management, gesture handling, custom modifiers, and navigation patterns.


Exercise 1: View Composition

Problem: Build a reusable profile card view that composes an image, name, and bio. The view should accept all data through parameters and use a VStack with an HStack for layout.

import SwiftUI

struct ProfileCard: View \{
    let name: String
    let bio: String
    let imageName: String

    var body: some View \{
        VStack(spacing: 12) \{
            Image(imageName)
                .resizable()
                .scaledToFill()
                .frame(width: 80, height: 80)
                .clipShape(Circle())
                .overlay(Circle().stroke(Color.blue, lineWidth: 2))

            Text(name)
                .font(.title2)
                .fontWeight(.bold)

            Text(bio)
                .font(.body)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
        \}
        .padding()
        .background(RoundedRectangle(cornerRadius: 12).fill(Color(.systemBackground)))
        .shadow(radius: 4)
    \}
\}

struct ProfileCardPreview: View \{
    var body: some View \{
        ProfileCard(
            name: "Jane Smith",
            bio: "SwiftUI developer and design enthusiast.",
            imageName: "person.circle.fill"
        )
    \}
\}

Solution: The ProfileCard is a pure, stateless view that receives data via initializer parameters. VStack arranges content vertically with spacing. clipShape(Circle()) and the overlay create a circular avatar with a border. The shadow and background provide visual depth.


Exercise 2: State Management with @State and @Binding

Problem: Create a temperature converter view with a stepper to adjust Celsius, and a binding that displays the equivalent Fahrenheit value. Use @State for the source value and @Binding for the converted display.

import SwiftUI

struct TemperatureConverter: View \{
    @State private var celsius: Double = 20.0
    @Binding var displayUnit: String

    var fahrenheit: Double \{
        celsius * 9 / 5 + 32
    \}

    var body: some View \{
        VStack(spacing: 20) \{
            Text("Temperature Converter")
                .font(.largeTitle)

            Stepper(
                value: $celsius,
                in: -40...100,
                step: 1
            ) \{
                Text("\(Int(celsius))°C")
            \}

            Divider()

            HStack \{
                Text("Fahrenheit:")
                    .font(.headline)
                Text("\(fahrenheit, specifier: "%.1f")°F")
                    .font(.title)
                    .foregroundColor(.blue)
            \}

            Picker("Display Unit", selection: $displayUnit) \{
                Text("Celsius").tag("C")
                Text("Fahrenheit").tag("F")
            \}
            .pickerStyle(.segmented)
        \}
        .padding()
    \}
\}

struct ConverterContainer: View \{
    @State private var unit: String = "C"

    var body: some View \{
        TemperatureConverter(displayUnit: $unit)
    \}
\}

Solution: @State manages the local Celsius value within TemperatureConverter. @Binding var displayUnit connects to a parent-owned state. The computed property fahrenheit derives the converted value. The Stepper modifies $celsius directly through the binding.


Exercise 3: Gesture Handling

Problem: Implement a view that responds to a long-press gesture to toggle a mode, and a drag gesture that moves a circle around the screen. Display the current position.

import SwiftUI

struct DraggableView: View \{
    @State private var offset = CGSize.zero
    @State private var isEditing = false

    var body: some View \{
        VStack \{
            Text(isEditing ? "Editing Mode" : "View Mode")
                .font(.headline)
                .foregroundColor(isEditing ? .red : .primary)

            Circle()
                .fill(isEditing ? Color.red : Color.blue)
                .frame(width: 80, height: 80)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged \{ value in
                            offset = value.translation
                        \}
                        .onEnded \{ _ in
                            withAnimation(.spring()) \{
                                offset = .zero
                            \}
                        \}
                )
                .gesture(
                    LongPressGesture(minimumDuration: 0.5)
                        .onEnded \{ _ in
                            withAnimation \{
                                isEditing.toggle()
                            \}
                        \}
                )

            Text("Position: (\(Int(offset.width)), \(Int(offset.height)))")
                .font(.caption)
                .foregroundColor(.secondary)
        \}
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    \}
\}

Solution: Two gestures are composed on the same view. DragGesture tracks translation and updates offset on change, resetting with animation on release. LongPressGesture toggles isEditing after 0.5 seconds. The circle’s fill color and position reflect the current state.


Exercise 4: Custom Modifiers

Problem: Create a custom ViewModifier that applies a card-style appearance with rounded corners, padding, shadow, and a border. Then create a convenience extension on View to apply it efficiently.

import SwiftUI

struct CardModifier: ViewModifier \{
    var cornerRadius: CGFloat
    var shadowRadius: CGFloat

    func body(content: Content) -> some View \{
        content
            .padding()
            .background(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .fill(Color(.systemBackground))
            )
            .overlay(
                RoundedRectangle(cornerRadius: cornerRadius)
                    .stroke(Color.gray.opacity(0.2), lineWidth: 1)
            )
            .shadow(radius: shadowRadius)
    \}
\}

extension View \{
    func cardStyle(
        cornerRadius: CGFloat = 12,
        shadowRadius: CGFloat = 4
    ) -> some View \{
        modifier(CardModifier(
            cornerRadius: cornerRadius,
            shadowRadius: shadowRadius
        ))
    \}
\}

struct ModifierDemo: View \{
    var body: some View \{
        VStack(spacing: 16) \{
            Text("Basic Card")
                .cardStyle()

            Text("Large Radius")
                .cardStyle(cornerRadius: 20, shadowRadius: 8)

            Text("No Shadow")
                .cardStyle(shadowRadius: 0)
        \}
        .padding()
    \}
\}

Solution: CardModifier encapsulates the card styling logic in a reusable ViewModifier. The extension View method provides a fluent API. Different configurations are achieved by passing parameters to the convenience method, keeping the modifier flexible and composable.


Exercise 5: Navigation with NavigationStack

Problem: Build a list-detail navigation pattern using NavigationStack. The list shows items, and tapping navigates to a detail view. Include a search bar and a toolbar button that presents a sheet.

import SwiftUI

struct Item: Identifiable \{
    let id = UUID()
    let title: String
    let description: String
\}

struct ItemListView: View \{
    @State private var items: [Item] = [
        Item(title: "Architecture", description: "MVVM and clean architecture patterns"),
        Item(title: "Networking", description: "URLSession and async/await"),
        Item(title: "Persistence", description: "CoreData and SwiftData"),
        Item(title: "Testing", description: "Unit and UI testing strategies"),
        Item(title: "Performance", description: "Instruments and profiling tools"),
    ]
    @State private var searchText = ""
    @State private var showingSheet = false

    var filteredItems: [Item] \{
        if searchText.isEmpty \{ return items \}
        return items.filter \{
            $0.title.localizedCaseInsensitiveContains(searchText) ||
            $0.description.localizedCaseInsensitiveContains(searchText)
        \}
    \}

    var body: some View \{
        NavigationStack \{
            List(filteredItems) \{ item in
                NavigationLink(item.title) \{
                    ItemDetailView(item: item)
                \}
            \}
            .navigationTitle("Topics")
            .searchable(text: $searchText, prompt: "Search topics")
            .toolbar \{
                Button(action: \{ showingSheet = true \}) \{
                    Image(systemName: "info.circle")
                \}
            \}
            .sheet(isPresented: $showingSheet) \{
                InfoSheet()
            \}
        \}
    \}
\}

struct ItemDetailView: View \{
    let item: Item

    var body: some View \{
        VStack(alignment: .leading, spacing: 16) \{
            Text(item.title)
                .font(.largeTitle)
            Text(item.description)
                .font(.body)
                .foregroundColor(.secondary)
        \}
        .padding()
    \}
\}

struct InfoSheet: View \{
    @Environment(\.dismiss) var dismiss

    var body: some View \{
        NavigationStack \{
            Text("Navigation patterns demo.")
                .navigationTitle("Info")
                .toolbar \{
                    Button("Done") \{ dismiss() \}
                \}
        \}
    \}
\}

Solution: NavigationStack manages the navigation path. NavigationLink pushes detail views. .searchable adds a built-in search bar that filters the list. .sheet presents a modal. @Environment(\.dismiss) dismisses the sheet. The filteredItems computed property handles search filtering.

Intuition

SwiftUI is a declarative UI framework: Instead of imperatively modifying UI elements, you describe what the UI should look like for a given state, and SwiftUI handles the updates. This eliminates many categories of UI bugs.

Why it matters: SwiftUI dramatically reduces the amount of code needed to build reactive, animated user interfaces for Apple platforms.

The key insight: SwiftUI’s @State and @Binding properties create a unidirectional data flow, state flows down, events flow up, making UI behaviour predictable and testable.

Common Mistakes

Mutating state in init instead of using @State: SwiftUI views are structs. If you modify properties in init, those changes are not reflected in the UI. Use @State for mutable view state. @State properties are stored separately from the view struct.

Confusing @State with @Binding: @State owns the data (declared in the parent view). @Binding references data owned by a parent. Using @State when you need @Binding creates separate state instances that dont sync.

Ignoring view identity: SwiftUI identifies views by their position in the view tree. Using ForEach without stable, unique identifiers causes rendering bugs. Always provide id: or use Identifiable conformance for collections.

See Also