Are there any way to work using NavigationStack with NavigationLink to support applications below iOS 16?

73 Views Asked by At

In some points of the code, I need to use this:

    NavigationLink(
        destination: LoginViewScreen(),
        isActive: $navToLogin
    ) { EmptyView() }

... because my navigation is not using a button, I use only the property as trigger to navigate.

I'm receiving this warning:

use NavigationLink(value:label:) inside a List within a NavigationStack or NavigationSplitView

The problem is that minimum iOS target is 14.0 and I'm not able to use NavigationStack because it is for iOS 16.0.

I would like something like that, where I can use the both ways. But I am not being able, because the property NavigationPath needs to be a @State, and the states need to be a property of the class, and if the property is iOS > 16, all the class is > 16 and a cannot instanciate it in my application

@State var navToLogin: Bool = false
@EnvironmentObject var navigationViewModel: NavigatorViewModel

var body: some View  {
    NavigationView {
        VStack {
            ExampleView()
                .onAppear() {
                    Task {
                        try? await Task.sleep(nanoseconds: 5000000000)
                        navigateToLogin()
                    }
                }
            navigationToLogin
        }
    }
}

var navigationToLogin: some View {
    NavigationLink(
        destination: LoginViewScreen(),
        isActive: $navToLogin
    ) { EmptyView() }
}

private func navigateToLogin() {
    if #available(iOS 16, *) {
        navigationViewModel.navigateTo(.loginScreen)
    } else {
        navToLogin.toggle()
    }
}

Does anyone know how to work well with it ?

Or is there any other way ?

2

There are 2 best solutions below

0
Eric Terrisson On

I don't have a direct answer to your question but I can offer you a different approach. Maybe this will suit you?

@State var navToLogin: Bool = false
@EnvironmentObject var navigationViewModel: NavigatorViewModel

var body: some View  {
    if navToLogin {
        LoginViewScreen()
    } else {
        NavigationView {
            VStack {
                ExempleView()
                    .onAppear() {
                        Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
                            navToLogin = true
                        }
                    }
                Button("Go to Login Page") {
                    navToLogin.toggle()
                }
            }
        }
    }
}    

We use a timer instead of sleep to avoid blocking the thread and the display is only conditioned on the value of navToLogin.

Happy coding!

0
Joao Macedo Dev On

I asked this in the R/SwiftUI too and I received a response that may help who is having the same problem.

The response:

https://www.reddit.com/r/SwiftUI/comments/1b8284a/comment/ktmuxoe/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

The solution:

    public struct NavigationViewStack<V>: View where V: View {

    @ViewBuilder private let content: () -> V

    public init(content: @escaping () -> V) {
        self.content = content
    }

    public var body: some View {
        if #available(iOS 16, *) {
            NavigationStack { content() }
        } else {
            NavigationView { content() }
        }
    }
}

public extension View {
    @ViewBuilder
    func navigationDestinationWrapper<V>(isPresented: Binding<Bool>, @ViewBuilder destination: () -> V) -> some View where V: View {
        if #available(iOS 16, *) {
            self.navigationDestination(isPresented: isPresented, destination: destination)
        } else {
            ZStack {
                NavigationLink(isActive: isPresented, destination: destination, label: {
                    EmptyView()
                })
                self
            }
        }
    }

    @ViewBuilder
    func navigationDestinationWrapper<D, C>(item: Binding<D?>, @ViewBuilder destination: @escaping (D) -> C) -> some View where D: Hashable, C: View {
        if #available(iOS 17, *) {
            self.navigationDestination(item: item, destination: destination)
        } else {
            ZStack {
                NavigationLink(
                    destination: generateDestination(item, destination),
                    isActive: Binding<Bool>(
                        get: { item.wrappedValue != nil },
                        set: { _ in
                            item.wrappedValue = nil
                        }
                    ),
                    label: { EmptyView() }
                )
                self
            }
        }
    }

    @ViewBuilder
    private func generateDestination<D, C>(_ item: Binding<D?>, @ViewBuilder _ destination: @escaping (D) -> C) -> some View where D: Hashable, C: View {
        if let unwrappedItem = item.wrappedValue {
            destination(unwrappedItem)
        } else {
            EmptyView()
        }
    }
}

Example:

NavigationViewStack {
  Text("First Page")
    .navigationDestinationWrapper(isPresented: $presentSecondPage, destination: {
        Text("Second Page")
    })
}

Here, we create the NavigationViewStack to wrap NavigationStack and NavigationView.

The extensions will wrap the functions of NavigationStack to have a similar functionally in NavigationView.

The best solution is upgrade to the minimum iOS version to 16.0, but it will be useful to not broke when the NavigationView be deprecated.