SwiftUI : how to access UINavigationController from NavigationView
Asked Answered
P

4

28

I am developing an app using SwiftUI. The app is based around a NavigationView.

I am using a third-party framework that provides UIKit components and the framework has not been updated to support SwiftUI yet.

One framework method is expecting a parameter of type UINavigationController

How can I supply this framework the NavigationController created by SwiftUI ? Or how can I create a UINavigationController that will replace SwiftUI's default ?

I read https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit and https://sarunw.com/posts/uikit-in-swiftui but these seems to address another question : they explain how to use UIKit components in a SwiftUI app. My problem is the other way around, I want to use SwiftUI App and access underlying NavigationController object.

[UPDATE] The code implementing my solution is available from this workshop : https://amplify-ios-workshop.go-aws.com/30_add_authentication/20_client_code.html#loginviewcontroller-swift

Peepul answered 22/9, 2019 at 13:51 Comment(1)
Please, don't downvote just because you don't get the question. It's a valid question.Anagram
A
11

I don't think you can do that right now. Looking at the view debugger for NavigationView I get the image below.

So it seems to you will have to go the other way around:
Start with a UINavigationController, and wrap the SwiftUI view(s) in UIHostingController.

enter image description here

Anagram answered 23/9, 2019 at 5:56 Comment(5)
Thank you for your reply and support :-) I explored your solution and have something working by mixing a manually managed UINavigationController on top of Swift UI as you suggest. I used the technique explained here developer.apple.com/tutorials/swiftui/interfacing-with-uikit I need to polish and refactor the code and share my solution to this in a couple of daysTricky
I realize I never posted my solution. It is published here amplify-ios-workshop.go-aws.com/30_add_authentication/…Tricky
Here is another possible solution, that works for other types as well: SwiftUI-Introspect.Anagram
@SébastienStormacq in your link I don't see how you access UINavigationController?Chee
@Chee I explained the solution here https://mcmap.net/q/494076/-swiftui-how-to-access-uinavigationcontroller-from-navigationviewTricky
P
12

Thanks to Yonat's explanation I understood how to do this and here is my solution, hoping it will help others.

Part 1 : The UI View Controller that will be used from Swift UI. It calls a third-party authentication library, passing the UINavigationControler as parameter. The UINavigationController is an empty view, just there to allow the third-party authentication library to have a Navigation Controller to pop up the Login Screen.

struct LoginViewController: UIViewControllerRepresentable {

    let navController =  UINavigationController()


    func makeUIViewController(context: Context) -> UINavigationController {
        navController.setNavigationBarHidden(true, animated: false)
        let viewController = UIViewController()
        navController.addChild(viewController)
        return navController
    }

    func updateUIViewController(_ pageViewController: UINavigationController, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    class Coordinator: NSObject {
        var parent: LoginViewController

        init(_ loginViewController: LoginViewController) {
            self.parent = loginViewController
        }
    }

    func authenticate() {
        let app = UIApplication.shared.delegate as! AppDelegate
        let userData = app.userData

        userData.authenticateWithDropinUI(navigationController: navController)
    }

}

Part 2 : The Swift UI View is displaying the (empty) UINavigationControler and overlays a SwiftUI view on top of it.

import SwiftUI

struct LandingView: View {
    @ObservedObject public var user : UserData

    var body: some View {

        let loginView = LoginViewController()

        return VStack {

            // .wrappedValue is used to extract the Bool from Binding<Bool> type
            if (!$user.isSignedIn.wrappedValue) {

                ZStack {
                    loginView
                    // build your welcome view here 
                    Button(action: { loginView.authenticate() } ) {
                        UserBadge().scaleEffect(0.5)
                    }
                }

            } else {

                // my main app view 
                // ...
            }
        }
    }
}
Peepul answered 25/9, 2019 at 12:18 Comment(2)
Can I ask if you're using snapchat loginkit? Because I'm facing the same problemDichotomous
Nope, I was using AWS Amplify Authentication component aws-amplify.github.io/docs/ios/authentication I created a workshop with step by step instructions to demonstrate AWS Amplify on iOS amplify-ios-workshop.go-aws.comTricky
A
11

I don't think you can do that right now. Looking at the view debugger for NavigationView I get the image below.

So it seems to you will have to go the other way around:
Start with a UINavigationController, and wrap the SwiftUI view(s) in UIHostingController.

enter image description here

Anagram answered 23/9, 2019 at 5:56 Comment(5)
Thank you for your reply and support :-) I explored your solution and have something working by mixing a manually managed UINavigationController on top of Swift UI as you suggest. I used the technique explained here developer.apple.com/tutorials/swiftui/interfacing-with-uikit I need to polish and refactor the code and share my solution to this in a couple of daysTricky
I realize I never posted my solution. It is published here amplify-ios-workshop.go-aws.com/30_add_authentication/…Tricky
Here is another possible solution, that works for other types as well: SwiftUI-Introspect.Anagram
@SébastienStormacq in your link I don't see how you access UINavigationController?Chee
@Chee I explained the solution here https://mcmap.net/q/494076/-swiftui-how-to-access-uinavigationcontroller-from-navigationviewTricky
M
0

I tried to do the same thing because I wanted to make the interactivePopGestureRecognizer work on the whole view.

I managed to access the current navigation controller using an UINavigationController extension and overriding viewDidAppear, checking if the interactivePopGestureRecognizer was enabled and changed it ( https://mcmap.net/q/504648/-navigation-pop-view-when-swipe-right-like-instagram-iphone-app-how-i-achieve-this)

At the end my effort was pointless. When the navigation view presented the DetailHostingController, it toggled off interactivePopGestureRecognizer.isEnabled!

The hosting view via topViewController.view does contain a gesture recogniser of private type SwiftUI.UIGestureRecognizer. No targets are set though...

Embedding a traditional UINavigationController may also be preferred because navigation view's own pop gesture isn't cancellable (if you drag the view a little bit and stop, it snaps back and then dismiss the detail view.

Mignon answered 29/9, 2019 at 19:57 Comment(0)
S
0

A possible solution to get UINavigationController which was created under the hood by SwiftUI after using NavigationView could look like below.

First create a view modifier that creates a view used as a hook to get a UIViewController existing in the current view hierarchy:

class VCHookViewController: UIViewController {
    var onViewWillAppear: ((UIViewController) -> Void)?
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        onViewWillAppear?(self)
    }
}
struct VCHookView: UIViewControllerRepresentable {
    typealias UIViewControllerType = VCHookViewController
    let onViewWillAppear: ((UIViewController) -> Void)
    func makeUIViewController(context: Context) -> VCHookViewController {
        let vc = VCHookViewController()
        vc.onViewWillAppear = onViewWillAppear
        return vc
    }
    func updateUIViewController(_ uiViewController: VCHookViewController, context: Context) {
    }
}
struct VCHookViewModifier: ViewModifier {
    let onViewWillAppear: ((UIViewController) -> Void)
    func body(content: Content) -> some View {
        content
            .background {
                VCHookView(onViewWillAppear: onViewWillAppear)
            }
    }
}
extension View {
    func onViewWillAppear(perform onViewWillAppear: @escaping ((UIViewController) -> Void)) -> some View {
        modifier(VCHookViewModifier(onViewWillAppear: onViewWillAppear))
    }
}

Then use this modifier in SwiftUI code to get existing UINavigationController like this:

NavigationView {
    VStack {
        Text("hello")
    }
    .onViewWillAppear { vc in
        print("UINavigationController: \(vc.navigationController)") // and here you have it
    }
    .navigationTitle("Test")
}
.navigationViewStyle(.stack)
Soucy answered 4/4 at 14:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.