- 1 SwiftUI for UI Development: SwiftUI is SwiftUI is a modern approach to create user interfaces of modern iOS applications with a layered, declarative approach. Navigation and data management help simplify the process of creating applications.
- 2 Core Data Integration: With Core Data, you are able to store and manipulate data in your app effectively This is done by integrating Core Data in your app. They improve the functionality of large-scale data storage and retrieval as it fastens the task processing or accomplishing.
- 3 Navigation and User Interaction: Meaningful navigation between the views and the user interactions such as adding or deleting a task feel very natural. Such setup shows that various components of SwiftUI can be integrated to develop a working tool for managing tasks.
Introduction to SwiftUI
SwiftUI has changed how coders build iOS views. It offers a declarative, intuitive approach to UI build.
In this masterclass, we’ll build a feature-rich iOS app using SwiftUI. We’ll cover navigation, data management, and user interactions, with code examples at every step.
SwiftUI Prerequisites
Before starting, make sure you have:
SwiftUI basics — if you’re new to SwiftUI, Apple’s official guide is a great starting point.
Xcode — install the latest version from the App Store.
What Is Swift?
Swift is Apple’s programming language for iOS, iPadOS, macOS, watchOS, tvOS, and Linux. It launched in 2014 as a replacement for Objective-C, which had been the primary language for Apple’s platforms.
Setting Up the Project
Start by creating a new SwiftUI project in Xcode:
Click Create.
Open Xcode and choose Create a new Xcode project.
Select the iOS tab, then the App template.
Set the interface to SwiftUI and click Next.
Name your project and choose a save location.
Building the UI
Our app will be a task management tool with a list of tasks and the ability to add new tasks. Open the ContentView.swift file and replace the existing code with the following:
import SwiftUI
struct ContentView: View {
@State private var tasks = ["Task 1", "Task 2", "Task 3"]
@State private var newTask = ""
var body: some View {
NavigationView {
List {
ForEach(tasks, id: \.self) { task in
Text(task)
}
.onDelete(perform: deleteTask)
}
.navigationTitle("Task Manager")
.navigationBarItems(trailing: addButton)
}
}
var addButton: some View {
Button(action: {
// Add a new task
tasks.append(newTask)
newTask = ""
}) {
Image(systemName: "plus")
}
.disabled(newTask.isEmpty)
}
func deleteTask(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This code sets up a simple task list with the ability to add new tasks. The List displays existing tasks, and the NavigationBar allows you to add new tasks.
Adding Navigation
Next, let’s add a detail view for each task.
Create a new file, TaskDetailView.swift, with this code:
import SwiftUI
struct TaskDetailView: View {
var task: String
var body: some View {
Text(task)
.navigationTitle("Task Detail")
}
}
struct TaskDetailView_Previews: PreviewProvider {
static var previews: some View {
TaskDetailView(task: "Sample Task")
}
}
Next, modify the ContentView.swift file to include navigation to the detail view. Update the ForEach block in the List:
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: TaskDetailView(task: task)) {
Text(task)
}
.onDelete(perform: deleteTask)
}
Now, when you tap on a task, it will navigate to the detail view.
Enhancing Data Management
Let’s make the app more strong by persisting tasks with Core Data.
Create a new file, Task+CoreDataProperties.swift:
import CoreData
extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
return NSFetchRequest<Task>(entityName: "Task")
}
@NSManaged public var title: String?
}
Update TaskDetailView.swift to use Core Data:
import SwiftUI
import CoreData
struct TaskDetailView: View {
@Environment(\.managedObjectContext) private var viewContext
var task: Task
var body: some View {
Text(task.title ?? "No title")
.navigationTitle("Task Detail")
}
}
Now, let’s modify the ContentView.swift file to fetch and display tasks from Core Data:
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(entity: Task.entity(), sortDescriptors: []) var tasks: FetchedResults<Task>
@State private var newTask = ""
var body: some View {
NavigationView {
List {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: TaskDetailView(task: task)) {
Text(task.title ?? "No title")
}
}
.onDelete(perform: deleteTask)
}
.navigationTitle("Task Manager")
.navigationBarItems(trailing: addButton)
}
}
// ... (rest of the code remains the same)
}
With these changes, our app now uses Core Data to store and fetch tasks.
Refactoring the Task Model for Real-World Use
So far, tasks are stored as simple strings. That works for a demo, but a real task needs more — a due date, completion state, priority, and so on.
To support that, replace the string array with an array of Task structs, or Core Data records with multiple fields.
This requires a few changes:
- Update the
ForEachloop to displaytask.title(ortask.name) instead of a plain string. - Pass the whole
taskobject toTaskDetailView, not just its title. - Rethink task creation. A single text field can’t capture a due date or priority — you’ll likely need a small “Add Task” form, presented as a sheet, with separate inputs for each field.
It’s worth doing this refactor early. Restructuring your data model gets much more disruptive once a lot of UI code depends on the simpler version.
Adding Search and Filtering
As your task list grows, users will need a way to find specific items quickly.
SwiftUI makes this easy with the .searchable modifier, bound to a String that stores the search query. From there, update your ForEach loop to show only tasks whose titles match the query.
You can go further with more filters — for example, a segmented Picker above the list that switches between “All,” “Completed,” or “Due Today.”
Handling Task Editing
So far, the app supports adding and deleting tasks — but not editing them. A real task manager needs that too.
You can reuse TaskDetailView, since it’s already shown when a user taps a task. Instead of static labels, add editable text fields.
Here’s the general approach:
- Add local
@Statevariables inTaskDetailViewthat mirror the task’s current values. - Bind your text fields and controls to those state variables.
- Write the updated values back to the task when the user confirms changes. For Core Data, call
save()on the managed object context.
One decision to make early: should edits save immediately as the user types, or only when they tap a “Save” button? Immediate saving feels more modern, but it requires more careful handling of partial or invalid input along the way.
Writing Tests for Your SwiftUI App
It’s tempting to skip testing on a small project like this. But building the testing habit early pays off as the app grows.
Xcode splits testing into two useful categories:
- Unit tests — for logic like
deleteTaskor any validation around task creation. - UI tests — for simulating real interactions, like tapping “add” and checking that a new task appears.
For the Core Data version, unit tests are especially valuable. They confirm that fetch requests return the right results, and that updates and deletions actually persist. Persistence bugs can be subtle and easy to miss with manual testing alone.
Use an in-memory Core Data store for your tests, rather than your app’s real store. This keeps tests fast and prevents them from corrupting real data.
Even a small test suite — covering adding, deleting, and navigating to a task’s detail view — gives you a safety net. That makes future changes much less risky than relying on manual testing before every release.
Working with Custom Views and Reusable Components
As your task properties grow (due dates, priority, completion status), it’s worth extracting reusable pieces into their own views.
Start with the task row. When the row only showed a title, keeping it inline was fine.
Once it needs to show priority, due date, and completion status, extract it into its own view — call it TaskRowView, and place it in the same file as the list.
This keeps the list view simple: its ForEach loop just creates TaskRowView instances and passes in the task data.
Apply the same pattern to other recurring UI elements.
This reduces complexity in each view, makes the code easier to maintain, and lets you use SwiftUI’s preview feature more well — since each custom view can be previewed on its own.
Handling Errors and Empty States
The task manager works, but it’s still missing two important things.
An empty state. Right now, there’s no dedicated view for an empty task list.
Users might mistake this for a bug rather than an empty list. Add a simple view that explains there are no tasks yet, and invites the user to add one.
Error handling. Core Data tasks can occasionally fail. Wrap calls to save() and fetch() in a do-try-catch block, and show the user a message if something goes wrong. This keeps the skill clear instead of leaving the user confused.
Supporting Multiple Users and Data Isolation
If you want to expand this app to support multiple users — whether through simple local profiles or a full backend-based account system — your Core Data setup needs rework.
Right now, every @FetchRequest in ContentView pulls from one shared store, with no concept of separate users. That’s fine for a single-user demo, but it breaks down the moment multiple users share a device, or need access across multiple devices.
The simplest fix: add a userId field (or similar identifier) to the Task entity. Then update your fetch requests to filter with an NSPredicate that only returns records matching the current user’s ID, instead of fetching everything.
It’s worth planning for this early. Retrofitting every fetch request in your codebase to filter by user is much more work than designing for it from the start.
Tracking the Logged-In User
Beyond storing user data separately, the app needs a way to track who’s currently logged in.
The cleanest approach: create an AppState class conforming to ObservableObject, injected as an @EnvironmentObject. This object can hold the current user or login state, and any view can check it to adjust its UI accordingly.
This is much cleaner than manually passing a user object or ID down through every view in your hierarchy.
It’s also worth deciding early how the app should behave when no user is logged in — whether that’s a dedicated login screen shown before ContentView, or some other default.
This is much easier to build if your @EnvironmentObject is already set up to track the active user.
Conclusion
Congratulations! You’ve completed the SwiftUI masterclass, building a full iOS app with navigation, data management, and user interactions.
SwiftUI’s declarative syntax, combined with Core Data, makes it a powerful tool for iOS build. Keep exploring SwiftUI’s more advanced features to continue leveling up your app build skills.
Feel free to experiment with more components and features to make your app your own.