[Swift/macOS] SwiftUI View의 크기와 동일한 NSViewController 만들기

서론

macOS 앱을 만들던 도중, Storyboard를 통해 아래와 같은 UI를 그리고 있었다.

전체적인 구조는 타이틀 바에 NSToolbar를 구성하고, NSSegmentedControl를 이용해서 탭을 전환하는 UI였는데 런 형태의 UI는 Storyboard로 구성하는 것이 코드로 UI 작성하는 것보다 한 눈에 알아보기도 편하고 NSToolbar 구성하기도 편해서 채택했다. 그런데…

본문

NSTabViewController의 children들은 SwiftUI를 통해 그릴 예정이였다.

그래서 NSHostingController를 사용했는데 자식인 SwiftUI의 View의 크기를 제대로 계산하지 못해 Window의 사이즈가 엉망진창이였다..!

검색해보니까 다들 동일한 이슈를 겪고있었고, 결국 오토 레이아웃으로 해결하길래 자체 Hosting Controller를 만들었다.

/// SwiftUI의 `View` 객체를 호스팅하는 역할을 하는 뷰 컨트롤러입니다.
/// `NSHostingController` 컨트롤러는 SwiftUI의 `View`를 호스팅할 수 있지만 
/// 주어진 `View`의 크기에 맞춰 자동으로 크기 조절을 할 수 없기 때문에 여러가지 불편함이 생길 수 있으므로 `PolkaHostingController` 컨트롤러를 통해 `View`를 호스팅한다면
/// 오토 레이아웃을 통해 주어진 `View`의 크기에 맞춰 컨트롤러의 크기를 조정합니다.
final class PolkaHostingController<HostingView>: NSViewController where HostingView: View {
    private var controller: NSHostingController<HostingView>!
    private let hostingView: HostingView
    
    init(hostingView: HostingView) {
        self.hostingView = hostingView
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.translatesAutoresizingMaskIntoConstraints = false
        
        controller = NSHostingController(rootView: hostingView)
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        
        addChild(controller)
        view.addSubview(controller.view)
        
        NSLayoutConstraint.activate([
            controller.view.leftAnchor.constraint(equalTo: view.leftAnchor),
            controller.view.topAnchor.constraint(equalTo: view.topAnchor),
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor),
            controller.view.heightAnchor.constraint(equalTo: view.heightAnchor)
        ])
    }
}

위의 Hosting Controller를 만들었으면 아래와 같이 사용하면 된다.

private var childViewControllers: [NSViewController] = [
    PolkaHostingController(hostingView: ChildView(segment: 1)),
    PolkaHostingController(hostingView: ChildView(segment: 2)),
    PolkaHostingController(hostingView: ChildView(segment: 3))
]

for childViewController in childViewControllers {
    tabViewController?.addChild(childViewController)
}

이제 SwiftUI View에서 정의한 크기만큼 제대로 보여진다.

결론

Hosting Controller + SwiftUI View와 함께라면 어떠한 복잡한 View라도 쉽게 그릴 수 있을거라 든든..

아직 macOS는 SwiftUI에서 Window에 대한 자료도 iOS에 비해 많이 없고, 있어도 background modifier를 통해 NSWindow에 접근해서 수정하는 방향이라 당분간은 Hosting Controller + SwiftUI View로도 충분할 것 같다.

참고 링크

https://www.rk-k.com/archives/7247