Merge branch 'develop' into refactor

This commit is contained in:
Matt Bruce 2026-02-12 16:34:25 -06:00
commit 35d8bb7b6b
2 changed files with 615 additions and 759 deletions

View File

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

View File

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