SwiftUI – Apple's Declarative UI Framework
SwiftUI is Apple's modern framework for building user interfaces across all Apple platforms (iOS, macOS, watchOS, tvOS, visionOS). Unlike UIKit's imperative approach, SwiftUI uses a declarative syntax where you describe what the UI should look like based on the current state, and the framework automatically handles updates. In 2026, SwiftUI is mature and feature-complete for most production apps, though UIKit integration remains valuable for complex custom needs.
- SwiftUI Fundamentals – Your First View
SwiftUI views are lightweight structs that conform to the View protocol. The body property describes the view's content using a composition of built-in components.
import SwiftUI // MARK: - Basic View Structure struct ContentView: View { var body: some View { VStack(spacing: 20) { Text("Welcome to SwiftUI") .font(.largeTitle) .fontWeight(.bold) Image(systemName: "swift") .font(.system(size: 60)) .foregroundColor(.orange) Button("Tap Me") { print("Button tapped!") } .buttonStyle(.borderedProminent) .controlSize(.large) } .padding() } } // MARK: - App Entry Point @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } } } // MARK: - Common Layout Containers struct LayoutExample: View { var body: some View { // VStack: Vertical stack VStack(alignment: .leading, spacing: 8) { Text("Vertical Stack") Divider() } // HStack: Horizontal stack HStack(spacing: 16) { Image(systemName: "heart") Text("Horizontal Stack") } // ZStack: Overlapping views (like a stack of cards) ZStack { Rectangle() .fill(Color.blue) .frame(width: 200, height: 200) Text("On Top") .foregroundColor(.white) } // Lazy stacks for scrolling content ScrollView { LazyVStack(alignment: .leading, spacing: 12) { ForEach(0..<100) { index in Text("Item \(index)") .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray.opacity(0.1)) } } } } }
- State Management – The Heart of SwiftUI
SwiftUI uses property wrappers to manage state and data flow. Understanding when to use each wrapper is critical for building reactive, bug-free apps.
import SwiftUI // MARK: - @State (Local view state) struct CounterView: View { @State private var count = 0 // Owned by this view, value type var body: some View { VStack { Text("Count: \(count)") .font(.title) Button("Increment") { count += 1 // Triggers view re-render } .buttonStyle(.borderedProminent) } } } // MARK: - @Binding (Two-way connection to parent state) struct ParentView: View { @State private var isOn = false var body: some View { VStack { Text("Parent: \(isOn ? "ON" : "OFF")") ChildSwitch(isOn: $isOn) // Pass binding } } } struct ChildSwitch: View { @Binding var isOn: Bool // References state from parent var body: some View { Toggle("Child Toggle", isOn: $isOn) .padding() } } // MARK: - @ObservedObject (External reference type state) class UserViewModel: ObservableObject { @Published var name = "" // @Published triggers updates @Published var age = 0 func incrementAge() { age += 1 } } struct ProfileView: View { @ObservedObject var viewModel: UserViewModel // Observes external object var body: some View { Form { TextField("Name", text: $viewModel.name) HStack { Text("Age: \(viewModel.age)") Button("+", action: viewModel.incrementAge) } } } } // MARK: - @StateObject (Owned observable object – creates and owns lifecycle) struct OwnerView: View { @StateObject private var viewModel = UserViewModel() // Owns the object var body: some View { ProfileView(viewModel: viewModel) } } // MARK: - @EnvironmentObject (Shared across view hierarchy) class AppSettings: ObservableObject { @Published var isDarkMode = false @Published var language = "en" } struct RootView: View { @StateObject private var settings = AppSettings() var body: some View { ContentView() .environmentObject(settings) // Inject into environment } } struct SettingsToggle: View { @EnvironmentObject var settings: AppSettings // Access anywhere var body: some View { Toggle("Dark Mode", isOn: $settings.isDarkMode) } } // MARK: - @Environment (System-provided values) struct SystemInfoView: View { @Environment(\.colorScheme) var colorScheme @Environment(\.locale) var locale @Environment(\.horizontalSizeClass) var sizeClass var body: some View { VStack { Text("Theme: \(colorScheme == .dark ? "Dark" : "Light")") Text("Language: \(locale.languageCode ?? "unknown")") Text("Size Class: \(sizeClass == .compact ? "Compact" : "Regular")") } } } // MARK: - @AppStorage (UserDefaults binding) struct UserPreferencesView: View { @AppStorage("username") private var username = "Guest" @AppStorage("notificationsEnabled") private var notificationsEnabled = true var body: some View { Form { TextField("Username", text: $username) Toggle("Enable Notifications", isOn: $notificationsEnabled) } } } // MARK: - @SceneStorage (State restoration per scene/window) struct DetailView: View { @SceneStorage("detail.selectedTab") private var selectedTab = 0 var body: some View { TabView(selection: $selectedTab) { Text("Tab 1").tag(0) Text("Tab 2").tag(1) } // Selected tab persists across app launches } }
- Navigation in SwiftUI
SwiftUI provides two navigation systems: the legacy NavigationView (deprecated in iOS 16+) and the modern NavigationStack (iOS 16+) with programmatic navigation and deep linking support.
import SwiftUI // MARK: - Modern NavigationStack (iOS 16+, Recommended) struct HomeView: View { @State private var path = NavigationPath() // Manages navigation stack var body: some View { NavigationStack(path: $path) { List { NavigationLink("Basic Detail", value: "Simple") NavigationLink("User Profile", value: User(id: 1, name: "Alice")) Button("Programmatic Push") { path.append("Programmatic") } } .navigationTitle("Home") .navigationDestination(for: String.self) { stringValue in Text("Detail: \(stringValue)") } .navigationDestination(for: User.self) { user in UserProfileView(user: user) } } } } struct User: Hashable { let id: Int let name: String } struct UserProfileView: View { let user: User @Environment(\.dismiss) private var dismiss var body: some View { VStack { Text("Profile: \(user.name)") Button("Go Back") { dismiss() // Programmatic dismiss } } } } // MARK: - Sheet & FullScreenCover (Modals) struct ModalExampleView: View { @State private var showingSheet = false @State private var showingFullScreen = false var body: some View { VStack(spacing: 20) { Button("Show Sheet") { showingSheet.toggle() } .sheet(isPresented: $showingSheet) { SheetContentView() .presentationDetents([.medium, .large]) // iOS 16+ .presentationDragIndicator(.visible) } Button("Show Full Screen") { showingFullScreen.toggle() } .fullScreenCover(isPresented: $showingFullScreen) { FullScreenView() } } } } struct SheetContentView: View { @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { Text("Sheet Content") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } } // MARK: - Alert & Confirmation Dialog struct AlertExampleView: View { @State private var showingAlert = false @State private var showingConfirmation = false var body: some View { VStack { Button("Show Alert") { showingAlert = true } .alert("Delete Item", isPresented: $showingAlert) { Button("Cancel", role: .cancel) { } Button("Delete", role: .destructive) { // Perform delete } } message: { Text("This action cannot be undone.") } Button("Show Options") { showingConfirmation = true } .confirmationDialog("Choose Option", isPresented: $showingConfirmation) { Button("Option 1") { } Button("Option 2") { } Button("Cancel", role: .cancel) { } } } } }
- Layout & Customization
SwiftUI offers powerful layout modifiers and custom layout capabilities for creating responsive, adaptive interfaces.
import SwiftUI // MARK: - Layout Modifiers struct LayoutModifiersExample: View { var body: some View { VStack(spacing: 20) { // Frame & sizing Text("Fixed Size") .frame(width: 200, height: 50) .background(Color.blue) Text("Max Width") .frame(maxWidth: .infinity) .background(Color.green) // Padding & margin Text("With Padding") .padding(.all, 16) .background(Color.yellow) // Overlay & background Text("Overlay") .padding() .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.red, lineWidth: 2) ) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.gray.opacity(0.1)) ) } .padding() } } // MARK: - Custom Layout (iOS 16+) struct EqualWidthHStack: Layout { func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let maxHeight = subviews.map { $0.sizeThatFits(.unspecified).height }.max() ?? 0 return CGSize(width: proposal.width ?? 0, height: maxHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let width = bounds.width / CGFloat(subviews.count) for (index, subview) in subviews.enumerated() { let x = bounds.minX + (CGFloat(index) * width) subview.place(at: CGPoint(x: x, y: bounds.midY), anchor: .center, proposal: ProposedViewSize(width: width, height: nil)) } } } struct CustomLayoutExample: View { var body: some View { EqualWidthHStack { Text("Short") .padding() .background(Color.red) Text("Much longer text") .padding() .background(Color.green) Text("Medium") .padding() .background(Color.blue) } .frame(height: 60) } } // MARK: - Size Classes & Adaptive Layout struct AdaptiveLayoutView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass var body: some View { if horizontalSizeClass == .compact { // iPhone layout VStack { Text("Compact Layout") // Stack vertically } } else { // iPad layout HStack { Text("Regular Layout") // Side-by-side } } } } // MARK: - GeometryReader (Access parent dimensions) struct GeometryReaderExample: View { var body: some View { GeometryReader { geometry in VStack { Text("Width: \(geometry.size.width)") Text("Height: \(geometry.size.height)") Rectangle() .fill(Color.blue) .frame(width: geometry.size.width * 0.7, height: 50) } .frame(maxWidth: .infinity, maxHeight: .infinity) } .frame(height: 200) .background(Color.gray.opacity(0.1)) } }
- Animations & Transitions
SwiftUI makes animation incredibly simple with implicit, explicit, and custom animations.
import SwiftUI // MARK: - Implicit Animation (automatic on state change) struct ImplicitAnimationView: View { @State private var isExpanded = false var body: some View { VStack { RoundedRectangle(cornerRadius: 10) .fill(Color.blue) .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100) .animation(.spring(), value: isExpanded) // Animate when value changes Button("Toggle") { isExpanded.toggle() } } } } // MARK: - Explicit Animation (withAnimation closure) struct ExplicitAnimationView: View { @State private var rotation = 0.0 var body: some View { Button("Rotate") { withAnimation(.easeInOut(duration: 1)) { rotation += 360 } } .rotationEffect(.degrees(rotation)) } } // MARK: - Transition Animations struct TransitionExampleView: View { @State private var showDetails = false var body: some View { VStack { Button("Toggle Details") { withAnimation(.easeInOut) { showDetails.toggle() } } if showDetails { Text("Detailed Information") .transition(.slide.combined(with: .opacity)) // Available transitions: .slide, .scale, .opacity, .move(edge:), .asymmetric } } } } // MARK: - Matched Geometry Effect (hero animations) struct MatchedGeometryExample: View { @Namespace private var namespace @State private var isExpanded = false var body: some View { if !isExpanded { VStack { Image(systemName: "photo") .matchedGeometryEffect(id: "image", in: namespace) .frame(width: 100, height: 100) Text("Tap to Expand") .matchedGeometryEffect(id: "text", in: namespace) } .onTapGesture { withAnimation(.spring()) { isExpanded.toggle() } } } else { VStack { Image(systemName: "photo") .matchedGeometryEffect(id: "image", in: namespace) .frame(width: 300, height: 300) Text("Expanded View") .matchedGeometryEffect(id: "text", in: namespace) Button("Close") { withAnimation(.spring()) { isExpanded.toggle() } } } } } } // MARK: - Custom Animations struct CustomAnimation: View { @State private var animate = false var body: some View { Circle() .fill(Color.red) .frame(width: 100, height: 100) .scaleEffect(animate ? 1.5 : 1) .opacity(animate ? 0.5 : 1) .animation(.easeInOut(duration: 1).repeatForever(), value: animate) .onAppear { animate = true } } }
- Lists & Forms
SwiftUI provides List, Form, and Lazy stacks for displaying collections of data efficiently.
import SwiftUI // MARK: - List with Dynamic Data struct Item: Identifiable { let id = UUID() let name: String let isCompleted: Bool } struct ListExample: View { @State private var items = [ Item(name: "Buy groceries", isCompleted: false), Item(name: "Walk dog", isCompleted: true), Item(name: "Write code", isCompleted: false) ] @State private var newItemName = "" var body: some View { List { // Section header Section { ForEach(items) { item in HStack { Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundColor(item.isCompleted ? .green : .gray) Text(item.name) .strikethrough(item.isCompleted) } .swipeActions { Button(role: .destructive) { deleteItem(item) } label: { Label("Delete", systemImage: "trash") } } } .onDelete(perform: deleteItems) .onMove(perform: moveItems) } header: { Text("Tasks") } footer: { Text("\(items.filter { !$0.isCompleted }.count) remaining") } // Add new item HStack { TextField("New task", text: $newItemName) Button("Add") { addItem() } .disabled(newItemName.isEmpty) } } .navigationTitle("Tasks") .toolbar { EditButton() } } private func deleteItems(at offsets: IndexSet) { items.remove(atOffsets: offsets) } private func moveItems(from source: IndexSet, to destination: Int) { items.move(fromOffsets: source, toOffset: destination) } private func deleteItem(_ item: Item) { items.removeAll { $0.id == item.id } } private func addItem() { let newItem = Item(name: newItemName, isCompleted: false) items.append(newItem) newItemName = "" } } // MARK: - Form (for settings & input screens) struct SettingsFormView: View { @AppStorage("notifications") private var notifications = true @AppStorage("darkMode") private var darkMode = false @State private var selectedColor = "Blue" @State private var volume = 0.5 let colors = ["Red", "Green", "Blue"] var body: some View { NavigationStack { Form { Section("Preferences") { Toggle("Enable Notifications", isOn: $notifications) Toggle("Dark Mode", isOn: $darkMode) } Section("Appearance") { Picker("Theme Color", selection: $selectedColor) { ForEach(colors, id: \.self) { color in Text(color) } } HStack { Text("Volume") Slider(value: $volume, in: 0...1) Text("\(Int(volume * 100))%") .foregroundColor(.secondary) } } Section("About") { HStack { Text("Version") Spacer() Text("1.0.0") .foregroundColor(.secondary) } Link("Visit Website", destination: URL(string: "https://apple.com")!) } } .navigationTitle("Settings") } } }
- Data Fetching & Async/Await
SwiftUI works seamlessly with Swift concurrency. Use .task modifier for async operations and @Observable for modern data models (iOS 17+).
import SwiftUI // MARK: - Async/Await with .task (iOS 15+) struct Post: Codable, Identifiable { let id: Int let title: String let body: String } struct PostsListView: View { @State private var posts: [Post] = [] @State private var errorMessage: String? @State private var isLoading = false var body: some View { NavigationStack { Group { if isLoading { ProgressView("Loading...") } else if let error = errorMessage { VStack { Text("Error: \(error)") .foregroundColor(.red) Button("Retry") { Task { await fetchPosts() } } } } else { List(posts) { post in VStack(alignment: .leading) { Text(post.title) .font(.headline) Text(post.body) .font(.caption) .foregroundColor(.secondary) } } } } .navigationTitle("Posts") .refreshable { await fetchPosts() } .task { await fetchPosts() // Runs when view appears } } } private func fetchPosts() async { isLoading = true errorMessage = nil do { let url = URL(string: "https://jsonplaceholder.typicode.com/posts")! let (data, _) = try await URLSession.shared.data(from: url) posts = try JSONDecoder().decode([Post].self, from: data) } catch { errorMessage = error.localizedDescription } isLoading = false } } // MARK: - @Observable Macro (iOS 17+, modern replacement for ObservableObject) @Observable class UserRepository { var users: [String] = [] var isLoading = false @ObservationIgnored // Don't track this property private let apiService = APIService() func fetchUsers() async { isLoading = true defer { isLoading = false } // Simulate network call try? await Task.sleep(nanoseconds: 1_000_000_000) users = ["Alice", "Bob", "Charlie"] } } struct UserListView: View { @State private var repository = UserRepository() // No @StateObject needed var body: some View { List(repository.users, id: \.self) { user in Text(user) } .overlay { if repository.isLoading { ProgressView() } } .task { await repository.fetchUsers() } } } // MARK: - Pull to Refresh & Pagination struct PaginatedListView: View { @State private var items: [Int] = [] @State private var currentPage = 1 @State private var isLoadingMore = false var body: some View { List(items, id: \.self) { item in Text("Item \(item)") .onAppear { if item == items.last && !isLoadingMore { loadMore() } } } .refreshable { await refresh() } .task { await loadInitial() } } private func loadInitial() async { items = Array(1...20) currentPage = 2 } private func loadMore() { isLoadingMore = true Task { try? await Task.sleep(nanoseconds: 1_000_000_000) let newItems = Array((currentPage*20)...((currentPage+1)*20 - 1)) await MainActor.run { items.append(contentsOf: newItems) currentPage += 1 isLoadingMore = false } } } private func refresh() async { await loadInitial() } }
- UIKit Integration
Use UIViewRepresentable and UIViewControllerRepresentable to embed UIKit components in SwiftUI views.
import SwiftUI import UIKit // MARK: - Embed UIKit UIView struct MapView: UIViewRepresentable { @Binding var centerCoordinate: CLLocationCoordinate2D func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator return mapView } func updateUIView(_ uiView: MKMapView, context: Context) { // Update when SwiftUI state changes let region = MKCoordinateRegion(center: centerCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)) uiView.setRegion(region, animated: true) } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, MKMapViewDelegate { var parent: MapView init(_ parent: MapView) { self.parent = parent } func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { parent.centerCoordinate = mapView.centerCoordinate } } } // Usage in SwiftUI struct ContentView: View { @State private var location = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194) var body: some View { MapView(centerCoordinate: $location) .frame(height: 300) } } // MARK: - Embed UIKit UIViewController class ImagePickerController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { var onImagePicked: ((UIImage) -> Void)? override func viewDidLoad() { super.viewDidLoad() let picker = UIImagePickerController() picker.delegate = self present(picker, animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let image = info[.originalImage] as? UIImage { onImagePicked?(image) } dismiss(animated: true) } } struct ImagePicker: UIViewControllerRepresentable { @Binding var image: UIImage? func makeUIViewController(context: Context) -> ImagePickerController { let picker = ImagePickerController() picker.onImagePicked = { image in self.image = image } return picker } func updateUIViewController(_ uiViewController: ImagePickerController, context: Context) {} } // Usage struct ProfileView: View { @State private var profileImage: UIImage? @State private var showingPicker = false var body: some View { VStack { if let image = profileImage { Image(uiImage: image) .resizable() .frame(width: 100, height: 100) .clipShape(Circle()) } Button("Select Photo") { showingPicker = true } } .sheet(isPresented: $showingPicker) { ImagePicker(image: $profileImage) } } }
- SwiftUI Best Practices
Keep Views Small – Break large views into reusable components. Each view should have a single responsibility.
Use @State for Local UI State – Not for persisted data or complex app state.
Prefer @StateObject Over @ObservedObject – When the view creates the object, use @StateObject. When receiving from parent, use @ObservedObject.
Avoid Side Effects in body – The body property should be pure. Use .task, .onAppear, or .onChange for side effects.
Use @Observable for iOS 17+ – Modern replacement for ObservableObject with cleaner syntax.
Lazy Stacks for Large Lists – Use LazyVStack/LazyHStack instead of VStack/HStack when scrolling large data sets.
Accessibility by Default – Add .accessibilityLabel, .accessibilityHint, and test with VoiceOver.
Preview-Driven Development – Use Xcode previews extensively with mocked data for faster iteration.
// MARK: - Preview with Mock Data struct PostListView_Previews: PreviewProvider { static var previews: some View { Group { PostListView(posts: mockPosts) .previewDisplayName("Light Mode") PostListView(posts: mockPosts) .preferredColorScheme(.dark) .previewDisplayName("Dark Mode") PostListView(posts: []) .previewDisplayName("Empty State") } } static var mockPosts: [Post] { [ Post(id: 1, title: "Mock Post 1", body: "This is a mock post"), Post(id: 2, title: "Mock Post 2", body: "Another mock post") ] } } // MARK: - View Composition Pattern struct UserAvatar: View { let imageURL: URL? let size: CGFloat var body: some View { AsyncImage(url: imageURL) { phase in switch phase { case .empty: ProgressView() case .success(let image): image.resizable() case .failure: Image(systemName: "person.circle.fill") @unknown default: EmptyView() } } .frame(width: size, height: size) .clipShape(Circle()) } } // MARK: - Custom View Modifier struct CardStyle: ViewModifier { func body(content: Content) -> some View { content .padding() .background(Color(.systemBackground)) .cornerRadius(12) .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2) } } extension View { func cardStyle() -> some View { modifier(CardStyle()) } } // Usage: Text("Hello").cardStyle()