100 Days of SwiftUI Day 36

100 Days of SwiftUI – Day 36 – iExpense

It’s day 36 of the 100 Days of SwiftUI! Yesterday, we had a consolidation day to review what we’ve learned over the past week and a half and also create a new app from scratch. Today, we’re starting a new project called iExpense. We’re going to be learning lots of new skills, so let’s dive in!

iExpense SwiftUI app introduction

Our seventh project will be a SwiftUI app called iExpense. It’s an expense tracker that separates personal costs from business costs. In order to make this app with the necessary functionality, we need to learn how to :

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

And even more.

Sharing SwiftUI state with @stateobject

Up until now, we’ve only really worked with structs and not with classes, even though we learned about classes all the way back during day 12. That is about to change!

When using a @State property, SwiftUI knows what value to watch and will reinvoke the body every time a change to that property occurs, which allows the changes to be reflected in our app.

This does not work when using a class, however. Instead of using @State, we need to create an instance of our class as a @StateObject. And that’s not all. Check out the example below, which includes explanations of the meaning of the new types we’re seeing.

import SwiftUI

class User: ObservableObject { // other things can monitor this class for changes
    @Published var firstName = "Bilbo" // @Published announces a change to a value, so that the body can be reinvoked when changes occur
    @Published var lastName = "Baggins"
}

struct ContentView: View {
    
    @StateObject var user = User() //@StateObject is used when you create the data, for example a new instance of a class. Everywhere else where we are reading or modifying it, but not creating a new one, we use @ObservedObject instead
    
    var body: some View {
        VStack {
            Text("Your name is \(user.firstName) \(user.lastName)")
            
            TextField("First name", text: $user.firstName)
            TextField("Last name", text: $user.lastName)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Showing and hiding views

We can show new views to users using a sheet. A sheet is essentially a new view that slides over the current view that can be dismissed either by swiping down, or by adding a button to dismiss it.

import SwiftUI

struct SecondView: View {
    @Environment(\.dismiss) var dismiss
    
    let name: String // our view must have a name property passed in
    
    var body: some View {
        VStack {
            Text("Hello, \(name)")
                .padding(20)
            Button("Dismiss") {
                dismiss()
            }
        }
    }
}

struct ContentView: View {
    
    @State private var showingSheet = false
    
    var body: some View {
        Button("Show Sheet") {
            showingSheet.toggle()
        }
        .sheet(isPresented: $showingSheet) { // present the sheet
            SecondView(name: "Darryl")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Deleting items with onDelete()

SwiftUI gives us the onDelete() modifier, which can be applied to ForEach views only. This modifier allows us to remove data from a collection and is almost exclusively used with a ForEach inside a List.

import SwiftUI

struct ContentView: View {
    
    @State private var numbers = [Int]()
    @State private var currentNumber = 1
    
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(numbers, id: \.self) { // .onDelete can only be called on a ForEach
                        Text("Row \($0)")
                    }
                    
                    .onDelete(perform: removeRows) // Calls onDelete and performs the remvoeRows function
                }
                
                Button("Add Number") {
                    numbers.append(currentNumber) // Each tap on the button adds a new row and increments the current number by 1
                    currentNumber += 1
                }
            }
            .navigationTitle("onDelete()")
            .toolbar {
                EditButton() // adds an edit button so rows can quickly be deleted
        }
        
        }
    }
    
    func removeRows(at offsets: IndexSet) { // An IndexSet tells us the position of all the items in the ForEach that should be removed.
        numbers.remove(atOffsets: offsets)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Storing user settings with UserDefaults

Most users expect apps to store their data, which is only logical. There are various ways to accomplish this in SwiftUI, but today, we’re looking at UserDefaults. UserDefaults allows us to store some data that get loaded each time our app boots.

import SwiftUI

struct ContentView: View {
    
    @State private var tapCount = UserDefaults.standard.integer(forKey: "Tap") // Reads the tapCount back from UserDefaults. It takes a few seconds for the app to write this to permanent storage, so data might not be saved if the app is closed very quickly after the value has updated. It might not have been written to storage.
    
    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
            UserDefaults.standard.set(tapCount, forKey: "Tap") // We create UserDefaults and put in our tapCount. We assign tapCount to a key named "Tap".
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

SwiftUI also provides a property wrapper called @AppStorage that wraps around and effectively ignores UserDefaults. Like a State property, @AppStorage checks if a value changes and will reinvoke the body when a change occurs in the property.

import SwiftUI

struct ContentView: View {
    
    @AppStorage("tapCount") private var tapCount = 0
    
    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Apple does not recommend storing a lot of data in @AppStorage, so we should be careful with that. It’s useful for storing simple values like Integers and Booleans but it’s less useful for complex data, like a Swift struct.

Archiving Swift objects with Codable

When working with simple data like Integers, Booleans, Strings, etc. Swift provides us with a protocol called Codeable: a protocol specifically for archiving and unarchiving data, which is a fancy way of saying “converting objects into plain text and back again.”

import SwiftUI

struct User: Codable {
    let firstName: String
    let lastName: String
}

struct ContentView: View {
    
    @State private var user = User(firstName: "Taylor", lastName: "Swift")
    
    var body: some View {
        Button("Save User") {
            let encoder = JSONEncoder() // Create an instance of a JSON encoder. If you want to unarchive, you can use JSONDecoder()
            
            if let data = try? encoder.encode(user) { // data is a new datatype called Data (with a capital D). This might throw errors, so we have to use try?
                UserDefaults.standard.set(data, forKey: "UserData") // Stores our user, which has been encoded as Data, in UserDefaults
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And that’s it for day 36! It’s high time to recharge as tomorrow, we’ll be building our iExpense app. Onwards and upwards we go.

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 82

100 Days of SwiftUI – Day 96

100 Days of SwiftUI – Day 89

100 Days of SwiftUI – Day 44