johnpatrickmorgan / NavigationBackport

Backported SwiftUI navigation APIs introduced in WWDC22
MIT License
858 stars 54 forks source link

Fatal error: No ObservableObject of type DestinationBuilderHolder found when mixing UIKit and SwiftUI #42

Open ejubber opened 1 year ago

ejubber commented 1 year ago

Hey @johnpatrickmorgan. I'm having an issue running iOS 15, using the latest version (0.9) of the library, and mixing SwiftUI and UIKit. For reference, our app has some UIKit navigation (via a UINavigationController), and some SwiftUI navigation (via embedded views in UIViewControllers, and using the NavigationBackport library).

I kept getting the error described in the title and couldn't figure out what was wrong. I realized the issue occurs when I have the following structure:

UINavigationController ----UIViewController (root view controller) --------SwiftUIView using NavigationStack (embedded in above ViewController).

When using the popToRoot functionality, I get the error described above each time.

Here's some sample code you can use to reproduce the issue.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        embedView()
        navigationController?.navigationBar.isHidden = true
        // Do any additional setup after loading the view.
    }

    func embedView() {
        let v = ContentView()
        let hostingController = UIHostingController(rootView: v)
        addChild(hostingController)
        hostingController.view.backgroundColor = UIColor.white
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)

        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

import SwiftUI
import NavigationBackport

struct ContentView: View {
    @State var path: NBNavigationPath = NBNavigationPath()

    @ViewBuilder private var SplashScreen: some View {
        VStack {
            Spacer()

            Text("0")

            Spacer()

            Button {
                path.push(Screen.first)
            } label: {
                Text("Next")
            }
        }
    }

    var body: some View {
        NBNavigationStack(path: $path) {
            SplashScreen
            .nbNavigationDestination(for: Screen.self) { screen in
                ViewForScreen(screen: screen)
            }
        }
    }
}

struct ViewForScreen: View {
    @EnvironmentObject var coordinator: PathNavigator
    var screen: Screen

    var buttonText: String {
        if let nscreen = screen.nextScreen {
            return "Next"
        } else {
            return "Pop to root"
        }
    }

    func buttonAction(_ nextScreen: Screen? = nil) {
        if let nextScreen {
            coordinator.push(nextScreen)
        } else {
            coordinator.popToRoot()
        }
    }

    var body: some View {
        VStack {
            Spacer()

            Text(screen.number)

            Spacer()

            Button {
                buttonAction(screen.nextScreen)
            } label: {
                Text(buttonText)
            }
        }
    }

}

enum Screen: Hashable {
    case first
    case second
    case third
    case fourth
    case fifth

    var number: String {
        switch self {
        case .first:
            return "1"
        case .second:
            return "2"
        case .third:
            return "3"
        case .fourth:
            return "4"
        case .fifth:
            return "5"
        }
    }

    var nextScreen: Screen? {
        switch self {
        case .first:
            return .second
        case .second:
            return .third
        case .third:
            return .fourth
        case .fourth:
            return .fifth
        case .fifth:
            return nil
        }
    }
}

If you embed the UIViewController in a UINavigationController, you'll be able to reproduce the issue. I've also attached a ZIP file here for your convenience.

I haven't been able to solve it yet by changing the configuration of the UINavigationController - not sure if there is anything you can change in the source code itself to fix this issue. Let me know if I can provide more info! Thanks

Test3.zip

johnpatrickmorgan commented 1 year ago

Thanks for raising this @ejubber, and for the reproduction. I was able to reproduce the issue, and as you pointed out, it only crashes when the whole thing is hosted in a UINavigationController, very strange! I have a few things I can try that might overcome the issue, will keep you posted.

tonicfx commented 1 year ago

Hello, I have the same problem, is there a fix ? Tanks for your help.

tonicfx commented 1 year ago

For information, problem could be reproductible in preview mode.

ejubber commented 12 months ago

Hey @johnpatrickmorgan , I was able to reproduce this on iOS 16.1.1 as well - just a heads up

aehlke commented 5 months ago

Find a solution?