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
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
Use the
.hidden()
modifier on thenavigationLink
to prevent the list item from capturing it's actionChange the
.buttonStyle
fromautomatic
to something else on the list item to prevent it from capturing the button action. (for exampleborderless
)
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:
- add these two @State variables to struct ContentView
@State private var selectedTask: Task?
@State private var linkIsActive = false
- 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()})
}
}
- 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
@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
How to Add a Show More/Show Less Uibutton to Control Uitextview
Adding Animation to Tabviews in Swiftui When Switching Between Tabs
Custom Mkannotation Not Moving When Coordinate Set
Insert a Comment Using the Youtube API and Alamofire
Argument of '#Selector' Does Not Refer to an '@Objc' Method, Property or Initializer
How to Decode the Body of an Error in Alamofire 5
Skshapenode Is Not Responding to Physicsbody
Avcapturevideopreviewlayer Is Not Visible on The Screenshot
Saving Coredata to a Web Server with Swift 3.0
Why Use an Extra Let Statement Here
Core Image Filter Cisourceovercompositing Not Appearing as Expected with Alpha Overlay
Swift Compiler Error Int Is Not Convertible to Cgfloat
Zposition of Sknode Relative to Its Parent
Ios: Ambiguous Use of Init(Cgimage)