TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Bindings in a List Causing Crashes

Forums > SwiftUI

I'm running into a problem I can't explain where modifiying elements of a list after deletions leads to a crash. First here's an example which doesn't crash to illustrate the setup:

import SwiftUI

struct TestItem: Identifiable {

    let id = UUID()

    var name = ""
    var quantity = 1

    init(_ name: String = "") {
        self.name = name
    }
}

class TestList: ObservableObject {

    var id: UUID = UUID()
    @Published var items: [TestItem]

    init() {
        self.items = [
            TestItem("Apples"),
            TestItem("Oranges"),
            TestItem("Bananas"),
        ]
    }
}

struct TestingView: View {

    @StateObject var list = TestList()

    var body: some View {
        List() {
            ForEach($list.items, editActions: .all) { $item in
                TestingDetailView(item: $item)
            }
        }
    }
}

struct TestingDetailView: View {

    @Binding var item: TestItem

    var body: some View {
        Button(action: {item.quantity += 1}) {
            HStack {
                Text(item.name)                
                Spacer()
                Text("\(item.quantity)")
            }
        }
    }
}

Here, tapping on a row causes the corresponding quantity to increase, and in particular this continue to work without any crashes even after deleting some elements of the list. Now, suppose that instead of using a Button to trigger this change, I instead trigger the change when the user interacts with a TextField using onEditingChanged:

struct TestingDetailView2: View {

    @Binding var item: TestItem

    @State private var input = ""

    func increaseQuantity(editing: Bool) {
        if !editing {
            item.quantity += 1
        }
    }

    var body: some View {
        HStack {
            Text(item.name)
            TextField("Type anything...", text: $input, onEditingChanged: increaseQuantity)
            Spacer()
            Text("\(item.quantity)")
        }
    }
}

At first glance all seems fine, but if I first delete the second entry ('Oranges') of the list and then submit something to the remaining 'Bananas' text field, the app crashes consistently. Naturally this is a simplified example, and there are some workarounds here such as using .onSubmit instead. However I'm keen to use onEditingChanged in the app I'm working on, so if anyone can explain what's going on here I'd be hugely grateful!

I'm running Xcode 14.3.1.

Thanks in advance!

2      

hi,

my guess is that when SwiftUI first instantiates the bananas view and puts it on screen, the TextField closure that references the bananas item (probably by index 2 because of the binding) is being captured ... it is escaping, after all. later, when you delete oranges, SwiftUI re-instantiates the bananas view struct (now at index 1), but is still hanging on to the original closure (which referenced the item at index 2).

that would explain the index out of range error you're getting.

in contrast, an .onSubmit modifier is code that's executed in real time, not code that was previously captured and kept around for later use. so that works fine.

you might look at using an .onChange(of: input) modifier attached to the TextField ... i did not test, however.

hope that's sort of on target for you,

DMG

2      

Try replacing the ForEach with this. (I'm still on MacOS 12 so I cannot test it.)

ForEach($list.items, editActions: .all) { element in
    if let idx = list.items.firstIndex(where: {$0.id==element.id}), idx < list.items.count {
        TestingDetailView(item: element)
    }
}

2      

Thank you both for the insightful replies, with your help I've managed to fix the issue!

2      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

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.