josip04
josip04

Reputation: 234

SwiftUI List messed up after delete action on iOS 15

It seems that there is a problem in SwiftUI with List and deleting items. The items in the list and data get out of sync.

This is the code sample that reproduces the problem:

import SwiftUI

struct ContentView: View {
    @State var popupShown = false

    var body: some View {
        VStack {
            Button("Show list") { popupShown.toggle() }
            if popupShown {
                MainListView()
            }
        }
        .animation(.easeInOut, value: popupShown)
    }
}

struct MainListView: View {
    @State var texts = (0...10).map(String.init)

    func delete(at positions: IndexSet) {
        positions.forEach { texts.remove(at: $0) }
    }

    var body: some View {
        List {
            ForEach(texts, id: \.self) { Text($0) }
                .onDelete { delete(at: $0) }
        }
        .frame(width: 300, height: 300)
    }
}

If you perform a delete action on the first row and scroll to the last row, the data and list contents are not in sync anymore.

This is only happening when animation is attached to it. Removing .animation(.easeInOut, value: popupShown) workarounds the issue.

This code sample works as expected on iOS 14 and doesn't work on iOS 15.

Is there a workaround for this problem other then removing animation?

Upvotes: 0

Views: 351

Answers (1)

Yrb
Yrb

Reputation: 9695

It isn't the animation(). The clue was seeing It appears that having the .animation outside of the conditional causes the problem. Moving it to the view itself corrected it to some extent. However, there is a problem with this ForEach construct: ForEach(texts, id: \.self). As soon as you start deleting elements of your array, the UI gets confused as to what to show where. You should ALWAYS use an Identifiable element in a ForEach. See the example code below:

struct ListDeleteView: View {
    @State var popupShown = false

    var body: some View {
        VStack {
            Button("Show list") { popupShown.toggle() }
            if popupShown {
                MainListView()
                    .animation(.easeInOut, value: popupShown)
            }
        }
    }
}

struct MainListView: View {
    @State var texts = (0...10).map({ TextMessage(message: $0.description) })

    func delete(at positions: IndexSet) {
        texts.remove(atOffsets: positions)
    }


    var body: some View {
        List {
            ForEach(texts) { Text($0.message) }
                .onDelete { delete(at: $0) }
        }
        .frame(width: 300, height: 300)
    }
}

struct TextMessage: Identifiable {
    let id = UUID()
    let message: String
}

Upvotes: 1

Related Questions