DEV Community

SwiftUI Tutorial for Beginners: Build Your First iOS App in 2026

So you want to build an iOS app. Good call. The App Store has over 1.8 million apps, and every single one of them started with someone staring at a blank Xcode project thinking "where do I even begin?"

That someone is you right now. And honestly, you picked the best time to start. SwiftUI in 2026 is nothing like it was when Apple first released it. It is mature, stable, and way more fun to work with than UIKit ever was.

This tutorial will take you from zero to a working iOS app. Not a toy "Hello World" thing. A real app with multiple screens, data flow, and clean architecture. Let's go.

What You Need Before We Start

Hardware: Any Mac with Apple Silicon (M1 or newer). Intel Macs work too, but the Simulator is painfully slow on them.

Software:

  • macOS 15 or later
  • Xcode 16 or later (free from the Mac App Store)
  • An Apple ID (also free)

You do NOT need a paid Apple Developer account to follow this tutorial. That $99/year fee is only required when you want to publish to the App Store.

Step 1: Create Your First Xcode Project

Open Xcode. Click Create New Project. Select iOS > App. Click Next.

Fill in these details:

  • Product Name: MyFirstApp
  • Organization Identifier: com.yourname (use your domain or just make something up)
  • Interface: SwiftUI (this is critical, do NOT pick Storyboard)
  • Language: Swift
  • Storage: None (we will add data later)

Click Next, choose where to save it, and click Create.

Step 2: Understanding the Project Structure

Xcode just generated several files for you. Here is what matters:

MyFirstApp/
  MyFirstAppApp.swift    // Entry point
  ContentView.swift       // Your first screen
  Assets.xcassets         // Images and colors
  Preview Content/        // Preview assets
Enter fullscreen mode Exit fullscreen mode

Open MyFirstAppApp.swift:

import SwiftUI

@main
struct MyFirstAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the entry point. The @main attribute tells iOS "start here." The WindowGroup creates a window, and ContentView() is the first thing users see.

Now open ContentView.swift:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}
Enter fullscreen mode Exit fullscreen mode

Hit Cmd+R to run it. You should see "Hello, world!" on the simulator. Congrats, you just built an iOS app. But we are not stopping here.

Step 3: Building a Real UI with SwiftUI

Let's build something useful. We will create a simple task manager app with a list of tasks, the ability to add new ones, and mark them as complete.

First, let's create our data model. Create a new Swift file (Cmd+N > Swift File) called Task.swift:

import Foundation

struct TaskItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
    var createdAt: Date = .now
}
Enter fullscreen mode Exit fullscreen mode

Why TaskItem and not just Task? Because Task is already a Swift concurrency keyword. Naming it Task would cause conflicts everywhere.

The Identifiable protocol tells SwiftUI "each item has a unique id." This is required for lists.

Step 4: Creating the ViewModel

SwiftUI works best with the MVVM pattern (Model-View-ViewModel). The ViewModel holds your data and business logic. The View just displays it.

Create a new file called TaskViewModel.swift:

import SwiftUI

@Observable
class TaskViewModel {
    var tasks: [TaskItem] = []
    var newTaskTitle: String = ""

    func addTask() {
        guard !newTaskTitle.trimmingCharacters(in: .whitespaces).isEmpty else {
            return
        }

        let task = TaskItem(title: newTaskTitle)
        tasks.insert(task, at: 0)
        newTaskTitle = ""
    }

    func toggleTask(_ task: TaskItem) {
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index].isCompleted.toggle()
        }
    }

    func deleteTask(_ task: TaskItem) {
        tasks.removeAll { $0.id == task.id }
    }

    var completedCount: Int {
        tasks.filter(\.isCompleted).count
    }

    var pendingCount: Int {
        tasks.count - completedCount
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice the @Observable macro. This is the modern way to do state management in SwiftUI (introduced in iOS 17). It replaced ObservableObject and @Published. If you see tutorials using ObservableObject, they are outdated.

Step 5: Building the Task List View

Now replace the contents of ContentView.swift:

import SwiftUI

struct ContentView: View {
    @State private var viewModel = TaskViewModel()

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Stats bar
                HStack {
                    Label("\(viewModel.pendingCount) pending",
                          systemImage: "clock")
                    Spacer()
                    Label("\(viewModel.completedCount) done",
                          systemImage: "checkmark.circle")
                }
                .font(.subheadline)
                .foregroundStyle(.secondary)
                .padding()

                // Input field
                HStack {
                    TextField("Add a new task...",
                              text: $viewModel.newTaskTitle)
                        .textFieldStyle(.roundedBorder)
                        .onSubmit {
                            viewModel.addTask()
                        }

                    Button(action: viewModel.addTask) {
                        Image(systemName: "plus.circle.fill")
                            .font(.title2)
                    }
                    .disabled(
                        viewModel.newTaskTitle
                            .trimmingCharacters(in: .whitespaces)
                            .isEmpty
                    )
                }
                .padding(.horizontal)

                // Task list
                List {
                    ForEach(viewModel.tasks) { task in
                        TaskRowView(task: task) {
                            viewModel.toggleTask(task)
                        }
                    }
                    .onDelete { indexSet in
                        for index in indexSet {
                            viewModel.deleteTask(
                                viewModel.tasks[index]
                            )
                        }
                    }
                }
                .listStyle(.plain)
                .overlay {
                    if viewModel.tasks.isEmpty {
                        ContentUnavailableView(
                            "No Tasks Yet",
                            systemImage: "checklist",
                            description: Text(
                                "Add your first task above"
                            )
                        )
                    }
                }
            }
            .navigationTitle("My Tasks")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Creating the Task Row Component

Create a new file called TaskRowView.swift:

import SwiftUI

struct TaskRowView: View {
    let task: TaskItem
    let onToggle: () -> Void

    var body: some View {
        HStack(spacing: 12) {
            Button(action: onToggle) {
                Image(systemName: task.isCompleted
                      ? "checkmark.circle.fill"
                      : "circle")
                    .font(.title3)
                    .foregroundStyle(
                        task.isCompleted ? .green : .gray
                    )
            }
            .buttonStyle(.plain)

            VStack(alignment: .leading, spacing: 4) {
                Text(task.title)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(
                        task.isCompleted ? .secondary : .primary
                    )

                Text(task.createdAt, style: .relative)
                    .font(.caption)
                    .foregroundStyle(.tertiary)
            }

            Spacer()
        }
        .padding(.vertical, 4)
        .animation(.easeInOut, value: task.isCompleted)
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the app (Cmd+R). You now have a fully functional task manager. You can add tasks, mark them complete, swipe to delete, and see stats at the top.

Step 7: Adding Navigation to a Detail Screen

Most apps have multiple screens. Let's add a detail view for each task. Create TaskDetailView.swift:

import SwiftUI

struct TaskDetailView: View {
    let task: TaskItem

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {
                Image(systemName: task.isCompleted
                      ? "checkmark.circle.fill"
                      : "circle")
                    .font(.largeTitle)
                    .foregroundStyle(
                        task.isCompleted ? .green : .orange
                    )

                VStack(alignment: .leading) {
                    Text(task.isCompleted
                         ? "Completed"
                         : "In Progress")
                        .font(.headline)
                    Text(
                        "Created \(task.createdAt, style: .relative) ago"
                    )
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
            }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(.ultraThinMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 12))

            Spacer()
        }
        .padding()
        .navigationTitle(task.title)
        .navigationBarTitleDisplayMode(.large)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now update the ForEach in ContentView.swift to wrap each row in a NavigationLink:

ForEach(viewModel.tasks) { task in
    NavigationLink(value: task) {
        TaskRowView(task: task) {
            viewModel.toggleTask(task)
        }
    }
}
.onDelete { indexSet in
    for index in indexSet {
        viewModel.deleteTask(viewModel.tasks[index])
    }
}
Enter fullscreen mode Exit fullscreen mode

And add a .navigationDestination modifier to the NavigationStack:

NavigationStack {
    VStack(spacing: 0) {
        // ... existing code
    }
    .navigationTitle("My Tasks")
    .navigationDestination(for: TaskItem.self) { task in
        TaskDetailView(task: task)
    }
}
Enter fullscreen mode Exit fullscreen mode

For this to work, make TaskItem conform to Hashable:

struct TaskItem: Identifiable, Hashable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
    var createdAt: Date = .now
}
Enter fullscreen mode Exit fullscreen mode

Now tapping any task row navigates to its detail view with a smooth push animation.

Step 8: Saving Data with UserDefaults

Right now, your tasks disappear when you close the app. Let's fix that by saving to UserDefaults. This is the simplest persistence option in iOS.

Update TaskItem to conform to Codable:

struct TaskItem: Identifiable, Hashable, Codable {
    let id: UUID
    var title: String
    var isCompleted: Bool
    var createdAt: Date

    init(id: UUID = UUID(), title: String,
         isCompleted: Bool = false,
         createdAt: Date = .now) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
        self.createdAt = createdAt
    }
}
Enter fullscreen mode Exit fullscreen mode

Now add save/load methods to the ViewModel:

@Observable
class TaskViewModel {
    var tasks: [TaskItem] = [] {
        didSet { save() }
    }
    var newTaskTitle: String = ""

    private let key = "saved_tasks"

    init() {
        load()
    }

    private func save() {
        if let data = try? JSONEncoder().encode(tasks) {
            UserDefaults.standard.set(data, forKey: key)
        }
    }

    private func load() {
        guard let data = UserDefaults.standard.data(forKey: key),
              let decoded = try? JSONDecoder()
                  .decode([TaskItem].self, from: data)
        else { return }
        tasks = decoded
    }

    // ... rest of the methods stay the same
}
Enter fullscreen mode Exit fullscreen mode

That is it. Your tasks now persist between app launches. For a production app you would use SwiftData instead, but UserDefaults works great for small amounts of data.

Step 9: Polishing the UI

Let's add some visual polish. SwiftUI makes this easy.

Add a custom color scheme to your ContentView:

.navigationTitle("My Tasks")
.toolbarBackground(.ultraThinMaterial, for: .navigationBar)
.toolbar {
    ToolbarItem(placement: .topBarTrailing) {
        Menu {
            Button("Clear Completed") {
                viewModel.tasks.removeAll(
                    where: \.isCompleted
                )
            }
        } label: {
            Image(systemName: "ellipsis.circle")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Add haptic feedback when completing a task. In the toggleTask method:

func toggleTask(_ task: TaskItem) {
    if let index = tasks.firstIndex(
        where: { $0.id == task.id }
    ) {
        tasks[index].isCompleted.toggle()

        let generator = UIImpactFeedbackGenerator(
            style: .light
        )
        generator.impactOccurred()
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 10: Running on Your Real iPhone

You do NOT need a paid developer account for this. Just:

  1. Plug your iPhone into your Mac with a cable
  2. In Xcode, select your iPhone from the device dropdown (top center)
  3. Go to Xcode > Settings > Accounts and sign in with your Apple ID
  4. Select your team in the project settings (Signing & Capabilities)
  5. Hit Cmd+R

The first time, your iPhone will ask you to trust the developer certificate. Go to Settings > General > VPN & Device Management and trust it.

What to Learn Next

You just built a complete app with:

  • SwiftUI views and components
  • MVVM architecture with @observable
  • NavigationStack for multi-screen apps
  • Data persistence with UserDefaults
  • Custom UI components
  • Haptic feedback

Here is what I would learn next, in this order:

  1. SwiftData for proper database storage
  2. Async/await for network requests
  3. Charts framework for data visualization
  4. WidgetKit for home screen widgets
  5. App Store submission process

The best way to learn is to build something you actually want to use. A habit tracker, a recipe app, a workout log. Pick something personal and start hacking.

Resources

If you want to speed up your learning, I put together a bunch of developer resources on my channels:

Happy coding. The iOS ecosystem needs more builders, not more tutorial watchers. Go ship something.

Top comments (0)