BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

SOLVED: Delete rows in sorted List

Forums > SwiftUI

Hello,

I am stuck on the following problem and hope you can help me. I have a view in which I display a list containing the contents of a class. The class contains objects of a structure that consists of two properties: a date and a value.

Struktur:
struct MeterReadingItems: Identifiable, Codable {

    var id = UUID()
    var date: Date
    var kwh: Int
}
Klasse
class MeterReadings180: ObservableObject {

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

        items = []
    }
    @Published var items = [MeterReadingItems]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(items) {
                UserDefaults.standard.set(encoded, forKey: "Items9546_180")
            }
        }
    }
}
View (... not complete View)
List {
    ForEach(readings180.items, id: \.date) { item in
                            HStack {
                                Text(item.date, formatter: dateFormatter)
                                    .environment(\.locale, Locale(identifier: AppData.userLocale))
                                Spacer()
                                Text("\(item.kwh)  kWh")
                            }
                            .listStyle()
                        }
                        .onDelete(perform: removeItems)

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

The input is then realized via a further view. Here, date and value are entered and added to the class. So far it works quite well. Now to my question/problem. I would like to sort the list of items by date. I have now come up with the following:

In the View / List / ForEach
ForEach(readings181.items.sorted(by: {$0.date.compare($1.date) == .orderedDescending}), id: \.date)

This also works at first sight, but not at second, because if I delete rows now everything gets messed up, because probably the function removeItems and IndexSet doesn't work anymore.

Is there a way to solve this in the removeItems function or do I have a bug here in general regarding sorting the list/items?

I am grateful for any advice!

Regards, Ralf

2      

You just need to match up the item(s) in the sorted array that you want to delete with the item(s) in the original array. Use the firstIndex(where:) method on your original array to find the matching item and delete it from there.

Hope that makes sense. No code because I'm typing this on my phone while lying in bed. :)

If you search the forums for the iExpense project, I believe that's the one that has an example of this sort of thing and you can see how others have solved the same problem.

2      

Hi @roosterboy,

thanks for the hint, I will see if I find a solution using firstIndex(where:) .. never used it before. I'm coming/learning from 100days of swift and the little app I'm trying now was based on the stuff I've learned from iExpense project, but unfortunately I wasn't able to find something in the forum that helps to find a solution.

If you find some time later on, maybe you can provide some code snipped for me .. or if I find a solution I will post it here.

2      

Hi,

I have not found a working solution yet, but I would like to know if my theoretical understanding is correct.

  • I determine the index in readings180.items which corresponds to the index of the selected index from the sorted list
  • The function removeItems is passed the selected index as "offsets"
  • I assign to the variable "indexToRemove" the value that I want to set by means of readings180.items.firstIndex(where: { Code to find match from selected to origin})
  • I remove the item at indexToRemove from the orginal array readings180.items

If I have understood this correctly, I am still missing the brilliant idea for the closure ... ??

Any comments are welcome :-)

2      

I've tried a few things and put in a few print statements, but still don't understand how to get this together .... ?? I have extended the removeItems function with print statements to 1. output all items in the original array and 2. the selected entry

func removeItems(at offsets: IndexSet) {          
            for items in readings180.items {
                print(items)
            }
            for offset in offsets {
                let data = readings180.items[offset]
                print ("----------------------------------------------------")
                print("Index: \(offset) \(readings180.items[offset])")
                print ("----------------------------------------------------")
            }
 } 

I have deleted item from "date: 2023-02-10", which was the 3rd item in the list, and here is the output from the console:

MeterReadingItems(id: 6DDFA0E2-C0ED-4DFF-98CD-96EF1BA699D5, date: 2023-03-14 21:01:00 +0000, kwh: 34234)
MeterReadingItems(id: 5809A628-F86C-4390-A120-FFEF4653B19F, date: 2023-01-31 19:56:00 +0000, kwh: 123)
MeterReadingItems(id: 0A86833F-14FB-463C-97E5-D9D31C6119E8, date: 2023-02-28 19:57:00 +0000, kwh: 876)
MeterReadingItems(id: 6C4BC78A-DD14-469A-8637-DDBDD286E7E2, date: 2023-02-10 19:57:00 +0000, kwh: 12)
----------------------------------------------------
Index: 2 MeterReadingItems(id: 0A86833F-14FB-463C-97E5-D9D31C6119E8, date: 2023-02-28 19:57:00 +0000, kwh: 876)
----------------------------------------------------

The selected row is index 3 in the array and not index 2. I currently have no idea how to map the row I select to delete in the sorted list with the entry in the original array.

2      

Hi @Obelix,

first of all thank you very much for the helpful and especially instructive hints about my code! This helps me a lot, because I'm still very much at the beginning and sometimes think I'll never make it ...

First I undid all my previous changes regarding sorting and then tried to incorporate your hints.

The sorting works very well and is now, due to the Comparable in the MeterReadingItem Struct, very clear:

struct MeterReadingItem: Identifiable, Codable, Comparable {
    static func < (lhs: MeterReadingItem, rhs: MeterReadingItem) -> Bool{
            lhs.date > rhs.date
    }

    var id = UUID()
    var date: Date
    var kwh: Int
}

In the view I now only have the following line in the ForEach:

ForEach(readings180.items.sorted()) { item in

Unfortunately, I can't get anywhere with the removeMeterReading function in the MeterReadings180 class. I don't know how to get the UUID of the entry marked as to be deleted by the user? In the .delete modifier I seem to get only the selected index, but I still need the UUID.

In my .onDelete modifier, I call the following:

.onDelete { indexSet in
           for index in indexSet {
                  readings180.removeMeterReading( withID: readings180.items[index].id )
          }
 }

My removeMeterReading function now looks like this:

public func removeMeterReading(withID identifier: UUID) {
        self.items.removeAll {
            $0.id == identifier
        }

        if let encoded = try? JSONEncoder().encode(items) {
            UserDefaults.standard.set(encoded, forKey: "Items9546_180")
        }
 }

Could you help me how to code the .delete modifier in the view to call the removeMeterReading function with the correct selected UUID?

2      

As i understood from conversation there is a mismatch between two arrays. So you have array in your model with @Published wrapper, which you use in your view but sorted. Why not to use sorted() upon init() of that array in your model. So meaning you already have it sorted and no need to use sorting method on ForEach. And in this case you have no mismatch between the model and view so indexes are the same... So you can use your function as it was i.e.

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

2      

May I offer two solutions without further confusing you with all the options offered here. I tried to use your code as much as possible so that you can understand what is going on. Option 1: You sort the array before using it in your view so that it knows the positions of each item and delete them in the right way

struct MeterReadingItem: Identifiable, Codable, Comparable {
    var id = UUID()
    var date: Date
    var kwh: Int

    static func < (lhs: MeterReadingItem, rhs: MeterReadingItem) -> Bool{
        lhs.date > rhs.date
    }
}

class MeterReadings180: ObservableObject {
    @Published var items = [MeterReadingItem]()

    init() {
        // This is just mock data that i used to populate the array so you use your JSONDecoder here
        items = [MeterReadingItem]()
        var itemsToSort = [MeterReadingItem]()
        for _ in 0...100 {
            let calendar = Calendar.current
            var comp = DateComponents()
            comp.year = 2023
            comp.month = Int.random(in: 1...12)
            comp.day = Int.random(in: 1...28)
            let date = calendar.date(from: comp) ?? Date()
            let item = MeterReadingItem(date: date, kwh: Int.random(in: 100...1000))
            itemsToSort.append(item)
        }
        // Sort your array before assigning it to items
        items = itemsToSort.sorted()
    }
}

import SwiftUI

struct ContentView: View {
    @StateObject var readings180 = MeterReadings180()

    var body: some View {
        NavigationStack {
            List {
                // You remove .sorted() as it is already sorted upon init()
                ForEach(readings180.items) { item in
                    HStack {
                        Text(item.kwh.formatted())
                        Spacer()
                        Text(item.date.formatted(date: .abbreviated, time: .omitted))
                    }

                }
                .onDelete(perform: removeItems)

            }
        }
    }

    // Use your remove function as before
    func removeItems(at offsets: IndexSet) {
        readings180.items.remove(atOffsets: offsets)
    }
}

Option 2: More elegant than above but some changes to be made in your code.

struct MeterReadingItem: Identifiable, Codable, Comparable {
    var id = UUID()
    var date: Date
    var kwh: Int

    static func < (lhs: MeterReadingItem, rhs: MeterReadingItem) -> Bool{
        lhs.date > rhs.date
    }
}

class MeterReadings180: ObservableObject {
    @Published var items = [MeterReadingItem]()

    init() {
        // This is just mock data so use your JSONDecoder to fetch the data from UserDefaults
        items = [MeterReadingItem]()
        for _ in 0...100 {
            let calendar = Calendar.current
            var comp = DateComponents()
            comp.year = 2023
            comp.month = Int.random(in: 1...12)
            comp.day = Int.random(in: 1...28)
            let date = calendar.date(from: comp) ?? Date()
            let item = MeterReadingItem(date: date, kwh: Int.random(in: 100...1000))
            items.append(item)
        }
    }

    // You pass an item to delete
    // find its index in your data model and remove it.
    func removeItem(_ item: MeterReadingItem) {
        var indexes = IndexSet()
        if let index = items.firstIndex(of: item) {
            indexes.insert(index)
        }
        items.remove(atOffsets: indexes)
    }

}

import SwiftUI

struct ContentView: View {
    @StateObject var readings180 = MeterReadings180()
    // You sort your readings prior using it in view
    var sortedItems: [MeterReadingItem] {
        readings180.items.sorted()
    }

    var body: some View {
        NavigationStack {
            List {
                ForEach(sortedItems) { item in
                    HStack {
                        Text(item.kwh.formatted())
                        Spacer()
                        Text(item.date.formatted(date: .abbreviated, time: .omitted))
                    }
                    // Instead of using onDelete you use swipe actions which is basically the same
                    .swipeActions {
                        Button(role: .destructive) {
                            // you delete item in your data model
                            // as it has @Published wrapper it posts all the changes and view refreshes
                            readings180.removeItem(item)
                        } label: {
                            Image(systemName: "trash")
                        }

                    }
                }
            }
        }
    }
}

There are many other options to do the same, but all depends on the logic in your workflow.

3      

@Obelix, @ygeras,

Many thanks for your support! Most important for me is the fact that you teach me how what works and what possibilities of the solution one has. This is very helpful for me as a beginner!

I have looked at your ideas and suggestions and tried to understand and implement it for me.

My first important realization, which I've been failing at repeatedly from my understanding over the past few days, the .onDelete modifier does not have access to my "item" object in my list. This is where I get an IndexSet. If I stick to the .onDelete I would use the suggested solution 1. from @ygeras and sort already in the class at init().

Since it is more logical and clearer for me, with the Comparable structure of my items, I tried to implement the ideas of @Obelix and @ygeras and then came for me to the following solution. This seems to work as desired:

The following function to delete the items in the MeterReadingItems180 class:

public func removeMeterReading(_ item: MeterReadingItem) {
        items.removeAll { deleteTarget in
            deleteTarget.id == item.id
        }
        if let encoded = try? JSONEncoder().encode(items) {
            UserDefaults.standard.set(encoded, forKey: "Items9546_180")
        }
    }

In the View (extract, not complete)

var body: some View {
        NavigationView {
            VStack {
                Group {
                    Text("Zähler \(meterNumber)")
                        .titleStyle()
                        .padding(.top, 20)
                    Text(meterInfo)
                        .headlineStyle()
                }

                List {
                    switch meterNumber {
                    case "9546-180":
                        ForEach(readings180.items.sorted()) { item in
                            HStack {
                                Text(item.date, formatter: dateFormatter)
                                    .environment(\.locale, Locale(identifier: AppData.userLocale))
                                Spacer()
                                Text("\(item.kwh)  kWh")
                                Spacer()
                                Text(item.id.uuidString)
                            }
                            .swipeActions {
                                Button(role: .destructive) {
                                    readings180.removeMeterReading(item)
                                } label: {
                                    Image(systemName: "trash")
                                }
                            }
                            .listStyle()
                        }

I will take the approach of @ygeras and do the sorting at the beginning of the structure and then give the sorted array into the view.

The conclusion for me: There is still so much to learn and sometimes I think that it exceeds my horizon, on the other hand it is a lot of fun and thanks to your help you can get further even if there is apparently no progress :-)

2      

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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.