Disclosure: This article was created or edited with AI assistance. Revised on 13 October 2026 to remove promotional links and unsupported outcome claims. It is educational guidance, not a verified personal case study or a measured result. Examples are illustrative; use only your own documented experience and measurements.
The sections below present options and illustrative examples. Adapt them to the requirements of your project or role; they do not establish a universal rule or promise a result.
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 can follow the simulator examples without buying a developer membership. Check Apple's current account and signing requirements for device testing and distribution.
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 Swift Concurrency already has a Task type. A distinct model name avoids shadowing and confusion.
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
}
}
This example uses @observable, available with Observation on iOS 17 and later. ObservableObject and Published remain relevant for compatible deployment targets and existing designs; use the ownership pattern appropriate to the model.
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
let selectedTasks = indexSet.map { viewModel.tasks[$0] }
for task in selectedTasks {
viewModel.deleteTask(task)
}
}
}
.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)
}
}
Build and run your adapted example, then check adding, completing and deleting tasks. The snippets have not been presented as a verified complete project.
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
let selectedTasks = indexSet.map { viewModel.tasks[$0] }
for task in selectedTasks {
viewModel.deleteTask(task)
}
}
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. This tutorial uses UserDefaults for a small demonstration. Choose persistence for a real app based on data volume, security, migration and concurrency requirements.
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
Follow the current Xcode signing instructions and the prompts on your device; the exact steps depend on OS and account configuration.
What to Learn Next
The tutorial sketches these areas:
- 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.
Scope of the code examples
These are separate teaching snippets, not a complete tested application or a production-ready package. Match availability to your deployment target, provide omitted model/service definitions, and check errors, cancellation, accessibility and persistence before adapting them.
Top comments (0)