|
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
}
}
|
|
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.
|
|
I found deleting the correct items from Expenses is pretty challenging. Hang in there and ask if you find yourself completely stuck on it.
|
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.
|
|
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) }
}
|
|
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:
- Need it’s own .onDelete modifier to call its own removeItems method, something like removeBusiness items and removePersonalItems.
- 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.
- If it can be found in expenses.items, delete it.
|
|
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()
}
}
|
|
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)
}
}
}
|
|
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.
|
|
Yeah I'm glad I didnt waste too much time on this one lol
Thanks for the solution. I'll definietly learn from it.
|
|
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!
|
|
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
|
|
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)
}
}
|
|
@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!
|
|
@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
|