app-patterns
App UI Patterns
Reusable UI patterns for utility apps — prompt managers, clipboard tools, text expanders, and launchers. All patterns use Liquid Glass and assume macOS 26+ / iOS 26+.
Pattern Index
| Pattern | When to Use | Key Skill Dependencies |
|---|---|---|
| Spotlight Quick Access | Hotkey → search → select → action | global-hotkeys, appkit-bridge |
| Category Sidebar | Main app window, content browsing | swiftui-core |
| Floating Editor | Edit content in overlay panel | appkit-bridge |
| Compact Palette | Grid selection without search | liquid-glass |
| Settings Form | App preferences | macos-app-structure |
| Toast Notification | Ephemeral feedback after actions | — |
| Variable System | {{placeholder}} detection, input, highlighting |
— |
| Template Picker | Create from predefined templates | — |
| Rich Copy | Multi-format clipboard (plain + HTML + RTF) | pasteboard-textinsertion |
| Diff View | Version comparison | — |
Pattern 1: Spotlight-Style Quick Access
The core pattern for hotkey-triggered search → select → action.
See references/spotlight-panel.md for complete implementation.
Key Behaviors
- Search field auto-focuses on appear
- Arrow keys navigate results, Enter selects, Escape dismisses
- ⌘Enter for alternate action (e.g., copy instead of paste)
- Selection morphs between rows via
.glassEffectID - Results scroll to keep selection visible
- Empty state shown when search has no matches
Structure
struct QuickAccessPanel: View {
@State private var query = ""
@State private var selectedIndex = 0
@FocusState private var searchFocused: Bool
@Namespace private var selection
var body: some View {
VStack(spacing: 0) {
// Search bar with glass
searchBar
.glassEffect(.regular, in: .rect(cornerRadius: 14))
// Results list with morphing selection
resultsList
.glassEffect(.clear, in: .rect(cornerRadius: 14))
}
.frame(width: 600)
.onAppear { searchFocused = true }
.onKeyPress { handleKeyPress($0) }
}
}
Keyboard Handling
func handleKeyPress(_ press: KeyPress) -> KeyPress.Result {
switch press.key {
case .upArrow:
selectedIndex = max(0, selectedIndex - 1)
return .handled
case .downArrow:
selectedIndex = min(filtered.count - 1, selectedIndex + 1)
return .handled
case .escape:
onDismiss()
return .handled
case .return where press.modifiers.contains(.command):
copyToClipboard(filtered[safe: selectedIndex])
return .handled
default:
return .ignored
}
}
Pattern 2: Category Sidebar
Three-column NavigationSplitView for the main app window.
struct MainView: View {
@State private var selectedCategory: Category?
@State private var selectedPrompt: Prompt?
var body: some View {
NavigationSplitView {
CategorySidebar(selection: $selectedCategory)
} content: {
PromptList(category: selectedCategory, selection: $selectedPrompt)
} detail: {
if let prompt = selectedPrompt {
PromptEditor(prompt: prompt)
} else {
ContentUnavailableView("Select a Prompt", systemImage: "text.quote")
}
}
.navigationSplitViewStyle(.balanced)
}
}
Sidebar with Glass Selection Morphing
struct CategorySidebar: View {
@Binding var selection: Category?
@Namespace private var categorySelection
var body: some View {
GlassEffectContainer {
List(selection: $selection) {
Section("Categories") {
ForEach(categories) { category in
Label(category.name, systemImage: category.icon)
.tag(category)
.listRowBackground(
selection == category ?
RoundedRectangle(cornerRadius: 8)
.glassEffect(.regular.tint(category.color.opacity(0.3)))
.glassEffectID("category", in: categorySelection)
: nil
)
}
}
Section("Smart Lists") {
Label("Favorites", systemImage: "star")
Label("Recent", systemImage: "clock")
}
}
}
}
}
Pattern 3: Floating Editor Panel
For editing content in an overlay. Pairs with appkit-bridge NSPanel.
struct FloatingEditorView: View {
@Bindable var prompt: Prompt
@State private var isPreviewExpanded = false
@Namespace private var editor
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// Title field
TextField("Prompt title", text: $prompt.title)
.textFieldStyle(.plain)
.font(.title3)
.padding(12)
.glassEffect(.clear, in: .rect(cornerRadius: 10))
// Content editor
TextEditor(text: $prompt.content)
.font(.body)
.scrollContentBackground(.hidden)
.padding(12)
.frame(minHeight: 200)
.glassEffect(.clear, in: .rect(cornerRadius: 10))
// Expandable preview
Button {
withAnimation(.spring(response: 0.3)) {
isPreviewExpanded.toggle()
}
} label: {
HStack {
Image(systemName: "eye")
Text("Preview")
Spacer()
Image(systemName: "chevron.right")
.rotationEffect(.degrees(isPreviewExpanded ? 90 : 0))
}
}
.buttonStyle(.glass)
if isPreviewExpanded {
Text(prompt.content)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.glassEffect(.regular.tint(.green.opacity(0.2)), in: .rect(cornerRadius: 10))
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
.padding()
}
.frame(width: 500, height: isPreviewExpanded ? 600 : 450)
}
}
Pattern 4: Compact Grid Palette
Grid selection with hover morphing. Good for quick access without search.
struct PromptPalette: View {
let prompts: [Prompt]
@State private var hoveredPrompt: Prompt?
@Namespace private var hover
var onSelect: (Prompt) -> Void
var body: some View {
GlassEffectContainer(spacing: 8) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))], spacing: 8) {
ForEach(prompts) { prompt in
VStack(spacing: 8) {
Image(systemName: prompt.icon)
.font(.title)
.foregroundStyle(prompt.categoryColor)
Text(prompt.title)
.font(.caption).fontWeight(.medium)
.lineLimit(2).multilineTextAlignment(.center)
}
.frame(width: 100, height: 80).padding(8)
.glassEffect(.regular, in: .rect(cornerRadius: 12))
.scaleEffect(hoveredPrompt == prompt ? 1.05 : 1.0)
.onHover { isHovered in
withAnimation(.easeOut(duration: 0.15)) {
hoveredPrompt = isHovered ? prompt : nil
}
}
.onTapGesture { onSelect(prompt) }
.background {
if hoveredPrompt == prompt {
RoundedRectangle(cornerRadius: 12)
.glassEffect(.regular.tint(.accentColor.opacity(0.3)))
.glassEffectID("hover", in: hover)
}
}
}
}
.padding()
}
.glassEffect(.clear, in: .rect(cornerRadius: 16))
}
}
Pattern 5: Settings Form with Glass Sections
struct SettingsView: View {
@AppStorage("hotkeyModifiers") var hotkeyModifiers: Int = 0
@AppStorage("hotkeyKey") var hotkeyKey: Int = 49
@AppStorage("launchAtLogin") var launchAtLogin = false
@AppStorage("showInDock") var showInDock = false
var body: some View {
Form {
Section {
HotkeyRecorder(modifiers: $hotkeyModifiers, key: $hotkeyKey)
} header: {
Label("Global Hotkey", systemImage: "keyboard")
}
.listRowBackground(
RoundedRectangle(cornerRadius: 10)
.glassEffect(.clear).padding(.vertical, 2)
)
Section {
Toggle("Launch at Login", isOn: $launchAtLogin)
Toggle("Show in Dock", isOn: $showInDock)
} header: {
Label("Behavior", systemImage: "gearshape")
}
.listRowBackground(
RoundedRectangle(cornerRadius: 10)
.glassEffect(.clear).padding(.vertical, 2)
)
}
.formStyle(.grouped)
.frame(width: 450, height: 350)
}
}
Pattern 6: Toast Notification
Ephemeral feedback overlay with auto-dismiss.
struct ToastView: View {
let message: String
let icon: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: icon).font(.title3)
Text(message).fontWeight(.medium)
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
.glassEffect(.regular.tint(.green.opacity(0.3)), in: .capsule)
}
}
// Toast hosting in parent view
struct ContentView: View {
@State private var showToast = false
@State private var toastMessage = ""
var body: some View {
ZStack(alignment: .bottom) {
MainContent()
if showToast {
ToastView(message: toastMessage, icon: "checkmark.circle")
.transition(.move(edge: .bottom).combined(with: .opacity))
.padding(.bottom, 40)
}
}
.animation(.spring(response: 0.3), value: showToast)
}
func showToast(_ message: String) {
toastMessage = message
showToast = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { showToast = false }
}
}
Variable Placeholder System
Detection & Expansion
extension Prompt {
/// Extract {{variable}} names from content
var detectedVariables: [String] {
let pattern = #"\{\{(\w+)\}\}"#
let regex = try? NSRegularExpression(pattern: pattern)
let range = NSRange(content.startIndex..., in: content)
let matches = regex?.matches(in: content, range: range) ?? []
return matches.compactMap { match in
Range(match.range(at: 1), in: content).map { String(content[$0]) }
}
}
/// Fill variables and return expanded content
func filled(with values: [String: String]) -> String {
var result = content
for (name, value) in values {
result = result.replacingOccurrences(of: "{{\(name)}}", with: value)
}
return result
}
}
Variable Input UI
struct QuickVariablePopover: View {
let variables: [String]
@State private var values: [String: String] = [:]
@FocusState private var focusedField: String?
var onComplete: ([String: String]) -> Void
var body: some View {
VStack(spacing: 12) {
ForEach(Array(variables.enumerated()), id: \.element) { index, variable in
HStack {
Text(variable.capitalized)
.frame(width: 100, alignment: .trailing)
.foregroundStyle(.secondary)
TextField("", text: binding(for: variable))
.textFieldStyle(.plain).padding(8)
.glassEffect(.clear, in: .rect(cornerRadius: 8))
.focused($focusedField, equals: variable)
.onSubmit {
if index < variables.count - 1 {
focusedField = variables[index + 1]
} else { onComplete(values) }
}
}
}
HStack {
Spacer()
Button("Fill & Copy") { onComplete(values) }
.buttonStyle(.glassProminent)
.keyboardShortcut(.return, modifiers: .command)
}
}
.padding().frame(width: 350)
.onAppear { focusedField = variables.first }
}
func binding(for variable: String) -> Binding<String> {
Binding(get: { values[variable] ?? "" }, set: { values[variable] = $0 })
}
}
Syntax Highlighting for Variables
struct HighlightedPromptView: View {
let content: String
var body: some View {
Text(attributedContent).textSelection(.enabled)
}
var attributedContent: AttributedString {
var attributed = AttributedString(content)
let pattern = #"\{\{(\w+)\}\}"#
if let regex = try? NSRegularExpression(pattern: pattern) {
let range = NSRange(content.startIndex..., in: content)
for match in regex.matches(in: content, range: range).reversed() {
if let swiftRange = Range(match.range, in: content),
let attrRange = Range(swiftRange, in: attributed) {
attributed[attrRange].foregroundColor = .accentColor
attributed[attrRange].backgroundColor = .accentColor.opacity(0.1)
attributed[attrRange].font = .body.monospaced()
}
}
}
return attributed
}
}
Rich Multi-Format Copy
Copy as plain text + HTML + RTF so paste works well in any app.
#if os(macOS)
func copyRich(_ content: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
// Plain text
pasteboard.setString(content, forType: .string)
// HTML (for rich text apps like Mail, Pages)
let html = convertToHTML(content)
if let htmlData = html.data(using: .utf8) {
pasteboard.setData(htmlData, forType: .html)
}
// RTF
let attributed = NSAttributedString(
string: content,
attributes: [.font: NSFont.systemFont(ofSize: 14), .foregroundColor: NSColor.textColor]
)
if let rtfData = try? attributed.data(
from: NSRange(location: 0, length: attributed.length),
documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]
) {
pasteboard.setData(rtfData, forType: .rtf)
}
}
#endif
Anti-Patterns
❌ Glass Overload
// Too much glass — confusing and poor performance
VStack {
header.glassEffect()
content.glassEffect() // ❌ Content shouldn't be glass
footer.glassEffect()
}
.glassEffect() // ❌ Nested glass
✅ Targeted Glass
VStack {
header.glassEffect() // ✅ Navigation/chrome only
content // ✅ Content is clear
actionBar.glassEffect() // ✅ Floating actions
}
❌ Glass on Dark Backgrounds
ZStack {
Color.black
Text("Hard to see").glassEffect() // ❌ Glass needs varied background
}
✅ Rich Backgrounds
ZStack {
Image("wallpaper").resizable()
Text("Easy to read").glassEffect() // ✅ Glass shines on varied imagery
}
More from makgunay/claude-swift-skills
swiftui-webkit
Native SwiftUI WebKit integration with the new WebView struct and WebPage observable class. Covers WebView creation from URLs, WebPage for navigation control and state management, JavaScript execution (callJavaScript with arguments and content worlds), custom URL scheme handlers, navigation management (load, reload, back/forward), navigation decisions, text search (findNavigator), content capture (snapshots, PDF generation, web archives), and configuration (data stores, user agents, JS permissions). Use when embedding web content in SwiftUI apps instead of the old WKWebView + UIViewRepresentable/NSViewRepresentable bridge pattern. This is a brand new API — do NOT use the old WKWebView wrapping approach.
10liquid-glass
Comprehensive guide to Apple's Liquid Glass design system introduced in macOS 26, iOS 26, and across all Apple platforms. Covers SwiftUI (.glassEffect(), GlassEffectContainer, .interactive(), .tint(), glassEffectID morphing, .buttonStyle(.glass), .buttonStyle(.glassProminent), glassEffectUnion), AppKit (NSGlassEffectView, NSGlassEffectContainerView), UIKit (UIGlassEffect, UIGlassContainerEffect, UIScrollEdgeEffect), and WidgetKit (accented rendering mode, widgetAccentable). Use whenever building UI with the new Apple design language, adopting glass effects, styling buttons or toolbars, or creating modern macOS/iOS interfaces. Always consult this skill when asked about new Apple design.
10tech-stack-validator
Validates and recommends technology stacks for native macOS/iOS app projects against PRD and architecture requirements. Reads the PRD and architecture documents (or gathers requirements interactively), then systematically checks every technology choice for: OS version availability, framework capability gaps, performance feasibility, distribution compatibility (sandbox vs direct), API deprecation risks, dependency conflicts, and timeline realism for the team size. Produces a structured validation report with go/no-go verdicts, risk flags, and alternative recommendations. Use when user says things like 'validate my tech stack', 'check if this architecture works', 'what should I build this with', 'is SwiftData the right choice', 'can I ship this on the App Store', 'review my architecture', or before starting implementation of any PRD. Also use when migrating between tech stacks or evaluating whether to adopt a new framework.
9swift-lang
Swift 6.2 language patterns and critical corrections for LLM code generation. Covers the concurrency paradigm shift (default MainActor isolation, @concurrent for background work, isolated conformances, nonisolated opt-out), @Observable vs ObservableObject, structured concurrency, modern error handling, property wrappers, InlineArray, Span, and value generics. Use whenever generating Swift code, especially async/concurrent code, class/struct design, or performance-critical paths. This skill prevents the most common LLM mistakes in Swift code generation.
8cross-platform
Patterns for sharing code between macOS and iOS in SwiftUI apps. Covers project structure (70% shared / 15% macOS / 15% iOS), platform abstraction via protocols and #if os() conditional compilation, adaptive navigation (NavigationSplitView on Mac/iPad → NavigationStack on iPhone), shared components with platform styling, iOS-specific extensions (custom keyboard extension, interactive widgets, share extension, action extension, Control Center widget, lock screen widget), App Groups for data sharing with extensions, CloudKit sync monitoring, JSON export/import, schema versioning and migration, URL scheme deep linking, and the full macOS→iOS migration checklist. Use when building apps that target both macOS and iOS, when adding iOS support to a macOS app, when building widgets or keyboard extensions, or when setting up iCloud sync with SwiftData.
8