G. Marc
G. Marc

Reputation: 5997

SwiftUI Navigation: How to switch detail view to a different item?

I'm struggling implementing the following navigation behavior: From a list the user can select an item which triggers a detail view for this item. On this detail view there is an "Add" button in the navigation bar which opens a modal sheet for adding an other item.

Up to this point, everything works as expected.

But after adding the item, I want the detail view to show the new item. I tried to set the list selection to the id of the new item. This triggers the detail view to disappear, the list selects the new item and show the details for a very short time, then the detail view disappears again and the list is shown.

I've tried adding a bridged binding and let the list view not set the selection to nil, this solves the issue at first, but then the "Back" button isn't working anymore.

Please note: I want the "Add" button on the detail view and not on the list view as you would expect it.

Here's the full code to test:

import Combine
import SwiftUI

struct ContentView: View {
    @ObservedObject private var state = AppState.shared
    
    var body: some View {
        NavigationView {
            List(state.items) {item in
                NavigationLink(destination: DetailView(item: item), tag: item.id, selection: self.$state.selectedId) {
                    Text(item.title)
                }
            }
            .navigationBarTitle("Items")
        }
    }
}

struct DetailView: View {
    var item: Item
    @State private var showForm = false

    var body: some View {
        Text(item.title)
            .navigationBarItems(trailing: Button("Add") {
                self.showForm = true
            })
            .sheet(isPresented: $showForm, content: { FormView() })
    }
}

struct FormView: View {
    @Environment(\.presentationMode) private var presentationMode
    private var state = AppState.shared

    var body: some View {
        Button("Add") {
            let id = self.state.items.count + 1
            self.state.items.append(Item(id: id, title: "Item \(id)"))
            self.presentationMode.wrappedValue.dismiss()
            
            self.state.selectedId = id
        }
    }
}

class AppState: ObservableObject {
    static var shared = AppState()

    @Published var items: [Item] = [Item(id: 1, title: "Item 1")]
    @Published var selectedId: Int?

}

struct Item: Identifiable {
    var id: Int
    var title: String
}

Upvotes: 3

Views: 889

Answers (1)

Asperi
Asperi

Reputation: 257711

In your scenario it is needed to make navigation link destination independent, so it want be reactivated/invalidated when destination changed.

Here is possible approach. Tested with Xcode 11.7 / iOS 13.7

demo

Updated code only:

struct ContentView: View {
    @ObservedObject private var state = AppState.shared
    @State private var isActive = false

    var body: some View {
        NavigationView {
            List(state.items) {item in
                HStack {
                    Button(item.title) {
                        self.state.selectedId = item.id
                        self.isActive = true
                    }
                    Spacer()
                    Image(systemName: "chevron.right").opacity(0.5)
                }
            }
            .navigationBarTitle("Items")
            .background(NavigationLink(destination: DetailView(), isActive: $isActive) { EmptyView() })
        }
    }
}

struct DetailView: View {
    @ObservedObject private var state = AppState.shared
    @State private var showForm = false

    @State private var fix = UUID()    // << fix for known issue with bar button misaligned after sheet
    var body: some View {
        Text(state.selectedId != nil ? state.items[state.selectedId! - 1].title : "")
            .navigationBarItems(trailing: Button("Add") {
                self.showForm = true
            }.id(fix))
            .sheet(isPresented: $showForm, onDismiss: { self.fix = UUID() }, content: { FormView() })
    }
}

Upvotes: 2

Related Questions