SwiftUI - Nested NavigationLink - ios

I have a layout that looks like this:
Layout Drawing
There is a main view which is the Feed that would be my NavigationView and then I have views inside: PostList -> Post -> PostFooter and in the PostFooter A Button that would be my NavigationLink
struct Feed: View {
var body: some View {
NavigationView {
PostList()
}
}
}
struct PostList: View {
var body: some View {
List {
ForEach(....) {
Post()
}
}
}
}
struct Post: View {
var body: some View {
PostHeader()
Image()
PostFooter()
}
}
struct PostFooter: View {
var body: some View {
NavigationLink(destination: Comment()) {
Text("comments")
}
}
}
But When when I tap on the comments, it goes to the Comment View then go back to Feed() then back to Comment() then back to Feed() and have weird behaviour.
Is there a better way to handle such a situation ?
Update
The Navigation is now working but the all Post component is Tapeable instead of just the Text in the PostFooter.
Is there any way to disable tap gesture on the cell and add multiple NavigationLink in a cell that go to different pages ?

How about programmatically active the NavigationLink, for example:
struct PostFooter: View {
#State var commentActive: Bool = false
var body: some View {
VStack{
Button("Comments") {
commentActive = true
}
NavigationLink(destination: Comment(), isActive: $commentActive) {
EmptyView()
}
}
}
}
Another benefit of above is, your NavigationLink destination View can accept #ObservedObject or #Binding for comments editing.

Related

Unwind NavigationView to root when switching tabs in SwiftUI

I have an app with a few tabs, and on one of those there is a NavigationLink which nests a couple of times.
I want to be able to switch tabs, and when going back to the other tab to have unwound all links to the root view.
I have seen these: https://stackoverflow.com/a/67014642/1086990 and https://azamsharp.medium.com/unwinding-segues-in-swiftui-abdf241be269 but they seem to be focusing on unwinding when active on the view, not switching from it.
struct MyTabView: View {
var body: some View {
TabView {
TabOne().tabItem { Image(systemName: "1.square") }
TabTwo().tabItem { Image(systemName: "2.square") }
}
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
var body: some View {
NavigationView {
NavigationLink("Go to sub view") {
TabTwoSub()
}
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Maybe I've missed something fairly basic but nothing seems to come up from searches on unwinding views when switching tabs.
I tried using the NavigationLink(isActive: , destination: , label: ) from the other SO answer but couldn't get it working in the root MyTabView.
I thought about using UserDefaults to set a isActive bool state and if not try and unwind the navigation, but that didn't seem very swifty to do.
What is happening
You'll need to keep track of the tab selection in the parent view and then pass that into the child views so that they can watch for changes. Upon seeing a change in the selection, the child view can then reset a #State variable that change the isActive property of the NavigationLink.
class NavigationManager : ObservableObject {
#Published var activeTab = 0
}
struct MyTabView: View {
#StateObject private var navigationManager = NavigationManager()
var body: some View {
TabView(selection: $navigationManager.activeTab) {
TabOne().tabItem { Image(systemName: "1.square") }.tag(0)
TabTwo().tabItem { Image(systemName: "2.square") }.tag(1)
}.environmentObject(navigationManager)
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
#EnvironmentObject private var navigationManager : NavigationManager
#State private var linkActive = false
var body: some View {
NavigationView {
NavigationLink("Go to sub view", isActive: $linkActive) {
TabTwoSub()
}
}.onChange(of: navigationManager.activeTab) { newValue in
linkActive = false
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Note: this will result in a "Unbalanced calls to begin/end appearance transitions" message in the console -- in my experience, this is not an error and not something we have to worry about

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

So I am writing a todo list app in SwiftUI in order to get the hang of it, but I am facing a problem.
In my first view (list of items) I have a toolbar with an "add" button which uses a NavigationLink to navigate to the detail view. In the detail view I also have a toolbar button acting as a save button which dismisses this view and also adds the item to a list of items kept in the view model used by both views.
The problem is that if I save the item when tapping the save button it will first navigate back to the first view and then auto navigate to the second view again. If I instead use the built in back button this issue doesn't happen, but obviously I would like to save the item and only when pressing save. This also only happens if I add the item to the item list in the view model before dismissing the view, if I only dismiss the view without saving the item when pressing done then this bug doesn't happen.
Is this not a standard way of saving and closing a view with SwiftUI, or is there some sort of other pattern that is better? In any case I need to resolve this issue.
First view:
struct TodoListView: View {
#EnvironmentObject var viewModel: TodoListViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.listOfTodos) { todoItem in
ItemCellView(todoItem: todoItem)
}
}
.navigationTitle("Things to do")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: AddEditTodoView(todoItem: TodoListInfo.TodoItem())
) {
Text("Add item") // The navigation bug happens when using this button
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ItemCellView: View {
var todoItem: TodoListInfo.TodoItem
var body: some View {
HStack {
NavigationLink(destination: AddEditTodoView(todoItem: todoItem)) {
Text(todoItem.title) // The navigation bug doesn't happen when editing an existing item
}
}
.padding()
}
}
Second view:
struct AddEditTodoView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var viewModel: TodoListViewModel
#State var todoItem: TodoListInfo.TodoItem
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $todoItem.title)
}
}
.navigationTitle(Text("Edit task"))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
viewModel.upsert(item: todoItem) // No bug if I comment out this line
presentationMode.wrappedValue.dismiss()
}
.disabled(todoItem.title == "")
}
}
}
}
View model:
class TodoListViewModel: ObservableObject {
#Published private var todoListInfo: TodoListInfo
private var autoSaveCancellable: AnyCancellable? // even without the autoSaveCancellable part, the bug happens
init(testData: Bool = false) {
todoListInfo = TodoListInfo(testData: testData)
autoSaveCancellable = $todoListInfo.sink {
TodoListInfo.persistTodoList($0)
}
}
var listOfTodos: [TodoListInfo.TodoItem] {
todoListInfo.todos
}
func upsert(item: TodoListInfo.TodoItem) {
if let itemIndex = todoListInfo.todos.firstIndex(where: { $0.id == item.id }) {
todoListInfo.todos[itemIndex] = item
} else {
todoListInfo.todos.append(item) // This gets called when adding an item
}
}
}
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.

Can I navigate ParentView to another View by button in ChildView?, swiftUI

I'm new to SwiftUI and struggling to move MainView to LoginView by clicking Text() in ChildView.
I tried make NavigationView and NavigationLink like this code but it's not working. It's much more complex in real code but I show only simple structure of my code to explain.
struct ContentView: View {
#State private var currViewIdx: Int = 0
var body: some View {
NavigationView {
CurrentView(currViewIdx: $currViewIdx)
}
}
}
struct CurrentView: View {
#Binding var currViewIdx: Int
var body: some View {
if self.currViewIdx == 0 {
HomeView()
.onTapGesture {
self.currViewIdx = 0
}
} else {
//SomeView.onTapGesture(self.currViewIdx = 1)
}
}
}
struct HomeView: View {
var body: some View {
NavigationLink(destination: TestView()) {
Text("Go TestView")
.contentShape(Rectangle())
}
}
}
struct TestView: View {
var body: some View {
Text("Here is Test View")
}
}
I think, the top hierarchy view, ContentView, has NavigationView and it should be changed when I click Text("Go TestView") in HomeView because it's in NavagationLink.
But the ContentView is not changed to TestView although I touch Text("Go TestView"). How to solve this problem? I considered way to add one more State value to change top level view, but it seems not good if I'll make lots of more values
Thank you

How it is possible to dismiss a view from a subtracted subview in SwiftUI

Whenever my code gets too big, SwiftUI starts acting weird and generates an error:
"The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
So I started breaking up my code into Extracted Subviews, one of the problems I came across is how to dismiss a view from a subtracted subview.
Example: we have here LoginContentView this view contains a button when the button is clicked it will show the next view UsersOnlineView.
struct LoginContentView: View {
#State var showUsersOnlineView = false
var body: some View {
Button(action: {
self.showUsersOnlineView = true
}) {
Text("Show the next view")
}
.fullScreenCover(isPresented: $showUsersOnlineView, content: {
UsersOnlineView()
})
}
On the other hand, we have a button that is extracted to subview, to dismiss the modal and go back to the original view:
import SwiftUI
struct UsersOnlineView: View {
var body: some View {
ZStack {
VStack {
CloseViewButton()
}
}
}
}
struct CloseViewButton: View {
var body: some View {
Button(action: {
// Close the Modal
}) {
Text("Close the view")
}
}
}
Give the sbview the state property that defines if the view is shown.
struct CloseViewButton: View {
#Binding var showView: Bool
var body: some View {
Button(
ShowView = false
}) {
Text("Close the view")
}
}
}
When you use the sub view give it the property
CloseButtonView(showView: $showOnlineView)
To allow the sub view to change the isShown property it needs to get a binding.
On the presentation mode. I think this only works with Swiftui presentations like sheet and alert.
The simplest solution for this scenario is to use presentationMode environment variable:
struct CloseViewButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close the view")
}
}
}
Tested with Xcode 12.1 / iOS 14.1

Navigate to same view in SwiftUI

So I'm trying to navigate from one view to another in SwiftUI and have stumbled upon a problem.
My views looks like this and trying to navigation from one DetailView to another DetailView but as soon I push the navigation link I'm forced back to the first DetailView.
Any ideas on how to achieve navigation to the same view in SwiftUI?
ListView.swift
struct ListView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView(data: data)) {
Text("Navigate")
}
}
}
}
}
DetailView.swift
struct DetailView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView(data: otherData)) {
Text("Navigate")
}
}
}
}
EDIT:
Remove the NavigationView in DetailView as pointed out below but still facing the same issue.
Remove NavigationView from DetailView, there should be only one in stack, so when you navigate all new links are opened in original NavigationView:
struct DetailView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView(data: otherData)) {
Text("Navigate")
}
}
}
}

Resources