Getting Started with SwiftUI — Declarative UI and State Management

What Is SwiftUI?

SwiftUI is a declarative UI framework released by Apple in 2019. Instead of UIKit’s imperative approach, you declare UI state and views, and the framework automatically updates the screen. Similar in paradigm to React and Flutter, it produces concise code and supports real-time previews.

This article covers SwiftUI’s basic view composition, state management, lists, and navigation with practical examples.

Basic Views and Layout

Every UI element in SwiftUI is a struct that conforms to the View protocol. Views are arranged using VStack, HStack, and ZStack.

import SwiftUI

// Basic view composition — Define app entry point with @main
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            // Text
            Text("Getting Started with SwiftUI")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.blue)

            // Horizontal layout with image and text
            HStack(spacing: 12) {
                Image(systemName: "swift")
                    .font(.system(size: 40))
                    .foregroundColor(.orange)
                VStack(alignment: .leading) {
                    Text("Swift Programming")
                        .font(.headline)
                    Text("Declarative UI Framework")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }

            // Button
            Button(action: {
                print("Button tapped!")
            }) {
                Text("Get Started")
                    .font(.headline)
                    .foregroundColor(.white)
                    .padding(.horizontal, 40)
                    .padding(.vertical, 12)
                    .background(Color.blue)
                    .cornerRadius(10)
            }
        }
        .padding()
    }
}

SwiftUI views are structs, making them lightweight and efficient. Extract reusable views into separate structs.

@State and @Binding — State Management

@State manages local state within a view. When state changes, SwiftUI automatically re-renders the view. @Binding passes a parent view’s state to a child view.

import SwiftUI

// Counter example — State management with @State
struct CounterView: View {
    @State private var count = 0
    @State private var isShowingDetail = false

    var body: some View {
        VStack(spacing: 20) {
            Text("Count: \(count)")
                .font(.system(size: 48, weight: .bold))

            HStack(spacing: 20) {
                // Decrease button
                Button("- Decrease") {
                    if count > 0 { count -= 1 }
                }
                .buttonStyle(.bordered)

                // Increase button
                Button("+ Increase") {
                    count += 1
                }
                .buttonStyle(.borderedProminent)
            }

            // Reset button — Pass state to child via @Binding
            ResetButton(count: $count)

            // Toggle
            Toggle("Show Details", isOn: $isShowingDetail)
                .padding(.horizontal)

            if isShowingDetail {
                Text("Square of current value: \(count * count)")
                    .font(.headline)
                    .foregroundColor(.purple)
                    .transition(.slide)
            }
        }
        .padding()
        .animation(.easeInOut, value: isShowingDetail)
    }
}

// Child view — Modify parent's state via @Binding
struct ResetButton: View {
    @Binding var count: Int

    var body: some View {
        Button("Reset") {
            withAnimation {
                count = 0
            }
        }
        .foregroundColor(.red)
        .disabled(count == 0)  // Disabled when 0
    }
}

@State is suitable for simple values used only within a view. For state shared across multiple views, use @ObservedObject or @EnvironmentObject.

List and ForEach — Dynamic Lists

List creates a scrollable list. When used with data conforming to the Identifiable protocol, efficient updates are possible.

import SwiftUI

// Data model — Conforming to Identifiable
struct TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

struct TodoListView: View {
    @State private var todos = [
        TodoItem(title: "Study Swift", isCompleted: false),
        TodoItem(title: "SwiftUI Tutorial", isCompleted: true),
        TodoItem(title: "Project Design", isCompleted: false),
        TodoItem(title: "Write Tests", isCompleted: false),
    ]
    @State private var newTodoTitle = ""

    var body: some View {
        NavigationStack {
            VStack {
                // Input field
                HStack {
                    TextField("New task", text: $newTodoTitle)
                        .textFieldStyle(.roundedBorder)
                    Button("Add") {
                        addTodo()
                    }
                    .disabled(newTodoTitle.isEmpty)
                }
                .padding(.horizontal)

                // Task list
                List {
                    // In Progress section
                    Section("In Progress (\(pendingCount))") {
                        ForEach($todos) { $todo in
                            if !todo.isCompleted {
                                TodoRow(todo: $todo)
                            }
                        }
                    }

                    // Completed section
                    Section("Completed") {
                        ForEach($todos) { $todo in
                            if todo.isCompleted {
                                TodoRow(todo: $todo)
                            }
                        }
                        .onDelete(perform: deleteCompleted)
                    }
                }
            }
            .navigationTitle("Todo List")
        }
    }

    private var pendingCount: Int {
        todos.filter { !$0.isCompleted }.count
    }

    private func addTodo() {
        let todo = TodoItem(title: newTodoTitle, isCompleted: false)
        todos.append(todo)
        newTodoTitle = ""
    }

    private func deleteCompleted(at offsets: IndexSet) {
        let completedTodos = todos.filter { $0.isCompleted }
        for index in offsets {
            if let todoIndex = todos.firstIndex(where: { $0.id == completedTodos[index].id }) {
                todos.remove(at: todoIndex)
            }
        }
    }
}

struct TodoRow: View {
    @Binding var todo: TodoItem

    var body: some View {
        HStack {
            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                .foregroundColor(todo.isCompleted ? .green : .gray)
                .onTapGesture {
                    todo.isCompleted.toggle()
                }
            Text(todo.title)
                .strikethrough(todo.isCompleted)
                .foregroundColor(todo.isCompleted ? .secondary : .primary)
        }
    }
}

NavigationStack manages navigation between screens. NavigationLink connects to destination views.

import SwiftUI

struct MenuItem: Identifiable {
    let id = UUID()
    let title: String
    let icon: String
    let description: String
}

struct MainMenuView: View {
    let menuItems = [
        MenuItem(title: "Profile", icon: "person.circle", description: "View user information"),
        MenuItem(title: "Settings", icon: "gear", description: "Change app settings"),
        MenuItem(title: "Notifications", icon: "bell", description: "View notification history"),
        MenuItem(title: "Help", icon: "questionmark.circle", description: "View frequently asked questions"),
    ]

    var body: some View {
        NavigationStack {
            List(menuItems) { item in
                NavigationLink(value: item.title) {
                    HStack(spacing: 12) {
                        Image(systemName: item.icon)
                            .font(.title2)
                            .foregroundColor(.blue)
                            .frame(width: 36)
                        VStack(alignment: .leading) {
                            Text(item.title)
                                .font(.headline)
                            Text(item.description)
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                    }
                    .padding(.vertical, 4)
                }
            }
            .navigationTitle("Menu")
            .navigationDestination(for: String.self) { title in
                DetailView(title: title)
            }
        }
    }
}

struct DetailView: View {
    let title: String

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "doc.text")
                .font(.system(size: 60))
                .foregroundColor(.blue)
            Text("\(title) Detail Screen")
                .font(.title)
            Text("This screen displays the details for the \(title) menu.")
                .multilineTextAlignment(.center)
                .foregroundColor(.secondary)
                .padding(.horizontal)
        }
        .navigationTitle(title)
        .navigationBarTitleDisplayMode(.inline)
    }
}

Practical Tips

  • @State is for view-internal use only: Use it for simple local state. For complex state, use @Observable classes (iOS 17+).
  • Decompose views: Break views exceeding 100 lines into sub-views. Each view should handle a single responsibility.
  • Use Previews: Write previews with various states using the #Preview macro (iOS 17+) and verify in real time.
  • $ creates a Binding: Prefixing a @State property with $ creates a Binding. Use it when passing to child views.
  • withAnimation: Wrap state changes in a withAnimation block for automatic animation.
  • Identifiable: Data used with List and ForEach should conform to Identifiable to provide a unique ID.

Was this article helpful?