SwiftUI - How to access UIHostingController from SwiftUI

10.1k views Asked by At

I have SwiftUI page, it's being navigated from UIKit view. I want to set a title to this page, what I'm doing is

// code of UIKit view
let controller = UIHostingController(rootView: SwiftUIView())
controller.title = "title"
MyNavigationManager.present(controller)

Is there a way I can access the hosting controller within SwiftUI?

Then I could write code like self.hostingController?.title = "title"

3

There are 3 answers

0
Asperi On BEST ANSWER

Here is a demo of possible approach - to use external configuration wrapper class to hold weak link to controller and inject it into SwiftUI view (as alternate it is also possible to make it ObservableObject to combine with other global properties and logic).

Tested with Xcode 12.5 / iOS 14.5

class Configuration {
    weak var hostingController: UIViewController?    // << wraps reference
}

struct SwiftUIView: View {
    let config: Configuration   // << reference here

    var body: some View {
        Button("Demo") {
            self.config.hostingController?.title = "New Title"
        }
    }
}

let configuration = ExtConfiguration()
let controller = UIHostingController(rootView: SwiftUIView(config: configuration))

// injects here, because `configuration` is a reference !!
configuration.hostingController = controller

controller.title = "title"
MyNavigationManager.present(controller)
0
Confused Vorlon On

I went for a different option - subclass NSHostingController so that it provides itself as an environment variable.

struct SwiftUIView: View {
    @EnvironmentObject var host:HSHostWrapper

    var body: some View {
        Button("Dismiss") {
            host.controller?.dismiss(self)
        }
    }
}

let controller = HSHostingController(rootView: SwiftUIView())

this is achieved with the following HSHostingController (which is available in the HSSwiftUI package)

import Foundation
import SwiftUI


#if os(iOS) || os(tvOS)
    import UIKit
    public typealias ViewController = UIViewController
    public typealias HostingController = UIHostingController
#elseif os(OSX)
    import AppKit
    public typealias ViewController = NSViewController
    public typealias HostingController = NSHostingController
#endif

public class HSHostWrapper:ObservableObject {
    public weak var controller:ViewController?
}


/// allows root view (and children) to access the hosting controller by adding
/// @EnvironmentObject var host:HSHostWrapper
/// then e.g. host.controller?.dismiss()
public class HSHostingController<Content>:HostingController<ModifiedContent<Content,SwiftUI._EnvironmentKeyWritingModifier<HSHostWrapper?>>> where Content : View {
    
    public init(rootView:Content) {
        let container = HSHostWrapper()
        let modified = rootView.environmentObject(container) as! ModifiedContent<Content, _EnvironmentKeyWritingModifier<HSHostWrapper?>>
        super.init(rootView: modified)
        container.controller = self
    }
  
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

edit: Note - this approach is now deprecated in HSSwiftUI. See my subsequent answer for a newer cleaner approach.

0
Confused Vorlon On

I have a new solution.

This relies on an extension to ViewController that provides itself as the input to a ViewBuilder.

I think it is very clean

let controller = ViewController() {
    vc in
    vc.title = "title"
    return SwiftUIView()
}
MyNavigationManager.present(controller)

this relies on the HSSwiftUI package

https://swiftpackageindex.com/ConfusedVorlon/HSSwiftUI