Merge branch 'develop' into refactor
This commit is contained in:
commit
35d8bb7b6b
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
|
||||
}
|
||||
```
|
||||
|
||||
<!-- </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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user