Creating a Budget Planner App in SwiftUI

Managing personal finances can be challenging, but building a budget planner app can provide a tailored and efficient solution. SwiftUI, Apple’s modern UI framework, offers an intuitive and declarative way to create such an app. This post will guide you through building a budget planner app using SwiftUI, covering essential features like income and expense tracking, category management, and visualizations.

Why Build a Budget Planner App?

  • Personalization: Tailor the app to fit your specific financial needs and habits.
  • Cost-Effectiveness: Avoid subscription fees associated with many commercial budgeting apps.
  • Learning Experience: Enhance your skills in SwiftUI, data management, and UI design.

Key Features of a Budget Planner App

  • Income Tracking: Record sources and amounts of income.
  • Expense Tracking: Log expenses, categorize them, and track spending patterns.
  • Budget Setting: Define budgets for different categories (e.g., food, rent, entertainment).
  • Data Visualization: Use charts and graphs to visualize income, expenses, and budget status.
  • Category Management: Add, edit, and delete expense categories.

Step-by-Step Implementation

Step 1: Setting Up the Project

Create a new Xcode project using the SwiftUI App template. Name your project “BudgetPlanner”.

Step 2: Data Model

Define the data structures to represent income, expenses, and categories.


import Foundation

// Represents an expense category
struct Category: Identifiable, Codable {
    var id = UUID()
    var name: String
    var color: String // Store the color as a string (e.g., "Red", "Blue")
}

// Represents an income entry
struct Income: Identifiable, Codable {
    var id = UUID()
    var date: Date
    var amount: Double
    var source: String
}

// Represents an expense entry
struct Expense: Identifiable, Codable {
    var id = UUID()
    var date: Date
    var amount: Double
    var category: Category
    var notes: String
}

Step 3: Data Storage

Use @State and @Environment to manage data and provide persistence.


import SwiftUI

// Observable object to manage categories
class CategoryViewModel: ObservableObject {
    @Published var categories: [Category] = [] {
        didSet {
            saveCategories()
        }
    }
    
    init() {
        loadCategories()
    }
    
    let categoriesKey: String = "categories_list"

    func saveCategories() {
        if let encodedData = try? JSONEncoder().encode(categories) {
            UserDefaults.standard.set(encodedData, forKey: categoriesKey)
        }
    }

    func loadCategories() {
        guard
            let data = UserDefaults.standard.data(forKey: categoriesKey),
            let savedCategories = try? JSONDecoder().decode([Category].self, from: data)
        else { return }
        
        self.categories = savedCategories
    }

    func addCategory(name: String, color: String) {
        let newCategory = Category(name: name, color: color)
        categories.append(newCategory)
    }
    
    func deleteCategory(indexSet: IndexSet) {
        categories.remove(atOffsets: indexSet)
    }
}

// Observable object to manage income and expenses
class BudgetViewModel: ObservableObject {
    @Published var incomes: [Income] = [] {
        didSet {
            saveIncomes()
        }
    }
    @Published var expenses: [Expense] = [] {
        didSet {
            saveExpenses()
        }
    }
    
    init() {
        loadIncomes()
        loadExpenses()
    }
    
    let incomesKey: String = "incomes_list"
    let expensesKey: String = "expenses_list"

    func saveIncomes() {
        if let encodedData = try? JSONEncoder().encode(incomes) {
            UserDefaults.standard.set(encodedData, forKey: incomesKey)
        }
    }

    func loadIncomes() {
        guard
            let data = UserDefaults.standard.data(forKey: incomesKey),
            let savedIncomes = try? JSONDecoder().decode([Income].self, from: data)
        else { return }
        
        self.incomes = savedIncomes
    }
    
    func saveExpenses() {
        if let encodedData = try? JSONEncoder().encode(expenses) {
            UserDefaults.standard.set(encodedData, forKey: expensesKey)
        }
    }

    func loadExpenses() {
        guard
            let data = UserDefaults.standard.data(forKey: expensesKey),
            let savedExpenses = try? JSONDecoder().decode([Expense].self, from: data)
        else { return }
        
        self.expenses = savedExpenses
    }

    func addIncome(date: Date, amount: Double, source: String) {
        let newIncome = Income(date: date, amount: amount, source: source)
        incomes.append(newIncome)
    }

    func addExpense(date: Date, amount: Double, category: Category, notes: String) {
        let newExpense = Expense(date: date, amount: amount, category: category, notes: notes)
        expenses.append(newExpense)
    }
    
    func deleteIncome(indexSet: IndexSet) {
        incomes.remove(atOffsets: indexSet)
    }
    
    func deleteExpense(indexSet: IndexSet) {
        expenses.remove(atOffsets: indexSet)
    }
}

To utilize these ObservableObject in our views:


@main
struct BudgetPlannerApp: App {
    @StateObject var categoryViewModel = CategoryViewModel()
    @StateObject var budgetViewModel = BudgetViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(categoryViewModel)
                .environmentObject(budgetViewModel)
        }
    }
}

Step 4: Income Tracking View

Create a view to add and display income entries.


import SwiftUI

struct IncomeView: View {
    @EnvironmentObject var budgetViewModel: BudgetViewModel
    @State private var newIncomeDate: Date = Date()
    @State private var newIncomeAmount: String = ""
    @State private var newIncomeSource: String = ""
    @State private var isAddingIncome: Bool = false

    var body: some View {
        NavigationView {
            List {
                ForEach(budgetViewModel.incomes) { income in
                    HStack {
                        Text(income.source)
                        Spacer()
                        Text("$\(String(format: "%.2f", income.amount))")
                    }
                }
                .onDelete(perform: budgetViewModel.deleteIncome)
            }
            .navigationTitle("Income")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        isAddingIncome = true
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $isAddingIncome) {
                AddIncomeView(isPresented: $isAddingIncome)
                    .environmentObject(budgetViewModel)
            }
        }
    }
}

struct AddIncomeView: View {
    @EnvironmentObject var budgetViewModel: BudgetViewModel
    @Binding var isPresented: Bool
    @State private var newIncomeDate: Date = Date()
    @State private var newIncomeAmount: String = ""
    @State private var newIncomeSource: String = ""

    var body: some View {
        NavigationView {
            Form {
                DatePicker("Date", selection: $newIncomeDate, displayedComponents: .date)
                TextField("Amount", text: $newIncomeAmount)
                    .keyboardType(.numberPad)
                TextField("Source", text: $newIncomeSource)
                
                Button("Add Income") {
                    if let amount = Double(newIncomeAmount) {
                        budgetViewModel.addIncome(date: newIncomeDate, amount: amount, source: newIncomeSource)
                        isPresented = false
                    }
                }
            }
            .navigationTitle("Add Income")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Dismiss") {
                        isPresented = false
                    }
                }
            }
        }
    }
}

Step 5: Expense Tracking View

Implement a view to record and categorize expenses.


import SwiftUI

struct ExpenseView: View {
    @EnvironmentObject var budgetViewModel: BudgetViewModel
    @EnvironmentObject var categoryViewModel: CategoryViewModel
    @State private var isAddingExpense: Bool = false

    var body: some View {
        NavigationView {
            List {
                ForEach(budgetViewModel.expenses) { expense in
                    HStack {
                        Text(expense.category.name)
                        Spacer()
                        Text("$\(String(format: "%.2f", expense.amount))")
                    }
                }
                .onDelete(perform: budgetViewModel.deleteExpense)
            }
            .navigationTitle("Expenses")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        isAddingExpense = true
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $isAddingExpense) {
                AddExpenseView(isPresented: $isAddingExpense)
                    .environmentObject(budgetViewModel)
                    .environmentObject(categoryViewModel)
            }
        }
    }
}

struct AddExpenseView: View {
    @EnvironmentObject var budgetViewModel: BudgetViewModel
    @EnvironmentObject var categoryViewModel: CategoryViewModel
    @Binding var isPresented: Bool
    @State private var newExpenseDate: Date = Date()
    @State private var newExpenseAmount: String = ""
    @State private var newExpenseCategory: Category? = nil
    @State private var newExpenseNotes: String = ""

    var body: some View {
        NavigationView {
            Form {
                DatePicker("Date", selection: $newExpenseDate, displayedComponents: .date)
                TextField("Amount", text: $newExpenseAmount)
                    .keyboardType(.numberPad)
                
                Picker("Category", selection: $newExpenseCategory) {
                    Text("Select Category").tag(nil as Category?)
                    ForEach(categoryViewModel.categories) { category in
                        Text(category.name).tag(category as Category?)
                    }
                }
                TextField("Notes", text: $newExpenseNotes)

                Button("Add Expense") {
                    if let amount = Double(newExpenseAmount), let category = newExpenseCategory {
                        budgetViewModel.addExpense(date: newExpenseDate, amount: amount, category: category, notes: newExpenseNotes)
                        isPresented = false
                    }
                }
            }
            .navigationTitle("Add Expense")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Dismiss") {
                        isPresented = false
                    }
                }
            }
        }
    }
}

Step 6: Category Management View

Create a view to add, edit, and delete expense categories.


import SwiftUI

struct CategoryView: View {
    @EnvironmentObject var categoryViewModel: CategoryViewModel
    @State private var newCategoryName: String = ""
    @State private var newCategoryColor: String = "Red" // Default color
    @State private var isAddingCategory: Bool = false
    let availableColors = ["Red", "Green", "Blue", "Yellow", "Purple", "Orange"] // List of available colors

    var body: some View {
        NavigationView {
            List {
                ForEach(categoryViewModel.categories) { category in
                    HStack {
                        Text(category.name)
                        Spacer()
                        Circle()
                            .fill(Color(category.color)) // Convert color string to SwiftUI Color
                            .frame(width: 20, height: 20)
                    }
                }
                .onDelete(perform: categoryViewModel.deleteCategory)
            }
            .navigationTitle("Categories")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        isAddingCategory = true
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $isAddingCategory) {
                AddCategoryView(isPresented: $isAddingCategory)
                    .environmentObject(categoryViewModel)
            }
        }
    }
}

struct AddCategoryView: View {
    @EnvironmentObject var categoryViewModel: CategoryViewModel
    @Binding var isPresented: Bool
    @State private var newCategoryName: String = ""
    @State private var newCategoryColor: String = "Red" // Default color
    let availableColors = ["Red", "Green", "Blue", "Yellow", "Purple", "Orange"] // List of available colors

    var body: some View {
        NavigationView {
            Form {
                TextField("Category Name", text: $newCategoryName)

                Picker("Category Color", selection: $newCategoryColor) {
                    ForEach(availableColors, id: \\.self) { color in
                        Text(color).tag(color)
                    }
                }

                Button("Add Category") {
                    categoryViewModel.addCategory(name: newCategoryName, color: newCategoryColor)
                    isPresented = false
                }
            }
            .navigationTitle("Add Category")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Dismiss") {
                        isPresented = false
                    }
                }
            }
        }
    }
}

Step 7: Data Visualization

Utilize charts to provide insights into spending patterns. Libraries like Swift Charts can be used.


// Placeholder for Data Visualization View
import SwiftUI
import Charts

struct VisualizationView: View {
    @EnvironmentObject var budgetViewModel: BudgetViewModel
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Expense Distribution")
                    .font(.title)
                
                // Create a dictionary to hold category-wise expenses
                let categoryExpenses = Dictionary(grouping: budgetViewModel.expenses, by: { $0.category.name })
                    .mapValues { expenses in
                        expenses.reduce(0) { (result, expense) -> Double in
                            result + expense.amount
                        }
                    }
                
                // Convert dictionary to array of tuples for Chart
                let chartData = categoryExpenses.map { (category, amount) in
                    (category: category, amount: amount)
                }
                
                // Pie Chart using Swift Charts
                Chart(chartData, id: \\.category) { item in
                    SectorMark(
                        angle: .value("Amount", item.amount),
                        innerRadius: .ratio(0.6) // Makes it a donut chart
                    )
                    .cornerRadius(5)
                    .foregroundStyle(by: .value("Category", item.category))
                }
                .padding()
            }
            .navigationTitle("Visualizations")
        }
    }
}

Step 8: Tab View for Navigation

Integrate all views using a TabView for easy navigation.


import SwiftUI

struct ContentView: View {
    var body: some View {
        TabView {
            IncomeView()
                .tabItem {
                    Label("Income", systemImage: "arrow.up.doc")
                }
            ExpenseView()
                .tabItem {
                    Label("Expenses", systemImage: "arrow.down.doc")
                }
            CategoryView()
                .tabItem {
                    Label("Categories", systemImage: "folder")
                }
            VisualizationView()
                .tabItem {
                    Label("Visualizations", systemImage: "chart.pie")
                }
        }
    }
}

Enhancements and Future Features

  • Budget Setting: Implement the ability to set budgets for different categories.
  • Recurring Transactions: Support recurring income and expenses.
  • Notifications: Set up notifications for budget alerts and reminders.
  • Cloud Sync: Integrate iCloud or other services for data synchronization across devices.

Conclusion

Building a budget planner app in SwiftUI provides a fantastic way to manage personal finances efficiently. This post covers essential features like income and expense tracking, category management, data visualization, and provides code samples to get you started. By continuing to develop and enhance this app, you can create a personalized financial tool tailored to your needs.