Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .claude/skills/implementing-ios-code/SKILL.md
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`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€” We basically never add anything to Core Data. If something's stored on disk, it's either in UserDefaults or the Keychain, depending on how sensitive of data it is.

Maybe this should be

- More-sensitive information persisted to disk is in `KeychainRepository`. Less-sensitive information persisted to disk is in `AppSettingsStore`. Both of these are exposed through `StateService`. Try to use a separate protocol instead of adding to the `StateService, `AppSettingsStore`, and `KeychainRepository` protocols, to ensure interface segregation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased. LMK what you think.


**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`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- [ ] Storing credentials? β†’ Must use `KeychainRepository`, not `UserDefaults`
- [ ] Storing credentials? β†’ Must use `KeychainRepository`, not `AppSettingsStore`

- [ ] User input? β†’ Must validate via `InputValidator`
- [ ] Surfacing errors? β†’ Sensitive errors must implement `NonLoggableError`
- [ ] Running in an extension? β†’ Check Argon2id memory if KDF is involved

## Step 6: Documentation

All new public types and methods require DocC (`///`) documentation.
Exceptions: protocol property/function implementations (docs live in the protocol), mock classes.
Use `// MARK: -` sections to organize code within files.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

319 changes: 319 additions & 0 deletions .claude/skills/implementing-ios-code/templates.md
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>/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 }
}
```
Loading