Overview
SwiftUI is Apple’s modern declarative framework for building user interfaces across all Apple platforms. Introduced at WWDC 2019, it allows developers to describe UI layouts and behaviors using Swift code with a syntax that reads like a description of the desired interface. SwiftUI handles the rendering, animation, and state management automatically, drastically reducing the amount of boilerplate code compared to UIKit or AppKit. The framework uses a reactive data flow model where views automatically update when the underlying state changes.
SwiftUI integrates deeply with Xcode, providing real-time previews that update as you write code. It supports all Apple platforms from a single codebase, though platform-specific adaptations are straightforward to implement. The framework includes built-in support for accessibility, localization, dark mode, and dynamic type. SwiftUI views are lightweight value types (structs) that conform to the View protocol, making them efficient to create and destroy as the UI updates.
Installation
# SwiftUI requires Xcode (macOS only)
# Install Xcode from Mac App Store or:
xcode-select --install
# Verify Xcode installation
xcodebuild -version
# Minimum requirements:
# - Xcode 15+ for latest SwiftUI features
# - macOS 14+ (Sonoma) for development
# - iOS 17+ / macOS 14+ deployment targets for latest APIs
# Create a new SwiftUI project via command line
mkdir MySwiftUIApp && cd MySwiftUIApp
# Or use Xcode: File > New > Project > App
# Select "SwiftUI" for Interface option
Basic View Structure
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
.font(.title)
.foregroundColor(.blue)
Image(systemName: "star.fill")
.imageScale(.large)
.foregroundStyle(.tint)
}
.padding()
}
}
#Preview {
ContentView()
}
Common Views and Modifiers
| View | Description |
|---|
Text("Hello") | Display static or dynamic text |
Image(systemName: "star") | SF Symbols or asset images |
Button("Tap") { action() } | Tappable button with action |
TextField("Placeholder", text: $value) | Text input field |
SecureField("Password", text: $pwd) | Password input |
Toggle("Label", isOn: $flag) | Boolean toggle switch |
Slider(value: $val, in: 0...100) | Numeric slider |
Picker("Label", selection: $sel) | Selection picker |
DatePicker("Date", selection: $date) | Date/time picker |
ProgressView(value: 0.5) | Progress indicator |
| Modifier | Description |
|---|
.font(.title) | Set text font style |
.foregroundColor(.red) | Set foreground color |
.background(.blue) | Set background color |
.padding() | Add padding around view |
.frame(width: 200, height: 100) | Set explicit dimensions |
.cornerRadius(10) | Round corners |
.shadow(radius: 5) | Add drop shadow |
.opacity(0.8) | Set transparency |
.rotationEffect(.degrees(45)) | Rotate view |
.scaleEffect(1.5) | Scale view size |
Layout Containers
// Vertical stack
VStack(alignment: .leading, spacing: 10) {
Text("First")
Text("Second")
Text("Third")
}
// Horizontal stack
HStack(spacing: 16) {
Image(systemName: "person")
Text("Username")
Spacer()
Text("Edit")
}
// Overlay / ZStack (layered)
ZStack {
Color.blue.ignoresSafeArea()
VStack {
Text("Overlaid Content")
.foregroundColor(.white)
}
}
// Grid layout (iOS 16+)
Grid {
GridRow {
Text("Row 1, Col 1")
Text("Row 1, Col 2")
}
GridRow {
Text("Row 2, Col 1")
Text("Row 2, Col 2")
}
}
// Lazy stacks for performance
ScrollView {
LazyVStack {
ForEach(0..<1000) { index in
Text("Row \(index)")
}
}
}
State Management
struct CounterView: View {
// Local state
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1
}
}
}
}
// Binding: pass state to child views
struct ParentView: View {
@State private var name = ""
var body: some View {
ChildView(name: $name)
}
}
struct ChildView: View {
@Binding var name: String
var body: some View {
TextField("Enter name", text: $name)
}
}
// ObservableObject for shared state
@Observable
class UserSettings {
var username = ""
var isLoggedIn = false
var theme: Theme = .light
}
struct SettingsView: View {
@State private var settings = UserSettings()
var body: some View {
Toggle("Logged In", isOn: $settings.isLoggedIn)
}
}
// Environment for dependency injection
struct MyApp: App {
@State private var settings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environment(settings)
}
}
}
struct ContentView: View {
@Environment(UserSettings.self) var settings
var body: some View {
Text(settings.username)
}
}
Navigation
// NavigationStack (iOS 16+)
NavigationStack {
List {
NavigationLink("Detail View") {
DetailView()
}
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationTitle("My App")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
// Tab-based navigation
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gear")
}
}
// Sheet / Modal presentation
struct MainView: View {
@State private var showSheet = false
var body: some View {
Button("Show Sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
SheetContent()
}
}
}
// Alert
.alert("Confirm Action", isPresented: $showAlert) {
Button("Cancel", role: .cancel) { }
Button("Delete", role: .destructive) {
deleteItem()
}
} message: {
Text("This action cannot be undone.")
}
Lists and Data Display
struct ItemListView: View {
@State private var items = ["Apple", "Banana", "Cherry"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.toolbar {
EditButton()
}
}
func delete(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
func move(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
}
}
// Sections in Lists
List {
Section("Fruits") {
Text("Apple")
Text("Banana")
}
Section("Vegetables") {
Text("Carrot")
Text("Broccoli")
}
}
Animations
struct AnimationExample: View {
@State private var isExpanded = false
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(
width: isExpanded ? 300 : 100,
height: isExpanded ? 300 : 100
)
.animation(.spring(duration: 0.5), value: isExpanded)
Button("Toggle") {
isExpanded.toggle()
}
}
}
}
// Explicit animation
withAnimation(.easeInOut(duration: 0.3)) {
showDetail.toggle()
}
// Transition effects
if showContent {
Text("Appearing!")
.transition(.slide)
}
// Matched geometry for hero animations
@Namespace private var animation
if isExpanded {
DetailView()
.matchedGeometryEffect(id: "hero", in: animation)
} else {
ThumbnailView()
.matchedGeometryEffect(id: "hero", in: animation)
}
Configuration and App Lifecycle
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
// App startup logic
}
}
// macOS: additional window types
#if os(macOS)
Settings {
SettingsView()
}
#endif
}
}
// Scene phases
struct ContentView: View {
@Environment(\.scenePhase) var scenePhase
var body: some View {
Text("Hello")
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active: print("App active")
case .inactive: print("App inactive")
case .background: print("App backgrounded")
@unknown default: break
}
}
}
}
Advanced Usage
// Custom ViewModifier
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.white)
.cornerRadius(12)
.shadow(radius: 4)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardModifier())
}
}
// Usage
Text("Card Content").cardStyle()
// Custom PreferenceKey for child-to-parent communication
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
// Async data loading
struct AsyncDataView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task {
do {
items = try await fetchItems()
} catch {
print("Error: \(error)")
}
}
}
}
// Custom shapes
struct Hexagon: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
for i in 0..<6 {
let angle = Angle(degrees: Double(i) * 60 - 90)
let point = CGPoint(
x: center.x + radius * cos(angle.radians),
y: center.y + radius * sin(angle.radians)
)
if i == 0 { path.move(to: point) }
else { path.addLine(to: point) }
}
path.closeSubpath()
return path
}
}
Troubleshooting
| Issue | Solution |
|---|
| Preview not updating | Clean build folder: Cmd+Shift+K, then rebuild |
| ”Type does not conform to View” | Ensure body returns a single view; wrap in Group or container |
| State not updating UI | Use @State for value types, @Observable for reference types |
| Navigation back button missing | Ensure view is inside NavigationStack |
| List performance slow | Use LazyVStack inside ScrollView instead of List for large datasets |
| Dark mode colors wrong | Use semantic colors like .primary, .secondary instead of hardcoded values |
| ”Cannot convert value” in ForEach | Make model conform to Identifiable or provide id: parameter |
| Keyboard covers text field | Wrap in ScrollView or use .scrollDismissesKeyboard(.interactively) |
| Sheet not dismissing | Use @Environment(\.dismiss) var dismiss and call dismiss() |
| Animation not working | Ensure value parameter matches in .animation(_:value:) |