Search code examples
swiftmacosswiftuiappkit

Passing an EnvronmentObject to NSHostingControllers


I'm using a custom SwiftUI View using NSSplitViewController that takes in ViewBuilders for the two subviews. My problem is any change in state of the environment doesn't propagate to the subviews inside SplitView, but propagates to another TextView in ContentView

import SwiftUI

class AppEnvironment : ObservableObject {
    @Published var value: String = "default"
}

struct ContentView: View {
    @EnvironmentObject var env : AppEnvironment

    var body: some View {
        HStack {
            Button(action: {
                self.env.value = "new value"
            }, label: { Text("Change value") })
            Text(self.env.value)
                GeometryReader { geometry in
                    SplitView(master: {
                        Text("master")
                            .background(Color.yellow)
                    }, detail: {
                        HStack {
                            Text(self.env.value)                        }
                            .background(Color.orange)
                    }).frame(width: geometry.size.width, height: geometry.size.height)
                }
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

//
// Source: https://gist.github.com/HashNuke/f8895192fff1f275e66c30340f304d80
//
struct SplitView<Master: View, Detail: View>: View {
    var master: Master
    var detail: Detail

    init(@ViewBuilder master: () -> Master, @ViewBuilder detail: () -> Detail) {
        self.master = master()
        self.detail = detail()
    }

    var body: some View {
        let viewControllers = [NSHostingController(rootView: master), NSHostingController(rootView: detail)]
        return SplitViewController(viewControllers: viewControllers)
    }
}

struct SplitViewController: NSViewControllerRepresentable {
    var viewControllers: [NSViewController]

    private let splitViewResorationIdentifier = "com.company.restorationId:mainSplitViewController"

    func makeNSViewController(context: Context) -> NSViewController {
        let controller = NSSplitViewController()

        controller.splitView.dividerStyle = .thin
        controller.splitView.autosaveName = NSSplitView.AutosaveName(splitViewResorationIdentifier)
        controller.splitView.identifier = NSUserInterfaceItemIdentifier(rawValue: splitViewResorationIdentifier)
        let vcLeft = viewControllers[0]
        let vcRight = viewControllers[1]
        vcLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
        vcRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 70).isActive = true
        let sidebarItem = NSSplitViewItem(contentListWithViewController: vcLeft)
        sidebarItem.canCollapse = false

        // I'm not sure if this has any impact
        // controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 800, height: 800))
        controller.addSplitViewItem(sidebarItem)

        let mainItem = NSSplitViewItem(viewController: vcRight)
        controller.addSplitViewItem(mainItem)

        return controller
    }

    func updateNSViewController(_ nsViewController: NSViewController, context: Context) {
        print("should update splitView")
    }
}

Solution

  • Yes, in such case EnvironmentObject is not injected automatically. The solution would be to separate content into designated views (for better design) and inject environment object manually.

    Here it is

    Text(self.env.value)
        GeometryReader { geometry in
            SplitView(master: {
                MasterView().environmentObject(self.env)
            }, detail: {
                HStack {
                    DetailView().environmentObject(self.env)
            }).frame(width: geometry.size.width, height: geometry.size.height)
        }
    

    and views

    struct MasterView: View {
        var body: some View {
            Text("master")
                .background(Color.yellow)
        }
    }
    
    struct DetailView: View {
        var body: some View {
            HStack {
                Text(self.env.value)                        }
                .background(Color.orange)
        }
    }