Compare commits
10 Commits
b5e7de10e7
...
6b7455f6ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7455f6ff | ||
|
|
d74e2ca3c1 | ||
|
|
370647c882 | ||
|
|
3d1e2835b0 | ||
|
|
35d8bb7b6b | ||
|
|
8533890d2c | ||
|
|
000babfe80 | ||
|
|
f3dd8d92bd | ||
|
|
88e4402d38 | ||
|
|
20446c5224 |
@ -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 |
|
||||
584
assets/agents/toyota-ios-developer.md
Normal file
584
assets/agents/toyota-ios-developer.md
Normal 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)
|
||||
@ -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.
|
||||
|
||||
116
assets/setup.sh
116
assets/setup.sh
@ -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
15
assets/skills/README.md
Normal 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.
|
||||
143
assets/skills/swift-clean-architecture/SKILL.md
Normal file
143
assets/skills/swift-clean-architecture/SKILL.md
Normal 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`
|
||||
224
assets/skills/swift-localization/SKILL.md
Normal file
224
assets/skills/swift-localization/SKILL.md
Normal 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
|
||||
```
|
||||
258
assets/skills/swift-model-design/SKILL.md
Normal file
258
assets/skills/swift-model-design/SKILL.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
287
assets/skills/swift-modern/SKILL.md
Normal file
287
assets/skills/swift-modern/SKILL.md
Normal 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) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
188
assets/skills/swift-pop/SKILL.md
Normal file
188
assets/skills/swift-pop/SKILL.md
Normal 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)
|
||||
}
|
||||
```
|
||||
359
assets/skills/swiftui-accessibility/SKILL.md
Normal file
359
assets/skills/swiftui-accessibility/SKILL.md
Normal 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
|
||||
373
assets/skills/swiftui-modern/SKILL.md
Normal file
373
assets/skills/swiftui-modern/SKILL.md
Normal 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
|
||||
}
|
||||
```
|
||||
228
assets/skills/swiftui-mvvm/SKILL.md
Normal file
228
assets/skills/swiftui-mvvm/SKILL.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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 team’s 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).
|
||||
@ -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).
|
||||
23
docs/ai/crossplatform/cross-platform.md
Normal file
23
docs/ai/crossplatform/cross-platform.md
Normal 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 repo’s 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)
|
||||
24
docs/ai/crossplatform/mcp-overview.md
Normal file
24
docs/ai/crossplatform/mcp-overview.md
Normal 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)
|
||||
10
docs/ai/crossplatform/plan-first-workflow.md
Normal file
10
docs/ai/crossplatform/plan-first-workflow.md
Normal 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.
|
||||
17
docs/ai/crossplatform/prompting-antipatterns.md
Normal file
17
docs/ai/crossplatform/prompting-antipatterns.md
Normal 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.
|
||||
8
docs/ai/crossplatform/prompting-patterns.md
Normal file
8
docs/ai/crossplatform/prompting-patterns.md
Normal 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.
|
||||
9
docs/ai/crossplatform/starter-prompts.md
Normal file
9
docs/ai/crossplatform/starter-prompts.md
Normal 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.
|
||||
BIN
docs/ai/images/Screenshot 2026-02-19 at 1.19.17 PM.png
Normal file
BIN
docs/ai/images/Screenshot 2026-02-19 at 1.19.17 PM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/ai/images/Screenshot 2026-02-19 at 1.19.25 PM.png
Normal file
BIN
docs/ai/images/Screenshot 2026-02-19 at 1.19.25 PM.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
@ -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.
|
||||
|
||||
@ -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).
|
||||
88
docs/ai/ios/ios-mcp-vscode.md
Normal file
88
docs/ai/ios/ios-mcp-vscode.md
Normal 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
35
docs/ai/ios/ios-mcp.md
Normal 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 Code’s `.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)
|
||||
145
docs/ai/ios/ios-xcodebuildmcp-vscode.md
Normal file
145
docs/ai/ios/ios-xcodebuildmcp-vscode.md
Normal 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).
|
||||
@ -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
47
docs/ai/ios/ios.md
Normal 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 Xcode’s 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 Xcode’s 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 you’re 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)
|
||||
34
docs/ai/ios/xcodebuildmcp-config.yaml.md
Normal file
34
docs/ai/ios/xcodebuildmcp-config.yaml.md
Normal 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 isn’t found, check simulatorName or simulatorId.
|
||||
- If you change project structure, update config.yaml accordingly.
|
||||
@ -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).
|
||||
|
||||
|
||||
@ -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
57
docs/ai/vscode-setup.md
Normal 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, you’ll 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).
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user