Как создать кастомную панель заголовка для приложения macOS в SwiftUI с выровненными элементами управления окном?
Я разрабатываю приложение для macOS с использованием SwiftUI и хочу создать единую панель заголовка, где мои иконки приложения идеально выровнены с кнопками управления окном (закрыть, свернуть, развернуть). В настоящее время, когда я использую .windowStyle(.hiddenTitleBar), пространство для панели заголовка все равно резервируется, что приводит к тому, что мой контент появляется под ней вместо использования всего пространства окна.
Текущая реализация:
import SwiftUI
import AppKit
@main
struct MyApp: App {
var body: some Scene {
DocumentGroup(newDocument: MyDocumentApp()) { file in
ContentView(document: file.$document, rootURL: file.fileURL)
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.defaultSize(width: 1400, height: 900)
}
}
Какой правильный подход к созданию кастомной панели заголовка в SwiftUI, который позволяет точное выравнивание кастомных иконок приложения с нативными элементами управления окном?
Создание кастомной строки заголовка в SwiftUI
Создание кастомной строки заголовка в SwiftUI с правильно выровненными элементами управления окном требует непосредственной работы с NSWindow через NSWindowController, поскольку встроенные параметры стилизации окон в SwiftUI имеют ограничения. Ключевым моментом является создание прозрачной строки заголовка и точное позиционирование вашего кастомного контента относительно нативных элементов управления окном.
Содержание
- Понимание проблемы
- Подход с использованием NSWindowController
- Реализация NSVisualEffectView
- Правильное выравнивание элементов управления окном
- Пример полной реализации
- Альтернативные подходы
- Устранение распространенных проблем
Понимание проблемы
Когда вы используете .windowStyle(.hiddenTitleBar) в SwiftUI, область строки заголовка остается зарезервированной для системных целей, даже если она визуально скрыта. Именно поэтому ваш контент отображается под ней, а не использует всего пространство окна. Согласно документации Apple Developer, строка заголовка выполняет определенные системные функции, которые нельзя полностью удалить с помощью модификаторов SwiftUI.
Сложность заключается в создании кастомной строки заголовка, которая:
- Выглядит единообразно с дизайном вашего приложения
- Поддерживает правильную функциональность элементов управления окном
- Позволяет точно выравнивать кастомные элементы с системными элементами управления
- Не мешает поведению управления окнами в macOS
Подход с использованием NSWindowController
Наиболее надежный метод включает создание подкласса NSWindowController и непосредственную настройку окна. Как показано в обсуждении на Reddit, этот подход дает полный контроль над внешним видом и поведением окна.
import Cocoa
import SwiftUI
class CustomWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Настройка окна для кастомной строки заголовка
window?.titleVisibility = .hidden
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
window?.isMovableByWindowBackground = true
// Удаление стандартной строки заголовка
window?.styleMask.remove(.titled)
// Настройка кастомного представления контента
let hostingView = NSHostingView(rootView: ContentView())
window?.contentView = hostingView
}
}
Этот подход позволяет начать с чистого листа, сохраняя доступ к базовым функциям окна.
Реализация NSVisualEffectView
Для более совершенной строки заголовка с эффектами размытия используйте NSVisualEffectView. Как показано в примере на GitHub, это обеспечивает лучшую визуальную интеграцию со стилем macOS.
import Cocoa
import SwiftUI
class VisualEffectWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Создание визуального эффекта
let visualEffect = NSVisualEffectView()
visualEffect.blendingMode = .behindWindow
visualEffect.state = .active
visualEffect.material = .underWindowBackground
// Настройка окна
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
// Создание хостинг-представления для вашего контента SwiftUI
let hostingView = NSHostingView(rootView: ContentView())
// Добавление визуального эффекта как основного представления контента
window?.contentView = visualEffect
visualEffect.addSubview(hostingView)
// Настройка хостинг-представления для заполнения окна
hostingView.translatesAutoresizingMaskIntoConstraints = false
hostingView.topAnchor.constraint(equalTo: visualEffect.topAnchor).isActive = true
hostingView.leadingAnchor.constraint(equalTo: visualEffect.leadingAnchor).isActive = true
hostingView.trailingAnchor.constraint(equalTo: visualEffect.trailingAnchor).isActive = true
hostingView.bottomAnchor.constraint(equalTo: visualEffect.bottomAnchor).isActive = true
}
}
NSVisualEffectView обеспечивает полупрозрачный фон, который естественно интегрируется с языком дизайна macOS, позволяя вашему кастомному контенту строки заголовка отображаться поверх него.
Правильное выравнивание элементов управления окном
Выравнивание вашего кастомного контента с элементами управления окном требует знания их точного позиционирования. Согласно документации по стилям NSWindow, элементы управления окном появляются в правом верхнем углу с определенным позиционированием.
// В теле вашего представления SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
// Ваш основной контент
Color.clear
// Кастомный оверлей строки заголовка
VStack(spacing: 0) {
// Область строки заголовка
HStack {
// Ваши иконки/контент приложения слева
Image(systemName: "star.fill")
.font(.title2)
.padding(.leading, 20)
Spacer()
// Область элементов управления окном - выравнивание с системными элементами
HStack(spacing: 12) {
CustomButton(icon: "minus") { /* действие сворачивания */ }
CustomButton(icon: "plus") { /* действие разворачивания */ }
CustomButton(icon: "xmark") { /* действие закрытия */ }
}
.padding(.trailing, 12)
}
.frame(height: 32)
// Линия разделителя
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(height: 1)
Spacer()
}
.background(Color.clear)
}
}
}
struct CustomButton: View {
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.system(size: 12, weight: .medium))
.frame(width: 12, height: 12)
}
.buttonStyle(PlainButtonStyle())
.onHover { isHovering in
// Добавление эффектов при наведении
}
}
}
Ключевым моментом является использование точно такого же отступа и интервала, как у системных элементов управления. Правый отступ в 12 пунктов соответствует стандартному системному интервалу для элементов управления окном.
Пример полной реализации
Вот полный пример, который объединяет все подходы:
import SwiftUI
import AppKit
// Кастомный контроллер окна
class CustomWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Настройка окна
window?.titleVisibility = .hidden
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
window?.isMovableByWindowBackground = true
// Удаление стандартного стиля строки заголовка
window?.styleMask.remove(.titled)
// Создание визуального эффекта для фона
let visualEffect = NSVisualEffectView()
visualEffect.blendingMode = .behindWindow
visualEffect.state = .active
visualEffect.material = .underWindowBackground
// Настройка кастомного контента
let hostingView = NSHostingView(rootView: ContentView())
window?.contentView = visualEffect
visualEffect.addSubview(hostingView)
// Настройка ограничений
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: visualEffect.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: visualEffect.leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: visualEffect.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: visualEffect.bottomAnchor)
])
}
}
// Кастомное представление контента
struct ContentView: View {
var body: some View {
ZStack {
// Фон
Color.clear
VStack(spacing: 0) {
// Кастомная строка заголовка
titleBarView
// Основная область контента
ZStack {
Color.red.opacity(0.1)
.cornerRadius(8)
.padding()
Text("Основная область контента")
.font(.title)
.padding()
}
}
}
.edgesIgnoringSafeArea(.all)
}
private var titleBarView: some View {
HStack {
// Контент слева
HStack(spacing: 8) {
Image(systemName: "star.fill")
.font(.title2)
.padding(.leading, 20)
Text("Мое приложение")
.font(.headline)
}
Spacer()
// Справа - элементы управления окном
HStack(spacing: 12) {
CustomWindowButton(icon: "minus", action: { /* сворачивание */ })
CustomWindowButton(icon: "plus", action: { /* разворачивание */ })
CustomWindowButton(icon: "xmark", action: { /* закрытие */ })
}
.padding(.trailing, 12)
}
.frame(height: 32)
.background(Color.clear)
}
}
// Кастомная кнопка окна
struct CustomWindowButton: View {
let icon: String
let action: () -> Void
@State private var isHovering = false
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.system(size: 11, weight: .medium))
.frame(width: 12, height: 12)
.foregroundColor(isHovering ? .primary : .secondary)
}
.buttonStyle(PlainButtonStyle())
.onHover { hovering in
isHovering = hovering
}
}
}
// Структура приложения
@main
struct CustomTitleBarApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.defaultSize(width: 800, height: 600)
.windowToolbarStyle(.unified)
}
}
Альтернативные подходы
Использование TitleBarWindowStyle
Согласно документации Apple Developer, вы можете использовать TitleBarWindowStyle для более интегрированного подхода:
.windowStyle(.titleBar)
.toolbar {
ToolbarItem(placement: .automatic) {
// Кастомный контент панели инструментов
}
}
Использование NavigationSplitView
Для приложений с навигацией по боковой панели, статья Swift and AppKit Tips предлагает использовать NavigationSplitView с кастомными элементами панели инструментов:
NavigationSplitView {
// Контент боковой панели
} detail: {
// Основной контент
}
.toolbar {
ToolbarItem(placement: .navigation) {
Text("Кастомный заголовок")
.font(.system(size: 20, weight: .regular))
}
}
Устранение распространенных проблем
Элементы управления окном не реагируют
Если ваши кастомные элементы управления окном не реагируют, убедитесь, что вы не мешаете тестированию попаданий (hit testing). Добавьте правильный размер фрейма:
CustomWindowButton(icon: "xmark", action: { /* закрытие */ })
.frame(width: 12, height: 12)
Строка заголовка не прозрачна
Убедитесь в правильной настройке:
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
Контент не заполняет окно
Проверьте ваши ограничения и убедитесь, что представление контента заполняет все окно:
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: visualEffect.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: visualEffect.leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: visualEffect.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: visualEffect.bottomAnchor)
])
Источники
- Stack Overflow - Как создать кастомную строку заголовка?
- Документация Apple Developer - TitleBarWindowStyle
- Reddit - Прозрачное окно с прозрачной строкой заголовка
- GitHub - Стили NSWindow
- Стили NSWindow - Реализация визуального эффекта
- Level Up Coding - SwiftUI/MacOS Кастомизация
- Medium - Кастомизация окна настроек SwiftUI
- Документация Apple Developer - Кастомизация стилей окон
Заключение
Создание кастомной строки заголовка в SwiftUI с правильно выровненными элементами управления окном требует понимания ограничений встроенных параметров стилизации окон в SwiftUI и непосредственной работы с компонентами AppKit. Ключевые выводы:
- Используйте NSWindowController для полного контроля над внешним видом и поведением окна
- Реализуйте NSVisualEffectView для правильной визуальной интеграции с macOS
- Точно выравнивайте элементы управления окном, используя тот же интервал и отступ, что и системные элементы
- Настраивайте свойства окна, такие как
titlebarAppearsTransparentиfullSizeContentView - Тщательно тестируйте, чтобы убедиться, что все элементы управления окном функционируют правильно
Следуя этим подходам, вы можете создать единую строку заголовка, которая бесшовно интегрирует ваш кастомный контент приложения с нативными элементами управления окном macOS, обеспечивая профессиональный и последовательный пользовательский опыт.