Swiftui Navigation: How to Switch Detail View to a Different Item

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

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() })
}
}

Navigating to detail view on button click in List Row(TableviewCell) in SwiftUI

  1. Use the .hidden() modifier on the navigationLink to prevent the list item from capturing it's action

  2. Change the .buttonStyle from automatic to something else on the list item to prevent it from capturing the button action. (for example borderless)

Working Demo

struct ContentView: View {
@State var selectedTag: Int?

var body: some View {
NavigationView {
List(1...10, id: \.self) { id in
HStack {
Text("Item \(id: id)")
Spacer()

Button("Show detail") { selectedTag = id }
.background(link(id))
}.buttonStyle(.borderless) /// ⚠️ on the item! **NOT** on the button!!!
}
}
}

func link(id: Int) -> some View {
NavigationLink("",
destination: Text("\(id) Selected"),
tag: id,
selection: $selectedTag
).hidden()
}
}

Using same View for different data via NavigationLink - SwiftUI

A way to have only one view that can reload is to have a dynamic way to define its contents. One might use an enum to save the state of the survey :

class ListModel: ObservableObject {
// The enum is the list of tests
enum Choosing {
case male
case female
case color
// Define each test title
var title: String {
switch self {
case .male:
return "Male Names"
case .female:
return "Female Names"
case .color:
return "Color Names"
}
}
// define each test possible values
var items: [String] {
switch self {
case .male:
return ["Todd", "Liam", "Noah", "Oliver", "James", "William"]
case .female:
return ["Jessica", "Monica", "Stephanie"]
case .color:
return ["Pink", "Blue", "Yellow", "Green"]
}
}
// choosing next test
var next: Choosing? {
switch self {
case .male:
return .female
case .female:
return .color
case .color:
return nil
}
}
}

@Published var choosedItems: [Choosing:[String]] = [.male:[], .female:[], .color:[]]
}

struct StartView: View {
@StateObject var listModel = ListModel()

var body: some View {
NavigationView{
NavigationLink(destination: {
// Just give model and first test
OwnListView(listModel: listModel,
choosing: .male)
}, label: {
Text("Start")
.bold()
})
}
}
}

The common view :

struct OwnListView: View {
//ListModel
@ObservedObject var listModel: ListModel

var choosing: ListModel.Choosing
// Use enum var to get title and items
var title: String {
choosing.title
}
var items: [String] {
choosing.items
}

var body: some View {
VStack{
Text(title)
.font(.largeTitle)
.bold()

ForEach(choosing.items, id: \.self){ item in
// Use the current test result
let alreadyInList: Bool = listModel.choosedItems[choosing]?.contains(where: { $0 == item }) ?? false

Button(action: {
if alreadyInList {
listModel.choosedItems[choosing]?.removeAll(where: { $0 == item })
} else {
listModel.choosedItems[choosing]?.append(item)
}
}, label: {
//Can be an own View, but for simplicity
ZStack{
Rectangle()
.fill(alreadyInList ? .black : .purple)
.frame(width: 250, height: 50)

Text(item)
.bold()
.foregroundColor(.white)
}
})
}

Spacer()

// if still somthing test next
if let next = choosing.next {
NavigationLink(destination: {
OwnListView(listModel: listModel,
choosing: next)
}, label: {
Text("Continue")
})
} else {
// Here you can have a button to navigation link to go to end of survey
Text("Finish")
}

Spacer()

}.navigationBarTitleDisplayMode(.inline)
}
}

Note: The enum and title and values could comes from external json file to make it more generic. Here was just a way to do it.
To complete survey, just complete the enum definitions.

Swift: How to change DetailView depending on SideBar Selection State with SwiftUI 4?

It looks like they forgot (or got broken) ViewBuilder for detail: part. It worth submitting feedback to Apple.

A safe workaround is to wrap conditional block into some stack, like

    } detail: {
VStack { // << here !!
if let safeNavigationItem = navigationItem {
safeNavigationItem.rootView
} else {
Text(String(localized: "select.an.option", defaultValue: "Select an Option"))
}
}
}

Tested with Xcode 14b3 / iOS 16

Issue when rearranging List item in detail view using SwiftUI Navigation View and Sorted FetchRequest

UPDATE 2 (iOS 14 beta 3)

The issue seems to be fixed in iOS 14 beta 3: the Detail view does no longer pop when making changes that affect the sort order.


UPDATE

It seems Apple sees this as a feature, not a bug; today they replied to my feedback (FB7651251) about this issue as follows:

We would recommend using isActive and managing the push yourself using
the selection binding if this is the behavior you desire. As is this
is behaving correctly.

This is because the identity of the pushed view changes when you
change the sort order.


As mentioned in my comment above I believe this is a bug in iOS 13.4.

A workaround could be to use a NavigationLink outside of the List and define the List rows as Buttons that

a) set the task to be edited (a new @State var selectedTask) and

b) trigger the NavigationLink to TaskDetail(task: selectedTask!).

This setup will uncouple the selected task from its position in the sorted list thus avoiding the misbehaviour caused by the re-sort potentially caused by editing the dueDate.

To achieve this:

  1. add these two @State variables to struct ContentView
    @State private var selectedTask: Task?
@State private var linkIsActive = false

  1. update the body of struct ContentView as follows
    var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: linkDestination(selectedTask: selectedTask),
isActive: self.$linkIsActive) {
EmptyView()
}
List(tasks) { task in
Button(action: {
self.selectedTask = task
self.linkIsActive = true
}) {
NavigationLink(destination: EmptyView()){
Text("\(task.title)")
}
}
}
}
.navigationBarTitle("Tasks").navigationBarItems(trailing: Button("new") {self.addTask()})
}
}

  1. add the following struct to ContentView.swift
    struct linkDestination: View {
let selectedTask: Task?
var body: some View {
return Group {
if selectedTask != nil {
TaskDetail(task: selectedTask!)
} else {
EmptyView()
}
}
}
}

SwiftUI auto navigates to detail view after saving and dismissing the view

The solution for this is to make the navigation be based on a binding state.

NavigationLink(
destination: ExchangeItemSelectedView(exchange: observer),
tag: exchange.id,
selection: $exchangeSelection
) {
Text("Tap Me")
}

then rather than using @State to store exchangeSelection use @SceneStorage this will let you access the binding from anywhere within your app, in the code that creates the new item it should then dispatch async to update the selection value to the new item ID.

Detail View from different Data Model (API response) in swiftUI

How about something like this?

struct TrackDetail: View{

var track: TrackResponse
@State var details: TrackDetails?

var body: some View{
HStack{
//Detail implementation
}.onAppear{
Task{
do{
try await doStuff()
}catch{
//Alert
}
}
}
}

func doStuff() async throws{
// pull Details from Api

}
}

SwiftUI: NavigationView detail view pops backstack when previous's view List changes

Resort changes order of IDs making list recreate content that leads to current NavigationLinks destroying, so navigating back.

A possible solution is to separate link from content - it can be done with introducing something like selection (tapped row) and one navigation link activated with that selection.

Tested with Xcode 14 / iOS 16

demo

@State private var selectedItem: IdAndScoreItem?  // selection !!

var isNavigate: Binding<Bool> { // link activator !!
Binding(get: { selectedItem != nil}, set: { _ in selectedItem = nil })
}

var body: some View {
List(items, id: \.id) { item in
Text("id: \(item.id) score:\(item.score)") // tappable row
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
selectedItem = item
}
}
.background(
NavigationLink(isActive: isNavigate) { // one link !!
ScoreClickerView {
if let item = selectedItem {
addScoreAndSort(item: item)
}
}
} label: {
EmptyView()
}
)
}


Related Topics



Leave a reply



Submit