---
name: swift-patrones
description: >
  Patrones Swift: TCA (The Composable Architecture), dependency injection
  basada en protocolos, MVVM con @Observable, Coordinator, Repository pattern,
  Codable avanzado, opaque types y generic constraints. Cargar cuando se diseñe
  arquitectura iOS/macOS, se implemente TCA/MVVM, o se estructuren dependencias.
version: "1.0.0"
herramientasPermitidas: [Read, Glob, Grep]
exclusiones:
  - "No cargar para implementar vistas SwiftUI concretas, async/await o SwiftData — para implementación cargar `swift-experto`."
  - "No cargar para escribir tests Swift — para testing cargar `swift-testing`."
  - "No cargar para errores de compilación Swift o Xcode — para build errors cargar `build-errors-swift`."
  - "No cargar para patrones Android/Kotlin (MVI, ViewModel) — TCA y MVVM con @Observable son patrones específicos de Apple platforms."
evolvable: true  # default para skill estandar
---
# Swift Patrones — TCA, DI, MVVM y Codable

## Cuándo NO cargar

- La pregunta es sobre implementar vistas SwiftUI, async/await concreto o SwiftData — para implementación cargar `swift-experto`.
- La tarea es escribir tests Swift — cargar `swift-testing`.
- Los errores son de compilación Swift o Xcode — cargar `build-errors-swift`.
- El contexto es Android/Kotlin — TCA y MVVM con @Observable son específicos de Apple platforms.

## TCA — The Composable Architecture

TCA organiza cada feature en: `State` (datos), `Action` (eventos), `Reducer` (lógica pura)
y dependencias inyectadas via `DependencyValues`. El State es inmutable; el Reducer retorna
el nuevo estado y `Effect` para operaciones async.

Para el ejemplo completo con Reducer, View y tests de Reducer, ver
[recursos/tca-ejemplo-completo.md](recursos/tca-ejemplo-completo.md).

```swift
// Patrón mínimo
struct MiFeature: Reducer {
    @ObservableState struct State: Equatable { var datos: [Item] = [] }
    enum Action { case cargar; case respuesta([Item]) }
    @Dependency(\.miClient) var client

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .cargar:
                return .run { send in await send(.respuesta(try await client.obtener())) }
            case .respuesta(let items):
                state.datos = items; return .none
            }
        }
    }
}
```

---

## Dependency Injection Basada en Protocolos

```swift
// Protocolo de la dependencia
protocol IFacturasRepository: Sendable {
    func obtener(empresaId: String) async throws -> [Factura]
    func guardar(_ factura: Factura) async throws
}

// Implementación real
final class FacturasRepositoryImpl: IFacturasRepository {
    private let api: IFacturasApi
    private let cache: CachFacturas

    init(api: IFacturasApi, cache: CachFacturas) {
        self.api = api
        self.cache = cache
    }

    func obtener(empresaId: String) async throws -> [Factura] {
        if let cached = await cache.obtener(empresaId: empresaId) { return cached }
        let facturas = try await api.obtenerFacturas(empresaId: empresaId)
        await cache.guardar(facturas, empresaId: empresaId)
        return facturas
    }
}

// TCA DependencyValues — registro de dependencias
struct FacturasClient {
    var obtener: @Sendable () async throws -> [Factura]
    var eliminar: @Sendable (String) async throws -> Void
}

extension FacturasClient: DependencyKey {
    static let liveValue = FacturasClient(
        obtener: { try await FacturasRepositoryImpl.shared.obtener(empresaId: currentUser.empresaId) },
        eliminar: { id in try await FacturasRepositoryImpl.shared.eliminar(id: id) }
    )

    static let testValue = FacturasClient(
        obtener: { [] },
        eliminar: { _ in }
    )
}

extension DependencyValues {
    var facturasClient: FacturasClient {
        get { self[FacturasClient.self] }
        set { self[FacturasClient.self] = newValue }
    }
}
```

---

## MVVM con @Observable

```swift
// @Observable elimina @Published y reduce boilerplate vs ObservableObject
@Observable
final class DetalleFacturaViewModel {
    var factura: Factura?
    var cargando = false
    var error: String?

    // Dependencia inyectada en init (no instanciada internamente)
    private let repository: IFacturasRepository

    init(repository: IFacturasRepository = FacturasRepositoryImpl.shared) {
        self.repository = repository
    }

    func cargar(id: String) async {
        cargando = true
        defer { cargando = false }
        do {
            factura = try await repository.obtenerPorId(id)
        } catch {
            self.error = error.localizedDescription
        }
    }
}

// Vista recibe el ViewModel — no lo instancia
struct DetalleFacturaView: View {
    @State private var viewModel: DetalleFacturaViewModel

    init(facturaId: String, repository: IFacturasRepository = FacturasRepositoryImpl.shared) {
        _viewModel = State(wrappedValue: DetalleFacturaViewModel(repository: repository))
        self.facturaId = facturaId
    }
}
```

---

## Codable Avanzado

```swift
// CodingKeys con nombres distintos al modelo
struct FacturaDto: Codable {
    let id: String
    let folio: String
    let totalNeto: Double
    let fechaEmision: Date

    enum CodingKeys: String, CodingKey {
        case id
        case folio
        case totalNeto = "total_neto"
        case fechaEmision = "fecha_emision"
    }
}

// Decodificación anidada — extraer dato de estructura profunda
struct RespuestaApi: Decodable {
    let factura: FacturaDto

    init(from decoder: Decoder) throws {
        let root = try decoder.container(keyedBy: RootKeys.self)
        let data = try root.nestedContainer(keyedBy: DataKeys.self, forKey: .data)
        factura = try data.decode(FacturaDto.self, forKey: .factura)
    }

    enum RootKeys: String, CodingKey { case data }
    enum DataKeys: String, CodingKey { case factura }
}

// Enum con valor asociado codificable
enum EstadoPago: Codable {
    case pendiente
    case pagado(fecha: Date, referencia: String)

    enum CodingKeys: String, CodingKey { case tipo, fecha, referencia }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        let tipo = try c.decode(String.self, forKey: .tipo)
        switch tipo {
        case "pendiente": self = .pendiente
        case "pagado":
            self = .pagado(
                fecha: try c.decode(Date.self, forKey: .fecha),
                referencia: try c.decode(String.self, forKey: .referencia)
            )
        default: throw DecodingError.dataCorrupted(
            .init(codingPath: decoder.codingPath, debugDescription: "Tipo desconocido: \(tipo)"))
        }
    }
}
```

---

## Opaque Types y Generic Constraints

```swift
// some Protocol — tipo concreto que el compilador infiere, no el llamador
// Útil para ocultar tipos concretos en implementaciones de repositorio
protocol IVistaFactura: View {}

// Retornar some View en lugar de AnyView (más eficiente)
func iconoEstatus(_ estatus: EstatusFactura) -> some View {
    Group {
        switch estatus {
        case .borrador: Image(systemName: "doc")
        case .emitida: Image(systemName: "checkmark.circle").foregroundStyle(.green)
        case .cancelada: Image(systemName: "xmark.circle").foregroundStyle(.red)
        }
    }
}

// Generic constraints — solo aceptar Codable + Identifiable
func guardarEnCache<T: Codable & Identifiable>(_ items: [T], clave: String)
    where T.ID == String
{
    // ...
}
```

---

## Result Type

```swift
// Preferir Result<T, E> sobre throws cuando el error se procesa en el mismo sitio
enum FacturasError: Error, LocalizedError {
    case noEncontrada(id: String)
    case folioDuplicado
    case redNoDisponible(Error)

    var errorDescription: String? {
        switch self {
        case .noEncontrada(let id): "Factura \(id) no encontrada"
        case .folioDuplicado: "Ya existe una factura con ese folio"
        case .redNoDisponible: "Sin conexión a internet"
        }
    }
}

// Mapear throws a Result para UI
func cargarConResult(id: String) async -> Result<Factura, FacturasError> {
    do {
        let factura = try await repo.obtener(id: id)
        return .success(factura)
    } catch let e as FacturasError {
        return .failure(e)
    } catch {
        return .failure(.redNoDisponible(error))
    }
}
```

---

## Anti-patrones

### Herencia profunda — evitar

```swift
// MAL — herencia fuerza acoplamiento
class BaseViewModel { ... }
class FacturasViewModel: BaseViewModel { ... }
class PaginatedFacturasViewModel: FacturasViewModel { ... }

// BIEN — composición con protocolos
protocol Paginable { var pagina: Int { get set } }
@Observable class FacturasViewModel: Paginable { ... }
```

### AnyPublisher en todos lados — evitar en código nuevo

```swift
// Para código nuevo en iOS 17+, usar async/await y @Observable
// AnyPublisher solo cuando se integra con código Combine existente
```

---

## Gotchas / Errores comunes no obvios

**TCA `Store.scope` con un `Reducer` hijo que tiene efectos no cancela esos efectos cuando el padre lo descarta**: si la vista padre descarta la vista hija (que tiene un `Store` scoped), los efectos async del reducer hijo siguen ejecutando. Causa: TCA no cancela los efectos automáticamente al destruir un `Store` scoped a menos que se use `Reducer.ifLet`. Fix: usar `Reducer.ifLet` en el reducer padre para modelar la presencia opcional del estado hijo y garantizar cancelación automática.

**Protocolo con `associated type` no puede usarse como `any MiProtocolo` hasta Swift 5.7+ con `some`/`any`**: en Swift < 5.7, intentar usar un protocolo con `associatedtype` como tipo de variable generaba error "can only be used as a generic constraint". En Swift 5.7+ se usa `any MiProtocolo` explícitamente. Fix: verificar la versión de Swift del target; en proyectos que soportan iOS < 16, puede ser necesario usar type erasure manual (`AnyMiProtocolo`) para código que debe correr en versiones anteriores.

**`Codable` con `enum` que tiene casos asociados requiere implementación manual**: `enum Resultado { case exito(Producto); case error(String) }` no se puede hacer `Codable` automáticamente — el compilador no genera la implementación para enums con valores asociados. Fix: implementar `init(from: Decoder)` y `encode(to: Encoder)` manualmente, o usar una librería como `CodableKit` para el boilerplate.

**MVVM con `@Observable` y propiedades computadas no notifica a la vista cuando las dependencias cambian**: si el `@Observable` ViewModel tiene una propiedad computada `var totalFormateado: String { formatear(total) }`, SwiftUI no trackea la propiedad computada directamente. Causa: `@Observable` trackea propiedades stored, no computed. Fix: convertir la propiedad computada a un stored property que se actualiza en el `didSet` del valor origen, o usar `onChange` en la vista para forzar la actualización.

## Checklist de arquitectura Swift

- [ ] Dependencias inyectadas en init, no instanciadas internamente
- [ ] Protocolos definidos en el módulo que los consume, no en el que los implementa
- [ ] @Observable en lugar de ObservableObject para código iOS 17+
- [ ] Codable usa CodingKeys explícitas cuando los nombres de API difieren del modelo
- [ ] Result<T, E> con tipos de error específicos (no Error genérico)
- [ ] Sin herencia más profunda que 1 nivel — preferir composición con protocolos
