UIViewControllerRepresentable: Navigation Title and Bar Button Items Ignored in NavigationView
Asked Answered
A

6

33
  • I have a UITableViewController subclass that I have wrapped in UIViewControllerRepresentable.
  • I have set the navigationItem.title and navigationItem.leftBarButtonItems in my view controller.
  • I present my UIViewControllerRepresentable instance as the destination of a SwiftUI NavigationLink within a SwiftUI NavigationView.
  • When the view is pushed onto the navigation stack, the table view appears but the title and bar button items do not.

What is happening here?

Aurthur answered 11/12, 2019 at 23:10 Comment(1)
could you post some code to reproduce please?Ripplet
A
64

Solved!

Problem and Expectation SwiftUI uses a UINavigationController under the hood. So, if I push a UIViewController onto a SwiftUI NavigationView using UIViewControllerRepresentable, then I would expect the navigation item and toolbar items of that view controller to be used by said navigation controller. As I mention above, they are ignored.

Root Cause It turns out the title and items are ignored because my view controller’s parent is not the UINavigationController as expected. Rather, it’s parent is an intermediary wrapper view controller used by SwiftUI under the hood, which is in turn pushed onto the navigation controller. It’s ignoring the title and items because the navigation controller is asking the wrapper for its items (which has none), rather than my view controller.

UIKit Solution So, if you want to set the title or bar button items or toolbarItems from your UIKit view controller, then you need to set them on it’s parent as such:

self.parent?.navigationItem.title = "My Title"

Furthermore, you cannot do this from viewDidLoad, because the view controller does not appear to have been wrapped by the SwiftUI parent by that time. You have to do it in viewWillAppear.

SwiftUI Solution You can also set the title and bar buttons from SwiftUI. On your UIViewControllerRepresentable instance, just add .navigationBarTitle and leading/trailing items as you normally would. Then you can have the buttons talk to your view controller from within your UIViewControllerRepresentable implementation.

Aurthur answered 13/12, 2019 at 7:4 Comment(5)
I am using swiftui way as you mentioned, but my back button is not working.Shool
Would be ok to use didMove(toParent)/willMove(toParent) ?Stanfill
You are a god for figuring this out. Couldn't for the life of me figure out why our custom navigation item was missing when using a view controller in swiftUI.Wyly
Just for those who are interested on I made it work by updating the parent navigation item inside my UIViewControllerRepresentable object as follows: func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { vc.parent?.navigationItem.title = vc.navigationItem.title vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems }Affine
Great solution! I needed to move this to viewWillAppear as parent wasn't yet set in viewDidLoad. Works like a charm now!Devin
M
23

Here's a UIKit solution that doesn't require making internal changes to your UIViewController and should only be called once, when the wrapper is added as parent.

struct MyViewControllerRepresentable: UIViewControllerRepresentable {

    class Coordinator {
        var parentObserver: NSKeyValueObservation?
    }

    func makeUIViewController(context: Self.Context) -> MyViewController {
        let viewController =  MyViewController()
        context.coordinator.parentObserver = viewController.observe(\.parent, changeHandler: { vc, _ in
            vc.parent?.title = vc.title
            vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems
        })
        return viewController
    }

    func updateUIViewController(_ uiViewController: MyViewController, context: Self.Context) {}

    func makeCoordinator() -> Self.Coordinator { Coordinator() }
}
Maloney answered 28/7, 2021 at 20:39 Comment(0)
D
15

Good suggestions from josephap But the his UIKit Solution made my navigation bar title 'flicker', not as smooth as it normally appear.

My solution was just add the navigation title from my SwiftUI:

NavigationLink(destination: <YourUIViewControllerRepresentable>()
                                    .edgesIgnoringSafeArea([.top,.bottom])
                                    .navigationTitle(item.name)) {
                        Text(item.name)
                    }) 
Desiredesirea answered 25/11, 2020 at 11:50 Comment(2)
Not really, because in legacy SDKs the nav title is needing be set within the UIViewController after some internal work and can't be done beforehand.Lauzon
Thanks for the .edgesIgnoringSafeArea tip too, I was wondering why my UIKit view wasn't properly full screen.Dogmatic
H
5

I use one key value observer extra when the leftBarButtonItems changes at runtime, for example when you use UIActions with a state change to show and hide a checkmark. Then you need to reload the leftBarButtonItems.

context.coordinator.leftBarButtonItemsObserver = viewController.observe(\.navigationItem.leftBarButtonItems, changeHandler: { vc, _ in
    vc.parent?.navigationItem.leftBarButtonItems = vc.navigationItem.leftBarButtonItems
})

so the full code would be like this:

struct MyViewControllerRepresentable: UIViewControllerRepresentable {

    class Coordinator {
        var parentObserver: NSKeyValueObservation?
        var leftBarButtonItemsObserver: NSKeyValueObservation?
    }

    func makeUIViewController(context: Self.Context) -> MyViewController {
        let viewController =  MyViewController()
        context.coordinator.parentObserver = viewController.observe(\.parent, changeHandler: { vc, _ in
            vc.parent?.title = vc.title
            vc.parent?.navigationItem.leftBarButtonItems = vc.navigationItem.leftBarButtonItems
        })
        
        context.coordinator.leftBarButtonItemsObserver = viewController.observe(\.navigationItem.leftBarButtonItems, changeHandler: { vc, _ in
            vc.parent?.navigationItem.leftBarButtonItems = vc.navigationItem.leftBarButtonItems
        })
        
        return viewController
    }

    func updateUIViewController(_ uiViewController: MyViewController, context: Self.Context) {}

    func makeCoordinator() -> Self.Coordinator { Coordinator() }
}
Haemoid answered 6/12, 2022 at 15:7 Comment(0)
T
1

When you present a UIViewController (wrapped in UIViewControllerRepresentable) from a SwiftUI NavigationView, the navigation bar of the parent SwiftUI view might not transfer automatically to the UIKit context. To correctly present the UIKit view controller with a navigation bar, you would need to embed it in a UINavigationController in the UIViewControllerRepresentable wrapper.

import SwiftUI
import UIKit

struct UIKitViewControllerWrapper: UIViewControllerRepresentable {
    typealias UIViewControllerType = UINavigationController
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<UIKitViewControllerWrapper>) -> UINavigationController {
        let viewController = CustomUIKitViewController() // Your custom UIViewController
        let navigationController = UINavigationController(rootViewController: viewController)
        return navigationController
    }
    
    func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<UIKitViewControllerWrapper>) {
        // Update the UIViewController if needed
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: UIKitViewControllerWrapper()) {
                Text("Go to UIKit View controller")
            }
        }
    }
}
Ticking answered 31/7, 2023 at 10:36 Comment(0)
U
0

I have had the same issue, the problem in my case was that the UIViewController could be presented from a SwiftUI View and another UIViewController. Using parent did not help, I ended up using

navigationController?.topViewController?.title and navigationController?.topViewController?.navigationItem

instead. This makes more sense since the topViewController can be a SwiftUI wrapper or the same viewController if it is pushed from another UIViewController.

Undershorts answered 21/2 at 22:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.