UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Day 38 Challenge 3: How to do it?

Forums > 100 Days of SwiftUI

I played around with it a bit, but couldn't get it. To make sure, are we supposed to present in ContentView 2 separate sections of Personal and Business? I tried and if and else condition, but I'm not sure where to put it. I tried wrap it before ForEach and after, neither works.

ContentView Code:

import SwiftUI

// a single expense
struct ExpenseItem : Identifiable, Codable {
    var id = UUID() // ask Swift to generate a UUID for us
    let name : String
    let type : String
    let amount : Double
}

// we want to store an array of ExpenseItems inside a single object
class Expenses : ObservableObject {
    @Published var items = [ExpenseItem]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(items) {
                UserDefaults.standard.set(encoded, forKey: "Items")
            }
        }
    }

    init() {
        if let savedItems = UserDefaults.standard.data(forKey: "Items") {
            if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
                items = decodedItems
                return
            }
        }

        items = []
    }
}

struct ContentView : View {
    @StateObject var expenses = Expenses() // an existing instance
    @State private var showingAddExpense = false

    var body: some View {
        NavigationView {
            List {
                // no longer needs to tell ForEach which one is unique because with identifiable, ForEach knows id is unique
                ForEach(expenses.items) { item in
                    HStack {
                        VStack(alignment: .leading) {
                            Text(item.name).font(.headline)
                            Text(item.type)
                        }

                        Spacer()
                        // Challenges 1 and 2, format and conditional coloring
                        Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "zh-Hant-HK")).foregroundColor(item.amount < 10 ? .red : item.amount > 100 ? .blue : .orange)
                    }
                }
                .onDelete(perform: removeItems)

            }
            .navigationTitle("iExpense")
            .toolbar {
                Button {
                    showingAddExpense.toggle()
                } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $showingAddExpense) {
                AddView(expenses: expenses)
            }
        }
    }

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

}

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

AddView Code:

import SwiftUI

struct AddView: View {
    @State private var name = ""
    @State private var type = "Personal"
    @State private var amount = 0.0
    // store an Expenses object
    @ObservedObject var expenses : Expenses

    // no need to specify the type. Swift can figure out thanks to the @Environment property wrapper
    @Environment(\.dismiss) var dismiss // the isPresented value will be flipped back to false by the enviornment when we call dismiss() on AddView

    let types = ["Personal", "Business"]

    var body: some View {
        NavigationView {
            Form {

                Section {

                    TextField("Name", text: $name)

                    Picker("Type", selection: $type) {
                        ForEach(types, id : \.self) {
                            Text($0)
                        }
                    }

                    TextField("Amount", value: $amount, format: .currency(code: Locale.current.currencyCode ?? "zh-Hant-HK")).keyboardType(.decimalPad)
                }
            }
            .navigationTitle("Add new expense")
            .toolbar {
                Button("Save") {
                    let item = ExpenseItem(name: name, type: type, amount: amount)
                    expenses.items.append(item)
                    dismiss()
                }
            }
        }
    }
}

struct AddView_Previews: PreviewProvider {
    static var previews: some View {
        AddView(expenses: Expenses()) // can pass in a dummy value for writing purposes
    }
}

2      

I found this one to be difficult when I first came across it. Here's a hint: try adding the following computed property to your Expense class and see if you can figure it out from there:

var personalItems: [ExpenseItem] {
  items.filter { $0.type == "Personal"}
}

Avoid trying to use an if statement here. Think about iterating over the above items in a ForEach like you did for expenses.items.

I do think this challenge is really hard. You've met .filter before but it was a long time ago in the lesson on complex data types.

6      

I found deleting the correct items from Expenses is pretty challenging. Hang in there and ask if you find yourself completely stuck on it.

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

@vtabmow, thank you very much for your hint. Solved it within minutes after seeing your comment haha.

@ty-n-42, I'm still working on that part.

I think for this part, it really depends on how we write the setter for the computed properties --- the ones that vtabmow listed above. This is tricky.

2      

I cannot figure out the last part, how to delete items properly. This is what I've got so far:

I'm having trouble writing out "ExpenseItem" in the setter for personalItems. I think this is the key.

Anybody chime in?

Please ignore the line written for businessItems...

class Expenses : ObservableObject {
    var personalItems : [ExpenseItem] {
        get { items.filter { $0.type == "Personal" } }
        set {
            if let position = items.firstIndex(of: ExpenseItem) {
                items.remove(at: position)
            }
        }
    }

    var businessItems: [ExpenseItem] {
        get { items.filter { $0.type == "Business" } }
        set { items.remove(at: items.firstIndex(where: { $0.amount == $0.amount }) ?? 0) }
    }

2      

Great! You’re halfway there.

You are correct. This is tricky. You will use what you’re learning in this particular challenge a lot, so keep at it.

Your original removeItems method receives an IndexSet of items to be deleted from expenses.items. Now, you probably have two sections in your ContentView where you are implementing a ForEach to list out personalItems and businessItems. So, each one of those sections will:

  1. Need it’s own .onDelete modifier to call its own removeItems method, something like removeBusiness items and removePersonalItems.
  2. In those methods, you will need to iterate over the IndexSet and determine if each item in the indexSet can be found in expenses.items.
  3. If it can be found in expenses.items, delete it.

6      

vtabmow, thank you for your help. I still couldn't get it.

Right now, for the method, I have this:

    func removePersonalItems(at offsets: IndexSet) {
        for (n, c) in offsets.enumerated() {
            if expenses.personalItems.contains(expenses.personalItems[n]) == true {
                expenses.personalItems.remove(at: c)
            }
        }
    }

I believe the for loop works. If it doesn't, I will try ForEach

The trouble is, I still don't know how to set the personalItems property in expenses. I'm having trouble writing that set { }. If I delete the line, the method gives an error that says personalItems is a get-only property.

How do I set it to be something that conforms to a "whatever's left after you delete"?

    var personalItems : [ExpenseItem] {
        get { items.filter { $0.type == "Personal" } }
        set { } ---> this, uh ? 
    }

full code:

struct ExpenseItem : Identifiable, Codable, Equatable {
    var id = UUID() // ask Swift to generate a UUID for us
    let name : String
    let type : String
    let amount : Double
}

// we want to store an array of ExpenseItems inside a single object
class Expenses : ObservableObject {
    var personalItems : [ExpenseItem] {
        get { items.filter { $0.type == "Personal" } }
        set { }
    }

    var businessItems: [ExpenseItem] {
        get { items.filter { $0.type == "Business" } }
    }

    @Published var items = [ExpenseItem]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(items) {
                UserDefaults.standard.set(encoded, forKey: "Items")
            }
        }
    }

    init() {
        if let savedItems = UserDefaults.standard.data(forKey: "Items") {
            if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
                items = decodedItems
                return
            }
        }
        items = []
    }

}

struct ContentView : View {
    @StateObject var expenses = Expenses() // an existing instance
    @State private var showingAddExpense = false

    var body: some View {
        NavigationView {
            List {
                // no longer needs to tell ForEach which one is unique because with identifiable, ForEach knows id is unique
                Section {
                    ForEach(expenses.businessItems) { item in
                        HStack {
                            VStack(alignment: .leading) {
                                Text(item.name).font(.headline)
                                Text(item.type)
                            }

                            Spacer()
                            // Challenges 1 and 2, format and conditional coloring
                            Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "zh-Hant-HK")).foregroundColor(item.amount < 10 ? .red : item.amount > 100 ? .blue : .orange)
                        }
                    }
                    .onDelete(perform: removeBusinessItems)
                }

                Section {
                    ForEach(expenses.personalItems) { item in
                        HStack {
                            VStack(alignment: .leading) {
                                Text(item.name).font(.headline)
                                Text(item.type)
                            }

                            Spacer()
                            // Challenges 1 and 2, format and conditional coloring
                            Text(item.amount, format: .currency(code: Locale.current.currencyCode ?? "zh-Hant-HK")).foregroundColor(item.amount < 10 ? .red : item.amount > 100 ? .blue : .orange)
                        }
                    }
                    .onDelete(perform: removePersonalItems)
                }
            }
            .navigationTitle("iExpense")
            .toolbar {
                Button {
                    showingAddExpense.toggle()
                } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $showingAddExpense) {
                AddView(expenses: expenses)
            }
        }
    }

    func removePersonalItems(at offsets: IndexSet) {
        for (n, c) in offsets.enumerated() {
            if expenses.personalItems.contains(expenses.personalItems[n]) == true {
                expenses.personalItems.remove(at: c)
            }
        }
    }

    func removeBusinessItems(at offsets: IndexSet) {
//        expenses.businessItems.remove(atOffsets: offsets)
    }

}

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

2      

So you want something like this:

func removePersonalItems(at offsets: IndexSet) {

  // look at each item we are trying to delete
  for offset in offsets {

    // look in the personalItems array and get that specific item we are trying to delete. Find it's corresponding match in the expenses.items array.
      if let index = expenses.items.firstIndex(where: {$0.id == expenses.personalItems[offset].id}) {

      // delete the item from the expenses.items array at the index you found its match
        expenses.items.remove(at: index)

      }
    }
  }

5      

That solves it ! Didn't think about removing items from expenses.items, which should have been the one since it is where information is stored, not personalItems or businessItems.

2      

Yeah I'm glad I didnt waste too much time on this one lol Thanks for the solution. I'll definietly learn from it.

2      

Why is the solution from @vtabmow working?

What does the $0.id mean on the context? Is the closure running an implicit for loop over expenses.items on the background? why?

I cannot get my head around why is the solution working. Because it does!

2      

It is not running an implict loop, but an explict loop defined by

for offset in offsets {
…
}

This chooses a single element offset, in sequence, from offsets, and passes it to the closure.

The $0.id is the 'id' property of the first element passed into the closure (it is a relative reference - and in this case is offset). Using $0 also means that you are not giving it an explict name, and is often the way of accessin garray elements in a closure.

Here are a couple of examples of using $0 and $1 which hopefully will help you understand better.

Sort example 1

Sort example 2

Number of parameters in closures

2      

I did manage to do this without adding too much. I was surprised how easy it was, at last. Or have I overlooked something crucial? The "All items"-Section is just for testing purposes, if the wanted items are deleted correctly.

struct ContentView: View {
    @StateObject var expenses = Expenses()
    @State private var showingAddExpense = false

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Personal costs")) {
                    ForEach (expenses.items.filter {$0.type == "Personal"}) { item in
                        HStack{
                            Text(item.name)
                                    .font(.headline)
                            Spacer()
                            AmountView(amount: item.amount)
                        }
                     }
                    .onDelete(perform: removeItems)
                 }

                Section(header: Text("Business costs")) {
                    ForEach (expenses.items.filter {$0.type == "Business"}) { item in
                        HStack{
                            Text(item.name)
                                    .font(.headline)
                            Spacer()
                            AmountView(amount: item.amount)
                        }
                     }
                    .onDelete(perform: removeItems)
                 }

                Section(header: Text("All items")) {
                    ForEach (expenses.items) { item in
                        HStack{
                            VStack(alignment: .leading) {
                                Text(item.name)
                                    .font(.headline)
                                Text(item.type)
                            }
                            Spacer()
                            AmountView(amount: item.amount)
                        }
                     }
                    .onDelete(perform: removeItems)
                }
            }
            .navigationBarTitle("iExpenses")
            .toolbar {
                Button {
                    showingAddExpense.toggle()
                } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $showingAddExpense) {
                AddView(expenses: expenses)
            }
        }
    }

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

struct AmountView: View {
    var amount: Double
    var color: Color {
        switch amount {
        case 0..<10:    return Color.green
        case 10..<100:  return Color.orange
        case 100...:    return Color.red
        default:        return Color.primary
        }
    }

    var body: some View{
        Text(amount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
            .foregroundColor(color)

    }
}

4      

@Ollikely I really like your very simple and, for me, clear solution - good job. For me as a beginner it was very helpful and i have learned a lot from your solution - Thanks!

2      

@olliklee I really like your approach, as it's clean and descriptive. But I am not pretty sure if it's really working.

When iterating like this:

ForEach (expenses.items.filter {$0.type == "Personal"}) { item in ...

and later:

ForEach (expenses.items.filter {$0.type == "Business"}) { item in ...

the filter function always returns a new Array (with offsets starting at 0). So when deleting an item in the second ForEach, the index also starts at 0 and the item in the first List is removed (as the index refers to the whole expenses Array). Or am I missing something?

Cheers Matthias

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.