Libiph
Libiph

Reputation: 537

Is there a way to remove a row in a list in SwiftUI?

I have created a quiet simple list in SwiftUI and want to make it editable, like a tableView in UIKit. I want to remove a row in the list with the all known gesture (swipe from, the right to the left).

I have tried to make with a button above the list, but it doesn't look nice an is not practicable for my app.

struct singleIsland: Identifiable {
    let id: Int
    let name:String
}

var islands = [
singleIsland(id: 0, name: "Wangerooge"),
singleIsland(id: 1, name: "Spiekeroog"),
singleIsland(id: 2, name: "Langeoog")
]

var body: some View {
    VStack {

        List(islands) { island in
            Text(island.name)
        }
    }
}

Upvotes: 8

Views: 12391

Answers (4)

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 119272

Add this to the list:

.onDelete { $0.forEach { islands.remove(at: $0) } }

After you turning islands into an @State

Upvotes: 3

Fogmeister
Fogmeister

Reputation: 77631

Yes, this is very straight forward with SwiftUI.

Updating your code like this...

struct SingleIsland: Identifiable {
    let id: Int
    let name:String
}

struct IslandListView: View {
    @State private var islands = [
        SingleIsland(id: 0, name: "Wangerooge"),
        SingleIsland(id: 1, name: "Spiekeroog"),
        SingleIsland(id: 2, name: "Langeoog")
    ]

    var body: some View {
        List {
            ForEach(islands.identified(by: \.name)) { island in
                Text(island.name)   
            }.onDelete(perform: delete)
        }
    }

    func delete(at offsets: IndexSet) {
        islands.remove(at: offsets)
    }
}

This will allow your view to swipe to delete rows.

Using @State sets up your view to depend on the islands array. Any update to that array will trigger the view to reload. So by deleting an item from the array it will animate the change to the list.

Upvotes: 5

mimo
mimo

Reputation: 871

you can not do that with a static list.

in the real world your list of islands would probably come from the outside of your view anyway.

we use your struct:

[...]
struct singleIsland: Identifiable {
    var id: Int
    var name:String
}
[...]

and create an bindable Object to hold those islands

[...]
class IslandStore : BindableObject {

   let didChange = PassthroughSubject<IslandStore, Never>()

   var islands : [singleIsland] {
       didSet { didChange.send(self) }
   }

   init (islands: [singleIsland] = []){
       self.islands = islands
   } 
}
[...]

you need to import combine to use BindableObject

[...]
import SwiftUI
import Combine
[...]

your view now binds the island store the .onDelete(perform: delete) automatically adds the swipe left to delete function. We have to code the delete function tho:

[...]
struct ForTesting : View {
    @ObjectBinding var store = IslandStore()
    var body: some View {
        List {

            ForEach(store.islands) { island in
                Text(island.name)
                }.onDelete(perform: delete)

        }
    }

    func delete(at offsets: IndexSet) {

        // theres seems to be a bug that prevents us from using atOffsets
        // so we convert to index
        guard let index = Array(offsets).first else { return }

        store.islands.remove(at: index)
    }
}
[...]

and while we are at it we add a EditButton() and a title. We have to wrap our list in a NavigationView to do this

[...]
struct ForTesting : View {
    @ObjectBinding var store = IslandStore()
    var body: some View {
        NavigationView {
            List {

                ForEach(store.islands) { island in
                    Text(island.name)
                    }.onDelete(perform: delete)
                }
                .navigationBarTitle(Text("Islands"))
                .navigationBarItems(trailing: EditButton())

        }
    }

    func delete(at offsets: IndexSet) {

        // theres seems to be a bug that prevents us from using atOffsets
        // so we convert to index
        guard let index = Array(offsets).first else { return }

        store.islands.remove(at: index)
    }
}
[...]

change your DEBUG section to initialise the islands store and hand it over to your view:

#if DEBUG

var islands = [
    singleIsland(id: 0, name: "Wangerooge"),
    singleIsland(id: 1, name: "Spiekeroog"),
    singleIsland(id: 2, name: "Langeoog"),
    singleIsland(id: 3, name: "Baltrum")
]

struct ForTesting_Previews : PreviewProvider {
    static var previews: some View {
        ForTesting(store: IslandStore(islands:islands))
    }
}

#endif

viewwithdelete

complete code

Upvotes: 2

Matteo Pacini
Matteo Pacini

Reputation: 22846

struct SingleIsland {
    let name: String
}

struct ContentView: View {

    @State var islands = [
        SingleIsland(name: "Wangerooge"),
        SingleIsland(name: "Spiekeroog"),
        SingleIsland(name: "Langeoog")
    ]

    var body: some View {
        List {
            ForEach(islands.identified(by: \.name)) { island in
                Text(island.name)
            }.onDelete(perform: delete)
        }
    }

    private func delete(with indexSet: IndexSet) {
        indexSet.forEach { islands.remove(at: $0) }
    }
}

Wrapping the data in a @State makes sure the view is redrawn if it changes.


Note:

I'm getting compilers errors if the List is built this way:

List(data) { item in
 [...]
}

It will complain that onDelete does not exist for the List.

My workaround is to use a ForEach inside the List, and the onDelete function on it.

Upvotes: 10

Related Questions