ai-docs/assets/skills/swift-pop/SKILL.md

4.9 KiB

name description globs
Swift Protocol-Oriented Programming Protocol-first architecture patterns for reusability and testability
**/*.swift

Protocol-Oriented Programming (POP)

Protocol-first architecture is a priority. When designing new features, always think about protocols and composition before concrete implementations.

When Architecting New Code

  1. Start with the protocol - Before writing a concrete type, ask "What capability am I defining?" and express it as a protocol.

  2. Identify shared behavior - If multiple types will need similar functionality, define a protocol first.

  3. Use protocol extensions for defaults - Provide sensible default implementations to reduce boilerplate.

  4. Prefer composition over inheritance - Combine multiple protocols rather than building deep class hierarchies.

When Reviewing Existing Code

  1. Look for duplicated patterns - Similar logic across files is a candidate for protocol extraction.

  2. Identify common interfaces - Types that expose similar properties/methods should conform to a shared protocol.

  3. Check before implementing - Search for existing protocols that could be adopted or extended.

  4. Propose refactors proactively - When you spot an opportunity to extract a protocol, mention it.

Protocol Design Guidelines

Naming Conventions

Use capability-based suffixes:

  • -able: Persistable, Shareable, Validatable
  • -ing: DataProviding, ErrorHandling, Loading
  • -Provider: ContentProvider, DataProvider
  • -Delegate: NavigationDelegate, FormDelegate

Keep Protocols Focused

Each protocol should represent one capability (Interface Segregation Principle):

// GOOD - Focused protocols
// Note: Swift provides Identifiable already — adopt it, don't redefine it.
protocol Nameable {
    var displayName: String { get }
}

protocol Timestamped {
    var createdAt: Date { get }
    var updatedAt: Date { get }
}

// Compose as needed (Identifiable comes from Swift standard library)
struct User: Identifiable, Nameable, Timestamped {
    let id: UUID
    var displayName: String
    var createdAt: Date
    var updatedAt: Date
}
// BAD - Kitchen sink protocol
protocol Entity {
    var id: UUID { get }
    var displayName: String { get }
    var createdAt: Date { get }
    var updatedAt: Date { get }
    func save() async throws
    func delete() async throws
    func validate() -> Bool
}

Associated Types

Use sparingly. Prefer concrete types or generics at the call site when possible:

// Prefer this for simple cases
protocol DataFetching {
    func fetch<T: Decodable>(from url: URL) async throws -> T
}

// Use associated types when the type is fundamental to the protocol
protocol Repository {
    associatedtype Entity
    func fetch(id: UUID) async throws -> Entity?
    func save(_ entity: Entity) async throws
}

Value vs Reference Semantics

Constrain to AnyObject only when reference semantics are required:

// Default - allows structs and classes
protocol Configurable {
    mutating func configure(with options: Options)
}

// When you need reference semantics (delegates, observers)
protocol NavigationDelegate: AnyObject {
    func didNavigate(to destination: Destination)
}

Protocol Extensions

Provide default implementations for common behavior:

protocol Validatable {
    var validationErrors: [String] { get }
    var isValid: Bool { get }
}

extension Validatable {
    var isValid: Bool {
        validationErrors.isEmpty
    }
}

Dependency Injection with Protocols

Define protocols for services to enable testing:

protocol NetworkServiceProtocol {
    func fetch<T: Decodable>(from url: URL) async throws -> T
}

// Production implementation
final class NetworkService: NetworkServiceProtocol { ... }

// Test mock
final class MockNetworkService: NetworkServiceProtocol { ... }

Benefits

  • Reusability - Shared protocols work across features
  • Testability - Mock types can conform to protocols for unit testing
  • Flexibility - New features can adopt existing protocols immediately
  • Maintainability - Fix a bug in a protocol extension, fix it everywhere
  • Discoverability - Protocols document the expected interface clearly

Common Patterns

Repository Pattern

protocol Repository {
    associatedtype Entity: Identifiable
    
    func fetch(id: Entity.ID) async throws -> Entity?
    func fetchAll() async throws -> [Entity]
    func save(_ entity: Entity) async throws
    func delete(_ entity: Entity) async throws
}

Service Pattern

protocol AuthServiceProtocol {
    var isAuthenticated: Bool { get }
    func signIn(email: String, password: String) async throws
    func signOut() async throws
}

Coordinator/Navigation Pattern

protocol NavigationCoordinating: AnyObject {
    func navigate(to destination: Destination)
    func dismiss()
    func presentSheet(_ sheet: SheetType)
}