Creating a Todo List App with SwiftUI

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 to Identifiable.
  • 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 to TodoItem to enable easy encoding and decoding.
  • saveTodos() encodes the todo items to JSON and saves them to UserDefaults.
  • loadTodos() attempts to load and decode the todo items from UserDefaults. If it fails, it sets default todos.
  • .onAppear modifier is added to TodoListView 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.