30 Days of SwiftUI - Day 19: Mastering State, Persistence, and Deletion in SwiftUI


Hello Swift developers! Today, we're building a mini expense tracking app, iExpense, and diving deep into essential SwiftUI concepts like state management, data persistence, and list manipulation. Let's get started!

State Management Demystified: Using @State with Classes

While @State is typically used with structs, it can also be used with classes. However, it's essential to understand the implications of reference types.

Example: @State with a Class (Conceptual)

import SwiftUI

class MyData: ObservableObject {
    @Published var count = 0
}

struct ClassStateView: View {
    @StateObject private var data = MyData() // Use @StateObject for classes

    var body: some View {
        Text("Count: \(data.count)")
        Button("Increment") {
            data.count += 1
        }
    }
}
  • @StateObject: Is used for classes that conform to ObservableObject, ensure the class is created only once.
  • @Published: Notifies views of changes.

State Sharing Simplified: Sharing SwiftUI State with @Observable

@Observable allows sharing state between views.

Example: Sharing State with @Observable

import SwiftUI

@Observable
class Expenses {
    var items = [ExpenseItem]()
}

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

struct ExpensesView: View {
    @State private var expenses = Expenses()

    var body: some View {
        List {
            ForEach(expenses.items) { item in
                Text("\(item.name) - \(item.amount)")
            }
        }
    }
}
  • @Observable: Makes the class observable.
  • ExpenseItem: Conforms to Identifiable and Codable.

Visibility Control: Showing and Hiding Views

We'll use state to control the visibility of views, such as an "add expense" form.

Example: Showing and Hiding a View

import SwiftUI

struct ToggleView: View {
    @State private var showDetails = false

    var body: some View {
        VStack {
            Button("Toggle Details") {
                showDetails.toggle()
            }
            if showDetails {
                Text("Detailed Information")
            }
        }
    }
}

Visual Representation:

  • A button toggles the visibility of "Detailed Information" text.

Data Removal: Deleting Items Using onDelete()

We'll allow users to delete expenses from the list.

Example: Deleting Items from a List

import SwiftUI

struct DeleteListView: View {
    @State private var items = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        List {
            ForEach(items, id: \.self) { item in
                Text(item)
            }
            .onDelete(perform: deleteItems)
        }
    }

    func deleteItems(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}

Visual Representation:

  • A list with the ability to swipe and delete items.

User Preferences: Storing User Settings with UserDefaults

We'll use UserDefaults to store user preferences, such as default settings.

Example: Storing User Settings

import SwiftUI

struct UserDefaultsView: View {
    @State private var username = ""

    var body: some View {
        TextField("Username", text: $username)
            .onDisappear {
                UserDefaults.standard.set(username, forKey: "username")
            }
            .onAppear {
                username = UserDefaults.standard.string(forKey: "username") ?? ""
            }
            .padding()
    }
}
  • UserDefaults.standard.set(value, forKey:): Stores data.
  • UserDefaults.standard.string(forKey:): Retrieves data.

Data Archival: Archiving Swift Objects with Codable

We'll use Codable to archive and unarchive expense items.

Example: Codable Struct

struct ExpenseItem: Identifiable, Codable {
    var id = UUID()
    let name: String
    let type: String
    let amount: Double
}
  • Codable: Enables encoding and decoding of objects.

Interactive Lists: Building a List We Can Delete From

Combining ForEach and onDelete(), we'll create an interactive list.

Example: Deletable List with Codable Items

import SwiftUI

struct CodableListView: View {
    @State private var expenses = [ExpenseItem]()

    var body: some View {
        List {
            ForEach(expenses) { item in
                Text("\(item.name) - \(item.amount)")
            }
            .onDelete(perform: deleteItems)
        }
    }

    func deleteItems(at offsets: IndexSet) {
        expenses.remove(atOffsets: offsets)
    }
}

Unique Identifiers: Working with Identifiable Items in SwiftUI

Identifiable ensures each item in a list has a unique ID.

Example: Identifiable Struct

struct IdentifiableItem: Identifiable {
    let id = UUID()
    let name: String
}

State Propagation: Sharing an Observed Object with a New View

We'll share the Expenses object with a new view for adding expenses.

Example: Sharing Observed Object

import SwiftUI

struct AddExpenseView: View {
    @Binding var expenses: Expenses

    var body: some View {
        Button("Add Expense") {
            let newItem = ExpenseItem(name: "New Item", type: "Personal", amount: 10.0)
            expenses.items.append(newItem)
        }
    }
}

struct ExpensesView2: View {
    @State private var expenses = Expenses()
    @State private var showAddExpense = false

    var body: some View {
        VStack {
            List {
                ForEach(expenses.items) { item in
                    Text("\(item.name) - \(item.amount)")
                }
                .onDelete(perform: deleteItems)
            }
            Button("Add Item") {
                showAddExpense = true
            }
        }
        .sheet(isPresented: $showAddExpense) {
            AddExpenseView(expenses: $expenses)
        }
    }

    func deleteItems(at offsets: IndexSet) {
        expenses.items.remove(atOffsets: offsets)
    }
}

Persistent Data: Making Changes Permanent with UserDefaults

We'll use UserDefaults to save and load expense data.

Example: Saving and Loading Codable Data

import SwiftUI

struct PersistentListView: View {
    @State private var expenses = [ExpenseItem]()

    var body: some View {
        // ... list and delete code ...
        .onAppear(perform: loadData)
        .onDisappear(perform: saveData)
    }

    func loadData() {
        if let data = UserDefaults.standard.data(forKey: "Expenses") {
            if let decoded = try? JSONDecoder().decode([ExpenseItem].self, from: data) {
                expenses = decoded
            }
        }
    }

    func saveData() {
        if let encoded = try? JSONEncoder().encode(expenses) {
            UserDefaults.standard.set(encoded, forKey: "Expenses")
        }
    }
}

🔥 Conclusion

Day 19 has been a deep dive into building a functional iExpense app. We've covered state management, data persistence, and list manipulation. Keep building, and you'll be creating more robust and user-friendly apps!


Follow me on Linkedin: igatitech 🚀🚀🚀

Comments

Popular posts from this blog

30 Days of SwiftUI — Day 1: Getting Started with SwiftUI

30 Days of SwiftUI Learning Journey: From Zero to Hero!

30 Days of SwiftUI - Day 11: Building Interactive SwiftUI Apps