본문 바로가기

SwiftUI macOS 앱에서 타이틀 바와 툴바가 통합된 윈도우 구현하기

기술적인 이야기/애플 플랫폼 개발 2020. 11. 12.
반응형

이 글의 목표는 SwiftUI를 사용하는 macOS 앱 프로젝트에서 아래와 같은 윈도우를 구현하는 것입니다.

나눠서 설명했더니 글이 좀 길어졌습니다. 결론만 보시려면 글 하단에 정리한 전체 코드를 참고하시면 될 것 같습니다.

기본 프로젝트

사용하는 프로젝트는 macOS App으로 SwiftUI를 사용하도록 설정했습니다. 당연히 이게 조건이니깐요.

기본 프로젝트는 위와 같은 모양으로 실행됩니다.

Unified Title and Toolbar

프로젝트 생성 시 보일러플레이트 코드가 생성된 AppDelegate.swift 파일을 열어보면 NSWindow 인스턴스를 생성하는 코드가 있습니다. 여기에서 styleMask를 설정할 수 있습니다. 여기에 아래와 같이 .unifiedTitleAndToolbar 플래그를 추가합니다.

window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
    styleMask: [.titled, 
                .unifiedTitleAndToolbar, 
                .closable, 
                .miniaturizable, 
                .resizable, 
                .fullSizeContentView],
    backing: .buffered, defer: false)

어찌 보면 이 글의 핵심 주제일지도 모를 부제목입니다. 그런데 실행을 시켜보면 이전과 동일합니다.

왜 그런지는 문서를 보면 알 수 있습니다. 저 플래그는 원래 예전에는 꼭 필요한 옵션이었는데 지금은 없어도 그냥 기본적으로 있는 것처럼 동작합니다. 따라서 이 항목은 생략해도 무관합니다.

하지만 전 의식적으로 이 코드를 넣기로 했습니다. 명확한 것이 좋아서요.

타이틀 숨기기

아래의 코드 한 줄을 추가하려 타이틀을 숨겨봅시다.

window.titleVisibility = .hidden

이걸 추가하면 타이틀바가 사라지...지는 않고 그대로입니다.

그야 원래부터 타이틀을 설정하지 않아서 차이를 못 느끼는 것뿐이지요.

타이틀바를 툴바 크기로 키우기

이번에는 아래 코드를 추가하여 윈도우에 툴바를 하나 생성해 줍니다.

window.toolbar = NSToolbar()

이번에는 뭔가 변화가 생깁니다.

뭔가 타이틀바가 원하는 느낌에 다가간 듯이 보입니다.

본격적으로 툴바의 내용을 채우기

사실 위에서 생성한 툴바 자체는 그냥 더미입니다. 타이틀 바의 크기를 툴바와 비슷하게 맞추기 위해 의도적으로 넣은 것이지요.

우리는 SwiftUI로 구현한 우리의 툴바가 필요합니다. 전 아래와 같이 대충 텍스트를 박은 뷰 하나를 툴바 용도로 따로 만들었습니다.

struct MyToolbarView: View {
    var body: some View {
        Text("This is Toolbar!")
    }
}

이 뷰를 툴바에 넣기 위해서는 몇 가지 준비가 더 필요합니다.

우선 위의 툴바 인스턴스를 만들어야겠지요.

let toolbarView = MyToolbarView()

그다음엔 이걸 AppKit에 실어줄 호스팅 뷰를 만들어줍니다. 사이즈도 딱 맞게 조절해주고요.

let accessoryHostingView = NSHostingView(rootView: toolbarView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize

만든 호스팅 뷰를 타이틀 바에 붙이기 위해 타이틀 바 액세서리로 만들어줍니다.

let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
titlebarAccessory.layoutAttribute = .leading

위에서 layoutAttribute는 원하는 대로 설정하면 됩니다. .leading이면 좌측에 붙고 .tailing이면 우측에 붙겠지요.

이제 마지막으로 이 액세서리 뷰 컨트롤러를 타이틀 바 액세서리로 붙여줍니다.

window.addTitlebarAccessoryViewController(titlebarAccessory)

이제 이러면 짜잔~

macOS Catalina에서는 위처럼 뭔가 이상합니다. 텍스트가 아름답게 보이지 않고 아래에 쳐져있네요. 간당간당 매달려서 말이죠.

위치 조절로 마무리하기

안타깝게도 카탈리나 이하의 macOS에서는 아직 SwiftUI 뷰와 타이틀 액세서리 간에 자동으로 레이아웃을 맞춰주는 기능이 없는 것 같습니다. 그러니 억지로 패딩을 끼워 넣어서 맞추는 식으로 해결해 봅시다.

let toolbarView = MyToolbarView()
    .padding([.top, .leading, .trailing], 16)
    .padding(.bottom, -20)
    .edgesIgnoringSafeArea(.top)

이렇게 세팅하니 아래와 같은 결과를 볼 수 있었습니다.

앞서 이야기했지만 macOS Big Sur의 경우는 위의 조정 코드가 필요 없습니다. 오히려 이 코드를 넣으면 반대로 아래로 쳐지는 효과가 나타난다는 것에 주의합시다.

이제 남은 일은 원하는 대로 툴바를 꾸미고 구현하는 것뿐이겠네요.

최종적으로 완성된 AppDelegate.swift 파일의 applicationDidFinishLaunching 메서드의 모습은 아래와 같습니다.

let contentView = ContentView()

// NOTE: Big Sur에서는 별도의 padding이나 safe area 조정이 필요 없음
let toolbarView = MyToolbarView()
    .padding([.top, .leading, .trailing], 16)
    .padding(.bottom, -20)
    .edgesIgnoringSafeArea(.top)

let accessoryHostingView = NSHostingView(rootView: toolbarView)
accessoryHostingView.frame.size = accessoryHostingView.fittingSize

let titlebarAccessory = NSTitlebarAccessoryViewController()
titlebarAccessory.view = accessoryHostingView
titlebarAccessory.layoutAttribute = .leading

window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
    styleMask: [.titled, 
                .unifiedTitleAndToolbar, 
                .closable, 
                .miniaturizable, 
                .resizable, 
                .fullSizeContentView],
    backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.titleVisibility = .hidden
window.toolbar = NSToolbar()
window.addTitlebarAccessoryViewController(titlebarAccessory)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)

이 내용이 완벽한 것은 아니고 SwiftUI나 Xcode가 업데이트되면서 바뀔 수도 있다는 점을 미리 알아둡시다. 참고로 이 글을 쓰는 시점의 Xcode 버전은 12.x입니다.

728x90
반응형

댓글