From 464201f7face60d1df8e8fbd6e0382ec67623919 Mon Sep 17 00:00:00 2001 From: Sree Raman <1271305+sreejithraman@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:57:59 -0400 Subject: [PATCH 1/4] Add SwiftUI and UIKit skills --- README.md | 1 - SOURCES.md | 12 +- THIRD_PARTY_NOTICES.md | 1 - skills/liquid-glass/SKILL.md | 223 -------- skills/liquid-glass/agents/openai.yaml | 2 - .../examples/landmarks-patterns.md | 481 ------------------ .../liquid-glass/references/api-reference.md | 410 --------------- .../references/migration-guide.md | 371 -------------- .../references/pitfalls-and-solutions.md | 371 -------------- .../references/platform-specifics.md | 302 ----------- skills/swiftui/SKILL.md | 40 ++ skills/swiftui/agents/openai.yaml | 4 + .../references/composition-and-identity.md | 42 ++ .../swiftui/references/materials-and-glass.md | 42 ++ .../references/navigation-and-presentation.md | 37 ++ .../swiftui/references/state-and-data-flow.md | 43 ++ skills/uikit/SKILL.md | 36 ++ skills/uikit/agents/openai.yaml | 4 + .../uikit/references/layout-and-safe-areas.md | 35 ++ skills/uikit/references/scenes-and-traits.md | 50 ++ .../view-controllers-and-collections.md | 39 ++ 21 files changed, 380 insertions(+), 2166 deletions(-) delete mode 100644 skills/liquid-glass/SKILL.md delete mode 100644 skills/liquid-glass/agents/openai.yaml delete mode 100644 skills/liquid-glass/examples/landmarks-patterns.md delete mode 100644 skills/liquid-glass/references/api-reference.md delete mode 100644 skills/liquid-glass/references/migration-guide.md delete mode 100644 skills/liquid-glass/references/pitfalls-and-solutions.md delete mode 100644 skills/liquid-glass/references/platform-specifics.md create mode 100644 skills/swiftui/SKILL.md create mode 100644 skills/swiftui/agents/openai.yaml create mode 100644 skills/swiftui/references/composition-and-identity.md create mode 100644 skills/swiftui/references/materials-and-glass.md create mode 100644 skills/swiftui/references/navigation-and-presentation.md create mode 100644 skills/swiftui/references/state-and-data-flow.md create mode 100644 skills/uikit/SKILL.md create mode 100644 skills/uikit/agents/openai.yaml create mode 100644 skills/uikit/references/layout-and-safe-areas.md create mode 100644 skills/uikit/references/scenes-and-traits.md create mode 100644 skills/uikit/references/view-controllers-and-collections.md diff --git a/README.md b/README.md index 515d8e9..e8ceab8 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ uses and adapts: - [Anthropic’s skills](https://github.com/anthropics/skills) — the Frontend Web Design skill. - [Anthropic’s Claude Plugins Community](https://github.com/anthropics/claude-plugins-community) — the `eli5` skill. - [GitHub’s gh-stack](https://github.com/github/gh-stack) — the stacked PR skill. -- [haider-nawaz](https://github.com/haider-nawaz/liquid-glass-skill) — the Liquid Glass skill. - [s0xDk](https://github.com/s0xDk/refactoring-ui-skill) — the Refactoring UI skill, based on Adam Wathan and Steve Schoger’s work. - [React Doctor](https://github.com/millionco/react-doctor) — the React diagnostics skill. - [Vercel’s agent-skills](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices) — the React Best Practices skill, originally created by [Shu Ding](https://x.com/shuding). diff --git a/SOURCES.md b/SOURCES.md index a5b46f8..8e5d764 100644 --- a/SOURCES.md +++ b/SOURCES.md @@ -164,12 +164,16 @@ only on request. - Local: `skills/swift-testing-modernization` (SreeStack). -## liquid-glass +## swiftui -- [haider-nawaz/liquid-glass-skill / plugins/liquid-glass/skills/liquid-glass](https://github.com/haider-nawaz/liquid-glass-skill/tree/2c1b2789c30dc2c9208f3b9a3811d42480714577/plugins/liquid-glass/skills/liquid-glass) — commit `2c1b2789c30dc2c9208f3b9a3811d42480714577`. +- Local: `skills/swiftui` (SreeStack). -Manual-only invocation: `disable-model-invocation: true` and -`allow_implicit_invocation: false`; upstream has neither setting. +Includes focused guidance for current platform materials and Liquid Glass rather +than keeping a separate visual-effect skill. + +## uikit + +- Local: `skills/uikit` (SreeStack). ## manual-verify diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index ac5af2b..d12e42b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -19,7 +19,6 @@ material keeps the terms listed below; the root license does not replace them. | s0xDk / s13k | refactoring-ui-skill | [MIT and scope note](skills/refactoring-ui-skill/LICENSE) | | Million Software | react-doctor | [Modified MIT](licenses/millionco-react-doctor.txt) | | Vercel Labs | react-best-practices | Unresolved: upstream repo README and skill frontmatter claim MIT; no license file found at the recorded revision | -| haider-nawaz | liquid-glass | Unresolved: no license file found at the recorded revision | The files in `licenses/` copy upstream license files at the revisions in SOURCES.md. Anthropic skills was checked on 2026-09-12; App Store Connect diff --git a/skills/liquid-glass/SKILL.md b/skills/liquid-glass/SKILL.md deleted file mode 100644 index 17ccfc2..0000000 --- a/skills/liquid-glass/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: liquid-glass -description: Build and migrate iOS, macOS, iPadOS, watchOS, tvOS, and visionOS - apps with Apple's Liquid Glass design system (iOS 26+, macOS 26 Tahoe+). Use - when creating new SwiftUI apps targeting Apple's 2025+ platforms, migrating - existing apps to Liquid Glass, applying .glassEffect(), - .backgroundExtensionEffect(), .buttonStyle(.glass), GlassEffectContainer, - glass toolbars, glass tab bars, glass sheets, or any Liquid Glass UI work. - Also use when the user asks about modern Apple design, navigation patterns - with glass, or SwiftUI best practices for the latest OS versions. -disable-model-invocation: true ---- - -# Liquid Glass Design System for Apple Platforms - -Build and migrate SwiftUI apps using Apple's Liquid Glass design language introduced at WWDC 2025. This skill covers iOS 26, iPadOS 26, macOS 26 (Tahoe), watchOS 26, tvOS 26, and visionOS 26. - -## Important: Use Latest Documentation - -Always fetch the latest Apple developer documentation when implementing Liquid Glass features. The APIs may evolve between OS betas. Key documentation URLs to reference: - -- `https://developer.apple.com/documentation/SwiftUI/Applying-Liquid-Glass-to-custom-views` -- `https://developer.apple.com/documentation/swiftui/view/glasseffect(_:in:)` -- `https://developer.apple.com/documentation/SwiftUI/Landmarks-Building-an-app-with-Liquid-Glass` -- `https://developer.apple.com/videos/play/wwdc2025/323/` (WWDC25 Session: Build a SwiftUI app with the new design) - -## Core Concept - -Liquid Glass is a translucent, dynamic material exclusively for the **navigation layer** (toolbars, tab bars, buttons, controls) that floats above app content. It bends and refracts light in real-time, responds to device motion with specular highlights, and adapts continuously to background content. - -**Never apply glass to content itself** (lists, tables, media, text blocks). Glass is for controls and navigation only. - -## Quick Start: Key APIs - -### 1. Glass Effect on Custom Views - -```swift -// Basic - capsule shape (default) -Text("Label") - .padding() - .glassEffect() - -// With shape and style -Image(systemName: "heart.fill") - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 16)) - -// Tinted glass -Text("Tinted") - .padding() - .glassEffect(.regular.tint(.blue)) - -// Interactive glass (scales, bounces, shimmers on touch - iOS only) -Button("Tap Me") { } - .glassEffect(.regular.interactive()) -``` - -### 2. Glass Styles - -| Style | Use Case | Transparency | -|-------|----------|-------------| -| `.regular` | Standard UI: toolbars, buttons, nav bars | Medium | -| `.clear` | Media-rich backgrounds where content is bold/bright | High | -| `.identity` | Conditionally disable glass (accessibility) | None | - -### 3. GlassEffectContainer (Critical) - -Glass cannot sample other glass. Nearby glass elements MUST share a container for visual consistency and morphing. - -```swift -GlassEffectContainer(spacing: 30.0) { - Button("Action 1") { } - .glassEffect() - .glassEffectID("btn1", in: namespace) - - Button("Action 2") { } - .glassEffect() - .glassEffectID("btn2", in: namespace) -} -``` - -### 4. Morphing Transitions - -Use `@Namespace` + `glassEffectID` inside a `GlassEffectContainer` for smooth glass morphing: - -```swift -@Namespace private var namespace -@State private var isExpanded = false - -GlassEffectContainer(spacing: 16) { - if isExpanded { - ForEach(items) { item in - ItemView(item: item) - .glassEffect(.regular, in: .rect(cornerRadius: 24)) - .glassEffectID(item.id, in: namespace) - } - } - Button { - withAnimation { isExpanded.toggle() } - } label: { Label("Toggle", systemImage: "chevron.down") } - .buttonStyle(.glass) - .glassEffectID("toggle", in: namespace) -} -``` - -### 5. Background Extension Effect - -Extends and blurs visual content behind navigation elements (toolbars, sidebars, inspectors): - -```swift -Image(landmark.backgroundImageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .backgroundExtensionEffect() -``` - -### 6. Glass Buttons - -```swift -// Standard glass button -Button("Action") { } - .buttonStyle(.glass) - -// Prominent glass button (for primary actions) -Button("Save") { } - .buttonStyle(.glassProminent) -``` - -### 7. Toolbars with Glass - -Toolbar items automatically receive glass styling. Use `ToolbarSpacer` and `ToolbarItemGroup` for layout: - -```swift -.toolbar { - ToolbarSpacer(.flexible) - ToolbarItem { ShareLink(item: data, preview: preview) } - ToolbarSpacer(.fixed) - ToolbarItemGroup { - Button("Favorite", systemImage: "heart") { } - Button("Add", systemImage: "plus") { } - } - ToolbarItem { - Button("Info", systemImage: "info") { } - } -} -.toolbar(removing: .title) // Remove title for clean glass toolbar -``` - -### 8. Tab Bars - -Tab bars automatically adopt glass when compiled with Xcode 26: - -```swift -TabView { - Tab("Home", systemImage: "house") { HomeView() } - Tab("Search", systemImage: "magnifyingglass") { SearchView() } -} -.tabBarMinimizeBehavior(.onScrollDown) // Collapse tab bar on scroll -``` - -### 9. Sheets with Glass - -Partial-height sheets automatically get Liquid Glass backgrounds: - -```swift -.sheet(isPresented: $showSheet) { - SheetContent() - .presentationDetents([.medium, .large]) - // Do NOT add .presentationBackground() - system handles it -} -``` - -## Migration Workflow (Existing Apps) - -For detailed migration steps, see [references/migration-guide.md](references/migration-guide.md). - -**Summary:** -1. Compile with Xcode 26 SDK - system components auto-adopt glass -2. Remove custom toolbar backgrounds (`.toolbarBackground`) -3. Replace custom materials on navigation elements with glass modifiers -4. Wrap grouped glass elements in `GlassEffectContainer` -5. Add `.backgroundExtensionEffect()` to hero images -6. Update SF Symbol variants (circle variants -> none variants on iOS 26+) -7. Test accessibility (Reduced Transparency, Increased Contrast, Reduced Motion) - -## Platform Considerations - -For platform-specific details, see [references/platform-specifics.md](references/platform-specifics.md). - -**Key differences:** -- **macOS**: Use `.tint(.clear)` on glass buttons for proper rendering; use `WindowBackgroundShapeStyle.windowBackground` instead of `Material` for editing backgrounds -- **iOS (iPhone)**: `.interactive()` works; use `UIDevice.current.userInterfaceIdiom` for layout -- **iPadOS**: Larger grid sizes; sidebar adaptable tab views -- **Conditional compilation**: Use `#if os(macOS)` / `#if os(iOS)` for platform-specific code - -## Common Pitfalls - -For detailed pitfalls and solutions, see [references/pitfalls-and-solutions.md](references/pitfalls-and-solutions.md). - -**Critical issues:** -- Glass elements outside `GlassEffectContainer` produce inconsistent visuals -- `rotationEffect` on glass views causes shape morphing - bridge to UIKit with `UIGlassEffect` -- Menu labels with glass cause animation artifacts - use custom `ButtonStyle` -- Hit-testing only registers on content, not glass area - use `contentShape()` to fix -- Multiple glass effects = multiple `CABackdropLayer` instances (3 offscreen textures each) - use containers to group - -## Real-World Example Patterns - -For complete code patterns from Apple's Landmarks sample app, see [examples/landmarks-patterns.md](examples/landmarks-patterns.md). - -## Architecture Best Practices - -1. **NavigationSplitView** as app root with glass sidebar -2. **NavigationStack** for deep navigation within detail columns -3. **@Observable** data model with `@Environment` injection -4. **FlexibleHeader** pattern: stretching hero images with scroll-linked parallax -5. **.inspector()** for supplementary detail panels -6. **.searchable()** for global search (auto-styled with glass) -7. Use `ToolbarSpacer(.flexible)` and `ToolbarSpacer(.fixed)` for toolbar layout -8. Prefer symbol-based buttons with text labels in toolbars -9. Use `.symbolVariant()` modifier for SF Symbol state changes -10. Use `.symbolEffect(.drawOn)` for animated icon transitions diff --git a/skills/liquid-glass/agents/openai.yaml b/skills/liquid-glass/agents/openai.yaml deleted file mode 100644 index 5b1f887..0000000 --- a/skills/liquid-glass/agents/openai.yaml +++ /dev/null @@ -1,2 +0,0 @@ -policy: - allow_implicit_invocation: false diff --git a/skills/liquid-glass/examples/landmarks-patterns.md b/skills/liquid-glass/examples/landmarks-patterns.md deleted file mode 100644 index 2e91845..0000000 --- a/skills/liquid-glass/examples/landmarks-patterns.md +++ /dev/null @@ -1,481 +0,0 @@ -# Real-World Liquid Glass Patterns: Apple Landmarks Sample App - -Code patterns extracted from Apple's official Landmarks sample app demonstrating production-quality Liquid Glass implementation. Source: [Landmarks: Building an app with Liquid Glass](https://developer.apple.com/documentation/SwiftUI/Landmarks-Building-an-app-with-Liquid-Glass) - -## Table of Contents - -1. [App Architecture](#app-architecture) -2. [Glass Badge Overlay with Morphing](#glass-badge-overlay-with-morphing) -3. [Background Extension for Hero Images](#background-extension-for-hero-images) -4. [Flexible Header with Parallax Scrolling](#flexible-header-with-parallax-scrolling) -5. [Glass Toolbar Layout](#glass-toolbar-layout) -6. [NavigationSplitView with Glass](#navigationsplitview-with-glass) -7. [Platform-Specific Constants](#platform-specific-constants) -8. [Collection Detail with Editing Mode](#collection-detail-with-editing-mode) -9. [Inspector Panel Pattern](#inspector-panel-pattern) - ---- - -## App Architecture - -The app uses a single `@Observable` data model injected via `.environment()`: - -```swift -@main -struct LandmarksApp: App { - @State private var modelData = ModelData() - - var body: some Scene { - WindowGroup { - LandmarksSplitView() - .environment(modelData) - .frame(minWidth: 375.0, minHeight: 375.0) - .onGeometryChange(for: CGSize.self) { geometry in - geometry.size - } action: { - modelData.windowSize = $0 - } - } - } -} -``` - -Key: Window size is tracked via `onGeometryChange` for responsive header calculations. - ---- - -## Glass Badge Overlay with Morphing - -A floating badge overlay using `GlassEffectContainer` with coordinated morphing animations. This is the most comprehensive glass pattern in the app. - -```swift -struct BadgesView: View { - @Environment(ModelData.self) private var modelData - @State private var isExpanded: Bool = false - @Namespace private var namespace - - var body: some View { - // GlassEffectContainer groups all glass elements for shared sampling - GlassEffectContainer(spacing: 16.0) { - VStack(alignment: .center, spacing: 20.0) { - if isExpanded { - VStack(spacing: 14.0) { - ForEach(modelData.earnedBadges) { - BadgeLabel(badge: $0) - // Glass with rounded rectangle shape - .glassEffect(.regular, in: .rect(cornerRadius: 24.0)) - // ID for morphing animations - .glassEffectID($0.id, in: namespace) - } - } - } - - Button { - withAnimation { - isExpanded.toggle() - } - } label: { - ToggleBadgesLabel(isExpanded: isExpanded) - .frame(width: 24.0, height: 32.0) - } - // Glass button style - .buttonStyle(.glass) - #if os(macOS) - .tint(.clear) // Required on macOS - #endif - // ID for morphing with badge elements - .glassEffectID("togglebutton", in: namespace) - } - .frame(width: 74.0) - } - } -} -``` - -**Overlay placement** via custom ViewModifier: - -```swift -private struct ShowsBadgesViewModifier: ViewModifier { - func body(content: Content) -> some View { - ZStack { - content - HStack { - Spacer() - VStack { - Spacer() - BadgesView() - .padding() - } - } - } - } -} - -extension View { - func showsBadges() -> some View { - modifier(ShowsBadgesViewModifier()) - } -} -``` - -Usage: `.showsBadges()` on any view that should display the floating badge overlay. - ---- - -## Background Extension for Hero Images - -Two patterns for extending images behind the navigation layer. - -### Featured Item (Landing Page) - -```swift -struct LandmarkFeaturedItemView: View { - @Environment(ModelData.self) var modelData - let landmark: Landmark - - var body: some View { - NavigationLink(value: landmark) { - Image(decorative: landmark.backgroundImageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .clipped() - .backgroundExtensionEffect() // Extends behind navigation - .overlay(alignment: .bottom) { - VStack { - Text("Featured Landmark") - .font(.subheadline).fontWeight(.bold) - .foregroundColor(.white).opacity(0.8) - Text(landmark.name) - .font(.largeTitle).fontWeight(.bold) - .foregroundColor(.white) - Button("Learn More") { - modelData.path.append(landmark) - } - .buttonStyle(.borderedProminent) - .padding(.bottom, 6.0) - } - .padding(.bottom, 40.0) - } - } - .buttonStyle(.plain) - } -} -``` - -### Detail View (Full Bleed) - -```swift -struct LandmarkDetailView: View { - let landmark: Landmark - - var body: some View { - ScrollView(.vertical) { - VStack(alignment: .leading, spacing: 14.0) { - Image(landmark.backgroundImageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .backgroundExtensionEffect() // Key modifier - .flexibleHeaderContent() // Parallax stretching - - VStack(alignment: .leading) { - Text(landmark.name).font(.title).fontWeight(.bold) - Text(landmark.description).textSelection(.enabled) - } - .padding(.leading, 26.0) - .padding(.trailing, 52.0) - } - } - .flexibleHeaderScrollView() // Scroll tracking for parallax - .ignoresSafeArea(edges: .top) // Full bleed - .toolbar(removing: .title) // Clean glass toolbar - } -} -``` - ---- - -## Flexible Header with Parallax Scrolling - -Custom view modifiers that create a stretching parallax effect when scrolling past the top bounds. - -```swift -@Observable private class FlexibleHeaderGeometry { - var offset: CGFloat = 0 -} - -private struct FlexibleHeaderContentModifier: ViewModifier { - @Environment(ModelData.self) private var modelData - @Environment(FlexibleHeaderGeometry.self) private var geometry - - func body(content: Content) -> some View { - let height = (modelData.windowSize.height / 2) - geometry.offset - content - .frame(height: height) - .padding(.bottom, geometry.offset) - .offset(y: geometry.offset) - } -} - -private struct FlexibleHeaderScrollViewModifier: ViewModifier { - @State private var geometry = FlexibleHeaderGeometry() - - func body(content: Content) -> some View { - content - .onScrollGeometryChange(for: CGFloat.self) { geometry in - min(geometry.contentOffset.y + geometry.contentInsets.top, 0) - } action: { _, offset in - geometry.offset = offset - } - .environment(geometry) - } -} - -// Extensions for clean API -extension ScrollView { - @MainActor func flexibleHeaderScrollView() -> some View { - modifier(FlexibleHeaderScrollViewModifier()) - } -} - -extension View { - func flexibleHeaderContent() -> some View { - modifier(FlexibleHeaderContentModifier()) - } -} -``` - -Usage pattern: -1. Apply `.flexibleHeaderContent()` to the hero image -2. Apply `.flexibleHeaderScrollView()` to the parent `ScrollView` -3. Track window size via `onGeometryChange` at the app level - ---- - -## Glass Toolbar Layout - -Comprehensive toolbar using spacers, groups, and glass-styled items: - -```swift -.toolbar { - ToolbarSpacer(.flexible) // Push items to trailing - - ToolbarItem { - ShareLink(item: landmark, preview: landmark.sharePreview) - } - - ToolbarSpacer(.fixed) // Fixed gap - - ToolbarItemGroup { // Grouped actions - LandmarkFavoriteButton(landmark: landmark) - LandmarkCollectionsMenu(landmark: landmark) - } - - ToolbarSpacer(.fixed) - - ToolbarItem { - Button("Info", systemImage: "info") { - modelData.selectedLandmark = landmark - modelData.isLandmarkInspectorPresented.toggle() - } - } -} -.toolbar(removing: .title) // Clean glass appearance -``` - ---- - -## NavigationSplitView with Glass - -The root navigation with sidebar, detail, search, and inspector: - -```swift -struct LandmarksSplitView: View { - @Environment(ModelData.self) var modelData - @State private var preferredColumn: NavigationSplitViewColumn = .detail - - var body: some View { - @Bindable var modelData = modelData - - NavigationSplitView(preferredCompactColumn: $preferredColumn) { - List { - Section { - ForEach(NavigationOptions.mainPages) { page in - NavigationLink(value: page) { - Label(page.name, systemImage: page.symbolName) - } - } - } - } - .navigationDestination(for: NavigationOptions.self) { page in - NavigationStack(path: $modelData.path) { - page.viewForPage() - } - .navigationDestination(for: Landmark.self) { landmark in - LandmarkDetailView(landmark: landmark) - } - .showsBadges() // Glass badge overlay - } - .frame(minWidth: 150) - } detail: { - NavigationStack(path: $modelData.path) { - NavigationOptions.landmarks.viewForPage() - } - .navigationDestination(for: Landmark.self) { landmark in - LandmarkDetailView(landmark: landmark) - } - .showsBadges() // Glass badge overlay - } - .searchable(text: $modelData.searchString, prompt: "Search") - .inspector(isPresented: $modelData.isLandmarkInspectorPresented) { - if let landmark = modelData.selectedLandmark { - LandmarkDetailInspectorView( - landmark: landmark, - inspectorIsPresented: $modelData.isLandmarkInspectorPresented - ) - } - } - } -} -``` - ---- - -## Platform-Specific Constants - -Centralized constants with platform-aware sizing: - -```swift -struct Constants { - // Glass-related constants - static let badgeGlassSpacing: CGFloat = 16.0 // GlassEffectContainer spacing - static let badgeCornerRadius: CGFloat = 24.0 // Glass shape corner radius - static let badgeSize: CGFloat = 52.0 - static let badgeFrameWidth: CGFloat = 74.0 - - // Platform-specific grid sizes - @MainActor static var collectionGridItemMinSize: CGFloat { - #if os(iOS) - if UIDevice.current.userInterfaceIdiom == .pad { - return 220.0 - } else { - return 160.0 - } - #else - return 220.0 - #endif - } - - // Platform-specific background styles - #if os(macOS) - static let editingBackgroundStyle = WindowBackgroundShapeStyle.windowBackground - #else - static let editingBackgroundStyle = Material.ultraThickMaterial - #endif -} -``` - ---- - -## Collection Detail with Editing Mode - -Animated editing transitions with symbol effects: - -```swift -struct CollectionDetailView: View { - @Environment(\.colorScheme) var colorScheme - @State var isEditing: Bool = false - - var body: some View { - ScrollView(.vertical) { - if isEditing { - CollectionDetailEditingView(collection: collection, ...) - } else { - CollectionDetailDisplayView(collection: collection) - } - } - #if os(iOS) - .background(Color(uiColor: isEditing && colorScheme == .light - ? .systemGray5 : .systemBackground)) - #endif - #if os(macOS) - .background(Color(nsColor: isEditing && colorScheme == .light - ? .secondarySystemFill : .windowBackgroundColor)) - #endif - .toolbar(removing: .title) - .toolbar { - ToolbarItem(placement: editButtonPlacement) { - Button { - withAnimation { isEditing.toggle() } - } label: { - if isEditing { - Image(systemName: "checkmark") - .transition(.editButtonTransition()) - } else { - Text("Edit") - } - } - } - } - } -} - -extension AnyTransition { - @MainActor static func editButtonTransition() -> AnyTransition { - .asymmetric( - insertion: .init(.symbolEffect(.drawOn)), - removal: .opacity - ) - } -} -``` - ---- - -## Inspector Panel Pattern - -Used for supplementary content alongside detail views: - -```swift -// On the NavigationSplitView or parent: -.inspector(isPresented: $modelData.isLandmarkInspectorPresented) { - if let landmark = modelData.selectedLandmark { - LandmarkDetailInspectorView( - landmark: landmark, - inspectorIsPresented: $modelData.isLandmarkInspectorPresented - ) - } else { - EmptyView() - } -} - -// The inspector view itself: -struct LandmarkDetailInspectorView: View { - let landmark: Landmark - @Binding var inspectorIsPresented: Bool - - var body: some View { - ScrollView { - // Map, activities, elevation info - LandmarkDetailMapView(landmark: landmark) - // ... more detail content - } - #if os(iOS) - .toolbarVisibility( - UIDevice.current.userInterfaceIdiom == .phone ? .visible : .hidden, - for: .automatic - ) - .toolbar { - if UIDevice.current.userInterfaceIdiom == .phone { - Button { - inspectorIsPresented.toggle() - } label: { - Label("Dismiss", systemImage: "xmark") - } - } - } - #endif - } -} -``` diff --git a/skills/liquid-glass/references/api-reference.md b/skills/liquid-glass/references/api-reference.md deleted file mode 100644 index 51852be..0000000 --- a/skills/liquid-glass/references/api-reference.md +++ /dev/null @@ -1,410 +0,0 @@ -# Liquid Glass API Reference - -Complete API reference for all SwiftUI Liquid Glass modifiers and types. Always verify against the latest Apple documentation as APIs may change between betas. - -## Table of Contents - -1. [glassEffect Modifier](#glasseffect-modifier) -2. [Glass Struct](#glass-struct) -3. [GlassEffectContainer](#glasseffectcontainer) -4. [glassEffectID](#glasseffectid) -5. [glassEffectUnion](#glasseffectunion) -6. [backgroundExtensionEffect](#backgroundextensioneffect) -7. [Button Styles](#button-styles) -8. [Toolbar Glass APIs](#toolbar-glass-apis) -9. [TabView Glass APIs](#tabview-glass-apis) -10. [Sheet Glass APIs](#sheet-glass-apis) -11. [Accessibility APIs](#accessibility-apis) -12. [UIKit Bridge](#uikit-bridge) - ---- - -## glassEffect Modifier - -Applies the Liquid Glass material to a view. - -```swift -func glassEffect( - _ glass: Glass = .regular, - in shape: S = DefaultGlassEffectShape, // capsule by default - isEnabled: Bool = true -) -> some View -``` - -**Parameters:** -- `glass`: The glass material style (`.regular`, `.clear`, `.identity`) -- `shape`: The shape of the glass effect background -- `isEnabled`: Whether the glass effect is active - -**Supported shapes:** -- `.capsule` (default) -- `.circle` -- `RoundedRectangle(cornerRadius: 16)` -- `.rect(cornerRadius: .containerConcentric)` - auto-aligns with container corners -- `.ellipse` -- Any custom `Shape` conformance - -**Examples:** - -```swift -// Default capsule shape -Text("Glass").padding().glassEffect() - -// Rounded rectangle -view.glassEffect(.regular, in: .rect(cornerRadius: 24)) - -// Circle -view.glassEffect(.regular, in: .circle) - -// Concentric corners (matches container) -view.glassEffect(.regular, in: .rect(cornerRadius: .containerConcentric)) - -// Conditionally enabled -view.glassEffect(.regular, isEnabled: showGlass) -``` - -**Availability:** iOS 26.0+, macOS 26.0+, iPadOS 26.0+, watchOS 26.0+, tvOS 26.0+, visionOS 26.0+ - ---- - -## Glass Struct - -The `Glass` struct defines the material appearance. - -### Static Properties - -| Property | Description | -|----------|-------------| -| `.regular` | Standard glass: medium transparency, full adaptation. For toolbars, buttons, nav bars | -| `.clear` | High transparency, limited adaptation. For media-rich backgrounds with bold/bright overlay content | -| `.identity` | No glass effect. For conditionally disabling glass | - -### Instance Methods - -```swift -// Add a tint color -func tint(_ color: Color) -> Glass - -// Enable interactive behavior (scaling, bouncing, shimmering on touch) -func interactive() -> Glass -``` - -**Method chaining:** Order-independent. - -```swift -.glassEffect(.regular.tint(.blue).interactive()) -// same as -.glassEffect(.regular.interactive().tint(.blue)) -``` - -**Tint examples:** - -```swift -.glassEffect(.regular.tint(.blue)) -.glassEffect(.regular.tint(.purple.opacity(0.6))) -.glassEffect(.regular.tint(Color("accentColor"))) -``` - -**Interactive behavior (iOS only):** -- Scales on press -- Bouncing animation on release -- Shimmering effect -- Touch-point illumination radiating to nearby glass elements -- Tap and drag responsiveness - ---- - -## GlassEffectContainer - -Groups multiple glass elements into a unified composition. Critical for visual consistency because glass cannot sample other glass. - -```swift -struct GlassEffectContainer: View { - init(spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) - init(@ViewBuilder content: () -> Content) -} -``` - -**Parameters:** -- `spacing`: Controls morphing threshold. Elements within this distance visually blend during transitions. - -**Why required:** Each glass element creates a `CABackdropLayer` with 3 offscreen textures. Containers share sampling regions, reducing texture count and improving rendering performance. - -```swift -GlassEffectContainer(spacing: 30.0) { - // All glass elements share the same sampling region - Button("A") { }.glassEffect() - Button("B") { }.glassEffect() - Button("C") { }.glassEffect() -} -``` - ---- - -## glassEffectID - -Links glass elements for morphing transitions within a `GlassEffectContainer`. - -```swift -func glassEffectID( - _ id: ID, - in namespace: Namespace.ID -) -> some View -``` - -**Requirements for morphing:** -1. Elements must be in the same `GlassEffectContainer` -2. Each view needs `glassEffectID` with a shared `@Namespace` -3. Conditional show/hide triggers morphing animation -4. Wrap state change in `withAnimation` - -```swift -@Namespace private var namespace -@State private var showExtra = false - -GlassEffectContainer(spacing: 16) { - Button("Toggle") { - withAnimation(.bouncy) { showExtra.toggle() } - } - .glassEffect() - .glassEffectID("toggle", in: namespace) - - if showExtra { - Button("Extra") { } - .glassEffect() - .glassEffectID("extra", in: namespace) - } -} -``` - ---- - -## glassEffectUnion - -Combines multiple glass effects into a single unified glass shape. - -```swift -func glassEffectUnion( - _ id: ID, - in namespace: Namespace.ID -) -> some View -``` - -Use when multiple separate views should appear as one continuous glass surface. - ---- - -## backgroundExtensionEffect - -Extends and blurs visual content beyond a view's bounds, creating continuous backgrounds behind navigation elements. - -```swift -func backgroundExtensionEffect() -> some View -``` - -**Use cases:** -- Hero images that extend behind toolbars/navigation bars -- Detail view images extending behind sidebar/inspector -- Featured content spanning multiple UI sections - -**Works with:** -- `NavigationSplitView` detail columns -- Sidebars and inspectors -- Toolbars and navigation bars -- Custom overlay controls - -```swift -Image(imageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .backgroundExtensionEffect() -``` - -**Best practices:** -- Test legibility across different images, themes, and contexts -- Combine with gradients for text readability over extended backgrounds -- Use with `.safeAreaInset()` for overlay content positioning -- Pairs well with `.clipShape()` for rounded corners - ---- - -## Button Styles - -### .glass - -Standard glass button for secondary/common actions: - -```swift -Button("Action") { } - .buttonStyle(.glass) -``` - -### .glassProminent - -Prominent glass button with full tint surface for primary actions: - -```swift -Button("Save") { } - .buttonStyle(.glassProminent) -``` - -**Note:** `.confirmationAction` toolbar placement auto-applies `.glassProminent`. - -**macOS note:** Apply `.tint(.clear)` on glass buttons for proper rendering: - -```swift -Button("Action") { } - .buttonStyle(.glass) - #if os(macOS) - .tint(.clear) - #endif -``` - ---- - -## Toolbar Glass APIs - -Toolbar items automatically receive glass styling when compiled with Xcode 26. - -### ToolbarSpacer - -```swift -ToolbarSpacer(.flexible) // Expands to fill available space -ToolbarSpacer(.fixed) // System-default fixed spacing -``` - -### ToolbarItemGroup - -Groups related actions with consistent glass styling: - -```swift -ToolbarItemGroup { - Button("Draw", systemImage: "pencil") { } - Button("Erase", systemImage: "eraser") { } -} -``` - -### Placement-Driven Styling - -| Placement | Auto-applied style | -|-----------|-------------------| -| `.confirmationAction` | `.glassProminent` | -| `.cancellationAction` | Standard glass | -| `.primaryAction` | Grouping-capable | -| `.topBarLeading` / `.topBarTrailing` | Navigation areas | -| `.secondaryAction` | Overflow menu (macOS) | - -### Removing Title - -```swift -.toolbar(removing: .title) -``` - ---- - -## TabView Glass APIs - -### Automatic Glass - -Tab bars auto-adopt glass when compiled with Xcode 26 SDK. No code changes needed. - -### Tab Bar Minimize - -```swift -TabView { /* tabs */ } - .tabBarMinimizeBehavior(.onScrollDown) -``` - -### Bottom Accessory - -```swift -.tabViewBottomAccessory { - Button("Quick Action") { } - .glassEffect(.regular.interactive()) -} -``` - -### Search Role Tab - -```swift -Tab("Search", systemImage: "magnifyingglass", role: .search) { - SearchView() -} -``` - ---- - -## Sheet Glass APIs - -Partial-height sheets auto-receive Liquid Glass background. - -**Requirements:** -- Specify at least one partial-height detent: `.presentationDetents([.medium, .large])` -- Do NOT use `.presentationBackground()` - system handles glass automatically -- View must be inside `NavigationStack` or `NavigationSplitView` for transitions - -### Morphing Sheet Transitions - -```swift -@Namespace private var transition - -// Source button -Button("Info", systemImage: "info") { showSheet = true } - .matchedTransitionSource(id: "info", in: transition) - -// Sheet content -.sheet(isPresented: $showSheet) { - InfoView() - .navigationTransition(.zoom(sourceID: "info", in: transition)) - .presentationDetents([.medium, .large]) -} -``` - ---- - -## Accessibility APIs - -Liquid Glass automatically adapts for accessibility settings. No code changes required for: - -- **Reduced Transparency:** Increases frosting for clarity -- **Increased Contrast:** Applies stark colors and borders -- **Reduced Motion:** Tones down animations/elastic effects -- **Tinted Mode (iOS 26.1+):** User-controlled opacity increase - -### Manual Override (when needed) - -```swift -@Environment(\.accessibilityReduceTransparency) var reduceTransparency - -var body: some View { - content - .glassEffect(reduceTransparency ? .identity : .regular) -} -``` - ---- - -## UIKit Bridge - -For cases where SwiftUI glass has rendering issues (e.g., rotation animations): - -```swift -// UIKit glass effect -let glassEffect = UIGlassEffect() -let effectView = UIVisualEffectView(effect: glassEffect) - -// In UIViewRepresentable -struct GlassView: UIViewRepresentable { - func makeUIView(context: Context) -> UIVisualEffectView { - let effect = UIGlassEffect() - return UIVisualEffectView(effect: effect) - } - func updateUIView(_ uiView: UIVisualEffectView, context: Context) { } -} -``` - -Use this bridge when: -- `rotationEffect` causes glass shape morphing artifacts -- You need precise control over glass rendering in complex layouts -- Integrating glass into existing UIKit view hierarchies diff --git a/skills/liquid-glass/references/migration-guide.md b/skills/liquid-glass/references/migration-guide.md deleted file mode 100644 index 9fc7705..0000000 --- a/skills/liquid-glass/references/migration-guide.md +++ /dev/null @@ -1,371 +0,0 @@ -# Liquid Glass Migration Guide - -Step-by-step guide for migrating existing iOS/macOS/iPadOS apps to the Liquid Glass design system. - -## Table of Contents - -1. [Phase 1: Automatic Adoption](#phase-1-automatic-adoption) -2. [Phase 2: Remove Conflicting Customizations](#phase-2-remove-conflicting-customizations) -3. [Phase 3: Enhance with Glass APIs](#phase-3-enhance-with-glass-apis) -4. [Phase 4: Platform-Specific Refinements](#phase-4-platform-specific-refinements) -5. [Phase 5: Accessibility and Testing](#phase-5-accessibility-and-testing) -6. [Backward Compatibility](#backward-compatibility) - ---- - -## Phase 1: Automatic Adoption - -**Do this first.** Many system components auto-adopt glass when compiled with Xcode 26. - -### What auto-adopts without code changes: -- Navigation bars (become glass) -- Tab bars (become glass with blur overlay) -- Toolbars (items get glass styling) -- Search bars (get glassy background) -- Sheets (partial-height sheets get glass background) -- Alerts and confirmation dialogs -- Context menus and popovers -- System controls (toggles, sliders, steppers) - -### Steps: -1. Open project in Xcode 26 -2. Set deployment target to iOS 26 / macOS 26 (or use `@available` checks) -3. Build and run - observe which elements already look correct -4. Document what needs manual adjustment - ---- - -## Phase 2: Remove Conflicting Customizations - -Custom backgrounds and materials on navigation elements conflict with automatic glass adoption. Remove or conditionally disable them. - -### Navigation bar backgrounds - -```swift -// REMOVE or conditionally disable: -.toolbarBackground(.visible, for: .navigationBar) -.toolbarBackground(Color.blue, for: .navigationBar) - -// If backward compatibility needed: -if #unavailable(iOS 26) { - view.toolbarBackground(.visible, for: .navigationBar) -} -``` - -### Tab bar backgrounds - -```swift -// REMOVE custom tab bar appearances: -UITabBar.appearance().backgroundColor = .systemBackground // Remove - -// REMOVE: -.toolbarBackground(.visible, for: .tabBar) -``` - -### Custom toolbar materials - -```swift -// REMOVE: -.toolbar { ... } -.toolbarBackground(.ultraThinMaterial, for: .navigationBar) - -// Glass is applied automatically -``` - -### Sheet backgrounds - -```swift -// REMOVE on iOS 26: -.presentationBackground(.ultraThinMaterial) - -// System applies Liquid Glass automatically for partial-height sheets -// Only specify detents: -.presentationDetents([.medium, .large]) -``` - -### Inline navigation title display mode - -Review `.navigationBarTitleDisplayMode(.inline)` usage. With glass, the system may handle title presentation differently. - ---- - -## Phase 3: Enhance with Glass APIs - -After removing conflicts, enhance your app with Liquid Glass APIs. - -### 3a. Add backgroundExtensionEffect to hero content - -Find hero images or featured content that should extend behind the navigation layer: - -```swift -// Before: -Image(imageName) - .resizable() - .aspectRatio(contentMode: .fill) - -// After: -Image(imageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .backgroundExtensionEffect() -``` - -Combine with `.ignoresSafeArea(edges: .top)` on the scroll view for full-bleed effect. - -### 3b. Convert custom floating buttons to glass - -```swift -// Before: -Button("Action") { } - .background(.ultraThinMaterial) - .clipShape(Capsule()) - -// After: -Button("Action") { } - .buttonStyle(.glass) -// or for interactive: - .glassEffect(.regular.interactive()) -``` - -### 3c. Wrap grouped glass elements in GlassEffectContainer - -Find places where multiple glass elements appear near each other: - -```swift -// Before: -VStack { - Button("A") { }.glassEffect() - Button("B") { }.glassEffect() -} - -// After: -GlassEffectContainer(spacing: 16) { - VStack { - Button("A") { }.glassEffect() - Button("B") { }.glassEffect() - } -} -``` - -### 3d. Update toolbar layout with spacers - -```swift -// Before: -.toolbar { - ToolbarItem(placement: .topBarTrailing) { - HStack { - Button("Share") { } - Button("Favorite") { } - Button("Info") { } - } - } -} - -// After: -.toolbar { - ToolbarSpacer(.flexible) - ToolbarItem { ShareLink(item: data) } - ToolbarSpacer(.fixed) - ToolbarItemGroup { - Button("Favorite", systemImage: "heart") { } - Button("More", systemImage: "ellipsis") { } - } - ToolbarItem { - Button("Info", systemImage: "info") { } - } -} -.toolbar(removing: .title) -``` - -### 3e. Add morphing animations with glassEffectID - -For elements that appear/disappear, add coordinated morphing: - -```swift -@Namespace private var namespace - -GlassEffectContainer(spacing: 16) { - if showItems { - ForEach(items) { item in - ItemView(item: item) - .glassEffect(.regular, in: .rect(cornerRadius: 16)) - .glassEffectID(item.id, in: namespace) - } - } - toggleButton - .buttonStyle(.glass) - .glassEffectID("toggle", in: namespace) -} -``` - -### 3f. Enable tab bar minimization - -```swift -TabView { /* tabs */ } - .tabBarMinimizeBehavior(.onScrollDown) -``` - -### 3g. Add sheet morphing transitions - -```swift -@Namespace private var transition - -// Toolbar button that opens sheet -Button("Info", systemImage: "info") { showInfo = true } - .matchedTransitionSource(id: "info", in: transition) - -// Sheet with morphing -.sheet(isPresented: $showInfo) { - InfoView() - .navigationTransition(.zoom(sourceID: "info", in: transition)) - .presentationDetents([.medium, .large]) -} -``` - ---- - -## Phase 4: Platform-Specific Refinements - -### macOS - -```swift -#if os(macOS) -// Glass buttons need clear tint for proper rendering -.tint(.clear) - -// Use WindowBackgroundShapeStyle for editing backgrounds -static let editingBackgroundStyle = WindowBackgroundShapeStyle.windowBackground - -// Use nsColor for background colors -.background(Color(nsColor: .windowBackgroundColor)) - -// Toolbar delete button placement -let deleteButtonPlacement: ToolbarItemPlacement = .secondaryAction -#endif -``` - -### iOS (iPhone) - -```swift -#if os(iOS) -// Device-specific layouts -if UIDevice.current.userInterfaceIdiom == .phone { - // Phone-specific toolbar visibility - .toolbarVisibility(.visible, for: .automatic) -} - -// Use uiColor for background colors -.background(Color(uiColor: .systemBackground)) - -// Use Material for editing backgrounds -static let editingBackgroundStyle = Material.ultraThickMaterial - -// Inline title display mode -.navigationBarTitleDisplayMode(.inline) - -// Toolbar placement -let deleteButtonPlacement: ToolbarItemPlacement = .topBarLeading -let editButtonPlacement: ToolbarItemPlacement = .topBarTrailing -#endif -``` - -### iPadOS - -```swift -#if os(iOS) -if UIDevice.current.userInterfaceIdiom == .pad { - // Larger grid item sizes - return 220.0 // vs 160.0 on iPhone -} -#endif -``` - ---- - -## Phase 5: Accessibility and Testing - -### Automatic Accessibility - -Liquid Glass auto-adapts for these settings (no code needed): -- Reduced Transparency -- Increased Contrast -- Reduced Motion -- Tinted Mode (iOS 26.1+) - -### Manual Testing Checklist - -1. **Reduced Transparency ON**: Verify glass elements are legible with increased frosting -2. **Increased Contrast ON**: Check borders and colors are visible -3. **Reduced Motion ON**: Confirm animations are subtle -4. **Dark Mode**: Test all glass elements in both light and dark color schemes -5. **Dynamic Type**: Verify glass containers resize properly with large text -6. **VoiceOver**: Ensure all glass controls have proper accessibility labels - -### Manual Accessibility Override - -```swift -@Environment(\.accessibilityReduceTransparency) var reduceTransparency -@Environment(\.accessibilityReduceMotion) var reduceMotion - -content - .glassEffect(reduceTransparency ? .identity : .regular) -``` - ---- - -## Backward Compatibility - -### Using @available for Gradual Adoption - -```swift -if #available(iOS 26, macOS 26, *) { - view.glassEffect(.regular, in: .rect(cornerRadius: 16)) -} else { - view.background(.ultraThinMaterial) - .clipShape(RoundedRectangle(cornerRadius: 16)) -} -``` - -### Conditional Toolbar Styles - -```swift -struct ToolbarLabelStyle: LabelStyle { - func makeBody(configuration: Configuration) -> some View { - if #available(iOS 26, *) { - Label(configuration) - } else { - Label(configuration).labelStyle(.titleOnly) - } - } -} -``` - -### SF Symbol Variants - -Replace circle-variant SF Symbols with plain variants on iOS 26+: - -```swift -// Use symbolVariant instead of string manipulation -Image(systemName: "heart") - .symbolVariant(isFavorite ? .fill : .none) -``` - -### Supporting Multiple OS Versions - -```swift -struct GlassButton: View { - let title: String - let action: () -> Void - - var body: some View { - if #available(iOS 26, macOS 26, *) { - Button(title, action: action) - .buttonStyle(.glass) - } else { - Button(title, action: action) - .buttonStyle(.bordered) - } - } -} -``` diff --git a/skills/liquid-glass/references/pitfalls-and-solutions.md b/skills/liquid-glass/references/pitfalls-and-solutions.md deleted file mode 100644 index f612b9d..0000000 --- a/skills/liquid-glass/references/pitfalls-and-solutions.md +++ /dev/null @@ -1,371 +0,0 @@ -# Liquid Glass: Common Pitfalls and Solutions - -Known issues, workarounds, and best practices when working with Liquid Glass. - -## Table of Contents - -1. [Visual Issues](#visual-issues) -2. [Animation Issues](#animation-issues) -3. [Performance Issues](#performance-issues) -4. [Interaction Issues](#interaction-issues) -5. [Layout Issues](#layout-issues) -6. [Migration Issues](#migration-issues) - ---- - -## Visual Issues - -### Glass elements look inconsistent near each other - -**Problem:** Nearby glass elements have different visual appearances, breaking visual harmony. - -**Cause:** Glass cannot sample other glass. Without a shared container, each glass element creates its own sampling region. - -**Solution:** Wrap all nearby glass elements in a `GlassEffectContainer`: - -```swift -// BAD - inconsistent glass -VStack { - view1.glassEffect() - view2.glassEffect() -} - -// GOOD - shared sampling region -GlassEffectContainer(spacing: 16) { - VStack { - view1.glassEffect() - view2.glassEffect() - } -} -``` - -### Glass button rendering on macOS - -**Problem:** Glass buttons appear incorrectly tinted on macOS. - -**Solution:** Apply `.tint(.clear)` on macOS: - -```swift -Button("Action") { } - .buttonStyle(.glass) - #if os(macOS) - .tint(.clear) - #endif -``` - -### Sheet background conflicts - -**Problem:** Custom `.presentationBackground()` conflicts with automatic glass styling. - -**Solution:** Remove `.presentationBackground()` on iOS 26. The system applies Liquid Glass automatically for partial-height sheets: - -```swift -// BAD -.sheet(isPresented: $show) { - Content() - .presentationBackground(.ultraThinMaterial) // Remove this - .presentationDetents([.medium, .large]) -} - -// GOOD -.sheet(isPresented: $show) { - Content() - .presentationDetents([.medium, .large]) - // System handles glass background -} -``` - -### Text illegibility over glass backgrounds - -**Problem:** Text is hard to read when overlaid on `backgroundExtensionEffect` images. - -**Solution:** Layer gradients over extended backgrounds: - -```swift -Image(imageName) - .backgroundExtensionEffect() - .overlay(alignment: .bottom) { - VStack { - Text("Title").font(.largeTitle).bold().foregroundColor(.white) - } - .padding() - } - .safeAreaInset(edge: .bottom) { - textContent - .background(LinearGradient( - colors: [.clear, .black.opacity(0.5)], - startPoint: .top, endPoint: .bottom - )) - } -``` - ---- - -## Animation Issues - -### rotationEffect causes glass shape morphing - -**Problem:** When applying `rotationEffect(_:anchor:)` to views with glass effects, the glass shape morphs unpredictably during animation instead of rotating smoothly. - -**Solution:** Bridge to UIKit using `UIViewRepresentable` with `UIGlassEffect`: - -```swift -struct RotatableGlassView: UIViewRepresentable { - let rotation: Angle - - func makeUIView(context: Context) -> UIVisualEffectView { - let effect = UIGlassEffect() - let view = UIVisualEffectView(effect: effect) - return view - } - - func updateUIView(_ uiView: UIVisualEffectView, context: Context) { - uiView.transform = CGAffineTransform(rotationAngle: rotation.radians) - } -} -``` - -### Menu morphing animation glitches - -**Problem:** Applying glass effects directly to Menu labels causes animation artifacts - morphing starts/ends as rectangle, then snaps to circle. - -**Solution (varies by OS version):** - -iOS 26.0-26.0.1: -```swift -Menu { /* items */ } label: { - Label("More", systemImage: "ellipsis") -} -.glassEffect(.regular.interactive()) -``` - -iOS 26.1+: -```swift -struct GlassMenuStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding() - .glassEffect(.regular) - } -} - -Menu { /* items */ } label: { - Label("More", systemImage: "ellipsis") -} -.buttonStyle(GlassMenuStyle()) -``` - -### GlassEffectContainer breaks Menu morphing - -**Problem:** On iOS 26.1, wrapping Menus in `GlassEffectContainer` causes morphing to break (works on 26.0). - -**Solution:** Test across OS versions. Consider placing Menu glass effects outside the container or file feedback with Apple. - -### Morphing doesn't animate - -**Problem:** Elements with `glassEffectID` don't morph when shown/hidden. - -**Checklist:** -1. Elements are in the same `GlassEffectContainer`? -2. Each element has `glassEffectID` with the same `@Namespace`? -3. State change is wrapped in `withAnimation`? -4. Views are conditionally rendered (not just hidden/opacity)? - -```swift -// BAD - opacity doesn't trigger morph -view.opacity(isVisible ? 1 : 0) - .glassEffectID("item", in: namespace) - -// GOOD - conditional rendering triggers morph -if isVisible { - view.glassEffect() - .glassEffectID("item", in: namespace) -} -``` - ---- - -## Performance Issues - -### Multiple glass effects cause frame drops - -**Problem:** Many individual glass effects create excessive `CABackdropLayer` instances, each requiring 3 offscreen textures. - -**Solution:** Group glass elements in `GlassEffectContainer`: - -```swift -// BAD - 5 separate backdrop layers (15 textures) -ForEach(items) { item in - ItemView(item: item).glassEffect() -} - -// GOOD - shared backdrop layer -GlassEffectContainer(spacing: 16) { - ForEach(items) { item in - ItemView(item: item).glassEffect() - } -} -``` - -### ScrollView with many glass elements - -**Problem:** Long lists with glass effects cause memory pressure and jank. - -**Solution:** Use `LazyVStack`/`LazyHStack` and minimize glass elements in scrollable content. Apply glass to floating overlays, not list items: - -```swift -// BAD - glass on every list item -ScrollView { - LazyVStack { - ForEach(items) { item in - ItemRow(item: item).glassEffect() // Too many! - } - } -} - -// GOOD - glass on floating controls only -ScrollView { - LazyVStack { - ForEach(items) { item in - ItemRow(item: item) // No glass on content - } - } -} -.overlay(alignment: .bottomTrailing) { - FloatingButton().glassEffect(.regular.interactive()) -} -``` - ---- - -## Interaction Issues - -### Hit-testing failures on glass buttons - -**Problem:** Glass effect buttons only register taps on the content (symbol/text), not the entire glass area. - -**Solution:** Use `contentShape()` to define the hit region: - -```swift -Button("Action") { } - .padding() - .glassEffect(.regular, in: .rect(cornerRadius: 16)) - .contentShape(.rect(cornerRadius: 16)) -``` - -### Interactive glass not responding - -**Problem:** `.interactive()` glass doesn't scale/bounce on interaction. - -**Checklist:** -- `.interactive()` is iOS only (not macOS) -- The view must be interactive (Button, gesture attached) -- Verify the glass modifier is applied correctly: `.glassEffect(.regular.interactive())` - ---- - -## Layout Issues - -### backgroundExtensionEffect clipping - -**Problem:** Content with `backgroundExtensionEffect` gets clipped by parent views. - -**Solution:** Ensure proper frame and ignore safe area: - -```swift -Image(imageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .clipped() - .backgroundExtensionEffect() - -// On the parent ScrollView: -.ignoresSafeArea(edges: .top) -``` - -### Glass container sizing - -**Problem:** `GlassEffectContainer` doesn't size correctly. - -**Solution:** Apply frame constraints to the inner content, not the container: - -```swift -GlassEffectContainer(spacing: 16) { - VStack { - ForEach(items) { item in - ItemView(item: item) - .glassEffect() - .glassEffectID(item.id, in: namespace) - } - } - .frame(width: 74) // Frame on inner content -} -``` - ---- - -## Migration Issues - -### Custom toolbar backgrounds override glass - -**Problem:** Existing `.toolbarBackground()` modifiers prevent glass from appearing. - -**Solution:** Remove or conditionally disable custom toolbar backgrounds: - -```swift -.toolbar { /* items */ } -// Remove: .toolbarBackground(.visible, for: .navigationBar) -// Remove: .toolbarBackground(.ultraThinMaterial, for: .navigationBar) -``` - -### Old Material usage on navigation elements - -**Problem:** `.background(.ultraThinMaterial)` on navigation containers conflicts with glass. - -**Solution:** Remove material backgrounds from navigation elements. Glass handles this automatically. Keep materials only on content backgrounds where appropriate: - -```swift -// OK - material on content editing background -#if os(macOS) -static let editingBackgroundStyle = WindowBackgroundShapeStyle.windowBackground -#else -static let editingBackgroundStyle = Material.ultraThickMaterial -#endif -``` - -### Navigation transitions not working - -**Problem:** Sheet morphing transitions or navigation transitions break. - -**Solution:** Ensure the view is inside `NavigationStack` or `NavigationSplitView`: - -```swift -// BAD - no navigation context -.sheet(isPresented: $show) { - ContentView() - .navigationTransition(.zoom(sourceID: "id", in: namespace)) -} - -// GOOD - wrapped in NavigationStack -.sheet(isPresented: $show) { - NavigationStack { - ContentView() - } - .navigationTransition(.zoom(sourceID: "id", in: namespace)) -} -``` - -### Symbol effect crashes on older OS - -**Problem:** `.symbolEffect(.drawOn)` or other iOS 26 symbol effects crash on older OS versions. - -**Solution:** Use `@available` checks: - -```swift -if #available(iOS 26, *) { - image.transition(.init(.symbolEffect(.drawOn))) -} else { - image.transition(.opacity) -} -``` diff --git a/skills/liquid-glass/references/platform-specifics.md b/skills/liquid-glass/references/platform-specifics.md deleted file mode 100644 index a7fbdf8..0000000 --- a/skills/liquid-glass/references/platform-specifics.md +++ /dev/null @@ -1,302 +0,0 @@ -# Platform-Specific Liquid Glass Implementation - -Detailed platform considerations for implementing Liquid Glass across Apple platforms. - -## Table of Contents - -1. [iOS 26 (iPhone)](#ios-26-iphone) -2. [iPadOS 26](#ipados-26) -3. [macOS 26 (Tahoe)](#macos-26-tahoe) -4. [watchOS 26](#watchos-26) -5. [tvOS 26](#tvos-26) -6. [visionOS 26](#visionos-26) -7. [Cross-Platform Patterns](#cross-platform-patterns) - ---- - -## iOS 26 (iPhone) - -### Glass Features Available -- Full `.glassEffect()` with all styles (`.regular`, `.clear`, `.identity`) -- `.interactive()` glass - scaling, bouncing, shimmering on touch -- `GlassEffectContainer` with morphing transitions -- `.backgroundExtensionEffect()` for hero images -- `.buttonStyle(.glass)` and `.buttonStyle(.glassProminent)` -- Automatic glass tab bars with `.tabBarMinimizeBehavior(.onScrollDown)` -- Glass sheets with `.presentationDetents([.medium, .large])` -- Glass search bars via `.searchable()` - -### iPhone-Specific Patterns - -```swift -#if os(iOS) -// Phone-specific toolbar visibility -if UIDevice.current.userInterfaceIdiom == .phone { - .toolbarVisibility(.visible, for: .automatic) - .toolbar { - Button { inspectorIsPresented.toggle() } label: { - Label("Toggle Inspector", systemImage: "info.circle") - } - } -} - -// Inline navigation title -.navigationBarTitleDisplayMode(.inline) - -// Smaller grid item sizes for compact screens -static var gridItemMinSize: CGFloat { - if UIDevice.current.userInterfaceIdiom == .phone { - return 160.0 - } - return 240.0 -} -#endif -``` - -### Navigation on iPhone -- `NavigationSplitView` collapses to single column -- Use `preferredCompactColumn` to control default view -- Search bar dynamically moves based on scroll position - ---- - -## iPadOS 26 - -### iPad-Specific Features -- Floating Liquid Glass sidebar -- Side-by-side split view with glass navigation -- Inspector panels with glass styling -- Larger touch targets for glass buttons - -### iPad Layout Patterns - -```swift -#if os(iOS) -if UIDevice.current.userInterfaceIdiom == .pad { - // Larger grid sizes - static let gridItemMinSize: CGFloat = 220.0 - static let gridItemEditingMinSize: CGFloat = 180.0 - - // Hide phone-specific toolbar items - .toolbarVisibility(.hidden, for: .automatic) -} -#endif -``` - -### Split View with Glass - -```swift -NavigationSplitView(preferredCompactColumn: $preferredColumn) { - // Sidebar - auto-glass on iPad - List { - ForEach(NavigationOptions.mainPages) { page in - NavigationLink(value: page) { - Label(page.name, systemImage: page.symbolName) - } - } - } - .frame(minWidth: 150) -} detail: { - NavigationStack(path: $modelData.path) { - MainContentView() - } -} -.searchable(text: $modelData.searchString, prompt: "Search") -.inspector(isPresented: $modelData.isInspectorPresented) { - InspectorView() -} -``` - ---- - -## macOS 26 (Tahoe) - -### macOS-Specific Considerations - -**Glass button tinting:** Always apply `.tint(.clear)` to glass buttons on macOS: - -```swift -Button("Action") { } - .buttonStyle(.glass) - #if os(macOS) - .tint(.clear) - #endif -``` - -**Background styles:** Use `WindowBackgroundShapeStyle` instead of `Material`: - -```swift -#if os(macOS) -static let editingBackgroundStyle = WindowBackgroundShapeStyle.windowBackground -.background(Color(nsColor: .windowBackgroundColor)) -.background(Color(nsColor: isEditing ? .secondarySystemFill : .windowBackgroundColor)) -#else -static let editingBackgroundStyle = Material.ultraThickMaterial -.background(Color(uiColor: .systemBackground)) -#endif -``` - -**Toolbar placement:** Use `.secondaryAction` for overflow items: - -```swift -#if os(macOS) -let deleteButtonPlacement: ToolbarItemPlacement = .secondaryAction -let editButtonPlacement: ToolbarItemPlacement = .automatic -#elseif os(iOS) -let deleteButtonPlacement: ToolbarItemPlacement = .topBarLeading -let editButtonPlacement: ToolbarItemPlacement = .topBarTrailing -#endif -``` - -**Window configuration:** - -```swift -WindowGroup { - ContentView() - .frame(minWidth: 375.0, minHeight: 375.0) -} -``` - -### macOS Navigation -- `NavigationSplitView` shows persistent sidebar -- Window toolbar integrates glass automatically -- Inspector panels slide from trailing edge -- Search bar appears in toolbar area - ---- - -## watchOS 26 - -### Glass on Apple Watch -- Simplified glass effects for smaller display -- System handles glass on navigation elements automatically -- Limited custom glass usage recommended (screen real estate) -- TabView with glass tab bar adapts to watch form factor - -### Considerations -- Avoid complex `GlassEffectContainer` layouts on watch -- Use `.regular` style only (`.clear` less effective on small screens) -- Test with all watch face complications active -- `.interactive()` works with Digital Crown interactions - ---- - -## tvOS 26 - -### Glass on Apple TV -- Focus-driven glass interactions -- Glass elements highlight on focus -- Remote-friendly touch targets -- Background extension effects work with media-rich content - -### Focus-Aware Glass - -```swift -Button("Play") { } - .buttonStyle(.glass) - .focusable() - // Glass automatically responds to focus state on tvOS -``` - ---- - -## visionOS 26 - -### Glass in Spatial Computing -- Glass integrates with spatial UI paradigms -- Window ornaments use glass automatically -- Volumetric content benefits from glass overlays -- System handles glass depth and parallax - ---- - -## Cross-Platform Patterns - -### Conditional Compilation Pattern - -```swift -#if os(macOS) -// macOS-specific implementation -#elseif os(iOS) -if UIDevice.current.userInterfaceIdiom == .pad { - // iPad-specific -} else { - // iPhone-specific -} -#elseif os(watchOS) -// Watch-specific -#elseif os(tvOS) -// TV-specific -#elseif os(visionOS) -// Vision-specific -#endif -``` - -### Shared Architecture Pattern - -Use a single codebase with platform adaptations: - -```swift -@main -struct MyApp: App { - @State private var modelData = ModelData() - - var body: some Scene { - WindowGroup { - ContentView() - .environment(modelData) - .frame(minWidth: 375.0, minHeight: 375.0) - .onGeometryChange(for: CGSize.self) { geometry in - geometry.size - } action: { - modelData.windowSize = $0 - } - } - } -} -``` - -### Responsive Layout Constants - -```swift -struct LayoutConstants { - @MainActor static var gridItemMinSize: CGFloat { - #if os(iOS) - if UIDevice.current.userInterfaceIdiom == .pad { - return 220.0 // iPad - } else { - return 160.0 // iPhone - } - #elseif os(macOS) - return 240.0 - #elseif os(watchOS) - return 80.0 - #else - return 200.0 - #endif - } -} -``` - -### NavigationSplitView Across Platforms - -```swift -NavigationSplitView(preferredCompactColumn: $preferredColumn) { - // Sidebar: glass on all platforms - SidebarContent() -} detail: { - // Detail: NavigationStack for deep navigation - NavigationStack(path: $path) { - DetailContent() - } - .navigationDestination(for: Item.self) { item in - ItemDetailView(item: item) - } -} -// Global search - positioned by system per platform -.searchable(text: $searchString, prompt: "Search") -// Inspector - system handles presentation per platform -.inspector(isPresented: $showInspector) { - InspectorContent() -} -``` diff --git a/skills/swiftui/SKILL.md b/skills/swiftui/SKILL.md new file mode 100644 index 0000000..0ab61a0 --- /dev/null +++ b/skills/swiftui/SKILL.md @@ -0,0 +1,40 @@ +--- +name: swiftui +description: Build, review, or restructure SwiftUI interfaces for Apple platforms. Use for state ownership and data flow, view composition and identity, navigation and presentation, or platform materials including Liquid Glass. +--- + +# SwiftUI + +Shape the interface around SwiftUI's ownership, identity, and environment rather +than reproducing an imperative view hierarchy. Preserve the product's existing +architecture and deployment targets unless the task changes them. + +## Workflow + +1. Inspect the affected scene, views, models, deployment targets, and existing + design system. Trace each mutable value to its source of truth and each user + action to the code that owns its effect. +2. Read only the references needed for the task: + - [State and data flow](references/state-and-data-flow.md) for ownership, + Observation, bindings, environment values, or editable drafts. + - [Composition and identity](references/composition-and-identity.md) for view + boundaries, collection identity, update scope, or restructuring a large view. + - [Navigation and presentation](references/navigation-and-presentation.md) for + stacks, split views, routes, sheets, dialogs, or restoration. + - [Materials and Liquid Glass](references/materials-and-glass.md) when adopting + current platform materials or reviewing custom glass effects. +3. Make the source of truth and view identity explicit before changing layout. + Give each view only the values, bindings, and actions it needs. Keep durable + storage and business rules outside transient view state. +4. Prefer native containers and controls for the target platform. Add a UIKit or + AppKit bridge at a deliberate boundary when SwiftUI lacks the required behavior; + keep lifecycle and ownership on one side of that boundary. +5. Build the affected targets and exercise the changed state transitions. Use + `manual-verify` for visual or interactive acceptance checks. Use `animate` for + motion design, `ios-haptics` for tactile feedback when its iOS 26+ SwiftUI scope + applies, and `diagnosing-bugs` when a failure or performance regression needs + investigation. + +After performance diagnosis identifies the bottleneck, use the data-flow and +composition guidance here to narrow invalidation or reduce body work without +changing behavior. diff --git a/skills/swiftui/agents/openai.yaml b/skills/swiftui/agents/openai.yaml new file mode 100644 index 0000000..99e3591 --- /dev/null +++ b/skills/swiftui/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "SwiftUI" + short_description: "Build maintainable SwiftUI interfaces" + default_prompt: "Use $swiftui to build this interface with clear state ownership, stable identity, and native platform structure." diff --git a/skills/swiftui/references/composition-and-identity.md b/skills/swiftui/references/composition-and-identity.md new file mode 100644 index 0000000..cd457b7 --- /dev/null +++ b/skills/swiftui/references/composition-and-identity.md @@ -0,0 +1,42 @@ +# Composition and Identity + +## Compose by responsibility + +Keep `body` declarative and quick to evaluate. Move side effects, data access, +formatting with material cost, and business decisions into the feature owner or a +focused helper. A semantic subview should have a clear job and a narrow interface. + +Use native layout and container types before building custom infrastructure. Keep +modifier order intentional because layout, drawing, hit testing, accessibility, +and effects compose in order. + +## Preserve identity + +SwiftUI associates state and lifetime with view identity. Keep the structural +position and explicit identity of a stateful view stable across ordinary updates. +Use an explicit `id` reset only when discarding that state is the intended result. + +For collections, use identifiers that represent the domain item across insertions, +deletions, sorting, and refreshes. Array offsets, a newly generated UUID during +each render, and mutable display text do not provide stable identity. If duplicate +values are valid, the value itself is not a unique identifier. + +Make conditional branches represent real structural alternatives. When only a +property changes, prefer a stable view with a conditional value or modifier so +focus, scroll position, task lifetime, and local state remain attached to the same +identity. + +## Control update cost + +After measurement identifies an expensive update, use Apple's +[SwiftUI performance guidance](https://developer.apple.com/documentation/xcode/understanding-and-improving-swiftui-performance) +to interpret it and reduce the dependency or work responsible: + +- read only the observable properties a view renders; +- move repeated computation out of `body` or cache it at an owner with a valid + invalidation rule; +- keep list identity stable; and +- prevent feedback loops in geometry, preference, and change handlers. + +Prefer concrete view types. Add type erasure only at a boundary that genuinely +needs heterogeneous storage or return types. diff --git a/skills/swiftui/references/materials-and-glass.md b/skills/swiftui/references/materials-and-glass.md new file mode 100644 index 0000000..7782bb5 --- /dev/null +++ b/skills/swiftui/references/materials-and-glass.md @@ -0,0 +1,42 @@ +# Materials and Liquid Glass + +Treat current platform materials as part of the system component hierarchy. An +app built with current SDKs often receives the current appearance through standard +navigation, toolbar, tab, menu, control, and presentation APIs. Start there, then +add a custom effect only when the design calls for a custom functional surface. +Apple's [adoption guidance](https://developer.apple.com/documentation/technologyoverviews/adopting-liquid-glass) +covers the system components and accessibility behavior. + +## Custom SwiftUI glass + +The current custom glass APIs require the current SDK and version 26 or later on +iOS, iPadOS, macOS, tvOS, and watchOS; they are unavailable on visionOS. Confirm +the boundary in the active SDK because it can change. In shared multiplatform +source, put the visionOS fallback in a compile-time `#if os(visionOS)` branch and +keep glass types and modifiers outside that branch. On supported platforms, put +custom glass behind a version availability check. Keep a fallback with the same +structure, actions, legibility, and interaction. + +- Apply `glassEffect(_:in:)` after modifiers that establish the view's appearance + and shape. +- Group related effects in one `GlassEffectContainer`. Its spacing controls when + neighboring shapes begin to blend, and the shared container reduces rendering + work. +- Add `interactive()` only to an element that actually responds to interaction. +- Use stable glass effect identifiers when shapes merge or transition. Coordinate + the state change with `animate`; the visual effect does not define the product's + motion behavior by itself. +- Use custom glass sparingly on the most important functional elements. Let the + content remain the visual focus. + +Apple documents modifier order, containers, unions, and transitions in +[Applying Liquid Glass to custom views](https://developer.apple.com/documentation/swiftui/applying-liquid-glass-to-custom-views). + +## Verify adaptations + +Check the result over representative light and dark content, with increased +contrast, reduced transparency, and reduced motion. System components adapt to +these settings; confirm that custom colors, effects, and transitions still keep +text and controls clear. Verify hit targets and focus independently of the glass +shape, and profile screens with several custom effects when rendering cost is a +concern. diff --git a/skills/swiftui/references/navigation-and-presentation.md b/skills/swiftui/references/navigation-and-presentation.md new file mode 100644 index 0000000..10164d7 --- /dev/null +++ b/skills/swiftui/references/navigation-and-presentation.md @@ -0,0 +1,37 @@ +# Navigation and Presentation + +## Model destinations + +Use `NavigationStack` for linear navigation and `NavigationSplitView` when the +platform and information architecture call for columns. Put navigation state at +the scene or feature boundary that must restore, deep-link, or coordinate it. + +Represent a route with a small stable value, usually an identifier plus the +minimum routing context. Resolve the current model at the destination. Passing a +large mutable model through the path turns navigation state into data transport +and makes restoration brittle. + +Use a typed array when every route has one type. Use `NavigationPath` when a stack +must contain heterogeneous route values. Register each value with the destination +that knows how to render it. See Apple's +[navigation stack guidance](https://developer.apple.com/documentation/swiftui/understanding-the-navigation-stack) +and [`NavigationStack`](https://developer.apple.com/documentation/swiftui/navigationstack). + +## Own presentations + +Keep sheet, popover, alert, and confirmation state with the feature that decides +to present it. Prefer item-driven presentation when the presented content has an +identity. The optional item then describes both whether the presentation exists +and what it displays. + +Treat dismissal as an outcome. Let the presented feature report save, cancel, or +delete through an action, then let the owner update durable state and presentation +state in a defined order. Use the environment dismiss action for a local dismissal +that needs no domain result. + +## Verify state transitions + +Exercise push and pop, programmatic routes, deep links, restoration if supported, +and presentation dismissal. On iPadOS and macOS, resize or change column visibility +when the changed flow uses a split view. Confirm that a repeated route reaches the +correct model and that cancelling a presentation leaves its source of truth intact. diff --git a/skills/swiftui/references/state-and-data-flow.md b/skills/swiftui/references/state-and-data-flow.md new file mode 100644 index 0000000..e0ad88a --- /dev/null +++ b/skills/swiftui/references/state-and-data-flow.md @@ -0,0 +1,43 @@ +# State and Data Flow + +## Assign ownership + +Give every mutable value one source of truth at the lowest common ancestor that +needs to own it. + +- Use an immutable property for input the view only reads. +- Use private `@State` for transient state the view creates and owns. Its lifetime + follows the view's identity, so it is not durable storage. +- Pass `@Binding` only when a child must mutate state owned elsewhere. Pass a value + and an action when the child only reports an event. +- On supported deployment targets, use `@Observable` for reference models and + keep a view-owned instance in `@State`. Use `@Bindable` when a view needs bindings + to that model's mutable properties. +- Put a model or dependency in the environment when descendants share it as part + of their surrounding context. Pass feature-local requirements explicitly. + +These choices follow Apple's [model data](https://developer.apple.com/documentation/swiftui/model-data) +and [UI state](https://developer.apple.com/documentation/swiftui/managing-user-interface-state/) +guidance. + +Keep older `ObservableObject`, `@StateObject`, and `@ObservedObject` code when the +deployment target or existing architecture requires it. Modernize Observation as +a coherent ownership change rather than wrapper-by-wrapper substitution. + +## Keep one truth + +Copy an input into local state only when the local value is intentionally an +independent draft or snapshot. Define when it is created, committed, reset, and +reconciled with upstream changes. Derived display values should remain derived. + +Place asynchronous loading and mutations with the feature owner. Start work from +a lifecycle-aware hook such as `task`, cancel or supersede stale requests, and +publish UI-observed mutations on the appropriate actor. Model loading, empty, +content, and failure as distinct states when they lead to distinct interfaces. + +## Narrow dependencies + +Read observable properties near the view that renders them. Passing a large model +through a broad subtree can make unrelated views depend on it. Extract a semantic +child view when doing so gives that child a smaller input surface and a stable +identity; avoid extraction performed only to shorten a file. diff --git a/skills/uikit/SKILL.md b/skills/uikit/SKILL.md new file mode 100644 index 0000000..77b33bd --- /dev/null +++ b/skills/uikit/SKILL.md @@ -0,0 +1,36 @@ +--- +name: uikit +description: Build, review, or restructure UIKit interfaces. Use for view-controller ownership and containment, scenes and trait context, adaptive layout and safe areas, collection identity, or modernization of legacy UIKit environment code. +--- + +# UIKit + +Build each interface around its actual view-controller, view, window, and scene +context. Preserve the product's architecture and deployment targets while making +ownership and lifecycle explicit. + +## Workflow + +1. Inspect the affected controller hierarchy, scene configuration, layout system, + data source, deployment targets, and user-visible behavior. Identify which + object owns the window, content, navigation, and mutable state. +2. Read only the references needed for the task: + - [View controllers and collections](references/view-controllers-and-collections.md) + for containment, lifecycle ownership, lists, or stable item identity. + - [Scenes and traits](references/scenes-and-traits.md) for windows, screens, + geometry, orientation, environment changes, or deprecated global lookups. + - [Layout and safe areas](references/layout-and-safe-areas.md) for constraints, + margins, bars, keyboard interaction, or resizable interfaces. +3. Choose the nearest valid context. UI code should normally derive environment + from its view or controller; non-UI code should receive the specific value or + capability it needs. +4. Use standard controls and containers where they express the behavior. If a + SwiftUI island is appropriate, place `UIHostingController` at an explicit + containment boundary and keep one owner for state and lifecycle. +5. For modernization, replace a deprecated or global assumption end to end. Trace + every caller, pass local context where needed, and preserve supported behavior + for each active scene rather than swapping symbols mechanically. +6. Build the affected targets and exercise resizing, traits, navigation, and data + updates relevant to the change. Use `manual-verify` for visual or interactive + acceptance checks, `animate` for motion, and `diagnosing-bugs` for runtime + failures or performance regressions. diff --git a/skills/uikit/agents/openai.yaml b/skills/uikit/agents/openai.yaml new file mode 100644 index 0000000..f4baf94 --- /dev/null +++ b/skills/uikit/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "UIKit" + short_description: "Build adaptive UIKit interfaces" + default_prompt: "Use $uikit to build this interface with explicit controller ownership, scene context, and adaptive layout." diff --git a/skills/uikit/references/layout-and-safe-areas.md b/skills/uikit/references/layout-and-safe-areas.md new file mode 100644 index 0000000..2bcf01f --- /dev/null +++ b/skills/uikit/references/layout-and-safe-areas.md @@ -0,0 +1,35 @@ +# Layout and Safe Areas + +## Describe relationships + +Use Auto Layout constraints and guides to express relationships inside the owning +view hierarchy. Create constraints once, then update constants or switch between +small named constraint sets when state changes. Base adaptation on container bounds, +traits, and content rather than a device model or global screen size. + +Use `layoutMarginsGuide` for component spacing, `readableContentGuide` for readable +text width, and [`safeAreaLayoutGuide`](https://developer.apple.com/documentation/uikit/uiview/safearealayoutguide) +for content that must avoid bars, rounded corners, and other obscured regions. +Backgrounds can intentionally extend to the view edges while interactive and +readable content remains constrained to the appropriate guide. + +## Let containers define insets + +The safe area is local to a view and can change after it joins a window, after bars +appear, or while a window resizes. Read `safeAreaInsets` when the view is in its +hierarchy and respond in `safeAreaInsetsDidChange()` only when constraint guides +cannot express the behavior. A custom container can adjust a child's additional +safe area when it owns an overlay; keep that adjustment with the container. + +Treat the keyboard as changing the usable region rather than as a fixed height. +Prefer UIKit's keyboard layout guide where supported. When notifications are +required by the deployment target or behavior, convert the reported frame into the +affected view's coordinate space and follow the notification's timing. + +## Verify adaptation + +Exercise the sizes and environments the feature supports: portrait and landscape, +split view or resizable windows, large Dynamic Type sizes, right-to-left layout, +system bars, presented controllers, and keyboard appearance where relevant. Check +for ambiguous constraints, clipped text, unreachable controls, and scroll content +hidden behind overlays. diff --git a/skills/uikit/references/scenes-and-traits.md b/skills/uikit/references/scenes-and-traits.md new file mode 100644 index 0000000..53a332d --- /dev/null +++ b/skills/uikit/references/scenes-and-traits.md @@ -0,0 +1,50 @@ +# Scenes and Traits + +## Follow scene context + +A window scene represents one UI instance and can have state independent of other +windows. Keep process-wide setup in the app delegate and scene-specific windows, +restoration, and lifecycle work with the scene or scene delegate. Apple's +[`UIWindowScene`](https://developer.apple.com/documentation/uikit/uiwindowscene) +documentation defines that boundary. + +Start from the object involved in the operation: + +- use a view's `window?.windowScene` for its scene; +- use that scene's `screen` when display properties are needed; and +- use `effectiveGeometry.interfaceOrientation` on iOS 16 and later when code + genuinely needs interface orientation rather than layout size or size classes. + +`UIScreen.main` does not identify the display for every window, and Apple directs +apps to use a screen obtained from context. See [`UIScreen.main`](https://developer.apple.com/documentation/uikit/uiscreen/main). +Likewise, `UIWindowScene.interfaceOrientation` is deprecated in favor of +[`effectiveGeometry.interfaceOrientation`](https://developer.apple.com/documentation/uikit/uiwindowscene/interfaceorientation). +For a deployment target below iOS 16, keep an availability-guarded fallback to +the contextual window scene's `interfaceOrientation`; do not replace it with a +process-wide screen or window lookup. + +When a service lacks UI context, pass the required screen, scene, geometry, or +derived value into it. A scan of connected scenes or key windows cannot establish +which scene owns an arbitrary operation. + +## Respond to traits + +Read traits from the nearest `UIView` or `UIViewController` environment. Use size +classes and current bounds for adaptive layout; device labels and orientation are +usually weaker proxies for the space actually available. + +On iOS 18 and later, place trait-dependent work in a lifecycle method that supports +automatic trait tracking, such as `layoutSubviews`, when it naturally belongs +there. Otherwise, on iOS 17 and later, register for the specific traits or semantic +trait set that affects the result. UIKit retains registrations for the observable +object's lifetime, so retain the registration token only when it must be stopped +earlier. On older deployment targets, keep an availability-guarded +`traitCollectionDidChange(_:)` implementation and filter for the traits that +change the result. Apple's +[trait change guidance](https://developer.apple.com/documentation/uikit/adapting-your-app-when-traits-change) +describes automatic tracking and `registerForTraitChanges` as replacements for +deprecated `traitCollectionDidChange` implementations. + +Initialize the trait-dependent result during setup; a change callback only handles +later changes. Use dynamic system colors and images where possible so UIKit performs +the adaptation without custom callbacks. diff --git a/skills/uikit/references/view-controllers-and-collections.md b/skills/uikit/references/view-controllers-and-collections.md new file mode 100644 index 0000000..1d2956d --- /dev/null +++ b/skills/uikit/references/view-controllers-and-collections.md @@ -0,0 +1,39 @@ +# View Controllers and Collections + +## Keep controller ownership explicit + +A view controller coordinates one screen or one coherent region. Let model and +service types own business rules and persistence. Let the controller translate +state into UIKit updates and route user actions to the owner of their effects. + +Use `UINavigationController`, `UITabBarController`, and `UISplitViewController` +when their behavior matches the product. For a custom container, keep the parent +and child relationship synchronized with the view hierarchy: + +1. Call `addChild(_:)`. +2. Add the child's root view and establish its frame or constraints. +3. Call `didMove(toParent:)` on the child. + +For removal, call `willMove(toParent: nil)`, remove the child's constraints and +view, then call `removeFromParent()`. Forward appearance and rotation decisions +deliberately when the default forwarding does not fit. Apple's +[custom container guide](https://developer.apple.com/documentation/uikit/creating-a-custom-container-view-controller) +describes the lifecycle contract. + +Keep each observer, task, and callback tied to an owner with a defined lifetime. +Capture controllers weakly in escaping callbacks when the callback owner could +otherwise retain the controller. Cancel work when its result no longer belongs to +the visible feature or current request. + +## Give collection items stable identity + +Prefer diffable data sources for table and collection state that changes over +time. Build snapshots from domain section and item identifiers that remain stable +through sorting, filtering, and refresh. Treat an index path as a current location, +not an item's identity. + +Apply one coherent snapshot for a model transition. Configure a cell from the +identifier's current model and make asynchronous image or content work verify that +the cell still represents that identifier before applying its result. Apple's +[diffable data source example](https://developer.apple.com/documentation/uikit/updating-collection-views-using-diffable-data-sources) +shows the identifier-based update model. From 4b6f42619719666d211207b784d78a4735444088 Mon Sep 17 00:00:00 2001 From: Sree Raman <1271305+sreejithraman@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:08:03 -0400 Subject: [PATCH 2/4] Add Apple UI framework choice guidance --- skills/swiftui/SKILL.md | 4 +- skills/swiftui/references/framework-choice.md | 64 +++++++++++++++++++ skills/uikit/SKILL.md | 4 +- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 skills/swiftui/references/framework-choice.md diff --git a/skills/swiftui/SKILL.md b/skills/swiftui/SKILL.md index 0ab61a0..0979648 100644 --- a/skills/swiftui/SKILL.md +++ b/skills/swiftui/SKILL.md @@ -1,6 +1,6 @@ --- name: swiftui -description: Build, review, or restructure SwiftUI interfaces for Apple platforms. Use for state ownership and data flow, view composition and identity, navigation and presentation, or platform materials including Liquid Glass. +description: Build, review, or restructure SwiftUI interfaces for Apple platforms, or choose SwiftUI versus UIKit for new Apple UI. Use for state ownership and data flow, view composition and identity, navigation and presentation, or platform materials including Liquid Glass. --- # SwiftUI @@ -15,6 +15,8 @@ architecture and deployment targets unless the task changes them. design system. Trace each mutable value to its source of truth and each user action to the code that owns its effect. 2. Read only the references needed for the task: + - [Choosing SwiftUI or UIKit](references/framework-choice.md) when the framework + is not fixed or the work could introduce a framework boundary. - [State and data flow](references/state-and-data-flow.md) for ownership, Observation, bindings, environment values, or editable drafts. - [Composition and identity](references/composition-and-identity.md) for view diff --git a/skills/swiftui/references/framework-choice.md b/skills/swiftui/references/framework-choice.md new file mode 100644 index 0000000..467dddb --- /dev/null +++ b/skills/swiftui/references/framework-choice.md @@ -0,0 +1,64 @@ +# Choosing SwiftUI or UIKit + +## Default new work to SwiftUI + +Use SwiftUI for a new app, scene, screen, or component unless a concrete +requirement points elsewhere. Apple calls SwiftUI its best choice for new apps +and recommends it for new features in existing apps. SwiftUI also supports one +declarative interface across Apple platforms. + +Sources: [SwiftUI apps](https://developer.apple.com/documentation/technologyoverviews/swiftui), +[Platforms State of the Union, WWDC24](https://developer.apple.com/videos/play/wwdc2024/102/). + +## Keep or choose UIKit for evidence + +Use UIKit for the affected surface when one or more of these conditions holds: + +- The feature already has stable UIKit controller, navigation, restoration, or + scene ownership, and the requested change does not justify replacing it. +- A required system component or behavior has no suitable SwiftUI API. Wrap or + host that capability instead of recreating it. +- The app's minimum OS version lacks the required SwiftUI API while UIKit provides + the supported behavior. +- Measurement shows a requirement the current SwiftUI implementation cannot meet, + and a UIKit implementation or boundary resolves that specific problem. + +UIKit remains current and interoperates in both directions with SwiftUI. A broad +preference for control or an assumed performance advantage is not evidence for +choosing it. Diagnose and measure the actual behavior first. + +Sources: [UIKit](https://developer.apple.com/documentation/uikit), +[UIKit integration](https://developer.apple.com/documentation/swiftui/uikit-integration), +[SwiftUI performance](https://developer.apple.com/documentation/xcode/understanding-and-improving-swiftui-performance). + +## Treat hybrid as an architecture + +An existing UIKit app does not need a wholesale rewrite. Add SwiftUI at a coherent +feature, screen, scene, or component boundary while the UIKit shell continues to +own its established lifecycle. In a SwiftUI app, wrap the smallest UIKit view or +view controller that owns a missing capability. + +Keep one side responsible for state and lifecycle across each boundary. Pass +values and actions explicitly, and translate them in one hosting or representable +adapter. Repeatedly alternating frameworks within one feature multiplies layout, +sizing, identity, and lifecycle coordination. + +Apple presents incremental adoption as a normal long-term approach and provides +`UIHostingController`, `UIHostingConfiguration`, representable protocols, and +scene bridges for it. + +Sources: [Use SwiftUI with AppKit and UIKit, WWDC26](https://developer.apple.com/videos/play/wwdc2026/272/), +[Using SwiftUI with UIKit](https://developer.apple.com/documentation/uikit/using-swiftui-with-uikit), +[`UIViewControllerRepresentable`](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable). + +## Decide at the feature boundary + +1. List the required platforms, minimum OS versions, system components, + interactions, restoration behavior, and measured constraints. +2. Start with SwiftUI for new work. Prove any uncertain capability with the + smallest useful prototype or documentation check. +3. If a requirement needs UIKit, choose the smallest coherent UIKit-owned + boundary and define its state, sizing, lifecycle, and event contract. +4. Preserve proven UIKit that the feature does not need to replace. +5. Verify the integrated result through the actual navigation, resizing, + accessibility, restoration, and performance paths it must support. diff --git a/skills/uikit/SKILL.md b/skills/uikit/SKILL.md index 77b33bd..f662fe1 100644 --- a/skills/uikit/SKILL.md +++ b/skills/uikit/SKILL.md @@ -1,6 +1,6 @@ --- name: uikit -description: Build, review, or restructure UIKit interfaces. Use for view-controller ownership and containment, scenes and trait context, adaptive layout and safe areas, collection identity, or modernization of legacy UIKit environment code. +description: Build, review, or restructure UIKit interfaces, or decide whether an existing UIKit surface should stay UIKit or adopt SwiftUI. Use for view-controller ownership and containment, scenes and trait context, adaptive layout and safe areas, collection identity, or modernization of legacy UIKit environment code. --- # UIKit @@ -15,6 +15,8 @@ ownership and lifecycle explicit. data source, deployment targets, and user-visible behavior. Identify which object owns the window, content, navigation, and mutable state. 2. Read only the references needed for the task: + - [Choosing SwiftUI or UIKit](../swiftui/references/framework-choice.md) when the + framework is not fixed or the work could introduce a framework boundary. - [View controllers and collections](references/view-controllers-and-collections.md) for containment, lifecycle ownership, lists, or stable item identity. - [Scenes and traits](references/scenes-and-traits.md) for windows, screens, From 0ca7084ce0f282f7b86e091c0be125aa108a76ff Mon Sep 17 00:00:00 2001 From: Sree Raman <1271305+sreejithraman@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:02:51 -0400 Subject: [PATCH 3/4] Tighten Apple UI skill routing --- skills/swiftui/SKILL.md | 6 ++- skills/uikit/SKILL.md | 9 ++++- .../uikit/references/materials-and-glass.md | 37 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 skills/uikit/references/materials-and-glass.md diff --git a/skills/swiftui/SKILL.md b/skills/swiftui/SKILL.md index 0979648..f1021a6 100644 --- a/skills/swiftui/SKILL.md +++ b/skills/swiftui/SKILL.md @@ -1,6 +1,6 @@ --- name: swiftui -description: Build, review, or restructure SwiftUI interfaces for Apple platforms, or choose SwiftUI versus UIKit for new Apple UI. Use for state ownership and data flow, view composition and identity, navigation and presentation, or platform materials including Liquid Glass. +description: SwiftUI interface work on Apple platforms. Use when choosing SwiftUI versus UIKit; building or restructuring SwiftUI views, state flow, navigation, presentation, or Liquid Glass; or applying a diagnosed SwiftUI performance fix. --- # SwiftUI @@ -16,7 +16,9 @@ architecture and deployment targets unless the task changes them. action to the code that owns its effect. 2. Read only the references needed for the task: - [Choosing SwiftUI or UIKit](references/framework-choice.md) when the framework - is not fixed or the work could introduce a framework boundary. + is not fixed or the work could introduce a framework boundary. If UIKit owns + the affected surface, continue with `uikit` and end this workflow. For a + hybrid, name the boundary and continue here only for the SwiftUI-owned side. - [State and data flow](references/state-and-data-flow.md) for ownership, Observation, bindings, environment values, or editable drafts. - [Composition and identity](references/composition-and-identity.md) for view diff --git a/skills/uikit/SKILL.md b/skills/uikit/SKILL.md index f662fe1..8784223 100644 --- a/skills/uikit/SKILL.md +++ b/skills/uikit/SKILL.md @@ -1,6 +1,6 @@ --- name: uikit -description: Build, review, or restructure UIKit interfaces, or decide whether an existing UIKit surface should stay UIKit or adopt SwiftUI. Use for view-controller ownership and containment, scenes and trait context, adaptive layout and safe areas, collection identity, or modernization of legacy UIKit environment code. +description: UIKit interface work. Use when building, maintaining, or modernizing UIKit controllers, scenes, traits, adaptive layout, collections, or Liquid Glass; or deciding whether an existing UIKit surface should adopt SwiftUI. --- # UIKit @@ -16,13 +16,18 @@ ownership and lifecycle explicit. object owns the window, content, navigation, and mutable state. 2. Read only the references needed for the task: - [Choosing SwiftUI or UIKit](../swiftui/references/framework-choice.md) when the - framework is not fixed or the work could introduce a framework boundary. + framework is not fixed or the work could introduce a framework boundary. If + SwiftUI owns the affected surface, continue with `swiftui` and end this + workflow. For a hybrid, name the boundary and continue here only for the + UIKit-owned side. - [View controllers and collections](references/view-controllers-and-collections.md) for containment, lifecycle ownership, lists, or stable item identity. - [Scenes and traits](references/scenes-and-traits.md) for windows, screens, geometry, orientation, environment changes, or deprecated global lookups. - [Layout and safe areas](references/layout-and-safe-areas.md) for constraints, margins, bars, keyboard interaction, or resizable interfaces. + - [Materials and Liquid Glass](references/materials-and-glass.md) when adopting + current UIKit materials or reviewing custom glass effects. 3. Choose the nearest valid context. UI code should normally derive environment from its view or controller; non-UI code should receive the specific value or capability it needs. diff --git a/skills/uikit/references/materials-and-glass.md b/skills/uikit/references/materials-and-glass.md new file mode 100644 index 0000000..4e52fbe --- /dev/null +++ b/skills/uikit/references/materials-and-glass.md @@ -0,0 +1,37 @@ +# Materials and Liquid Glass + +Build with the current SDK and inspect the interface with standard UIKit +navigation, bars, controls, and presentations before adding custom effects. These +components adopt the current system appearance and accessibility adaptations +automatically. Apple's [adoption guidance](https://developer.apple.com/documentation/technologyoverviews/adopting-liquid-glass) +describes the UIKit components that receive the design. + +## Custom UIKit glass + +The current `UIGlassEffect` and `UIGlassContainerEffect` APIs require iOS, +iPadOS, Mac Catalyst, or tvOS 26 or later and are unavailable on visionOS and +watchOS. Confirm the boundary in the active SDK. Keep unavailable types outside +compile-time branches for unsupported platforms, then use a version availability +check on supported platforms. Preserve the same structure, actions, legibility, +and interaction in the fallback. + +- Put `UIGlassEffect` in a `UIVisualEffectView`; add the element's content to the + effect view's `contentView`. +- Set `isInteractive` only for a control that responds to interaction. Use + `tintColor` to communicate prominence without obscuring content. +- When several glass elements should render and merge together, put their glass + effect views inside the `contentView` of a `UIVisualEffectView` configured with + `UIGlassContainerEffect`. Its `spacing` defines when neighboring elements begin + to merge. +- Keep custom glass on the functional layer above content and use it sparingly. + +Apple documents the effect types in [`UIGlassEffect`](https://developer.apple.com/documentation/uikit/uiglasseffect) +and [UIKit appearance customization](https://developer.apple.com/documentation/uikit/appearance-customization). + +## Verify adaptations + +Check custom effects over representative light and dark content, with increased +contrast, reduced transparency, and reduced motion. Confirm that labels, icons, +focus, and hit targets remain clear when the system reduces or changes the visual +effect. Profile screens with several custom effects when rendering cost is a +concern. From 2dc212f771776edb7049fc4738dba89086b3b200 Mon Sep 17 00:00:00 2001 From: Sree Raman <1271305+sreejithraman@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:25:23 -0400 Subject: [PATCH 4/4] Refresh checks after rebase