Search code examples
swiftswiftuiswiftui-navigationlink

SwiftUI generic navigation link using block variable


Here I would like to create navigation code, I'm able to do this Swift UIKit. I'm trying the same functionality in SwiftUI, but I'm facing an issue with my code. How can we convert the SwiftUI view to AnyView.

Is there any other way to achieve the same functionality in SwiftUI?

Your help would be greatly appreciated.!!


/// Swift code
public struct Navigator {
    public var onLoginSuccess: (UINavigationController) -> Void = { navigationController in
        navigationController.pushViewController(UIViewController(), animated: true)
    }
}

/// Usage
var router = Navigator()
router.onLoginSuccess = { nav in
    nav.pushViewController(UIViewController(), animated: true)
}


/// SwiftUI  Code
struct Navigator {
    static var onTap: (AnyView) -> Void = { view in
        _ = view.navigate(to: Text("SS"))
    }
}

extension View {
    func navigate<SomeView: View>(to view: SomeView) -> some View {
        modifier(NavigateModifier(destination: view))
    }
}


fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
    fileprivate let destination: SomeView
    
    fileprivate func body(content: Content) -> some View {
        NavigationView {
            ZStack {
                content
                NavigationLink(destination: destination) {
                    EmptyView()
                }
            }
        }
    }
}

/// Usage 
NavigationView {
    Button("Home") {
        Navigator.onTap(self)
    }
}

Here is another solution code working fine with a single destination, but I can't change destination runtime. Router.onLogin should accept destination view.

struct ContentView: View {
    var body: some View {
        NavigationView {
            HStack {
                NavigationLink(destination: Router.onLogin) {
                    Text("HOME")
                }
            }
        }
    }
}


struct Router {
    @ViewBuilder
    static var onLogin: some View {
        Text("Hello")
    }
}

Solution

  • This wouldn't work because in Navigator onTap return void and it will not push view on any view.

    But you can do by this

    extension View {
        /// Navigate to a new view.
        /// - Parameters:
        ///   - view: View to navigate to.
        ///   - binding: Active binding
        func navigate<NewView: View>(to view: NewView, when binding: Binding<Bool>) -> some View {
            ZStack {
                self
                NavigationLink(
                    destination: view,
                    isActive: binding
                ) {
                    EmptyView()
                }
            }
        }
    }
    

    Usage:

    struct ContentView: View {
        
        @State private var isNextScreen: Bool = false
        
        var body: some View {
            NavigationView {
                Button("Home") {
                    isNextScreen.toggle()
                }.navigate(to: Text("SS"),when: $isNextScreen)
            }
        }
    }
    

    Update

    As you mention in a comment, you want multiple and dynamic destinations.

    Then you can use it this way.

    View extension for navigation

    extension View {
        func navigate(to view: Binding<Navigator?>) -> some View {
            ZStack {
                self
                if let wrappedValue = view.wrappedValue {
                    NavigationLink(
                        destination: wrappedValue.navigateView,
                        tag: wrappedValue,
                        selection: view,
                        label: {EmptyView()})
                }
            }
        }
    }
    

    Create Navigator

    enum Navigator: Identifiable {
        case onTap
        case onLogin
        
        var id: Navigator {
            return self
        }
        
        @ViewBuilder
        var navigateView: some View {
            switch self {
            case .onTap:
                Text("SS")
                
            case .onLogin:
                Text("Login View")
            }
        }
    }
    

    Usage Content View

    struct ContentView: View {
        
        @State private var nextScreen: Navigator? = nil
        
        var body: some View {
            NavigationView {
                VStack{
                    Button("Home") {
                        nextScreen = .onTap
                    }
                    
                    Button("Login") {
                        nextScreen = .onLogin
                    }
                    
                }.navigate(to: $nextScreen)
            }
        }
    }
    

    enter image description here