macos-development
4
总安装量
4
周安装量
#50800
全站排名
安装命令
npx skills add https://github.com/bluewaves-creations/bluewaves-skills --skill macos-development
Agent 安装分布
opencode
4
claude-code
3
codex
3
kode
2
mcpjam
2
Skill 文档
macOS Development Patterns
Comprehensive guide to macOS Tahoe development, window management, menu bars, document-based apps, and macOS-specific SwiftUI patterns.
Prerequisites
- macOS Tahoe (macOS 26) or later
- Xcode 26+
Window Management
Window Styles
import SwiftUI
@main
struct MyMacApp: App {
var body: some Scene {
// Standard window
WindowGroup {
ContentView()
}
.windowStyle(.automatic) // Default
// Hideable title bar (content extends to top)
WindowGroup("Editor", id: "editor") {
EditorView()
}
.windowStyle(.hiddenTitleBar)
// Plain window (no chrome)
WindowGroup("Floating", id: "floating") {
FloatingView()
}
.windowStyle(.plain)
}
}
Window Size and Position
WindowGroup {
ContentView()
}
// Default size
.defaultSize(width: 800, height: 600)
// Size constraints
.windowResizability(.contentSize) // Fit content
.windowResizability(.contentMinSize) // Min = content, resizable larger
.windowResizability(.automatic) // System decides
// Fixed size window
.windowResizability(.contentSize)
.frame(width: 400, height: 300)
// Position
.defaultPosition(.center)
.defaultPosition(.topLeading)
.defaultPosition(UnitPoint(x: 0.75, y: 0.25))
Multiple Windows
@main
struct MultiWindowApp: App {
@Environment(\.openWindow) private var openWindow
var body: some Scene {
// Main window
WindowGroup {
MainView()
.toolbar {
Button("New Editor") {
openWindow(id: "editor")
}
}
}
.commands {
CommandGroup(after: .newItem) {
Button("New Editor Window") {
openWindow(id: "editor")
}
.keyboardShortcut("e", modifiers: [.command, .shift])
}
}
// Secondary window type
WindowGroup("Editor", id: "editor") {
EditorView()
}
.defaultSize(width: 600, height: 400)
// Single instance window
Window("Settings", id: "settings") {
SettingsView()
}
.keyboardShortcut(",", modifiers: .command)
.defaultSize(width: 500, height: 400)
}
}
Window with Data
// Define window value type
struct DocumentInfo: Codable, Hashable {
let id: UUID
let title: String
}
@main
struct DocumentApp: App {
var body: some Scene {
WindowGroup(for: DocumentInfo.self) { $document in
if let document {
DocumentView(info: document)
}
}
}
}
// Open with specific data
struct ContentView: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
Button("Open Document") {
openWindow(value: DocumentInfo(id: UUID(), title: "New Doc"))
}
}
}
NSWindow Integration
import AppKit
import SwiftUI
struct WindowAccessor: NSViewRepresentable {
let callback: (NSWindow?) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
callback(view.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
// Usage
struct ContentView: View {
@State private var window: NSWindow?
var body: some View {
Text("Hello")
.background(WindowAccessor { window in
self.window = window
// Customize window
window?.titlebarAppearsTransparent = true
window?.titleVisibility = .hidden
window?.styleMask.insert(.fullSizeContentView)
window?.isMovableByWindowBackground = true
})
}
}
Menu Bar
App Menu Customization
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
// Replace existing menu group
CommandGroup(replacing: .newItem) {
Button("New Document") {
// Create new document
}
.keyboardShortcut("n")
Button("New from Template...") {
// Show template picker
}
.keyboardShortcut("n", modifiers: [.command, .shift])
}
// Add to existing menu
CommandGroup(after: .sidebar) {
Divider()
Button("Toggle Inspector") {
// Toggle inspector
}
.keyboardShortcut("i", modifiers: [.command, .option])
}
// Custom menu
CommandMenu("Canvas") {
Button("Zoom In") { }
.keyboardShortcut("+")
Button("Zoom Out") { }
.keyboardShortcut("-")
Divider()
Button("Fit to Window") { }
.keyboardShortcut("0")
}
}
}
}
Context Menus (Right-Click)
struct ItemView: View {
let item: Item
@State private var isRenaming = false
var body: some View {
Text(item.title)
.contextMenu {
Button("Open") {
// Open action
}
Button("Open in New Window") {
// Open in new window
}
Divider()
Button("Rename") {
isRenaming = true
}
Button("Duplicate") {
// Duplicate action
}
Divider()
Button("Delete", role: .destructive) {
// Delete action
}
}
}
}
Menu Bar Extra (Status Bar Items)
@main
struct StatusBarApp: App {
var body: some Scene {
// Optional main window
WindowGroup {
ContentView()
}
// Status bar item
MenuBarExtra("My App", systemImage: "star.fill") {
Button("Show Dashboard") {
// Open main window
}
.keyboardShortcut("d")
Divider()
Menu("Recent Items") {
ForEach(recentItems) { item in
Button(item.name) {
// Open item
}
}
}
Divider()
Button("Preferences...") {
// Open preferences
}
.keyboardShortcut(",")
Button("Quit") {
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("q")
}
}
@State private var recentItems: [RecentItem] = []
}
// Custom status bar view
struct StatusBarApp2: App {
var body: some Scene {
MenuBarExtra {
StatusBarPopover()
} label: {
HStack(spacing: 4) {
Image(systemName: "cpu")
Text("45%")
.font(.caption)
}
}
.menuBarExtraStyle(.window) // Popover style
}
}
struct StatusBarPopover: View {
var body: some View {
VStack(spacing: 16) {
Text("System Status")
.font(.headline)
// Status content
StatusRow(title: "CPU", value: "45%")
StatusRow(title: "Memory", value: "8.2 GB")
StatusRow(title: "Storage", value: "234 GB")
Divider()
Button("Open Activity Monitor") {
// Open Activity Monitor
}
}
.padding()
.frame(width: 200)
}
}
Document-Based Apps
Document Type Definition
import SwiftUI
import UniformTypeIdentifiers
// Define document type
extension UTType {
static var myDocument: UTType {
UTType(exportedAs: "com.mycompany.mydocument")
}
}
// Document model
struct MyDocument: FileDocument {
static var readableContentTypes: [UTType] { [.myDocument, .plainText] }
static var writableContentTypes: [UTType] { [.myDocument] }
var content: String
init(content: String = "") {
self.content = content
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8) else {
throw CocoaError(.fileReadCorruptFile)
}
content = string
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = content.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// Document-based app
@main
struct DocumentApp: App {
var body: some Scene {
DocumentGroup(newDocument: MyDocument()) { file in
DocumentView(document: file.$document)
}
.commands {
// Document-specific commands
CommandGroup(after: .saveItem) {
Button("Export as PDF...") {
// Export
}
.keyboardShortcut("e", modifiers: [.command, .shift])
}
}
}
}
struct DocumentView: View {
@Binding var document: MyDocument
@FocusedValue(\.document) private var focusedDocument
var body: some View {
TextEditor(text: $document.content)
.font(.body.monospaced())
.focusedValue(\.document, $document)
}
}
Reference File Documents (Large Files)
import SwiftUI
import UniformTypeIdentifiers
// For large files, use ReferenceFileDocument
@Observable
class ImageDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [.png, .jpeg] }
static var writableContentTypes: [UTType] { [.png] }
var image: NSImage?
var annotations: [Annotation] = []
init() {}
required init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
image = NSImage(data: data)
}
func snapshot(contentType: UTType) throws -> Data {
guard let image, let data = image.tiffRepresentation else {
throw CocoaError(.fileWriteUnknown)
}
return data
}
func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: snapshot)
}
}
@main
struct ImageEditorApp: App {
var body: some Scene {
DocumentGroup(newDocument: { ImageDocument() }) { file in
ImageEditorView(document: file.document)
}
}
}
Mac Catalyst
Enabling Mac Catalyst
In Xcode: Target â General â Deployment Info â Mac (Mac Catalyst)
Platform-Specific Code
struct ContentView: View {
var body: some View {
VStack {
#if targetEnvironment(macCatalyst)
// Mac Catalyst specific UI
MacToolbar()
#else
// iOS specific UI
iOSToolbar()
#endif
MainContent()
}
}
}
// Check at runtime
struct PlatformAwareView: View {
var body: some View {
Group {
if ProcessInfo.processInfo.isMacCatalystApp {
MacLayout()
} else {
iOSLayout()
}
}
}
}
Catalyst-Specific Features
#if targetEnvironment(macCatalyst)
import AppKit
extension View {
func configureMacWindow() -> some View {
self.onAppear {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
// Window size
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 800, height: 600)
windowScene.sizeRestrictions?.maximumSize = CGSize(width: 1920, height: 1080)
// Title bar
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
}
}
}
#endif
Optimizing for Mac
struct CatalystOptimizedApp: App {
var body: some Scene {
WindowGroup {
ContentView()
#if targetEnvironment(macCatalyst)
.frame(minWidth: 800, minHeight: 600)
.onAppear(perform: setupMacEnvironment)
#endif
}
#if targetEnvironment(macCatalyst)
.commands {
// Mac-specific menu commands
CommandGroup(replacing: .help) {
Button("MyApp Help") {
// Open help
}
}
}
#endif
}
#if targetEnvironment(macCatalyst)
private func setupMacEnvironment() {
// Enable hover effects
// Configure pointer interactions
// Set up keyboard shortcuts
}
#endif
}
// Pointer/hover support
struct HoverButton: View {
@State private var isHovered = false
var body: some View {
Button("Click Me") { }
.buttonStyle(.borderedProminent)
.scaleEffect(isHovered ? 1.05 : 1.0)
.onHover { hovering in
withAnimation(.easeInOut(duration: 0.15)) {
isHovered = hovering
}
}
}
}
macOS-Specific Modifiers
Toolbar Customization
struct MacToolbarView: View {
@State private var searchText = ""
var body: some View {
NavigationSplitView {
Sidebar()
} detail: {
DetailView()
}
.toolbar {
// Leading items
ToolbarItem(placement: .navigation) {
Button(action: {}) {
Image(systemName: "sidebar.left")
}
}
// Principal (center)
ToolbarItem(placement: .principal) {
Picker("View", selection: .constant(0)) {
Text("Grid").tag(0)
Text("List").tag(1)
}
.pickerStyle(.segmented)
.frame(width: 150)
}
// Trailing items
ToolbarItemGroup(placement: .primaryAction) {
Button(action: {}) {
Image(systemName: "plus")
}
Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
}
// Search field (trailing)
ToolbarItem(placement: .automatic) {
TextField("Search", text: $searchText)
.textFieldStyle(.roundedBorder)
.frame(width: 200)
}
}
.toolbarBackground(.visible, for: .windowToolbar)
}
}
Sidebar and Split View
struct ThreeColumnLayout: View {
@State private var selectedFolder: Folder?
@State private var selectedItem: Item?
@State private var columnVisibility: NavigationSplitViewVisibility = .all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
// Sidebar (first column)
List(folders, selection: $selectedFolder) { folder in
NavigationLink(value: folder) {
Label(folder.name, systemImage: folder.icon)
}
}
.navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 250)
} content: {
// Content (second column)
if let folder = selectedFolder {
List(folder.items, selection: $selectedItem) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
} else {
ContentUnavailableView("Select a Folder", systemImage: "folder")
}
} detail: {
// Detail (third column)
if let item = selectedItem {
ItemDetailView(item: item)
} else {
ContentUnavailableView("Select an Item", systemImage: "doc")
}
}
.navigationSplitViewStyle(.balanced)
}
@State private var folders: [Folder] = []
}
Settings/Preferences Window
@main
struct PreferencesApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
#if os(macOS)
Settings {
SettingsView()
}
#endif
}
}
struct SettingsView: View {
var body: some View {
TabView {
GeneralSettings()
.tabItem {
Label("General", systemImage: "gear")
}
AppearanceSettings()
.tabItem {
Label("Appearance", systemImage: "paintbrush")
}
AccountSettings()
.tabItem {
Label("Account", systemImage: "person.crop.circle")
}
AdvancedSettings()
.tabItem {
Label("Advanced", systemImage: "gearshape.2")
}
}
.frame(width: 450, height: 300)
}
}
struct GeneralSettings: View {
@AppStorage("launchAtLogin") private var launchAtLogin = false
@AppStorage("checkForUpdates") private var checkForUpdates = true
var body: some View {
Form {
Toggle("Launch at Login", isOn: $launchAtLogin)
Toggle("Check for Updates Automatically", isOn: $checkForUpdates)
}
.padding()
}
}
Inspector Panel
struct InspectorView: View {
@State private var showInspector = true
@State private var selectedItem: Item?
var body: some View {
NavigationStack {
ContentList(selection: $selectedItem)
}
.inspector(isPresented: $showInspector) {
if let item = selectedItem {
ItemInspector(item: item)
} else {
ContentUnavailableView("No Selection", systemImage: "sidebar.right")
}
}
.inspectorColumnWidth(min: 250, ideal: 300, max: 400)
.toolbar {
ToolbarItem {
Button {
showInspector.toggle()
} label: {
Image(systemName: "sidebar.right")
}
}
}
}
}
Keyboard Shortcuts
Custom Keyboard Shortcuts
struct KeyboardShortcutDemo: View {
@State private var text = ""
var body: some View {
TextEditor(text: $text)
.toolbar {
// Toolbar button with shortcut
Button("Bold") {
makeBold()
}
.keyboardShortcut("b", modifiers: .command)
Button("Italic") {
makeItalic()
}
.keyboardShortcut("i", modifiers: .command)
}
// Background keyboard shortcuts
.background {
Group {
Button("") { duplicateLine() }
.keyboardShortcut("d", modifiers: [.command, .shift])
Button("") { deleteLine() }
.keyboardShortcut(.delete, modifiers: [.command, .shift])
}
.frame(width: 0, height: 0)
.opacity(0)
}
}
private func makeBold() {}
private func makeItalic() {}
private func duplicateLine() {}
private func deleteLine() {}
}
Focus-Based Commands
struct FocusedDocument: FocusedValueKey {
typealias Value = Binding<MyDocument>
}
extension FocusedValues {
var document: Binding<MyDocument>? {
get { self[FocusedDocument.self] }
set { self[FocusedDocument.self] = newValue }
}
}
struct DocumentCommands: Commands {
@FocusedValue(\.document) var document
var body: some Commands {
CommandGroup(after: .textEditing) {
Button("Word Count") {
if let doc = document?.wrappedValue {
let count = doc.content.split(separator: " ").count
print("Words: \(count)")
}
}
.keyboardShortcut("w", modifiers: [.command, .shift])
.disabled(document == nil)
}
}
}
Best Practices
DO
// â Use native macOS controls
NavigationSplitView { ... }
// â Support keyboard navigation
.focusable()
.onKeyPress(.tab) { ... }
// â Respect system appearance
@Environment(\.colorScheme) var colorScheme
// â Use Settings scene for preferences
Settings { SettingsView() }
// â Support window restoration
.handlesExternalEvents(matching: ["main"])
DON’T
// â Don't use iOS-only patterns
.navigationBarHidden(true) // Use .toolbar instead
// â Don't ignore keyboard shortcuts
// Always add for common actions
// â Don't hardcode window sizes
.frame(width: 800, height: 600) // Use defaultSize instead
// â Don't forget right-click menus
// Add contextMenu to interactive elements