How to create UIHostingController and pass environmentObject?

Hi,

I'm trying to create a ViewController that extends UIHostingController and host an AttachmentView there. Also, I would like to pass an environment object to the AttachmentView, but I get a compile error because the type of rootView does not match what I specify in UIHostingController.

How can I get this code to compile?

Thanks!

Accepted Reply

When you show some code, you should better show it as text using Code Block. With easily testable code shown, more readers would be involved solving your issue.

(Of course, images are very useful for some additional info.)


Modifiers like environmentObject(_:) would make an instance of a certain private type, like ModifiedContent<AttachmentView, _EnvironmentKeyWritingModifier<AttachmentViewModel?>> (may change in versions of SwiftUI).

So, you cannot specify the generic parameter of UIHostingController as AttachmentView, neither cannot use ModifiedContent<...> explicitly as Swift compiler hides details of some View.

It seems you have not many options:

  • Avoid subclassing UIHostingController, that requires the generic parameter specified.
  • Avoid any modifiers.

With the latter, you can write something like this:

class AttachmentViewController: UIHostingController<AttachmentView> {
    let model = AttachmentViewModel()
    
    init() {
        let attachmentView = AttachmentView(model: model)
        super.init(rootView: attachmentView)
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

struct AttachmentView: View {
    var model: AttachmentViewModel
    
    var body: some View {
        InnerAttachemntView()
            .environmentObject(model)
    }
}
  • Thank you for your answer! I think I'll go with the second option!

Add a Comment

Replies

When you show some code, you should better show it as text using Code Block. With easily testable code shown, more readers would be involved solving your issue.

(Of course, images are very useful for some additional info.)


Modifiers like environmentObject(_:) would make an instance of a certain private type, like ModifiedContent<AttachmentView, _EnvironmentKeyWritingModifier<AttachmentViewModel?>> (may change in versions of SwiftUI).

So, you cannot specify the generic parameter of UIHostingController as AttachmentView, neither cannot use ModifiedContent<...> explicitly as Swift compiler hides details of some View.

It seems you have not many options:

  • Avoid subclassing UIHostingController, that requires the generic parameter specified.
  • Avoid any modifiers.

With the latter, you can write something like this:

class AttachmentViewController: UIHostingController<AttachmentView> {
    let model = AttachmentViewModel()
    
    init() {
        let attachmentView = AttachmentView(model: model)
        super.init(rootView: attachmentView)
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

struct AttachmentView: View {
    var model: AttachmentViewModel
    
    var body: some View {
        InnerAttachemntView()
            .environmentObject(model)
    }
}
  • Thank you for your answer! I think I'll go with the second option!

Add a Comment

How do you get this working with previews though?

Presumable with previews you may want to pass in dummy environment objects/services:

MyView().environment( \.database, database )

whereas in the runnable app you might want the hosting controller to pass along other services.

Specifying it in the view definition itself doesn't allow you to pass it in from the outside.

UPDATE: Ended up creating a wrapper view MyView_RUNTIME which has the runtime dependencies specified.

This way in the #preview block you can instantiate MyView with your dummy environment variables, while in the UIHostingController you can instantiate the real ones.

ie:

// use within UIHostingController (ie: super.init( coder: aDecoder, rootView: DebugView_RUNTIME() ))
struct MyView_RUNTIME : View {
	var body: some View {
		MyView().environment( \.database, ServiceLocator.database ) // NOTE: real DB registered during the bootstrapping of the runtime app
	}
}


// use within previews
#Preview {
	MyView().environment( \.database, MyDatabase( inMemory: true )) // in-memory, dummy database
}