leontedev / ExpenseTracker

SwiftUI - Project 7 - iExpense

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Expense Tracker

A SwiftUI Expenses Tracker.

gif.gif gif2.gif

Day 36

@ObservedObject & @Published

Using classes to track data & state across multiple views, as structs are owned by a single object.

class User {
    @Published var firstName = "Bilbo"
    @Published var lastName = "Baggins"
}

@ObservedObject var user = User()

Sheets are similar to Alerts in SwiftUI, where you would use a property to track it’s state. And then add it as a view modifier:

Button("Show Sheet, greet: Leonte") {
  self.showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
   SecondView(name: "Leonte")
 }

ForEach {}.onDelete(perform: removeRows)

It has to be added as a modifier on ForEach, it won’t work directly with a List. The onDelete(perform:) takes as parameter the name of a function which takes an IndexSet: Optional<(IndexSet) -> Void And in it we can directly call the remove(atOffsets:) array function.

func removeRows(at offsets: IndexSet) {
    numbers.remove(atOffsets: offsets)
}

Edit Mode

Using a NavigationView, add a modifier on the inner view (eg: VStack):

.navigationBarItems(leading: EditButton())

UserDefaults

UserDefaults.standard.set(self.tapCount, forKey: "Tap") //to set
@State var tapCount = UserDefaults.standard.integer(forKey: "Tap") // to retrieve

Codable

Day 37

Sharing data between views using classes that conform to the ObservableObject protocol

We defined a class outside of the ContentView, which contains properties marked with @Published. Each time the value is updated by any of the views, the other views will be reloaded.

class Expenses: ObservableObject {
    @Published var items = [ExpenseItem]()

And then in the main view we create a new instance:

struct ContentView: View {
    @ObservedObject var expenses = Expenses()

However in the other views we don’t create another instance:

struct AddView: View {
    @ObservedObject var expenses: Expenses

Instead, we pass a reference to the original instance, eg: in ContentView:

.sheet(isPresented: $showingAddExpense, content: {
    AddView(expenses: self.expenses)
})

By having the ExpenseItem struct conform to Identifiable, we don’t need to specify a unique id in ForEach { }

To conform to the Identifiable protocol, we need the type to contain a property called id:

struct ExpenseItem: Identifiable, Codable {
    let id = UUID()
    let name: String
    let type: String
    let amount: Int
}

And now ForEach can uniquely identify each value of the array of ExpenseItems

Reading & Writing user data to UserDefaults.standard

On instantiating the Expenses class - we read the data from UserDefaults. And when data is added or removed, we update the UserDefaults data as well:

class Expenses: ObservableObject {
    @Published var items = [ExpenseItem]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(items) {
                UserDefaults.standard.set(data, forKey: "Items")
            }
        }
    }
    
    init() {
        if let data = UserDefaults.standard.data(forKey: "Items") {
            let decoder = JSONDecoder()
            if let decodedData = try? decoder.decode([ExpenseItem].self, from: data) {
                items = decodedData
                return
            }
        }
        items = []
    }
}

Day 38

  1. Add an Edit/Done button to ContentView so users can delete rows more easily.

Added a .navigationBarItems(leading: EditButton()) on the child view of the NavigationView

  1. Modify the expense amounts in ContentView to contain some styling depending on their value – expenses under $10 should have one style, expenses under $100 another, and expenses over $100 a third style. What those styles are depend on you.

Had some trouble with this error, when adding two view modifiers to the same Text view: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.

Text("$\(item.amount)")
    .foregroundColor(item.amount > 100 ? .red : .black)
    .font(item.amount < 10 ? .caption : .headline)

I extracted the subview (the Canvas has to be enabled for the option to appear in the Actions menu) and the error went away. There seems to be a bug/weird behaviour caused by List/ForEach in conjunction with more… “complex” views.

[...]

    ItemAmount(item: item)

[...]


struct ItemAmount: View {
    var item: ExpenseItem
    
    var body: some View {
        Text("$\(item.amount)")
            .foregroundColor(item.amount < 10 ? .gray : .black)
            .font(item.amount > 100 ? .headline : .body)
    }
}
  1. Add some validation to the Save button in AddView. If you enter “fish” or another thing that can’t be converted to an integer, show an alert telling users what the problem is.
if let actualAmount = Int(self.amount) {
                    let item = ExpenseItem(name: self.name, type: self.type, amount: actualAmount)
                    self.expenses.items.append(item)
                    self.name = ""
                    self.amount = ""
                    self.presentationMode.wrappedValue.dismiss()
                } else {
                    self.showAlert = true
                }
            })
        }.alert(isPresented: $showAlert) {
            Alert(title: Text("Invalid amount"), message: Text("You need to enter an Integer number."))
        }

About

SwiftUI - Project 7 - iExpense


Languages

Language:Swift 100.0%