Code Monkey home page Code Monkey logo

swiftui-navigation-stack's Introduction

swiftui-navigation-stack

An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations and programmatic navigation.

NavigationStack

Installation

Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler: open xCode, click on File -> Swift Packages -> Add Package dependency... and use the repository URL (https://github.com/matteopuc/swiftui-navigation-stack.git) to download the package.

In xCode, when prompted for Version or branch, the suggestion is to use Branch: master.

Then in your View simply include import NavigationStack and follow usage examples below.

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate NavigationStack into your Xcode project using CocoaPods, specify it in your Podfile:

pod 'NavigationStack'

Then in your View simply include import NavigationStack and follow usage examples below.

Usage

In SwiftUI we have a couple of views to manage the navigation: NavigationView and NavigationLink. At the moment these views have some limitations:

  • we can't turn off the transition animations;
  • we can't customise the transition animations;
  • we can't navigate back either to root (i.e. the first app view), or to a specific view;
  • we can't push programmatically without using a view;

NavigationStackView is a view that mimics all the behaviours belonging to the standard NavigationView, but it adds the features listed here above. You have to wrap your view hierarchy inside a NavigationStackView:

import NavigationStack

struct RootView: View {
    var body: some View {
        NavigationStackView {
            MyHome()
        }
    }
}

Jan-07-2020 15-40-35

You can even customise transitions and animations in some different ways. The NavigationStackView will apply them to the hierarchy:

  • you could decide to go for no transition at all by creating the navigation stack this way NavigationStackView(transitionType: .none);
  • you could create the navigation stack with a custom transition:
import NavigationStack

struct RootView: View {
    var body: some View {
        NavigationStackView(transitionType: .custom(.scale)) {
            MyHome()
        }
    }
}

Jan-10-2020 15-31-40

  • NavigationStackView has a default easing for transitions. The easing can be customised during the initialisation
struct RootView: View {
    var body: some View {
        NavigationStackView(transitionType: .custom(.scale), easing: .spring(response: 0.5, dampingFraction: 0.25, blendDuration: 0.5)) {
            MyHome()
        }
    }
}

Important: The above is the recommended way to customise the easing function for your transitions. Please, note that you could even specify the easing this other way:

NavigationStackView(transitionType: .custom(AnyTransition.scale.animation(.spring(response: 0.5, dampingFraction: 0.25, blendDuration: 0.5))))

attaching the easing directly to the transition. Don't do this. SwiftUI has still some problems with implicit animations attached to transitions, so it may not work. For example, implicit animations attached to a .slide transition won't work.

Push

In order to navigate forward you have two options:

  • Using the PushView;
  • Programmatically push accessing the navigation stack directly;

PushView

The basic usage of PushView is:

PushView(destination: ChildView()) {
    Text("PUSH")
}

which creates a tappable view (in this case a simple Text) to navigate to a destination. There are other ways to trigger the navigation using the PushView:

struct MyHome: View {
    @State private var isActive = false
    
    var body: some View {
        VStack {
            PushView(destination: ChildView(), isActive: $isActive) {
                Text("PUSH")
            }
            
            Button(action: {
                self.isActive.toggle()
            }, label: {
                Text("Trigger push")
            })
        }
    }
}

this way you have a tappable view as before, but you can even exploit the isActive bool to trigger the navigation (also in this case the navigation is triggered through the PushView).

If you have several destinations and you want to avoid having a lot of @State booleans you can use this other method:

enum ViewDestinations {
    case noDestination
    case child1
    case child2
    case child3
}

struct MyHome: View {
    @ObservedObject var viewModel: ViewModel
    @State private var isSelected: ViewDestinations? = .noDestination

    var body: some View {
        VStack {
            PushView(destination: ChildView1(), tag: ViewDestinations.child1, selection: $isSelected) {
                Text("PUSH TO CHILD 1")
            }

            PushView(destination: ChildView2(), tag: ViewDestinations.child2, selection: $isSelected) {
                Text("PUSH TO CHILD 2")
            }

            PushView(destination: ChildView3(), tag: ViewDestinations.child3, selection: $isSelected) {
                Text("PUSH TO CHILD 3")
            }

            Button(action: {
                self.isSelected = self.viewModel.getDestination()
            }, label: {
                Text("Trigger push")
            })
        }
    }
}

Now you have three tappable views and the chance to trigger the navigation through a tag (the navigation is always triggered by the PushView).

Push programmatically:

Inside the NavigationStackView you have access to the navigation stack as an EnvironmentObject. If you need to trigger the navigation programmatically without relying on a PushView (i.e. without having a tappable view) you can do like this:

struct MyHome: View {
    @ObservedObject var viewModel: ViewModel
    @EnvironmentObject private var navigationStack: NavigationStackCompat

    var body: some View {
        Button(action: {
            self.viewModel.performBackgroundActivities(withCallback: {
                DispatchQueue.main.async {
                    self.navigationStack.push(ChildView())
                }
            })
        }, label: {
            Text("START BG ACTIVITY")
        })
    }
}

Specifying an ID

It's not mandatory, but if you want to come back to a specific view at some point later you need to specify an ID for that view. Both PushView and programmatic push allow you to do that:

struct MyHome: View {
    private static let childID = "childID"
    @ObservedObject var viewModel: ViewModel
    @EnvironmentObject private var navigationStack: NavigationStackCompat

    var body: some View {
        VStack {
            PushView(destination: ChildView(), destinationId: Self.childID) {
                Text("PUSH")
            }
            Button(action: {
                self.viewModel.performBackgroundActivities(withCallback: {
                    DispatchQueue.main.async {
                        self.navigationStack.push(ChildView(), withId: Self.childID)
                    }
                })
            }, label: {
                Text("START BG ACTIVITY")
            })
        }
    }
}

Pop

Pop operation works as the push operation. We have the same two options:

  • Using the PopView;
  • Programmatically pop accessing the navigation stack directly;

PopView

The basic usage of PopView is:

struct ChildView: View {
    var body: some View {
        PopView {
            Text("POP")
        }        
    }
}

which pops to the previous view. You can even specify a destination for your pop operation:

struct ChildView: View {
    var body: some View {
        VStack {
            PopView(destination: .root) {
                Text("POP TO ROOT")
            }
            PopView(destination: .view(withId: "aViewId")) {
                Text("POP TO THE SPECIFIED VIEW")
            }
            PopView {
                Text("POP")
            }
        }
    }
}

PopView has the same features as the PushView. You can create a PopView that triggers with the isActive bool or with the tag. Also, you can trigger the navigation programmatically without relying on the PopView itself, but accessing the navigation stack directly:

struct ChildView: View {
    @ObservedObject var viewModel: ViewModel
    @EnvironmentObject private var navigationStack: NavigationStackCompat

    var body: some View {
        Button(action: {
            self.viewModel.performBackgroundActivities(withCallback: {
                self.navigationStack.pop()
            })
        }, label: {
            Text("START BG ACTIVITY")
        })
    }
}

NavigationStack injection

By default you can programmatically push and pop only inside the NavigationStackView hierarchy (by accessing the NavigationStackCompat environment object). If you want to use the NavigationStackCompat outside the NavigationStackView you need to create your own NavigationStackCompat (wherever you want) and pass it as a parameter to the NavigationStackView. This is useful when you want to decouple your routing logic from views.

Important: Every NavigationStackCompat must be associated to a NavigationStackView. A NavigationStackCompat cannot be shared between multiple NavigationStackView.

For example:

struct RootView: View {
    let navigationStack: NavigationStackCompat

    var body: some View {
        NavigationStackView(navigationStack: navigationStack) {
            HomeScreen(router: MyRouter(navStack: navigationStack))
        }
    }
}

class MyRouter {
    private let navStack: NavigationStackCompat

    init(navStack: NavigationStackCompat) {
        self.navStack = navStack
    }

    func toLogin() {
        self.navStack.push(LoginScreen())
    }

    func toSignUp() {
        self.navStack.push(SignUpScreen())
    }
}

struct HomeScreen: View {
    let router: MyRouter

    var body: some View {
        VStack {
            Text("Home")
            Button("To Login") {
                router.toLogin()
            }
            Button("To SignUp") {
                router.toSignUp()
            }
        }
    }
}

Important

Please, note that NavigationStackView navigates between views and two views may be smaller than the entire screen. In that case the transition animation won't involve the whole screen, but just the two views. Let's make an example:

struct Root: View {
    var body: some View {
        NavigationStackView {
            A()
        }
    }
}

struct A: View {
    var body: some View {
        VStack(spacing: 50) {
            Text("Hello World")
            PushView(destination: B()) {
                Text("PUSH")
            }
        }
        .background(Color.green)
    }
}

struct B: View {
    var body: some View {
        PopView {
            Text("POP")
        }
        .background(Color.yellow)
    }
}

The result is:

Jan-10-2020 15-47-43

The transition animation uses just the minimum amount of space necessary for the views to enter/exit the screen (i.e. in this case the maximum width between view1 and view2) and this is exactly how it is meant to be.

On the other hand you also probably want to use the NavgationStackView to navigate screens. Since in SwiftUI a screen (the old UIKit ViewController) it's just a View I suggest you create an handy and simple custom view called Screen like this:

extension Color {
    static let myAppBgColor = Color.white
}

struct Screen<Content>: View where Content: View {
    let content: () -> Content

    var body: some View {
        ZStack {
            Color.myAppBgColor.edgesIgnoringSafeArea(.all)
            content()
        }
    }
}

Now we can rewrite the example above using the Screen view:

struct Root: View {
    var body: some View {
        NavigationStackView {
            A()
        }
    }
}

struct A: View {
    var body: some View {
        Screen {
            VStack(spacing: 50) {
                Text("Hello World")
                PushView(destination: B()) {
                    Text("PUSH")
                }
            }
            .background(Color.green)
        }
    }
}

struct B: View {
    var body: some View {
        Screen {
            PopView {
                Text("POP")
            }
            .background(Color.yellow)
        }
    }
}

This time the transition animation involves the whole screen:

Jan-10-2020 16-10-59

Issues

  • SwiftUI resets all the properties of a view marked with @State every time the view is removed from a view hierarchy. For the NavigationStackView this is a problem because when I come back to a previous view (with a pop operation) I want all my view controls to be as I left them before (for example I want my TextFields to contain the text I previously typed in). In order to workaround this problem you have to use @ObservableObject when you need to make some state persist between push/pop operations. For example:
class ViewModel: ObservableObject {
    @Published var text = ""
}

struct MyView: View {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            TextField("Type something...", text: $viewModel.text)
            PushView(destination: MyView2()) {
                Text("PUSH")
            }
        }
    }
}

Other

SwiftUI is really new, there are some unexpected behaviours and several API not yet documented. Please, report any issue may arise and feel free to suggest any improvement or changing to this implementation of a navigation stack.

swiftui-navigation-stack's People

Contributors

503geek avatar matteopuc avatar mattevigo avatar mustafaozhan avatar pacu avatar piterwilson avatar sadmansamee avatar saket avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swiftui-navigation-stack's Issues

Clear Stack feature

Maybe an implementation of a function that clears the ViewStack

When called it will :

  • Set the last view as Root View
  • Clear the stack

Mandatory pops to the root view when modifying observed object in the stacked view

Hi, this one of the great library I ever found. However when trying to modifying an observed object in the second view (stack view), then it pop to the root view immediately, here is the minimum code to reproduce the issue, please have a look:

import SwiftUI
import NavigationStack

class Model : ObservableObject {
    @Published var counter1 : Int = 0
    @Published var counter2 : Int = 0
}

struct SecondView: View {
    @ObservedObject var model : Model
    
    var body: some View {
        VStack() {
            Text("2st Page")
            Button(action: {
                model.counter2 += 1
                print(model.counter2)
            }, label: {
                Text("Press to increase (\(model.counter2))")
            })
            PopView(label: {
                Text("Back")
            })
        }
    }
}

struct ContentView: View {
    @ObservedObject var model : Model = Model()
    
    var body: some View {
        NavigationStackView() {
            VStack() {
                Text("1st Page")
                Button(action: {
                    model.counter1 += 1
                    print(model.counter1)
                }, label: {
                    Text("Press to increase (\(model.counter1))")
                })
                PushView(destination: SecondView(model : model), label: {
                    Text("Go to 2nd view")
                })
            }
        }
    }
}

When press the "Press to increase" button in the second view, the app pop to the root view immediately
Runs on Xcode 12.2 & iOS simulator iPhone 12 Pro, iOS 14.2

popping a view makes the previous view non clickable

I have a home screen with 3 Tabs one of the tab has scroll view with some clickable items, on pushing this navigation view stack, and popping back the view remains non-clickable unless the scroll is moved a little and then everything is back to clickable again.

HELP!

Collapsed sidebar change side bar link cause crash

Steps to reproduce:

  1. Enter full screen.
  2. Collapse the sidebar by dragging it all the way to the left.
  3. Let the mouse go.
  4. Move the mouse left until the sidebar appears as overlay.
  5. Select another sidebar tab -> crash

Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1a4503df8)

[Suggestion] Drag gestures

Hi,

I'd love to see a really great future of standard NavigationView, such as drag gesture for going back, It's relatively easy to do

PushView/PopView define their own animation

Hi,

I may be missing something obvious, or not fully understanding the use of this library, but I'd like for different PushView and PopView (and their programmatic counterparts) to allow for overriding the default animation of the entire stack.

Is that something that is possible today? It doesn't seem to appear so from the code, where it uses withAnimation(easing). Is it recommended to create a new NavigationStackView whenever you want to change the animation style?

Thanks!

How is viewId determined when PopView is used?

I'm using your NavigationStack in combination with SwiftUI's default Tabbed app, with each tab having its own NavigationView. I have wrapped the entire app's ContentView into NavigationStackView so that I can display **PushView()**s in fullscreen mode, including over the Tab bar. My question is where do I specify/define the viewId for the nested view that I would like to PopView() back to? Documentation provides a vague example of how to supply destinationId of the ChildView() when PushView() is used but I have a hard time understanding how the passed on childViewId helps in popping back to the parent or maybe a view nested 2 levels inside NavigationView? Can you please clarify or provide a slightly complex example? Thank you again!

SceneDelegate .environmentObject

For programmatic push we need:

@EnvironmentObject private var navigationStack: NavigationStack

But this results in this error (which is normal):

A View.environmentObject(_:) for NavigationStack may be missing as an ancestor of this view

The normal fix for this, in the root view or a parent view, as I understand it, is to init and "attach" the bindable like this (such as in SceneDelegate):

var navigationStack = NavigationStack()

let contentView = SomeView().environmentObject(navigationStack)

But this gives the compiler error:

'NavigationStack' initializer is inaccessible due to 'internal' protection level

How did you work around this in your examples for programmatic push? Is there another way to attach the env obj other than initializing it?

Question: How to change transition of the view which was already pushed into nav stack

First of all thanks for the great library! Am trying to figure out how to achieve different transitions on a view which was already pushed to the stack. Let's say i pushed a view with a default transition and on the view there are two buttons: one is leading to the next screen with a default transition but the other is showing a view over the current like a popover. Because the host view is already pushed to the stack with defined push and pop transition i cannot see a way how to modify it for my needs. Any ideas are appreciated! Thanks!

Fast Forward

Hi, awesome work here.
I wonder how to navigate programmatically to any view in hierarchy, but not just popping to a previous one also pushing a far view.
I have a navigation mechanism that should be able to navigate from any view to any other view keeping stack. Useful to use with deep links for example, which I need. It was working fine until found some issues in SwiftUI for certain routes.
So, is this possible now? Maybe a navigationStack.push([View]) Perhaps View should conform to protocol Identifiable ....
Example:
I have a view targetView which can be reached by this route: [root, view1, view2, targetView]
I'm currently in another view currentView which route could be like [root, view1, view3, currentView]
I ned to go from currentView to targetView in seamless way but keeping stack, so I don't want to view how it animates popping to view1and then pushing view2 and after that targetView.

Thanks in advance!

@ObservedObject var viewModel: ViewModel

you have mentioned programmatic push/pop in the examples, but it's not clear how you implemented the ObservedObject var viewModel: ViewModel, and what it's purpose is, and what function performBackgroundActivities() is doing, would it be same if you just called DispatchQueue.main.async { self.navigationStack.push(ChildView()) } inside button's action?, can you explain this more? or do we have to implement ViewModel logic ourselves

struct MyHome: View {
    @ObservedObject var viewModel: ViewModel
    @EnvironmentObject private var navigationStack: NavigationStack

    var body: some View {
        Button(action: {
            self.viewModel.performBackgroundActivities(withCallback: {
                DispatchQueue.main.async {
                    self.navigationStack.push(ChildView())
                }
            })
        }, label: {
            Text("START BG ACTIVITY")
        })
    }
}

Default UINavigationController animation?

Hi, quick question - how do I replicate the default NavigationView's transition animation?

It's not a plain .slide, it's both slide and slide of the original one at a half distance.

PopView doesn't work for me (Xcode 11.3.1)

Hello,
I've been trying your awesome navigation stack and it's been great to push the views, but I couldn't manage to make to PopView work ( it doesn't send me to the previous view even when I set an Id ) I honestly don't know what am doing wrong.

ParentView where I use the PushView ( works fine )
ScrollView(.horizontal, showsIndicators: false) { HStack (spacing: 16){ ForEach(productData){ item in PushView(destination: ProductDetails()){ SingleProduct(showCount: false, count: 01, countnull: false, singleProduct: item) } } }.frame(height: 320) .padding(.top,20) .padding(.horizontal) } .offset( y: -15)
`

ChildView ( Doesn't work )

HStack{ PopView(isActive: $isActive){ Button(action: { self.isActive.toggle() }){ Image(systemName: "arrow.left") .font(.system(size: 24, weight:.regular, design: .default)) .foregroundColor(Color.black) .frame(width:45,height: 45) .background(Color.white) .cornerRadius(13) .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 12) } } } .padding(.horizontal,16)

onAppear called immediately after push/pop

Hi there,

First, thanks for this stack, really useful for my small side project as a SwiftUI rookie.

Actually, I've just encountered the following behavior: onAppear is called immediately when pushing/popping pages, and not when the transition is finished. As I have an advanced custom transition taking more time and leading to a page that will have its own animation, I would like onAppear to be delayed until the transition is finished.

I believe this is not really a bug but I'm wondering if this could be solved by this stack. Note: I can probably use state and enhance my custom transition to workaround (even though tbh custom transitions are a bit buggy when I want to deal with timing/delays).

Here is a simple example:

struct DestinationView: View {
    var body: some View {
        VStack {
            Spacer()
            HStack {
                PopView {
                    Text("Page 2").onAppear {
                        print("Page 2 onAppear") // Printed immediately
                    }
                }
            }
            Spacer()
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
    }
}

struct Transition_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            NavigationStackView(transitionType: .default,
                                easing: Animation.linear(duration: 5)) {
                                    PushView(destination: DestinationView()) {
                                        HStack {
                                            Spacer()
                                            Text("Page 1").onAppear {
                                                print("Page 1 onAppear") // Printed immediately
                                            }
                                            Spacer()
                                        }
                                    }
            }
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
    }
}

PushView with Buttons

How do I PushView with buttons...??

This does nothing..
PushView(destination: SignInView()) { Button(action: { self.isActive.toggle() }) { HStack { Image("mail-icon") Text(" Login with Email/Mobile") .font(.custom("Avenir Next Bold", size: 14.0)) Spacer() } } .modifier(PrimaryButton(colorInput: .red)) }

But this works fine..??

PushView(destination: SignInView()) { HStack { Image("mail-icon") Text(" Login with Email/Mobile") .font(.custom("Avenir Next Bold", size: 14.0)) Spacer() } .modifier(PrimaryButton(colorInput: .red)) }

Cocoapod support

Could you also add this to cocoapods, for those who haven't yet migrated to SPM? :)

Swipe from left to right to go back

Trying to implement a custom navigation as the NavigationView that SwiftUI supports doesnt provide many features,

Notice the one in here is quite impressive. But I noticed this doesnt have the feature to go back to the previous screen by dragging left to right (default go back feature that iOS provides)

Is there a way to get it done ?

NavugationBar Title

hi
how I can mimic the navigation bar functionality in the navigation stack.how I can add navigation title and navigation Buttons and background color. should I use the ViewModifier to add headView to handler the navigation title and .. and another ContentView to render the body
best regards

Navigation with matchedGeometryEffect?

Ciao Matteo!

Is it possible to do a navigation with scaling animation using matchedGeometryEffect?

On the example bellow I made it work with a Stack, works fine. But I cannot get it to work as a navigation between views.

Maybe there is already a way to do using your library?

cheers from Stockholm!

import SwiftUI


class CoverData: ObservableObject {
    @Published var images = ["cover", "cover2"]
    @Published var selected = ""
    @Published var showDetails: Bool = false
}

struct Grid: View {
    @EnvironmentObject var viewModel: CoverData
    
    let namespace: Namespace.ID
    
    var body: some View {
        
        List {
            ForEach(viewModel.images.indices) { index in
                let image = viewModel.images[index]
                Image(image)
                    .resizable()
                    .frame(width: 50, height: 50)
                    .cornerRadius(4)
                    .padding()
                    .matchedGeometryEffect(id: image, in: namespace)
                    .onTapGesture {
                        viewModel.selected = image
                        withAnimation(.spring()) {
                            viewModel.showDetails.toggle()
                        }
                    }
            }
        }
   }
}

struct Detail: View {
    @EnvironmentObject var viewModel: CoverData

    let namespace: Namespace.ID
    
    var body: some View {
        
            Image(viewModel.selected)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .cornerRadius(10)
                .padding(40)
                .matchedGeometryEffect(id: viewModel.selected, in: namespace)
        
      
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(#colorLiteral(red: 0.234857142, green: 0.043259345, blue: 0.04711621255, alpha: 1)))
    }
}

struct ContentView: View {
    
    @Namespace private var ns
    @StateObject private var coverData = CoverData()
    
    var body: some View {
        ZStack {
            Spacer()
            if coverData.showDetails {
                Detail(namespace: ns)
                .onTapGesture {
                    withAnimation(.spring()) {
                        coverData.showDetails.toggle()
                    }
                }
            }
            else {
                Grid(namespace: ns)
            }
        }
        .environmentObject(coverData)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Incompatibility with macOS through cocoapods

Hey, thanks for maintaining this library! I am running into this error when trying to import it into my macOS app through cocoapods:

[!] The platform of the target `Teleport` (macOS 11.0) is not compatible with `NavigationStack (1.0.2)`, which does not support `macOS`.

What am I missing?

navViewModel not persisted

This might be due to my limited knowledge of SwiftUI or a flaw in my app. However, it happens that NavigationStackView is re-initialized for rerendering and because navViewModel is created in the constructor, it forgets its state.

Marking it is a @StateObject fixes the problem for me, but this method is only available starting iOS 14, so I'm not sure if that's what you want for this library.

And as I mentioned, perhaps NavigationStackView shouldn't be reinitialized at all, but that seems to be a problem with either SwiftUI or my app.

@State variables get reset after pop

Using a standard NavigationView and NavigationLink, the @State variables retain their values when pushed and subsequently popped back into view. iOS example:

import SwiftUI


struct ContentView: View {
    var body: some View {
        NavigationView {
            AAA()
        }
    }
}

struct AAA: View {
    @State var text = "default text"
    var body: some View {
        VStack {
            TextEditor(text: $text)
            NavigationLink(destination: BBB()) {
                Text("Push")
            }
        }
    }
}

struct BBB: View {
    var body: some View {
        Text("BBB")
    }
}

Edit the text, click Push, then go Back. The texteditor retains the changes that were made before pushing.

I tried the same thing with StackNavigationView and find that my @State variable is reset after a pop.

iOS example:

import SwiftUI
import NavigationStack

struct ContentView: View {
    var body: some View {
        NavigationStackView {
            AAA().padding()
        }
    }
}

struct AAA : View {
    @State private var data = "default text"

    var body: some View {
        VStack {
            TextEditor(text: $data)
            PushView(destination: BBB()) {
                Text("Push")
            }
        }
    }
}

struct BBB : View {
    var body: some View {
        PopView {
            Text("Pop")
        }
    }
}

If you edit text the editor, hit Push, then Pop back you see the text has been reset to its default state.

Is this expected behavior?

Navigation bar

Hi!

This is some impressive work! Thanks for that!

We are really facing lots of issues with the NavigationView comes with SwiftUI and looking for an alternative, the problem is that we do need the option to add a "Back" button to views, similar to the way native NavigationView let you.

Is there any example of how to do so? or maybe while using another library that provides a drop-in bar?

Thanks again!

sheets stop showing

after changing the parent of a view from NavigationView to NavigationStackView sheets no longer work inside its children eg:

struct ChildView: View {
    @State var showPopup = false
    var body: some View {
        Text("test")
            .onTapGesture { self.showPopup = true }
            .onAppear { self.showPopup = true }
            .sheet(isPresented: self.$showPopup) { Text("popup") }
    }
}

popup is never shown, if I revert back to NavigationView it works fine.

Toolbar integration

Hello.
Thanks for this amazing work. Could you please suggest how to integrate NavigationStackView with .toolbar { }. Currently I have two ideas:

  1. Add .toolbar {} to the NavigationStackView {}. In this case it is not clear how to switch between views forth and back. Each time the user presses a toolbar button a new instance of the view will be added instead of pop/push combination
  2. Do not mix .toolbar {} with NavigationStackView {}. At the top level we might have NavigationView.toolbar {} to switch between high level views. For the deeper navigation levels use NavigationStackView. In this case some of the original issue addressed by NavigationStackView might return back.

Also I tried to push view from the toolbar block which leads to the following issue: Fatal error: No ObservableObject of type NavigationStack found. A View.environmentObject(_:) for NavigationStack may be missing as an ancestor of this view.: file SwiftUI, line 0. It happens because NavigationStack is available only inside NavigationStackView:

struct HomeView: View {
    @EnvironmentObject private var navigationStack: NavigationStack
    var body: some View {
        NavigationStackView {
            AppScreen {
                VStack {
                    Text("Home screen")
                }
            }
        }
        .toolbar {
            ToolbarItemGroup(placement: .bottomBar) {
                Button("Home") {
                    self.navigationStack.push(HomeView())
                }
                Button("Settings") {
                    self.navigationStack.push(SettingsView())
                }
            }
        }
    }
}

If I place it next to the AppScreen then a toolbar will gone when the next view is pushed.

What thoughts do you have on this subject?

Fatal error: No ObservableObject of type NavigationStack found.

Open curiosity: Is there a way to use this inside of a modal or nav view? (Works well otherwise)

If I attempt either, I get the following error:

Fatal error: No ObservableObject of type NavigationStack found.

I'm down to try anything as a potential workaround

How to manually make view .root

I have this app where I use userDefaults to store if the user was previously logged in. If they weren't previousy logged in, the first view is a login page. If they were previously logged in, the first view is a different view. That said, I want to implement a logout feature. But if they were already logged in, and the first view is not the login page, how can I pop back to a view that never appeared ie (the login page).

Support for iOS 15.

This is not working in iOS 15 and also breaks things. Breaks formatting in some subViews, colors, etc. Also, some buttons not working, I have tested buttons initiating bluetooth scan action. Could be interoperability with CoreBluetooth.

[Unknown process name] CGAffineTransformInvert: singular matrix

First off, your utility is AMAZING! Thank you so much for this! I'm running into the following warning/error:

2020-07-04 08:37:50.344351-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.
2020-07-04 08:37:50.344494-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.
2020-07-04 08:37:50.365485-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.
2020-07-04 08:37:50.561570-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.
2020-07-04 08:37:52.577640-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.
2020-07-04 08:37:52.779411-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.
2020-07-04 08:37:52.779592-0400 NavStackTest[11370:1800054] [Unknown process name] CGAffineTransformInvert: singular matrix.

After doing some digging, it seems to be originating from specifying the transitionType attribute for the NavigationStackView while having a List view inside the destination. If I leave this attribute out, there are no errors. Also, I'm not sure if this is related to an already closed issue #22. It would be greatly appreciated if you can help. Thank you in advance. Please see reproducible code see below:

import SwiftUI
import NavigationStack

struct ContentView: View {
  var body: some View {
    NavigationStackView(transitionType: .custom(.scale)) {
      ZStack {
        Color.orange.edgesIgnoringSafeArea(.all)
        HStack {
          PushView(destination:ChildView()) {
            Text("Settings")
          }
        }
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

struct ChildView: View {
  @State var appName: String = ""
  var body: some View {
    ZStack {
      Color.yellow.edgesIgnoringSafeArea(.all)
      
      VStack {
        List {
          ForEach(0...10, id: \.self) { i in
            ListRow(item: "One")
          }
        }
        
        PopView { Text("Go Back") }
      }
    }
  }
}

struct ListRow: View {
  @State var item: String
  var body: some View {
    Text(self.item)
  }
}

TextField or UITextView Representable not working

If you have any TextField or a UITextView Representable object in any view that is inside NavigationStack, user is prevented from typing anything. The text just disappears.

e.g.
TextField("test", text: $workingText)

use of performBackgroundActivities in viewmodel

I can not apply this logic, could you give me an example of how to apply it, I need to manage the screen transactions through my viewmodel.

struct MyHome: View {
    @ObservedObject var viewModel: ViewModel
    @EnvironmentObject private var navigationStack: NavigationStack

    var body: some View {
        Button(action: {
            self.viewModel.performBackgroundActivities(withCallback: {
                DispatchQueue.main.async {
                    self.navigationStack.push(ChildView())
                }
            })
        }, label: {
            Text("START BG ACTIVITY")
        })
    }
}

Broken behavior when the view is changing during animation

First of all: thanks for that awesome library! It's so much better that what is currently available in SwiftUI!

Now to describe the actual problem: when I am navigating between static views, everything works just fine. But as soon as a view is changing, it is not animated anymore. This does not happen when the text of a Text() view is changing for example. It only occurs when a view is replaced during the transition, like when showing a ProgressView() and switching to the Text() once the content is available. The error looks like this during the animation:

EE7DE9D6-3CCE-4F02-874C-415268FB41DC

The Text() should be fully contained within the red area, but as you can see, it is located where it should be after the transition. I setup a minimal working example below. The transition is slowed down to visualize the error. The asyncAfter operation stands for some network request.

@main
struct MyApp: App {
    @StateObject private var viewModel = ViewModel()
    @StateObject var navigationStack = NavigationStack(easing: Animation.easeOut(duration: 3))

    var body: some Scene {
        WindowGroup {
            NavigationStackView(navigationStack: navigationStack) {
                VStack(alignment: .center) {
                    HStack { Spacer() }
                    Spacer()
                                        
                    Button(action: {
                        self.navigationStack.push(SecondView())
                        DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
                            DispatchQueue.main.sync {
                                self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
                            }
                        }
                    }){
                        Text("Go")
                    }
                    
                    Spacer()
                }.background(Color.green)
            }.environmentObject(viewModel)
        }
    }
}

final class ViewModel: NSObject, ObservableObject {
    @Published var someText: String = ""
}

struct SecondView: View {
    @EnvironmentObject var viewModel: ViewModel
    
    var body: some View {
        VStack(alignment: .center) {
            HStack { Spacer() }
            Spacer()
        
            if(viewModel.someText.isEmpty) {
                ProgressView()
                    .progressViewStyle(CircularProgressViewStyle())
            } else {
                Text(viewModel.someText)
            }
            
            Spacer()
        }.background(Color.red)
    }
}

Is it possible to fix this? I can only load the content, after the user taps the button which starts the transition. If not, the only idea which comes to my mind is that any view updates could be delayed until the transition is finished.

Pushing to certain view after Push notification received

Hello, great library!
I was wondering if you have any suggestion or example to use it to push a view after a push notification is received from AppDelegate.
I tried to push the view like this:
self.appState.navigationStack?.push((MyView()))
But apparently it cannot be done, unless you trigger the push from a View/button.
Thank you!

Stack Reset / Jump to a completely different view

Thanks for this project!
I'm interested in the following functionality:
Consider, we have a list of views in the stack: [Z, A, B, C]
I'd like to reset stack completely to have new view hierarchy as [Z, X] by e.g. pressing a button in the view C.
Is this possible as of right now?

NavigationBar not presented

Hi,

I really miss the possibility to add custom transitions in swiftui. So thank you for working on this framework.
Unfortunatly if I switch from NavigationView to NavigationStackView the navigationbar disappears.

Transition animation not working

I've successfully removed the slide animation but now no animation is working when I transition between view. Ideally the MainMenu screen would fade in after the text from the first view is gone.

struct ContentView: View {
	var body: some View {
		NavigationStackView(transitionType: .custom(.opacity), easing: .easeOut(duration: 2)) {
			IntroScreen()
		}
	}
}

struct IntroScreen: View {
	@State private var start = true
	@State private var accelerate = false
	@State private var change = false
	@State private var counter = 0

	let timer = Timer.publish(every: 1.5, on: .main, in: .common).autoconnect()

	var body: some View {
		VStack {
			Text("Sick library bro")
				.font(.system(size: 35))
				.offset(x: 0, y: (start ? -500 : 0))
				.animation(.easeOut(duration: 1.5))
				.opacity((accelerate ? 0 : 1))
				.animation(.easeIn(duration: 1))
				.onAppear() {
					start = false
				}
				.onReceive(timer){ _ in
					if counter < 1 {
						accelerate = true
					} else {
						change = true
					}
					counter += 1
				}

			PushView(destination: MainMenu(), isActive: $change) {}
		}

	}

}

MainMenu:

struct MainMenu: View {
	var body: some View {
		VStack {
			Color.blue.edgesIgnoringSafeArea(.all)
			Text("hi")
				.font(.system(size: 35))
		}
	}
}

This is what I get

ezgif com-gif-maker

NavigationStack stops functioning once I open a fullScreenCover

I have this chat tab on my app which uses a fullScreenCover. The problem is, after I open that fullScreenCover, all the Push functions of the navigationStack stop working. They animate but done push to anything, its like it is sliding itself on top of it.

No ObservableObject of type NavigationStack found. A View.environmentObject(_:) for NavigationStack may be missing as an ancestor of this view.

Fantastic library.

What I did:

  • I created a NavigationViewModel that will handle navigation across pages that need it. See NavigationVM below.
  • Programmatically want to push to the next view while providing the viewId

Error:
No ObservableObject of type NavigationStack found. A View.environmentObject(_:) for NavigationStack may be missing as an ancestor of this view.

Thank you for your help.

import Foundation
import SwiftUI
import NavigationStack

class NavigationVM: ObservableObject {
    @EnvironmentObject private var navigationStack: NavigationStack
    
    init(){
        
    }
    
    func pushView(view: AnyView, viewId: String? = nil){
        DispatchQueue.main.async {
            if viewId?.isEmpty == true {
                self.navigationStack.push(view)
            } else {
                self.navigationStack.push(view, withId: viewId!)
            }
        }
    }
    
    func popView(viewId: String? = nil){
        DispatchQueue.main.async {
            if viewId?.isEmpty == true {
                self.navigationStack.pop()
            } else {
                self.navigationStack.pop(to: PopDestination.view(withId: viewId!))
            }
        }
    }
    
    func popToRoot() {
        DispatchQueue.main.async {
            self.navigationStack.pop(to: .root)
        }
    }
}

Here's my implementation:

var body: some View {
        NavigationStackView {
              merchantGroupsBody()
        }
}

private func merchantGroupsBody() -> some View {
        VStack{
            ForEach(exploreVM.merchantGroups) { merchantGroup in
                if merchantGroup.merchants.count > 0 {
                    VStack (alignment: .leading){
                        merchantGroupHeaderBody(merchantGroup)
                        ScrollView(.horizontal){
                            HStack{
                                ForEach(merchantGroup.merchants, id: \.self){ merchant in
                                    merchantBody(merchant)
                                }
                            }
                        }
                    }
                    //.foregroundColor(.white)
                }
            }
        }
    }

private func merchantBody(_ merchant: Merchant) -> some View {
        var alreadyCached: Bool {
            ImageCache.default.isCached(forKey: merchant.locationImageUrl)
        }
        
        return
            //NavigationLink(destination: MerchantView(MerchantVM(merchant))) {
            VStack (alignment: .leading) {
                KFImage(URL(string: merchant.attachments.first!.url))
                    .onSuccess { r in
                        print("Success: \(merchant.name) - \(r.cacheType)")
                    }
                    .onFailure { e in
                        print("Error for mechant: \(merchant.name): \(e)")
                    }
                    .onProgress { downloaded, total in
                        print("\(downloaded) / \(total))")
                    }
                    .placeholder {
                        HStack {
                            Image(systemName: "arrow.2.circlepath.circle")
                                .resizable()
                                .frame(width: 50, height: 50)
                                .padding(10)
                            Text("Loading...").font(.title)
                        }
                        .foregroundColor(.gray)
                    }
                    .cancelOnDisappear(true)
                    .resizable()
                    .frame(width: 200, height: 100)
                    .aspectRatio(contentMode: .fill)
                    .opacity(doneLoadingImage || alreadyCached ? 1.0 : 0.3)
                    .animation(.linear(duration: 0.4))
                Text(merchant.name)
                Text("\(merchant.distanceToCustomerString) | \(merchant.hoursOfOperationString)")
                    .font(.system(size:12))
                Spacer()
            }
            .onTapGesture {
                navigationVM.pushView(view: AnyView(MerchantView(MerchantVM(merchant))), viewId: CustomerViewIds.MerchantView.rawValue)
            }
        //}
    }

Programmatically navigation does not work on simulator or physical device with iOS 14.5

On a simulator with iOS 13.0, programmatically navigation works fine, but on the same simulator with iOS 14.5 and on a physical device with iOS 14.5, the transition does not work. With what it can be connected?

// First View
@EnvironmentObject var navigationStack: NavigationStack
//...
DispatchQueue.main.async {
    let destination = ModelDetailContainerView()
        .environmentObject(appStore)
        .environmentObject(navigationStack)
    self.navigationStack.push(destination)
}

// Second View
@EnvironmentObject var navigationStack: NavigationStack

var body: some View {
    ModelDetailRenderView(item: detailedSelectedItem)
        .valueChanged(value: detailedSelectedItem.wrappedValue) { value in
            if value == nil {
                navigationStack.pop(to: .root)
            }
        }
}

UPD
Transition occurs by tap on the context menu item of custom cell. Programmatic behavior of all interested entities does not change depending on iOS version.

Transition not applied to entire View

When I use a PushView, part of the contents of the View are not treated the same for the transition. They will transition instantly, while the rest of the content transitions normally. It seems to primarily affect "dynamic" content, such as a ForEach. Is there a way around this?

ViewStack not persistent

Hi, thanks for this nice implementation. I've bumped into a problem. I have the rootView which is made out of some bottom view and navigationViewStack. The bottom view needs to be displayed over some of the navigation stack, but not all of them. This rootView has a viewModel that controls the visibility of the bottom view and can be shared down the stack. After I navigate through the stack, when modifing the rootView viewModel the whole stack is reseting and the rootview is displayed. I think this is because of ViewStack is reinitialized. Making it persistent per navigation stack instance should resolve these kind of issues.

P.S. : I've resolved this issue by making a singleton of HashMaps [id: NavigationStack] and a methode to get/add a new NavigationStack singletone inside NavigationStack class. The hashmap for maintaining the functionality of multiple navigations flow. Every navigation flow will have an id and from here it's own NavigationStack singletone. Inside the NavigationStackVIew init also ask for an id with the default "root" if it's not specified.

Also if you have a more elegant/efficient way of achieving this, please let me know.

warning emitted: CGAffineTransformInvert: singular matrix

I get following warning when pushing or popping from NavigationStack
[90654:2639075] [Unknown process name] CGAffineTransformInvert: singular matrix.
Am I doing something wrong here:

        PushView(destination:SettingsView()) {
          VStack {
            Image(systemName: "gear")
              .resizable()
              .scaledToFit()
              .clipShape(Rectangle())
              .shadow(color: .primary, radius: 5)
              .padding([.horizontal, .top], 7)
            Text("Settings").lineLimit(1)
          }
          .frame(minWidth: 50, maxWidth:100, alignment: .center)
        }

          PopView {
            Text("<-")
          }

Fatal error: No observable object of type NavigationStack.Type found (iOS 13.0)

Hello there!

First of all, thank you for this amazing library which fixed many problems we had while implementing a navigation stack on our own, e.g. when using conditional navigation via switch-case-statements.

While this library is working completely fine on iOS 13.1, 13.2.2 and 13.3, it leads to the following crash during app start on iOS 13.0:

Fatal error: No observable object of type NavigationStack.Type found. A View.environmentObject(_:) for NavigationStack.Type may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-24.4/Core/EnvironmentObject.swift, line 161 Fatal error: No observable object of type NavigationStack.Type found. A View.environmentObject(_:) for NavigationStack.Type may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-24.4/Core/EnvironmentObject.swift, line 161 (lldb)

There seems to be a problem with attaching NavigationStack as an EnvironmentObject. We were able to fix this issue by setting it as an EnvironmentObject by our own. This though is not possible with the library as it is, since its initializes is internal and therefor not accessible.

My question is now how to fix this issue on iOS 13.0. If there is no solution, maybe you could provide a public initializer as a workaround, so we can add the EnvironmentObject by our own.

Thanks in advance and best regards,
Philipp

Pop to previous (back button)?

Hello guys, tanks for sharing such a needed library!

Is it possible to do a Pop to previous (back button), instead of "Pop to root"?

cheers form Stockholm!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.