Xcode 14 NavigationPath – two views pushed onto nav stack when multiple links in form section - swift

Create new SwiftUI project
Replace ContentView with the code below
Tap either link.
Both views are pushed onto the nav stack
class ContentViewModel: ObservableObject {
#Published var navigationPath = NavigationPath()
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
NavigationStack(path: $viewModel.navigationPath) {
Form {
Section {
VStack {
NavigationLink(value: 1) {
Text("Location")
}
NavigationLink(value: 2) {
Text("Category")
}
}
}
}.navigationDestination(for: Int.self) { route in
switch route {
case 1: Text("Location")
case 2: Text("Category")
default: Text("Unknown")
}
}.navigationTitle("Home")
}
}
}
What am I doing wrong? I would only expect the tapped item to be pushed into the stack.

Removing the VStack solves the problem

Related

Changing scroll position from another view not working

I am working on a project where I need to reset a TabView's root view controllers (NavigationViews with Lists inside) when the TabViews selected item changes. This is pretty simple in UIKit, however in SwiftUI it doesn't seem that easy.
Let's say I have the following code:
class AppState: ObservableObject {
let objectWillChange = PassthroughSubject<AppState, Never>()
#Published var theScrollPosition: Int64? {
didSet {
print("Did set scroll position")
objectWillChange.send(self)
}
}
#Published var selectedTab: Tabs
{
didSet {
print("Tab switched, switching back to root view")
selectedItemID = nil
selectedRow = nil
theScrollPosition = -1
}
}
#Published var selectedItemID: Int64? {
didSet {
objectWillChange.send(self)
}
}
#Published var selectedRow: Int64? {
didSet {
objectWillChange.send(self)
}
}
}
struct ContentView: View {
#EnvironmentObject var state: AppState
var body: some View {
TabView {
View1().id(Tabs.Tab1)
View2().id(Tabs.Tab2)
View3().id(Tabs.Tab3)
}
}
}
struct View1: View {
#EnvironmentObject var state: AppState
#ObservedObject var viewModel: ListViewModel()
var body: some View {
ScrollViewReader { proxy in
List(viewModel.items) { item in
Section {
NavigationLink(destination:ListItemDetailView(item), tag: item.id, selection: state.selectedItemID) {
ListItemView(item)
}
}
}.onChange(of: self.state.selectedItemID) { newValue in
print("Scrolling to top")
proxy.scrollTo(0)
}
}
}
There may be some typos in the code as this is not the actual production code however the flow is this:
Selection state of TabView and NavigationLink are stored in a global EnvironmentObject. When the TabView selection changes, View1 should scroll back up to the top.
However, the onChange method is never called.
Next time, provide something runnable or at least a start...
But here is a example where view 1 will always scroll to the top, you are missing something like onAppear()
And you don't need to have AppState.theScrollPosition as a published, instead change to the right tab and have that view read the position or tag in the model.
import SwiftUI
struct ContentView: View {
var view: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
Text("TOP").id("topID")
Divider()
Spacer()
.frame(height:1000)
Text("BOTTOM").id("bottomID")
}
}
.onAppear {
proxy.scrollTo("topID")
}
}
}
var view2: some View {
VStack {
Text("View 2")
}
}
var body: some View {
TabView {
view.tag(0)
.tabItem {
Text("View 1")
}
view2.tag(1)
.tabItem {
Text("View 2")
}
}
}
}
Thanks, for some reason I was totally missing onAppear.

Why does SwiftUI automatically disable all buttons when dismissing a sheet and then changing the NavigationPath?

This example consists of a simple NavigationStack leading to Subview. The latter displays SomeSheet which, when its button is tapped, dismisses itself and changes the NavigationPath in order to go back to ContentView.
The mechanism works perfectly. However, all the buttons in ContentView are automatically disabled (example in video). Why does this happen? Is it a SwiftUI bug?
enum Root: Hashable {
case subview
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationStack(path: $model.path) {
List {
Button("Some button") {} // FIXME: All ContentView buttons are disabled when SomeSheet's button is tapped
NavigationLink(value: Root.subview) {
Text("Go to Subview")
}
}
.navigationDestination(for: Root.self) { _ in Subview() }
}
.environmentObject(model)
}
}
extension ContentView {
#MainActor final class Model: ObservableObject {
#Published var path = NavigationPath()
}
}
struct Subview: View {
#State private var sheetIsPresented = false
var body: some View {
Button("Show sheet") { sheetIsPresented = true }
.sheet(isPresented: $sheetIsPresented) { SomeSheet() }
}
}
struct SomeSheet: View {
#EnvironmentObject var model: ContentView.Model
#Environment(\.dismiss) var dismiss
var body: some View {
Button("Dismiss and go back to ContentView") {
dismiss()
model.path.removeLast()
}
}
}
Tested with Xcode 14.0 (iOS 16).

SwiftUI macOS NavigationView - onChange(of: Bool) action tried to update multiple times per frame

I'm seeing onChange(of: Bool) action tried to update multiple times per frame warnings when clicking on NavigationLinks in the sidebar for a SwiftUI macOS App.
Here's what I currently have:
import SwiftUI
#main
struct BazbarApp: App {
#StateObject private var modelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
class ModelData: ObservableObject {
#Published var myLinks = [URL(string: "https://google.com")!, URL(string: "https://apple.com")!, URL(string: "https://amazon.com")!]
}
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
#State private var selected: URL?
var body: some View {
NavigationView {
List(selection: $selected) {
Section(header: Text("Bookmarks")) {
ForEach(modelData.myLinks, id: \.self) { url in
NavigationLink(destination: DetailView(selected: $selected) ) {
Text(url.absoluteString)
}
.tag(url)
}
}
}
.onDeleteCommand {
if let selected = selected {
modelData.myLinks.remove(at: modelData.myLinks.firstIndex(of: selected)!)
}
selected = nil
}
Text("Choose a link")
}
}
}
struct DetailView: View {
#Binding var selected: URL?
var body: some View {
if let selected = selected {
Text("Currently selected: \(selected)")
}
else {
Text("Choose a link")
}
}
}
When I alternate clicking on the second and third links in the sidebar, I eventually start seeing the aforementioned warnings in my console.
Here's a gif of what I'm referring to:
Interestingly, the warning does not appear when alternating clicks between the first and second link.
Does anyone know how to fix this?
I'm using macOS 12.2.1 & Xcode 13.2.1.
Thanks in advance
I think the issue is that both the List(selection:) and the NavigationLink are trying to update the state variable selected at once. A List(selection:) and a NavigationLink can both handle the task of navigation. The solution is to abandon one of them. You can use either to handle navigation.
Since List look good, I suggest sticking with that. The NavigationLink can then be removed. The second view under NavigationView is displayed on the right, so why not use DetailView(selected:) there. You already made the selected parameter a binding variable, so the view will update if that var changes.
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
#State private var selected: URL?
var body: some View {
NavigationView {
List(selection: $selected) {
Section(header: Text("Bookmarks")) {
ForEach(modelData.myLinks, id: \.self) { url in
Text(url.absoluteString)
.tag(url)
}
}
}
.onDeleteCommand {
if let selected = selected {
modelData.myLinks.remove(at: modelData.myLinks.firstIndex(of: selected)!)
}
selected = nil
}
DetailView(selected: $selected)
}
}
}
I can recreate this problem with the simplest example I can think of so my guess is it's an internal bug in NavigationView.
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink("A", destination: Text("A"))
NavigationLink("B", destination: Text("B"))
NavigationLink("C", destination: Text("C"))
}
}
}
}

Two UINavigationControllers after using NavigationLink in sheet

I have a modal sheet that is presented from my home view as such:
Button(action: {
...
}) {
...
}
.sheet(isPresented: ...) {
MySheetView()
}
In MySheetView, there is a NavigationView and a NavigationLink to push another view onto its view stack (while I'm on MySheetView screen and use the view inspector, there's only one UINavigationController associated with it which is what I expect).
However, as soon as I get to my next view that is presented from MySheetView using the NavigationLink, and I use the view hierarchy debugger, there are TWO UINavigationControllers on-top of each other. Note, this view does NOT have a NavigationView inside it, only MySheetView does.
Does anyone know what's going on here? I have a feeling this is causing some navigation bugs im experiencing. This can be easily reproduced in an example app with the same structure.
Ex:
// These are 3 separate SwiftUI files
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
Button(action: { self.isPresented = true }) {
Text("Press me")
}
.sheet(isPresented: $isPresented) {
ModalView()
}
}
}
}
struct ModalView: View {
var body: some View {
NavigationView {
NavigationLink(destination: FinalView()) {
Text("Go to final")
}
}
}
}
struct FinalView: View {
var body: some View {
Text("Hello, World!")
}
}
I don't observe the behaviour you described. Used Xcode 11.2. Probably you need to provide your code to find the reason.
Here is an example of using navigation views in main screen and sheet. (Note: removing navigation view in main screen does not affect one in sheet).
import SwiftUI
struct TestNavigationInSheet: View {
#State private var hasSheet = false
var body: some View {
NavigationView {
Button(action: {self.hasSheet = true }) {
Text("Show it")
}
.navigationBarTitle("Main")
.sheet(isPresented: $hasSheet) { self.sheetContent }
}
}
private var sheetContent: some View {
NavigationView {
VStack {
Text("Properties")
.navigationBarTitle("Sheet")
NavigationLink(destination: properties) {
Text("Go to Inspector")
}
}
}
}
private var properties: some View {
VStack {
Text("Inspector")
}
}
}
struct TestNavigationInSheet_Previews: PreviewProvider {
static var previews: some View {
TestNavigationInSheet()
}
}

How to trigger sheet on TabView click

How can I show a sheet when I click a tab in TabView? All the examples on the internet use a Button to trigger an update but I want to make the sheet appear when a user clicks one of the tabs in TabView.
I tried changing the boolean state variable in a tabbed view by adding .onAppear(), but it doesn't seem to work.
struct ContentView: View {
#State var showSheet: Bool = false
var body: some View {
return TabView {
HomeView()
.tabItem {
Image(systemName: "house")
}
}
.sheet(isPresented: self.$showSheet) {
SheetView(isShown: self.$showSheet)
}
}
}
In the above example, I basically want SheetView to show up when I click the tab. I don't want to replace HomeView with SheetView since I want it to be a sheet instead of static view. Thanks!
This can be achieved, albeit rather hackishly, by moving your State variables up one level and controlling the flow within a Group. Here, I just moved them to the app state for simplicity.
final class AppState: ObservableObject {
#Published var shouldShowActionSheet: Bool = true
#Published var selectedContentViewTab: ContentViewTabs = .none
}
In SceneDelegate.swift:
. . .
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(AppState()))
. . .
And finally in your view:
public enum ContentViewTabs: Hashable {
case none
case home
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
Group {
if (appState.selectedContentViewTab == .home && self.appState.shouldShowActionSheet) {
Text("").sheet(isPresented: self.$appState.shouldShowActionSheet, onDismiss: {
self.appState.shouldShowActionSheet.toggle()
self.appState.selectedContentViewTab = .none
}, content: {
Text("Oll Korrect, Chaps!")
})
} else {
TabView(selection: self.$appState.selectedContentViewTab) {
Text("First View")
.font(.title)
.tabItem {
VStack {
Image("first")
Text("First")
}
}.tag(ContentViewTabs.home)
}
}
}
}
}