From 97455055ced52c4448d254475d0d9b8ca0fe97d6 Mon Sep 17 00:00:00 2001 From: Duyet Le Date: Sat, 13 Jun 2026 23:36:11 +0700 Subject: [PATCH 1/3] feat(build-ios-apps): add iOS app building skills and agents New plugin for building, profiling, debugging, and refining iOS apps with SwiftUI and Xcode workflows. Skills: App Intents, Simulator debugging, performance profiling (ettrace), memory leak detection (memgraph), SwiftUI performance audit, liquid glass, view refactor, UI patterns. Includes Claude + Codex manifests and an agent definition; already registered in the marketplace manifests. Co-Authored-By: duyetbot --- build-ios-apps/.claude-plugin/plugin.json | 12 + build-ios-apps/.codex-plugin/plugin.json | 23 + build-ios-apps/README.md | 52 ++ build-ios-apps/agents/openai.yaml | 6 + .../skills/ios-app-intents/SKILL.md | 77 ++ .../references/code-templates.md | 303 +++++++ .../references/example-patterns.md | 161 ++++ .../references/first-pass-checklist.md | 57 ++ .../references/system-surfaces.md | 31 + .../skills/ios-debugger-agent/SKILL.md | 51 ++ .../skills/ios-ettrace-performance/SKILL.md | 196 +++++ .../skills/ios-memgraph-leaks/SKILL.md | 76 ++ .../scripts/capture_sim_memgraph.sh | 143 +++ .../scripts/summarize_memgraph_leaks.py | 159 ++++ .../skills/ios-simulator-browser/SKILL.md | 52 ++ .../scripts/swiftui-preview-browser.mjs | 823 ++++++++++++++++++ .../skills/swiftui-liquid-glass/SKILL.md | 90 ++ .../skills/swiftui-performance-audit/SKILL.md | 107 +++ .../references/code-smells.md | 150 ++++ .../references/profiling-intake.md | 44 + .../references/report-template.md | 47 + .../skills/swiftui-ui-patterns/SKILL.md | 96 ++ .../skills/swiftui-view-refactor/SKILL.md | 203 +++++ 23 files changed, 2959 insertions(+) create mode 100644 build-ios-apps/.claude-plugin/plugin.json create mode 100644 build-ios-apps/.codex-plugin/plugin.json create mode 100644 build-ios-apps/README.md create mode 100644 build-ios-apps/agents/openai.yaml create mode 100644 build-ios-apps/skills/ios-app-intents/SKILL.md create mode 100644 build-ios-apps/skills/ios-app-intents/references/code-templates.md create mode 100644 build-ios-apps/skills/ios-app-intents/references/example-patterns.md create mode 100644 build-ios-apps/skills/ios-app-intents/references/first-pass-checklist.md create mode 100644 build-ios-apps/skills/ios-app-intents/references/system-surfaces.md create mode 100644 build-ios-apps/skills/ios-debugger-agent/SKILL.md create mode 100644 build-ios-apps/skills/ios-ettrace-performance/SKILL.md create mode 100644 build-ios-apps/skills/ios-memgraph-leaks/SKILL.md create mode 100644 build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh create mode 100644 build-ios-apps/skills/ios-memgraph-leaks/scripts/summarize_memgraph_leaks.py create mode 100644 build-ios-apps/skills/ios-simulator-browser/SKILL.md create mode 100644 build-ios-apps/skills/ios-simulator-browser/scripts/swiftui-preview-browser.mjs create mode 100644 build-ios-apps/skills/swiftui-liquid-glass/SKILL.md create mode 100644 build-ios-apps/skills/swiftui-performance-audit/SKILL.md create mode 100644 build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md create mode 100644 build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md create mode 100644 build-ios-apps/skills/swiftui-performance-audit/references/report-template.md create mode 100644 build-ios-apps/skills/swiftui-ui-patterns/SKILL.md create mode 100644 build-ios-apps/skills/swiftui-view-refactor/SKILL.md diff --git a/build-ios-apps/.claude-plugin/plugin.json b/build-ios-apps/.claude-plugin/plugin.json new file mode 100644 index 0000000..5427191 --- /dev/null +++ b/build-ios-apps/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "build-ios-apps", + "version": "1.0.0", + "description": "Build, profile, debug, and refine iOS apps with SwiftUI and Xcode workflows including App Intents, Simulator debugging, performance profiling, and memory leak detection.", + "author": { + "name": "duyet", + "url": "https://github.com/duyet" + }, + "homepage": "https://github.com/duyet/codex-claude-plugins", + "repository": "https://github.com/duyet/codex-claude-plugins", + "license": "MIT" +} diff --git a/build-ios-apps/.codex-plugin/plugin.json b/build-ios-apps/.codex-plugin/plugin.json new file mode 100644 index 0000000..11d4241 --- /dev/null +++ b/build-ios-apps/.codex-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "build-ios-apps", + "version": "1.0.0", + "description": "Build, profile, debug, and refine iOS apps with SwiftUI and Xcode workflows including App Intents, Simulator debugging, performance profiling, and memory leak detection.", + "author": { + "name": "duyet" + }, + "skills": "./skills/", + "agents": "./agents/", + "interface": { + "displayName": "Build iOS Apps", + "shortDescription": "Build, profile, debug, and refine iOS apps with SwiftUI and Xcode workflows", + "developerName": "duyet", + "category": "development", + "capabilities": [ + "Skill", + "Agent" + ], + "links": { + "homepage": "https://github.com/duyet/codex-claude-plugins" + } + } +} diff --git a/build-ios-apps/README.md b/build-ios-apps/README.md new file mode 100644 index 0000000..5f20679 --- /dev/null +++ b/build-ios-apps/README.md @@ -0,0 +1,52 @@ +# Build iOS Apps Plugin + +This plugin packages iOS and Swift workflows. + +It currently includes these skills: + +- `ios-debugger-agent` +- `ios-simulator-browser` +- `ios-ettrace-performance` +- `ios-memgraph-leaks` +- `ios-app-intents` +- `swiftui-liquid-glass` +- `swiftui-performance-audit` +- `swiftui-ui-patterns` +- `swiftui-view-refactor` + +## What It Covers + +- designing App Intents, app entities, and App Shortcuts for system surfaces +- building and refactoring SwiftUI UI using current platform patterns +- reviewing or adopting iOS 26+ Liquid Glass APIs +- auditing SwiftUI performance and guiding profiling workflows +- capturing symbolicated ETTrace simulator profiles for focused app flows +- capturing and comparing iOS memgraphs to root-cause leaks +- debugging iOS apps on simulators with XcodeBuildMCP-backed flows +- mirroring Simulator in the browser and hot-reloading package-backed SwiftUI previews +- restructuring large SwiftUI views toward smaller, more stable compositions + +## Plugin Structure + +``` +build-ios-apps/ +├── .claude-plugin/ # Claude manifest +├── .codex-plugin/ # Codex manifest +├── agents/ +│ └── openai.yaml # OpenAI surface metadata +├── assets/ # Icons and SVGs +└── skills/ # Skill payloads + ├── ios-debugger-agent/ + ├── ios-simulator-browser/ + ├── ios-ettrace-performance/ + ├── ios-memgraph-leaks/ + ├── ios-app-intents/ + ├── swiftui-liquid-glass/ + ├── swiftui-performance-audit/ + ├── swiftui-ui-patterns/ + └── swiftui-view-refactor/ +``` + +## License + +MIT diff --git a/build-ios-apps/agents/openai.yaml b/build-ios-apps/agents/openai.yaml new file mode 100644 index 0000000..c02b566 --- /dev/null +++ b/build-ios-apps/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Build iOS Apps" + short_description: "Build, profile, debug, and refine iOS apps with SwiftUI and Xcode workflows" + icon_small: "./assets/build-ios-apps-small.svg" + icon_large: "./assets/app-icon.png" + default_prompt: "Build or debug an iOS app with SwiftUI, App Intents, and Simulator." diff --git a/build-ios-apps/skills/ios-app-intents/SKILL.md b/build-ios-apps/skills/ios-app-intents/SKILL.md new file mode 100644 index 0000000..098e520 --- /dev/null +++ b/build-ios-apps/skills/ios-app-intents/SKILL.md @@ -0,0 +1,77 @@ +--- +name: ios-app-intents +description: Design App Intents, app entities, and App Shortcuts for iOS system surfaces. Use when exposing app actions or content to Shortcuts, Siri, Spotlight, widgets, or controls. +--- + +# iOS App Intents + +## Overview +Expose the smallest useful action and entity surface to the system. Start with the verbs and objects people would actually want outside the app, then implement a narrow App Intents layer that can deep-link or hand off cleanly into the main app when needed. + +Read these references as needed: + +- `references/first-pass-checklist.md` for choosing the first intent and entity surface +- `references/example-patterns.md` for concrete example shapes to copy and adapt +- `references/code-templates.md` for generalized App Intents code templates +- `references/system-surfaces.md` for how to think about Shortcuts, Siri, Spotlight, widgets, and other system entry points + +## Core workflow + +### 1) Start with actions, not screens +- Identify the 1-3 highest-value actions that should work outside the app UI. +- Prefer verbs like compose, open, find, filter, continue, inspect, or start. +- Do not mirror the entire app navigation tree as intents. + +### 2) Define a small entity surface +- Add `AppEntity` types only for the objects the system needs to understand or route. +- Keep the entity shape narrower than the app's persistence model. +- Add `EntityQuery` or other query types only where disambiguation or suggestions are genuinely useful. + +### 3) Decide whether the action completes in place or opens the app +- Use non-opening intents for actions that can complete directly from the system surface. +- Use `openAppWhenRun` or open-style intents when the user should land in a specific in-app workflow. +- When the app must react inside the main scene, add one clear runtime handoff path instead of scattering ad hoc routing logic. +- If the action can work in both modes, consider shipping both an inline version and an open-app version rather than forcing one compromise. + +### 4) Make the actions discoverable +- Add `AppShortcutsProvider` entries for the first set of high-value intents. +- Choose titles, phrases, and symbols that make sense in Shortcuts, Siri, and Spotlight. +- Keep shortcut phrases direct and task-oriented. +- Reuse the same action model for widgets and controls when a widget configuration or intent-driven control already needs the same parameters. + +### 5) Validate the runtime handoff +- Build the app and confirm the intents target compiles cleanly. +- Verify the app opens or routes to the expected place when an intent runs. +- Summarize which actions are now exposed, which entities back them, and how the app handles invocation. + +## Strong defaults + +- Prefer a dedicated intents target or module for the system-facing layer. +- Keep intent types thin; business logic should stay in app services or domain models. +- Keep app entities small and display-friendly. +- Use `AppEnum` for fixed app choices such as tabs, modes, or visibility levels before reaching for a full entity type. +- Prefer one predictable app-intent routing surface in the main app scene or root router. +- Treat App Intents as system integration infrastructure, not only as a Shortcuts feature. + +## Anti-patterns + +- Exposing every screen or tab as its own intent without a real user value. +- Mirroring the entire model graph as `AppEntity` types. +- Hiding runtime handoff in global side effects with no clear app entry path. +- Adding App Shortcuts with vague phrases or generic titles. +- Treating the first App Intents pass as a broad taxonomy project instead of a small useful release. + +## Notes + +- Apple documentation to use as primary references: + - `https://developer.apple.com/documentation/appintents/making-actions-and-content-discoverable-and-widely-available` + - `https://developer.apple.com/documentation/appintents/creating-your-first-app-intent` + - `https://developer.apple.com/documentation/appintents/adopting-app-intents-to-support-system-experiences` +- In addition to the links above, use web search to consult current Apple Developer documentation when App Intents APIs or platform behavior may have changed. +- A good first pass often includes one open-app intent, one action intent, one or two entity types, and a small `AppShortcutsProvider`. +- Good example families to cover are: + - open a destination or editor in the app + - perform a lightweight action inline without opening the app + - choose from a fixed enum such as a tab or mode + - resolve one or more entities through `EntityQuery` + - power widget configuration or controls from the same entity surface diff --git a/build-ios-apps/skills/ios-app-intents/references/code-templates.md b/build-ios-apps/skills/ios-app-intents/references/code-templates.md new file mode 100644 index 0000000..08a76aa --- /dev/null +++ b/build-ios-apps/skills/ios-app-intents/references/code-templates.md @@ -0,0 +1,303 @@ +# Code templates + +These templates are intentionally generic. Rename types and services to fit the app. + +## Open-app handoff intent + +```swift +import AppIntents + +struct OpenComposerIntent: AppIntent { + static let title: LocalizedStringResource = "Open composer" + static let description = IntentDescription("Open the app to compose content") + static let openAppWhenRun = true + + @Parameter( + title: "Prefilled text", + inputConnectionBehavior: .connectToPreviousIntentResult + ) + var text: String? + + func perform() async throws -> some IntentResult { + await MainActor.run { + AppIntentRouter.shared.handledIntent = .init(intent: self) + } + return .result() + } +} +``` + +Scene-side handoff: + +```swift +import AppIntents +import Observation + +@Observable +final class AppIntentRouter { + struct HandledIntent: Equatable { + let id = UUID() + let intent: any AppIntent + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + } + + static let shared = AppIntentRouter() + var handledIntent: HandledIntent? + + private init() {} +} + +private func handleIntent() { + guard let intent = appIntentRouter.handledIntent?.intent else { return } + + if let composerIntent = intent as? OpenComposerIntent { + appRouter.presentComposer(prefilledText: composerIntent.text ?? "") + } else if let sectionIntent = intent as? OpenSectionIntent { + selectedTab = sectionIntent.section.toTab + } +} +``` + +## Inline action intent + +```swift +import AppIntents + +struct CreateItemIntent: AppIntent { + static let title: LocalizedStringResource = "Create item" + static let description = IntentDescription("Create a new item without opening the app") + static let openAppWhenRun = false + + @Parameter(title: "Title") + var title: String + + @Parameter(title: "Workspace") + var workspace: WorkspaceEntity + + func perform() async throws -> some IntentResult & ProvidesDialog { + do { + try await ItemService.shared.createItem(title: title, workspaceID: workspace.id) + return .result(dialog: "Created \(title).") + } catch { + return .result(dialog: "Could not create the item. Please try again.") + } + } +} +``` + +## Fixed selection with `AppEnum` + +```swift +import AppIntents + +enum SectionIntentValue: String, AppEnum { + case inbox + case projects + case settings + + static var typeDisplayName: LocalizedStringResource { "Section" } + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Section" + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] { + [ + .inbox: "Inbox", + .projects: "Projects", + .settings: "Settings", + ] + } + + var toTab: AppTab { + switch self { + case .inbox: .inbox + case .projects: .projects + case .settings: .settings + } + } +} + +struct OpenSectionIntent: AppIntent { + static let title: LocalizedStringResource = "Open section" + static let openAppWhenRun = true + + @Parameter(title: "Section") + var section: SectionIntentValue + + func perform() async throws -> some IntentResult { + await MainActor.run { + AppIntentRouter.shared.handledIntent = .init(intent: self) + } + return .result() + } +} +``` + +## Entity and query + +```swift +import AppIntents + +struct WorkspaceEntity: AppEntity, Identifiable { + let workspace: Workspace + + var id: String { workspace.id } + + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Workspace" + static let defaultQuery = WorkspaceQuery() + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(workspace.name)") + } +} + +struct WorkspaceQuery: EntityQuery { + func entities(for identifiers: [WorkspaceEntity.ID]) async throws -> [WorkspaceEntity] { + let workspaces = try await WorkspaceStore.shared.workspaces(matching: identifiers) + return workspaces.map(WorkspaceEntity.init) + } + + func suggestedEntities() async throws -> [WorkspaceEntity] { + try await WorkspaceStore.shared.recentWorkspaces().map(WorkspaceEntity.init) + } + + func defaultResult() async -> WorkspaceEntity? { + guard let workspace = try? await WorkspaceStore.shared.currentWorkspace() else { return nil } + return WorkspaceEntity(workspace: workspace) + } +} +``` + +## Dependent query + +```swift +import AppIntents + +struct ProjectEntity: AppEntity, Identifiable { + let project: Project + + var id: String { project.id } + + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Project" + static let defaultQuery = ProjectQuery() + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(project.name)") + } +} + +struct ProjectSelectionIntent: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Project widget configuration" + + @Parameter(title: "Workspace") + var workspace: WorkspaceEntity? + + @Parameter(title: "Project") + var project: ProjectEntity? +} + +struct ProjectQuery: EntityQuery { + @IntentParameterDependency(\.$workspace) + var workspace + + func entities(for identifiers: [ProjectEntity.ID]) async throws -> [ProjectEntity] { + try await fetchProjects().filter { identifiers.contains($0.id) }.map(ProjectEntity.init) + } + + func suggestedEntities() async throws -> [ProjectEntity] { + try await fetchProjects().map(ProjectEntity.init) + } + + func defaultResult() async -> ProjectEntity? { + try? await fetchProjects().first.map(ProjectEntity.init) + } + + private func fetchProjects() async throws -> [Project] { + guard let workspaceID = workspace?.id else { return [] } + return try await ProjectStore.shared.projects(in: workspaceID) + } +} +``` + +## Widget configuration intent + +```swift +import AppIntents +import WidgetKit + +struct ActivityWidgetConfiguration: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Activity widget configuration" + static let description = IntentDescription("Choose which workspace and filter the widget should show") + + @Parameter(title: "Workspace") + var workspace: WorkspaceEntity? + + @Parameter(title: "Filter") + var filter: ActivityFilterEntity? +} +``` + +## App shortcuts provider + +```swift +import AppIntents + +struct AppShortcuts: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: OpenComposerIntent(), + phrases: [ + "Open composer in \(.applicationName)", + "Draft with \(.applicationName)", + ], + shortTitle: "Open composer", + systemImageName: "square.and.pencil" + ) + + AppShortcut( + intent: CreateItemIntent(), + phrases: [ + "Create item with \(.applicationName)", + "Add a task in \(.applicationName)", + ], + shortTitle: "Create item", + systemImageName: "plus.circle" + ) + } +} +``` + +## Inline file input + +```swift +import AppIntents +import UniformTypeIdentifiers + +struct ImportAttachmentIntent: AppIntent { + static let title: LocalizedStringResource = "Import attachment" + static let openAppWhenRun = false + + @Parameter( + title: "Files", + supportedContentTypes: [.image, .pdf, .plainText], + inputConnectionBehavior: .connectToPreviousIntentResult + ) + var files: [IntentFile] + + func perform() async throws -> some IntentResult & ProvidesDialog { + guard !files.isEmpty else { + return .result(dialog: "No files were provided.") + } + + for file in files { + guard let url = file.fileURL else { continue } + _ = url.startAccessingSecurityScopedResource() + defer { url.stopAccessingSecurityScopedResource() } + try await AttachmentImporter.shared.importFile(at: url) + } + + return .result(dialog: "Imported \(files.count) file(s).") + } +} +``` diff --git a/build-ios-apps/skills/ios-app-intents/references/example-patterns.md b/build-ios-apps/skills/ios-app-intents/references/example-patterns.md new file mode 100644 index 0000000..3fd0b15 --- /dev/null +++ b/build-ios-apps/skills/ios-app-intents/references/example-patterns.md @@ -0,0 +1,161 @@ +# Example patterns + +Use these as starting points when deciding what to expose first. + +## 1) Open-app handoff intent + +Best for: + +- compose flows +- editors +- navigation to a destination +- actions that need the full app scene, auth state, or richer UI + +Pattern: + +- `openAppWhenRun = true` +- collect lightweight input in the intent +- store one handled-intent payload in a central router or handoff service +- let the app scene translate that payload into tabs, sheets, routes, or windows + +Example: + +- "Open the app to compose a draft" +- "Open the app on a selected section" +- "Open an editor prefilled with content from the previous shortcut step" + +## 2) Inline background action intent + +Best for: + +- quick create/update actions +- send, archive, mark, favorite, or toggle operations +- actions that can finish without the main app UI + +Pattern: + +- `openAppWhenRun = false` +- perform the operation directly in `perform()` +- return dialog or snippet feedback so the result feels complete in Shortcuts or Siri + +Example: + +- "Create a task" +- "Send a message" +- "Archive a document" + +## 3) Paired open-app and inline variants + +Best for: + +- actions that need both automation and richer manual review +- flows where some users want a background shortcut but others want to land in the app + +Pattern: + +- keep parameter names aligned between the two intents +- let the open-app version hand off to UI +- let the inline version call the same domain service directly +- expose both in `AppShortcutsProvider` with clear titles + +Example: + +- "Draft in app" and "Send now" +- "Open image post editor" and "Post images in background" + +## 4) Fixed choice via `AppEnum` + +Best for: + +- tabs +- modes +- visibility levels +- small sets of filters or categories + +Pattern: + +- define an `AppEnum` +- give every case a user-facing `DisplayRepresentation` +- map enum cases into app-specific types in one place + +Example: + +- open a selected tab +- run an action in "public", "private", or "team" mode + +## 5) Entity-backed selection via `AppEntity` + +Best for: + +- accounts +- projects +- lists +- destinations +- saved searches + +Pattern: + +- expose only the fields needed for display and lookup +- add `suggestedEntities()` for picker UX +- add `defaultResult()` only when there is a genuinely helpful default +- keep network or database fetch logic inside the query type, not the view layer + +Example: + +- choose an account to post from +- pick a project to open +- select a saved list for a widget + +## 6) Query dependency between parameters + +Best for: + +- when one parameter changes the valid choices for another +- widget or control configuration where "account" determines "project" + +Pattern: + +- use `@IntentParameterDependency` inside the query +- read the upstream parameter +- scope entity fetching to the chosen parent value + +Example: + +- selected workspace filters available documents +- selected account filters available lists + +## 7) Widget configuration intent + +Best for: + +- widgets that need a selected account, project, filter, or destination +- intent-driven controls that should reuse the same parameter model + +Pattern: + +- define a `WidgetConfigurationIntent` +- use the same `AppEntity` types that your shortcuts already use +- provide preview-friendly sample values when the widget needs them + +Example: + +- choose account plus list +- choose project plus status filter + +## 8) Shortcut phrase design + +Best for: + +- making actions discoverable in Siri and Shortcuts + +Pattern: + +- keep phrases short and verb-led +- expose one or two canonical phrases, then add only a few natural variants +- use precise `shortTitle` and `systemImageName` + +Example: + +- "Create a note with \(.applicationName)" +- "Open inbox in \(.applicationName)" +- "Send image with \(.applicationName)" diff --git a/build-ios-apps/skills/ios-app-intents/references/first-pass-checklist.md b/build-ios-apps/skills/ios-app-intents/references/first-pass-checklist.md new file mode 100644 index 0000000..274003f --- /dev/null +++ b/build-ios-apps/skills/ios-app-intents/references/first-pass-checklist.md @@ -0,0 +1,57 @@ +# First-pass checklist + +Use this checklist when deciding what to expose in the first App Intents release. + +## Pick the first actions + +Choose actions that are: + +- useful without browsing the full app first +- easy to describe in one sentence +- valuable in Shortcuts, Siri, Spotlight, or widgets +- backed by existing app logic instead of requiring a major rewrite + +Good first candidates: + +- compose something +- open a destination or object +- find or filter a known object +- continue an existing workflow +- start a focused action + +Avoid as a first pass: + +- giant setup flows +- actions that only make sense after many in-app taps +- low-value screens exposed only because they exist + +## Pick the first entities + +Use app entities when the system needs to identify or display app objects. + +Good first entities: + +- account +- list +- filter +- destination +- draft +- media item + +Keep each entity focused on: + +- identifier +- display representation +- the few fields the system needs for routing or disambiguation + +Do not mirror the entire persistence model if a much smaller system-facing type will do. + +## Decide the handoff model + +For each intent, ask: + +- Can this finish directly from the system surface? +- Should this open the app to a specific place? +- If it opens the app, what is the single clean route back into the main scene? + +Prefer one explicit routing or handoff service over many feature-specific side channels. diff --git a/build-ios-apps/skills/ios-app-intents/references/system-surfaces.md b/build-ios-apps/skills/ios-app-intents/references/system-surfaces.md new file mode 100644 index 0000000..becd986 --- /dev/null +++ b/build-ios-apps/skills/ios-app-intents/references/system-surfaces.md @@ -0,0 +1,31 @@ +# System surfaces + +Think in system entry points, not just in shortcuts. + +## Shortcuts + +- Good for direct actions and automation chains. +- Expose the actions that users would actually want to reuse. +- Add `AppShortcutsProvider` entries for the first high-value intents. + +## Siri + +- Good for clear verbs and deep-linkable actions. +- Phrase titles and parameters so the system can present and disambiguate them clearly. + +## Spotlight + +- Good for discoverability of both actions and entities. +- Use strong display representations and clear type names. + +## Widgets, Live Activities, and controls + +- Good when the same actions already make sense as intent-driven entry points. +- Reuse the same intent surface where practical instead of inventing separate action models. + +## General guidance + +- Design one small action layer that can serve several surfaces. +- Keep action names concrete and user-facing. +- Prefer structured entities and parameters over trying to encode everything in free-form text. +- Start narrow, ship a useful set, then expand based on real use. diff --git a/build-ios-apps/skills/ios-debugger-agent/SKILL.md b/build-ios-apps/skills/ios-debugger-agent/SKILL.md new file mode 100644 index 0000000..092e79a --- /dev/null +++ b/build-ios-apps/skills/ios-debugger-agent/SKILL.md @@ -0,0 +1,51 @@ +--- +name: ios-debugger-agent +description: Build, run, and debug iOS apps on Simulator with XcodeBuildMCP. Use when launching an app, inspecting simulator UI or logs, or diagnosing runtime behavior. +--- + +# iOS Debugger Agent + +## Overview +Use XcodeBuildMCP to build and run the current project scheme on a booted iOS simulator, interact with the UI, and capture logs. Prefer the MCP tools for simulator control, logs, and view inspection. + +## Core Workflow +Follow this sequence unless the user asks for a narrower action. + +### 1) Discover the booted simulator +- Call `mcp__XcodeBuildMCP__list_sims` and select the simulator with state `Booted`. +- If none are booted, ask the user to boot one (do not boot automatically unless asked). + +### 2) Set session defaults +- Call `mcp__XcodeBuildMCP__session-set-defaults` with: + - `projectPath` or `workspacePath` (whichever the repo uses) + - `scheme` for the current app + - `simulatorId` from the booted device + - Optional: `configuration: "Debug"`, `useLatestOS: true` + +### 3) Build + run (when requested) +- Call `mcp__XcodeBuildMCP__build_run_sim`. +- **If the build fails**, check the error output and retry (optionally with `preferXcodebuild: true`) or escalate to the user before attempting any UI interaction. +- **After a successful build**, verify the app launched by calling `mcp__XcodeBuildMCP__describe_ui` or `mcp__XcodeBuildMCP__screenshot` before proceeding to UI interaction. +- If the app is already built and only launch is requested, use `mcp__XcodeBuildMCP__launch_app_sim`. +- If bundle id is unknown: + 1) `mcp__XcodeBuildMCP__get_sim_app_path` + 2) `mcp__XcodeBuildMCP__get_app_bundle_id` + +## UI Interaction & Debugging +Use these when asked to inspect or interact with the running app. + +- **Describe UI**: `mcp__XcodeBuildMCP__describe_ui` before tapping or swiping. +- **Tap**: `mcp__XcodeBuildMCP__tap` (prefer `id` or `label`; use coordinates only if needed). +- **Type**: `mcp__XcodeBuildMCP__type_text` after focusing a field. +- **Gestures**: `mcp__XcodeBuildMCP__gesture` for common scrolls and edge swipes. +- **Screenshot**: `mcp__XcodeBuildMCP__screenshot` for visual confirmation. + +## Logs & Console Output +- Start logs: `mcp__XcodeBuildMCP__start_sim_log_cap` with the app bundle id. +- Stop logs: `mcp__XcodeBuildMCP__stop_sim_log_cap` and summarize important lines. +- For console output, set `captureConsole: true` and relaunch if required. + +## Troubleshooting +- If build fails, ask whether to retry with `preferXcodebuild: true`. +- If the wrong app launches, confirm the scheme and bundle id. +- If UI elements are not hittable, re-run `describe_ui` after layout changes. diff --git a/build-ios-apps/skills/ios-ettrace-performance/SKILL.md b/build-ios-apps/skills/ios-ettrace-performance/SKILL.md new file mode 100644 index 0000000..5a12e39 --- /dev/null +++ b/build-ios-apps/skills/ios-ettrace-performance/SKILL.md @@ -0,0 +1,196 @@ +--- +name: ios-ettrace-performance +description: Capture and interpret iOS Simulator ETTrace profiles. Use when profiling launch or runtime latency, comparing traces, or finding CPU-heavy stacks. +--- + +# iOS ETTrace Performance + +Use this skill to capture a focused, symbolicated ETTrace profile from an iOS simulator app. Pair it with `../ios-debugger-agent/SKILL.md` when the task also needs simulator build, install, launch, UI driving, logs, or screenshots. + +## Core Workflow + +1. Pick one focused flow and write down the expected start and stop points. +2. Build the exact simulator app that will be installed and profiled. +3. Temporarily link ETTrace into that app target for simulator/debug profiling. +4. Collect UUID-matched dSYMs for the app executable and embedded dynamic frameworks. +5. Capture one launch or runtime trace. +6. Preserve the processed flamegraph JSON immediately after the run. +7. Analyze only the processed JSON and report the flow, artifacts, hotspots, and caveats. + +Avoid broad "use the app for a while" captures. One trace should correspond to one user-visible flow. + +## Setup + +Use a writable run folder for each profiling session: + +```bash +if [ -z "${RUN_DIR:-}" ]; then + RUN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/codex-ios-ettrace.XXXXXX")" +fi +mkdir -p "$RUN_DIR" +``` + +Install the ETTrace runner CLI if it is not already available: + +```bash +brew install emergetools/homebrew-tap/ettrace +``` + +`ettrace` is the host-side macOS runner. The app must also link an `ETTrace.xcframework` for the iOS Simulator architecture. +This workflow is validated for ETTrace v1.1.0 processed `output_.json` files with top-level `nodes`. + +## Link ETTrace Into The App + +Wire ETTrace into the exact app target being profiled. Keep the integration in a clearly temporary patch and remove it when the profiling task is done unless the user explicitly asks to keep it. + +Preferred options: + +- Reuse an existing simulator-compatible `ETTrace.xcframework` if the repo already vendors one. +- If none exists, build a simulator-only copy into `RUN_DIR` from the upstream ETTrace package. +- Link the framework directly into the app target, not only into tests, resources, data files, or a nested launcher target. +- Confirm launch logs print `Starting ETTrace`. +- Profile only one ETTrace-instrumented simulator app at a time because simulator mode listens on a fixed localhost port. + +Build a simulator framework when needed: + +```bash +ETTRACE_TAG="${ETTRACE_TAG:-v1.1.0}" # Override to match the installed runner when Homebrew updates. +ETTRACE_SRC="$RUN_DIR/ETTrace-src" +if [ ! -d "$ETTRACE_SRC" ]; then + git clone --depth 1 --branch "$ETTRACE_TAG" https://github.com/EmergeTools/ETTrace "$ETTRACE_SRC" +fi + +rm -rf "$RUN_DIR/ETTrace-iphonesimulator.xcarchive" "$RUN_DIR/ETTrace.xcframework" +pushd "$ETTRACE_SRC" >/dev/null +xcodebuild archive \ + -scheme ETTrace \ + -archivePath "$RUN_DIR/ETTrace-iphonesimulator.xcarchive" \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + INSTALL_PATH='Library/Frameworks' \ + SKIP_INSTALL=NO \ + CLANG_CXX_LANGUAGE_STANDARD=c++17 + +xcodebuild -create-xcframework \ + -framework "$RUN_DIR/ETTrace-iphonesimulator.xcarchive/Products/Library/Frameworks/ETTrace.framework" \ + -output "$RUN_DIR/ETTrace.xcframework" +popd >/dev/null +``` + +For Bazel apps, a temporary import usually looks like: + +```python +load("@rules_apple//apple:apple.bzl", "apple_dynamic_xcframework_import") + +package(default_visibility = ["//visibility:public"]) + +apple_dynamic_xcframework_import( + name = "ETTrace", + xcframework_imports = glob(["ETTrace.xcframework/**"]), +) +``` + +For Xcode projects, temporarily add the simulator `ETTrace.xcframework` to the app target's Link Binary With Libraries / Embed Frameworks phases for the debug simulator build you are profiling, then remove that wiring after profiling. + +## Symbolication Gate + +Do not draw conclusions from an unsymbolicated flamegraph. Before every capture, prepare a dSYM folder that includes the app dSYM and any embedded first-party dynamic framework dSYMs. + +Collect dSYMs after the final build that produced the installed app: + +```bash +SKILL_DIR="" +APP="" +DSYMS="$RUN_DIR/dsyms" + +"$SKILL_DIR/scripts/collect_ios_dsyms.sh" \ + --app "$APP" \ + --out-dir "$DSYMS" \ + --search-root "$(dirname "$APP")" \ + --search-root "$PWD" \ + --extra-dsym "$RUN_DIR/ETTrace-iphonesimulator.xcarchive/dSYMs/ETTrace.framework.dSYM" +``` + +Add `--require-framework ` for app-owned dynamic frameworks that must symbolicate; use `--require-all-frameworks` only when every embedded framework is app-owned or expected to have symbols. If the helper reports a missing required app or framework dSYM, rebuild the exact simulator app with dSYM generation before tracing, or add the build output directory that contains those dSYMs as another `--search-root`. + +Verify important UUIDs before tracing when the report looks suspicious: + +```bash +dwarfdump --uuid "$APP/$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$APP/Info.plist")" +find "$DSYMS" -maxdepth 1 -type d -name '*.dSYM' -print -exec dwarfdump --uuid {} \; +``` + +After ETTrace exits, read its symbolication summary. Treat meaningful first-party "have library but no symbol" lines as a failed trace unless they are tiny noise. Unsymbolicated system-framework or ETTrace internal buckets are usually acceptable. + +## Capture + +For launch traces: + +```bash +cd "$RUN_DIR" +CAPTURE_MARKER="$RUN_DIR/.ettrace-capture-start" +: > "$CAPTURE_MARKER" +find "$RUN_DIR" -maxdepth 1 \( -name 'output.json' -o -name 'output_*.json' \) -delete +ettrace --simulator --launch --verbose --dsyms "$DSYMS" +``` + +Use `--launch` only when measuring startup or first render. The first launch connection can force quit the app; relaunch from the simulator home screen rather than Xcode if prompted. For first-launch-after-install traces, temporarily set `ETTraceRunAtStartup=YES` in the app Info.plist, then run `ettrace --simulator` and launch from the home screen. + +For runtime flow traces: + +```bash +cd "$RUN_DIR" +CAPTURE_MARKER="$RUN_DIR/.ettrace-capture-start" +: > "$CAPTURE_MARKER" +find "$RUN_DIR" -maxdepth 1 \( -name 'output.json' -o -name 'output_*.json' \) -delete +ettrace --simulator --verbose --dsyms "$DSYMS" +``` + +Start from a stable screen, start ETTrace, perform exactly one focused flow, wait until visible work is complete, then stop the runner. For wider attribution, add `--multi-thread`; otherwise start with the main thread. + +In Codex, run `ettrace` with a TTY and answer prompts with `write_stdin`. Without a TTY, the runner can exit without a useful trace. + +## Preserve Outputs + +The next ETTrace run can overwrite processed flamegraph files, so preserve fresh `output_.json` files immediately. Do not analyze a saved `output.json`; ETTrace also serves a viewer route with that name, and raw `emerge-output/output.json` files are not the processed flamegraph artifacts this workflow expects. + +```bash +PRESERVED_DIR="$(mktemp -d "$RUN_DIR/run-$(date +%Y%m%d-%H%M%S).XXXXXX")" +: > "$PRESERVED_DIR/summary.txt" +if [ ! -e "$CAPTURE_MARKER" ]; then + echo "error: capture marker missing; start a fresh ETTrace capture before preserving outputs" >&2 + exit 1 +fi +find "$RUN_DIR" -maxdepth 1 -name 'output_*.json' -newer "$CAPTURE_MARKER" -print | while IFS= read -r json; do + preserved="$PRESERVED_DIR/${json##*/}" + cp "$json" "$preserved" + { + echo "## ${preserved##*/}" + python3 "$SKILL_DIR/scripts/analyze_flamegraph_json.py" "$preserved" + } >> "$PRESERVED_DIR/summary.txt" +done +if [ ! -s "$PRESERVED_DIR/summary.txt" ]; then + echo "error: no fresh processed ETTrace output JSON found in $RUN_DIR" >&2 + exit 1 +fi +``` + +Analyze only processed `output_*.json` files in `RUN_DIR`. Ignore `output.json` and raw `emerge-output/output.json` files unless debugging ETTrace itself. If the analyzer rejects the JSON shape, capture again with the Homebrew ETTrace runner and matching app-side `ETTrace.xcframework` tag instead of trying to interpret the rejected file. + +## Read The Profile + +Start from `run-*/summary.txt`, then inspect processed JSON directly if needed. + +Report: + +- exact flow, app build, simulator model/runtime, and run count +- processed flamegraph JSON paths +- top active leaves and inclusive first-party stacks with sample weights or percentages +- whether symbols were complete for app-owned binaries +- caveats such as first-run setup, simulator-only cost, network variance, or low sample count +- before/after deltas only when the same flow was captured with comparable setup + +## Cleanup + +Remove temporary ETTrace app wiring when profiling is complete unless the user asked to keep it. Keep or discard run artifacts based on the active task. diff --git a/build-ios-apps/skills/ios-memgraph-leaks/SKILL.md b/build-ios-apps/skills/ios-memgraph-leaks/SKILL.md new file mode 100644 index 0000000..bddac59 --- /dev/null +++ b/build-ios-apps/skills/ios-memgraph-leaks/SKILL.md @@ -0,0 +1,76 @@ +--- +name: ios-memgraph-leaks +description: Capture and inspect iOS leaks and memgraphs. Use when debugging leaked objects, retain cycles, memory growth, or before/after leak evidence. +--- + +# iOS Memgraph Leaks + +Use this skill to prove iOS leaks from a live simulator process or an existing `.memgraph`. Pair it with `../ios-debugger-agent/SKILL.md` when the task also needs simulator build, install, launch, UI driving, logs, or screenshots. + +## Core Workflow + +1. Build, launch, and drive the exact flow that should release objects. +2. Capture a memgraph from the running simulator process with `scripts/capture_sim_memgraph.sh`. +3. Summarize leaks with `scripts/summarize_memgraph_leaks.py`. +4. For each app-owned leaked type, inspect ownership with `leaks --traceTree=
` and grouped leak evidence. +5. Make the smallest root-cause patch, then recapture the same flow on the same simulator when possible. +6. Report proof: before/after leak counts, disappeared root types, remaining leaks, memgraph paths, and test/build results. + +Do not claim a leak fix from a smaller memgraph alone. A credible fix explains the ownership path that kept the object alive and shows that the same path or type disappears after the patch. + +## Capture + +Prefer capturing from the simulator already used for the reproduction. Resolve the simulator UDID and app bundle identifier, then capture the running app: + +```bash +SKILL_DIR="" +SIM="" +BUNDLE_ID="" +MEMGRAPH_DIR="$(mktemp -d "${TMPDIR:-/tmp}/codex-ios-memgraph.XXXXXX")" + +"$SKILL_DIR/scripts/capture_sim_memgraph.sh" \ + --udid "$SIM" \ + --bundle-id "$BUNDLE_ID" \ + --out-dir "$MEMGRAPH_DIR" +``` + +Do not derive `SKILL_DIR` from the target app repo's `pwd`; installed plugins usually live outside the app being debugged. Store captures in a run-specific temp or user-chosen folder, not under `SKILL_DIR`. + +If the process cannot be found, confirm the bundle identifier and use `xcrun simctl spawn "$SIM" launchctl list` to inspect running labels. + +## Summarize + +Summarize an existing memgraph: + +```bash +"$SKILL_DIR/scripts/summarize_memgraph_leaks.py" \ + /path/to/app.memgraph \ + --trace-limit 5 \ + --out /path/to/leak-summary.md +``` + +Use `--trace-limit` sparingly. Trace trees are useful root-cause evidence, but large memgraphs can produce noisy output. If a trace tree says `Found 0 roots referencing`, treat it as an unreachable/self-retained leak candidate and use the summary's grouped leak tree or `leaks --groupByType ` to identify the retained fields and payload chain. + +## Root Cause Rules + +- Identify the first app-owned leaked type in the leak output or trace. +- Determine the intended lifetime: process, session, account, view, request, or task. +- Treat lazy or deferred allocation as a scope reduction, not a leak fix, unless the original eager allocation itself violated the intended lifetime. +- Prove retain-cycle claims with either a `traceTree` ownership path or an isolated reproduction. +- For unreachable/self-cycle leaks, `traceTree` may have no root path; use `leaks --groupByType` plus source verification to find the self-retaining edge. +- Do not claim success just because total leak count went down; prove the specific type or path disappeared. +- Separate real root-cause branches from candidate/noise branches. +- Prefer deleting the retaining edge over adding broad cleanup code. + +## Report + +A useful leak report includes: + +- the exact flow and simulator/app build +- the memgraph and summary paths +- app-owned leaked types and counts +- at least one ownership path, or grouped leak tree evidence when the object is unreachable from roots +- the smallest proposed or applied retaining-edge fix +- before/after evidence when a fix was made + +If the memgraph shows only framework/runtime noise, say that and recommend the next narrower capture rather than inventing an app leak. diff --git a/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh b/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh new file mode 100644 index 0000000..f0804f0 --- /dev/null +++ b/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Capture a memory graph from a running iOS simulator app. + +Required: + --udid UDID Simulator UDID + --bundle-id ID App bundle identifier, e.g. com.example.app + +Optional: + --out-dir DIR Output directory for the memgraph and leaks output + +Example: + capture_sim_memgraph.sh --udid "$SIM" --bundle-id com.example.app --out-dir /tmp/codex-ios-memgraph +USAGE +} + +require_value() { + local flag="$1" + local value="${2:-}" + if [[ -z "$value" ]]; then + echo "$flag requires a value" >&2 + usage >&2 + exit 2 + fi +} + +bundle_id="" +out_dir="" +udid="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --bundle-id) + require_value "$1" "${2:-}" + bundle_id="$2" + shift 2 + ;; + --out-dir) + require_value "$1" "${2:-}" + out_dir="$2" + shift 2 + ;; + --udid) + require_value "$1" "${2:-}" + udid="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$udid" ]]; then + echo "--udid is required" >&2 + usage >&2 + exit 2 +fi + +if [[ -z "$bundle_id" ]]; then + echo "--bundle-id is required" >&2 + usage >&2 + exit 2 +fi + +if [[ -z "$out_dir" ]]; then + out_dir="$(mktemp -d "${TMPDIR:-/tmp}/codex-ios-memgraph.XXXXXX")" +fi + +matching_processes="$( + xcrun simctl spawn "$udid" launchctl list | + awk -v bundle_id="$bundle_id" ' + $1 == "-" { + next + } + $3 == bundle_id { + print $1 "\t" $3 + next + } + index($3, "UIKitApplication:" bundle_id "[") == 1 { + print $1 "\t" $3 + } + ' +)" + +if [[ -z "$matching_processes" ]]; then + echo "Could not find a running PID for $bundle_id on $udid" >&2 + exit 1 +fi + +if [[ "$(printf '%s\n' "$matching_processes" | wc -l | tr -d ' ')" -ne 1 ]]; then + echo "Found multiple running PIDs for $bundle_id on $udid:" >&2 + printf '%s\n' "$matching_processes" >&2 + exit 1 +fi + +pid="$(printf '%s\n' "$matching_processes" | awk '{ print $1 }')" +process_label="$(printf '%s\n' "$matching_processes" | cut -f2-)" + +mkdir -p "$out_dir" + +timestamp="$(date +%Y%m%d-%H%M%S)" +safe_bundle="$(printf '%s' "$bundle_id" | tr -c 'A-Za-z0-9_.-' '_')" +memgraph="$out_dir/$safe_bundle-$pid-$timestamp.memgraph" +leaks_output="$out_dir/$safe_bundle-$pid-$timestamp.leaks.txt" +metadata="$out_dir/$safe_bundle-$pid-$timestamp.metadata.txt" + +{ + echo "date: $(date)" + echo "udid: $udid" + echo "bundle_id: $bundle_id" + echo "process_label: $process_label" + echo "pid: $pid" + echo "memgraph: $memgraph" + echo "leaks_output: $leaks_output" +} > "$metadata" + +set +e +leaks "--outputGraph=$memgraph" "$pid" > "$leaks_output" 2>&1 +leaks_status=$? +set -e + +echo "leaks_exit_status: $leaks_status" >> "$metadata" + +if [[ ! -f "$memgraph" ]]; then + echo "memgraph_missing: true" >> "$metadata" + echo "leaks failed to create a memgraph; see: $leaks_output" >&2 + echo "metadata: $metadata" >&2 + exit 1 +fi + +echo "memgraph: $memgraph" +echo "leaks output: $leaks_output" +echo "metadata: $metadata" diff --git a/build-ios-apps/skills/ios-memgraph-leaks/scripts/summarize_memgraph_leaks.py b/build-ios-apps/skills/ios-memgraph-leaks/scripts/summarize_memgraph_leaks.py new file mode 100644 index 0000000..4c77ec1 --- /dev/null +++ b/build-ios-apps/skills/ios-memgraph-leaks/scripts/summarize_memgraph_leaks.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Summarize leaks output from an Apple .memgraph file.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from collections import Counter +from pathlib import Path + + +LEAK_RE = re.compile(r"^Leak:\s+(?P
0x[0-9a-fA-F]+)\s+size=(?P\d+)\s+(?P.*)$") +TOTAL_RE = re.compile(r"Process\s+\S+:\s+(?P\d+)\s+leaks?\s+for\s+(?P\d+)\s+total leaked bytes") + + +def run_leaks(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(["leaks", *args], text=True, capture_output=True, check=False) + + +def parse_leaks(output: str) -> tuple[str | None, list[dict[str, str]]]: + total = None + leaks: list[dict[str, str]] = [] + for line in output.splitlines(): + if total is None: + match = TOTAL_RE.search(line) + if match: + total = f"{match.group('count')} leaks / {match.group('bytes')} bytes" + match = LEAK_RE.match(line) + if match: + fields = match.groupdict() + rest = fields.pop("rest") + rest = re.sub(r"^zone:\s+\S+\s+", "", rest) + parts = re.split(r"\s{2,}", rest.strip(), maxsplit=2) + if len(parts) == 3: + fields["type"], fields["language"], fields["image"] = parts + elif len(parts) == 2: + fields["type"], fields["image"] = parts + fields["language"] = "" + else: + fields["type"] = rest.strip() or "" + fields["language"] = "" + fields["image"] = "" + leaks.append(fields) + return total, leaks + + +def trace_excerpt(memgraph: Path, address: str, max_lines: int) -> str: + result = run_leaks([f"--traceTree={address}", str(memgraph)]) + text = result.stdout or result.stderr + lines = [line.rstrip() for line in text.splitlines() if line.strip()] + return "\n".join(lines[:max_lines]) + + +def group_by_type_excerpt(memgraph: Path, max_lines: int) -> str: + result = run_leaks(["--groupByType", str(memgraph)]) + text = result.stdout or result.stderr + lines = [line.rstrip() for line in text.splitlines() if line.strip()] + return "\n".join(lines[:max_lines]) + + +def render(memgraph: Path, trace_limit: int, trace_lines: int, raw_output: str) -> str: + total, leaks = parse_leaks(raw_output) + by_type = Counter(leak["type"] for leak in leaks) + by_image = Counter(leak["image"] for leak in leaks) + + lines: list[str] = [] + lines.append(f"# Leak Summary: {memgraph}") + lines.append("") + lines.append(f"- Total: {total or 'not found'}") + lines.append(f"- Parsed leak entries: {len(leaks)}") + lines.append("") + + if by_type: + lines.append("## Top Types") + for name, count in by_type.most_common(20): + lines.append(f"- {count}x {name}") + lines.append("") + + if by_image: + lines.append("## Top Images") + for name, count in by_image.most_common(20): + lines.append(f"- {count}x {name}") + lines.append("") + + if leaks: + lines.append("## Leak Entries") + for leak in leaks[:50]: + lines.append( + f"- {leak['address']} size={leak['size']} type={leak['type']} " + f"image={leak['image']}" + ) + if len(leaks) > 50: + lines.append(f"- ... {len(leaks) - 50} more") + lines.append("") + + if trace_limit > 0 and leaks: + lines.append("## TraceTree Excerpts") + for leak in leaks[:trace_limit]: + lines.append(f"### {leak['address']} {leak['type']}") + excerpt = trace_excerpt(memgraph, leak["address"], trace_lines) + lines.append("~~~text") + lines.append(excerpt or "") + lines.append("~~~") + lines.append("") + + if leaks: + lines.append("## Grouped Leak Tree") + lines.append("Use this when `traceTree` has no roots, which is common for unreachable retain cycles.") + lines.append("~~~text") + lines.append(group_by_type_excerpt(memgraph, trace_lines) or "") + lines.append("~~~") + lines.append("") + + lines.append("## Raw Commands") + lines.append("~~~bash") + lines.append(f"leaks --list {memgraph}") + if leaks: + lines.append(f"leaks --groupByType {memgraph}") + if leaks: + lines.append(f"leaks --traceTree={leaks[0]['address']} {memgraph}") + lines.append("~~~") + lines.append("") + + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("memgraph", type=Path) + parser.add_argument("--trace-limit", type=int, default=0, help="Number of leaks to trace with --traceTree") + parser.add_argument("--trace-lines", type=int, default=80, help="Max lines per traceTree excerpt") + parser.add_argument("--out", type=Path, help="Write markdown summary to this file") + args = parser.parse_args() + + if not args.memgraph.exists(): + print(f"memgraph not found: {args.memgraph}", file=sys.stderr) + return 2 + + result = run_leaks(["--list", str(args.memgraph)]) + raw = result.stdout or result.stderr + total, leaks = parse_leaks(raw) + if result.returncode != 0 and total is None and not leaks: + print(raw, file=sys.stderr, end="" if raw.endswith("\n") else "\n") + return result.returncode or 1 + + summary = render(args.memgraph, args.trace_limit, args.trace_lines, raw) + if args.out: + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(summary) + print(args.out) + else: + print(summary) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build-ios-apps/skills/ios-simulator-browser/SKILL.md b/build-ios-apps/skills/ios-simulator-browser/SKILL.md new file mode 100644 index 0000000..8e490ab --- /dev/null +++ b/build-ios-apps/skills/ios-simulator-browser/SKILL.md @@ -0,0 +1,52 @@ +--- +name: ios-simulator-browser +description: Mirror an iOS Simulator into the Codex in-app browser and render SwiftUI previews from importable Swift packages in that simulator with hot reload. +--- + +# iOS Simulator Browser + +## Browser Workflow + +1. Obtain an explicit Simulator UDID from the existing iOS build/run workflow or from `xcrun simctl list devices available`. +2. Start `serve-sim` in a long-running terminal pinned to that simulator. Clean up any tracked stale helper for this simulator before starting, and install a trap so the helper is cleaned up when this terminal exits: + + ```bash + SIM="" + cleanup_serve_sim() { + npx --yes serve-sim@latest --kill "$SIM" >/dev/null 2>&1 || true + } + trap cleanup_serve_sim EXIT INT TERM HUP + cleanup_serve_sim + npx --yes serve-sim@latest "$SIM" + ``` + +3. Open the exact local preview URL printed by `serve-sim` in the Codex in-app browser. +4. Verify that a real frame is rendering before reporting success. A loaded page alone is not proof that the simulator stream is healthy. + +- Keep the terminal alive while the browser mirror is in use. When finished, stop the terminal and wait for it to exit so the trap runs. +- If the terminal disappeared or did not exit cleanly, run `npx --yes serve-sim@latest --kill "$SIM"` before starting another mirror for that simulator. +- Never run an unscoped `serve-sim --kill`; another thread may own a different simulator mirror. + +## SwiftUI Preview Workflow + +Use the bundled launcher when the requested previews live in an importable Swift package. Point it at the package manifest and select the target whose previews should be displayed. It generates a disposable host project outside the user's source tree, installs and launches that host in Simulator, and watches the package for edits. + +```bash +node /scripts/swiftui-preview-browser.mjs \ + /absolute/path/to/Package.swift \ + --package-target "" \ + --device "" +``` + +- Watch mode is enabled by default. On a Swift package source edit, the launcher rebuilds a generated dylib and hot-swaps it into the running host without relaunching the app. +- The generated host shows every preview variant discovered in the selected Swift Package target with in-simulator page controls. To show a subset instead, pass `--preview-filter `; it matches display names and code identifiers such as `StatusRowView_Previews`. +- Once the launcher prints the selected Simulator UDID, start `serve-sim` for that same UDID and open its printed URL in the in-app browser. + +## Support Boundary + +- Support Swift Package-backed `PreviewProvider` and `#Preview` declarations through the generated host. +- Do not edit the user's `.xcodeproj`, `.xcworkspace`, `Package.swift`, schemes, or build settings to force preview support. + +## Proof + +For browser or preview QA, capture a browser screenshot showing the simulator frame. For hot reload QA, also report the launcher's `hot reloaded package preview ... in pid ...` output and show the changed frame after editing. diff --git a/build-ios-apps/skills/ios-simulator-browser/scripts/swiftui-preview-browser.mjs b/build-ios-apps/skills/ios-simulator-browser/scripts/swiftui-preview-browser.mjs new file mode 100644 index 0000000..3f801c2 --- /dev/null +++ b/build-ios-apps/skills/ios-simulator-browser/scripts/swiftui-preview-browser.mjs @@ -0,0 +1,823 @@ +#!/usr/bin/env node + +import { createHash, randomUUID } from "node:crypto"; +import { existsSync, readFileSync, watch } from "node:fs"; +import { copyFile, mkdir, open, rm, writeFile } from "node:fs/promises"; +import { basename, dirname, join, relative, resolve } from "node:path"; +import { execFile, execFileSync, spawn } from "node:child_process"; +import { tmpdir } from "node:os"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { + createPackageProjectFile, + generatedPackageConfigurationSwiftSource, +} from "./lib/xcode-project.mjs"; + +const TARGET_NAME = "PreviewHost"; +const PACKAGE_PLUGIN_TARGET_NAME = "PreviewReloadPlugin"; +const BUNDLE_ID = "dev.swiftui-preview-browser.host"; +const HOT_RELOAD_NOTIFICATION = "dev.swiftui-preview-browser.reload"; +const TEMPLATE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "templates"); +const TEMPLATE_FILE_NAMES = [ + "FocusedPreviewApp.swift", + "FocusedPreviewHotReloadRuntime.swift", + "PreviewBrowserEntries.swift", +]; +const CONFIGURATION_FILE_NAME = "PreviewBrowserConfiguration.swift"; +const execFileAsync = promisify(execFile); + +/** + * Runs the preview-browser workflow from command-line arguments. + * + * This resolves the Swift Package module, launches a generated host app in + * Simulator, and starts watching the package for hot reloads. + * + * @param {string[]} argv Command-line arguments excluding `node` and script path. + * @returns {Promise} + */ +export async function main(argv) { + const options = parseArgs(argv); + if (options.help) { + printHelp(); + return; + } + + const packageSwiftPath = resolvePackageSwiftPath(options.packageSwiftPath); + const packagePreviewConfiguration = resolvePackagePreviewConfiguration(packageSwiftPath, options); + const scratchRoot = defaultScratchRoot(packageSwiftPath, packagePreviewConfiguration); + const buildRoot = join(scratchRoot, "build"); + const projectRoot = join(scratchRoot, "GeneratedPreviewHost"); + const udid = options.device; + + const state = { + projectRoot, + buildRoot, + packagePreviewConfiguration, + udid, + dataContainer: null, + appPid: null, + previewName: packagePreviewConfiguration.previewFilters?.join(", ") + ?? `${packagePreviewConfiguration.packageModule} previews`, + hotReloading: false, + hotReloadQueued: false, + }; + + log(`Package.swift: ${packageSwiftPath}`); + log(`package: ${packagePreviewConfiguration.packageRoot}`); + log(`product: ${packagePreviewConfiguration.packageProduct}`); + log(`module: ${packagePreviewConfiguration.packageModule}`); + if (packagePreviewConfiguration.previewFilters) { + log(`preview filters: ${packagePreviewConfiguration.previewFilters.join(", ")}`); + } + log(`simulator: ${udid}`); + await ensureBooted(udid); + await buildAndLaunchPackage(state); + console.log(`swiftui-preview-browser ready on simulator ${udid}`); + + const handleChange = async () => { + log("change detected; hot reloading"); + await enqueueHotReload(state); + }; + + watchPackageTree(state.packagePreviewConfiguration.packageRoot, handleChange); +} + +/** + * Parses supported CLI flags into the options consumed by the launcher. + * + * @param {string[]} argv Raw command-line arguments. + * @returns {object} Normalized launcher options. + */ +function parseArgs(argv) { + const options = { + packageSwiftPath: null, + device: null, + packageTarget: null, + previewFilters: null, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--help" || arg === "-h") { + options.help = true; + } else if (arg === "--device" || arg === "-d") { + options.device = argv[++index]; + } else if (arg === "--package-target") { + options.packageTarget = argv[++index]; + } else if (arg === "--preview-filter") { + options.previewFilters = parsePreviewFilters(argv[++index]); + } else if (!options.packageSwiftPath) { + options.packageSwiftPath = arg; + } else { + throw new Error(`Unexpected argument: ${arg}`); + } + } + + if (!options.help && !options.packageSwiftPath) { + throw new Error("Pass the path to Package.swift."); + } + if (!options.help && !options.packageTarget) { + throw new Error("Pass --package-target to choose which Swift Package target to preview."); + } + if (!options.help && !options.device) { + throw new Error("Pass --device to choose the Simulator explicitly."); + } + return options; +} + +/** + * Prints the command-line usage and supported flags. + * + * @returns {void} + */ +function printHelp() { + console.log(`swiftui-preview-browser + +Render SwiftUI previews from one Swift Package target in an iOS Simulator. + +Usage: + swiftui-preview-browser --package-target --device + +Options: + --package-target Swift Package target to scan. Required. + --preview-filter Comma-separated regex filters matched against preview names and identifiers. + --device, -d Simulator UDID. Required. +`); +} + +/** + * Resolves and validates the `Package.swift` file selected by the user. + * + * @param {string} file User-provided `Package.swift` path. + * @returns {string} Absolute path to an existing `Package.swift`. + */ +function resolvePackageSwiftPath(file) { + const resolved = resolve(file); + if (!existsSync(resolved) || basename(resolved) !== "Package.swift") { + throw new Error(`Package.swift not found: ${resolved}`); + } + return resolved; +} + +/** + * Creates a stable scratch-directory path for one module preview selection. + * + * Including selection details lets separate sessions from one package avoid + * rewriting each other's generated project. + * + * @param {string} packageSwiftPath Absolute path to `Package.swift`. + * @param {object} packagePreviewConfiguration Resolved preview selection and build metadata. + * @returns {string} Generated scratch directory. + */ +function defaultScratchRoot(packageSwiftPath, packagePreviewConfiguration) { + const identity = JSON.stringify({ + packageSwiftPath, + packageModule: packagePreviewConfiguration.packageModule, + previewFilters: packagePreviewConfiguration.previewFilters, + }); + const scratchId = createHash("sha256").update(identity).digest("hex").slice(0, 12); + return join(tmpdir(), "swiftui-preview-browser", scratchId); +} + +/** + * Generates, builds, installs, and launches the disposable preview host app. + * + * @param {object} state Mutable launcher state for the current preview session. + * @returns {Promise} + */ +async function buildAndLaunchPackage(state) { + try { + await rm(state.projectRoot, { recursive: true, force: true }); + await mkdir(join(state.projectRoot, `${TARGET_NAME}.xcodeproj`), { recursive: true }); + await mkdir(join(state.projectRoot, TARGET_NAME), { recursive: true }); + + await copySwiftTemplates(state.projectRoot); + await writeFile( + join(state.projectRoot, TARGET_NAME, CONFIGURATION_FILE_NAME), + generatedPackageConfigurationSwiftSource({ + packageModule: state.packagePreviewConfiguration.packageModule, + previewFilters: state.packagePreviewConfiguration.previewFilters, + }), + ); + await writeFile( + join(state.projectRoot, `${TARGET_NAME}.xcodeproj`, "project.pbxproj"), + createPackageProjectFile({ + targetName: TARGET_NAME, + bundleId: BUNDLE_ID, + packageRelativePath: relative(state.projectRoot, state.packagePreviewConfiguration.packageRoot) || ".", + packageProduct: state.packagePreviewConfiguration.packageProduct, + pluginTargetName: PACKAGE_PLUGIN_TARGET_NAME, + deploymentTarget: state.packagePreviewConfiguration.deploymentTarget, + }), + ); + + const buildLogPath = join(state.buildRoot, "logs", "preview-host.log"); + await run( + "xcodebuild", + [ + "-project", + join(state.projectRoot, `${TARGET_NAME}.xcodeproj`), + "-scheme", + TARGET_NAME, + "-configuration", + "Debug", + "-destination", + `id=${state.udid}`, + "-sdk", + "iphonesimulator", + "-derivedDataPath", + state.buildRoot, + "CODE_SIGNING_ALLOWED=NO", + "build", + ], + { + cwd: state.projectRoot, + timeoutMs: 900_000, + outputFile: buildLogPath, + }, + ); + log(`built preview host; build log: ${buildLogPath}`); + + const appPath = join( + state.buildRoot, + "Build/Products/Debug-iphonesimulator", + `${TARGET_NAME}.app`, + ); + if (!existsSync(appPath)) { + throw new Error(`Build succeeded but app bundle was not found: ${appPath}`); + } + + await run("xcrun", ["simctl", "terminate", state.udid, BUNDLE_ID], { + allowFailure: true, + logOutput: true, + }); + await run("xcrun", ["simctl", "install", state.udid, appPath], { logOutput: true }); + state.dataContainer = await appDataContainer(state.udid); + await rm(join(state.dataContainer, "Documents", "swiftui-preview-browser"), { + recursive: true, + force: true, + }); + const launchResult = await run( + "xcrun", + ["simctl", "launch", state.udid, BUNDLE_ID], + { logOutput: true }, + ); + state.appPid = launchPid(launchResult); + await waitForHostReady(state); + + log(`launched package preview host for ${state.previewName}`); + } catch (error) { + log(error instanceof Error ? error.message : String(error)); + throw error; + } +} + +/** + * Copies the reusable Swift preview host sources into the disposable project. + * + * Only the package import and filter values are generated per run; the + * runtime, preview UI, and reload bridge remain ordinary Swift source files. + * + * @param {string} projectRoot Root directory of the generated Xcode project. + * @returns {Promise} + */ +async function copySwiftTemplates(projectRoot) { + await Promise.all( + TEMPLATE_FILE_NAMES.map((fileName) => + copyFile(join(TEMPLATE_ROOT, fileName), join(projectRoot, TARGET_NAME, fileName)), + ), + ); +} + +/** + * Starts a hot reload while converting failures into already-logged events. + * + * This is used by the file watcher so a failed edit does not terminate watch + * mode and the next edit can recover. + * + * @param {object} state Mutable launcher state. + * @returns {Promise} + */ +async function enqueueHotReload(state) { + try { + await hotReload(state); + } catch { + // hotReload already logged the failure. + } +} + +/** + * Serializes hot reload work and queues one follow-up reload when edits race. + * + * @param {object} state Mutable launcher state. + * @returns {Promise} + */ +async function hotReload(state) { + if (state.hotReloading) { + state.hotReloadQueued = true; + return; + } + + state.hotReloading = true; + + try { + await hotReloadPackage(state); + } finally { + state.hotReloading = false; + if (state.hotReloadQueued) { + state.hotReloadQueued = false; + void enqueueHotReload(state); + } + } +} + +/** + * Compiles the updated package-backed preview into a dylib and injects it. + * + * The host reads a reload manifest from its documents directory, loads that + * dylib in process, and reports the PID so this method can prove no relaunch + * occurred. + * + * @param {object} state Mutable launcher state. + * @returns {Promise} + */ +async function hotReloadPackage(state) { + try { + const hostBefore = readHostStatus(state); + const expectedPid = hostBefore?.pid ?? state.appPid; + + const token = randomUUID(); + const buildLogPath = join(state.buildRoot, "logs", "hot-reload.log"); + await run( + "xcodebuild", + [ + "-project", + join(state.projectRoot, `${TARGET_NAME}.xcodeproj`), + "-scheme", + PACKAGE_PLUGIN_TARGET_NAME, + "-configuration", + "Debug", + "-destination", + `id=${state.udid}`, + "-sdk", + "iphonesimulator", + "-derivedDataPath", + state.buildRoot, + "CODE_SIGNING_ALLOWED=NO", + "build", + ], + { + cwd: state.projectRoot, + timeoutMs: 900_000, + outputFile: buildLogPath, + }, + ); + log(`built hot reload plugin; build log: ${buildLogPath}`); + + const dylibPath = join( + state.buildRoot, + "Build/Products/Debug-iphonesimulator", + `lib${PACKAGE_PLUGIN_TARGET_NAME}.dylib`, + ); + if (!existsSync(dylibPath)) { + throw new Error(`Package hot reload build succeeded but dylib was not found: ${dylibPath}`); + } + + const reloadDir = join(state.dataContainer, "Documents", "swiftui-preview-browser"); + await mkdir(reloadDir, { recursive: true }); + const containerDylibPath = join(reloadDir, `lib${PACKAGE_PLUGIN_TARGET_NAME}-${token}.dylib`); + await copyFile(dylibPath, containerDylibPath); + const manifestPath = join(reloadDir, "reload.json"); + await writeFile(manifestPath, JSON.stringify({ token, dylibPath: containerDylibPath })); + await run("xcrun", ["simctl", "spawn", state.udid, "notifyutil", "-p", HOT_RELOAD_NOTIFICATION], { + logOutput: true, + }); + + const hostAfter = await waitForHostReload(state, token); + if (expectedPid && hostAfter.pid !== expectedPid) { + throw new Error(`Hot reload changed PID from ${expectedPid} to ${hostAfter.pid}`); + } + + state.appPid = hostAfter.pid; + log(`hot reloaded package preview ${state.previewName} in pid ${hostAfter.pid}`); + } catch (error) { + log(error instanceof Error ? error.message : String(error)); + throw error; + } +} + +/** + * Derives all build and selection information for a Swift Package preview. + * + * The target is selected explicitly by the user, while the linkable library + * product is selected internally from `Package.swift`. + * + * @param {string} packageSwiftPath Absolute path to `Package.swift`. + * @param {object} options Parsed CLI options. + * @returns {object} Package target, product, filters, and build settings. + */ +function resolvePackagePreviewConfiguration(packageSwiftPath, options) { + const packageRoot = dirname(packageSwiftPath); + const packageDump = runJson("swift", ["package", "dump-package", "--package-path", packageRoot]); + const packageTarget = resolvePackageTarget(options.packageTarget, packageDump); + const packageProduct = inferPackageProduct(packageTarget, packageDump); + const packageDescription = runJson( + "swift", + ["package", "--package-path", packageRoot, "describe", "--type", "json"], + ); + const packageModule = resolvePackageModule(packageTarget, packageDescription); + const previewFilters = options.previewFilters; + const deploymentTarget = inferPackageDeploymentTarget(packageDump); + + return { + packageRoot, + packageProduct, + packageModule, + previewFilters, + deploymentTarget, + }; +} + +/** + * Validates the package target/module requested for preview discovery. + * + * @param {string} requestedTarget Requested module name. + * @param {object} packageDump Parsed `swift package dump-package` output. + * @returns {string} Swift package target/module name. + */ +function resolvePackageTarget(requestedTarget, packageDump) { + const target = (packageDump.targets ?? []).find((candidate) => + candidate.name === requestedTarget && candidate.type === "regular", + ); + if (target) { + return target.name; + } + + throw new Error(`Swift package does not contain a regular target named "${requestedTarget}".`); +} + +/** + * Resolves the importable module identifier computed by Swift Package Manager. + * + * SwiftPM target names may contain characters such as hyphens that are invalid + * in Swift imports, so use the target's described C99-compatible module name. + * + * @param {string} packageTarget Selected Swift package target name. + * @param {object} packageDescription Parsed `swift package describe --type json` output. + * @returns {string} Swift module name to use in generated imports. + */ +function resolvePackageModule(packageTarget, packageDescription) { + const target = (packageDescription.targets ?? []).find((candidate) => + candidate.name === packageTarget, + ); + if (target?.c99name) { + return target.c99name; + } + + throw new Error(`Swift package target "${packageTarget}" does not expose an importable module name.`); +} + +/** + * Finds the exported library product that contains the preview target. + * + * Product selection is internal because users select modules rather than link + * artifacts. Prefer a product named after the target, then the smallest + * product exporting it, then alphabetical order for deterministic builds. + * Explicit dynamic products are unsupported because hot reload only replaces + * the generated preview plugin dylib, leaving an already-loaded framework stale. + * + * @param {string} packageTarget Target/module containing the preview. + * @param {object} packageDump Parsed `swift package dump-package` output. + * @returns {string} Product name to link into the generated host. + */ +function inferPackageProduct(packageTarget, packageDump) { + const products = (packageDump.products ?? []) + .filter((product) => + product.type?.library != null && (product.targets ?? []).includes(packageTarget), + ); + const supportedProducts = products.filter((product) => !product.type.library.includes("dynamic")); + const product = supportedProducts.find(({ name }) => name === packageTarget) + ?? supportedProducts.sort((left, right) => + (left.targets?.length ?? 0) - (right.targets?.length ?? 0) + || left.name.localeCompare(right.name) + )[0]; + if (product) { + return product.name; + } + if (products.length > 0) { + throw new Error( + `Swift package target "${packageTarget}" is only exported by dynamic library products. ` + + "Dynamic library products are not supported because hot reload only replaces the generated preview plugin dylib.", + ); + } + throw new Error(`Swift package target "${packageTarget}" is not exported by a library product.`); +} + +/** + * Uses at least iOS 17 for the generated host because it uses Observation. + * A package that supports earlier systems can still be linked into that host. + * + * @param {object} packageDump Parsed `swift package dump-package` output. + * @returns {string} iOS deployment target version. + */ +function inferPackageDeploymentTarget(packageDump) { + const iosPlatform = (packageDump.platforms ?? []).find((platform) => platform.platformName === "ios"); + const packageDeploymentTarget = iosPlatform?.version ?? "17.0"; + return Number.parseFloat(packageDeploymentTarget) < 17 ? "17.0" : packageDeploymentTarget; +} + +/** + * Parses user-supplied comma-separated preview selection filters. + * + * @param {string} value Filter argument passed to `--preview-filter`. + * @returns {string[]} Non-empty filters in user-specified order. + */ +function parsePreviewFilters(value) { + const filters = value + ?.split(",") + .map((filter) => filter.trim()) + .filter(Boolean); + if (!filters?.length) { + throw new Error("Pass at least one non-empty value to --preview-filter."); + } + return filters; +} + +/** + * Boots the selected simulator, waiting until it is ready for install/launch. + * + * @param {string} udid Simulator identifier. + * @returns {Promise} + */ +async function ensureBooted(udid) { + const json = runJson("xcrun", ["simctl", "list", "devices", "available", "-j"]); + const device = Object.values(json.devices ?? {}).flat().find((entry) => entry.udid === udid); + if (!device) throw new Error(`Simulator not found: ${udid}`); + if (device.state !== "Booted") { + log(`booting ${device.name}`); + await run("xcrun", ["simctl", "boot", udid], { allowFailure: true, logOutput: true }); + } + await run("xcrun", ["simctl", "bootstatus", udid, "-b"], { + timeoutMs: 120_000, + logOutput: true, + }); +} + +/** + * Watches the package tree and schedules a reload whenever source content changes. + * + * A short debounce collapses the multiple filesystem events commonly emitted + * by a single editor save into one reload. + * + * @param {string} packageRoot Absolute package root path. + * @param {() => Promise | void} onChange Reload callback. + * @returns {void} + */ +function watchPackageTree(packageRoot, onChange) { + let reloadTimer = null; + watch(packageRoot, { recursive: true }, (_eventType, fileName) => { + if (fileName && shouldSkipWatchedPackagePath(fileName)) return; + + clearTimeout(reloadTimer); + reloadTimer = setTimeout(() => void onChange(), 250); + }); +} + +/** + * Excludes generated and VCS paths that should not trigger hot reload. + * + * @param {string} fileName Package-relative path reported by `fs.watch`. + * @returns {boolean} Whether the path belongs to an excluded directory. + */ +function shouldSkipWatchedPackagePath(fileName) { + return fileName.split(/[\\/]/).some((component) => + component === ".build" || component === ".git" || component === ".swiftpm" + ); +} + +/** + * Runs a short synchronous command and decodes its JSON output. + * + * This is used only during setup where later work depends immediately on the + * result, such as package metadata and simulator discovery. + * + * @param {string} command Executable name. + * @param {string[]} args Command arguments. + * @returns {object} Parsed JSON output. + */ +function runJson(command, args) { + try { + const stdout = execFileSync(command, args, { encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + return JSON.parse(stdout); + } catch (error) { + throw new Error(`${command} ${args.join(" ")} failed: ${error.stderr || error.stdout || error.message}`); + } +} + +/** + * Runs a command asynchronously, echoes relevant output, and enforces success. + * + * @param {string} command Executable name. + * @param {string[]} args Command arguments. + * @param {object} [options] Timeout, logging, output-file, and failure-handling options. + * @returns {Promise<{code: number | null, stdout: string, stderr: string}>} Process result. + */ +async function run(command, args, options = {}) { + if (options.outputFile) { + return runWithOutputFile(command, args, options); + } + + try { + const result = await execFileAsync(command, args, { + cwd: options.cwd, + encoding: "utf8", + timeout: options.timeoutMs ?? 30_000, + maxBuffer: options.maxBuffer ?? 20 * 1024 * 1024, + }); + if (options.logOutput && result.stdout.trim()) log(result.stdout.trim()); + if (options.logOutput && result.stderr.trim()) log(result.stderr.trim()); + return { code: 0, ...result }; + } catch (error) { + const stdout = error.stdout ?? ""; + const stderr = error.stderr ?? ""; + if (options.logOutput && stdout.trim()) log(stdout.trim()); + if (options.logOutput && stderr.trim()) log(stderr.trim()); + if (options.allowFailure) { + return { code: error.code ?? null, stdout, stderr }; + } + throw new Error(`${command} ${args.join(" ")} failed: ${stderr || stdout || error.message}`); + } +} + +/** + * Runs a command with stdout and stderr connected directly to a diagnostic file. + * + * @param {string} command Executable name. + * @param {string[]} args Command arguments. + * @param {object} options Timeout, output-file, and failure-handling options. + * @returns {Promise<{code: number | null, stdout: string, stderr: string}>} Process result. + */ +async function runWithOutputFile(command, args, options) { + const outputFile = options.outputFile; + await mkdir(dirname(outputFile), { recursive: true }); + const outputHandle = await open(outputFile, "w"); + let result; + + try { + result = await new Promise((resolveResult, rejectResult) => { + const child = spawn(command, args, { + cwd: options.cwd, + stdio: ["ignore", outputHandle.fd, outputHandle.fd], + timeout: options.timeoutMs ?? 30_000, + }); + child.once("error", rejectResult); + child.once("close", (code, signal) => resolveResult({ code, signal })); + }); + } catch (error) { + if (options.allowFailure) { + return { code: error.code ?? null, stdout: "", stderr: "" }; + } + throw new Error(`${command} ${args.join(" ")} failed; full output: ${outputFile}: ${error.message}`); + } finally { + await outputHandle.close(); + } + + if (result.code === 0) { + return { code: 0, stdout: "", stderr: "" }; + } + if (options.allowFailure) { + return { code: result.code, stdout: "", stderr: "" }; + } + + const reason = result.signal ? `terminated by ${result.signal}` : `exited with code ${result.code}`; + throw new Error(`${command} ${args.join(" ")} failed (${reason}); full output: ${outputFile}`); +} + +/** + * Emits a timestamped launcher progress line. + * + * @param {string} message Message to show in the terminal. + * @returns {void} + */ +function log(message) { + const line = `[${new Date().toLocaleTimeString()}] ${message}`; + console.log(line); +} + +/** + * Extracts the PID printed by `simctl launch` for the generated host bundle. + * + * @param {{stdout: string, stderr: string}} result Launch command output. + * @returns {number | null} Running host PID, when reported by Simulator. + */ +function launchPid(result) { + const output = `${result.stdout}\n${result.stderr}`; + const escapedBundleId = BUNDLE_ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = output.match(new RegExp(`${escapedBundleId}:\\s*(\\d+)`)); + return match ? Number(match[1]) : null; +} + +/** + * Resolves the generated host's simulator data container for reload handoff. + * + * Reload manifests and host status files are exchanged through this container's + * documents directory because both Node and the running app can access it. + * + * @param {string} udid Simulator identifier. + * @returns {Promise} Absolute path to the app data container. + */ +async function appDataContainer(udid) { + const result = await run( + "xcrun", + ["simctl", "get_app_container", udid, BUNDLE_ID, "data"], + { timeoutMs: 15_000 }, + ); + return result.stdout.trim(); +} + +/** + * Reads the latest status emitted by the running Swift host app. + * + * @param {object} state Mutable launcher state with a data-container path. + * @returns {object | null} Host status, or `null` before it has been written. + */ +function readHostStatus(state) { + if (!state.dataContainer) return null; + const statusPath = join(state.dataContainer, "Documents", "swiftui-preview-browser", "status.json"); + try { + return JSON.parse(readFileSync(statusPath, "utf8")); + } catch { + return null; + } +} + +/** + * Waits for the initially launched app to publish its running state. + * + * @param {object} state Mutable launcher state including the expected PID. + * @returns {Promise} Initial host status. + */ +async function waitForHostReady(state) { + return waitForHostStatus(state, { + attempts: 50, + isReady: (status) => status.pid === state.appPid && status.phase === "running", + timeoutMessage: + "Preview host launched but did not render. The selected preview may not be self-contained; inspect Simulator logs.", + }); +} + +/** + * Waits until the host reports that a specific reload manifest was applied. + * + * @param {object} state Mutable launcher state. + * @param {string} token Unique token written into the reload manifest. + * @returns {Promise} Updated host status. + */ +async function waitForHostReload(state, token) { + return waitForHostStatus(state, { + attempts: 80, + isReady: (status) => status.lastToken === token && status.phase === "reloaded", + statusError: (status) => + status.lastToken === token && status.phase === "error" + ? new Error(`Hot reload failed inside host: ${status.lastError ?? "unknown error"}`) + : null, + timeoutMessage: "Timed out waiting for the running host app to apply the hot reload", + }); +} + +/** + * Polls host status until a caller-specific terminal state is reached. + * + * @param {object} state Mutable launcher state. + * @param {{attempts: number, isReady: (status: object) => boolean, statusError?: (status: object) => Error | null, timeoutMessage: string}} options Polling conditions. + * @returns {Promise} Matching host status. + */ +async function waitForHostStatus(state, options) { + for (let attempt = 0; attempt < options.attempts; attempt += 1) { + await delay(100); + const status = readHostStatus(state); + if (!status) continue; + const statusError = options.statusError?.(status); + if (statusError) throw statusError; + if (options.isReady(status)) return status; + } + throw new Error(options.timeoutMessage); +} + +/** + * Pauses polling while waiting for simulator state changes. + * + * @param {number} ms Milliseconds to wait. + * @returns {Promise} + */ +function delay(ms) { + return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + main(process.argv.slice(2)).catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md b/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md new file mode 100644 index 0000000..9a0fb20 --- /dev/null +++ b/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md @@ -0,0 +1,90 @@ +--- +name: swiftui-liquid-glass +description: Implement and review iOS 26+ SwiftUI Liquid Glass UI. Use when adopting Liquid Glass or checking its correctness, performance, and design fit. +--- + +# SwiftUI Liquid Glass + +## Overview +Use this skill to build or review SwiftUI features that fully align with the iOS 26+ Liquid Glass API. Prioritize native APIs (`glassEffect`, `GlassEffectContainer`, glass button styles) and Apple design guidance. Keep usage consistent, interactive where needed, and performance aware. + +## Workflow Decision Tree +Choose the path that matches the request: + +### 1) Review an existing feature +- Inspect where Liquid Glass should be used and where it should not. +- Verify correct modifier order, shape usage, and container placement. +- Check for iOS 26+ availability handling and sensible fallbacks. + +### 2) Improve a feature using Liquid Glass +- Identify target components for glass treatment (surfaces, chips, buttons, cards). +- Refactor to use `GlassEffectContainer` where multiple glass elements appear. +- Introduce interactive glass only for tappable or focusable elements. + +### 3) Implement a new feature using Liquid Glass +- Design the glass surfaces and interactions first (shape, prominence, grouping). +- Add glass modifiers after layout/appearance modifiers. +- Add morphing transitions only when the view hierarchy changes with animation. + +## Core Guidelines +- Prefer native Liquid Glass APIs over custom blurs. +- Use `GlassEffectContainer` when multiple glass elements coexist. +- Apply `.glassEffect(...)` after layout and visual modifiers. +- Use `.interactive()` for elements that respond to touch/pointer. +- Keep shapes consistent across related elements for a cohesive look. +- Gate with `#available(iOS 26, *)` and provide a non-glass fallback. + +## Review Checklist +- **Availability**: `#available(iOS 26, *)` present with fallback UI. +- **Composition**: Multiple glass views wrapped in `GlassEffectContainer`. +- **Modifier order**: `glassEffect` applied after layout/appearance modifiers. +- **Interactivity**: `interactive()` only where user interaction exists. +- **Transitions**: `glassEffectID` used with `@Namespace` for morphing. +- **Consistency**: Shapes, tinting, and spacing align across the feature. + +## Implementation Checklist +- Define target elements and desired glass prominence. +- Wrap grouped glass elements in `GlassEffectContainer` and tune spacing. +- Use `.glassEffect(.regular.tint(...).interactive(), in: .rect(cornerRadius: ...))` as needed. +- Use `.buttonStyle(.glass)` / `.buttonStyle(.glassProminent)` for actions. +- Add morphing transitions with `glassEffectID` when hierarchy changes. +- Provide fallback materials and visuals for earlier iOS versions. + +## Quick Snippets +Use these patterns directly and tailor shapes/tints/spacing. + +```swift +if #available(iOS 26, *) { + Text("Hello") + .padding() + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 16)) +} else { + Text("Hello") + .padding() + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) +} +``` + +```swift +GlassEffectContainer(spacing: 24) { + HStack(spacing: 24) { + Image(systemName: "scribble.variable") + .frame(width: 72, height: 72) + .font(.system(size: 32)) + .glassEffect() + Image(systemName: "eraser.fill") + .frame(width: 72, height: 72) + .font(.system(size: 32)) + .glassEffect() + } +} +``` + +```swift +Button("Confirm") { } + .buttonStyle(.glassProminent) +``` + +## Resources +- Reference guide: `references/liquid-glass.md` +- Prefer Apple docs for up-to-date API details, and use web search to consult current Apple Developer documentation in addition to the references above. diff --git a/build-ios-apps/skills/swiftui-performance-audit/SKILL.md b/build-ios-apps/skills/swiftui-performance-audit/SKILL.md new file mode 100644 index 0000000..db22c9d --- /dev/null +++ b/build-ios-apps/skills/swiftui-performance-audit/SKILL.md @@ -0,0 +1,107 @@ +--- +name: swiftui-performance-audit +description: Audit SwiftUI runtime performance from code first. Use when diagnosing slow rendering, janky scrolling, expensive updates, or profiling needs. +--- + +# SwiftUI Performance Audit + +## Quick start + +Use this skill to diagnose SwiftUI performance issues from code first, then request profiling evidence when code review alone cannot explain the symptoms. + +## Workflow + +1. Classify the symptom: slow rendering, janky scrolling, high CPU, memory growth, hangs, or excessive view updates. +2. If code is available, start with a code-first review using `references/code-smells.md`. +3. If code is not available, ask for the smallest useful slice: target view, data flow, reproduction steps, and deployment target. +4. If code review is inconclusive or runtime evidence is required, guide the user through profiling with `references/profiling-intake.md`. +5. Summarize likely causes, evidence, remediation, and validation steps using `references/report-template.md`. + +## 1. Intake + +Collect: +- Target view or feature code. +- Symptoms and exact reproduction steps. +- Data flow: `@State`, `@Binding`, environment dependencies, and observable models. +- Whether the issue shows up on device or simulator, and whether it was observed in Debug or Release. + +Ask the user to classify the issue if possible: +- CPU spike or battery drain +- Janky scrolling or dropped frames +- High memory or image pressure +- Hangs or unresponsive interactions +- Excessive or unexpectedly broad view updates + +For the full profiling intake checklist, read `references/profiling-intake.md`. + +## 2. Code-First Review + +Focus on: +- Invalidation storms from broad observation or environment reads. +- Unstable identity in lists and `ForEach`. +- Heavy derived work in `body` or view builders. +- Layout thrash from complex hierarchies, `GeometryReader`, or preference chains. +- Large image decode or resize work on the main thread. +- Animation or transition work applied too broadly. + +Use `references/code-smells.md` for the detailed smell catalog and fix guidance. + +Provide: +- Likely root causes with code references. +- Suggested fixes and refactors. +- If needed, a minimal repro or instrumentation suggestion. + +## 3. Guide the User to Profile + +If code review does not explain the issue, ask for runtime evidence: +- A trace export or screenshots of the SwiftUI timeline and Time Profiler call tree. +- Device/OS/build configuration. +- The exact interaction being profiled. +- Before/after metrics if the user is comparing a change. + +Use `references/profiling-intake.md` for the exact checklist and collection steps. + +## 4. Analyze and Diagnose + +- Map the evidence to the most likely category: invalidation, identity churn, layout thrash, main-thread work, image cost, or animation cost. +- Prioritize problems by impact, not by how easy they are to explain. +- Distinguish code-level suspicion from trace-backed evidence. +- Call out when profiling is still insufficient and what additional evidence would reduce uncertainty. + +## 5. Remediate + +Apply targeted fixes: +- Narrow state scope and reduce broad observation fan-out. +- Stabilize identities for `ForEach` and lists. +- Move heavy work out of `body` into derived state updated from inputs, model-layer precomputation, memoized helpers, or background preprocessing. Use `@State` only for view-owned state, not as an ad hoc cache for arbitrary computation. +- Use `equatable()` only when equality is cheaper than recomputing the subtree and the inputs are truly value-semantic. +- Downsample images before rendering. +- Reduce layout complexity or use fixed sizing where possible. + +Use `references/code-smells.md` for examples, Observation-specific fan-out guidance, and remediation patterns. + +## 6. Verify + +Ask the user to re-run the same capture and compare with baseline metrics. +Summarize the delta (CPU, frame drops, memory peak) if provided. + +## Outputs + +Provide: +- A short metrics table (before/after if available). +- Top issues (ordered by impact). +- Proposed fixes with estimated effort. + +Use `references/report-template.md` when formatting the final audit. + +## References + +- Profiling intake and collection checklist: `references/profiling-intake.md` +- Common code smells and remediation patterns: `references/code-smells.md` +- Audit output template: `references/report-template.md` +- Add Apple documentation and WWDC resources under `references/` as they are supplied by the user. +- Optimizing SwiftUI performance with Instruments: `references/optimizing-swiftui-performance-instruments.md` +- Understanding and improving SwiftUI performance: `references/understanding-improving-swiftui-performance.md` +- Understanding hangs in your app: `references/understanding-hangs-in-your-app.md` +- Demystify SwiftUI performance (WWDC23): `references/demystify-swiftui-performance-wwdc23.md` +- In addition to the references above, use web search to consult current Apple Developer documentation when Instruments workflows or SwiftUI performance guidance may have changed. diff --git a/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md b/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md new file mode 100644 index 0000000..8d5a7bb --- /dev/null +++ b/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md @@ -0,0 +1,150 @@ +# Common code smells and remediation patterns + +## Intent + +Use this reference during code-first review to map visible SwiftUI patterns to likely runtime costs and safer remediation guidance. + +## High-priority smells + +### Expensive formatters in `body` + +```swift +var body: some View { + let number = NumberFormatter() + let measure = MeasurementFormatter() + Text(measure.string(from: .init(value: meters, unit: .meters))) +} +``` + +Prefer cached formatters in a model or dedicated helper: + +```swift +final class DistanceFormatter { + static let shared = DistanceFormatter() + let number = NumberFormatter() + let measure = MeasurementFormatter() +} +``` + +### Heavy computed properties + +```swift +var filtered: [Item] { + items.filter { $0.isEnabled } +} +``` + +Prefer deriving this once per meaningful input change in a model/helper, or store derived view-owned state only when the view truly owns the transformation lifecycle. + +### Sorting or filtering inside `body` + +```swift +List { + ForEach(items.sorted(by: sortRule)) { item in + Row(item) + } +} +``` + +Prefer sorting before render work begins: + +```swift +let sortedItems = items.sorted(by: sortRule) +``` + +### Inline filtering inside `ForEach` + +```swift +ForEach(items.filter { $0.isEnabled }) { item in + Row(item) +} +``` + +Prefer a prefiltered collection with stable identity. + +### Unstable identity + +```swift +ForEach(items, id: \.self) { item in + Row(item) +} +``` + +Avoid `id: \.self` for non-stable values or collections that reorder. Use a stable domain identifier. + +### Top-level conditional view swapping + +```swift +var content: some View { + if isEditing { + editingView + } else { + readOnlyView + } +} +``` + +Prefer one stable base view and localize conditions to sections or modifiers. This reduces root identity churn and makes diffing cheaper. + +### Image decoding on the main thread + +```swift +Image(uiImage: UIImage(data: data)!) +``` + +Prefer decode and downsample work off the main thread, then store the processed image. + +## Observation fan-out + +### Broad `@Observable` reads on iOS 17+ + +```swift +@Observable final class Model { + var items: [Item] = [] +} + +var body: some View { + Row(isFavorite: model.items.contains(item)) +} +``` + +If many views read the same broad collection or root model, small changes can fan out into wide invalidation. Prefer narrower derived inputs, smaller observable surfaces, or per-item state closer to the leaf views. + +### Broad `ObservableObject` reads on iOS 16 and earlier + +```swift +final class Model: ObservableObject { + @Published var items: [Item] = [] +} +``` + +The same warning applies to legacy observation. Avoid having many descendants observe a large shared object when they only need one derived field. + +## Remediation notes + +### `@State` is not a generic cache + +Use `@State` for view-owned state and derived values that intentionally belong to the view lifecycle. Do not move arbitrary expensive computation into `@State` unless you also define when and why it updates. + +Better alternatives: +- precompute in the model or store +- update derived state in response to a specific input change +- memoize in a dedicated helper +- preprocess on a background task before rendering + +### `equatable()` is conditional guidance + +Use `equatable()` only when: +- equality is cheaper than recomputing the subtree, and +- the view inputs are value-semantic and stable enough for meaningful equality checks + +Do not apply `equatable()` as a blanket fix for all redraws. + +## Triage order + +When multiple smells appear together, prioritize in this order: +1. Broad invalidation and observation fan-out +2. Unstable identity and list churn +3. Main-thread work during render +4. Image decode or resize cost +5. Layout and animation complexity diff --git a/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md b/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md new file mode 100644 index 0000000..39b6530 --- /dev/null +++ b/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md @@ -0,0 +1,44 @@ +# Profiling intake and collection checklist + +## Intent + +Use this checklist when code review alone cannot explain the SwiftUI performance issue and you need runtime evidence from the user. + +## Ask for first + +- Exact symptom: CPU spike, dropped frames, memory growth, hangs, or excessive view updates. +- Exact interaction: scrolling, typing, initial load, navigation push/pop, animation, sheet presentation, or background refresh. +- Target device and OS version. +- Whether the issue was reproduced on a real device or only in Simulator. +- Build configuration: Debug or Release. +- Whether the user already has a baseline or before/after comparison. + +## Default profiling request + +Ask the user to: +- Run the app in a Release build when possible. +- Use the SwiftUI Instruments template. +- Reproduce the exact problematic interaction only long enough to capture the issue. +- Capture the SwiftUI timeline and Time Profiler together. +- Export the trace or provide screenshots of the key SwiftUI lanes and the Time Profiler call tree. + +## Ask for these artifacts + +- Trace export or screenshots of the relevant SwiftUI lanes +- Time Profiler call tree screenshot or export +- Device/OS/build configuration +- A short note describing what action was happening at the time of the capture +- If memory is involved, the memory graph or Allocations data if available + +## When to ask for more + +- Ask for a second capture if the first run mixes multiple interactions. +- Ask for a before/after pair if the user has already tried a fix. +- Ask for a device capture if the issue only appears in Simulator or if scrolling smoothness matters. + +## Common traps + +- Debug builds can distort SwiftUI timing and allocation behavior. +- Simulator traces can miss device-only rendering or memory issues. +- Mixed interactions in one capture make attribution harder. +- Screenshots without the reproduction note are much harder to interpret. diff --git a/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md b/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md new file mode 100644 index 0000000..97982c7 --- /dev/null +++ b/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md @@ -0,0 +1,47 @@ +# Audit output template + +## Intent + +Use this structure when reporting SwiftUI performance findings so the user can quickly see the symptom, evidence, likely cause, and next validation step. + +## Template + +```markdown +## Summary + +[One short paragraph on the most likely bottleneck and whether the conclusion is code-backed or trace-backed.] + +## Findings + +1. [Issue title] + - Symptom: [what the user sees] + - Likely cause: [root cause] + - Evidence: [code reference or profiling evidence] + - Fix: [specific change] + - Validation: [what to measure after the fix] + +2. [Issue title] + - Symptom: ... + - Likely cause: ... + - Evidence: ... + - Fix: ... + - Validation: ... + +## Metrics + +| Metric | Before | After | Notes | +| --- | --- | --- | --- | +| CPU | [value] | [value] | [note] | +| Frame drops / hitching | [value] | [value] | [note] | +| Memory peak | [value] | [value] | [note] | + +## Next step + +[One concrete next action: apply a fix, capture a better trace, or validate on device.] +``` + +## Notes + +- Order findings by impact, not by file order. +- Say explicitly when a conclusion is still a hypothesis. +- If no metrics are available, omit the table and say what should be measured next. diff --git a/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md b/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md new file mode 100644 index 0000000..bb1c733 --- /dev/null +++ b/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md @@ -0,0 +1,96 @@ +--- +name: swiftui-ui-patterns +description: Build and refactor SwiftUI UI with component patterns and examples. Use when shaping navigation, state, layouts, controls, or screen composition. +--- + +# SwiftUI UI Patterns + +## Quick start + +Choose a track based on your goal: + +### Existing project + +- Identify the feature or screen and the primary interaction model (list, detail, editor, settings, tabbed). +- Find a nearby example in the repo with `rg "TabView\("` or similar, then read the closest SwiftUI view. +- Apply local conventions: prefer SwiftUI-native state, keep state local when possible, and use environment injection for shared dependencies. +- Choose the relevant component reference from `references/components-index.md` and follow its guidance. +- If the interaction reveals secondary content by dragging or scrolling the primary content away, read `references/scroll-reveal.md` before implementing gestures manually. +- Build the view with small, focused subviews and SwiftUI-native data flow. + +### New project scaffolding + +- Start with `references/app-wiring.md` to wire TabView + NavigationStack + sheets. +- Add a minimal `AppTab` and `RouterPath` based on the provided skeletons. +- Choose the next component reference based on the UI you need first (TabView, NavigationStack, Sheets). +- Expand the route and sheet enums as new screens are added. + +## General rules to follow + +- Use modern SwiftUI state (`@State`, `@Binding`, `@Observable`, `@Environment`) and avoid unnecessary view models. +- If the deployment target includes iOS 16 or earlier and cannot use the Observation API introduced in iOS 17, fall back to `ObservableObject` with `@StateObject` for root ownership, `@ObservedObject` for injected observation, and `@EnvironmentObject` only for truly shared app-level state. +- Prefer composition; keep views small and focused. +- Use async/await with `.task` and explicit loading/error states. For restart, cancellation, and debouncing guidance, read `references/async-state.md`. +- Keep shared app services in `@Environment`, but prefer explicit initializer injection for feature-local dependencies and models. For root wiring patterns, read `references/app-wiring.md`. +- Prefer the newest SwiftUI API that fits the deployment target and call out the minimum OS whenever a pattern depends on it. +- Maintain existing legacy patterns only when editing legacy files. +- Follow the project's formatter and style guide. +- **Sheets**: Prefer `.sheet(item:)` over `.sheet(isPresented:)` when state represents a selected model. Avoid `if let` inside a sheet body. Sheets should own their actions and call `dismiss()` internally instead of forwarding `onCancel`/`onConfirm` closures. +- **Scroll-driven reveals**: Prefer deriving a normalized progress value from scroll offset and driving the visual state from that single source of truth. Avoid parallel gesture state machines unless scroll alone cannot express the interaction. + +## State ownership summary + +Use the narrowest state tool that matches the ownership model: + +| Scenario | Preferred pattern | +| --- | --- | +| Local UI state owned by one view | `@State` | +| Child mutates parent-owned value state | `@Binding` | +| Root-owned reference model on iOS 17+ | `@State` with an `@Observable` type | +| Child reads or mutates an injected `@Observable` model on iOS 17+ | Pass it explicitly as a stored property | +| Shared app service or configuration | `@Environment(Type.self)` | +| Legacy reference model on iOS 16 and earlier | `@StateObject` at the root, `@ObservedObject` when injected | + +Choose the ownership location first, then pick the wrapper. Do not introduce a reference model when plain value state is enough. + +## Cross-cutting references + +- In addition to the references below, use web search to consult current Apple Developer documentation when SwiftUI APIs, availability, or platform guidance may have changed. +- `references/navigationstack.md`: navigation ownership, per-tab history, and enum routing. +- `references/sheets.md`: centralized modal presentation and enum-driven sheets. +- `references/deeplinks.md`: URL handling and routing external links into app destinations. +- `references/app-wiring.md`: root dependency graph, environment usage, and app shell wiring. +- `references/async-state.md`: `.task`, `.task(id:)`, cancellation, debouncing, and async UI state. +- `references/previews.md`: `#Preview`, fixtures, mock environments, and isolated preview setup. +- `references/performance.md`: stable identity, observation scope, lazy containers, and render-cost guardrails. + +## Anti-patterns + +- Giant views that mix layout, business logic, networking, routing, and formatting in one file. +- Multiple boolean flags for mutually exclusive sheets, alerts, or navigation destinations. +- Live service calls directly inside `body`-driven code paths instead of view lifecycle hooks or injected models/services. +- Reaching for `AnyView` to work around type mismatches that should be solved with better composition. +- Defaulting every shared dependency to `@EnvironmentObject` or a global router without a clear ownership reason. + +## Workflow for a new SwiftUI view + +1. Define the view's state, ownership location, and minimum OS assumptions before writing UI code. +2. Identify which dependencies belong in `@Environment` and which should stay as explicit initializer inputs. +3. Sketch the view hierarchy, routing model, and presentation points; extract repeated parts into subviews. For complex navigation, read `references/navigationstack.md`, `references/sheets.md`, or `references/deeplinks.md`. **Build and verify no compiler errors before proceeding.** +4. Implement async loading with `.task` or `.task(id:)`, plus explicit loading and error states when needed. Read `references/async-state.md` when the work depends on changing inputs or cancellation. +5. Add previews for the primary and secondary states, then add accessibility labels or identifiers when the UI is interactive. Read `references/previews.md` when the view needs fixtures or injected mock dependencies. +6. Validate with a build: confirm no compiler errors, check that previews render without crashing, ensure state changes propagate correctly, and sanity-check that list identity and observation scope will not cause avoidable re-renders. Read `references/performance.md` if the screen is large, scroll-heavy, or frequently updated. For common SwiftUI compilation errors — missing `@State` annotations, ambiguous `ViewBuilder` closures, or mismatched generic types — resolve them before updating callsites. **If the build fails:** read the error message carefully, fix the identified issue, then rebuild before proceeding to the next step. If a preview crashes, isolate the offending subview, confirm its state initialisation is valid, and re-run the preview before continuing. + +## Component references + +Use `references/components-index.md` as the entry point. Each component reference should include: +- Intent and best-fit scenarios. +- Minimal usage pattern with local conventions. +- Pitfalls and performance notes. +- Paths to existing examples in the current repo. + +## Adding a new component reference + +- Create `references/.md`. +- Keep it short and actionable; link to concrete files in the current repo. +- Update `references/components-index.md` with the new entry. diff --git a/build-ios-apps/skills/swiftui-view-refactor/SKILL.md b/build-ios-apps/skills/swiftui-view-refactor/SKILL.md new file mode 100644 index 0000000..5617e31 --- /dev/null +++ b/build-ios-apps/skills/swiftui-view-refactor/SKILL.md @@ -0,0 +1,203 @@ +--- +name: swiftui-view-refactor +description: Refactor SwiftUI view files into stable, testable structure. Use when splitting large views, tightening data flow, or cleaning Observation ownership. +--- + +# SwiftUI View Refactor + +## Overview +Refactor SwiftUI views toward small, explicit, stable view types. Default to vanilla SwiftUI: local state in the view, shared dependencies in the environment, business logic in services/models, and view models only when the request or existing code clearly requires one. + +## Core Guidelines + +### 1) View ordering (top → bottom) +- Enforce this ordering unless the existing file has a stronger local convention you must preserve. +- Environment +- `private`/`public` `let` +- `@State` / other stored properties +- computed `var` (non-view) +- `init` +- `body` +- computed view builders / other view helpers +- helper / async functions + +### 2) Default to MV, not MVVM +- Views should be lightweight state expressions and orchestration points, not containers for business logic. +- Favor `@State`, `@Environment`, `@Query`, `.task`, `.task(id:)`, and `onChange` before reaching for a view model. +- Inject services and shared models via `@Environment`; keep domain logic in services/models, not in the view body. +- Do not introduce a view model just to mirror local view state or wrap environment dependencies. +- If a screen is getting large, split the UI into subviews before inventing a new view model layer. + +### 3) Strongly prefer dedicated subview types over computed `some View` helpers +- Flag `body` properties that are longer than roughly one screen or contain multiple logical sections. +- Prefer extracting dedicated `View` types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview. +- Keep computed `some View` helpers rare and small. Do not build an entire screen out of `private var header: some View`-style fragments. +- Pass small, explicit inputs (data, bindings, callbacks) into extracted subviews instead of handing down the entire parent state. +- If an extracted subview becomes reusable or independently meaningful, move it to its own file. + +Prefer: + +```swift +var body: some View { + List { + HeaderSection(title: title, subtitle: subtitle) + FilterSection( + filterOptions: filterOptions, + selectedFilter: $selectedFilter + ) + ResultsSection(items: filteredItems) + FooterSection() + } +} + +private struct HeaderSection: View { + let title: String + let subtitle: String + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title).font(.title2) + Text(subtitle).font(.subheadline) + } + } +} + +private struct FilterSection: View { + let filterOptions: [FilterOption] + @Binding var selectedFilter: FilterOption + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(filterOptions, id: \.self) { option in + FilterChip(option: option, isSelected: option == selectedFilter) + .onTapGesture { selectedFilter = option } + } + } + } + } +} +``` + +Avoid: + +```swift +var body: some View { + List { + header + filters + results + footer + } +} + +private var header: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title).font(.title2) + Text(subtitle).font(.subheadline) + } +} +``` + +### 3b) Extract actions and side effects out of `body` +- Do not keep non-trivial button actions inline in the view body. +- Do not bury business logic inside `.task`, `.onAppear`, `.onChange`, or `.refreshable`. +- Prefer calling small private methods from the view, and move real business logic into services/models. +- The body should read like UI, not like a view controller. + +```swift +Button("Save", action: save) + .disabled(isSaving) + +.task(id: searchText) { + await reload(for: searchText) +} + +private func save() { + Task { await saveAsync() } +} + +private func reload(for searchText: String) async { + guard !searchText.isEmpty else { + results = [] + return + } + await searchService.search(searchText) +} +``` + +### 4) Keep a stable view tree (avoid top-level conditional view swapping) +- Avoid `body` or computed views that return completely different root branches via `if/else`. +- Prefer a single stable base view with conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, etc.). +- Root-level branch swapping causes identity churn, broader invalidation, and extra recomputation. + +Prefer: + +```swift +var body: some View { + List { + documentsListContent + } + .toolbar { + if canEdit { + editToolbar + } + } +} +``` + +Avoid: + +```swift +var documentsListView: some View { + if canEdit { + editableDocumentsList + } else { + readOnlyDocumentsList + } +} +``` + +### 5) View model handling (only if already present or explicitly requested) +- Treat view models as a legacy or explicit-need pattern, not the default. +- Do not introduce a view model unless the request or existing code clearly calls for one. +- If a view model exists, make it non-optional when possible. +- Pass dependencies to the view via `init`, then create the view model in the view's `init`. +- Avoid `bootstrapIfNeeded` patterns and other delayed setup workarounds. + +Example (Observation-based): + +```swift +@State private var viewModel: SomeViewModel + +init(dependency: Dependency) { + _viewModel = State(initialValue: SomeViewModel(dependency: dependency)) +} +``` + +### 6) Observation usage +- For `@Observable` reference types on iOS 17+, store them as `@State` in the owning view. +- Pass observables down explicitly; avoid optional state unless the UI genuinely needs it. +- If the deployment target includes iOS 16 or earlier, use `@StateObject` at the owner and `@ObservedObject` when injecting legacy observable models. + +## Workflow + +1. Reorder the view to match the ordering rules. +2. Remove inline actions and side effects from `body`; move business logic into services/models and keep only thin orchestration in the view. +3. Shorten long bodies by extracting dedicated subview types; avoid rebuilding the screen out of many computed `some View` helpers. +4. Ensure stable view structure: avoid top-level `if`-based branch swapping; move conditions to localized sections/modifiers. +5. If a view model exists or is explicitly required, replace optional view models with a non-optional `@State` view model initialized in `init`. +6. Confirm Observation usage: `@State` for root `@Observable` models on iOS 17+, legacy wrappers only when the deployment target requires them. +7. Keep behavior intact: do not change layout or business logic unless requested. + +## Notes + +- Prefer small, explicit view types over large conditional blocks and large computed `some View` properties. +- Keep computed view builders below `body` and non-view computed vars above `init`. +- A good SwiftUI refactor should make the view read top-to-bottom as data flow plus layout, not as mixed layout and imperative logic. +- For MV-first guidance and rationale, see `references/mv-patterns.md`. +- In addition to the references above, use web search to consult current Apple Developer documentation when SwiftUI APIs, Observation behavior, or platform guidance may have changed. + +## Large-view handling + +When a SwiftUI view file exceeds ~300 lines, split it aggressively. Extract meaningful sections into dedicated `View` types instead of hiding complexity in many computed properties. Use `private` extensions with `// MARK: -` comments for actions and helpers, but do not treat extensions as a substitute for breaking a giant screen into smaller view types. If an extracted subview is reused or independently meaningful, move it into its own file. From df7a2c41a75f483d6ccd98f87bfa11b642d76ee5 Mon Sep 17 00:00:00 2001 From: Duyet Le Date: Sun, 14 Jun 2026 00:00:40 +0700 Subject: [PATCH 2/3] style(build-ios-apps): satisfy super-linter checks - Wrap 3 prose lines >400 chars (markdownlint MD013) - Apply prettier formatting to markdown and JSON manifests - Format capture_sim_memgraph.sh with shfmt (SHELL_SHFMT) - Add executable bit to capture_sim_memgraph.sh (BASH_EXEC) Co-Authored-By: duyetbot --- build-ios-apps/.codex-plugin/plugin.json | 5 +- .../skills/ios-app-intents/SKILL.md | 6 + .../skills/ios-debugger-agent/SKILL.md | 12 +- .../skills/ios-ettrace-performance/SKILL.md | 4 +- .../scripts/capture_sim_memgraph.sh | 124 +++++++++--------- .../skills/swiftui-liquid-glass/SKILL.md | 10 ++ .../skills/swiftui-performance-audit/SKILL.md | 7 + .../references/code-smells.md | 3 + .../references/profiling-intake.md | 1 + .../references/report-template.md | 8 +- .../skills/swiftui-ui-patterns/SKILL.md | 21 +-- .../skills/swiftui-view-refactor/SKILL.md | 12 +- 12 files changed, 130 insertions(+), 83 deletions(-) mode change 100644 => 100755 build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh diff --git a/build-ios-apps/.codex-plugin/plugin.json b/build-ios-apps/.codex-plugin/plugin.json index 11d4241..c8aeab1 100644 --- a/build-ios-apps/.codex-plugin/plugin.json +++ b/build-ios-apps/.codex-plugin/plugin.json @@ -12,10 +12,7 @@ "shortDescription": "Build, profile, debug, and refine iOS apps with SwiftUI and Xcode workflows", "developerName": "duyet", "category": "development", - "capabilities": [ - "Skill", - "Agent" - ], + "capabilities": ["Skill", "Agent"], "links": { "homepage": "https://github.com/duyet/codex-claude-plugins" } diff --git a/build-ios-apps/skills/ios-app-intents/SKILL.md b/build-ios-apps/skills/ios-app-intents/SKILL.md index 098e520..c75bbb0 100644 --- a/build-ios-apps/skills/ios-app-intents/SKILL.md +++ b/build-ios-apps/skills/ios-app-intents/SKILL.md @@ -6,6 +6,7 @@ description: Design App Intents, app entities, and App Shortcuts for iOS system # iOS App Intents ## Overview + Expose the smallest useful action and entity surface to the system. Start with the verbs and objects people would actually want outside the app, then implement a narrow App Intents layer that can deep-link or hand off cleanly into the main app when needed. Read these references as needed: @@ -18,28 +19,33 @@ Read these references as needed: ## Core workflow ### 1) Start with actions, not screens + - Identify the 1-3 highest-value actions that should work outside the app UI. - Prefer verbs like compose, open, find, filter, continue, inspect, or start. - Do not mirror the entire app navigation tree as intents. ### 2) Define a small entity surface + - Add `AppEntity` types only for the objects the system needs to understand or route. - Keep the entity shape narrower than the app's persistence model. - Add `EntityQuery` or other query types only where disambiguation or suggestions are genuinely useful. ### 3) Decide whether the action completes in place or opens the app + - Use non-opening intents for actions that can complete directly from the system surface. - Use `openAppWhenRun` or open-style intents when the user should land in a specific in-app workflow. - When the app must react inside the main scene, add one clear runtime handoff path instead of scattering ad hoc routing logic. - If the action can work in both modes, consider shipping both an inline version and an open-app version rather than forcing one compromise. ### 4) Make the actions discoverable + - Add `AppShortcutsProvider` entries for the first set of high-value intents. - Choose titles, phrases, and symbols that make sense in Shortcuts, Siri, and Spotlight. - Keep shortcut phrases direct and task-oriented. - Reuse the same action model for widgets and controls when a widget configuration or intent-driven control already needs the same parameters. ### 5) Validate the runtime handoff + - Build the app and confirm the intents target compiles cleanly. - Verify the app opens or routes to the expected place when an intent runs. - Summarize which actions are now exposed, which entities back them, and how the app handles invocation. diff --git a/build-ios-apps/skills/ios-debugger-agent/SKILL.md b/build-ios-apps/skills/ios-debugger-agent/SKILL.md index 092e79a..c7c2da9 100644 --- a/build-ios-apps/skills/ios-debugger-agent/SKILL.md +++ b/build-ios-apps/skills/ios-debugger-agent/SKILL.md @@ -6,16 +6,20 @@ description: Build, run, and debug iOS apps on Simulator with XcodeBuildMCP. Use # iOS Debugger Agent ## Overview + Use XcodeBuildMCP to build and run the current project scheme on a booted iOS simulator, interact with the UI, and capture logs. Prefer the MCP tools for simulator control, logs, and view inspection. ## Core Workflow + Follow this sequence unless the user asks for a narrower action. ### 1) Discover the booted simulator + - Call `mcp__XcodeBuildMCP__list_sims` and select the simulator with state `Booted`. - If none are booted, ask the user to boot one (do not boot automatically unless asked). ### 2) Set session defaults + - Call `mcp__XcodeBuildMCP__session-set-defaults` with: - `projectPath` or `workspacePath` (whichever the repo uses) - `scheme` for the current app @@ -23,15 +27,17 @@ Follow this sequence unless the user asks for a narrower action. - Optional: `configuration: "Debug"`, `useLatestOS: true` ### 3) Build + run (when requested) + - Call `mcp__XcodeBuildMCP__build_run_sim`. - **If the build fails**, check the error output and retry (optionally with `preferXcodebuild: true`) or escalate to the user before attempting any UI interaction. - **After a successful build**, verify the app launched by calling `mcp__XcodeBuildMCP__describe_ui` or `mcp__XcodeBuildMCP__screenshot` before proceeding to UI interaction. - If the app is already built and only launch is requested, use `mcp__XcodeBuildMCP__launch_app_sim`. - If bundle id is unknown: - 1) `mcp__XcodeBuildMCP__get_sim_app_path` - 2) `mcp__XcodeBuildMCP__get_app_bundle_id` + 1. `mcp__XcodeBuildMCP__get_sim_app_path` + 2. `mcp__XcodeBuildMCP__get_app_bundle_id` ## UI Interaction & Debugging + Use these when asked to inspect or interact with the running app. - **Describe UI**: `mcp__XcodeBuildMCP__describe_ui` before tapping or swiping. @@ -41,11 +47,13 @@ Use these when asked to inspect or interact with the running app. - **Screenshot**: `mcp__XcodeBuildMCP__screenshot` for visual confirmation. ## Logs & Console Output + - Start logs: `mcp__XcodeBuildMCP__start_sim_log_cap` with the app bundle id. - Stop logs: `mcp__XcodeBuildMCP__stop_sim_log_cap` and summarize important lines. - For console output, set `captureConsole: true` and relaunch if required. ## Troubleshooting + - If build fails, ask whether to retry with `preferXcodebuild: true`. - If the wrong app launches, confirm the scheme and bundle id. - If UI elements are not hittable, re-run `describe_ui` after layout changes. diff --git a/build-ios-apps/skills/ios-ettrace-performance/SKILL.md b/build-ios-apps/skills/ios-ettrace-performance/SKILL.md index 5a12e39..098260f 100644 --- a/build-ios-apps/skills/ios-ettrace-performance/SKILL.md +++ b/build-ios-apps/skills/ios-ettrace-performance/SKILL.md @@ -112,7 +112,9 @@ DSYMS="$RUN_DIR/dsyms" --extra-dsym "$RUN_DIR/ETTrace-iphonesimulator.xcarchive/dSYMs/ETTrace.framework.dSYM" ``` -Add `--require-framework ` for app-owned dynamic frameworks that must symbolicate; use `--require-all-frameworks` only when every embedded framework is app-owned or expected to have symbols. If the helper reports a missing required app or framework dSYM, rebuild the exact simulator app with dSYM generation before tracing, or add the build output directory that contains those dSYMs as another `--search-root`. +Add `--require-framework ` for app-owned dynamic frameworks that must symbolicate; use `--require-all-frameworks` only when every embedded framework is app-owned or expected to have symbols. If the helper reports a missing required app or framework dSYM, rebuild the exact simulator app with dSYM generation before tracing, or add the build output directory that +contains those dSYMs +as another `--search-root`. Verify important UUIDs before tracing when the report looks suspicious: diff --git a/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh b/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh old mode 100644 new mode 100755 index f0804f0..f2cc20d --- a/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh +++ b/build-ios-apps/skills/ios-memgraph-leaks/scripts/capture_sim_memgraph.sh @@ -2,7 +2,7 @@ set -euo pipefail usage() { - cat <<'USAGE' + cat <<'USAGE' Capture a memory graph from a running iOS simulator app. Required: @@ -18,13 +18,13 @@ USAGE } require_value() { - local flag="$1" - local value="${2:-}" - if [[ -z "$value" ]]; then - echo "$flag requires a value" >&2 - usage >&2 - exit 2 - fi + local flag="$1" + local value="${2:-}" + if [[ -z "$value" ]]; then + echo "$flag requires a value" >&2 + usage >&2 + exit 2 + fi } bundle_id="" @@ -32,53 +32,53 @@ out_dir="" udid="" while [[ $# -gt 0 ]]; do - case "$1" in - --bundle-id) - require_value "$1" "${2:-}" - bundle_id="$2" - shift 2 - ;; - --out-dir) - require_value "$1" "${2:-}" - out_dir="$2" - shift 2 - ;; - --udid) - require_value "$1" "${2:-}" - udid="$2" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown argument: $1" >&2 - usage >&2 - exit 2 - ;; - esac + case "$1" in + --bundle-id) + require_value "$1" "${2:-}" + bundle_id="$2" + shift 2 + ;; + --out-dir) + require_value "$1" "${2:-}" + out_dir="$2" + shift 2 + ;; + --udid) + require_value "$1" "${2:-}" + udid="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac done if [[ -z "$udid" ]]; then - echo "--udid is required" >&2 - usage >&2 - exit 2 + echo "--udid is required" >&2 + usage >&2 + exit 2 fi if [[ -z "$bundle_id" ]]; then - echo "--bundle-id is required" >&2 - usage >&2 - exit 2 + echo "--bundle-id is required" >&2 + usage >&2 + exit 2 fi if [[ -z "$out_dir" ]]; then - out_dir="$(mktemp -d "${TMPDIR:-/tmp}/codex-ios-memgraph.XXXXXX")" + out_dir="$(mktemp -d "${TMPDIR:-/tmp}/codex-ios-memgraph.XXXXXX")" fi matching_processes="$( - xcrun simctl spawn "$udid" launchctl list | - awk -v bundle_id="$bundle_id" ' + xcrun simctl spawn "$udid" launchctl list | + awk -v bundle_id="$bundle_id" ' $1 == "-" { next } @@ -93,14 +93,14 @@ matching_processes="$( )" if [[ -z "$matching_processes" ]]; then - echo "Could not find a running PID for $bundle_id on $udid" >&2 - exit 1 + echo "Could not find a running PID for $bundle_id on $udid" >&2 + exit 1 fi if [[ "$(printf '%s\n' "$matching_processes" | wc -l | tr -d ' ')" -ne 1 ]]; then - echo "Found multiple running PIDs for $bundle_id on $udid:" >&2 - printf '%s\n' "$matching_processes" >&2 - exit 1 + echo "Found multiple running PIDs for $bundle_id on $udid:" >&2 + printf '%s\n' "$matching_processes" >&2 + exit 1 fi pid="$(printf '%s\n' "$matching_processes" | awk '{ print $1 }')" @@ -115,27 +115,27 @@ leaks_output="$out_dir/$safe_bundle-$pid-$timestamp.leaks.txt" metadata="$out_dir/$safe_bundle-$pid-$timestamp.metadata.txt" { - echo "date: $(date)" - echo "udid: $udid" - echo "bundle_id: $bundle_id" - echo "process_label: $process_label" - echo "pid: $pid" - echo "memgraph: $memgraph" - echo "leaks_output: $leaks_output" -} > "$metadata" + echo "date: $(date)" + echo "udid: $udid" + echo "bundle_id: $bundle_id" + echo "process_label: $process_label" + echo "pid: $pid" + echo "memgraph: $memgraph" + echo "leaks_output: $leaks_output" +} >"$metadata" set +e -leaks "--outputGraph=$memgraph" "$pid" > "$leaks_output" 2>&1 +leaks "--outputGraph=$memgraph" "$pid" >"$leaks_output" 2>&1 leaks_status=$? set -e -echo "leaks_exit_status: $leaks_status" >> "$metadata" +echo "leaks_exit_status: $leaks_status" >>"$metadata" if [[ ! -f "$memgraph" ]]; then - echo "memgraph_missing: true" >> "$metadata" - echo "leaks failed to create a memgraph; see: $leaks_output" >&2 - echo "metadata: $metadata" >&2 - exit 1 + echo "memgraph_missing: true" >>"$metadata" + echo "leaks failed to create a memgraph; see: $leaks_output" >&2 + echo "metadata: $metadata" >&2 + exit 1 fi echo "memgraph: $memgraph" diff --git a/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md b/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md index 9a0fb20..4a9474e 100644 --- a/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md +++ b/build-ios-apps/skills/swiftui-liquid-glass/SKILL.md @@ -6,27 +6,33 @@ description: Implement and review iOS 26+ SwiftUI Liquid Glass UI. Use when adop # SwiftUI Liquid Glass ## Overview + Use this skill to build or review SwiftUI features that fully align with the iOS 26+ Liquid Glass API. Prioritize native APIs (`glassEffect`, `GlassEffectContainer`, glass button styles) and Apple design guidance. Keep usage consistent, interactive where needed, and performance aware. ## Workflow Decision Tree + Choose the path that matches the request: ### 1) Review an existing feature + - Inspect where Liquid Glass should be used and where it should not. - Verify correct modifier order, shape usage, and container placement. - Check for iOS 26+ availability handling and sensible fallbacks. ### 2) Improve a feature using Liquid Glass + - Identify target components for glass treatment (surfaces, chips, buttons, cards). - Refactor to use `GlassEffectContainer` where multiple glass elements appear. - Introduce interactive glass only for tappable or focusable elements. ### 3) Implement a new feature using Liquid Glass + - Design the glass surfaces and interactions first (shape, prominence, grouping). - Add glass modifiers after layout/appearance modifiers. - Add morphing transitions only when the view hierarchy changes with animation. ## Core Guidelines + - Prefer native Liquid Glass APIs over custom blurs. - Use `GlassEffectContainer` when multiple glass elements coexist. - Apply `.glassEffect(...)` after layout and visual modifiers. @@ -35,6 +41,7 @@ Choose the path that matches the request: - Gate with `#available(iOS 26, *)` and provide a non-glass fallback. ## Review Checklist + - **Availability**: `#available(iOS 26, *)` present with fallback UI. - **Composition**: Multiple glass views wrapped in `GlassEffectContainer`. - **Modifier order**: `glassEffect` applied after layout/appearance modifiers. @@ -43,6 +50,7 @@ Choose the path that matches the request: - **Consistency**: Shapes, tinting, and spacing align across the feature. ## Implementation Checklist + - Define target elements and desired glass prominence. - Wrap grouped glass elements in `GlassEffectContainer` and tune spacing. - Use `.glassEffect(.regular.tint(...).interactive(), in: .rect(cornerRadius: ...))` as needed. @@ -51,6 +59,7 @@ Choose the path that matches the request: - Provide fallback materials and visuals for earlier iOS versions. ## Quick Snippets + Use these patterns directly and tailor shapes/tints/spacing. ```swift @@ -86,5 +95,6 @@ Button("Confirm") { } ``` ## Resources + - Reference guide: `references/liquid-glass.md` - Prefer Apple docs for up-to-date API details, and use web search to consult current Apple Developer documentation in addition to the references above. diff --git a/build-ios-apps/skills/swiftui-performance-audit/SKILL.md b/build-ios-apps/skills/swiftui-performance-audit/SKILL.md index db22c9d..f5acd0d 100644 --- a/build-ios-apps/skills/swiftui-performance-audit/SKILL.md +++ b/build-ios-apps/skills/swiftui-performance-audit/SKILL.md @@ -20,12 +20,14 @@ Use this skill to diagnose SwiftUI performance issues from code first, then requ ## 1. Intake Collect: + - Target view or feature code. - Symptoms and exact reproduction steps. - Data flow: `@State`, `@Binding`, environment dependencies, and observable models. - Whether the issue shows up on device or simulator, and whether it was observed in Debug or Release. Ask the user to classify the issue if possible: + - CPU spike or battery drain - Janky scrolling or dropped frames - High memory or image pressure @@ -37,6 +39,7 @@ For the full profiling intake checklist, read `references/profiling-intake.md`. ## 2. Code-First Review Focus on: + - Invalidation storms from broad observation or environment reads. - Unstable identity in lists and `ForEach`. - Heavy derived work in `body` or view builders. @@ -47,6 +50,7 @@ Focus on: Use `references/code-smells.md` for the detailed smell catalog and fix guidance. Provide: + - Likely root causes with code references. - Suggested fixes and refactors. - If needed, a minimal repro or instrumentation suggestion. @@ -54,6 +58,7 @@ Provide: ## 3. Guide the User to Profile If code review does not explain the issue, ask for runtime evidence: + - A trace export or screenshots of the SwiftUI timeline and Time Profiler call tree. - Device/OS/build configuration. - The exact interaction being profiled. @@ -71,6 +76,7 @@ Use `references/profiling-intake.md` for the exact checklist and collection step ## 5. Remediate Apply targeted fixes: + - Narrow state scope and reduce broad observation fan-out. - Stabilize identities for `ForEach` and lists. - Move heavy work out of `body` into derived state updated from inputs, model-layer precomputation, memoized helpers, or background preprocessing. Use `@State` only for view-owned state, not as an ad hoc cache for arbitrary computation. @@ -88,6 +94,7 @@ Summarize the delta (CPU, frame drops, memory peak) if provided. ## Outputs Provide: + - A short metrics table (before/after if available). - Top issues (ordered by impact). - Proposed fixes with estimated effort. diff --git a/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md b/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md index 8d5a7bb..88f1af0 100644 --- a/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md +++ b/build-ios-apps/skills/swiftui-performance-audit/references/code-smells.md @@ -127,6 +127,7 @@ The same warning applies to legacy observation. Avoid having many descendants ob Use `@State` for view-owned state and derived values that intentionally belong to the view lifecycle. Do not move arbitrary expensive computation into `@State` unless you also define when and why it updates. Better alternatives: + - precompute in the model or store - update derived state in response to a specific input change - memoize in a dedicated helper @@ -135,6 +136,7 @@ Better alternatives: ### `equatable()` is conditional guidance Use `equatable()` only when: + - equality is cheaper than recomputing the subtree, and - the view inputs are value-semantic and stable enough for meaningful equality checks @@ -143,6 +145,7 @@ Do not apply `equatable()` as a blanket fix for all redraws. ## Triage order When multiple smells appear together, prioritize in this order: + 1. Broad invalidation and observation fan-out 2. Unstable identity and list churn 3. Main-thread work during render diff --git a/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md b/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md index 39b6530..ad2a530 100644 --- a/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md +++ b/build-ios-apps/skills/swiftui-performance-audit/references/profiling-intake.md @@ -16,6 +16,7 @@ Use this checklist when code review alone cannot explain the SwiftUI performance ## Default profiling request Ask the user to: + - Run the app in a Release build when possible. - Use the SwiftUI Instruments template. - Reproduce the exact problematic interaction only long enough to capture the issue. diff --git a/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md b/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md index 97982c7..5652de2 100644 --- a/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md +++ b/build-ios-apps/skills/swiftui-performance-audit/references/report-template.md @@ -29,11 +29,11 @@ Use this structure when reporting SwiftUI performance findings so the user can q ## Metrics -| Metric | Before | After | Notes | -| --- | --- | --- | --- | -| CPU | [value] | [value] | [note] | +| Metric | Before | After | Notes | +| ---------------------- | ------- | ------- | ------ | +| CPU | [value] | [value] | [note] | | Frame drops / hitching | [value] | [value] | [note] | -| Memory peak | [value] | [value] | [note] | +| Memory peak | [value] | [value] | [note] | ## Next step diff --git a/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md b/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md index bb1c733..b584aa8 100644 --- a/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md +++ b/build-ios-apps/skills/swiftui-ui-patterns/SKILL.md @@ -42,14 +42,14 @@ Choose a track based on your goal: Use the narrowest state tool that matches the ownership model: -| Scenario | Preferred pattern | -| --- | --- | -| Local UI state owned by one view | `@State` | -| Child mutates parent-owned value state | `@Binding` | -| Root-owned reference model on iOS 17+ | `@State` with an `@Observable` type | -| Child reads or mutates an injected `@Observable` model on iOS 17+ | Pass it explicitly as a stored property | -| Shared app service or configuration | `@Environment(Type.self)` | -| Legacy reference model on iOS 16 and earlier | `@StateObject` at the root, `@ObservedObject` when injected | +| Scenario | Preferred pattern | +| ----------------------------------------------------------------- | ----------------------------------------------------------- | +| Local UI state owned by one view | `@State` | +| Child mutates parent-owned value state | `@Binding` | +| Root-owned reference model on iOS 17+ | `@State` with an `@Observable` type | +| Child reads or mutates an injected `@Observable` model on iOS 17+ | Pass it explicitly as a stored property | +| Shared app service or configuration | `@Environment(Type.self)` | +| Legacy reference model on iOS 16 and earlier | `@StateObject` at the root, `@ObservedObject` when injected | Choose the ownership location first, then pick the wrapper. Do not introduce a reference model when plain value state is enough. @@ -79,11 +79,14 @@ Choose the ownership location first, then pick the wrapper. Do not introduce a r 3. Sketch the view hierarchy, routing model, and presentation points; extract repeated parts into subviews. For complex navigation, read `references/navigationstack.md`, `references/sheets.md`, or `references/deeplinks.md`. **Build and verify no compiler errors before proceeding.** 4. Implement async loading with `.task` or `.task(id:)`, plus explicit loading and error states when needed. Read `references/async-state.md` when the work depends on changing inputs or cancellation. 5. Add previews for the primary and secondary states, then add accessibility labels or identifiers when the UI is interactive. Read `references/previews.md` when the view needs fixtures or injected mock dependencies. -6. Validate with a build: confirm no compiler errors, check that previews render without crashing, ensure state changes propagate correctly, and sanity-check that list identity and observation scope will not cause avoidable re-renders. Read `references/performance.md` if the screen is large, scroll-heavy, or frequently updated. For common SwiftUI compilation errors — missing `@State` annotations, ambiguous `ViewBuilder` closures, or mismatched generic types — resolve them before updating callsites. **If the build fails:** read the error message carefully, fix the identified issue, then rebuild before proceeding to the next step. If a preview crashes, isolate the offending subview, confirm its state initialisation is valid, and re-run the preview before continuing. +6. Validate with a build: confirm no compiler errors, check that previews render without crashing, ensure state changes propagate correctly, and sanity-check that list identity and observation scope will not cause avoidable re-renders. Read `references/performance.md` if the screen is large, scroll-heavy, or frequently updated. For common SwiftUI compilation errors — missing + `@State` annotations, + ambiguous `ViewBuilder` closures, or mismatched generic types — resolve them before updating callsites. **If the build fails:** read the error message carefully, fix the identified issue, then rebuild before proceeding to the next step. If a preview crashes, isolate the offending subview, confirm its state initialisation is valid, and re-run the preview before continuing. ## Component references Use `references/components-index.md` as the entry point. Each component reference should include: + - Intent and best-fit scenarios. - Minimal usage pattern with local conventions. - Pitfalls and performance notes. diff --git a/build-ios-apps/skills/swiftui-view-refactor/SKILL.md b/build-ios-apps/skills/swiftui-view-refactor/SKILL.md index 5617e31..d1745f6 100644 --- a/build-ios-apps/skills/swiftui-view-refactor/SKILL.md +++ b/build-ios-apps/skills/swiftui-view-refactor/SKILL.md @@ -6,11 +6,13 @@ description: Refactor SwiftUI view files into stable, testable structure. Use wh # SwiftUI View Refactor ## Overview + Refactor SwiftUI views toward small, explicit, stable view types. Default to vanilla SwiftUI: local state in the view, shared dependencies in the environment, business logic in services/models, and view models only when the request or existing code clearly requires one. ## Core Guidelines ### 1) View ordering (top → bottom) + - Enforce this ordering unless the existing file has a stronger local convention you must preserve. - Environment - `private`/`public` `let` @@ -22,6 +24,7 @@ Refactor SwiftUI views toward small, explicit, stable view types. Default to van - helper / async functions ### 2) Default to MV, not MVVM + - Views should be lightweight state expressions and orchestration points, not containers for business logic. - Favor `@State`, `@Environment`, `@Query`, `.task`, `.task(id:)`, and `onChange` before reaching for a view model. - Inject services and shared models via `@Environment`; keep domain logic in services/models, not in the view body. @@ -29,6 +32,7 @@ Refactor SwiftUI views toward small, explicit, stable view types. Default to van - If a screen is getting large, split the UI into subviews before inventing a new view model layer. ### 3) Strongly prefer dedicated subview types over computed `some View` helpers + - Flag `body` properties that are longer than roughly one screen or contain multiple logical sections. - Prefer extracting dedicated `View` types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview. - Keep computed `some View` helpers rare and small. Do not build an entire screen out of `private var header: some View`-style fragments. @@ -100,6 +104,7 @@ private var header: some View { ``` ### 3b) Extract actions and side effects out of `body` + - Do not keep non-trivial button actions inline in the view body. - Do not bury business logic inside `.task`, `.onAppear`, `.onChange`, or `.refreshable`. - Prefer calling small private methods from the view, and move real business logic into services/models. @@ -127,6 +132,7 @@ private func reload(for searchText: String) async { ``` ### 4) Keep a stable view tree (avoid top-level conditional view swapping) + - Avoid `body` or computed views that return completely different root branches via `if/else`. - Prefer a single stable base view with conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, etc.). - Root-level branch swapping causes identity churn, broader invalidation, and extra recomputation. @@ -159,6 +165,7 @@ var documentsListView: some View { ``` ### 5) View model handling (only if already present or explicitly requested) + - Treat view models as a legacy or explicit-need pattern, not the default. - Do not introduce a view model unless the request or existing code clearly calls for one. - If a view model exists, make it non-optional when possible. @@ -176,6 +183,7 @@ init(dependency: Dependency) { ``` ### 6) Observation usage + - For `@Observable` reference types on iOS 17+, store them as `@State` in the owning view. - Pass observables down explicitly; avoid optional state unless the UI genuinely needs it. - If the deployment target includes iOS 16 or earlier, use `@StateObject` at the owner and `@ObservedObject` when injecting legacy observable models. @@ -200,4 +208,6 @@ init(dependency: Dependency) { ## Large-view handling -When a SwiftUI view file exceeds ~300 lines, split it aggressively. Extract meaningful sections into dedicated `View` types instead of hiding complexity in many computed properties. Use `private` extensions with `// MARK: -` comments for actions and helpers, but do not treat extensions as a substitute for breaking a giant screen into smaller view types. If an extracted subview is reused or independently meaningful, move it into its own file. +When a SwiftUI view file exceeds ~300 lines, split it aggressively. Extract meaningful sections into dedicated `View` types instead of hiding complexity in many computed properties. Use `private` extensions with `// MARK: -` comments for actions and helpers, but do not treat extensions as a substitute for breaking a giant screen into smaller view types. If an extracted subview +is reused or +independently meaningful, move it into its own file. From 8f77a6067378ba209bb2c3243f2913ad8e96857c Mon Sep 17 00:00:00 2001 From: duyetbot Date: Sun, 14 Jun 2026 00:28:34 +0700 Subject: [PATCH 3/3] style(build-ios-apps): tag directory-tree fence as text (MD040) Co-Authored-By: duyetbot --- build-ios-apps/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-ios-apps/README.md b/build-ios-apps/README.md index 5f20679..9fc0e1b 100644 --- a/build-ios-apps/README.md +++ b/build-ios-apps/README.md @@ -28,7 +28,7 @@ It currently includes these skills: ## Plugin Structure -``` +```text build-ios-apps/ ├── .claude-plugin/ # Claude manifest ├── .codex-plugin/ # Codex manifest