24
  • 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?

josephap
  • 2,075
  • 17
  • 24

5 Answers5

59

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.

Mike
  • 6,285
  • 4
  • 18
  • 22
josephap
  • 2,075
  • 17
  • 24
  • I am using swiftui way as you mentioned, but my back button is not working. – user832 Aug 12 '20 at 18:21
  • Would be ok to use didMove(toParent)/willMove(toParent) ? – Lorenzo Fiamingo Jan 30 '21 at 12:06
  • 4
    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. – Xaxxus Feb 09 '21 at 06:52
  • 1
    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 }``` – Santiago Carmona González Jul 08 '21 at 20:38
16

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() }
}
Devin Pitcher
  • 2,562
  • 1
  • 18
  • 11
11

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)
                    }) 
f0rz
  • 1,495
  • 2
  • 14
  • 26
  • 1
    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. – TruMan1 Jun 02 '21 at 17:03
  • 1
    Thanks for the `.edgesIgnoringSafeArea` tip too, I was wondering why my UIKit view wasn't properly full screen. – Jeremy Sep 30 '21 at 15:52
3

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() }
}
Janevin
  • 63
  • 4
0

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")
            }
        }
    }
}
Eric Yuan
  • 1,213
  • 11
  • 13