A navigationDestination for “app.Type" was declared earlier on the stack [duplicate] - swift

I am trying to integrate NavigationStack in my SwiftUI app.
I have four views: CealUIApp, OnBoardingView, UserTypeView and RegisterView.
I want to navigate from OnBoardingView to UserTypeView when user presses a button in OnBoardingView.
And, navigate from UserTypeView to RegisterView when user presses a button in UserTypeView
Below is my code for CealUIApp
#main
struct CealUIApp: App {
#State private var path = [String]()
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
OnBoardingView(path: $path)
}
}
}
}
In OnBoardingView
Button {
path.append("UserTypeView")
}
label: {
Text("Hello")
}
.navigationDestination(for: String.self) { string in
UserTypeView(path: $path)
}
In UserTypeView
Button {
path.append("RegisterView")
}
label: {
Text("Hello")
}
.navigationDestination(for: String.self) { string in
RegisterView()
}
When the button on UserTypeView is pressed, it navigates to UserTypeView instead of RegisterView.
Also, the Xcode logs saying Only root-level navigation destinations are effective for a navigation stack with a homogeneous path.

You can get rid of Only root-level navigation destinations are effective for a navigation stack with a homogeneous path by changing the path type to NavigationPath.
#State private var path: NavigationPath = .init()
But then you get a message/error that I think explains the issue better A navigationDestination for “Swift.String” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used.
Apple has decided that scanning all views that are available is very inefficient so they will use the navigationDestination will take priority.
Just imagine if your OnBoardingView also had an option for "RegisterView"
.navigationDestination(for: String.self) { string in
switch string{
case "UserTypeView":
UserTypeView(path: $path)
case "RegisterView":
Text("fakie register view")
default:
Text("No view has been set for \(string)")
}
}
How would SwiftUI pick the right one?
So how to "fix"? You can try this alternative.
import SwiftUI
#available(iOS 16.0, *)
struct CealUIApp: View {
#State private var path: NavigationPath = .init()
var body: some View {
NavigationStack(path: $path){
OnBoardingView(path: $path)
.navigationDestination(for: ViewOptions.self) { option in
option.view($path)
}
}
}
//Create an `enum` so you can define your options
enum ViewOptions{
case userTypeView
case register
//Assign each case with a `View`
#ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View{
switch self{
case .userTypeView:
UserTypeView(path: path)
case .register:
RegisterView()
}
}
}
}
#available(iOS 16.0, *)
struct OnBoardingView: View {
#Binding var path: NavigationPath
var body: some View {
Button {
//Append to the path the enum value
path.append(CealUIApp.ViewOptions.userTypeView)
} label: {
Text("Hello")
}
}
}
#available(iOS 16.0, *)
struct UserTypeView: View {
#Binding var path: NavigationPath
var body: some View {
Button {
//Append to the path the enum value
path.append(CealUIApp.ViewOptions.register)
} label: {
Text("Hello")
}
}
}
#available(iOS 16.0, *)
struct RegisterView: View {
var body: some View {
Text("Register")
}
}
#available(iOS 16.0, *)
struct CealUIApp_Previews: PreviewProvider {
static var previews: some View {
CealUIApp()
}
}

As an alternative to #lorem ipsum's answer, I'd suggest using NavigationLink instead of a Button as that will handle adding the values internal NavigationPath for NavigationStack. I would only add and pass around your own path if you wanted to do navigation programatically (for example after a network request).
First we have an enum to handle the possible routes and creation of their views:
enum Route {
case register
case userType
#ViewBuilder
var view: some View {
switch self {
case .register:
RegisterView()
case .userType:
UserTypeView()
}
}
}
Then we have the main app:
#main
struct CealUIApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
OnboardingView()
.navigationDestination(for: Route.self) { route in
route.view
}
}
}
}
}
And finally the implementation of those views with the various NavigationLink in place:
struct OnboardingView: View {
var body: some View {
NavigationLink("Hello", value: Route.userType)
}
}
struct UserTypeView: View {
var body: some View {
NavigationLink("Hello", value: Route.register)
}
}
struct RegisterView: View {
var body: some View {
Text("Register View")
}
}

Following #lorem ipsum example I think you can change this state variable #State private var path: NavigationPath = .init() with an #ObservableObject so you don't need to pass #Bindings on all the views. You just pass it down from the CealUIApp view as an WnvironmentObject
class NavigationStack: ObservableObject {
#Published var paths: NavigationPath = .init()
}
#main
struct CealUIApp: App {
let navstack = NavigationStack()
var body: some Scene {
WindowGroup {
AppEntry()
.environmentObject(navstack)
}
}
}
extension CealUIApp {
enum ScreenDestinations {
case userTypeView
case registerView
//Assign each case with a `View`
#ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View {
switch self{
case .permissions:
UserTypeView()
case .seedPhrase:
RegisterView()
}
}
}
}
// An extra view after the AppView
struct AppEntry: View {
#EnvironmentObject var navStack: NavigationStack
var body: some View {
NavigationStack(path: $navStack.paths) {
OnBoardingView()
.navigationDestination(for: CealUIApp.ScreenDestinations.self) {
$0.view($navStack.paths)
}
}
}
}
And then the rest remain the same as #lorem ipsum said.

Related

Show new view and completely destroy old view when clicking on Text

So I have been wanting to do this for some time, but I can't figure out how to approach this, so I'm reaching out to see if someone might be able to help me.
So let's say that I have the following code, which when the app loads, loads the "MainView":
struct MapGlider: App {
#ObservedObject var mainViewModel = MainViewModel()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(mainViewModel)
}
}
}
This loads the map as soon as the app is opened, which is great! All works great there.
Now I will be switching that out to show the OnboardingView() when the app loads such as:
struct MapGlider: App {
#ObservedObject var mainViewModel = MainViewModel()
var body: some Scene {
WindowGroup {
OnboardingView()
}
}
}
Now, I have a OnboardingView that shows a ZStack with some options, as show in this code below:
struct OnboardingView: View {
#State private var showGetStartedSheet = false
#ObservedObject var mainViewModel = MainViewModel()
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
ZStack(alignment: .top) {
VStack {
LazyVGrid(columns: [GridItem(), GridItem(), GridItem(alignment: .topTrailing)], content: {
Spacer()
Image("onboarding-logo")
.border(.red)
NavigationLink(destination: MainView().environmentObject(mainViewModel), label: {
Text("Skip")
})
})
.border(.red)
}
}
.border(.blue)
}
} else {
// Fallback on earlier versions
}
}
}
Which outputs the following:
What I'm trying to achieve:
When someone clicks on the "Skip" text, to kill the OnboardingView and show the MainView().
The closest I got is setting a NavigationLink, but that had a back button and doesn't work so well, I want to be able to go to the MainView and not be able to go back to OnboardingView.
All help will be appreciated!
You could use a container view that conditionally displays the onboarding or main views, depending on the state of a variable (stored at the parent level). That variable can be passed down via a Binding:
Simplified example that should be easily applicable to your code:
class AppState: ObservableObject {
#Published var showOnboarding = true
}
#main
struct CustomCardViewApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
MainScreenContainer(showOnboarding: $appState.showOnboarding)
}
}
}
struct MainScreenContainer: View {
#Binding var showOnboarding: Bool
var body: some View {
if showOnboarding {
OnboardingView(showOnboarding: $showOnboarding)
} else {
MainView()
}
}
}
struct OnboardingView: View {
#Binding var showOnboarding: Bool
var body: some View {
Text("Onboarding")
Button("Skip") {
showOnboarding = false
}
}
}
struct MainView: View {
var body: some View {
Text("Main")
}
}

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 - Nested links within NavigationStack inside a NavigationSplitView not working

I'm playing around with the new navigation API's offered in ipadOS16/macOS13, but having some trouble working out how to combine NavigationSplitView, NavigationStack and NavigationLink together on macOS 13 (Testing on a Macbook Pro M1). The same code does work properly on ipadOS.
I'm using a two-column NavigationSplitView. Within the 'detail' section I have a list of SampleModel1 instances wrapped in a NavigationStack. On the List I've applied navigationDestination's for both SampleModel1 and SampleModel2 instances.
When I select a SampleModel1 instance from the list, I navigate to a detailed view that itself contains a list of SampleModel2 instances. My intention is to navigate further into the NavigationStack when clicking on one of the SampleModel2 instances but unfortunately this doesn't seem to work. The SampleModel2 instances are selectable but no navigation is happening.
When I remove the NavigationSplitView completely, and only use the NavigationStack the problem does not arise, and i can successfully navigate to the SampleModel2 instances.
Here's my sample code:
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
enum NavItem {
case first
}
var body: some View {
NavigationSplitView {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
} detail: {
SampleListView()
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
#State var path = NavigationPath()
#State var selection: SampleModel1.ID? = nil
var body: some View {
NavigationStack(path: $path) {
List(SampleModel1.samples, selection: $selection) { model in
NavigationLink("\(model.id)", value: model)
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
var body: some View {
Text("Model 1 ID \(model.id)")
List (SampleModel2.samples) { model2 in
NavigationLink("\(model2.id)", value: model2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I removed this unclear ZStack and all works fine. Xcode 14b3 / iOS 16
// ZStack { // << this !!
SampleListView()
// }
Apple just releases macos13 beta 5 and they claimed this was resolved through feedback assistant, but unfortunately this doesn't seem to be the case.
I cross-posted this question on the apple developers forum and user nkalvi posted a workaround for this issue. I’ll post his example code here for future reference.
import SwiftUI
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
#State var path = NavigationPath()
enum NavItem: Hashable, Equatable {
case first
}
var body: some View {
NavigationSplitView {
List {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
}
} detail: {
SampleListView(path: $path)
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
// Get the selection from DetailView and append to path
// via .onChange
#State var selection2: SampleModel2? = nil
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel1.samples) { model in
NavigationLink("Model1: \(model.id)", value: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
.navigationTitle("navigationDestination(for: SampleModel2.self)")
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model, path: $path, selection2: $selection2)
.navigationTitle("navigationDestination(for: SampleModel1.self)")
}
.navigationTitle("First")
}
.onChange(of: selection2) { newValue in
path.append(newValue!)
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
#Binding var path: NavigationPath
#Binding var selection2: SampleModel2?
var body: some View {
NavigationStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel2.samples, selection: $selection2) { model2 in
NavigationLink("Model2: \(model2.id)", value: model2)
// This also works (without .onChange):
// Button(model2.id.uuidString) {
// path.append(model2)
// }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

I'm trying to implement a view stack in swiftui and my #State objects are being reset for reasons that are unclear to me

I'm new to swiftui and doing an experiment with pushing and popping views with a stack. When I pop a view off the stack, the #State variable of the prior view has been reset and I don't understand why.
This demo code was tested on macos.
import SwiftUI
typealias Push = (AnyView) -> ()
typealias Pop = () -> ()
struct PushKey: EnvironmentKey {
static let defaultValue: Push = { _ in }
}
struct PopKey: EnvironmentKey {
static let defaultValue: Pop = {() in }
}
extension EnvironmentValues {
var push: Push {
get { self[PushKey.self] }
set { self[PushKey.self] = newValue }
}
var pop: Pop {
get { self[PopKey.self] }
set { self[PopKey.self] = newValue }
}
}
struct ContentView: View {
#State private var stack: [AnyView]
var body: some View {
currentView()
.environment(\.push, push)
.environment(\.pop, pop)
.frame(width: 600.0, height: 400.0)
}
public init() {
_stack = State(initialValue: [AnyView(AAA())])
}
private func currentView() -> AnyView {
if stack.count == 0 {
return AnyView(Text("stack empty"))
}
return stack.last!
}
public func push(_ content: AnyView) {
stack.append(content)
}
public func pop() {
stack.removeLast()
}
}
struct AAA : View {
#State private var data = "default text"
#Environment(\.push) var push
var body: some View {
VStack {
TextEditor(text: $data)
Button("Push") {
self.push(AnyView(BBB()))
}
}
}
}
struct BBB : View {
#Environment(\.pop) var pop
var body: some View {
VStack {
Button("Pop") {
self.pop()
}
}
}
}
If I type some text into the editor then hit Push, then Pop out of that view, I was expecting the text editor to maintain my changes but it reverts to the default text.
What am I missing?
Edit:
I guess this is really a question of how are NavigationView and NavigationLink implemented. This simple code does the what I'm trying to do:
import SwiftUI
struct MyView: View {
#State var text = "default text"
var body: some View {
VStack {
TextEditor(text: $text)
NavigationLink(destination: MyView()) {
Text("Push")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyView()
}
}
}
run that on iOS so you get a nav stack. edit the text, then push. Edit again if you want, then go back and see state is retained.
My code is trying to do the same thing in principle.
I'll share this attempt maybe it will help you create your version of this.
This all started with an attempt to create something like NavigationView and NavigationLink but being able to back track to a random View in the stack
I have a protocol where an object returns a View. Usually it is an enum. The view() references a View with a switch that provides the correct child View. The ContentView/MainView works almost like a storyboard and just presents whatever is designated in the current or path variables.
//To make the View options generic
protocol ViewOptionsProtocol: Equatable {
associatedtype V = View
#ViewBuilder func view() -> V
}
This is the basic navigation router that keep track of the main view and the NavigationLink/path. Which looks similar to what you want to do.
//A generic Navigation Router
class ViewNavigationRouter<T: ViewOptionsProtocol>: ObservableObject{
//MARK: Variables
var home: T
//Keep track of your current screen
#Published private (set) var current: T
//Keep track of the path
#Published private (set) var path: [T] = []
//MARK: init
init(home: T, current: T){
self.home = home
self.current = current
}
//MARK: Functions
//Control how you get to the screen
///Navigates to the nextScreen adding to the path/cookie crumb
func push(nextScreen: T){
//This is a basic setup just going forward
path.append(nextScreen)
}
///Goes back one step in the path/cookie crumb
func pop(){
//Use the stored path to go back
_ = path.popLast()
}
///clears the path/cookie crumb and goes to the home screen
func goHome(){
path.removeAll()
current = home
}
///Clears the path/cookie crumb array
///sets the current View to the desired screen
func show(nextScreen: T){
goHome()
current = nextScreen
}
///Searches in the path/cookie crumb for the desired View in the latest position
///Removes the later Views
///sets the nextScreen
func dismissTo(nextScreen: T){
while !path.isEmpty && path.last != nextScreen{
pop()
}
if path.isEmpty{
show(nextScreen: nextScreen)
}
}
}
It isn't an #Environment but it can easily be an #EnvrionmentObject and all the views have to be in the enum so the views are not completely unknown but it is the only way I have been able to circumvent AnyView and keep views in an #ViewBuilder.
I use something like this as the main portion in the main view body
router.path.last?.view() ?? router.current.view()
Here is a simple implementation of your sample
import SwiftUI
class MyViewModel: ViewNavigationRouter<MyViewModel.ViewOptions> {
//In some view router concepts the data that is /preserved/shared among the views is preserved in the router itself.
#Published var preservedData: String = "preserved"
init(){
super.init(home: .aaa ,current: .aaa)
}
enum ViewOptions: String, ViewOptionsProtocol, CaseIterable{
case aaa
case bbb
#ViewBuilder func view() -> some View{
ViewOptionsView(option: self)
}
}
struct ViewOptionsView: View{
let option: ViewOptions
var body: some View{
switch option {
case .aaa:
AAA()
case .bbb:
BBB()
}
}
}
}
struct MyView: View {
#StateObject var router: MyViewModel = .init()
var body: some View {
NavigationView{
ScrollView {
router.path.last?.view() ?? router.current.view()
}
.toolbar(content: {
//Custom back button
ToolbarItem(placement: .navigationBarLeading, content: {
if !router.path.isEmpty {
Button(action: {
router.pop()
}, label: {
HStack(alignment: .center, spacing: 2, content: {
Image(systemName: "chevron.backward")
if router.path.count >= 2{
Text(router.path[router.path.count - 2].rawValue)
}else{
Text(router.current.rawValue)
}
})
})
}
})
})
.navigationTitle(router.path.last?.rawValue ?? router.current.rawValue)
}.environmentObject(router)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
struct AAA : View {
//This will reset because the view is cosmetic. the data needs to be preserved somehow via either persistence or in the router for sharing with other views.
#State private var data = "default text"
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
TextEditor(text: $data)
TextEditor(text: $vm.preservedData)
Button("Push") {
vm.push(nextScreen: .bbb)
}
}
}
}
struct BBB : View {
#EnvironmentObject var vm: MyViewModel
var body: some View {
VStack {
Button("Pop") {
vm.pop()
}
}
}
}

How do I switch views with swiftUI MacOS?

I found this question - What is the best way to switch views in SwiftUI? - but I have not been able to get the answer to work for me.
struct view4x: View {
#State var goView: Bool = false
var body: some View {
if goView {
view5x(goView1: self.$goView)
} else {
Form {
/* ... */
}
}
}
}
and the button is inside the form:
Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
}
and for my other view I have:
struct view5x: View {
#Binding var goView1: Bool
var body: some View {
Text("TEST")
Button(action: {
self.goView1.toggle()
}) {
Text("Return")
}
}
}
I just get errors that both bodies declare an opaque return type. It does not preview.
Ok, here are similar mistakes in your views. To understand them better to look at View protocol:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// Declares the content and behavior of this view.
var body: Self.Body { get }
}
so, body is just a computed variable and it should return some View. Mistake in your view5x is that you put into it 2 different views instead 1. The solution here is to embed them into VStack for example:
struct view5x: View{
#Binding var goView1: Bool
var body: some View{
VStack {
Text("TEST")
Button(action: {
self.goView1.toggle()
}) {
Text("Return")
}
}
}
}
The problem it the view4x is similar - it's unclear what view returns body because of if...else statements, I think. You can fix it in the same way:
struct view4x: View {
#State var goView: Bool = false
var body: some View {
VStack {
if goView {
view5x(goView1: $goView)
} else {
Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
}
}
}
}
}
The other way is to say what view should body return if you wrap each of them into AnyView and type return before. In this example changes of goView don't switch views, but you can see the other syntax:
struct view4x: View {
#State var goView: Bool = false
var body: some View {
if goView {
return AnyView(view5x(goView1: $goView))
} else {
return AnyView(Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
})
}
}
}