In this comprehensive guide, we will walk through the process of creating an expense tracker application using SwiftUI. This tutorial will cover everything from setting up the project to implementing core features such as adding, editing, deleting, and categorizing expenses. SwiftUI offers a declarative and efficient way to build user interfaces across all Apple platforms.
Why Build an Expense Tracker App?
- Personal Finance: Helps users manage their finances by tracking expenses.
- Skill Development: Provides an excellent opportunity to learn SwiftUI and related technologies.
- Customization: Allows tailoring the app to specific needs, something generic apps might not offer.
Prerequisites
Before we start, make sure you have the following:
- Xcode: Version 13 or later.
- macOS: A macOS environment to run Xcode.
- Basic Knowledge: Familiarity with Swift programming language and basic UI concepts.
Setting Up the Project
Let’s begin by creating a new Xcode project:
- Open Xcode and click “Create a new Xcode project.”
- Select “iOS” and then choose “App.” Click “Next.”
- Enter “ExpenseTracker” as the project name.
- Choose “SwiftUI” as the interface and “Swift” as the language.
- Click “Next” and select a location to save your project.
Defining the Data Model
First, we need to define the data structure for an expense. Create a new Swift file named Expense.swift and add the following code:
import Foundation
struct Expense: Identifiable, Codable {
var id = UUID()
var name: String
var amount: Double
var category: String
var date: Date
}
In this struct:
idis a unique identifier for each expense.nameis the name of the expense.amountis the expense amount (a Double).categoryis the category to which the expense belongs.dateis the date when the expense was incurred.
Creating the Expense List View
Next, let’s create a view to display the list of expenses. Open ContentView.swift and replace the default content with the following code:
import SwiftUI
struct ContentView: View {
@State private var expenses: [Expense] = []
var body: some View {
NavigationView {
List {
ForEach(expenses) { expense in
HStack {
Text(expense.name)
Spacer()
Text("$(expense.amount, specifier: "%.2f")")
}
}
}
.navigationTitle("Expense Tracker")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This code displays a simple list of expenses. However, the expenses array is currently empty. Let’s add some sample data to preview how it looks.
struct ContentView: View {
@State private var expenses: [Expense] = [
Expense(name: "Groceries", amount: 50.00, category: "Food", date: Date()),
Expense(name: "Dinner", amount: 30.00, category: "Food", date: Date()),
Expense(name: "Gas", amount: 40.00, category: "Transportation", date: Date())
]
// Body remains the same as above
}
Now, you should see the list populated with the sample expenses in the preview.
Adding a New Expense
To allow users to add expenses, we need to create a new view. Create a new SwiftUI file named AddExpenseView.swift and add the following code:
import SwiftUI
struct AddExpenseView: View {
@Environment(.dismiss) var dismiss
@State private var name: String = ""
@State private var amount: String = ""
@State private var category: String = "Food"
@State private var date: Date = Date()
let categories = ["Food", "Transportation", "Entertainment", "Other"]
@Binding var expenses: [Expense]
var body: some View {
NavigationView {
Form {
TextField("Expense Name", text: $name)
TextField("Amount", text: $amount)
.keyboardType(.decimalPad)
Picker("Category", selection: $category) {
ForEach(categories, id: .self) {
Text($0)
}
}
DatePicker("Date", selection: $date, displayedComponents: .date)
Button("Save") {
if let amountValue = Double(amount) {
let newExpense = Expense(name: name, amount: amountValue, category: category, date: date)
expenses.append(newExpense)
dismiss()
}
}
}
.navigationTitle("Add Expense")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
}
}
}
}
This view includes fields for the expense name, amount, category, and date. It also contains a save button to add the expense to the list.
Next, update ContentView.swift to present this view as a modal:
import SwiftUI
struct ContentView: View {
@State private var expenses: [Expense] = [
Expense(name: "Groceries", amount: 50.00, category: "Food", date: Date()),
Expense(name: "Dinner", amount: 30.00, category: "Food", date: Date()),
Expense(name: "Gas", amount: 40.00, category: "Transportation", date: Date())
]
@State private var isAddingExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses) { expense in
HStack {
Text(expense.name)
Spacer()
Text("$(expense.amount, specifier: "%.2f")")
}
}
}
.navigationTitle("Expense Tracker")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
isAddingExpense = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isAddingExpense) {
AddExpenseView(expenses: $expenses)
}
}
}
}
Now, tapping the “+” button in the toolbar will present the AddExpenseView as a modal, allowing you to add new expenses.
Deleting an Expense
To allow users to delete expenses, add the onDelete modifier to the ForEach loop in ContentView.swift:
import SwiftUI
struct ContentView: View {
@State private var expenses: [Expense] = [
Expense(name: "Groceries", amount: 50.00, category: "Food", date: Date()),
Expense(name: "Dinner", amount: 30.00, category: "Food", date: Date()),
Expense(name: "Gas", amount: 40.00, category: "Transportation", date: Date())
]
@State private var isAddingExpense = false
var body: some View {
NavigationView {
List {
ForEach(expenses) { expense in
HStack {
Text(expense.name)
Spacer()
Text("$(expense.amount, specifier: "%.2f")")
}
}
.onDelete(perform: deleteExpense)
}
.navigationTitle("Expense Tracker")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
isAddingExpense = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isAddingExpense) {
AddExpenseView(expenses: $expenses)
}
}
}
func deleteExpense(at offsets: IndexSet) {
expenses.remove(atOffsets: offsets)
}
}
The onDelete modifier allows users to swipe an expense and delete it. The deleteExpense function is called when the user confirms the deletion.
Displaying Total Expenses
To show the total expenses, let’s add a computed property to calculate the sum of all expense amounts in ContentView.swift:
import SwiftUI
struct ContentView: View {
@State private var expenses: [Expense] = [
Expense(name: "Groceries", amount: 50.00, category: "Food", date: Date()),
Expense(name: "Dinner", amount: 30.00, category: "Food", date: Date()),
Expense(name: "Gas", amount: 40.00, category: "Transportation", date: Date())
]
@State private var isAddingExpense = false
var totalExpenses: Double {
expenses.reduce(0) { $0 + $1.amount }
}
var body: some View {
NavigationView {
VStack {
Text("Total Expenses: $(totalExpenses, specifier: "%.2f")")
.font(.headline)
.padding()
List {
ForEach(expenses) { expense in
HStack {
Text(expense.name)
Spacer()
Text("$(expense.amount, specifier: "%.2f")")
}
}
.onDelete(perform: deleteExpense)
}
}
.navigationTitle("Expense Tracker")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
isAddingExpense = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isAddingExpense) {
AddExpenseView(expenses: $expenses)
}
}
}
func deleteExpense(at offsets: IndexSet) {
expenses.remove(atOffsets: offsets)
}
}
This code adds a totalExpenses computed property to calculate the sum of all expense amounts, and it displays the total at the top of the view.
Implementing Data Persistence
To ensure the expenses are saved even after the app is closed, let’s implement data persistence using UserDefaults. Create a new function to save expenses to UserDefaults in ContentView.swift:
import SwiftUI
struct ContentView: View {
@State private var expenses: [Expense] = [] {
didSet {
saveExpenses()
}
}
@State private var isAddingExpense = false
var totalExpenses: Double {
expenses.reduce(0) { $0 + $1.amount }
}
var body: some View {
NavigationView {
VStack {
Text("Total Expenses: $(totalExpenses, specifier: "%.2f")")
.font(.headline)
.padding()
List {
ForEach(expenses) { expense in
HStack {
Text(expense.name)
Spacer()
Text("$(expense.amount, specifier: "%.2f")")
}
}
.onDelete(perform: deleteExpense)
}
}
.navigationTitle("Expense Tracker")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
isAddingExpense = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isAddingExpense) {
AddExpenseView(expenses: $expenses)
}
}
.onAppear {
loadExpenses()
}
}
func deleteExpense(at offsets: IndexSet) {
expenses.remove(atOffsets: offsets)
}
func saveExpenses() {
if let encoded = try? JSONEncoder().encode(expenses) {
UserDefaults.standard.set(encoded, forKey: "expenses")
}
}
func loadExpenses() {
if let savedExpenses = UserDefaults.standard.data(forKey: "expenses") {
if let decoded = try? JSONDecoder().decode([Expense].self, from: savedExpenses) {
expenses = decoded
}
}
}
}
In this code:
saveExpenses()encodes theexpensesarray as JSON and saves it toUserDefaults.loadExpenses()retrieves the saved data fromUserDefaultsand decodes it back into theexpensesarray when the view appears.
Conclusion
This guide provides a detailed walkthrough of creating an expense tracker app using SwiftUI. We covered the key aspects such as defining the data model, displaying expenses in a list, adding new expenses, deleting expenses, calculating the total expenses, and implementing data persistence using UserDefaults. By following these steps, you can create a fully functional expense tracker app that helps users manage their personal finances effectively.