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.