It’s day 37 of the 100 Days of SwiftUI! Yesterday, we’ve had our introduction to the iExpense app, our 7th project. Today, we make the app and polish it up so that’s fully functional and usable.
This is the first project were we’ve been working in various files, instead of only the ContentView
, so I’ll be breaking up my code accordingly so that it’s easier to read and understand. I’ve added tons of comments in the code explaining what’s happening.
ExpenseItem
We’ve created a new file called ExpenseItem.swift
. This file contains a struct for an ExpenseItem
.
import Foundation
struct ExpenseItem: Identifiable, Codable { // Represents a single expense, Identifiable means that it can be identified, codable allows the JSON Encoder/Decoder to work with the data in the struct
var id = UUID() // Generates a unique ID for each instance of ExpenseItem
let name: String
let type: String
let amount: Double
}
Expenses
The second file we created is Expenses.swift
and it contains our Expenses
class.
import Foundation
class Expenses: ObservableObject {
@Published var items = [ExpenseItem]() { // An array of ExpenseItem
didSet { // the code within didSet will run any time a change is detected in our ExpenseItem array
let encoder = JSONEncoder() // create an instance of the JSONEncoder
if let encoded = try? encoder.encode(items) { // create an encoded variable that will store the encoded items. Try? is used cause it might throw an error
UserDefaults.standard.set(encoded, forKey: "Items") // Store our encoded items with the key "Items"
}
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Items") { // read data from UserDefaults for the key "Items"
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) { // If we can read the data, attempt decoded and convert the data into ExpenseItem.self. .self in this case means that mean an array of ExpenseItems.
items = decodedItems // the array of ExpenseItem is now an array of ExpenseItem that contains the decodedItems
return
}
}
items = [] // If code does not run, for example if the data can't be read or there isn't any data to be read, return an empty array instead
}
}
AddView
In order to add new items to our items
array in a clean and user-friendly way, we created a new view where the user can fill out the details for their expense.
import SwiftUI
struct AddView: View {
@ObservedObject var expenses: Expenses // Tells SwiftUI that our view requires an instant of the Expenses class to be passed in to read/modify. We are NOT creating a new instance, we are just passing one in that already exists.
@Environment(\.dismiss) var dismiss // dismisses the view when it is called
@State private var name = ""
@State private var type = "Personal"
@State private var amount = 0.0
let types = ["Business", "Personal"]
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
Picker("Type", selection: $type) {
ForEach(types, id: \.self) {
Text($0)
}
}
TextField("Amount", value: $amount, format: .currency(code: "USD"))
.keyboardType(.decimalPad) // Used to get the number specific keyboard
}
.navigationTitle("Add new expense")
.toolbar{
Button("Save") {
let item = ExpenseItem(name: name, type: type, amount: amount)
expenses.items.append(item) // We create an item and bind the values of our form to the item, then add that item to our list of items in the instance of the expenses class.
dismiss() // closes the view when the save button is pressed
}
}
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView(expenses: Expenses())
}
}
ContentView
Lastly, our ContentView
, the main view
for our app.
import SwiftUI
struct ContentView: View {
@StateObject var expenses = Expenses() // create a new instance of our Expenses class
@State private var showingAddExpense = false // Used to track whether our sheet containing our AddView view should be sown or not
var body: some View {
NavigationView {
List {
ForEach(expenses.items) { item in // Loop through our items in the expenses class instance
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.type)
}
Spacer()
Text(item.amount, format: .currency(code: "USD"))
}
}
.onDelete(perform: removeItems) // Used to delete a row
}
.navigationTitle("iExpense")
.toolbar{
Button {
showingAddExpense = true // If the button is pressed, a sheet is presented with our AddView view
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $showingAddExpense) { // presents a sheet when showingAddExpense = true
AddView(expenses: expenses) // shows our AddView view and passes in our instance of the expenses class
}
}
}
func removeItems(at offsets: IndexSet) { // function used to delete items in our list
expenses.items.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}



iExpense SwiftUI Wrap up
And that’s it for day 37! I hope you enjoyed going through the development of this app as much as I did. Tomorrow, we’ll review what we’ve learned in this project as well as face a few challenges. Time to recharge the batteries!
100 Days of SwiftUI – Day 37