SwiftUI List Not Updating Correctly with ForEach

I am attempting to add a favorites feature to my app using a List( ) with sections. I have two arrays stored in an @EnvironmentObject which are iterated through to populate the favorites and other sections. When the arrays are modified, the content (button) moves as expected, but the action and formating does not change as expected.


The screenshots below demonstrate the issue:


1. Opening screen

2. Clicking on the heart icon moves the item, but the color is not updated

3. Clicking on the heart icon once it has moved behaves correctly, but adds the item to the favorites list a second time


The expected behavior would be that the item moves and the action and format are updated on the first press. Here is the code for the page I am using:


import SwiftUI


struct ContentView: View {
   
    @EnvironmentObject var state: ApplicationState
   
    var body: some View {
       
        VStack{
            List {
                Section(header: Text("Favorites")) {
                   
                    ForEach(state.favorites, id: \.self) { favorite in


                        HStack{
                           
                            Button(
                                action: {
                                    self.state.others.append(favorite)
                                   
                                    self.state.favorites = self.state.favorites.filter {$0 != favorite}
                                   
                                    self.sortItems()
                            },
                                label: {Image(systemName: "heart.fill").foregroundColor(.red)}
                            )
                                .id(favorite.id)
                           
                            Text(favorite.name)
                           
                        }
                    }
                }
               
                Section(header: Text("Others")) {
                   
                    ForEach(state.others, id: \.self) { other in
                       
                        HStack{
                           
                            Button(
                                action: {
                                    self.state.favorites.append(other)
                                   
                                    self.state.others = self.state.others.filter {$0 != other}
                                   
                                    self.sortItems()
                            },
                                label: {Image(systemName: "heart")}
                            )
                                .id(other.id)
                           
                           
                            Text(other.name)
                           
                        }
                    }
                }
            }
        }.onAppear(){
           
            self.populateState()
        }
    }
   
    func sortItems(){


        self.state.favorites.sort {
            $0.name < $1.name
        }


        self.state.others.sort {
            $0.name < $1.name
        }
    }
   
    func populateState(){
       
        state.others.append(ListItem(id: 1, name: "One"))
       
        state.others.append(ListItem(id: 2, name: "Two"))
       
        state.others.append(ListItem(id: 3, name: "Three"))
       
        state.others.append(ListItem(id: 4, name: "Four"))
       
        state.others.append(ListItem(id: 5, name: "Five"))
       
    }
}


A full minimal example is here: https://github.com/benbaran/swift-ui-favorites-list


Does anyone see anything I am doing wrong? Perhaps this is a bug?


Thanks!

Accepted Reply

Seems SwiftUI tries to update views as `move` when an item of exactly the same id is moved inside the data source of a List.

You may call it a bug, or one of the current limitations of SwiftUI.

Anyway, you can send a bug report using Apple's bug reporter.


A hacky workaround. Define different ids for favorites and others:

extension ListItem {
    var favoriteId: String {
        "favorite\(id)"
    }
    var otherId: String {
        "other\(id)"
    }
}

And use each id for each section:

    var body: some View {
        VStack{
            List {
                Section(header: Text("Favorites")) {
                    ForEach(state.favorites, id: \.favoriteId) { favorite in
                        //...
                    }
                }
                Section(header: Text("Others")) {
                    ForEach(state.others, id: \.otherId) { other in
                        //...
                    }
                }
            }
        }.onAppear(){
            self.populateState()
        }
    }


By the way, we cannot see your screenshots below, this site might have removed it.

Replies

Seems SwiftUI tries to update views as `move` when an item of exactly the same id is moved inside the data source of a List.

You may call it a bug, or one of the current limitations of SwiftUI.

Anyway, you can send a bug report using Apple's bug reporter.


A hacky workaround. Define different ids for favorites and others:

extension ListItem {
    var favoriteId: String {
        "favorite\(id)"
    }
    var otherId: String {
        "other\(id)"
    }
}

And use each id for each section:

    var body: some View {
        VStack{
            List {
                Section(header: Text("Favorites")) {
                    ForEach(state.favorites, id: \.favoriteId) { favorite in
                        //...
                    }
                }
                Section(header: Text("Others")) {
                    ForEach(state.others, id: \.otherId) { other in
                        //...
                    }
                }
            }
        }.onAppear(){
            self.populateState()
        }
    }


By the way, we cannot see your screenshots below, this site might have removed it.

Thanks for the answer. I was stuck on this for hours.