29

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

1
  • 12
    Please, don't downvote just because you don't get the question. It's a valid question. Commented Sep 23, 2019 at 5:43

4 Answers 4

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 
                // ...
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Can I ask if you're using snapchat loginkit? Because I'm facing the same problem
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.com
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

5 Comments

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 days
I realize I never posted my solution. It is published here amplify-ios-workshop.go-aws.com/30_add_authentication/…
Here is another possible solution, that works for other types as well: SwiftUI-Introspect.
@SébastienStormacq in your link I don't see how you access UINavigationController?
@hyouuu I explained the solution here stackoverflow.com/a/58098217/663360
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://stackoverflow.com/a/58068947/1745000)

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.

Comments

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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.