1

On SwiftUI with macOS and iPadOS I'm getting a crash, when I make the number of NavigationLink items variable and then try removing the last item. Deleting any other but the last item is fine. On crash I'm getting a log print:

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444

It seems like the destination of the NavigationLink is kept alive longer than the NavigationLink itself, which for a brief moment gives the the destination an invalid binding. This crash only manifests, if the destination needs a Binding (e.g. to be able to edit some content). If I use a Text View as destination instead of a TextField I don't get a crash.

Here's a minimal example to reproduce:

import SwiftUI

struct ContentView: View {
    @State var strings = ["Hello World 1", "Hello World 2", "Hello World 3"]
    @State var selectedStringIndex: Int?
    
    var body: some View {
        NavigationView {
            VStack {
                Button("Remove Selected") { // when removing the last element => crash
                    if let selectedStringIndex = selectedStringIndex {
                        strings.remove(at: selectedStringIndex)
                    }
                }
                
                List(strings.indices, id: \.self, selection: $selectedStringIndex) { stringIndex in
                    NavigationLink(destination: TextField("Name", text: $strings[stringIndex]),
                                   tag: stringIndex, selection: $selectedStringIndex) {
                        Text(strings[stringIndex])
                    }
                }
            }
        }
    }
}

What's the recommended way of handling dynamic number of NavigationLink items?

I'm using Xcode 12.2 beta 4 on macOS 11.0.1 Beta

John Henryk
  • 101
  • 5

1 Answers1

1

A possible solution is to use a custom Binding and check if the index is in range:

func binding(for index: Int) -> Binding<String> {
    .init(get: {
        guard strings.indices.contains(index) else { return "" } // check if `index` is valid
        return strings[index]
    }, set: {
        strings[index] = $0
    })
}

Then you can use this binding in the NavigationLink:

NavigationLink(destination: TextField("Name", text: binding(for: stringIndex)), ...)

Also make sure you reset selectedStringIndex after deletion:

Button("Remove Selected") {
    if let selectedStringIndex = selectedStringIndex {
        strings.remove(at: selectedStringIndex)
        self.selectedStringIndex = nil // reset here
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209