Compare commits

..

10 Commits

Author SHA1 Message Date
Matt Bruce
6b7455f6ff added images
Signed-off-by: Matt Bruce <matt.bruce1@toyota.com>
2026-02-22 14:17:04 -06:00
Matt Bruce
d74e2ca3c1 Signed-off-by: Matt Bruce <matt.bruce1@toyota.com> 2026-02-12 16:58:59 -06:00
Matt Bruce
370647c882 more edits
Signed-off-by: Matt Bruce <matt.bruce1@toyota.com>
2026-02-12 16:50:00 -06:00
Matt Bruce
3d1e2835b0 Signed-off-by: Matt Bruce <matt.bruce1@toyota.com> 2026-02-12 16:45:14 -06:00
Matt Bruce
35d8bb7b6b Merge branch 'develop' into refactor 2026-02-12 16:34:25 -06:00
Matt Bruce
8533890d2c refactored filesystem
Signed-off-by: Matt Bruce <matt.bruce1@toyota.com>
2026-02-12 16:33:53 -06:00
Shawn Casey
000babfe80 updated instructions and moved bigger instructions that I want to be active all the time to a new Toyota iOS Dev agent 2026-02-12 15:02:52 -06:00
Matt Bruce
f3dd8d92bd Signed-off-by: Matt Bruce <matt.bruce1@toyota.com> 2026-02-11 13:22:51 -06:00
Matt Bruce
88e4402d38 fix: use tool-agnostic ~/.agents/ as default install path
Copilot, Claude, Cursor, and others all read from ~/.agents/.
The npx skills CLI handles fan-out to tool-specific directories.
2026-02-11 12:13:20 -06:00
Matt Bruce
20446c5224 feat: auto-discover custom skills from assets/skills/
- setup.sh skills command now does two things:
  1. Installs registry skills from <platform>-skills.txt
  2. Auto-discovers and copies custom skill folders from assets/skills/
- Added SKILLS_DIR config (default: ~/.copilot/skills/)
- Added download_dir_to helper for copying skill folders remotely
- Created assets/skills/ for team-maintained custom skills
2026-02-11 12:11:01 -06:00
35 changed files with 3329 additions and 1158 deletions

View File

@ -41,7 +41,7 @@ That's it.
| Command | What It Does |
|---------|-------------|
| `setup.sh skills [platform]` | Install skills from a curated list |
| `setup.sh skills [platform]` | Install registry skills + custom skills (auto-discovered) |
| `setup.sh agents` | Install all agent prompt files (auto-discovered) |
| `setup.sh instructions` | Install all instruction rule files (auto-discovered) |
| `setup.sh all [platform]` | All of the above in one shot |
@ -59,19 +59,23 @@ That's it.
| Asset | Default Location | Override |
|-------|-----------------|----------|
| Skills | Managed by `npx skills` CLI | — |
| Agents | `~/.copilot/agents/` | `AGENTS_DIR` |
| Registry skills | Managed by `npx skills` CLI | — |
| Custom skills | `~/.agents/skills/` | `SKILLS_DIR` |
| Agents | `~/.agents/agents/` | `AGENTS_DIR` |
| Instructions | `./instructions/` | `INSTRUCTIONS_DIR` |
> The `~/.agents/` directory is tool-agnostic. Copilot, Claude, Cursor, and others all read from it. If you need a tool-specific path, override with the env var.
## Adding New Assets
- **Agents or instructions** — Drop the file into `assets/agents/` or `assets/instructions/` and push. The script discovers files from the directory automatically. No manifest to update.
- **Skills** — Add the install command to the appropriate `.txt` file (e.g., `ios-skills.txt`). One command per line.
- **Agents or instructions** — Drop the file into `assets/agents/` or `assets/instructions/` and push. Auto-discovered.
- **Custom skills** — Add a folder with a `SKILL.md` to `assets/skills/` and push. Auto-discovered.
- **Registry skills** — Add the install entry to the appropriate `.txt` file (e.g., `ios-skills.txt`). One per line.
## How It Works
| Mode | Agents / Instructions | Skills |
|------|----------------------|--------|
| Mode | Agents / Instructions / Custom Skills | Registry Skills |
|------|---------------------------------------|----------------|
| **Local** (cloned repo) | `find` scans the directory | Reads the `.txt` file |
| **Remote** (no clone) | Queries GitLab/GitHub API to list files | Downloads the `.txt` file |
@ -83,6 +87,9 @@ assets/
ios-skills.txt ← curated iOS skills (one per line)
android-skills.txt ← curated Android skills
shared-skills.txt ← curated cross-platform skills
skills/ ← custom skill folders (auto-discovered)
my-skill/
SKILL.md
agents/ ← agent prompt files (auto-discovered)
instructions/ ← instruction rule files (auto-discovered)
```
@ -92,6 +99,5 @@ assets/
| Variable | Purpose | Required? |
|----------|---------|-----------|
| `ASSETS_BASE_URL` | Base URL for remote downloads | Only without a clone |
| `AGENTS_DIR` | Custom agents install path | No |
| `INSTRUCTIONS_DIR` | Custom instructions install path | No |
| `AGENTS_DIR` | Custom agents install path | No || `SKILLS_DIR` | Custom skills install path | No || `INSTRUCTIONS_DIR` | Custom instructions install path | No |
| `REPO_TOKEN` | Auth token for private repos | Only if API rejects |

View File

@ -0,0 +1,584 @@
---
name: Toyota iOS Developer
description: 'Toyota OneApp iOS development agent. Enforces Clean Architecture, Swift Package Manager modules, async/await patterns, GraphQL networking, and project-wide standards for planning, code review, and implementation.'
---
# Toyota iOS Developer Agent
You are an expert iOS engineer on the Toyota OneApp project. You enforce project-wide architectural standards, dependency management policies, and modern Swift practices in all tasks — including planning, code review, architecture discussions, onboarding, and implementation.
## Core Directives
You WILL follow Toyota OneApp iOS development standards and architectural patterns.
You WILL prioritize modern Swift practices including async/await, SwiftUI, and local Swift Package Manager (SPM) modules.
You WILL adhere to Clean Architecture principles as defined by Robert C. Martin.
You WILL NEVER introduce RxSwift or Combine for new code — use async/await and Swift concurrency instead.
### Dependency Management Policy
CRITICAL: This project is migrating away from CocoaPods to Swift Package Manager (SPM).
You MUST NOT suggest or add any new CocoaPods dependencies.
You WILL use Swift Package Manager for all dependency management.
You WILL use local Swift packages in `localPackages/` for internal modules.
## Project Structure
### Targets
Here is a list of all the targets possible to be build from the project:
ToyotaOneApp, LexusOneApp, ToyotaShareToOneApp, ToyotaSiriIntent, ToyotaWatchApp, LexusShareToOneApp,
LexusSiriIntent, LexusWatchApp, SubaruOneApp, SubaruShareToOneApp, SubaruSiriIntent, SubaruWatchApp,
ToyotaOneAppAU, LexusOneAppAU, ToyotaShareToOneAppAU, LexusShareToOneAppAU, ToyotaSiriIntentAU, LexusSiriIntentAU
When asked to validate the building all the targets, build all of the targets listed above.
When asked to validate the building a specific target, build that specific target only.
By default either use the single package being worked on or the ToyotaOneApp target if the context is not clear.
### Local Package Organization
You MUST organize code into local Swift packages within the `localPackages/` directory.
Each feature MUST be implemented as a separate Swift package following this naming convention: `{FeatureName}Feature`
**Package Structure Example:**
```
localPackages/ClimateFeature/
├── Package.swift
├── Sources/
│ └── ClimateFeature/
│ ├── Climate/
│ │ ├── Domain/ # Business logic, entities, repository protocols
│ │ ├── Presentation/ # Views, StateNotifiers, UI components
│ │ ├── Application/ # Use cases, business workflows
│ │ └── Mocks/ # Mock implementations for testing and previews
│ └── ClimateSchedule/
│ ├── Domain/
│ ├── Presentation/
│ ├── Application/
│ ├── DataAccess/ # Repository implementations, API clients
│ └── Mocks/
└── Tests/
└── ClimateFeatureTests/
```
### Package Dependencies
You MUST declare dependencies in `Package.swift` following these patterns:
- Local package dependencies use `.package(path: "../{PackageName}")`
- External dependencies use `.package(url:...)` with version constraints
- Set minimum platform to `.iOS(.v17)` for new packages
**Example Package.swift:**
```swift
// swift-tools-version: 5.9
import PackageDescription
public let package = Package(
name: "ClimateFeature",
platforms: [
.iOS(.v17)
],
products: [
.library(name: "ClimateFeature", targets: ["ClimateFeature"]),
],
dependencies: [
.package(path: "../Components"),
.package(path: "../Navigation"),
.package(path: "../Analytics"),
.package(path: "../NetworkClients"),
],
targets: [
.target(
name: "ClimateFeature",
dependencies: [
"Components",
"Navigation",
"Analytics",
"NetworkClients"
]
),
.testTarget(
name: "ClimateFeatureTests",
dependencies: ["ClimateFeature"]
),
]
)
```
## GraphQL Networking Layer
### Overview
The Toyota OneApp iOS project uses **Apollo iOS (v1.7.0)** for GraphQL networking, organized in local Swift packages:
- `localPackages/GraphQLLib` — Core GraphQL networking library with Apollo client wrappers
- `localPackages/NetworkClients` — API client implementations, GraphQL operations, and generated schema
### GraphQL Operations
**Location:** `localPackages/NetworkClients/Sources/NetworkClients/GraphQL/`
You WILL organize GraphQL operations by feature domain:
- **Queries:** `Operations/Query.graphql`
- **Mutations:** `Operations/Mutation.graphql`
- **Subscriptions:** `Operations/Subscription.graphql`
- **Feature-specific:** `Operations/{FeatureName}/{Operation}.graphql`
**Schema Location:** `GraphQL/schema.graphqls` (auto-generated from introspection)
### Apollo Codegen
You WILL regenerate GraphQL types after modifying operations:
```bash
cd localPackages/NetworkClients/Sources/NetworkClients/GraphQL
./Apollo-codegen/apollo-ios-cli generate
```
**Configuration:** `apollo-codegen-config.json`
- Schema namespace: `VehicleStateAPI`
- Generated types: `GraphQL/VehicleStateAPI/`
### Network Client Architecture
**Entry Point:**
```swift
let client = NetworkClients.graphQLApi()
```
**Client Stack:**
```
NetworkClients.graphQLApi()
└── GraphQLApiClient
├── AuthenticationService (token refresh)
├── RestDefaultHeaderService (HTTP headers)
└── GraphService (from GraphQLLib)
└── GraphClient (Apollo HTTP + WebSocket)
```
### Making GraphQL Requests
You WILL use async/await patterns with Apollo GraphQL:
```swift
let client = NetworkClients.graphQLApi()
let result = await client.authenticated.call(
operation: GetVehicleStatusQuery(vin: vehicleVin),
additionalHeaders: [:]
)
switch result {
case .success(let response):
let vehicleStatus = response.data?.getVehicleStatus
case .error(let message):
// Handle error
}
```
### Authentication & Headers
**Authentication:**
- Token management: `AuthenticationService.swift`
- Automatic refresh on 401/403 via `GraphAuthenticateInterceptor`
- Retry policy configured per client (default: 1 retry for auth errors)
**Standard Headers:**
```swift
[
"x-channel": "oneapp",
"x-os-name": systemName,
"x-os-version": systemVersion,
"x-app-version": appVersion,
"x-app-brand": appBrand,
"x-locale": language,
"x-api-key": apiKey,
"x-guid": guid,
"x-device-id": deviceId,
"x-correlation-id": correlationId
]
```
### Error Handling
You WILL handle GraphQL errors using the established error types:
```swift
enum GraphQLLibError: Error {
case queryDocumentError
case invalidJsonError
case invalidToken
case graphClientError(Error)
}
extension Error {
var isNetworkError: Bool { /* ... */ }
var isNetworkTimedout: Bool { /* ... */ }
}
```
**Retry Configuration:**
- 401/403: 1 retry with token refresh (1 second delay)
- 5xx errors: 3 retries, no delay
- Exponential backoff via `GraphRetryInterceptor`
### Interceptor Chain
You WILL understand the request/response flow:
1. `GraphDefaultHeaderInterceptor` → Adds standard headers
2. `GraphAuthenticateInterceptor` → Adds bearer token
3. `GraphCacheReadInterceptor` → Checks in-memory cache
4. `GraphRequestLogInterceptor` → Logs request
5. **HTTP/WebSocket Request**
6. `GraphResponseErrorInterceptor` → Parses errors
7. `GraphResponseLogInterceptor` → Logs response
8. `GraphCacheWriteInterceptor` → Updates cache
### Caching Strategy
**Current Implementation:**
- In-memory cache only (`GraphInMemoryNormalizedCache`)
- Default policy: `.fetchIgnoringCacheCompletely`
- NO persistent disk caching
- Cache is cleared on app restart
You WILL NOT implement persistent GraphQL caching unless explicitly requested.
### WebSocket Subscriptions
You WILL use subscriptions for real-time updates:
```swift
let subscription = SubscriptionRemoteCommandsSubscription(vin: vehicleVin)
let result = await client.authenticated.call(
operation: subscription,
additionalHeaders: [:]
)
```
**WebSocket Transport:**
- Auto-reconnection on auth refresh
- Connection managed by `WebSocketTransportFactory`
### Key File Locations
| Component | Path |
|-----------|------|
| **GraphQL Client** | `NetworkClients/Clients/GraphQLApiClient/Client/GraphQLApiClient.swift` |
| **Authentication** | `NetworkClients/Clients/GraphQLApiClient/Client/AuthenticationService.swift` |
| **Apollo Service** | `GraphQLLib/Networking/BaseNetwork/GraphService/GraphService.swift` |
| **Operations** | `NetworkClients/GraphQL/Operations/*.graphql` |
| **Schema** | `NetworkClients/GraphQL/schema.graphqls` |
| **Generated Types** | `NetworkClients/GraphQL/VehicleStateAPI/` |
| **Codegen Config** | `NetworkClients/GraphQL/apollo-codegen-config.json` |
| **Interceptors** | `GraphQLLib/Networking/BaseNetwork/Interceptor/Graph/` |
## Clean Architecture Requirements
### Layer Responsibilities
You MUST organize code into these layers within each feature:
**Domain Layer** (`Domain/`)
- You WILL define business entities, value objects, and domain models
- You WILL create repository protocols (interfaces)
- You WILL keep domain logic independent of frameworks and UI
- You WILL use `internal` access control by default for domain types
- CRITICAL: Domain layer MUST NOT depend on Presentation or DataAccess layers
**Application Layer** (`Application/`)
- You WILL implement use case protocols and concrete implementations
- You WILL orchestrate business workflows and coordinate between repositories
- You WILL handle business rule validation and orchestration
- You MUST use async/await for asynchronous operations
- CRITICAL: Use cases MUST be protocol-based for testability
**Presentation Layer** (`Presentation/`)
- You WILL create SwiftUI views and state notifiers
- You WILL implement state management using `@Published` in state notifier classes
- You WILL inject use cases via initializers for dependency injection
- You WILL keep views declarative and presentation logic minimal
- MANDATORY: You MUST create SwiftUI previews using `#Preview` macro for all views
- CRITICAL: Views MUST NOT directly access repositories or data sources
**DataAccess Layer** (`DataAccess/`)
- You WILL implement repository protocols defined in Domain layer
- You WILL handle network requests, database operations, and caching
- You WILL use dependency injection for API clients and data sources
- You MUST use async/await for all asynchronous data operations
**Example Clean Architecture Implementation:**
```swift
// Domain/Repos/ClimateScheduleRepo.swift
internal protocol ClimateScheduleRepo {
func fetchClimateScheduleList(
generation: Generation,
vin: String,
make: VehicleMake
) async -> Result<ClimateScheduleSettingsData, RequestFailure>
}
// Application/ClimateScheduleUseCases.swift
public protocol ClimateScheduleUseCases {
var state: Published<ClimateScheduleState>.Publisher { get }
func toggleSchedule(id: Int)
func refreshList(refresh: Bool)
}
// DataAccess/ClimateScheduleAPIRepo.swift
internal final class ClimateScheduleAPIRepo: ClimateScheduleRepo {
private let apiClient: APIClient
init(apiClient: APIClient) {
self.apiClient = apiClient
}
func fetchClimateScheduleList(
generation: Generation,
vin: String,
make: VehicleMake
) async -> Result<ClimateScheduleSettingsData, RequestFailure> {
// Implementation using async/await
}
}
// Presentation/ClimateScheduleView.swift
public struct ClimateScheduleView: View {
@StateObject private var stateNotifier: ClimateScheduleStateNotifier
public init(useCases: ClimateScheduleUseCases) {
_stateNotifier = StateObject(
wrappedValue: ClimateScheduleStateNotifier(useCases: useCases)
)
}
public var body: some View {
List(stateNotifier.schedules) { schedule in
Text(schedule.name)
}
}
}
#Preview {
ClimateScheduleView(useCases: ClimateScheduleUseCasesMock())
}
```
## Modern Swift Practices
### Async/Await Requirements
You MUST use async/await for all asynchronous operations.
You WILL NEVER use RxSwift or Combine in new code.
You WILL migrate existing Combine/RxSwift code to async/await when making significant changes.
```swift
// ✅ CORRECT: Use async/await
func fetchClimateStatus(vehicle: Vehicle) async -> Result<Bool, RequestFailure> {
do {
let status = try await apiClient.fetchStatus(vehicle)
return .success(status.isEnabled)
} catch {
return .failure(.networkError(error))
}
}
// ✅ CORRECT: Use Task for calling async from sync context
func refreshData() {
Task {
await fetchScheduleList()
}
}
// ❌ AVOID: Combine publishers or RxSwift observables in new code
```
### SwiftUI Requirements
You MUST use SwiftUI for all new UI features.
You WILL use `@StateObject`, `@ObservedObject`, and `@Published` for state management.
MANDATORY: You MUST provide `#Preview` for every SwiftUI view using mock implementations.
## Testing Requirements
### Unit Testing Standards
You MUST write unit tests for all business logic, use cases, and repositories.
You WILL create mock implementations in `Mocks/` subdirectory within each feature module.
You WILL use XCTest framework for all tests.
CRITICAL: Mocks MUST be reusable for both unit tests AND SwiftUI previews.
### Mock Organization
You WILL place mock implementations in a `Mocks/` directory at the feature level:
- Structure: `Sources/{FeatureName}/{SubFeature}/Mocks/`
- Mocks are accessible to both production code (for previews) and test code
- Mock classes MUST have public initializers for use in previews
**Testing and Mock Patterns:**
```swift
// Sources/ClimateFeature/ClimateSchedule/Mocks/ClimateScheduleRepoMock.swift
public final class ClimateScheduleRepoMock: ClimateScheduleRepo {
public var fetchClimateScheduleListResult: Result<ClimateScheduleSettingsData, RequestFailure>?
public var fetchClimateScheduleListCallCount = 0
public init() {}
public func fetchClimateScheduleList(
generation: Generation,
vin: String,
make: VehicleMake
) async -> Result<ClimateScheduleSettingsData, RequestFailure> {
fetchClimateScheduleListCallCount += 1
return fetchClimateScheduleListResult ?? .failure(.unknown)
}
}
// Tests/ClimateFeatureTests/ClimateScheduleLogicTests.swift
import XCTest
@testable import ClimateFeature
final class ClimateScheduleLogicTests: XCTestCase {
private var sut: ClimateScheduleLogic!
private var mockRepo: ClimateScheduleRepoMock!
override func setUp() {
super.setUp()
mockRepo = ClimateScheduleRepoMock()
sut = ClimateScheduleLogic(repository: mockRepo)
}
func testFetchScheduleList_WhenSuccessful_UpdatesState() async {
let expectedData = ClimateScheduleSettingsData(schedules: [])
mockRepo.fetchClimateScheduleListResult = .success(expectedData)
await sut.fetchScheduleList()
XCTAssertEqual(mockRepo.fetchClimateScheduleListCallCount, 1)
}
}
```
## Migration Guidelines
### Legacy Code Interaction
When working with existing legacy code:
- You WILL gradually migrate from RxSwift/Combine to async/await when touching legacy modules
- You WILL bridge UIKit and SwiftUI using `UIViewRepresentable` or `UIHostingController` when necessary
- You WILL prioritize refactoring legacy code into Clean Architecture packages when feasible
- You WILL NOT introduce new RxSwift/Combine dependencies
### Deprecation Patterns
You WILL mark deprecated code with `@available` attributes:
```swift
@available(*, deprecated, message: "Use async/await version instead")
func fetchDataWithCombine() -> AnyPublisher<Data, Error> {
// Legacy implementation
}
func fetchData() async throws -> Data {
// Modern implementation
}
```
## Error Handling
### Result Type Usage
You WILL use Swift's `Result` type for operations that can fail:
```swift
func fetchClimateStatus(vehicle: Vehicle) async -> Result<Bool, RequestFailure> {
do {
let status = try await apiClient.fetchStatus(vehicle)
return .success(status.isEnabled)
} catch let error as NetworkError {
return .failure(.networkError(error))
} catch {
return .failure(.unknown)
}
}
```
### Error Types
You WILL define custom error types conforming to `Error` protocol:
```swift
enum ClimateScheduleError: Error {
case invalidScheduleTime
case scheduleConflict
case networkFailure(underlying: Error)
var localizedDescription: String {
switch self {
case .invalidScheduleTime:
return "The schedule time is invalid"
case .scheduleConflict:
return "This schedule conflicts with an existing one"
case .networkFailure(let error):
return "Network error: \(error.localizedDescription)"
}
}
}
```
## Fastlane Integration
You WILL use Fastlane for build automation, testing, and deployment tasks.
You MUST reference existing lanes defined in `fastlane/Fastfile` and imported Fastfiles.
**Common Fastlane Commands:**
- Build: `fastlane build`
- Run tests: `fastlane test`
- Lint code: `fastlane lint`
- Run locally: `fastlane run_local`
### CI/CD Considerations
You WILL ensure all code changes pass CI/CD pipelines:
- SwiftLint must pass without warnings
- All unit tests must pass
- Build must succeed for all variants (Toyota/Lexus NA, Subaru, Toyota/Lexus AU)
## How to Compile
Individual Packages:
```bash
xcodebuild -workspace OneApp.xcworkspace -scheme {PACKAGE} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' clean build
```
Entire workspace:
```bash
xcodebuild -workspace OneApp.xcworkspace -scheme ToyotaOneApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
```
## Quick Reference: Do's and Don'ts
**✅ DO:**
- Use async/await for asynchronous operations
- Create local Swift packages for new features
- Follow Clean Architecture with Domain/Application/Presentation/DataAccess layers
- Use SwiftUI for new UI features with StateNotifiers
- Create `#Preview` for every SwiftUI view
- Write unit tests with mocks stored in `Mocks/` subdirectory
- Make mocks reusable for tests and previews
- Include copyright headers
- Follow SwiftLint and swift-format rules
- Use protocol-based dependency injection
- Comment the "why", never the "what"
**❌ DON'T:**
- Introduce RxSwift or Combine in new code
- Use force unwrapping or force try in production code
- Create direct dependencies between Presentation and DataAccess layers
- Exceed 120 character line length
- Skip writing unit tests for business logic
- Skip creating previews for SwiftUI views
- Use UIKit for new features (unless bridging is necessary)
- Hardcode API endpoints or configuration values
- Call state management classes "ViewModels" — use "StateNotifier" instead
- Write comments that restate what code does
- Create separate mock packages — keep mocks within the feature package
- Suggest or add CocoaPods dependencies (project is migrating to SPM)

View File

@ -1,483 +1,12 @@
---
description: 'Best practices and patterns for Swift'
description: 'Swift coding style, formatting, and syntax rules for Toyota OneApp iOS'
applyTo: "**/*.swift, **/Package.swift, **/Package.resolved"
---
# Swift Development Instructions
# Swift Coding Style Instructions
## Core Directives
These rules auto-apply when editing Swift files. For full architectural guidance, project structure, GraphQL networking, Clean Architecture patterns, and planning — use the **Swift iOS Engineer** agent.
You WILL follow Toyota OneApp iOS development standards and architectural patterns when working with Swift code.
You WILL prioritize modern Swift practices including async/await, SwiftUI, and local Swift Package Manager (SPM) modules.
You WILL adhere to Clean Architecture principles as defined by Robert C. Martin.
You WILL NEVER introduce RxSwift or Combine for new code - use async/await and Swift concurrency instead.
### Dependency Management Policy
CRITICAL: This project is migrating away from CocoaPods to Swift Package Manager (SPM).
You MUST NOT suggest or add any new CocoaPods dependencies.
You WILL use Swift Package Manager for all dependency management.
You WILL use local Swift packages in `localPackages/` for internal modules.
## Project Structure
<!-- <project-structure> -->
### Local Package Organization
You MUST organize code into local Swift packages within the `localPackages/` directory.
Each feature MUST be implemented as a separate Swift package following this naming convention: `{FeatureName}Feature`
<!-- <package-structure-example> -->
**Package Structure Example:**
```
localPackages/ClimateFeature/
├── Package.swift
├── Sources/
│ └── ClimateFeature/
│ ├── Climate/
│ │ ├── Domain/ # Business logic, entities, repository protocols
│ │ ├── Presentation/ # Views, StateNotifiers, UI components
│ │ ├── Application/ # Use cases, business workflows
│ │ └── Mocks/ # Mock implementations for testing and previews
│ └── ClimateSchedule/
│ ├── Domain/
│ ├── Presentation/
│ ├── Application/
│ ├── DataAccess/ # Repository implementations, API clients
│ └── Mocks/
└── Tests/
└── ClimateFeatureTests/
```
<!-- </package-structure-example> -->
### Package Dependencies
You MUST declare dependencies in `Package.swift` following these patterns:
- Local package dependencies use `.package(path: "../{PackageName}")`
- External dependencies use `.package(url:...)` with version constraints
- Set minimum platform to `.iOS(.v17)` for new packages
<!-- <package-swift-example> -->
**Example Package.swift:**
```swift
// swift-tools-version: 5.9
import PackageDescription
public let package = Package(
name: "ClimateFeature",
platforms: [
.iOS(.v17)
],
products: [
.library(name: "ClimateFeature", targets: ["ClimateFeature"]),
],
dependencies: [
.package(path: "../Components"),
.package(path: "../Navigation"),
.package(path: "../Analytics"),
.package(path: "../NetworkClients"),
],
targets: [
.target(
name: "ClimateFeature",
dependencies: [
"Components",
"Navigation",
"Analytics",
"NetworkClients"
]
),
.testTarget(
name: "ClimateFeatureTests",
dependencies: ["ClimateFeature"]
),
]
)
```
<!-- </package-swift-example> -->
<!-- </project-structure> -->
## GraphQL Networking Layer
<!-- <graphql-networking> -->
### Overview
The Toyota OneApp iOS project uses **Apollo iOS (v1.7.0)** for GraphQL networking, organized in local Swift packages:
- `localPackages/GraphQLLib` - Core GraphQL networking library with Apollo client wrappers
- `localPackages/NetworkClients` - API client implementations, GraphQL operations, and generated schema
### GraphQL Operations
**Location:** `localPackages/NetworkClients/Sources/NetworkClients/GraphQL/`
You WILL organize GraphQL operations by feature domain:
- **Queries:** `Operations/Query.graphql`
- **Mutations:** `Operations/Mutation.graphql`
- **Subscriptions:** `Operations/Subscription.graphql`
- **Feature-specific:** `Operations/{FeatureName}/{Operation}.graphql`
**Schema Location:** `GraphQL/schema.graphqls` (auto-generated from introspection)
### Apollo Codegen
You WILL regenerate GraphQL types after modifying operations:
```bash
cd localPackages/NetworkClients/Sources/NetworkClients/GraphQL
./Apollo-codegen/apollo-ios-cli generate
```
**Configuration:** `apollo-codegen-config.json`
- Schema namespace: `VehicleStateAPI`
- Generated types: `GraphQL/VehicleStateAPI/`
### Network Client Architecture
**Entry Point:**
```swift
// Access GraphQL client
let client = NetworkClients.graphQLApi()
```
**Client Stack:**
```
NetworkClients.graphQLApi()
└── GraphQLApiClient
├── AuthenticationService (token refresh)
├── RestDefaultHeaderService (HTTP headers)
└── GraphService (from GraphQLLib)
└── GraphClient (Apollo HTTP + WebSocket)
```
### Making GraphQL Requests
You WILL use async/await patterns with Apollo GraphQL:
```swift
// Example: Execute a query
let client = NetworkClients.graphQLApi()
let result = await client.authenticated.call(
operation: GetVehicleStatusQuery(vin: vehicleVin),
additionalHeaders: [:]
)
switch result {
case .success(let response):
let vehicleStatus = response.data?.getVehicleStatus
case .error(let message):
// Handle error
}
```
### Authentication & Headers
**Authentication:**
- Token management: `AuthenticationService.swift`
- Automatic refresh on 401/403 via `GraphAuthenticateInterceptor`
- Retry policy configured per client (default: 1 retry for auth errors)
**Standard Headers:**
```swift
[
"x-channel": "oneapp",
"x-os-name": systemName,
"x-os-version": systemVersion,
"x-app-version": appVersion,
"x-app-brand": appBrand,
"x-locale": language,
"x-api-key": apiKey,
"x-guid": guid,
"x-device-id": deviceId,
"x-correlation-id": correlationId
]
```
### Error Handling
You WILL handle GraphQL errors using the established error types:
```swift
// GraphQL-specific errors
enum GraphQLLibError: Error {
case queryDocumentError
case invalidJsonError
case invalidToken
case graphClientError(Error)
}
// Network errors
extension Error {
var isNetworkError: Bool { /* ... */ }
var isNetworkTimedout: Bool { /* ... */ }
}
```
**Retry Configuration:**
- 401/403: 1 retry with token refresh (1 second delay)
- 5xx errors: 3 retries, no delay
- Exponential backoff via `GraphRetryInterceptor`
### Interceptor Chain
You WILL understand the request/response flow:
1. `GraphDefaultHeaderInterceptor` → Adds standard headers
2. `GraphAuthenticateInterceptor` → Adds bearer token
3. `GraphCacheReadInterceptor` → Checks in-memory cache
4. `GraphRequestLogInterceptor` → Logs request
5. **HTTP/WebSocket Request**
6. `GraphResponseErrorInterceptor` → Parses errors
7. `GraphResponseLogInterceptor` → Logs response
8. `GraphCacheWriteInterceptor` → Updates cache
### Caching Strategy
**Current Implementation:**
- In-memory cache only (`GraphInMemoryNormalizedCache`)
- Default policy: `.fetchIgnoringCacheCompletely`
- NO persistent disk caching
- Cache is cleared on app restart
You WILL NOT implement persistent GraphQL caching unless explicitly requested.
### WebSocket Subscriptions
You WILL use subscriptions for real-time updates:
```swift
// Example: Subscribe to remote commands
let subscription = SubscriptionRemoteCommandsSubscription(vin: vehicleVin)
let result = await client.authenticated.call(
operation: subscription,
additionalHeaders: [:]
)
```
**WebSocket Transport:**
- Auto-reconnection on auth refresh
- Connection managed by `WebSocketTransportFactory`
### Key File Locations
| Component | Path |
|-----------|------|
| **GraphQL Client** | `NetworkClients/Clients/GraphQLApiClient/Client/GraphQLApiClient.swift` |
| **Authentication** | `NetworkClients/Clients/GraphQLApiClient/Client/AuthenticationService.swift` |
| **Apollo Service** | `GraphQLLib/Networking/BaseNetwork/GraphService/GraphService.swift` |
| **Operations** | `NetworkClients/GraphQL/Operations/*.graphql` |
| **Schema** | `NetworkClients/GraphQL/schema.graphqls` |
| **Generated Types** | `NetworkClients/GraphQL/VehicleStateAPI/` |
| **Codegen Config** | `NetworkClients/GraphQL/apollo-codegen-config.json` |
| **Interceptors** | `GraphQLLib/Networking/BaseNetwork/Interceptor/Graph/` |
<!-- </graphql-networking> -->
## Clean Architecture Requirements
<!-- <clean-architecture> -->
### Layer Responsibilities
You MUST organize code into these layers within each feature:
**Domain Layer** (`Domain/`)
- You WILL define business entities, value objects, and domain models
- You WILL create repository protocols (interfaces)
- You WILL keep domain logic independent of frameworks and UI
- You WILL use `internal` access control by default for domain types
- CRITICAL: Domain layer MUST NOT depend on Presentation or DataAccess layers
**Application Layer** (`Application/`)
- You WILL implement use case protocols and concrete implementations
- You WILL orchestrate business workflows and coordinate between repositories
- You WILL handle business rule validation and orchestration
- You MUST use async/await for asynchronous operations
- CRITICAL: Use cases MUST be protocol-based for testability
**Presentation Layer** (`Presentation/`)
- You WILL create SwiftUI views and state notifiers
- You WILL implement state management using `@Published` in state notifier classes
- You WILL inject use cases via initializers for dependency injection
- You WILL keep views declarative and presentation logic minimal
- MANDATORY: You MUST create SwiftUI previews using `#Preview` macro for all views
- CRITICAL: Views MUST NOT directly access repositories or data sources
**DataAccess Layer** (`DataAccess/`)
- You WILL implement repository protocols defined in Domain layer
- You WILL handle network requests, database operations, and caching
- You WILL use dependency injection for API clients and data sources
- You MUST use async/await for all asynchronous data operations
<!-- <clean-architecture-example> -->
**Example Clean Architecture Implementation:**
```swift
// Domain/Repos/ClimateScheduleRepo.swift
internal protocol ClimateScheduleRepo {
func fetchClimateScheduleList(
generation: Generation,
vin: String,
make: VehicleMake
) async -> Result<ClimateScheduleSettingsData, RequestFailure>
}
// Application/ClimateScheduleUseCases.swift
public protocol ClimateScheduleUseCases {
var state: Published<ClimateScheduleState>.Publisher { get }
func toggleSchedule(id: Int)
func refreshList(refresh: Bool)
}
// DataAccess/ClimateScheduleAPIRepo.swift
internal final class ClimateScheduleAPIRepo: ClimateScheduleRepo {
private let apiClient: APIClient
init(apiClient: APIClient) {
self.apiClient = apiClient
}
func fetchClimateScheduleList(
generation: Generation,
vin: String,
make: VehicleMake
) async -> Result<ClimateScheduleSettingsData, RequestFailure> {
// Implementation using async/await
}
}
// Presentation/ClimateScheduleView.swift
public struct ClimateScheduleView: View {
@StateObject private var stateNotifier: ClimateScheduleStateNotifier
public init(useCases: ClimateScheduleUseCases) {
_stateNotifier = StateObject(
wrappedValue: ClimateScheduleStateNotifier(useCases: useCases)
)
}
public var body: some View {
List(stateNotifier.schedules) { schedule in
Text(schedule.name)
}
}
}
#Preview {
ClimateScheduleView(useCases: ClimateScheduleUseCasesMock())
}
```
<!-- </clean-architecture-example> -->
<!-- </clean-architecture> -->
## Modern Swift Practices
<!-- <modern-swift> -->
### Async/Await Requirements
You MUST use async/await for all asynchronous operations.
You WILL NEVER use RxSwift or Combine in new code.
You WILL migrate existing Combine/RxSwift code to async/await when making significant changes.
<!-- <async-await-examples> -->
**Async/Await Patterns:**
```swift
// ✅ CORRECT: Use async/await for asynchronous functions
func fetchClimateStatus(vehicle: Vehicle) async -> Result<Bool, RequestFailure> {
do {
let status = try await apiClient.fetchStatus(vehicle)
return .success(status.isEnabled)
} catch {
return .failure(.networkError(error))
}
}
// ✅ CORRECT: Use Task for calling async from sync context
func refreshData() {
Task {
await fetchScheduleList()
}
}
// ❌ AVOID: Do not use Combine publishers in new code
// var cancellables = Set<AnyCancellable>()
// apiClient.fetchStatus().sink { ... }
// ❌ AVOID: Do not use RxSwift observables
// apiClient.fetchStatus().subscribe(onNext: { ... })
```
<!-- </async-await-examples> -->
### SwiftUI Requirements
You MUST use SwiftUI for all new UI features.
You WILL create declarative, composable views.
You WILL use `@StateObject`, `@ObservedObject`, and `@Published` for state management.
MANDATORY: You MUST provide `#Preview` for every SwiftUI view using mock implementations.
<!-- <swiftui-examples> -->
**SwiftUI Patterns:**
```swift
// ✅ CORRECT: SwiftUI view with proper state management and preview
public struct ClimateDetailView: View {
@StateObject private var stateNotifier: ClimateDetailStateNotifier
@State private var showTimeSheet = false
public init(useCases: ClimateDetailUseCases) {
_stateNotifier = StateObject(
wrappedValue: ClimateDetailStateNotifier(useCases: useCases)
)
}
public var body: some View {
VStack {
Text(stateNotifier.temperature)
Button("Change Time") {
showTimeSheet = true
}
}
.sheet(isPresented: $showTimeSheet) {
TimeSelectionView()
}
}
}
#Preview {
ClimateDetailView(useCases: ClimateDetailUseCasesMock())
}
// ✅ CORRECT: StateNotifier with async operations
@MainActor
final class ClimateDetailStateNotifier: ObservableObject {
@Published var temperature: String = ""
@Published var isLoading: Bool = false
private let useCases: ClimateDetailUseCases
init(useCases: ClimateDetailUseCases) {
self.useCases = useCases
}
func updateTemperature(_ temp: Double) {
Task {
isLoading = true
await useCases.changeTemperature(temp)
isLoading = false
}
}
}
```
<!-- </swiftui-examples> -->
<!-- </modern-swift> -->
## Code Style and Quality
<!-- <code-style> -->
## Code Style and Formatting
### SwiftLint and swift-format Compliance
@ -497,11 +26,12 @@ You MUST follow Swift API Design Guidelines:
- Variables/Functions: `lowerCamelCase` (e.g., `fetchClimateStatus`, `reservationId`)
- Constants: `lowerCamelCase` (e.g., `maxTemperature`, `defaultTimeout`)
- Protocols: Descriptive names ending in `-able`, `-ing`, or role-based (e.g., `ClimateScheduleRepo`, `Codable`)
- State management classes: Use "StateNotifier" — NEVER "ViewModel"
### File Headers
You MUST include copyright headers in all Swift files.
You WILL use the current year (2026) in copyright headers.
You WILL use the current year in copyright headers.
```swift
// Copyright © 2026 Toyota. All rights reserved.
@ -512,312 +42,54 @@ You WILL use the current year (2026) in copyright headers.
You WILL organize code with MARK comments for major sections only.
You WILL use MARK comments to separate significant logical groupings within a file.
<!-- <mark-comment-examples> -->
**MARK Comment Guidelines:**
```swift
// ✅ CORRECT: MARK for major sections and protocols
// ✅ CORRECT: MARK for major sections
// MARK: - Climate Schedule Use Cases
public protocol ClimateScheduleUseCases {
var state: Published<ClimateScheduleState>.Publisher { get }
func toggleSchedule(id: Int)
func refreshList(refresh: Bool)
}
final class ClimateScheduleLogic: ClimateScheduleUseCases {
private let repository: ClimateScheduleRepo
@Published private var _state = ClimateScheduleState()
var state: Published<ClimateScheduleState>.Publisher { $_state }
init(repository: ClimateScheduleRepo) {
self.repository = repository
}
func toggleSchedule(id: Int) {
// Implementation
}
func refreshList(refresh: Bool) {
// Implementation
}
}
// ❌ AVOID: Excessive MARK comments for every section
final class ExampleClass {
// MARK: Properties // Too granular
private let value: String
// MARK: Initialization // Too granular
init(value: String) {
self.value = value
}
// MARK: Public Methods // Too granular
func doSomething() {}
}
```
<!-- </mark-comment-examples> -->
<!-- </code-style> -->
## Testing Requirements
<!-- <testing> -->
### Unit Testing Standards
You MUST write unit tests for all business logic, use cases, and repositories.
You WILL create mock implementations in `Mocks/` subdirectory within each feature module.
You WILL use XCTest framework for all tests.
CRITICAL: Mocks MUST be reusable for both unit tests AND SwiftUI previews.
### Mock Organization
You WILL place mock implementations in a `Mocks/` directory at the feature level:
- Structure: `Sources/{FeatureName}/{SubFeature}/Mocks/`
- Mocks are accessible to both production code (for previews) and test code
- Mock classes MUST have public initializers for use in previews
<!-- <testing-examples> -->
**Testing and Mock Patterns:**
```swift
// Sources/ClimateFeature/ClimateSchedule/Mocks/ClimateScheduleRepoMock.swift
public final class ClimateScheduleRepoMock: ClimateScheduleRepo {
public var fetchClimateScheduleListResult: Result<ClimateScheduleSettingsData, RequestFailure>?
public var fetchClimateScheduleListCallCount = 0
public init() {}
public func fetchClimateScheduleList(
generation: Generation,
vin: String,
make: VehicleMake
) async -> Result<ClimateScheduleSettingsData, RequestFailure> {
fetchClimateScheduleListCallCount += 1
return fetchClimateScheduleListResult ?? .failure(.unknown)
}
}
// Sources/ClimateFeature/ClimateSchedule/Mocks/ClimateScheduleUseCasesMock.swift
public final class ClimateScheduleUseCasesMock: ClimateScheduleUseCases {
public var state: Published<ClimateScheduleState>.Publisher { $_state }
@Published public var _state = ClimateScheduleState()
public var toggleScheduleCallCount = 0
public init() {}
public func toggleSchedule(id: Int) {
toggleScheduleCallCount += 1
}
public func refreshList(refresh: Bool) {
// Mock implementation
}
}
// Tests/ClimateFeatureTests/ClimateScheduleLogicTests.swift
import XCTest
@testable import ClimateFeature
final class ClimateScheduleLogicTests: XCTestCase {
private var sut: ClimateScheduleLogic!
private var mockRepo: ClimateScheduleRepoMock!
override func setUp() {
super.setUp()
mockRepo = ClimateScheduleRepoMock()
sut = ClimateScheduleLogic(repository: mockRepo)
}
override func tearDown() {
sut = nil
mockRepo = nil
super.tearDown()
}
func testFetchScheduleList_WhenSuccessful_UpdatesState() async {
// Given
let expectedData = ClimateScheduleSettingsData(schedules: [])
mockRepo.fetchClimateScheduleListResult = .success(expectedData)
// When
await sut.fetchScheduleList()
// Then
XCTAssertEqual(mockRepo.fetchClimateScheduleListCallCount, 1)
}
}
// Sources/ClimateFeature/ClimateSchedule/Presentation/ClimateScheduleView.swift
public struct ClimateScheduleView: View {
@StateObject private var stateNotifier: ClimateScheduleStateNotifier
public init(useCases: ClimateScheduleUseCases) {
_stateNotifier = StateObject(
wrappedValue: ClimateScheduleStateNotifier(useCases: useCases)
)
}
public var body: some View {
List {
Text("Climate Schedules")
}
}
}
#Preview {
ClimateScheduleView(useCases: ClimateScheduleUseCasesMock())
}
```
<!-- </testing-examples> -->
<!-- </testing> -->
## Fastlane Integration
<!-- <fastlane> -->
### Fastlane Usage
You WILL use Fastlane for build automation, testing, and deployment tasks.
You MUST reference existing lanes defined in `fastlane/Fastfile` and imported Fastfiles.
**Common Fastlane Commands:**
- Build: `fastlane build`
- Run tests: `fastlane test`
- Lint code: `fastlane lint`
- Run locally: `fastlane run_local`
### CI/CD Considerations
You WILL ensure all code changes pass CI/CD pipelines:
- SwiftLint must pass without warnings
- All unit tests must pass
- Build must succeed for all variants (Toyota/Lexus NA, Subaru, Toyota/Lexus AU)
<!-- </fastlane> -->
## Migration Guidelines
<!-- <migration> -->
### Legacy Code Interaction
When working with existing legacy code:
- You WILL gradually migrate from RxSwift/Combine to async/await when touching legacy modules
- You WILL bridge UIKit and SwiftUI using `UIViewRepresentable` or `UIHostingController` when necessary
- You WILL prioritize refactoring legacy code into Clean Architecture packages when feasible
- You WILL NOT introduce new RxSwift/Combine dependencies
### Deprecation Patterns
You WILL mark deprecated code with `@available` attributes:
```swift
@available(*, deprecated, message: "Use async/await version instead")
func fetchDataWithCombine() -> AnyPublisher<Data, Error> {
// Legacy implementation
}
// New async/await version
func fetchData() async throws -> Data {
// Modern implementation
}
// MARK: Properties // Too granular
// MARK: Initialization // Too granular
```
<!-- </migration> -->
## Swift Language Rules
## Error Handling
### Concurrency
<!-- <error-handling> -->
### Result Type Usage
You WILL use Swift's `Result` type for operations that can fail:
You MUST use async/await for all asynchronous operations.
You WILL NEVER introduce RxSwift or Combine in new code.
```swift
// ✅ CORRECT
func fetchClimateStatus(vehicle: Vehicle) async -> Result<Bool, RequestFailure> {
do {
let status = try await apiClient.fetchStatus(vehicle)
return .success(status.isEnabled)
} catch let error as NetworkError {
return .failure(.networkError(error))
} catch {
return .failure(.unknown)
return .failure(.networkError(error))
}
}
// ❌ AVOID: Combine or RxSwift
```
### Error Types
### SwiftUI
You WILL define custom error types conforming to `Error` protocol:
You MUST use SwiftUI for all new UI features.
You WILL use `@StateObject`, `@ObservedObject`, and `@Published` for state management.
MANDATORY: You MUST provide `#Preview` for every SwiftUI view using mock implementations.
```swift
enum ClimateScheduleError: Error {
case invalidScheduleTime
case scheduleConflict
case networkFailure(underlying: Error)
var localizedDescription: String {
switch self {
case .invalidScheduleTime:
return "The schedule time is invalid"
case .scheduleConflict:
return "This schedule conflicts with an existing one"
case .networkFailure(let error):
return "Network error: \(error.localizedDescription)"
}
}
}
```
### Dependency Management
<!-- </error-handling> -->
CRITICAL: Do NOT add CocoaPods dependencies. Use Swift Package Manager only.
- Local packages: `.package(path: "../{PackageName}")`
- External packages: `.package(url:...)` with version constraints
- Minimum platform: `.iOS(.v17)` for new packages
## Quick Reference
### Error Handling
<!-- <quick-reference> -->
You WILL use Swift's `Result` type for operations that can fail.
You WILL define custom error types conforming to `Error` protocol.
### How to compile:
### Comments
Here are examples on how to compile individual packages and the entire workspace:
Individual Packages
$ xcodebuild -workspace OneApp.xcworkspace -scheme {PACKAGE} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' clean build
Entire workspace:
$ xcodebuild -workspace OneApp.xcworkspace -scheme ToyotaOneApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
### Do's and Don'ts
**✅ DO:**
- Use async/await for asynchronous operations
- Create local Swift packages for new features
- Follow Clean Architecture with Domain/Application/Presentation/DataAccess layers
- Use SwiftUI for new UI features with StateNotifiers
- Create `#Preview` for every SwiftUI view
- Write unit tests with mocks stored in `Mocks/` subdirectory
- Make mocks reusable for tests and previews
- Include copyright headers
- Follow SwiftLint and swift-format rules
- Use protocol-based dependency injection
- Comment the "why", never the "what"
**❌ DON'T:**
- Introduce RxSwift or Combine in new code
- Use force unwrapping or force try in production code
- Create direct dependencies between Presentation and DataAccess layers
- Exceed 120 character line length
- Skip writing unit tests for business logic
- Skip creating previews for SwiftUI views
- Use UIKit for new features (unless bridging is necessary)
- Hardcode API endpoints or configuration values
- Call state management classes "ViewModels" - use "StateNotifier" instead
- Write comments that restate what code does
- Create separate mock packages - keep mocks within the feature package
- Suggest or add CocoaPods dependencies (project is migrating to SPM)
<!-- </quick-reference> -->
Comment the "why", never the "what". Do not write comments that restate what code does.

View File

@ -13,11 +13,15 @@ set -euo pipefail
# bash <(curl -fsSL "$ASSETS_BASE_URL/setup.sh") all ios
# ─────────────────────────────────────────────────────────────────────
VERSION="2.0.0"
VERSION="2.1.0"
# ── Configuration (override with env vars) ───────────────────────────
# Default paths use ~/.agents/ — the tool-agnostic directory.
# The npx skills CLI copies into tool-specific dirs (~/.copilot/,
# ~/.claude/, ~/.cursor/) automatically.
ASSETS_BASE_URL="${ASSETS_BASE_URL:-}"
AGENTS_DIR="${AGENTS_DIR:-$HOME/.copilot/agents}"
AGENTS_DIR="${AGENTS_DIR:-$HOME/.agents/agents}"
SKILLS_DIR="${SKILLS_DIR:-$HOME/.agents/skills}"
INSTRUCTIONS_DIR="${INSTRUCTIONS_DIR:-./instructions}"
REPO_TOKEN="${REPO_TOKEN:-}"
@ -135,34 +139,99 @@ download_to() {
fi
}
# Download an entire remote directory (one level deep).
download_dir_to() {
local src_subdir="$1" dest_dir="$2"
mkdir -p "$dest_dir"
if [[ "$MODE" == "local" ]]; then
cp -R "$ASSETS_DIR/$src_subdir/"* "$dest_dir/" 2>/dev/null || true
else
local files
files=$(list_remote_files "$src_subdir" "")
while IFS= read -r file; do
[[ -z "$file" ]] && continue
curl -fsSL ${REPO_TOKEN:+-H "Authorization: Bearer $REPO_TOKEN"} "$ASSETS_BASE_URL/$src_subdir/$file" -o "$dest_dir/$file"
done <<< "$files"
fi
}
# ── Commands ─────────────────────────────────────────────────────────
# -- skills [platform] ────────────────────────────────────────────────
# Reads a plain-text skills file. Each non-empty, non-comment line is
# passed to `npx skills add`.
# 1. Install registry skills from <platform>-skills.txt
# 2. Auto-discover and copy custom skills from assets/skills/
cmd_skills() {
local platform="${1:-shared}"
local manifest="${platform}-skills.txt"
heading "Skills ($platform)"
# ── Registry skills ──
heading "Registry Skills ($platform)"
local content
content="$(fetch "$manifest")" || fail "Could not fetch $manifest"
content="$(fetch "$manifest" 2>/dev/null)" || content=""
local count=0
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
info "npx skills add $line"
read -r -a args <<< "$line"
npx skills add "${args[@]}"
count=$((count + 1))
done <<< "$content"
if [[ $count -eq 0 ]]; then
warn "No entries in $manifest — nothing to install."
else
ok "$count skill(s) installed. Restart your editor if they don't appear."
local reg_count=0
if [[ -n "$content" ]]; then
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
info "npx skills add $line"
read -r -a args <<< "$line"
npx skills add "${args[@]}"
reg_count=$((reg_count + 1))
done <<< "$content"
fi
if [[ $reg_count -eq 0 ]]; then
warn "No entries in $manifest."
else
ok "$reg_count registry skill(s) installed."
fi
# ── Custom / local skills ──
heading "Custom Skills → $SKILLS_DIR"
mkdir -p "$SKILLS_DIR"
local skill_dirs count=0
if [[ "$MODE" == "local" ]]; then
if [[ -d "$ASSETS_DIR/skills" ]]; then
skill_dirs=$(find "$ASSETS_DIR/skills" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
else
skill_dirs=""
fi
else
# Remote: list subdirectories in skills/ via API.
# The API returns entries — filter for directories (type "tree" in GitLab, "dir" in GitHub).
local api_url
api_url=$(derive_api_url "skills") || { warn "Cannot list remote skills directories."; return 0; }
local auth_header=""
[[ -n "$REPO_TOKEN" ]] && auth_header="Authorization: Bearer $REPO_TOKEN"
local response
response=$(curl -fsSL ${auth_header:+-H "$auth_header"} "$api_url" 2>/dev/null) || { warn "Could not list remote skills."; return 0; }
# GitLab uses "type":"tree" for dirs, GitHub uses "type":"dir"
skill_dirs=$(echo "$response" \
| grep -o '"name":"[^"]*"[^}]*"type":"\(tree\|dir\)"' \
| grep -o '"name":"[^"]*"' \
| sed 's/"name":"//;s/"//' || true)
fi
if [[ -z "$skill_dirs" ]]; then
warn "No custom skill folders found in assets/skills/."
else
while IFS= read -r skill; do
[[ -z "$skill" ]] && continue
info "$skill"
download_dir_to "skills/$skill" "$SKILLS_DIR/$skill"
count=$((count + 1))
done <<< "$skill_dirs"
ok "$count custom skill(s) installed."
fi
printf "\n"
ok "Restart your editor if skills don't appear."
}
# -- agents ───────────────────────────────────────────────────────────
@ -243,9 +312,9 @@ ${BOLD}USAGE${NC}
setup.sh <command> [platform]
${BOLD}COMMANDS${NC}
skills [platform] Install skills from a curated list
agents Install all agent prompt files (auto-discovered)
instructions Install all instruction files (auto-discovered)
skills [platform] Install registry + custom skills (auto-discovered)
agents Install all agent prompt files (auto-discovered)
instructions Install all instruction files (auto-discovered)
all [platform] Install everything at once
help Show this message
@ -268,7 +337,8 @@ ${BOLD}EXAMPLES${NC}
${BOLD}ENVIRONMENT VARIABLES${NC}
ASSETS_BASE_URL Base URL for remote downloads (required without clone)
AGENTS_DIR Install location for agents (default: ~/.copilot/agents)
AGENTS_DIR Install location for agents (default: ~/.agents/agents)
SKILLS_DIR Install location for custom skills (default: ~/.agents/skills)
INSTRUCTIONS_DIR Install location for instructions (default: ./instructions)
REPO_TOKEN Auth token for private repos (optional)

15
assets/skills/README.md Normal file
View File

@ -0,0 +1,15 @@
# Custom Skills
Drop skill folders here. Each folder should contain a `SKILL.md` file.
```
assets/skills/
my-custom-skill/
SKILL.md
references/ (optional)
another-skill/
SKILL.md
```
The setup script auto-discovers and installs every folder in this directory.
No manifest to update — just add the folder and push.

View File

@ -0,0 +1,143 @@
---
name: Swift Clean Architecture
description: File organization, layer separation, folder structures, and proactive refactoring
globs: ["**/*.swift"]
---
# Clean Architecture for Swift
**Separation of concerns is mandatory.** Code should be organized into distinct layers with clear responsibilities and dependencies flowing inward.
## File Organization Rules
### One Public Type Per File
Each file should contain exactly one public struct, class, or enum. Private supporting types may be included only if they are small and used exclusively by the main type.
### Keep Files Lean (300 Line Limit)
Aim for files under 300 lines. If a file exceeds this:
- Extract reusable sub-views into `Components/` folder
- Extract sheets/modals into `Sheets/` folder
- Extract complex logic into dedicated types
- Split private view structs into their own files
### No Duplicate Code
Before writing new code:
1. Search for existing implementations
2. Extract common patterns into reusable components
3. Consider protocol extraction for shared behavior
## Layer Responsibilities
| Layer | Contains | Depends On |
|-------|----------|------------|
| **Views** | SwiftUI views, UI components | State, Models |
| **State** | `@Observable` stores, view models | Models, Services |
| **Services** | Business logic, networking, persistence | Models |
| **Models** | Data types, entities, DTOs | Nothing |
| **Protocols** | Interfaces for services and stores | Models |
### Layer Rules
1. **Views are dumb renderers** - No business logic. Read state and call methods.
2. **State holds business logic** - Computations, validations, data transformations.
3. **Services are stateless** - Pure functions where possible. Injected via protocols.
4. **Models are simple** - Plain data types. No dependencies on UI or services.
## Folder Structures
Choose based on project size and team structure:
### Feature-First (Large Apps / Teams)
Best for: Multiple developers, features that could become SPM packages, complex apps.
```
App/
├── Shared/
│ ├── Design/ # Colors, typography, constants
│ ├── Protocols/ # Shared protocol definitions
│ ├── Services/ # Shared services (networking, auth)
│ └── Components/ # Shared UI components
└── Features/
├── Home/
│ ├── Views/
│ │ ├── HomeView.swift
│ │ ├── Components/
│ │ │ ├── HomeHeaderView.swift
│ │ │ └── HomeCardView.swift
│ │ └── Sheets/
│ │ └── HomeFilterSheet.swift
│ ├── Models/
│ │ └── HomeItem.swift
│ └── State/
│ └── HomeStore.swift
└── Profile/
├── Views/
├── Models/
└── State/
```
### Layer-First (Small/Medium Apps / Solo)
Best for: Solo developers, simpler apps, faster navigation.
```
App/
├── Design/ # Colors, typography, constants
├── Models/ # All data models
├── Protocols/ # All protocol definitions
├── Services/ # All services
├── State/ # All observable stores
└── Views/
├── Components/ # Shared reusable components
├── Sheets/ # Shared modal presentations
├── Home/ # Home feature views
└── Profile/ # Profile feature views
```
## Proactive Refactoring
**The agent will actively identify and suggest fixes for these violations:**
### File Size Violations
When a file exceeds 300 lines, suggest specific extractions:
- "This file is 450 lines. Consider extracting `SomePrivateView` (lines 200-280) to `Components/SomePrivateView.swift`"
### Duplicate Code Detection
When similar code patterns appear:
- "This filtering logic also exists in `OtherStore.swift`. Consider extracting to a shared protocol or utility."
### View Struct Proliferation
When a view file contains multiple private struct definitions:
- "This view has 5 private structs. Extract `HeaderView`, `RowView`, and `FooterView` to the `Components/` folder."
### Misplaced Business Logic
When business logic appears in views:
- "This validation logic belongs in the Store, not the View. Move `isValid` computed property to `FeatureStore`."
### Protocol Extraction Opportunities
When similar interfaces appear across types:
- "Both `UserService` and `TeamService` have similar fetch/save patterns. Consider a `Persistable` protocol."
## Naming Conventions
- **Views**: `FeatureNameView.swift`, `FeatureNameRowView.swift`
- **Stores**: `FeatureNameStore.swift`
- **Models**: `FeatureName.swift` or `FeatureNameModel.swift`
- **Services**: `FeatureNameService.swift`
- **Protocols**: `FeatureNameProviding.swift` or `Persistable.swift`

View File

@ -0,0 +1,224 @@
---
name: Swift Localization
description: Localization patterns using String Catalogs and modern APIs
globs: ["**/*.swift", "**/*.xcstrings"]
---
# Localization with String Catalogs
Use **String Catalogs** (`.xcstrings` files) for localization in modern Swift projects.
## Language Support
At minimum, support the base development language (typically English). Add additional locales based on your app's target markets. Common additions include Spanish and French variants.
## How String Catalogs Work
### Automatic Extraction
SwiftUI `Text` views with string literals are automatically extracted:
```swift
// Automatically added to String Catalog
Text("Hello, World!")
Text("Welcome back, \(user.name)!")
```
### Manual Extraction for Non-Text Strings
For strings outside of `Text` views, use `String(localized:)`:
```swift
// Use String(localized:) for alerts, buttons, accessibility
let title = String(localized: "Delete Item")
let message = String(localized: "Are you sure you want to delete this item?")
// With comments for translators
let greeting = String(
localized: "greeting_message",
defaultValue: "Hello!",
comment: "Greeting shown on the home screen"
)
```
## Never Use NSLocalizedString
```swift
// BAD - Old API
let text = NSLocalizedString("Hello", comment: "Greeting")
// GOOD - Modern API
let text = String(localized: "Hello")
```
## String Interpolation
String Catalogs handle interpolation automatically:
```swift
// In Swift
Text("You have \(count) items")
// In String Catalog, translators see:
// "You have %lld items"
// They can reorder: "Items: %lld" for languages that need different order
```
## Pluralization
Use automatic grammar agreement for plurals:
```swift
// Automatic pluralization
Text("^[\(count) item](inflect: true)")
// Result:
// count = 1: "1 item"
// count = 5: "5 items"
```
For complex pluralization rules, define in String Catalog with plural variants.
## Formatting Numbers, Dates, Currency
Always use formatters - they respect locale automatically:
```swift
// Numbers
Text(price, format: .currency(code: "USD"))
Text(percentage, format: .percent)
Text(count, format: .number)
// Dates
Text(date, format: .dateTime.month().day().year())
Text(date, format: .relative(presentation: .named))
// Measurements
let distance = Measurement(value: 5, unit: UnitLength.miles)
Text(distance, format: .measurement(width: .abbreviated))
```
## Localized String Keys
Use meaningful keys for complex strings:
```swift
// For simple UI text, use the text itself
Text("Settings")
Text("Cancel")
// For complex or contextual strings, use keys
Text("home.welcome.title") // Key in String Catalog
Text("profile.empty.message")
```
## Accessibility Labels
Localize all accessibility content:
```swift
Image(systemName: "heart.fill")
.accessibilityLabel(String(localized: "Favorite"))
Button { } label: {
Image(systemName: "trash")
}
.accessibilityLabel(String(localized: "Delete"))
.accessibilityHint(String(localized: "Removes this item permanently"))
```
## String Catalog Organization
### File Structure
```
App/
├── Localizable.xcstrings # Main strings
├── InfoPlist.xcstrings # Info.plist strings (app name, permissions)
└── Intents.xcstrings # Siri/Shortcuts strings (if applicable)
```
### Comments for Translators
Add comments to help translators understand context:
```swift
Text("Save", comment: "Button to save the current document")
Text("Save", comment: "Menu item to save all changes")
// These become separate entries with context
```
## Common Patterns
### Error Messages
```swift
enum AppError: LocalizedError {
case networkUnavailable
case invalidData
var errorDescription: String? {
switch self {
case .networkUnavailable:
return String(localized: "error.network.unavailable")
case .invalidData:
return String(localized: "error.data.invalid")
}
}
}
```
### Attributed Strings
```swift
var attributedGreeting: AttributedString {
var string = AttributedString(localized: "Welcome to **MyApp**!")
// Markdown formatting is preserved
return string
}
```
### Dynamic Strings from Server
For server-provided strings that need localization:
```swift
// Use a mapping approach
let serverKey = response.messageKey // e.g., "subscription_expired"
let localizedMessage = String(localized: String.LocalizationValue(serverKey))
```
## Testing Localization
### Preview with Different Locales
```swift
#Preview {
ContentView()
.environment(\.locale, Locale(identifier: "es-MX"))
}
#Preview {
ContentView()
.environment(\.locale, Locale(identifier: "fr-CA"))
}
```
### Pseudo-Localization
Enable in scheme to find truncation and layout issues:
1. Edit Scheme → Run → Options
2. Set "Application Language" to a pseudo-language
3. Look for strings that don't expand properly
## Export/Import for Translation
```bash
# Export for translators
xcodebuild -exportLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations
# Import translations
xcodebuild -importLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations/es-MX.xcloc
```

View File

@ -0,0 +1,258 @@
---
name: Swift Model Design
description: Model design patterns including single source of truth and computed properties
globs: ["**/*.swift"]
---
# Model Design Patterns
**Computed properties should be the single source of truth for derived data.**
## Single Source of Truth Principle
Never store data that can be computed from other stored data. This prevents sync bugs and simplifies maintenance.
### Name Fields Pattern
When a model has multiple name components, use a computed property for the display name:
```swift
@Model
final class Person {
var prefix: String = "" // "Dr.", "Mr.", etc.
var firstName: String = ""
var middleName: String = ""
var lastName: String = ""
var suffix: String = "" // "Jr.", "III", etc.
var nickname: String = ""
// GOOD - Computed from individual fields
var fullName: String {
var parts: [String] = []
if !prefix.isEmpty { parts.append(prefix) }
if !firstName.isEmpty { parts.append(firstName) }
if !middleName.isEmpty { parts.append(middleName) }
if !lastName.isEmpty { parts.append(lastName) }
if !suffix.isEmpty { parts.append(suffix) }
return parts.joined(separator: " ")
}
// For display with nickname
var displayName: String {
if !nickname.isEmpty {
return nickname
}
if !firstName.isEmpty {
return firstName
}
return fullName
}
// Plain format for export (no special formatting)
var vCardName: String {
[firstName, middleName, lastName]
.filter { !$0.isEmpty }
.joined(separator: " ")
}
// BAD - Stored displayName that can get out of sync
// var storedDisplayName: String // Never add this
}
```
### Benefits of Computed Properties
- **Always up to date**: Changes to individual fields are immediately reflected
- **No sync bugs**: No risk of stored value diverging from component fields
- **Simpler code**: No need to update derived values when editing source fields
- **Less storage**: No duplicate data in database
## Derived State Patterns
### Counts and Aggregates
```swift
@Model
final class Project {
var name: String = ""
@Relationship(deleteRule: .cascade)
var tasks: [Task]? = []
// GOOD - Computed counts
var taskCount: Int {
tasks?.count ?? 0
}
var completedTaskCount: Int {
tasks?.filter(\.isCompleted).count ?? 0
}
var progress: Double {
guard taskCount > 0 else { return 0 }
return Double(completedTaskCount) / Double(taskCount)
}
var isComplete: Bool {
taskCount > 0 && completedTaskCount == taskCount
}
// BAD - Stored counts that need manual updates
// var storedTaskCount: Int = 0
// var storedCompletedCount: Int = 0
}
```
### Status and State
```swift
@Model
final class Order {
var createdAt: Date = Date()
var paidAt: Date?
var shippedAt: Date?
var deliveredAt: Date?
var cancelledAt: Date?
// GOOD - Computed status
var status: OrderStatus {
if cancelledAt != nil { return .cancelled }
if deliveredAt != nil { return .delivered }
if shippedAt != nil { return .shipped }
if paidAt != nil { return .paid }
return .pending
}
var isActive: Bool {
cancelledAt == nil && deliveredAt == nil
}
var canCancel: Bool {
shippedAt == nil && cancelledAt == nil
}
}
enum OrderStatus: String, Codable {
case pending, paid, shipped, delivered, cancelled
}
```
### Validation State
```swift
@Model
final class UserProfile {
var email: String = ""
var phone: String = ""
var firstName: String = ""
var lastName: String = ""
// GOOD - Computed validation
var isEmailValid: Bool {
email.contains("@") && email.contains(".")
}
var isPhoneValid: Bool {
let digits = phone.filter(\.isNumber)
return digits.count >= 10
}
var isComplete: Bool {
!firstName.isEmpty && !lastName.isEmpty && isEmailValid
}
var validationErrors: [String] {
var errors: [String] = []
if firstName.isEmpty { errors.append("First name is required") }
if lastName.isEmpty { errors.append("Last name is required") }
if !isEmailValid { errors.append("Valid email is required") }
return errors
}
}
```
## When to Store vs Compute
### Store When:
- Data comes from an external source (API, user input)
- Computation is expensive and value is accessed frequently
- Historical accuracy matters (price at time of purchase)
- You need to query/filter by the value in database
### Compute When:
- Value is derived from other stored properties
- Value can change when source properties change
- Keeping values in sync would be error-prone
- Computation is fast (string concatenation, simple math)
## Caching Expensive Computations
For expensive computed values accessed frequently:
```swift
@Observable
@MainActor
final class AnalyticsStore {
private var items: [Item] = []
// Cache invalidation tracking
private var itemsVersion = 0
private var cachedStatsVersion = -1
private var cachedStats: Statistics?
var statistics: Statistics {
// Return cached if valid
if cachedStatsVersion == itemsVersion, let cached = cachedStats {
return cached
}
// Compute and cache
let stats = computeStatistics()
cachedStats = stats
cachedStatsVersion = itemsVersion
return stats
}
func updateItems(_ newItems: [Item]) {
items = newItems
itemsVersion += 1 // Invalidate cache
}
private func computeStatistics() -> Statistics {
// Expensive computation
}
}
```
## Identifiable and Hashable
Implement `Identifiable` for use with SwiftUI lists:
```swift
@Model
final class Item {
var id: UUID = UUID() // Or use @Attribute(.unique) if not using CloudKit
var name: String = ""
}
// SwiftData models are automatically Identifiable if they have an 'id' property
```
For value types used in Sets or as Dictionary keys:
```swift
struct Tag: Hashable, Codable {
let id: UUID
let name: String
// Hashable based on id only
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Tag, rhs: Tag) -> Bool {
lhs.id == rhs.id
}
}
```

View File

@ -0,0 +1,287 @@
---
name: Modern Swift
description: Modern Swift language patterns, concurrency, and API usage
globs: ["**/*.swift"]
---
# Modern Swift Patterns
Use modern Swift language features and avoid deprecated patterns.
## Concurrency
### Always Mark Observable Classes with @MainActor
```swift
@Observable
@MainActor
final class FeatureStore {
// All properties and methods run on main actor
}
```
### Use Modern Concurrency (No GCD)
```swift
// BAD - Old GCD patterns
DispatchQueue.main.async {
self.updateUI()
}
DispatchQueue.global().async {
let result = self.heavyWork()
DispatchQueue.main.async {
self.handle(result)
}
}
// GOOD - Modern concurrency
await MainActor.run {
updateUI()
}
Task.detached {
let result = await heavyWork()
await MainActor.run {
handle(result)
}
}
```
### Use Task.sleep(for:) Not nanoseconds
```swift
// BAD
try await Task.sleep(nanoseconds: 1_000_000_000)
// GOOD
try await Task.sleep(for: .seconds(1))
try await Task.sleep(for: .milliseconds(500))
```
### Strict Concurrency Compliance
```swift
// Ensure data crossing actor boundaries is Sendable
struct Item: Sendable {
let id: UUID
let name: String
}
// Use @unchecked Sendable only when you've manually verified thread safety
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
}
```
## Foundation APIs
### Use Modern URL APIs
```swift
// BAD - Deprecated patterns
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let file = docs.appendingPathComponent("data.json")
// GOOD - Modern APIs
let docs = URL.documentsDirectory
let file = docs.appending(path: "data.json")
```
### Use Modern Date Formatting
```swift
// BAD - DateFormatter
let formatter = DateFormatter()
formatter.dateStyle = .medium
let string = formatter.string(from: date)
// GOOD - format method
let string = date.formatted(.dateTime.month().day().year())
let relative = date.formatted(.relative(presentation: .named))
```
### Use Modern Number Formatting
```swift
// BAD - C-style or NumberFormatter
let string = String(format: "%.2f", price)
// GOOD - format method
let string = price.formatted(.currency(code: "USD"))
let percent = ratio.formatted(.percent.precision(.fractionLength(1)))
```
## String Handling
### Use localizedStandardContains for User Search
```swift
// BAD - Case-sensitive or manual lowercasing
items.filter { $0.name.lowercased().contains(query.lowercased()) }
// GOOD - Locale-aware, case/diacritic insensitive
items.filter { $0.name.localizedStandardContains(query) }
```
### String Interpolation Over Concatenation
```swift
// BAD
let message = "Hello, " + user.name + "!"
// GOOD
let message = "Hello, \(user.name)!"
```
## Type Safety
### Avoid Force Unwraps
```swift
// BAD
let value = dictionary["key"]!
let url = URL(string: urlString)!
// GOOD - Guard or optional binding
guard let value = dictionary["key"] else {
throw ValidationError.missingKey
}
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
```
### Avoid Force Casts
```swift
// BAD
let view = cell as! CustomCell
// GOOD
guard let view = cell as? CustomCell else {
assertionFailure("Expected CustomCell")
return
}
```
### Avoid Force Try
```swift
// BAD
let data = try! encoder.encode(object)
// GOOD - Handle the error
do {
let data = try encoder.encode(object)
} catch {
logger.error("Encoding failed: \(error)")
}
```
## Prefer Swift-Native Patterns
### Static Member Lookup
```swift
// BAD - Struct instances
.clipShape(RoundedRectangle(cornerRadius: 8))
// GOOD - Static member lookup
.clipShape(.rect(cornerRadius: 8))
```
### Result Builders Over Imperative Construction
```swift
// BAD - Imperative array building
var views: [AnyView] = []
if showHeader {
views.append(AnyView(HeaderView()))
}
views.append(AnyView(ContentView()))
// GOOD - ViewBuilder
@ViewBuilder
var content: some View {
if showHeader {
HeaderView()
}
ContentView()
}
```
### KeyPath Expressions
```swift
// BAD
items.map { $0.name }
items.sorted { $0.date < $1.date }
// GOOD
items.map(\.name)
items.sorted(using: KeyPathComparator(\.date))
```
## Collections
### Use First(where:) Over Filter().first
```swift
// BAD - Creates intermediate array
let item = items.filter { $0.id == targetId }.first
// GOOD - Short-circuits
let item = items.first { $0.id == targetId }
```
### Use Contains(where:) Over Filter().isEmpty
```swift
// BAD
let hasActive = !items.filter { $0.isActive }.isEmpty
// GOOD
let hasActive = items.contains { $0.isActive }
```
### Use Lazy for Chained Operations
```swift
// Process large collections efficiently
let result = largeArray
.lazy
.filter { $0.isValid }
.map { $0.transformed }
.prefix(10)
.map(Array.init) // Materialize only when needed
```
## Error Handling
### Prefer Typed Throws (Swift 6 / Xcode 16+)
Typed throws requires the **Swift 6 compiler** (shipped with Xcode 16). The feature works at any deployment target, but your project must compile with Swift 6. If your team hasn't migrated yet, continue using untyped `throws`.
```swift
// Swift 6 - Typed throws
enum NetworkError: Error {
case notFound
case unauthorized
case serverError(Int)
}
func fetch() throws(NetworkError) -> Data {
// ...
}
```
### Use Result for Async Callbacks (When Not Using async/await)
```swift
func fetch(completion: @escaping (Result<Data, NetworkError>) -> Void) {
// ...
}
```

View File

@ -0,0 +1,188 @@
---
name: Swift Protocol-Oriented Programming
description: Protocol-first architecture patterns for reusability and testability
globs: ["**/*.swift"]
---
# Protocol-Oriented Programming (POP)
**Protocol-first architecture is a priority.** When designing new features, always think about protocols and composition before concrete implementations.
## When Architecting New Code
1. **Start with the protocol** - Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol.
2. **Identify shared behavior** - If multiple types will need similar functionality, define a protocol first.
3. **Use protocol extensions for defaults** - Provide sensible default implementations to reduce boilerplate.
4. **Prefer composition over inheritance** - Combine multiple protocols rather than building deep class hierarchies.
## When Reviewing Existing Code
1. **Look for duplicated patterns** - Similar logic across files is a candidate for protocol extraction.
2. **Identify common interfaces** - Types that expose similar properties/methods should conform to a shared protocol.
3. **Check before implementing** - Search for existing protocols that could be adopted or extended.
4. **Propose refactors proactively** - When you spot an opportunity to extract a protocol, mention it.
## Protocol Design Guidelines
### Naming Conventions
Use capability-based suffixes:
- `-able`: `Persistable`, `Shareable`, `Validatable`
- `-ing`: `DataProviding`, `ErrorHandling`, `Loading`
- `-Provider`: `ContentProvider`, `DataProvider`
- `-Delegate`: `NavigationDelegate`, `FormDelegate`
### Keep Protocols Focused
Each protocol should represent one capability (Interface Segregation Principle):
```swift
// GOOD - Focused protocols
// Note: Swift provides Identifiable already — adopt it, don't redefine it.
protocol Nameable {
var displayName: String { get }
}
protocol Timestamped {
var createdAt: Date { get }
var updatedAt: Date { get }
}
// Compose as needed (Identifiable comes from Swift standard library)
struct User: Identifiable, Nameable, Timestamped {
let id: UUID
var displayName: String
var createdAt: Date
var updatedAt: Date
}
```
```swift
// BAD - Kitchen sink protocol
protocol Entity {
var id: UUID { get }
var displayName: String { get }
var createdAt: Date { get }
var updatedAt: Date { get }
func save() async throws
func delete() async throws
func validate() -> Bool
}
```
### Associated Types
Use sparingly. Prefer concrete types or generics at the call site when possible:
```swift
// Prefer this for simple cases
protocol DataFetching {
func fetch<T: Decodable>(from url: URL) async throws -> T
}
// Use associated types when the type is fundamental to the protocol
protocol Repository {
associatedtype Entity
func fetch(id: UUID) async throws -> Entity?
func save(_ entity: Entity) async throws
}
```
### Value vs Reference Semantics
Constrain to `AnyObject` only when reference semantics are required:
```swift
// Default - allows structs and classes
protocol Configurable {
mutating func configure(with options: Options)
}
// When you need reference semantics (delegates, observers)
protocol NavigationDelegate: AnyObject {
func didNavigate(to destination: Destination)
}
```
## Protocol Extensions
Provide default implementations for common behavior:
```swift
protocol Validatable {
var validationErrors: [String] { get }
var isValid: Bool { get }
}
extension Validatable {
var isValid: Bool {
validationErrors.isEmpty
}
}
```
## Dependency Injection with Protocols
Define protocols for services to enable testing:
```swift
protocol NetworkServiceProtocol {
func fetch<T: Decodable>(from url: URL) async throws -> T
}
// Production implementation
final class NetworkService: NetworkServiceProtocol { ... }
// Test mock
final class MockNetworkService: NetworkServiceProtocol { ... }
```
## Benefits
- **Reusability** - Shared protocols work across features
- **Testability** - Mock types can conform to protocols for unit testing
- **Flexibility** - New features can adopt existing protocols immediately
- **Maintainability** - Fix a bug in a protocol extension, fix it everywhere
- **Discoverability** - Protocols document the expected interface clearly
## Common Patterns
### Repository Pattern
```swift
protocol Repository {
associatedtype Entity: Identifiable
func fetch(id: Entity.ID) async throws -> Entity?
func fetchAll() async throws -> [Entity]
func save(_ entity: Entity) async throws
func delete(_ entity: Entity) async throws
}
```
### Service Pattern
```swift
protocol AuthServiceProtocol {
var isAuthenticated: Bool { get }
func signIn(email: String, password: String) async throws
func signOut() async throws
}
```
### Coordinator/Navigation Pattern
```swift
protocol NavigationCoordinating: AnyObject {
func navigate(to destination: Destination)
func dismiss()
func presentSheet(_ sheet: SheetType)
}
```

View File

@ -0,0 +1,359 @@
---
name: SwiftUI Accessibility
description: Dynamic Type support and VoiceOver accessibility implementation
globs: ["**/*.swift"]
---
# Accessibility: Dynamic Type and VoiceOver
Accessibility is not optional. All apps must support Dynamic Type and VoiceOver.
## Dynamic Type
### Always Support Dynamic Type
Use system text styles that scale automatically:
```swift
// GOOD - Scales with Dynamic Type
Text("Title")
.font(.title)
Text("Body text")
.font(.body)
Text("Caption")
.font(.caption)
```
### Use @ScaledMetric for Custom Dimensions
When you need custom sizes that should scale with Dynamic Type:
```swift
struct CustomCard: View {
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = 24
@ScaledMetric(relativeTo: .body) private var spacing: CGFloat = 12
@ScaledMetric(relativeTo: .title) private var headerHeight: CGFloat = 44
var body: some View {
VStack(spacing: spacing) {
Image(systemName: "star")
.font(.system(size: iconSize))
Text("Content")
}
}
}
```
### Choose Appropriate relativeTo Styles
Match the scaling behavior to the content's purpose:
| Content Type | relativeTo |
|-------------|-----------|
| Body content spacing | `.body` |
| Title decorations | `.title` |
| Caption elements | `.caption` |
| Large headers | `.largeTitle` |
### Fixed Sizes (Use Sparingly)
Only use fixed sizes when absolutely necessary, and document the reason:
```swift
// Fixed size for app icon badge - must match system badge size
private let badgeSize: CGFloat = 24 // Fixed: matches system notification badge
// Fixed for external API requirements
private let avatarUploadSize: CGFloat = 256 // Fixed: server requires exactly 256x256
```
### Prefer System Text Styles
```swift
// GOOD - System styles
.font(.body)
.font(.headline)
.font(.title)
.font(.caption)
// AVOID - Custom sizes that don't scale
.font(.system(size: 14))
// IF you must use custom sizes, use ScaledMetric
@ScaledMetric private var customSize: CGFloat = 14
.font(.system(size: customSize))
```
## VoiceOver
### Accessibility Labels
All interactive elements must have meaningful labels:
```swift
// GOOD - Descriptive label
Button { } label: {
Image(systemName: "trash")
}
.accessibilityLabel("Delete item")
// GOOD - Context-aware label
Button { } label: {
Image(systemName: "heart.fill")
}
.accessibilityLabel(item.isFavorite ? "Remove from favorites" : "Add to favorites")
```
### Accessibility Values
Use for dynamic state that changes:
```swift
Slider(value: $volume)
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(volume * 100)) percent")
Toggle(isOn: $isEnabled) {
Text("Notifications")
}
.accessibilityValue(isEnabled ? "On" : "Off")
```
### Accessibility Hints
Describe what happens when the user interacts:
```swift
Button("Submit") { }
.accessibilityLabel("Submit order")
.accessibilityHint("Double-tap to place your order and proceed to payment")
NavigationLink(value: item) {
ItemRow(item: item)
}
.accessibilityHint("Opens item details")
```
### Accessibility Traits
Use traits to convey element type and behavior:
```swift
// Button trait (usually automatic)
Text("Tap me")
.onTapGesture { }
.accessibilityAddTraits(.isButton)
// Header trait for section headers
Text("Settings")
.font(.headline)
.accessibilityAddTraits(.isHeader)
// Selected state
ItemRow(item: item)
.accessibilityAddTraits(isSelected ? .isSelected : [])
// Image trait removal for decorative images
Image("decorative-background")
.accessibilityHidden(true)
```
### Hide Decorative Elements
Hide elements that don't provide meaningful information:
```swift
// Decorative separator
Divider()
.accessibilityHidden(true)
// Background decoration
Image("pattern")
.accessibilityHidden(true)
// Redundant icon next to text
HStack {
Image(systemName: "envelope")
.accessibilityHidden(true) // Label conveys the meaning
Text("Email")
}
```
### Group Related Elements
Reduce navigation complexity by grouping related content:
```swift
// GOOD - Single VoiceOver element
HStack {
Image(systemName: "person")
VStack(alignment: .leading) {
Text(user.name)
Text(user.email)
.font(.caption)
}
}
.accessibilityElement(children: .combine)
// OR create a completely custom accessibility representation
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(user.name), \(user.email)")
```
### Accessibility Actions
Add custom actions for complex interactions:
```swift
ItemRow(item: item)
.accessibilityAction(named: "Delete") {
deleteItem(item)
}
.accessibilityAction(named: "Edit") {
editItem(item)
}
.accessibilityAction(named: "Share") {
shareItem(item)
}
```
### Accessibility Announcements
Announce important state changes:
```swift
func completeTask() {
task.isCompleted = true
// Announce the change
AccessibilityNotification.Announcement("Task completed")
.post()
}
func showError(_ message: String) {
errorMessage = message
// Announce errors immediately
AccessibilityNotification.Announcement(message)
.post()
}
```
### Accessibility Focus
Control focus for important UI changes:
```swift
struct ContentView: View {
@AccessibilityFocusState private var isSearchFocused: Bool
@State private var showingSearch = false
var body: some View {
VStack {
if showingSearch {
TextField("Search", text: $searchText)
.accessibilityFocused($isSearchFocused)
}
Button("Search") {
showingSearch = true
isSearchFocused = true // Move focus to search field
}
}
}
}
```
## Common Patterns
### Cards and List Items
```swift
struct ItemCard: View {
let item: Item
var body: some View {
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
HStack {
Label("\(item.likes)", systemImage: "heart")
Label("\(item.comments)", systemImage: "bubble.right")
}
.font(.caption)
}
// Combine into single VoiceOver element
.accessibilityElement(children: .combine)
// Add meaningful summary
.accessibilityLabel("\(item.title), \(item.subtitle)")
.accessibilityValue("\(item.likes) likes, \(item.comments) comments")
}
}
```
### Interactive Charts
```swift
Chart {
ForEach(data) { point in
LineMark(x: .value("Date", point.date), y: .value("Value", point.value))
}
}
.accessibilityLabel("Sales chart")
.accessibilityValue("Showing data from \(startDate) to \(endDate)")
.accessibilityHint("Swipe up or down to hear individual data points")
.accessibilityChartDescriptor(self)
```
### Custom Controls
```swift
struct RatingControl: View {
@Binding var rating: Int
var body: some View {
HStack {
ForEach(1...5, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.onTapGesture { rating = star }
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Rating")
.accessibilityValue("\(rating) of 5 stars")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
rating = min(5, rating + 1)
case .decrement:
rating = max(1, rating - 1)
@unknown default:
break
}
}
}
}
```
## Testing Accessibility
### Enable VoiceOver in Simulator
1. Settings → Accessibility → VoiceOver
2. Or use Accessibility Inspector (Xcode → Open Developer Tool)
### Audit Checklist
- [ ] All interactive elements have labels
- [ ] Dynamic content announces changes
- [ ] Decorative elements are hidden
- [ ] Text scales with Dynamic Type
- [ ] Touch targets are at least 44pt
- [ ] Color is not the only indicator of state
- [ ] Groups reduce navigation complexity

View File

@ -0,0 +1,373 @@
---
name: Modern SwiftUI
description: Modern SwiftUI API usage and best practices
globs: ["**/*.swift"]
---
# Modern SwiftUI Patterns
Use modern SwiftUI APIs and avoid deprecated patterns.
## Styling APIs
### Use foregroundStyle() Not foregroundColor()
```swift
// BAD - Deprecated
Text("Hello")
.foregroundColor(.blue)
// GOOD
Text("Hello")
.foregroundStyle(.blue)
// GOOD - With gradients
Text("Hello")
.foregroundStyle(.linearGradient(colors: [.blue, .purple], startPoint: .leading, endPoint: .trailing))
```
### Use clipShape(.rect()) Not cornerRadius()
```swift
// BAD - Deprecated
Image("photo")
.cornerRadius(12)
// GOOD
Image("photo")
.clipShape(.rect(cornerRadius: 12))
// GOOD - With specific corners
Image("photo")
.clipShape(.rect(cornerRadii: .init(topLeading: 12, topTrailing: 12)))
```
### Use bold() Not fontWeight(.bold)
```swift
// Less preferred
Text("Title")
.fontWeight(.bold)
// Preferred
Text("Title")
.bold()
```
## Navigation
### Use NavigationStack with navigationDestination
```swift
// BAD - Old NavigationView with NavigationLink
NavigationView {
List(items) { item in
NavigationLink(destination: DetailView(item: item)) {
ItemRow(item: item)
}
}
}
// GOOD - NavigationStack with typed destinations
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
```
### Use NavigationPath for Programmatic Navigation
```swift
@Observable
@MainActor
final class NavigationStore {
var path = NavigationPath()
func navigate(to item: Item) {
path.append(item)
}
func popToRoot() {
path.removeLast(path.count)
}
}
```
## Observable Pattern
### Use @Observable Not ObservableObject
```swift
// BAD - Old Combine-based pattern
class FeatureStore: ObservableObject {
@Published var items: [Item] = []
}
struct FeatureView: View {
@StateObject var store = FeatureStore()
// or @ObservedObject
}
// GOOD - Modern Observation
@Observable
@MainActor
final class FeatureStore {
var items: [Item] = []
}
struct FeatureView: View {
@State var store = FeatureStore()
// or for external injection:
@Bindable var store: FeatureStore
}
```
## Event Handling
### Use Button Not onTapGesture()
```swift
// BAD - No accessibility, no button styling
Text("Submit")
.onTapGesture {
submit()
}
// GOOD - Proper button semantics
Button("Submit") {
submit()
}
// When you need tap location/count, onTapGesture is acceptable
SomeView()
.onTapGesture(count: 2) { location in
handleDoubleTap(at: location)
}
```
### Use Two-Parameter onChange()
```swift
// BAD - Deprecated single parameter
.onChange(of: searchText) { newValue in
search(for: newValue)
}
// GOOD - Two parameter version
.onChange(of: searchText) { oldValue, newValue in
search(for: newValue)
}
// GOOD - When you don't need old value
.onChange(of: searchText) { _, newValue in
search(for: newValue)
}
```
## Layout
### Avoid UIScreen.main.bounds
```swift
// BAD - Hardcoded screen size
let width = UIScreen.main.bounds.width
// GOOD - GeometryReader when needed
GeometryReader { geometry in
SomeView()
.frame(width: geometry.size.width * 0.8)
}
// BETTER - containerRelativeFrame (iOS 17+)
SomeView()
.containerRelativeFrame(.horizontal) { size, _ in
size * 0.8
}
```
### Prefer containerRelativeFrame Over GeometryReader
```swift
// Avoid GeometryReader when possible
ScrollView(.horizontal) {
LazyHStack {
ForEach(items) { item in
ItemCard(item: item)
.containerRelativeFrame(.horizontal, count: 3, spacing: 16)
}
}
}
```
## View Composition
### Extract to View Structs Not Computed Properties
```swift
// BAD - Computed properties for view composition
struct ContentView: View {
private var header: some View {
HStack {
Text("Title")
Spacer()
Button("Action") { }
}
}
var body: some View {
VStack {
header
// ...
}
}
}
// GOOD - Separate View struct
struct HeaderView: View {
let title: String
let action: () -> Void
var body: some View {
HStack {
Text(title)
Spacer()
Button("Action", action: action)
}
}
}
```
### Avoid AnyView
```swift
// BAD - Type erasure loses optimization
func makeView(for type: ViewType) -> AnyView {
switch type {
case .list: return AnyView(ListView())
case .grid: return AnyView(GridView())
}
}
// GOOD - @ViewBuilder
@ViewBuilder
func makeView(for type: ViewType) -> some View {
switch type {
case .list: ListView()
case .grid: GridView()
}
}
```
## Lists and ForEach
### Use Identifiable Conformance for ForEach
Let `ForEach` use `Identifiable` conformance directly — don't bypass it with manual key paths or offset-based identity.
```swift
// BAD - offset-based id breaks animations when items change
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
// ...
}
// BAD - index-based id has the same animation problem
ForEach(items.indices, id: \.self) { index in
let item = items[index]
// ...
}
// GOOD - Identifiable items work directly (protocol-based)
ForEach(items) { item in
// ...
}
// GOOD - When index is also needed, use enumerated keyed on the item's identity
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
// Stable identity comes from item.id (Identifiable conformance),
// not from the offset — so animations and diffing work correctly.
}
```
### Hide Scroll Indicators
```swift
// Use scrollIndicators modifier
ScrollView {
// content
}
.scrollIndicators(.hidden)
```
## Button Labels with Images
### Always Include Text with Image Buttons
```swift
// BAD - No accessibility label
Button {
addItem()
} label: {
Image(systemName: "plus")
}
// GOOD - Text alongside image
Button {
addItem()
} label: {
Label("Add Item", systemImage: "plus")
}
// GOOD - If you only want to show the image
Button {
addItem()
} label: {
Label("Add Item", systemImage: "plus")
.labelStyle(.iconOnly)
}
```
## Design Constants
### Never Use Raw Numeric Literals
```swift
// BAD
.padding(16)
.clipShape(.rect(cornerRadius: 12))
.opacity(0.7)
// GOOD - Use design constants
.padding(Design.Spacing.medium)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
.opacity(Design.Opacity.strong)
```
### Never Use Inline Colors
```swift
// BAD
.foregroundStyle(Color(red: 0.2, green: 0.4, blue: 0.8))
.background(Color(hex: "#3366CC"))
// GOOD - Semantic color names
.foregroundStyle(Color.Theme.primary)
.background(Color.Background.secondary)
```
## Image Rendering
### Prefer ImageRenderer Over UIGraphicsImageRenderer
```swift
// For SwiftUI → Image conversion
let renderer = ImageRenderer(content: MyView())
if let uiImage = renderer.uiImage {
// use image
}
```

View File

@ -0,0 +1,228 @@
---
name: SwiftUI MVVM
description: View/State separation patterns for SwiftUI with Observable stores
globs: ["**/*.swift"]
---
# View/State Separation (MVVM-lite)
**Views should be "dumb" renderers.** All business logic belongs in stores or dedicated view models.
## What Belongs in State/Store
- **Business logic**: Calculations, validations, rules
- **Computed properties based on data**: Hints, recommendations, derived values
- **State checks**: `canSubmit`, `isLoading`, `hasError`
- **Data transformations**: Filtering, sorting, aggregations
- **Side effects**: Network calls, persistence, analytics
## What is Acceptable in Views
- **Pure UI layout logic**: Adaptive layouts based on size class
- **Visual styling**: Color selection based on state
- **@ViewBuilder sub-views**: Breaking up complex layouts (keep in same file if small)
- **Accessibility labels**: Combining data into accessible descriptions
- **Simple conditionals for UI**: `if isExpanded { ... }`
## Store Pattern
```swift
@Observable
@MainActor
final class FeatureStore {
// MARK: - State
private(set) var items: [Item] = []
private(set) var isLoading = false
private(set) var error: Error?
// MARK: - Computed Properties (Business Logic)
var isEmpty: Bool { items.isEmpty }
var itemCount: Int { items.count }
var canSubmit: Bool { !selectedItems.isEmpty && !isLoading }
var filteredItems: [Item] {
guard !searchText.isEmpty else { return items }
return items.filter { $0.name.localizedStandardContains(searchText) }
}
// MARK: - User Input
var searchText = ""
var selectedItems: Set<Item.ID> = []
// MARK: - Dependencies
private let service: FeatureServiceProtocol
init(service: FeatureServiceProtocol) {
self.service = service
}
// MARK: - Actions
func load() async {
isLoading = true
defer { isLoading = false }
do {
items = try await service.fetchItems()
} catch {
self.error = error
}
}
func submit() async {
guard canSubmit else { return }
// ...
}
}
```
## View Pattern
```swift
struct FeatureView: View {
@Bindable var store: FeatureStore
var body: some View {
List(store.filteredItems) { item in
ItemRow(item: item)
}
.searchable(text: $store.searchText)
.overlay {
if store.isEmpty {
ContentUnavailableView("No Items", systemImage: "tray")
}
}
.toolbar {
Button("Submit") {
Task { await store.submit() }
}
.disabled(!store.canSubmit)
}
.task {
await store.load()
}
}
}
```
## Bad vs Good Examples
### Validation Logic
```swift
// BAD - Business logic in view
struct MyView: View {
@Bindable var store: FeatureStore
private var isValid: Bool {
!store.name.isEmpty && store.email.contains("@")
}
var body: some View {
Button("Save") { }
.disabled(!isValid)
}
}
// GOOD - Logic in Store, view just reads
// In FeatureStore:
var isValid: Bool {
!name.isEmpty && email.contains("@")
}
// In View:
Button("Save") { store.save() }
.disabled(!store.isValid)
```
### Filtering Logic
```swift
// BAD - Filtering in view
struct ListView: View {
@Bindable var store: ListStore
var filteredItems: [Item] {
store.items.filter { $0.isActive && $0.category == selectedCategory }
}
var body: some View {
List(filteredItems) { ... }
}
}
// GOOD - Filtering in Store
// In ListStore:
var filteredItems: [Item] {
items.filter { $0.isActive && $0.category == selectedCategory }
}
// In View:
List(store.filteredItems) { ... }
```
### Error Handling
```swift
// BAD - Error formatting in view
struct ProfileView: View {
@Bindable var store: ProfileStore
var errorMessage: String? {
guard let error = store.error else { return nil }
if let networkError = error as? NetworkError {
switch networkError {
case .notFound: return "Profile not found"
case .unauthorized: return "Please sign in"
default: return "Something went wrong"
}
}
return error.localizedDescription
}
}
// GOOD - Error message in Store
// In ProfileStore:
var errorMessage: String? {
guard let error else { return nil }
return ErrorFormatter.message(for: error)
}
// In View:
if let message = store.errorMessage {
Text(message)
}
```
## When to Create a Separate ViewModel
Use a dedicated ViewModel (instead of a Store) when:
1. The view has complex local state that doesn't need to persist
2. You need to transform data from multiple stores for a single view
3. The view has form validation with many fields
4. You're wrapping a UIKit component that needs state management
```swift
@Observable
@MainActor
final class FormViewModel {
// Form-specific state
var firstName = ""
var lastName = ""
var email = ""
// Validation
var isValid: Bool {
!firstName.isEmpty && !lastName.isEmpty && email.contains("@")
}
var firstNameError: String? {
firstName.isEmpty ? "First name is required" : nil
}
// Submit creates domain object
func createUser() -> User {
User(firstName: firstName, lastName: lastName, email: email)
}
}
```

View File

@ -1,6 +1,6 @@
# Android AI Setup
You are here: [AI Docs Home](index.md) > Android Setup
You are here: [AI Docs Home](../index.md) > Android Setup
## Contents
- Setup
@ -9,29 +9,10 @@ You are here: [AI Docs Home](index.md) > Android Setup
- Android Troubleshooting
- Next Steps
## Setup: VS Code + GitHub Copilot Extension
### Prerequisites
- VS Code installed and up to date.
- GitHub account with Copilot Enterprise access.
## Setup: VS Code + GitHub Copilot
Before starting, complete the [VS Code Initial Setup](../vscode-setup.md) for Copilot and Copilot Chat. This covers installing VS Code, GitHub Copilot, signing in, and verifying suggestions.
### Install and Sign In (High-Level)
1. Install the GitHub Copilot extension.
2. Sign in with your GitHub account.
3. Confirm Copilot is enabled in VS Code.
4. Run a simple prompt to verify suggestions appear.
### Example Prompt (VS Code)
Open a Kotlin file and add:
Example prompt:
```text
// Create a Kotlin data class for a user profile with name and email.
```
### Verification Steps
- Open a Kotlin file and start a small function.
- Confirm inline suggestions appear.
- Open Copilot Chat and ask a short question.
Once Copilot is working, continue below for Android-specific guidance.
## Android-Specific Guidance
- Ask for Kotlin/Java patterns with small, bounded tasks.
@ -45,7 +26,7 @@ Refactor this repository to reduce duplication. Keep the public API the same and
```
## MCP For Android (High-Level)
MCP tools can automate Android tasks like builds, tests, and diagnostics. The exact tool depends on your team setup.
MCP tools can automate Android tasks like builds, tests, and diagnostics. Complete the [VS Code Initial Setup](../vscode-setup.md) first, then follow your teams approved Android MCP tool instructions here.
### What To Add Here
When you identify the approved Android MCP tool(s), add:
@ -72,6 +53,6 @@ Refactor this file to reduce duplication without changing behavior.
- Multiple AI extensions competing for suggestions.
## Next Steps
- For workflow patterns, read [Cross-Platform AI Usage](cross-platform.md).
- For safety rules, read [Governance, Privacy, and Policy](governance.md).
- For cost guidance, read [Usage and Token Budgeting](usage-tokens.md).
- For workflow patterns, read [Cross-Platform AI Usage](../cross-platform.md).
- For safety rules, read [Governance, Privacy, and Policy](../governance.md).
- For cost guidance, read [Usage and Token Budgeting](../usage-tokens.md).

View File

@ -1,175 +0,0 @@
# Cross-Platform AI Usage
You are here: [AI Docs Home](index.md) > Cross-Platform AI Usage
## Contents
- Instructions (Always-On Rules)
- Agents.md
- Agents vs Skills
- Prompting Patterns
- Prompting Anti-Patterns
- Plan-First Workflow
- Starter Prompts
- MCP Overview
- Next Steps
## Instructions (Always-On Rules)
Instructions are repo-scoped rules that auto-apply based on file patterns. They are always on and do not need to be invoked.
Where they live:
- Repo instructions directory (for example, [assets/instructions/](../../assets/instructions/))
- Editor or org-level instruction files when configured by your team
## Agents.md
### What It Is
Agents define a structured workflow so tasks are broken into clear steps, with explicit inputs and outputs.
### How to Use It
1. Read [Agents.md](../../Agents.md).
2. Choose a workflow that matches your task.
3. Provide the inputs in a short, bounded request.
### Example Request
Example prompt:
```text
Goal: Improve readability in this module
Inputs: src/foo/Bar.kt, keep behavior the same
Output: A small refactor and a short summary
Verification: No tests needed
```
### When to Use Agents vs Chat
- Use agents for multi-step tasks like refactors, doc audits, or migrations.
- Use chat for quick questions or one-off explanations.
## Agents vs Skills (Quick Compare)
- Agents are full modes/personas that control behavior end-to-end. They can be stored in a repo or a user-level folder.
- Skills are focused workflows you load for specific tasks. They are typically installed globally via the approved skills directory.
## Chat Modes In Copilot
Ask: Quick Q and A or summaries.
Edit: Targeted file edits with constraints.
Plan: Planning only, no edits yet.
Agent: Multi-step work across files or tools.
Example prompts:
```text
Ask: Explain this error message and list the top 3 likely fixes.
Edit: In this file, extract a helper function for validation and keep behavior the same.
Plan: Provide a 5-step plan to split this class into smaller components. Wait for approval.
Agent: Refactor the service layer, update tests, run the test task, and summarize results.
```
### Request Template
Use this structure to get consistent results:
```text
Goal: <one sentence>
Inputs: <files, constraints, context>
Output: <expected deliverable>
Verification: <tests or checks>
```
## Prompting Patterns
Example prompts:
```text
Refactors: Refactor this file to improve readability without changing behavior.
Tests: Add unit tests for this service. Keep the existing public API.
Debugging: Explain this error and list likely fixes in order.
Understanding code: Summarize what this module does and call out risks.
```
## Prompting Anti-Patterns (And Fixes)
Common mistakes and better alternatives:
Example prompts:
```text
Too broad: Fix everything in this project.
Better: Refactor this file to remove duplication. Do not change behavior.
Too vague: Make this code better.
Better: Improve naming and add comments only where logic is complex.
Missing inputs: Update the service to handle retries.
Better: Update ServiceA in src/service/ServiceA.kt to retry 2 times on 5xx. Keep API the same.
```
### Example: Too Broad vs Scoped
Example prompts:
```text
Too broad: Fix everything in this project.
Scoped: Refactor this file to remove duplication. Do not change behavior.
```
## Plan-First Workflow
1. Ask for a short plan.
2. Approve or adjust the plan.
3. Ask for targeted changes.
4. Verify with tests or review.
### Example Plan Request
Example prompt:
```text
Provide a 5-step plan to refactor this module. Wait for approval before edits.
```
## Starter Prompts (Copy/Paste)
Use these when you are not sure where to begin:
Example prompts:
```text
Summarize this file in 5 bullets and list 2 risks.
Refactor this function to remove duplication. Keep behavior the same.
List tests I should add for this change.
Explain this error and propose the top 3 fixes.
Create a 5-step plan to split this class into smaller components.
```
## Connecting Skills
- Use skills for tasks with known workflows.
- Combine multiple skills when a task spans domains.
- Pass concise context between steps to reduce repetition.
## Efficiency Tips
- Keep prompts small and scoped.
- Reuse context from earlier steps instead of repeating it.
- Ask for summaries before asking for edits.
### Example Summary Request
Example prompt:
```text
Summarize the decisions so far in 6 bullets so I can start a new chat.
```
## MCP (Model Context Protocol) - Cross-Platform Overview
MCP is an open standard that lets tools and agents interact with real systems. In plain language, MCP lets the assistant "do" things (like run builds or fetch logs) instead of just talking about them.
### How MCP Helps In A Workflow
Think of MCP as a set of safe, structured buttons the assistant can press. You ask a question, the assistant calls a tool, and it returns a clear result.
### Common MCP Use Cases (All Platforms)
- Run builds and tests
- Fetch logs or diagnostics
- Query project configuration
- Generate previews or reports
### Example Workflow
1. Ask the assistant to run a build.
2. The MCP tool runs it and returns the result.
3. You ask for a summary and next steps.
### Example Prompt
Example prompt:
```text
Use the build MCP tool to run the build and summarize any errors.
```
### Where To Learn More
- iOS examples: see [iOS Setup](ios.md)
- Android examples: see [Android Setup](android.md)
## Next Steps
- If you need skills guidance, read [Skills Library](skills.md).
- For safety rules, read [Governance, Privacy, and Policy](governance.md).
- If setup is not working, follow [Troubleshooting and FAQ](troubleshooting.md).

View File

@ -0,0 +1,23 @@
# Cross-Platform AI Usage (Overview)
You are here: [AI Docs Home](../index.md) > Cross-Platform AI Usage
This page gives a high-level overview of cross-platform AI usage, agent workflows, and prompting patterns. For detailed patterns, anti-patterns, and workflows, see the linked guides below.
## Quick Links
- [Prompting Patterns](prompting-patterns.md)
- [Prompting Anti-Patterns](prompting-antipatterns.md)
- [Plan-First Workflow](plan-first-workflow.md)
- [Starter Prompts](starter-prompts.md)
- [MCP Overview](mcp-overview.md)
## Repo Instructions (Always-On Rules)
Instructions are repo-scoped rules that auto-apply based on file patterns. See your repos instructions directory (e.g., [assets/instructions/](../../assets/instructions/)).
## Agents and Skills
- Agents: Use for multi-step, structured tasks (see [Agents.md](../../Agents.md)).
- Skills: Use for focused, repeatable workflows (see [Skills Library](../skills.md)).
## Next Steps
- For safety rules, read [Governance, Privacy, and Policy](../governance.md)
- If setup is not working, follow [Troubleshooting and FAQ](../troubleshooting.md)

View File

@ -0,0 +1,24 @@
# MCP (Model Context Protocol) - Cross-Platform Overview
MCP is an open standard that lets tools and agents interact with real systems. In plain language, MCP lets the assistant "do" things (like run builds or fetch logs) instead of just talking about them.
## How MCP Helps In A Workflow
Think of MCP as a set of safe, structured buttons the assistant can press. You ask a question, the assistant calls a tool, and it returns a clear result.
## Common MCP Use Cases (All Platforms)
- Run builds and tests
- Fetch logs or diagnostics
- Query project configuration
- Generate previews or reports
## Example Workflow
1. Ask the assistant to run a build.
2. The MCP tool runs it and returns the result.
3. You ask for a summary and next steps.
## Example Prompt
Use the build MCP tool to run the build and summarize any errors.
## Where To Learn More
- iOS examples: see [iOS Setup](../ios/ios.md)
- Android examples: see [Android Setup](../android/android.md)

View File

@ -0,0 +1,10 @@
# Plan-First Workflow
1. Ask for a short plan.
2. Approve or adjust the plan.
3. Ask for targeted changes.
4. Verify with tests or review.
### Example Plan Request
Provide a 5-step plan to refactor this module. Wait for approval before edits.

View File

@ -0,0 +1,17 @@
# Prompting Anti-Patterns (And Fixes)
Common mistakes and better alternatives:
**Too broad:** Fix everything in this project.
**Better:** Refactor this file to remove duplication. Do not change behavior.
**Too vague:** Make this code better.
**Better:** Improve naming and add comments only where logic is complex.
**Missing inputs:** Update the service to handle retries.
**Better:** Update ServiceA in src/service/ServiceA.kt to retry 2 times on 5xx. Keep API the same.
### Example: Too Broad vs Scoped
Too broad: Fix everything in this project.
Scoped: Refactor this file to remove duplication. Do not change behavior.

View File

@ -0,0 +1,8 @@
# Prompting Patterns
Example prompts:
- Refactor this file to improve readability without changing behavior.
- Add unit tests for this service. Keep the existing public API.
- Explain this error and list likely fixes in order.
- Summarize what this module does and call out risks.

View File

@ -0,0 +1,9 @@
# Starter Prompts (Copy/Paste)
Use these when you are not sure where to begin:
- Summarize this file in 5 bullets and list 2 risks.
- Refactor this function to remove duplication. Keep behavior the same.
- List tests I should add for this change.
- Explain this error and propose the top 3 fixes.
- Create a 5-step plan to split this class into smaller components.

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -5,28 +5,25 @@ You are here: AI Docs Home
## Contents
- Guided Paths
- Start Here
- Platform Setup
- [VS Code Initial Setup](vscode-setup.md)
- [iOS Setup](ios/ios.md)
- [Android Setup](android/android.md)
- Use AI Day-To-Day
- Safety And Cost
- iOS MCP Details
Welcome. This section is the entry point for AI enablement. It assumes no prior knowledge and is safe to follow step-by-step.
## Who This Is For
You can be an expert engineer and still be new to AI. This guide explains every step and includes examples to keep you from guessing.
## How To Use This Guide
Follow the steps in order. Do not skip ahead if this is your first time.
### Example Path (New iOS Engineer)
1. Read [AI Overview](overview.md)
2. Follow [iOS Setup](ios.md)
2. Follow [iOS Setup](ios/ios.md)
3. Skim [Usage and Token Budgeting](usage-tokens.md)
4. Return to [Cross-Platform AI Usage](cross-platform.md)
## Guided Paths (Pick One)
- New to AI: [AI Overview](overview.md) -> [Cross-Platform AI Usage](cross-platform.md) -> [Usage and Token Budgeting](usage-tokens.md)
- iOS setup: [AI Overview](overview.md) -> [iOS Setup](ios.md) -> [XcodeBuildMCP (iOS)](ios-xcodebuildmcp.md)
- Android setup: [AI Overview](overview.md) -> [Android Setup](android.md) -> [Cross-Platform AI Usage](cross-platform.md)
- iOS setup: [AI Overview](overview.md) -> [iOS Setup](ios/ios.md) -> [MCP](ios/ios-mcp.md)
- Android setup: [AI Overview](overview.md) -> [Android Setup](android/android.md)
- Contributors: [AI Overview](overview.md) -> [Governance, Privacy, and Policy](governance.md) -> [Troubleshooting and FAQ](troubleshooting.md)
## If You Are New
@ -40,20 +37,19 @@ If you will edit these docs, start with the "Key Repo Files" section in the over
## Start Here
- [AI Overview](overview.md)
- [iOS Setup](ios.md)
- [Android Setup](android.md)
- [iOS Setup](ios/ios.md)
- [Android Setup](android/android.md)
## Use AI Day-To-Day
- [Cross-Platform AI Usage](cross-platform.md) - Agents, instructions, prompting patterns, MCP overview, and workflow tips
- [Cross-Platform AI Usage](crossplatform/cross-platform.md) - Overview, agent workflows, and navigation to:
- [Prompting Patterns](crossplatform/prompting-patterns.md)
- [Prompting Anti-Patterns](crossplatform/prompting-antipatterns.md)
- [Plan-First Workflow](crossplatform/plan-first-workflow.md)
- [Starter Prompts](crossplatform/starter-prompts.md)
- [MCP Overview](crossplatform/mcp-overview.md)
- [Skills Library](skills.md)
## Safety And Cost
- [Governance, Privacy, and Policy](governance.md)
- [Usage and Token Budgeting](usage-tokens.md)
- [Troubleshooting and FAQ](troubleshooting.md)
## iOS MCP Details
- [XcodeBuildMCP (iOS)](ios-xcodebuildmcp.md) - Full MCP setup and usage for Xcode workflows
## Confluence Mapping
This folder maps 1:1 to Confluence pages so content can be copied without rework.

View File

@ -1,99 +0,0 @@
# iOS AI Setup
You are here: [AI Docs Home](index.md) > iOS Setup
## Contents
- Setup Path A (Xcode)
- Setup Path B (VS Code)
- iOS-Specific Guidance
- MCP For iOS
- iOS Troubleshooting
- Next Steps
## Setup Path A: Xcode + GitHub Copilot for Xcode
### Prerequisites
- Xcode installed and up to date.
- GitHub account with Copilot Enterprise access.
### Install and Sign In (High-Level)
1. Install the GitHub Copilot for Xcode plugin.
2. Sign in with your GitHub account.
3. Confirm Copilot is enabled in Xcode.
4. Run a simple prompt to verify suggestions appear.
### Example Prompt (Xcode)
Type this in a Swift file comment, then wait for a suggestion:
Example prompt:
```text
// Create a function that validates an email string.
```
### Verification Steps
- Open a Swift file and start a small function.
- Confirm inline suggestions appear.
- Open the Copilot chat panel and ask a short question.
## Setup Path B: VS Code + GitHub Copilot Extension
### When to Use VS Code on iOS
- Editing shared docs or configs.
- Rapid refactors or explorations.
- Working on multi-platform files.
### Install and Sign In (High-Level)
1. Install VS Code and the GitHub Copilot extension.
2. Sign in with your GitHub account.
3. Confirm Copilot is enabled in VS Code.
4. Run a simple prompt to verify suggestions appear.
### Example Prompt (VS Code)
Open a Swift file and add:
Example prompt:
```text
// Create a Swift struct for a user profile with name and email.
```
### Verification Steps
- Open a Swift file and type a short function signature.
- Confirm inline suggestions appear.
- Open Copilot Chat and request a short summary.
## iOS-Specific Guidance
- Ask for Swift/SwiftUI patterns with small, bounded tasks.
- Request tests or sample usages for view models.
- Favor refactor or patch requests over full rewrites.
### Example Request
Example prompt:
```text
Refactor this SwiftUI view to reduce duplication. Do not change behavior. Provide a short diff summary.
```
### Starter Prompts
Example prompts:
```text
Create a SwiftUI view model for this screen and list its inputs and outputs.
Write unit tests for this view model using XCTest.
Refactor this view to reduce duplication without changing behavior.
```
## MCP For iOS
The detailed MCP setup and XcodeBuildMCP guidance is in a dedicated page:
- [XcodeBuildMCP (iOS)](ios-xcodebuildmcp.md)
## iOS Troubleshooting
- If suggestions are missing, confirm sign-in and access.
- If the plugin is not visible, verify extension compatibility.
- If responses are blocked, check network or policy constraints.
### Common Setup Gaps
- Copilot access not provisioned for the GitHub account.
- Xcode plugin disabled after update.
- Multiple AI plugins competing for suggestions.
## Next Steps
- For workflow patterns, read [Cross-Platform AI Usage](cross-platform.md).
- For MCP automation, read [XcodeBuildMCP (iOS)](ios-xcodebuildmcp.md).
- For safety rules, read [Governance, Privacy, and Policy](governance.md).

View File

@ -0,0 +1,88 @@
# MCP for iOS in VS Code
You are here: [AI Docs Home](../index.md) > MCP for iOS in VS Code
## Contents
- Overview
- Prerequisites
- XcodeBuildMCP Setup
- Xcode Native MCP Setup
- Example Prompts
- Troubleshooting
- Next Steps
---
## Overview
This guide covers how to set up MCP (Model Context Protocol) automation for iOS development in VS Code. Complete the [VS Code Initial Setup](../vscode-setup.md) first.
---
## Prerequisites
- macOS with Xcode 26.3+ installed (for native MCP) or Xcode 13+ (for XcodeBuildMCP)
- VS Code with GitHub Copilot and Copilot Chat enabled ([see setup guide](../vscode-setup.md))
- Node.js 18+ (for XcodeBuildMCP)
- This repo opened in VS Code (workspace root)
---
## XcodeBuildMCP Setup (External Tool)
1. Install XcodeBuildMCP:
```bash
brew tap getsentry/xcodebuildmcp
brew install xcodebuildmcp
```
2. Ensure `.xcodebuildmcp/config.yaml` is present and configured for your workspace, scheme, and simulator. ([What is config.yaml? See reference and sample.](xcodebuildmcp-config.yaml.md))
3. Add or update `.vscode/mcp.json`:
```json
{
"servers": {
"XcodeBuildMCP": {
"command": "xcodebuildmcp",
"args": ["mcp"]
}
}
}
```
4. Restart VS Code. Copilot Chat will discover MCP tools.
---
## Xcode Native MCP Setup (Xcode 26.3+)
1. Open Xcode.
2. Go to Settings > Intelligence tab.
3. Enable Model Context Protocol (toggle on Xcode Tools or Allow external connections).
4. Add or update `.vscode/mcp.json`:
```json
{
"servers": {
"XcodeNative": {
"command": "xcrun",
"args": ["mcpbridge"]
}
}
}
```
5. Restart VS Code. Copilot Chat will discover the native MCP server.
---
## Example Prompts
- "Build and run ToyotaOneApp on the iPhone 17 Pro Max simulator."
- "Run all UI tests and summarize failures."
- "Take a screenshot of the home screen."
---
## Troubleshooting
- If MCP tools are not available, confirm `.vscode/mcp.json` is present and correct.
- For XcodeBuildMCP, check `.xcodebuildmcp/config.yaml` for correct paths and scheme.
- Simulator not found? Boot it in Xcode or Simulator.app first.
---
## Next Steps
- For general VS Code setup, see [VS Code Initial Setup](../vscode-setup.md).
- For iOS basics, see [iOS Setup](ios.md).

35
docs/ai/ios/ios-mcp.md Normal file
View File

@ -0,0 +1,35 @@
# iOS MCP Overview
You are here: [AI Docs Home](../index.md) > iOS MCP Overview
## What is MCP for iOS?
MCP (Model Context Protocol) enables automation and agent workflows for iOS development. It allows tools like Copilot Chat to build, test, run, and interact with Xcode projects programmatically.
## Why Use MCP?
- Automate builds, tests, and diagnostics
- Enable agentic workflows in Copilot Chat and other tools
- Standardize and speed up repetitive tasks
- Reduce context switching between tools
## MCP Options for iOS
There are two main ways to use MCP with iOS projects:
### 1. Xcode Native MCP (Xcode 26.3+)
- Built into Xcode 26.3 and later
- Official Apple support
- Best for seamless integration and live previews
- [Setup & details](ios-xcodebuildmcp-xcode.md)
### 2. XcodeBuildMCP (External Tool)
- For details and setup, see [XcodeBuildMCP for iOS in VS Code](ios-xcodebuildmcp-vscode.md).
## When to Use Which?
- Use Xcode Native MCP for the simplest, most integrated experience (especially for new projects or live previews)
- Use XcodeBuildMCP if you need advanced automation, CI/CD, or support for older Xcode versions
- You can configure both and switch as needed in VS Codes `.vscode/mcp.json`
## Next Steps
- [MCP for iOS in VS Code](ios-mcp-vscode.md)
- [XcodeBuildMCP (Xcode)](ios-xcodebuildmcp-xcode.md)
- [XcodeBuildMCP for iOS in VS Code](ios-xcodebuildmcp-vscode.md)

View File

@ -0,0 +1,145 @@
# XcodeBuildMCP for iOS in VS Code
You are here: [AI Docs Home](../index.md) > XcodeBuildMCP for iOS in VS Code
## Contents
- Overview
- Requirements
- One-time Repo Setup
- XcodeBuildMCP Installation & Configuration
- Using XcodeBuildMCP in Copilot Chat
- Common Actions & Prompts
- Troubleshooting
- References
- Next Steps
---
## Overview
XcodeBuildMCP lets you run Xcode build and test tasks from within VS Code, using agent workflows and Copilot Chat. This reduces context switching and makes iOS automation repeatable.
---
## Requirements
- macOS with Xcode installed (includes iOS Simulator)
- VS Code with GitHub Copilot Chat enabled
- MCP tools configured in VS Code (repo includes .vscode/mcp.json)
- Node.js 18+ (required by the MCP host)
- This repo opened in VS Code (workspace root)
Optional but commonly needed for successful builds:
- Ruby + Bundler
- CocoaPods
- Flutter + FVM (if working on the Flutter module)
---
## One-time Repo Setup
Run the standard project setup so builds succeed:
1. `./scripts/setup.sh`
2. `bundle exec pod install`
3. If using Flutter: `cd ../oneappmodule-2.0/apps/oneapp && fvm flutter pub get`
---
## XcodeBuildMCP Installation & Configuration
### 1. Install XcodeBuildMCP
```bash
brew tap getsentry/xcodebuildmcp
brew install xcodebuildmcp
```
### 2. Configure XcodeBuildMCP
- The repo should include `.xcodebuildmcp/config.yaml`. Make sure it points to the correct workspace, scheme, and simulator.
- [What is config.yaml? See reference and sample.](xcodebuildmcp-config.yaml.md)
### 3. Configure MCP in VS Code
- Ensure `.vscode/mcp.json` exists:
```json
{
"servers": {
"XcodeBuildMCP": {
"command": "xcodebuildmcp",
"args": ["mcp"]
}
}
}
```
- Restart VS Code or reload the window. Copilot should discover the tools.
---
## Using XcodeBuildMCP in Copilot Chat
From Copilot Chat, you can ask to build and run via XcodeBuildMCP. Example prompts:
- "build and run ToyotaOneApp on the iPhone 17 Pro Max simulator"
- "launch ToyotaOneApp without building (use the existing build)"
- "take a screenshot of the home screen"
Common tool actions the agent will use:
- list_devices, list_schemes
- session_set_defaults
- build_run_sim
- launch_app_sim (no build)
- screenshot
---
## Common Actions & Prompts
- **Build:**
"Use XcodeBuildMCP to build the app for the iOS simulator and summarize errors in 5 bullets."
- **Unit Tests:**
"Run unit tests with XcodeBuildMCP and list failing tests with file names."
- **UI Tests:**
"Run UI tests with XcodeBuildMCP on iPhone 17 Pro Max (iOS 26.2) and list any failures."
- **Screenshots:**
"Run the UI test that captures screenshots and list the output paths."
- **Run without building:**
"launch ToyotaOneApp without building"
- **Home screen screenshot (example flow):**
1. "launch ToyotaOneApp without building"
2. Wait for the app to load the home screen
3. "take a screenshot of the home screen"
---
## Troubleshooting
- **Build DB locked:** another build is running. Stop it and retry.
- **Wrong scheme or workspace:** confirm in `.xcodebuildmcp/config.yaml`.
- **Simulator not found:** install/boot the simulator in Xcode or Simulator.app.
- If MCP tools are not available in VS Code, confirm `.vscode/mcp.json` is present and enabled.
---
## References
- https://github.com/getsentry/XcodeBuildMCP
- https://www.apple.com/newsroom/2026/02/xcode-26-point-3-unlocks-the-power-of-agentic-coding/
- https://code.visualstudio.com/docs/copilot/customization/mcp-servers
---
## Next Steps
- For iOS setup basics, read [iOS Setup](ios.md).
- For cross-platform usage patterns, read [Cross-Platform AI Usage](../cross-platform.md).

View File

@ -1,16 +1,16 @@
# XcodeBuildMCP (iOS)
# XcodeBuildMCP (Xcode)
You are here: [AI Docs Home](index.md) > XcodeBuildMCP (iOS)
You are here: [AI Docs Home](../index.md) > XcodeBuildMCP (Xcode)
## Contents
- What XcodeBuildMCP Is
- What It Can Do
- Xcode 26.3 MCP Setup
- Xcode 26.3 MCP Setup (Xcode)
- Standardize The Simulator
- References
- Next Steps
This page focuses on MCP-based Xcode workflows and the XcodeBuildMCP setup.
This page focuses on MCP-based Xcode workflows and the XcodeBuildMCP setup for Xcode users.
## What XcodeBuildMCP Is
XcodeBuildMCP can run build and test tasks without you switching into Xcode for every step. This reduces context switching and makes workflows repeatable.
@ -73,14 +73,10 @@ Example prompt:
Build with XcodeBuildMCP and extract the first error message only.
```
## Xcode 26.3 MCP Setup (Detailed)
These steps reflect a common setup as of February 2026. Wording may vary slightly in release candidates.
## Xcode 26.3 MCP Setup (Xcode)
### Prerequisites
- Xcode 26.3 (RC or full release), opened at least once with your project
- VS Code installed (stable or Insiders)
- GitHub Copilot extension installed and signed in
- Node.js and npm installed (for XcodeBuildMCP)
### Step 1: Enable Xcode MCP Server
1. Open Xcode.
@ -89,29 +85,7 @@ These steps reflect a common setup as of February 2026. Wording may vary slightl
4. Find the Model Context Protocol section.
5. Toggle on Xcode Tools (or Allow external connections).
### Step 2: Install And Configure XcodeBuildMCP (Recommended Bridge)
Install:
```bash
npm install -g xcodebuildmcp@latest
```
Add to VS Code (create or edit .vscode/mcp.json):
```json
{
"servers": {
"XcodeBuildMCP": {
"command": "npx",
"args": ["-y", "xcodebuildmcp@latest", "mcp"]
}
}
}
```
Restart VS Code or reload the window. Copilot should discover the tools.
### Step 3: Optional Native Xcode MCP Bridge
### Step 2: Optional Native Xcode MCP Bridge
Xcode 26.3 includes a native MCP bridge. This exposes Xcode tools directly.
Add to VS Code:
@ -127,20 +101,9 @@ Add to VS Code:
}
```
### Using MCP In VS Code With Copilot
1. Open your iOS project in VS Code.
2. Use Copilot Chat in agent mode for multi-step tasks.
3. MCP tools appear as slash commands once discovered.
Example prompt:
```text
Use XcodeBuildMCP to build and summarize errors. Then suggest fixes.
```
### Tips And Caveats
- Xcode must be running (or launchable) for MCP tools to respond.
- Native Xcode MCP is often best for previews.
- XcodeBuildMCP is often best for heavy builds and automation.
## Standardize The Simulator (Avoid Back-And-Forth)
Pin a simulator name and OS version in your project guidance so the assistant always uses the same target.
@ -183,4 +146,4 @@ https://www.apple.com/newsroom/2026/02/xcode-26-point-3-unlocks-the-power-of-age
## Next Steps
- For iOS setup basics, read [iOS Setup](ios.md).
- For cross-platform usage patterns, read [Cross-Platform AI Usage](cross-platform.md).
- For cross-platform usage patterns, read [Cross-Platform AI Usage](../cross-platform.md).

47
docs/ai/ios/ios.md Normal file
View File

@ -0,0 +1,47 @@
# iOS AI Setup (Quick Start)
You are here: [AI Docs Home](../index.md) > iOS Setup
## Which Path Should I Choose?
- **Use Xcode** if you want the most integrated Apple experience for Swift/SwiftUI and UI design.
- **Use VS Code** if you want automation, Copilot Chat agent workflows, or work on cross-platform code.
- You can use both! Many developers do.
---
## Xcode: Copilot Setup Steps
1. Install Xcode from the Mac App Store and open it once.
2. Install the [GitHub Copilot for Xcode plugin](https://github.com/github/copilot-xcode) and enable it in Xcodes Extensions.
3. Sign in with your GitHub account (Copilot access required).
4. Open a Swift file and type `//` to trigger a suggestion.
5. Open Copilot Chat and try a simple prompt (e.g., “Write a Swift function to reverse a string”).
## VS Code: Copilot Setup Steps
1. Follow the [VS Code Initial Setup](../vscode-setup.md) to install VS Code, Copilot, and sign in.
2. Open a Swift file and type `//` to trigger a suggestion.
3. Open Copilot Chat and try a simple prompt.
---
## Add MCP Automation (Optional, Advanced)
- For general MCP usage in VS Code: See [MCP for iOS in VS Code](ios-mcp-vscode.md)
- For Xcodes built-in MCP: See [XcodeBuildMCP (Xcode)](ios-xcodebuildmcp-xcode.md)
- For XcodeBuildMCP in VS Code: See [XcodeBuildMCP for iOS in VS Code](ios-xcodebuildmcp-vscode.md)
---
## Troubleshooting
- If suggestions are missing, confirm youre signed in and Copilot is enabled.
- If the plugin/extension is not visible, check compatibility and restart the app.
- If responses are blocked, check your network or company policy.
- If you have multiple AI plugins, disable all but Copilot.
---
## Next Steps
- For cross-platform patterns, read [Cross-Platform AI Usage](../cross-platform.md)
- For automation, see the MCP guides above
- For safety rules, read [Governance, Privacy, and Policy](../governance.md)

View File

@ -0,0 +1,34 @@
# xcodebuildmcp/config.yaml Reference
This file is required for XcodeBuildMCP to automate builds, tests, and simulator actions. It tells the tool which workspace, scheme, configuration, and simulator to use.
## Example config.yaml
```yaml
schemaVersion: 1
sessionDefaults:
workspacePath: ./OneApp.xcworkspace
scheme: ToyotaOneApp
configuration: Debug
simulatorName: iPhone 17 Pro Max
simulatorId: <SIMULATOR_UDID>
useLatestOS: true
```
## Key Fields
- `workspacePath`: Path to your .xcworkspace or .xcodeproj
- `scheme`: The Xcode scheme to build/test
- `configuration`: Build configuration (Debug, Release, etc.)
- `simulatorName`: Name of the simulator to use
- `simulatorId`: UDID of the simulator (optional; takes precedence over name)
- `useLatestOS`: If true, always use the latest available iOS version
## Where to put it
Place this file at `.xcodebuildmcp/config.yaml` in your repo root.
## Why it matters
This config lets XcodeBuildMCP run builds, tests, and launches without manual setup. It ensures all developers and CI jobs use the same settings.
## Troubleshooting
- If builds fail, check workspacePath and scheme.
- If the simulator isnt found, check simulatorName or simulatorId.
- If you change project structure, update config.yaml accordingly.

View File

@ -211,7 +211,7 @@ Refactor only the validation logic in this file. Keep behavior the same and list
```
## Next Steps
- If you have not set up an editor yet, go to [iOS Setup](ios.md) or [Android Setup](android.md).
- If you have not set up an editor yet, go to [iOS Setup](ios/ios.md) or [Android Setup](android/android.md).
- For day-to-day usage patterns, read [Cross-Platform AI Usage](cross-platform.md).
- For safety and cost guidance, read [Governance, Privacy, and Policy](governance.md) and [Usage and Token Budgeting](usage-tokens.md).

View File

@ -46,10 +46,11 @@ Keep the approved list in a single repo and organize by platform. This repo alre
```text
/assets/
setup.sh ← the installer (auto-discovers agents & instructions)
setup.sh ← the installer (auto-discovers everything)
ios-skills.txt ← curated iOS skills (one per line)
android-skills.txt ← curated Android skills
shared-skills.txt ← curated cross-platform skills
skills/ ← custom skill folders (auto-discovered)
agents/ ← agent prompt files (auto-discovered)
instructions/ ← instruction rule files (auto-discovered)
```

57
docs/ai/vscode-setup.md Normal file
View File

@ -0,0 +1,57 @@
# VS Code Initial Setup (All Platforms)
You are here: [AI Docs Home](index.md) > VS Code Initial Setup
## Contents
- Overview
- Install VS Code
- Install GitHub Copilot
- Sign In and Verify
- Try Copilot Suggestions
- Next Steps
---
## Overview
This guide covers the basic setup for Visual Studio Code and GitHub Copilot. It applies to all platforms (iOS, Android, cross-platform). Complete these steps before any platform-specific automation or MCP setup.
---
## Install VS Code
1. Download Visual Studio Code from [code.visualstudio.com](https://code.visualstudio.com/).
2. Open the installer and follow the prompts to install.
3. Launch VS Code.
---
## Install GitHub Copilot
1. In VS Code, open the Extensions sidebar (⇧⌘X).
2. Search for "GitHub Copilot" and click Install.
3. (Optional) Search for and install "GitHub Copilot Chat" for chat-based workflows.
---
## Sign In and Verify
1. After installing, youll be prompted to sign in with your GitHub account.
2. Complete the sign-in flow in your browser.
3. You must have Copilot access (Copilot Individual, Business, or Enterprise).
4. Open a code file (e.g., Swift, Kotlin, Java, JS) and type `//` or start a function to trigger a suggestion.
5. You should see Copilot suggestions inline.
---
## Try Copilot Suggestions
- Open a file for your platform (Swift for iOS, Kotlin for Android, etc.).
- Type a comment or function signature, e.g.:
- `// Create a function to validate an email address.`
- `fun main() {` (for Kotlin)
- Accept a suggestion with Tab or Enter.
- Open the Copilot Chat sidebar and try a prompt, e.g.:
- "Write a function to reverse a string in Kotlin."
---
## Next Steps
- For iOS automation, see [MCP for iOS in VS Code](ios/ios-mcp-vscode.md).
- For Android automation, see [Android Setup](android/android.md).

View File

@ -33,7 +33,7 @@ The docs under docs/ai map 1:1 to Confluence pages.
- overview.md
- ios.md
- ios-xcodebuildmcp.md
- android.md
- android/android.md
- cross-platform.md
- skills.md
- usage-tokens.md