100 Days of SwiftUI

Day 36

2026-05-25

Project 7, part 1

Your core SwiftUI skills are strong, so it’s time to push past the basics and start building bigger apps.

Lucy Ricardo laughing, which turns into a wail

iExpense: Introduction

Learning how to

  • Present and dismiss a second screen of data.
  • Delete rows from a list
  • Save and load user data

Using @State with classes

  • @State is for the current view
  • recall that structs are independent, so changing one does not change other copies of it
  • classes all point to the same data, so changing it one place changes it everywhere
  • If you define a class, say User() and change it in a view, the @State variable won’t track changes to it
    • to “fix” this, use the @Observable keyword before the class definition

Sharing SwiftUI state with @Observable

If you use @State with a struct, your SwiftUI view will update automatically when a value changes, but if you use @State with a class then you must mark that class with @Observable if you want SwiftUI to watch its contents for changes.

  • @Observable tells SwiftUI to watch each individual property inside the class for changes
  • @Observable is a macro (from the Observation framework) that has a lot (a lot a lot) of hidden stuff inside it

When working with structs, the @State property wrapper keeps a value alive and also watches it for changes. On the other hand, when working with classes, @State is just there for keeping object alive – all the watching for changes and updating the view is taken care of by @Observable.

Showing and hiding views

  • sheet is a new view presented on top of the existing one
  • similar to alerts, we define the conditions when it should be shown
  • you can pass variable values to the sheet if needed
  • dismissing the sheet view
    • drag it out of the way
    • add something like @Environment(\.dismiss) var dismiss to the second view and put a dismiss() button on it
  • @Environment allows us to create properties that store values provided to us externally (from the user’s device settings)

To dismiss another view we need another property wrapper – and yes, I realize that so often the solution to a problem in SwiftUI is to use another property wrapper!

Deleting items using onDelete()

This is another place where SwiftUI does a heck of a lot of work on our behalf, but it does have a few interesting quirks…

  • onDelete() is almost exclusively used with ForEach and List
  • onDelete() modifier only exists on ForEach because List can have static rows (Quirk 1)
  • think of IndexSet as a helper for onDelete()
    • it’s a Set of Indexes that happen to be sorted, and you make a func for .onDelete(perform: func) and put it on the ForEach
  • user can then delete row by swiping left
  • and we get a “free” toolbar button for Edit/Delete to do several at the same time (although for this implementation it’s faster and easier to swipe)
    • .toolbar { EditButton() }

Storing user settings with UserDefaults

  • users expect their preferences to stick
  • UserDefaults are good for a few preferences (how much is too much? It depends on how long it takes to load when the app starts)
    • when was the app last launched
    • last item read
    • app preferences (e.g. music: on or off?)
  • write to UserDefaults when something happens you want to track (value, forKey:"key") e.g. UserDefaults.standard.set(tapCount, forKey: "Tap")
  • call the stored data when you initialize the struct: @State private var tapCount = UserDefaults.standard.integer(forKey: "Tap")
  • supported by the property wrapper @AppStorage, which is useful for simple data (not structs, for instance)
  • reduces the code to @AppStorage("tapCount") private var tapCount = 0 and no call to UserDefaults is required

Archiving Swift objects with Codable

  • for when @AppStorage isn’t enough
  • Codable protocol is frequently used with JSONEncoder and its counterpart JSONDecoder
  • for example, this saves data to UserDefaults
    • data is a constant of type Data
    • anything that is data can be stored as type Data
Button("Save User") {
    let encoder = JSONEncoder()

    if let data = try? encoder.encode(user) {
        UserDefaults.standard.set(data, forKey: "UserData")
    }
}

Lebowski stirs a white Russian while watching