SwiftUI, Apple’s modern declarative UI framework, makes it easier than ever to build elegant and responsive user interfaces across all Apple platforms. Creating a Todo List app is a great way to learn and understand SwiftUI’s core concepts, such as state management, list views, and data persistence.
What is SwiftUI?
SwiftUI is a user interface toolkit that lets us design apps in a declarative way. It enables developers to write less code while building high-performance and visually appealing applications. Key features include live previews, a declarative syntax, and seamless integration with Swift.
Why Build a Todo List App with SwiftUI?
- Practical Learning: Covers essential SwiftUI concepts in a real-world application.
- Declarative Approach: Demonstrates the simplicity and power of declarative UI.
- State Management: Introduces handling application state efficiently.
- Cross-Platform: Easily adaptable for iOS, macOS, watchOS, and tvOS.
Step-by-Step Guide to Creating a Todo List App
Step 1: Setting Up the Project
Open Xcode and create a new project. Select “App” under the iOS tab (or any other platform you prefer). Give your project a name, such as “TodoListApp”, and make sure the Interface is set to “SwiftUI”.
Step 2: Defining the Data Model
Create a struct to represent a Todo item. This struct will contain the task description and a boolean to indicate whether the task is completed.
import SwiftUI
struct TodoItem: Identifiable {
let id = UUID()
var task: String
var isCompleted: Bool = false
}
In this code:
TodoItem
is a struct conforming toIdentifiable
.id
is a unique identifier for each item.task
is the description of the todo item.isCompleted
is a boolean indicating task completion status.
Step 3: Creating the Todo List View
Create a view to display the list of todo items. Use a List
to render the items and include a Toggle
for marking tasks as complete.
import SwiftUI
struct TodoListView: View {
@State var todoItems: [TodoItem] = [
TodoItem(task: "Buy groceries"),
TodoItem(task: "Do laundry", isCompleted: true),
TodoItem(task: "Walk the dog")
]
var body: some View {
NavigationView {
List {
ForEach($todoItems) { $item in
HStack {
Toggle(isOn: $item.isCompleted) {
Text(item.task)
.strikethrough(item.isCompleted)
}
}
}
.onDelete(perform: deleteTodo)
}
.navigationTitle("Todo List")
.toolbar {
Button(action: {
// Add new todo item
}) {
Image(systemName: "plus")
}
}
}
}
func deleteTodo(at offsets: IndexSet) {
todoItems.remove(atOffsets: offsets)
}
}
struct TodoListView_Previews: PreviewProvider {
static var previews: some View {
TodoListView()
}
}
Key components of this view:
@State var todoItems
: Stores the list of todo items, marked with@State
to allow modifications and UI updates.NavigationView
: Provides a navigation bar for the list.List
: Displays the todo items in a scrollable list.ForEach
: Iterates through the todo items and creates a row for each.HStack
: Arranges the toggle and text horizontally.Toggle
: Allows marking tasks as complete. The.strikethrough
modifier visually indicates completed tasks..onDelete
: Enables swipe-to-delete functionality.Toolbar
: Adds a plus button for adding new todo items.
Step 4: Adding New Todo Items
Implement a sheet or a new view to add new todo items to the list. Use @State
to manage the new task description.
import SwiftUI
struct AddTodoView: View {
@Environment(\.dismiss) var dismiss
@State private var newTask: String = ""
@Binding var todoItems: [TodoItem]
var body: some View {
NavigationView {
Form {
TextField("Enter task", text: $newTask)
Button("Add") {
let newItem = TodoItem(task: newTask)
todoItems.append(newItem)
dismiss()
}
.disabled(newTask.isEmpty)
}
.navigationTitle("Add New Task")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
}
}
}
}
Explanation:
@Environment(\.dismiss) var dismiss
: Allows the view to dismiss itself.@State private var newTask
: Stores the new task description.@Binding var todoItems
: Binds to the todoItems array in the parent view, allowing updates.TextField
: Allows the user to enter the task description.Button
: Adds the new task to the list and dismisses the view..disabled(newTask.isEmpty)
: Disables the button if the task description is empty.Toolbar
: Adds a cancel button to dismiss the view.
Update the TodoListView
to present the AddTodoView
:
import SwiftUI
struct TodoListView: View {
@State var todoItems: [TodoItem] = [
TodoItem(task: "Buy groceries"),
TodoItem(task: "Do laundry", isCompleted: true),
TodoItem(task: "Walk the dog")
]
@State private var showingAddTodo = false
var body: some View {
NavigationView {
List {
ForEach($todoItems) { $item in
HStack {
Toggle(isOn: $item.isCompleted) {
Text(item.task)
.strikethrough(item.isCompleted)
}
}
}
.onDelete(perform: deleteTodo)
}
.navigationTitle("Todo List")
.toolbar {
Button(action: {
showingAddTodo = true
}) {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showingAddTodo) {
AddTodoView(todoItems: $todoItems)
}
}
}
func deleteTodo(at offsets: IndexSet) {
todoItems.remove(atOffsets: offsets)
}
}
Step 5: Persisting Data
To persist the data, you can use UserDefaults
, Core Data
, or CloudKit
. Here’s an example using UserDefaults
to save and load the todo items.
import SwiftUI
struct TodoListView: View {
@State var todoItems: [TodoItem] = []
@State private var showingAddTodo = false
var body: some View {
NavigationView {
List {
ForEach($todoItems) { $item in
HStack {
Toggle(isOn: $item.isCompleted) {
Text(item.task)
.strikethrough(item.isCompleted)
}
}
}
.onDelete(perform: deleteTodo)
}
.navigationTitle("Todo List")
.toolbar {
Button(action: {
showingAddTodo = true
}) {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showingAddTodo) {
AddTodoView(todoItems: $todoItems)
}
.onAppear {
loadTodos()
}
}
}
func deleteTodo(at offsets: IndexSet) {
todoItems.remove(atOffsets: offsets)
saveTodos()
}
func saveTodos() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(todoItems) {
UserDefaults.standard.set(encoded, forKey: "todos")
}
}
func loadTodos() {
if let savedTodos = UserDefaults.standard.data(forKey: "todos") {
let decoder = JSONDecoder()
if let loadedTodos = try? decoder.decode([TodoItem].self, from: savedTodos) {
todoItems = loadedTodos
return
}
}
// Default todos if loading fails
todoItems = [
TodoItem(task: "Buy groceries"),
TodoItem(task: "Do laundry", isCompleted: true),
TodoItem(task: "Walk the dog")
]
}
}
Update TodoItem
to conform to Codable
:
import SwiftUI
struct TodoItem: Identifiable, Codable {
let id = UUID()
var task: String
var isCompleted: Bool = false
}
Explanation of data persistence changes:
Codable
conformance is added toTodoItem
to enable easy encoding and decoding.saveTodos()
encodes the todo items to JSON and saves them toUserDefaults
.loadTodos()
attempts to load and decode the todo items fromUserDefaults
. If it fails, it sets default todos..onAppear
modifier is added toTodoListView
to load todos when the view appears.saveTodos()
is called after deleting a todo item to persist the change.
Conclusion
Building a Todo List app in SwiftUI is a fantastic way to grasp the fundamentals of declarative UI development, state management, and data persistence. SwiftUI’s intuitive syntax and live previews allow for rapid development and experimentation. By following this step-by-step guide, you can create a functional and visually appealing Todo List app that demonstrates the power and simplicity of SwiftUI.