Back to all articles

SwiftUI Development: Complete Modern iOS Guide 2025

SwiftUI is Apple's future—90% of new iOS projects use it. Apps built with SwiftUI ship 40% faster with 50% less code than UIKit. But many developers struggle with its paradigm shift. This comprehensive guide takes you from SwiftUI basics to advanced patterns used in production apps.

Why SwiftUI

Benefits Over UIKit

  • Less code: 50-70% reduction in boilerplate
  • Faster development: Live previews, hot reload
  • Declarative: Describe UI, not how to build it
  • Cross-platform: iOS, macOS, watchOS, tvOS
  • Modern: Dark mode, dynamic type, accessibility built-in
  • Type-safe: Compile-time error checking
  • Composable: Small, reusable components

When to Use SwiftUI

✓ Use SwiftUI:
- New projects (iOS 15+ target)
- Rapid prototyping
- Content-heavy apps
- Standard UI patterns
- Cross-platform apps
- Small teams

✗ Use UIKit:
- Complex custom controls
- iOS 13 or earlier support needed
- Heavy reliance on third-party UIKit libraries
- Performance-critical scrolling (improving in SwiftUI)
- Need mature tooling

Hybrid approach:
- New features in SwiftUI
- Existing features in UIKit
- Bridge with UIViewRepresentable/UIViewControllerRepresentable
- Gradually migrate

SwiftUI Basics

Hello World

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
            .font(.largeTitle)
            .foregroundColor(.blue)
    }
}

#Preview {
    ContentView()
}

Key concepts:
- View protocol: Defines UI
- body property: Returns content
- some View: Opaque return type
- Modifiers: Chain to customize
- Preview: See changes live

Basic Views

import SwiftUI

struct BasicViewsDemo: View {
    var body: some View {
        VStack(spacing: 20) {
            // Text
            Text("SwiftUI Basics")
                .font(.title)
                .fontWeight(.bold)

            // Image
            Image(systemName: "star.fill")
                .font(.system(size: 50))
                .foregroundColor(.yellow)

            // Button
            Button("Tap Me") {
                print("Button tapped")
            }
            .buttonStyle(.borderedProminent)

            // TextField
            TextField("Enter text", text: .constant(""))
                .textFieldStyle(.roundedBorder)
                .padding(.horizontal)

            // Toggle
            Toggle("Enable Feature", isOn: .constant(true))
                .padding(.horizontal)

            // Slider
            Slider(value: .constant(0.5), in: 0...1)
                .padding(.horizontal)

            // ProgressView
            ProgressView(value: 0.75)
                .padding(.horizontal)
        }
        .padding()
    }
}

Layout views:
- VStack: Vertical stack
- HStack: Horizontal stack
- ZStack: Overlapping stack
- List: Scrollable list
- ScrollView: Custom scrolling
- LazyVStack/LazyHStack: Lazy loading
- Grid: iOS 16+ grid layout

State Management

import SwiftUI

// @State - Simple local state
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
                .font(.title)

            Button("Increment") {
                count += 1
            }
        }
    }
}

// @Binding - Share state with parent
struct ChildView: View {
    @Binding var text: String

    var body: some View {
        TextField("Enter text", text: $text)
    }
}

struct ParentView: View {
    @State private var text = ""

    var body: some View {
        VStack {
            Text("You typed: \(text)")
            ChildView(text: $text)
        }
    }
}

// @StateObject - Observable object ownership
class ViewModel: ObservableObject {
    @Published var items: [String] = []

    func loadItems() {
        items = ["Item 1", "Item 2", "Item 3"]
    }
}

struct MyView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        List(viewModel.items, id: \.self) { item in
            Text(item)
        }
        .onAppear {
            viewModel.loadItems()
        }
    }
}

// @ObservedObject - Observable object (not owned)
struct DetailView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        List(viewModel.items, id: \.self) { item in
            Text(item)
        }
    }
}

// @EnvironmentObject - Shared across views
class AppState: ObservableObject {
    @Published var isLoggedIn = false
    @Published var username = ""
}

@main
struct MyApp: App {
    @StateObject private var appState = AppState()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appState)
        }
    }
}

struct AnyChildView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        if appState.isLoggedIn {
            Text("Welcome, \(appState.username)!")
        } else {
            Text("Please log in")
        }
    }
}

Property wrapper guide:
@State - Owned simple value
@Binding - Shared with parent
@StateObject - Owned observable object
@ObservedObject - Passed observable object
@EnvironmentObject - Injected global state
@Published - Triggers view updates
@Environment - System environment values

Navigation

NavigationStack (iOS 16+)

import SwiftUI

struct User: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let age: Int
}

struct NavigationDemo: View {
    let users = [
        User(name: "Alice", age: 25),
        User(name: "Bob", age: 30),
        User(name: "Charlie", age: 35)
    ]

    var body: some View {
        NavigationStack {
            List(users) { user in
                NavigationLink(value: user) {
                    Text(user.name)
                }
            }
            .navigationTitle("Users")
            .navigationDestination(for: User.self) { user in
                UserDetailView(user: user)
            }
        }
    }
}

struct UserDetailView: View {
    let user: User

    var body: some View {
        VStack {
            Text(user.name)
                .font(.largeTitle)
            Text("Age: \(user.age)")
        }
        .navigationTitle("Details")
    }
}

// Programmatic navigation
struct ProgrammaticNav: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Button("Go to Level 1") {
                    path.append(1)
                }
                Button("Go to Level 2") {
                    path.append(2)
                }
                Button("Go to Root") {
                    path.removeLast(path.count)
                }
            }
            .navigationDestination(for: Int.self) { level in
                Text("Level \(level)")
            }
        }
    }
}

Sheets and Modals

import SwiftUI

struct ModalDemo: View {
    @State private var showingSheet = false
    @State private var showingFullScreen = false
    @State private var showingAlert = false

    var body: some View {
        VStack(spacing: 20) {
            // Sheet (bottom drawer)
            Button("Show Sheet") {
                showingSheet = true
            }
            .sheet(isPresented: $showingSheet) {
                SheetView()
            }

            // Full screen cover
            Button("Show Full Screen") {
                showingFullScreen = true
            }
            .fullScreenCover(isPresented: $showingFullScreen) {
                FullScreenView()
            }

            // Alert
            Button("Show Alert") {
                showingAlert = true
            }
            .alert("Important Message", isPresented: $showingAlert) {
                Button("OK", role: .cancel) { }
                Button("Delete", role: .destructive) { }
            } message: {
                Text("Are you sure?")
            }
        }
    }
}

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

    var body: some View {
        NavigationStack {
            Text("Sheet Content")
                .navigationTitle("Sheet")
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("Done") {
                            dismiss()
                        }
                    }
                }
        }
        .presentationDetents([.medium, .large])
        .presentationDragIndicator(.visible)
    }
}

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

    var body: some View {
        ZStack {
            Color.blue.ignoresSafeArea()

            VStack {
                Text("Full Screen")
                    .font(.largeTitle)
                    .foregroundColor(.white)

                Button("Dismiss") {
                    dismiss()
                }
                .buttonStyle(.bordered)
            }
        }
    }
}

Lists and Collections

List Basics

import SwiftUI

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

struct ListDemo: View {
    let items = [
        Item(title: "Item 1", subtitle: "Description 1"),
        Item(title: "Item 2", subtitle: "Description 2"),
        Item(title: "Item 3", subtitle: "Description 3")
    ]

    var body: some View {
        List {
            // Simple list
            ForEach(items) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    Text(item.subtitle)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }

            // Sections
            Section("Section 1") {
                Text("Row 1")
                Text("Row 2")
            }

            Section("Section 2") {
                Text("Row 3")
                Text("Row 4")
            }
        }
        .listStyle(.insetGrouped)
    }
}

// Swipe actions
struct SwipeActionsDemo: View {
    @State private var items = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .swipeActions(edge: .trailing) {
                        Button(role: .destructive) {
                            items.removeAll { $0 == item }
                        } label: {
                            Label("Delete", systemImage: "trash")
                        }

                        Button {
                            print("Edit \(item)")
                        } label: {
                            Label("Edit", systemImage: "pencil")
                        }
                        .tint(.blue)
                    }
                    .swipeActions(edge: .leading) {
                        Button {
                            print("Pin \(item)")
                        } label: {
                            Label("Pin", systemImage: "pin")
                        }
                        .tint(.orange)
                    }
            }
        }
    }
}

// Pull to refresh
struct RefreshableList: View {
    @State private var items = ["Item 1", "Item 2"]

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
        .refreshable {
            await loadData()
        }
    }

    func loadData() async {
        try? await Task.sleep(for: .seconds(1))
        items.append("New Item")
    }
}

LazyVGrid and LazyHGrid

import SwiftUI

struct GridDemo: View {
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(1...100, id: \.self) { number in
                    Rectangle()
                        .fill(Color.blue)
                        .frame(height: 100)
                        .overlay {
                            Text("\(number)")
                                .font(.title)
                                .foregroundColor(.white)
                        }
                }
            }
            .padding()
        }
    }
}

// Adaptive columns
struct AdaptiveGrid: View {
    let items = Array(1...100)

    var body: some View {
        ScrollView {
            LazyVGrid(
                columns: [GridItem(.adaptive(minimum: 100))],
                spacing: 16
            ) {
                ForEach(items, id: \.self) { item in
                    RoundedRectangle(cornerRadius: 8)
                        .fill(Color.blue)
                        .frame(height: 100)
                        .overlay {
                            Text("\(item)")
                                .foregroundColor(.white)
                        }
                }
            }
            .padding()
        }
    }
}

Data Flow

MVVM Pattern

import SwiftUI
import Combine

// Model
struct Post: Identifiable, Codable {
    let id: Int
    let title: String
    let body: String
}

// ViewModel
@MainActor
class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    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
    }

    func deletePost(_ post: Post) {
        posts.removeAll { $0.id == post.id }
    }
}

// View
struct PostsView: View {
    @StateObject private var viewModel = PostsViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView()
                } else if let error = viewModel.errorMessage {
                    VStack {
                        Text("Error")
                            .font(.headline)
                        Text(error)
                            .foregroundColor(.secondary)
                        Button("Retry") {
                            Task {
                                await viewModel.fetchPosts()
                            }
                        }
                    }
                } else {
                    List {
                        ForEach(viewModel.posts) { post in
                            NavigationLink(value: post) {
                                VStack(alignment: .leading) {
                                    Text(post.title)
                                        .font(.headline)
                                    Text(post.body)
                                        .font(.subheadline)
                                        .foregroundColor(.secondary)
                                        .lineLimit(2)
                                }
                            }
                        }
                        .onDelete { indexSet in
                            for index in indexSet {
                                viewModel.deletePost(viewModel.posts[index])
                            }
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        Task {
                            await viewModel.fetchPosts()
                        }
                    } label: {
                        Image(systemName: "arrow.clockwise")
                    }
                }
            }
            .navigationDestination(for: Post.self) { post in
                PostDetailView(post: post)
            }
        }
        .task {
            await viewModel.fetchPosts()
        }
    }
}

struct PostDetailView: View {
    let post: Post

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                Text(post.title)
                    .font(.title)

                Text(post.body)
                    .font(.body)
            }
            .padding()
        }
        .navigationTitle("Post #\(post.id)")
        .navigationBarTitleDisplayMode(.inline)
    }
}

Animations

Basic Animations

import SwiftUI

struct AnimationDemo: View {
    @State private var isExpanded = false
    @State private var rotation = 0.0
    @State private var scale = 1.0
    @State private var offset = CGSize.zero

    var body: some View {
        VStack(spacing: 40) {
            // Implicit animation
            RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)
                .fill(Color.blue)
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .animation(.spring(response: 0.6), value: isExpanded)

            Button("Toggle Size") {
                isExpanded.toggle()
            }

            // Rotation
            Image(systemName: "arrow.right")
                .font(.largeTitle)
                .rotationEffect(.degrees(rotation))
                .onTapGesture {
                    withAnimation(.easeInOut(duration: 1)) {
                        rotation += 360
                    }
                }

            // Scale
            Circle()
                .fill(Color.green)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
                .onTapGesture {
                    withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                        scale = scale == 1.0 ? 1.5 : 1.0
                    }
                }

            // Drag gesture with animation
            Circle()
                .fill(Color.red)
                .frame(width: 80, height: 80)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            offset = value.translation
                        }
                        .onEnded { _ in
                            withAnimation(.spring()) {
                                offset = .zero
                            }
                        }
                )
        }
    }
}

Custom Transitions

import SwiftUI

struct TransitionDemo: View {
    @State private var show = false

    var body: some View {
        VStack {
            Button("Toggle") {
                withAnimation {
                    show.toggle()
                }
            }

            if show {
                // Slide transition
                Text("Slide")
                    .transition(.slide)

                // Scale transition
                Text("Scale")
                    .transition(.scale)

                // Opacity transition
                Text("Opacity")
                    .transition(.opacity)

                // Move transition
                Text("Move")
                    .transition(.move(edge: .trailing))

                // Combined transition
                Text("Combined")
                    .transition(.asymmetric(
                        insertion: .scale.combined(with: .opacity),
                        removal: .move(edge: .trailing)
                    ))

                // Custom transition
                Text("Custom")
                    .transition(.customSlide)
            }
        }
    }
}

// Custom transition
extension AnyTransition {
    static var customSlide: AnyTransition {
        .modifier(
            active: CustomSlideModifier(offset: 100),
            identity: CustomSlideModifier(offset: 0)
        )
    }
}

struct CustomSlideModifier: ViewModifier {
    let offset: CGFloat

    func body(content: Content) -> some View {
        content
            .offset(x: offset)
            .opacity(offset == 0 ? 1 : 0)
    }
}

Best Practices

Performance Optimization

1. Use Lazy containers:
✓ LazyVStack instead of VStack in ScrollView
✓ LazyHStack instead of HStack in ScrollView
✓ LazyVGrid/LazyHGrid for grids

2. Avoid expensive operations in body:
✗ Don't compute heavy calculations
✗ Don't perform networking
✗ Don't use Date() multiple times

✓ Cache computed properties
✓ Use @State for derived values
✓ Move logic to ViewModel

3. Use Identifiable:
✓ Conform to Identifiable
✓ Use stable IDs (UUID, database ID)
✗ Don't use array index as ID

4. Minimize view updates:
✓ Split views into smaller components
✓ Use @Published only for UI-relevant properties
✓ Use equatable to prevent unnecessary updates

5. Optimize images:
✓ Use AsyncImage for remote images
✓ Cache images
✓ Resize large images
✓ Use appropriate formats (HEIC, WebP)

Code Organization

Project structure:

MyApp/
├── App/
│   ├── MyApp.swift
│   └── ContentView.swift
├── Models/
│   ├── User.swift
│   └── Post.swift
├── ViewModels/
│   ├── UsersViewModel.swift
│   └── PostsViewModel.swift
├── Views/
│   ├── Users/
│   │   ├── UsersView.swift
│   │   └── UserRow.swift
│   └── Posts/
│       ├── PostsView.swift
│       └── PostDetailView.swift
├── Services/
│   ├── APIService.swift
│   └── AuthService.swift
├── Utilities/
│   ├── Extensions.swift
│   └── Constants.swift
└── Resources/
    └── Assets.xcassets

Best practices:
✓ One view per file
✓ Extract reusable components
✓ Keep views small (< 200 lines)
✓ Use view extensions for modifiers
✓ Group related files in folders

Conclusion

SwiftUI represents the future of iOS development with its declarative syntax, powerful state management, and seamless integration with modern Swift features. While there's a learning curve coming from UIKit, the productivity gains and code reduction make it worthwhile. Start with simple views, master state management, and gradually adopt advanced patterns. SwiftUI is mature enough for production—don't wait to adopt it.

Beautiful SwiftUI apps need great support infrastructure. Our support URL generator creates professional support pages that meet App Store requirements, ensuring your modern SwiftUI app has modern support to match.

Need a Support URL for Your App?

Generate a compliant, professional support page in under a minute. Our easy-to-use generator creates everything you need for App Store and Google Play submissions.