🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [22편] NSPanel과 커스텀 윈도우
왜 AppKit과 혼합해야 하나?
SwiftUI는 강력하지만 아직 AppKit의 모든 기능을 커버하지는 못합니다. macOS 앱을 만들다 보면 다음 상황에서 AppKit이 필요합니다.
- SwiftUI에 없는 컴포넌트:
NSTextView(풍부한 텍스트 편집기),WKWebView(웹뷰),NSTableView(고성능 테이블) - 세밀한 이벤트 제어: 마우스 오버, 드래그, 키보드 이벤트
- 기존 AppKit 코드와의 통합
다행히 SwiftUI는 AppKit 뷰와 양방향으로 통합할 수 있는 브릿지를 제공합니다.
NSViewRepresentable — AppKit 뷰를 SwiftUI에서 사용
NSViewRepresentable은 NSView를 SwiftUI 뷰처럼 사용할 수 있게 해주는 프로토콜입니다.
기본 구조
import SwiftUI
import AppKit
struct MyNSViewWrapper: NSViewRepresentable {
// 1. 어떤 NSView 타입을 감쌀지 선언
typealias NSViewType = SomeNSView
// 2. 뷰 생성 (한 번만 호출)
func makeNSView(context: Context) -> SomeNSView {
let view = SomeNSView()
// 초기 설정
return view
}
// 3. 뷰 업데이트 (SwiftUI 상태 변화 시마다 호출)
func updateNSView(_ nsView: SomeNSView, context: Context) {
// 상태 변화를 뷰에 반영
}
}
실전 예제 — WKWebView 래핑
import SwiftUI
import WebKit
struct WebView: NSViewRepresentable {
let url: URL
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.load(URLRequest(url: url))
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {
// URL이 바뀌면 새로 로드
if nsView.url != url {
nsView.load(URLRequest(url: url))
}
}
}
// 사용
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.apple.com")!)
.frame(minWidth: 800, minHeight: 600)
}
}
Coordinator — AppKit 이벤트를 SwiftUI로 전달
AppKit 뷰의 델리게이트 이벤트를 SwiftUI의 상태로 연결하려면 Coordinator가 필요합니다.
struct WebViewWithProgress: NSViewRepresentable {
@Binding var isLoading: Bool
@Binding var progress: Double
let url: URL
// Coordinator: AppKit 델리게이트 이벤트를 받는 중간자
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebViewWithProgress
init(parent: WebViewWithProgress) {
self.parent = parent
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.isLoading = false
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator // Coordinator를 델리게이트로 설정
webView.load(URLRequest(url: url))
// KVO로 progress 관찰
webView.addObserver(context.coordinator,
forKeyPath: "estimatedProgress",
options: .new,
context: nil)
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) { }
}
데이터 흐름을 정리하면:
AppKit 이벤트 → Coordinator → @Binding 업데이트 → SwiftUI 뷰 재렌더링
NSViewControllerRepresentable
뷰 대신 뷰 컨트롤러를 감쌀 때 사용합니다. 사용 방법은 NSViewRepresentable과 동일합니다.
struct SplitViewController: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> NSSplitViewController {
let splitVC = NSSplitViewController()
let sidebarVC = NSHostingController(rootView: SidebarView())
let contentVC = NSHostingController(rootView: ContentAreaView())
splitVC.addSplitViewItem(NSSplitViewItem(sidebarWithViewController: sidebarVC))
splitVC.addSplitViewItem(NSSplitViewItem(viewController: contentVC))
return splitVC
}
func updateNSViewController(_ vc: NSSplitViewController, context: Context) { }
}
NSHostingController — SwiftUI를 AppKit에 삽입
반대 방향 — AppKit 코드 안에 SwiftUI 뷰를 넣을 때는 NSHostingController를 사용합니다. 이미 앞 편에서 봤지만, 더 자세히 살펴봅니다.
class MainViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// SwiftUI 뷰를 자식 뷰 컨트롤러로 추가
let swiftUIView = MySwiftUIView()
let hostingVC = NSHostingController(rootView: swiftUIView)
addChild(hostingVC)
hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingVC.view)
NSLayoutConstraint.activate([
hostingVC.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
NSEvent — 전역 마우스·키보드 이벤트
AppKit을 사용하면 앱 외부의 이벤트도 감지할 수 있습니다.
class GlobalEventMonitor {
private var monitor: Any?
// 앱 외부 이벤트 감지 (다른 앱 사용 중에도 동작)
func startMonitoring() {
monitor = NSEvent.addGlobalMonitorForEvents(
matching: [.leftMouseDown, .rightMouseDown]
) { event in
print("전역 클릭 감지: \(event.locationInWindow)")
}
}
// 앱 내부 이벤트 감지
func startLocalMonitoring() {
monitor = NSEvent.addLocalMonitorForEvents(
matching: .keyDown
) { event in
print("키 입력: \(event.keyCode)")
return event // nil 반환 시 이벤트 소비 (다른 뷰로 전달 안 함)
}
}
func stopMonitoring() {
if let monitor = monitor {
NSEvent.removeMonitor(monitor)
}
}
}
전역 이벤트 모니터는 앱 포커스 없이도 동작하므로, 전역 단축키나 포커스 외 클릭 감지에 사용됩니다. 단, Accessibility 권한이 필요할 수 있습니다.
NotificationCenter — AppKit 알림
class AppEventObserver {
private var observers: [NSObjectProtocol] = []
func startObserving() {
// 화면 해상도 변경
observers.append(
NotificationCenter.default.addObserver(
forName: NSApplication.didChangeScreenParametersNotification,
object: nil,
queue: .main
) { _ in
print("화면 설정 변경됨")
}
)
// 시스템 다크모드 변경 감지
observers.append(
DistributedNotificationCenter.default().addObserver(
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
object: nil,
queue: .main
) { _ in
print("다크모드 변경됨")
}
)
}
deinit {
observers.forEach { NotificationCenter.default.removeObserver($0) }
}
}
핵심 요약
- NSViewRepresentable: AppKit NSView를 SwiftUI 뷰로 감싸는 브릿지
- makeNSView: 뷰 생성 (한 번), updateNSView: 상태 변화 시 호출
- Coordinator: AppKit 델리게이트 이벤트 → SwiftUI 상태 변환 중간자
- NSHostingController: SwiftUI 뷰를 AppKit에 삽입
- NSEvent.addGlobalMonitorForEvents: 전역 마우스·키보드 이벤트 감지
- 두 프레임워크를 혼합할 때는 SwiftUI를 최대한 활용하고 AppKit은 필요한 곳에만 사용하는 것이 원칙
다음 편에서는 macOS의 Accessibility API와 전역 키보드 단축키를 다룹니다.
🤖 Generated with Claude Code