-
Notifications
You must be signed in to change notification settings - Fork 99
[PM-33572] llm: Add implementing-ios-code skill with Swift templates #2446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
505bac7
8668f72
642a37e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||
| --- | ||||||
| name: implementing-ios-code | ||||||
| description: Implement, write code, add a new screen, create a feature, new view, new processor, or wire up a new service in Bitwarden iOS. Use when asked to "implement", "write code", "add screen", "create feature", "new view", "new processor", "add service", or when translating a design doc into actual Swift code. | ||||||
| --- | ||||||
|
|
||||||
| # Implementing iOS Code | ||||||
|
|
||||||
| Use this skill to implement Bitwarden iOS features following established patterns. | ||||||
|
|
||||||
| ## Prerequisites | ||||||
|
|
||||||
| - A plan should exist in `.claude/outputs/plans/<ticket-id>.md`. If not, invoke `planning-ios-implementation` first. | ||||||
| - Read `Docs/Architecture.md` β it is the authoritative source for all patterns. This skill references it, not replaces it. | ||||||
|
|
||||||
| ## Step 1: Determine Scope | ||||||
|
|
||||||
| From the plan, identify: | ||||||
| - Is this a new feature (full file-set) or modification of existing code? | ||||||
| - Which framework: `BitwardenShared`, `AuthenticatorShared`, or `BitwardenKit`? | ||||||
| - Which domain: `Auth/`, `Autofill/`, `Platform/`, `Tools/`, `Vault/`? | ||||||
|
|
||||||
| See `templates.md` for file-set skeletons. | ||||||
|
|
||||||
| ## Step 2: Core Layer First | ||||||
|
|
||||||
| Implement from the bottom up: | ||||||
|
|
||||||
| **Data Models** (if needed) | ||||||
| - Request/Response types in `Core/<Domain>/Models/Request/` and `Response/` | ||||||
| - Enum types in `Core/<Domain>/Models/Enum/` | ||||||
| - CoreData entities only if persistence is needed (add to `Bitwarden.xcdatamodeld`) | ||||||
|
||||||
|
|
||||||
| **Services / Repositories** | ||||||
| - Define protocol with `// sourcery: AutoMockable` | ||||||
| - Implement `Default<Name>Service` / `Default<Name>Repository` | ||||||
| - Add `Has<Name>` protocol | ||||||
| - See `templates.md` for service skeleton | ||||||
|
|
||||||
| ## Step 3: UI Layer (File-Set Pattern) | ||||||
|
|
||||||
| For new screens, create all required files together (see `templates.md`): | ||||||
|
|
||||||
| 1. **Route** β Add case to the parent Coordinator's route enum | ||||||
| 2. **Coordinator** β Navigation logic, screen instantiation, `Services` typealias | ||||||
| 3. **State** β Value type (`struct`) holding all view-observable data | ||||||
| 4. **Action** β Enum of user interactions handled synchronously in `receive(_:)` | ||||||
| 5. **Effect** β Enum of async work handled in `perform(_:)` | ||||||
| 6. **Processor** β `StateProcessor` subclass, business logic only | ||||||
| 7. **View** β SwiftUI view using `store.binding`, `store.perform`, `@ObservedObject` | ||||||
|
|
||||||
| ## Step 4: Wire Dependency Injection | ||||||
|
|
||||||
| After creating a new service/repository: | ||||||
| - Add `Has<Name>` conformance to `ServiceContainer` via extension | ||||||
| - Add `Has<Name>` to the `Services` typealias of any processor that needs it | ||||||
|
|
||||||
| ## Step 5: Security Check | ||||||
|
|
||||||
| Before finishing: | ||||||
| - [ ] Vault data? β Must use `BitwardenSdk` for all encryption/decryption | ||||||
| - [ ] Storing credentials? β Must use `KeychainRepository`, not `UserDefaults` | ||||||
|
||||||
| - [ ] Storing credentials? β Must use `KeychainRepository`, not `UserDefaults` | |
| - [ ] Storing credentials? β Must use `KeychainRepository`, not `AppSettingsStore` |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Use `// MARK: -` sections to organize code within files. | |
| Use pragma marks to organize code. `// MARK: -` is used to denote different objects in the same file; `// MARK:` is used to denote different sections within an object. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,319 @@ | ||
| # iOS Implementation Templates | ||
|
|
||
| Minimal copy-paste skeletons derived from actual codebase patterns. | ||
| See `Docs/Architecture.md` for full pattern documentation. | ||
|
|
||
| ## New Feature File-Set | ||
|
|
||
| When adding a new screen, create these files (replace `<Feature>` throughout): | ||
|
|
||
| ``` | ||
| BitwardenShared/UI/<Domain>/<Feature>/ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π€ Sometimes a new screen requires a coordinator, but not always; often one coordinator manages multiple screens (and therefore multiple processors). I'm not sure how best to capture that judgement call here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added detail about this, giving guidance on how to make the "correct" decision. LMK what you think.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, that sounds good. |
||
| βββ <Feature>Coordinator.swift | ||
| βββ <Feature>Processor.swift | ||
| βββ <Feature>State.swift | ||
| βββ <Feature>Action.swift | ||
| βββ <Feature>Effect.swift | ||
| βββ <Feature>View.swift | ||
| ``` | ||
|
|
||
| Add a new case to the **parent** Coordinator's existing `Route` enum rather than creating a new `Route` file. | ||
|
|
||
| --- | ||
|
|
||
| ## Coordinator Skeleton | ||
|
|
||
| Based on: `BitwardenShared/UI/Auth/AuthCoordinator.swift` | ||
|
|
||
| ```swift | ||
| import BitwardenKit | ||
| import BitwardenResources | ||
| import SwiftUI | ||
|
|
||
| // MARK: - <Feature>Coordinator | ||
|
|
||
| /// A coordinator that manages navigation in the <feature> flow. | ||
| /// | ||
| final class <Feature>Coordinator: Coordinator, HasStackNavigator { | ||
| // MARK: Types | ||
|
|
||
| typealias Services = Has<Service1> | ||
| & Has<Service2> | ||
|
|
||
| // MARK: Properties | ||
|
|
||
| private let services: Services | ||
| private(set) weak var stackNavigator: StackNavigator? | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| init(services: Services, stackNavigator: StackNavigator) { | ||
| self.services = services | ||
| self.stackNavigator = stackNavigator | ||
| } | ||
|
|
||
| // MARK: Methods | ||
|
|
||
| func navigate(to route: <Parent>Route, context: AnyObject?) { | ||
| switch route { | ||
| case .dismiss: | ||
| stackNavigator?.dismiss() | ||
| case .<featureRoute>: | ||
| show<Feature>() | ||
| } | ||
| } | ||
|
|
||
| func start() {} | ||
|
|
||
| // MARK: Private | ||
|
|
||
| private func show<Feature>() { | ||
| let processor = <Feature>Processor( | ||
| coordinator: asAnyCoordinator(), | ||
| services: services, | ||
| state: <Feature>State(), | ||
| ) | ||
| let store = Store(processor: processor) | ||
| let view = <Feature>View(store: store) | ||
| stackNavigator?.push(view) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Processor Skeleton | ||
|
|
||
| Based on: `BitwardenShared/UI/Auth/Landing/LandingProcessor.swift` | ||
|
|
||
| ```swift | ||
| import BitwardenKit | ||
|
|
||
| // MARK: - <Feature>Processor | ||
|
|
||
| /// The processor used to manage state and handle actions for the <feature> screen. | ||
| /// | ||
| class <Feature>Processor: StateProcessor<<Feature>State, <Feature>Action, <Feature>Effect> { | ||
| // MARK: Types | ||
|
|
||
| typealias Services = Has<Service1> | ||
| & Has<Service2> | ||
|
|
||
| // MARK: Private Properties | ||
|
|
||
| /// The coordinator that handles navigation. | ||
| private let coordinator: AnyCoordinator<<Parent>Route, <Parent>Event> | ||
|
|
||
| /// The services required by this processor. | ||
| private let services: Services | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| init( | ||
| coordinator: AnyCoordinator<<Parent>Route, <Parent>Event>, | ||
| services: Services, | ||
| state: <Feature>State, | ||
| ) { | ||
| self.coordinator = coordinator | ||
| self.services = services | ||
| super.init(state: state) | ||
| } | ||
|
|
||
| // MARK: Methods | ||
|
|
||
| override func perform(_ effect: <Feature>Effect) async { | ||
| switch effect { | ||
| case .appeared: | ||
| await loadData() | ||
| } | ||
| } | ||
|
|
||
| override func receive(_ action: <Feature>Action) { | ||
| switch action { | ||
| case let .someValueChanged(newValue): | ||
| state.someValue = newValue | ||
| } | ||
| } | ||
|
|
||
| // MARK: Private | ||
|
|
||
| private func loadData() async { | ||
| do { | ||
| // use services.<service>.<method>() | ||
| } catch { | ||
| coordinator.showErrorAlert(error: error) | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## State / Action / Effect Skeletons | ||
|
|
||
| Based on: `BitwardenShared/UI/Auth/Landing/Landing{State,Action,Effect}.swift` | ||
|
|
||
| ```swift | ||
| // MARK: - <Feature>State | ||
|
|
||
| /// An object that defines the current state of a `<Feature>View`. | ||
| /// | ||
| struct <Feature>State: Equatable { | ||
| // MARK: Properties | ||
|
|
||
| var someValue: String = "" | ||
| } | ||
| ``` | ||
|
|
||
| ```swift | ||
| // MARK: - <Feature>Action | ||
|
|
||
| /// Actions that can be processed by a `<Feature>Processor`. | ||
| /// | ||
| enum <Feature>Action: Equatable { | ||
| /// A value was changed by the user. | ||
| case someValueChanged(String) | ||
| } | ||
| ``` | ||
|
|
||
| ```swift | ||
| // MARK: - <Feature>Effect | ||
|
|
||
| /// Effects that can be processed by a `<Feature>Processor`. | ||
| /// | ||
| enum <Feature>Effect: Equatable { | ||
| /// The view appeared on screen. | ||
| case appeared | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## View Skeleton | ||
|
|
||
| Based on: `BitwardenShared/UI/Auth/Landing/LandingView.swift` | ||
|
|
||
| ```swift | ||
| import BitwardenKit | ||
| import BitwardenResources | ||
| import SwiftUI | ||
|
|
||
| // MARK: - <Feature>View | ||
|
|
||
| /// A view for <feature description>. | ||
| /// | ||
| struct <Feature>View: View { | ||
| // MARK: Properties | ||
|
|
||
| /// The `Store` for this view. | ||
| @ObservedObject var store: Store<<Feature>State, <Feature>Action, <Feature>Effect> | ||
|
|
||
| // MARK: View | ||
|
|
||
| var body: some View { | ||
| content | ||
| .task { | ||
| await store.perform(.appeared) | ||
| } | ||
| } | ||
|
|
||
| // MARK: Private Views | ||
|
|
||
| private var content: some View { | ||
| VStack { | ||
| // Example: text field backed by store state | ||
| BitwardenTextField( | ||
| title: Localizations.someLabel, | ||
| text: store.binding( | ||
| get: \.someValue, | ||
| send: <Feature>Action.someValueChanged, | ||
| ), | ||
| ) | ||
| } | ||
| .navigationBarTitle(Localizations.featureTitle, displayMode: .inline) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Service Skeleton | ||
|
|
||
| Based on: `BitwardenShared/Core/Platform/Services/PasteboardService.swift` | ||
|
|
||
| ```swift | ||
| import BitwardenKit | ||
|
|
||
| // MARK: - <Name>Service | ||
|
|
||
| // sourcery: AutoMockable | ||
| /// A protocol for a service that <description>. | ||
| /// | ||
| protocol <Name>Service: AnyObject { | ||
| /// Does something asynchronously. | ||
| /// | ||
| func doSomething() async throws | ||
| } | ||
|
|
||
| // MARK: - Default<Name>Service | ||
|
|
||
| /// A default implementation of `<Name>Service`. | ||
| /// | ||
| class Default<Name>Service: <Name>Service { | ||
| // MARK: Private Properties | ||
|
|
||
| private let errorReporter: ErrorReporter | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| init(errorReporter: ErrorReporter) { | ||
| self.errorReporter = errorReporter | ||
| } | ||
|
|
||
| // MARK: Methods | ||
|
|
||
| func doSomething() async throws { | ||
| // Implementation | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Has<Name>Service | ||
|
|
||
| /// A protocol for an object that provides a `<Name>Service`. | ||
| /// | ||
| protocol Has<Name>Service { | ||
| /// The service used for <description>. | ||
| var <name>Service: <Name>Service { get } | ||
| } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Repository Pattern | ||
|
|
||
| Repositories follow the same protocol + Default + Has* pattern as services, but operate on domain models and typically inject both a data store and a network client: | ||
|
|
||
| ```swift | ||
| // sourcery: AutoMockable | ||
| protocol <Name>Repository: AnyObject { | ||
| func fetchItems() async throws -> [<Model>] | ||
| } | ||
|
|
||
| class Default<Name>Repository: <Name>Repository { | ||
| private let <name>DataStore: <Name>DataStore | ||
| private let <name>APIService: <Name>APIService | ||
|
|
||
| init(<name>DataStore: <Name>DataStore, <name>APIService: <Name>APIService) { | ||
| self.<name>DataStore = <name>DataStore | ||
| self.<name>APIService = <name>APIService | ||
| } | ||
|
|
||
| func fetchItems() async throws -> [<Model>] { | ||
| // Implementation | ||
| } | ||
| } | ||
|
|
||
| protocol Has<Name>Repository { | ||
| var <name>Repository: <Name>Repository { get } | ||
| } | ||
| ``` | ||
Uh oh!
There was an error while loading. Please reload this page.