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
Open MyFirstAppApp.swift:
import SwiftUI
@main
struct MyFirstAppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
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()
}
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
}
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
}
}
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")
}
}
}
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)
}
}
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)
}
}
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])
}
}
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)
}
}
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
}
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
}
}
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
}
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")
}
}
}
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()
}
}
Step 10: Running on Your Real iPhone
You do NOT need a paid developer account for this. Just:
- Plug your iPhone into your Mac with a cable
- In Xcode, select your iPhone from the device dropdown (top center)
- Go to Xcode > Settings > Accounts and sign in with your Apple ID
- Select your team in the project settings (Signing & Capabilities)
- 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:
- SwiftData for proper database storage
- Async/await for network requests
- Charts framework for data visualization
- WidgetKit for home screen widgets
- 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:
- Follow me on Telegram for daily SwiftUI tips: t.me/SwiftUIDaily
- Check out my developer tools and templates: boosty.to/swiftuidev
Happy coding. The iOS ecosystem needs more builders, not more tutorial watchers. Go ship something.
Top comments (0)