NavigationBackport icon indicating copy to clipboard operation
NavigationBackport copied to clipboard

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

Open ejubber opened this issue 2 years ago • 5 comments

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

ejubber avatar Jul 09 '23 01:07 ejubber

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.

johnpatrickmorgan avatar Jul 12 '23 11:07 johnpatrickmorgan

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

tonicfx avatar Sep 13 '23 09:09 tonicfx

For information, problem could be reproductible in preview mode.

tonicfx avatar Sep 13 '23 13:09 tonicfx

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

ejubber avatar Oct 19 '23 22:10 ejubber

Find a solution?

aehlke avatar May 10 '24 16:05 aehlke

It seems, issue is fixed in this commit f97c504

and now not able to reproduce this issue. if anyone still facing this, I can look into it.

mwaqasbhati avatar Nov 01 '24 04:11 mwaqasbhati

Thanks @mwaqasbhati! That would suggest that the issue was caused by a difference in how the environment is propagated through the navigation stack between UIKit and SwiftUI. I'm glad that change seems to have fixed the issue!

johnpatrickmorgan avatar Nov 04 '24 13:11 johnpatrickmorgan