| name | macos |
| description | Implementa funcionalidades específicas de macOS em apps SwiftUI — Scenes (Settings, MenuBarExtra, WindowGroup, Window, UtilityWindow, DocumentGroup), estilos de janela e toolbar, NavigationSplitView, Inspector, Commands, HSplitView, Table, operações de arquivo, drag-drop entre apps, e interop AppKit. Use quando o usuário precisar de qualquer API exclusiva ou com comportamento diferenciado no macOS.
|
| argument-hint | Informe o modo desejado:
• scenes — criar/configurar cenas do app (Settings, MenuBarExtra, WindowGroup, Window, UtilityWindow, DocumentGroup)
• window — toolbar styles, sizing, Inspector, NavigationSplitView, Commands/atalhos de teclado
• views — HSplitView/VSplitView, Table, PasteButton, operações de arquivo, drag-drop, NSViewRepresentable
Se não especificado, pergunte ao usuário qual modo usar.
|
Skill: macOS
Você é um especialista em APIs SwiftUI específicas do macOS.
Regra obrigatória: todas as APIs exclusivas de macOS DEVEM estar dentro de #if os(macOS) em projetos multiplataforma.
Identificação do Modo
Se $ARGUMENTS não especificar o modo, pergunte:
"Qual aspecto macOS você quer implementar?
- scenes — configurar cenas do app (Settings, MenuBarExtra, Window…)
- window — estilos de toolbar/janela, Inspector, NavigationSplitView, Commands
- views — HSplitView, Table, arquivos, drag-drop, NSViewRepresentable"
Modo: scenes
Antes de criar
- Leia o
@main App atual para entender as cenas existentes.
- Identifique qual cena adicionar ou ajustar.
- Wrap sempre em
#if os(macOS) quando o projeto é multiplataforma.
Referência de Cenas
| Cena | Disponibilidade | macOS-only? | Uso |
|---|
WindowGroup | macOS 11.0+ | Não | Múltiplas janelas, tabs, Window menu automático |
Window | macOS 13.0+ | Não | Janela singleton; app sai quando fecha (se for única) |
UtilityWindow | macOS 15.0+ | Sim | Paleta flutuante; recebe FocusedValues da janela ativa |
Settings | macOS 11.0+ | Sim | Janela de preferências (Cmd+,) |
MenuBarExtra | macOS 13.0+ | Sim | Ícone/menu persistente na barra de menus |
DocumentGroup | macOS 11.0+ | Não | Menus File automáticos; múltiplos documentos |
Settings
#if os(macOS)
Settings {
TabView {
Tab("General", systemImage: "gear") { GeneralSettingsView() }
Tab("Advanced", systemImage: "star") { AdvancedSettingsView() }
}
.scenePadding()
.frame(maxWidth: 350, minHeight: 100)
}
#endif
Abrir programaticamente (macOS 14.0+):
struct OpenSettingsButton: View {
@Environment(\.openSettings) private var openSettings
var body: some View {
Button("Preferências") { openSettings() }
}
}
SettingsLink (macOS 14.0+):
SettingsLink {
Label("Preferências", systemImage: "gear")
}
MenuBarExtra
Estilo menu (dropdown):
#if os(macOS)
MenuBarExtra("MyApp", systemImage: "hammer") {
Button("Ação") { }
Divider()
Button("Sair") { NSApplication.shared.terminate(nil) }
}
#endif
Estilo window (painel popover):
#if os(macOS)
MenuBarExtra("Status", systemImage: "chart.bar") {
DashboardView()
.frame(width: 240)
}
.menuBarExtraStyle(.window)
#endif
App somente na barra de menus:
- Use
MenuBarExtra como única cena
- Adicione
LSUIElement = YES no Info.plist para ocultar o ícone no Dock
- O app se encerra automaticamente se o usuário remover o ícone da barra
Visibilidade controlável:
@AppStorage("showMenuBarExtra") private var showMenuBarExtra = true
MenuBarExtra("Status", systemImage: "bolt", isInserted: $showMenuBarExtra) { ... }
WindowGroup (macOS)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
WindowGroup("Detalhe", for: Item.ID.self) { $itemID in
ItemDetailView(itemID: itemID)
}
}
}
struct OpenButton: View {
var item: Item
@Environment(\.openWindow) private var openWindow
var body: some View {
Button("Abrir Detalhe") {
openWindow(value: item.id)
}
}
}
Window (singleton)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
Window("Connection Doctor", id: "connection-doctor") {
ConnectionDoctorView()
}
}
}
struct OpenDoctorButton: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
Button("Connection Doctor") {
openWindow(id: "connection-doctor")
}
}
}
Prefira WindowGroup para a cena principal. Use Window apenas para
janelas suplementares singleton.
UtilityWindow (macOS 15.0+)
Paleta flutuante que recebe FocusedValues da janela principal ativa.
#if os(macOS)
UtilityWindow("Informações", id: "photo-info") {
PhotoInfoViewer()
}
#endif
struct PhotoInfoViewer: View {
@FocusedValue(PhotoSelection.self) private var selectedPhotos
var body: some View {
if let photos = selectedPhotos {
Text("\(photos.count) fotos selecionadas")
} else {
Text("Sem seleção").foregroundStyle(.secondary)
}
}
}
DocumentGroup
DocumentGroup(newDocument: MyDocument()) { config in
ContentView(document: config.$document)
}
struct MyDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
var text: String = ""
init() {}
init(configuration: ReadConfiguration) throws {
text = String(data: configuration.file.regularFileContents ?? Data(),
encoding: .utf8) ?? ""
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: Data(text.utf8))
}
}
Modo: window
Toolbar Styles (macOS-only)
Aplique no nível da Scene:
WindowGroup { ContentView() }
.windowToolbarStyle(.unified)
.windowToolbarStyle(.unifiedCompact)
.windowToolbarStyle(.expanded)
.windowToolbarStyle(.unified(showsTitle: false))
Conteúdo da toolbar na View:
.toolbar {
ToolbarItem(placement: .automatic) {
Button(action: addItem) {
Label("Adicionar", systemImage: "plus")
}
}
}
.searchable(text: $searchText, placement: .sidebar)
Window Sizing & Positioning
WindowGroup {
ContentView()
.frame(minWidth: 600, minHeight: 400)
}
.defaultSize(width: 900, height: 600)
.defaultPosition(.center)
.windowResizability(.contentMinSize)
windowResizability opções:
| Valor | Comportamento |
|---|
.automatic | Sistema decide |
.contentSize | Tamanho fixo, sem redimensione |
.contentMinSize | Redimensionável com mínimo pelo frame |
Posicionamento preciso (macOS 15.0+):
.windowIdealPlacement { context in
let screen = context.defaultDisplay.visibleArea
return WindowPlacement(x: screen.midX, y: screen.midY,
width: screen.width / 2,
height: screen.height)
}
Window Style
WindowGroup { ContentView() }
.windowStyle(.titleBar)
WindowGroup { ContentView() }
.windowStyle(.hiddenTitleBar)
NavigationSplitView no macOS
No macOS, as colunas são exibidas lado a lado (nunca sobrepostas). A sidebar recebe fundo translúcido automaticamente.
NavigationSplitView {
List(items, selection: $selectedID) { item in
Text(item.name)
}
.navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 300)
} detail: {
DetailView(id: selectedID)
}
.navigationSplitViewStyle(.balanced)
Três colunas:
NavigationSplitView {
SidebarView()
} content: {
ContentListView(selection: $selectedItem)
} detail: {
DetailView(item: selectedItem)
}
Inspector (macOS 14.0+)
Painel lateral direito redimensionável pelo usuário.
struct ContentView: View {
@State private var showInspector = false
var body: some View {
MainContent()
.inspector(isPresented: $showInspector) {
InspectorView()
.inspectorColumnWidth(min: 200, ideal: 250, max: 400)
}
.toolbar {
ToolbarItem {
Button {
showInspector.toggle()
} label: {
Label("Inspetor", systemImage: "info.circle")
}
}
}
}
}
Commands & Atalhos de Teclado
.commands {
CommandMenu("Ferramentas") {
Button("Executar Análise") { }
.keyboardShortcut("r", modifiers: [.command, .shift])
}
CommandGroup(after: .newItem) {
Button("Novo Pelo Template…") { }
}
}
Atalhos em botões:
Button("Salvar") { save() }
.keyboardShortcut("s", modifiers: .command)
Button("Excluir") { delete() }
.keyboardShortcut(.delete, modifiers: .command)
Posicionamentos de CommandGroup: .newItem, .saveItem, .help, .toolbar, .sidebar
Use .replacing(_:) para substituir um grupo do sistema.
Modo: views
HSplitView / VSplitView (macOS-only)
Use para layouts IDE-style onde todos os painéis são pares iguais. Para navegação
sidebar → conteúdo, prefira NavigationSplitView.
HSplitView {
FileTreeView()
.frame(minWidth: 200)
CodeEditorView()
.frame(minWidth: 400)
PreviewPane()
.frame(minWidth: 200)
}
VSplitView é idêntico mas divide verticalmente (use minHeight em vez de minWidth).
Table (macOS 12.0+)
struct PeopleTable: View {
@State private var people: [Person] = Person.samples
@State private var selection: Set<Person.ID> = []
@State private var sortOrder = [KeyPathComparator(\Person.name)]
var body: some View {
Table(people, selection: $selection, sortOrder: $sortOrder) {
TableColumn("Nome", value: \.name)
TableColumn("Idade") { Text("\($0.age)") }
TableColumn("Departamento", value: \.department)
}
.tableStyle(.bordered(alternatesRowBackgrounds: true))
.onChange(of: sortOrder) {
people.sort(using: sortOrder)
}
}
}
Estilos de tabela (macOS-only):
.tableStyle(.bordered)
.tableStyle(.bordered(alternatesRowBackgrounds: true))
.tableStyle(.inset)
.tableColumnHeaders(.hidden)
PasteButton & CopyButton
PasteButton (lê clipboard; não valida automaticamente no macOS):
PasteButton(payloadType: String.self) { strings in
pastedText = strings[0]
}
CopyButton (macOS 15.0+, macOS-only):
HStack {
Text(shareableText)
CopyButton(item: shareableText)
}
Operações de Arquivo
fileImporter:
.fileImporter(
isPresented: $showImporter,
allowedContentTypes: [.pdf],
allowsMultipleSelection: false
) { result in
if case .success(let urls) = result, let url = urls.first {
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
}
}
OBRIGATÓRIO: sempre chame startAccessingSecurityScopedResource() em URLs
retornadas e stopAccessingSecurityScopedResource() ao terminar.
fileExporter:
.fileExporter(
isPresented: $showExporter,
document: myDocument,
contentType: .plainText,
defaultFilename: "MeuArquivo.txt"
) { result in
}
Personalização dos painéis (macOS 13.0+, macOS-only):
.fileImporter( ) { result in }
.fileDialogMessage("Selecione uma imagem para o perfil")
.fileDialogConfirmationLabel("Usar esta foto")
.fileExporter( ) { result in }
.fileExporterFilenameLabel("Exportar como:")
Drag & Drop entre Apps
Moderno (Transferable — preferido):
Text(item.title)
.draggable(item)
VStack { }
.dropDestination(for: MyItem.self) { items, location in
droppedItems.append(contentsOf: items)
return true
}
Legado (NSItemProvider — somente para compatibilidade):
Image(systemName: "doc")
.onDrag {
NSItemProvider(object: fileURL as NSURL)
}
Text("Arraste arquivos aqui")
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
return true
}
AppKit Interop
NSViewRepresentable (sem Coordinator):
struct WebView: NSViewRepresentable {
let url: URL
func makeNSView(context: Context) -> WKWebView { WKWebView() }
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.load(URLRequest(url: url))
}
}
NSViewRepresentable com Coordinator (para delegates/callbacks):
struct SearchField: NSViewRepresentable {
@Binding var text: String
func makeNSView(context: Context) -> NSSearchField {
let field = NSSearchField()
field.delegate = context.coordinator
return field
}
func updateNSView(_ nsView: NSSearchField, context: Context) {
nsView.stringValue = text
}
func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
class Coordinator: NSObject, NSSearchFieldDelegate {
var text: Binding<String>
init(text: Binding<String>) { self.text = text }
func controlTextDidChange(_ obj: Notification) {
if let field = obj.object as? NSSearchField {
text.wrappedValue = field.stringValue
}
}
}
}
Nunca defina frame/bounds diretamente no NSView gerenciado — o SwiftUI controla o layout.
NSViewControllerRepresentable:
struct MapViewWrapper: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> MapViewController {
MapViewController()
}
func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
}
}
NSHostingController / NSHostingView (AppKit → SwiftUI):
let hostingController = NSHostingController(rootView: MySwiftUIView())
window.contentViewController = hostingController
let hostingView = NSHostingView(rootView: MySwiftUIView())
someNSView.addSubview(hostingView)
Checklist de Revisão