SwiftUI NavigationLink breaks after removing then inserting new ForEach element

For some reason, my NavigationLink is breaking in a specific circumstance:

Given the code below, here's the steps to reproduce:

  • Tap Sign In, which inserts an account into the list
  • Hit back to pop the stack
  • Swipe left and Delete, which removes the first element of the list
  • Tap Sign In again (should push onto the stack but does not)
  • Tap the first row (should push onto the stack but does not)

Here's the code:

import SwiftUI

class Account: ObservableObject, Identifiable, Equatable, Hashable {
    let id: String
    
    init(id: String) {
        self.id = id
    }
    
    static func == (lhs: Account, rhs: Account) -> Bool {
        return lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

class AccountManager: ObservableObject {
    @Published private (set) var isLoading: Bool = false
    @Published private (set) var accounts: [Account] = []
    
    init() {
        load()
    }
    
    func load() {
        isLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
            self.accounts = [ Account(id: UUID().uuidString) ]
            self.isLoading = false
        }
    }
    
    func add(account: Account) {
        accounts.insert(account, at: 0)
    }
    
    func delete(at offsets: IndexSet) {
        accounts.remove(atOffsets: offsets)
    }
}

struct AccountManagerEnvironmentKey: EnvironmentKey {
    static var defaultValue: AccountManager = AccountManager()
}

extension EnvironmentValues {
    var accountManager: AccountManager {
        get { return self[AccountManagerEnvironmentKey.self] }
        set { self[AccountManagerEnvironmentKey.self] = newValue }
    }
}

struct ContentView: View {
    @Environment(\.accountManager) var accountManager
    
    @State var isLoading: Bool = false
    @State var accounts: [Account] = []
    @State var selectedAccount: Account? = nil
    
    var body: some View {
        NavigationView() {
            ZStack {
                List {
                    ForEach(accounts) { account in
                        NavigationLink(
                            destination: Text(account.id),
                            tag: account,
                            selection: $selectedAccount
                        ) {
                            Text(account.id)
                        }
                    }
                    .onDelete(perform: { offsets in
                        accountManager.delete(at: offsets)
                    })
                }
                if isLoading {
                    ProgressView("Loading...")
                }
            }
            .navigationBarTitle("Accounts", displayMode: .inline)
            .toolbar(content: {
                ToolbarItem(placement: .primaryAction) {
                    Button("Sign In") {
                        let newAccount = Account(id: UUID().uuidString)
                        accountManager.add(account: newAccount)
                        selectedAccount = newAccount
                    }
                }
            })
            .onReceive(accountManager.$isLoading) { value in
                isLoading = value
            }
            .onReceive(accountManager.$accounts) { value in
                accounts = value
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

If I change the button action to do this, it works:

                        accountManager.add(account: newAccount)
                        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
                            selectedAccount = newAccount
                        }

But that seems like a massive hack.



Read more here: https://stackoverflow.com/questions/66344894/swiftui-navigationlink-breaks-after-removing-then-inserting-new-foreach-element

Content Attribution

This content was originally published by Donald at Recent Questions - Stack Overflow, and is syndicated here via their RSS feed. You can read the original post over there.

%d bloggers like this: