🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [21편] MenuBarExtra와 메뉴바 앱
NSWindow와 NSPanel의 차이
macOS에는 두 가지 기본 윈도우 타입이 있습니다.
- NSWindow: 일반 앱 윈도우. 타이틀바, 닫기/최소화/최대화 버튼이 있고, 다른 앱으로 전환해도 화면에 유지됩니다.
- NSPanel: 보조 윈도우. 주 윈도우 앞에 띄우는 팔레트, 인스펙터, 알림창 등에 사용됩니다.
NSWindow의 서브클래스입니다.
메뉴바 앱의 팝업창, 노치 오버레이 같은 특수 UI는 대부분 NSPanel을 커스터마이징해서 만듭니다.
NSPanel 기본 사용법
import AppKit
import SwiftUI
class FloatingPanel: NSPanel {
init(contentView: some View) {
super.init(
contentRect: NSRect(x: 0, y: 0, width: 320, height: 200),
styleMask: [
.nonactivatingPanel, // 패널이 앱을 활성화하지 않음
.titled,
.closable,
.fullSizeContentView // 콘텐츠가 타이틀바 영역까지 확장
],
backing: .buffered,
defer: false
)
// SwiftUI 뷰 연결
self.contentViewController = NSHostingController(rootView: contentView)
// 항상 다른 윈도우 위에 표시
self.level = .floating
// 화면 중앙에 위치
self.center()
// 타이틀바를 투명하게
self.titlebarAppearsTransparent = true
self.titleVisibility = .hidden
// 그림자 활성화
self.hasShadow = true
// 배경 투명화 (SwiftUI로 커스텀 배경 그리기)
self.isOpaque = false
self.backgroundColor = .clear
}
}
패널 표시와 숨기기
class PanelController {
private var panel: FloatingPanel?
func show() {
if panel == nil {
panel = FloatingPanel(contentView: MyPanelView())
}
panel?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
func hide() {
panel?.orderOut(nil)
}
func toggle() {
if panel?.isVisible == true {
hide()
} else {
show()
}
}
}
윈도우 레벨(Window Level)
macOS에서 윈도우는 레벨(Z-order)에 따라 어느 윈도우 위에 표시될지 결정됩니다.
// 일반 윈도우 레벨 (기본값)
window.level = .normal
// 도크 위에 표시
window.level = .floating
// 스크린 세이버 위에 표시
window.level = .screenSaver
// 상태바(메뉴바) 위에 표시
window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.statusWindow)) + 1)
// 모든 윈도우 위 (주의해서 사용)
window.level = .popUpMenu
메뉴바 오버레이 UI처럼 항상 화면 최상단에 표시하려면 적절한 레벨을 선택해야 합니다.
커스텀 모양 — 둥근 모서리와 그림자
struct RoundedPanelView: View {
var body: some View {
VStack(spacing: 12) {
Text("알림")
.font(.headline)
Text("작업이 완료되었습니다.")
.foregroundColor(.secondary)
Button("확인") { }
.buttonStyle(.borderedProminent)
}
.padding(20)
.background {
RoundedRectangle(cornerRadius: 16)
.fill(.regularMaterial) // 반투명 배경
.shadow(radius: 20)
}
.padding() // 그림자가 잘리지 않도록 여백
}
}
// 패널을 배경 투명으로 설정해야 모양이 보임
// panel.isOpaque = false
// panel.backgroundColor = .clear
Visual Effect (반투명 배경)
// SwiftUI .regularMaterial 계층
struct BlurredBackground: View {
var body: some View {
ZStack {
// 시스템 블러 배경
Rectangle()
.fill(.ultraThinMaterial)
// 콘텐츠
VStack {
Text("투명 배경 패널")
.font(.headline)
}
.padding()
}
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.ultraThinMaterial, .thinMaterial, .regularMaterial, .thickMaterial 등 다양한 두께의 블러 재질을 사용할 수 있습니다. macOS의 Control Center나 Spotlight 같은 시스템 UI에서 볼 수 있는 효과입니다.
포커스 잃으면 닫히는 패널
메뉴바 팝업처럼 다른 곳을 클릭하면 자동으로 닫히게 하려면:
class AutoDismissPanel: NSPanel {
// 포커스를 잃으면 닫힘
override var canBecomeKey: Bool { true }
init(contentView: some View) {
super.init(
contentRect: .zero,
styleMask: [.nonactivatingPanel, .fullSizeContentView],
backing: .buffered,
defer: false
)
self.contentViewController = NSHostingController(rootView: contentView)
self.level = .floating
self.isOpaque = false
self.backgroundColor = .clear
self.hasShadow = true
// 포커스 잃으면 자동으로 닫힘
NotificationCenter.default.addObserver(
self,
selector: #selector(windowDidResignKey),
name: NSWindow.didResignKeyNotification,
object: self
)
}
@objc func windowDidResignKey() {
self.orderOut(nil)
}
}
화면 좌표계 이해
macOS와 iOS의 좌표계는 반대입니다.
// macOS: 좌하단이 원점 (0,0)
// iOS/SwiftUI: 좌상단이 원점 (0,0)
// 화면 크기 가져오기
let screenFrame = NSScreen.main?.frame ?? .zero
let screenWidth = screenFrame.width
let screenHeight = screenFrame.height
// 화면 중앙에 윈도우 배치
let windowSize = CGSize(width: 400, height: 300)
let x = (screenWidth - windowSize.width) / 2
let y = (screenHeight - windowSize.height) / 2 // 하단 기준
window.setFrameOrigin(NSPoint(x: x, y: y))
// 또는 간단하게
window.center()
메뉴바 아래에 패널 표시
func showBelowMenuBar(panel: NSPanel, button: NSStatusBarButton) {
// 버튼의 화면 위치 계산
guard let buttonWindow = button.window else { return }
let buttonFrame = buttonWindow.convertToScreen(button.frame)
// 패널 크기
let panelSize = panel.frame.size
// 메뉴바 아이콘 바로 아래에 배치
let x = buttonFrame.midX - panelSize.width / 2
let y = buttonFrame.minY - panelSize.height
panel.setFrameOrigin(NSPoint(x: x, y: y))
panel.makeKeyAndOrderFront(nil)
}
노치 영역 활용 (Mac 노치 모델)
MacBook Pro 노치 모델(2021~)에서는 메뉴바가 노치 양옆으로 나뉩니다. 노치 영역 자체는 앱이 직접 사용할 수 없지만, 노치 바로 아래 영역에 패널을 표시하는 앱들이 있습니다.
// 노치 영역 크기 확인 (safeAreaInsets 활용)
if let screen = NSScreen.main {
// 메뉴바 높이
let menuBarHeight = NSApp.mainMenu?.menuBarHeight ?? 24
// 노치가 있는 경우 추가 safeArea
if #available(macOS 12.0, *) {
let safeArea = screen.safeAreaInsets
let notchHeight = safeArea.top
print("노치 높이: \(notchHeight)")
}
}
노치 바로 아래를 가리는 형태의 UI는 macOS에서 독특한 UX를 제공할 수 있습니다. 단, 다른 메뉴바 아이콘을 가리지 않도록 주의해야 합니다.
핵심 요약
- NSPanel: 보조 윈도우. NSWindow의 서브클래스로 팝업·팔레트·오버레이에 적합
- .nonactivatingPanel: 패널 표시 시 현재 앱 포커스를 빼앗지 않음
- window.level: 윈도우 Z-order. .floating으로 항상 위에 표시
- isOpaque = false + backgroundColor = .clear: 투명 배경으로 커스텀 모양 구현
- .regularMaterial / .ultraThinMaterial: 시스템 블러 반투명 배경
- NSWindow.didResignKeyNotification: 포커스를 잃으면 자동 닫힘 구현
- macOS 좌표계는 좌하단이 원점 (iOS와 반대)
다음 편에서는 AppKit과 SwiftUI를 혼합해서 사용하는 방법인 NSViewRepresentable과 NSViewControllerRepresentable을 배웁니다.
🤖 Generated with Claude Code