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 and Screen Transitions
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
@Observableclasses (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
#Previewmacro (iOS 17+) and verify in real time. - $ creates a Binding: Prefixing a
@Stateproperty with$creates aBinding. Use it when passing to child views. - withAnimation: Wrap state changes in a
withAnimationblock for automatic animation. - Identifiable: Data used with
ListandForEachshould conform toIdentifiableto provide a unique ID.