Saltar a contenido

MOB-IOS-001: Arquitectura iOS MedTime

Identificador: MOB-IOS-001 Version: 1.0.0 Fecha: 2025-12-08 Autor: SpecQueen Technical Division Estado: Aprobado



1. Introduccion

1.1. Proposito

Este documento define la arquitectura tecnica de la aplicacion iOS de MedTime. Establece los patrones, estructuras y decisiones arquitectonicas que guian el desarrollo de la app.

MedTime iOS es una aplicacion de gestion de medicamentos que prioriza:

  1. Privacidad: Arquitectura Zero-Knowledge donde el servidor no puede leer datos del usuario
  2. Offline-First: Funcionalidad completa sin conexion a internet
  3. Seguridad: Cifrado E2E de todos los datos sensibles (PHI)
  4. UX: Interfaz intuitiva con SwiftUI moderno

1.2. Alcance

Este documento cubre:

  • Arquitectura de software (Clean Architecture)
  • Patrones de diseno (MVVM, Repository, Coordinator)
  • Persistencia local (SwiftData, KeyChain)
  • Networking (URLSession, async/await)
  • Cifrado (CryptoKit)
  • Sincronizacion offline
  • Extensiones (Widgets, Watch)
  • Testing

1.3. Requisitos del Sistema

Requisito Valor Justificacion
iOS Minimo 15.0 SwiftUI improvements, async/await
Swift 5.9+ Macros, Observation framework
Xcode 15.0+ SwiftData, visionOS preview
Dispositivos iPhone, iPad Universal app
Watch watchOS 9.0+ Companion y standalone

1.4. Referencias

Documento Descripcion
01-arquitectura-tecnica.md Arquitectura base Clean Architecture
04-seguridad-cliente.md Seguridad Zero-Knowledge, E2E
07-testing-strategy.md Estrategia de testing
MTS-OFF-001 Modo offline
MTS-WCH-001 Apple Watch
MTS-WDG-001 Widgets

2. Arquitectura General

2.1. Clean Architecture Overview

MedTime iOS implementa Clean Architecture con tres capas principales:

┌─────────────────────────────────────────────────────────────────┐
│                      PRESENTATION LAYER                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │   Views     │  │ ViewModels  │  │     Coordinators        │ │
│  │  (SwiftUI)  │  │   (MVVM)    │  │    (Navigation)         │ │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│                        DOMAIN LAYER                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │  Entities   │  │  Use Cases  │  │  Repository Protocols   │ │
│  │  (Models)   │  │ (Interactor)│  │       (Ports)           │ │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│                         DATA LAYER                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │Repositories │  │Data Sources │  │       Mappers           │ │
│  │ (Adapters)  │  │(Local/Remote│  │    (DTO <-> Entity)     │ │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Flujo de dependencias:

Presentation → Domain ← Data

- Presentation depende de Domain (usa Use Cases)
- Data depende de Domain (implementa Repository Protocols)
- Domain NO depende de nadie (capa central independiente)

2.2. Estructura de Proyecto

MedTime/
├── App/
│   ├── MedTimeApp.swift              # Entry point
│   ├── AppDelegate.swift             # Push notifications, lifecycle
│   ├── SceneDelegate.swift           # Scene management
│   └── DependencyContainer.swift     # DI container
├── Domain/
│   ├── Entities/
│   │   ├── Medication.swift
│   │   ├── Schedule.swift
│   │   ├── DoseLog.swift
│   │   ├── User.swift
│   │   ├── ClinicalAnalysis.swift
│   │   └── Prescription.swift
│   │
│   ├── UseCases/
│   │   ├── Medications/
│   │   │   ├── GetMedicationsUseCase.swift
│   │   │   ├── AddMedicationUseCase.swift
│   │   │   ├── UpdateMedicationUseCase.swift
│   │   │   └── DeleteMedicationUseCase.swift
│   │   ├── Schedules/
│   │   │   ├── GetSchedulesUseCase.swift
│   │   │   └── UpdateScheduleUseCase.swift
│   │   ├── DoseLogs/
│   │   │   ├── LogDoseUseCase.swift
│   │   │   └── GetDoseHistoryUseCase.swift
│   │   ├── Sync/
│   │   │   ├── SyncDataUseCase.swift
│   │   │   └── ResolveConflictUseCase.swift
│   │   └── Auth/
│   │       ├── LoginUseCase.swift
│   │       ├── LogoutUseCase.swift
│   │       └── RefreshTokenUseCase.swift
│   │
│   ├── Repositories/
│   │   ├── MedicationRepositoryProtocol.swift
│   │   ├── ScheduleRepositoryProtocol.swift
│   │   ├── DoseLogRepositoryProtocol.swift
│   │   ├── UserRepositoryProtocol.swift
│   │   ├── SyncRepositoryProtocol.swift
│   │   └── AuthRepositoryProtocol.swift
│   │
│   └── Errors/
│       ├── DomainError.swift
│       ├── ValidationError.swift
│       └── SyncError.swift
├── Data/
│   ├── Repositories/
│   │   ├── MedicationRepository.swift
│   │   ├── ScheduleRepository.swift
│   │   ├── DoseLogRepository.swift
│   │   ├── UserRepository.swift
│   │   ├── SyncRepository.swift
│   │   └── AuthRepository.swift
│   │
│   ├── DataSources/
│   │   ├── Local/
│   │   │   ├── SwiftDataSource.swift
│   │   │   ├── KeyChainDataSource.swift
│   │   │   └── UserDefaultsDataSource.swift
│   │   └── Remote/
│   │       ├── APIClient.swift
│   │       ├── AuthAPIDataSource.swift
│   │       ├── MedicationAPIDataSource.swift
│   │       ├── SyncAPIDataSource.swift
│   │       └── CatalogAPIDataSource.swift
│   │
│   ├── Models/
│   │   ├── DTOs/
│   │   │   ├── MedicationDTO.swift
│   │   │   ├── ScheduleDTO.swift
│   │   │   ├── SyncBlobDTO.swift
│   │   │   └── AuthResponseDTO.swift
│   │   └── Persistence/
│   │       ├── MedicationEntity.swift      # SwiftData
│   │       ├── ScheduleEntity.swift
│   │       ├── DoseLogEntity.swift
│   │       └── SyncQueueEntity.swift
│   │
│   └── Mappers/
│       ├── MedicationMapper.swift
│       ├── ScheduleMapper.swift
│       ├── DoseLogMapper.swift
│       └── SyncBlobMapper.swift
├── Presentation/
│   ├── Screens/
│   │   ├── Home/
│   │   │   ├── HomeView.swift
│   │   │   ├── HomeViewModel.swift
│   │   │   └── Components/
│   │   │       ├── TodayDosesCard.swift
│   │   │       ├── NextDoseWidget.swift
│   │   │       └── AdherenceRing.swift
│   │   ├── Medications/
│   │   │   ├── MedicationListView.swift
│   │   │   ├── MedicationDetailView.swift
│   │   │   ├── AddMedicationView.swift
│   │   │   └── MedicationViewModel.swift
│   │   ├── Calendar/
│   │   │   ├── CalendarView.swift
│   │   │   ├── DayDetailView.swift
│   │   │   └── CalendarViewModel.swift
│   │   ├── Profile/
│   │   │   ├── ProfileView.swift
│   │   │   ├── SettingsView.swift
│   │   │   └── ProfileViewModel.swift
│   │   └── Auth/
│   │       ├── LoginView.swift
│   │       ├── RegisterView.swift
│   │       └── AuthViewModel.swift
│   │
│   ├── Navigation/
│   │   ├── AppCoordinator.swift
│   │   ├── TabCoordinator.swift
│   │   ├── AuthCoordinator.swift
│   │   └── NavigationPath+Extensions.swift
│   │
│   ├── Components/
│   │   ├── MedicationCard.swift
│   │   ├── DoseButton.swift
│   │   ├── TimeSlotPicker.swift
│   │   ├── LoadingView.swift
│   │   └── ErrorView.swift
│   │
│   └── Theme/
│       ├── Colors.swift
│       ├── Typography.swift
│       ├── Spacing.swift
│       └── Icons.swift
├── Core/
│   ├── Crypto/
│   │   ├── CryptoManager.swift
│   │   ├── KeyDerivation.swift
│   │   ├── BlobEncryption.swift
│   │   └── SecureRandom.swift
│   │
│   ├── Sync/
│   │   ├── SyncManager.swift
│   │   ├── SyncQueue.swift
│   │   ├── ConflictResolver.swift
│   │   └── NetworkMonitor.swift
│   │
│   ├── Notifications/
│   │   ├── NotificationManager.swift
│   │   ├── NotificationScheduler.swift
│   │   └── NotificationActions.swift
│   │
│   └── Extensions/
│       ├── Date+Extensions.swift
│       ├── String+Extensions.swift
│       ├── Data+Extensions.swift
│       └── Publisher+Extensions.swift
├── Extensions/
│   ├── Widget/
│   │   ├── MedTimeWidget.swift
│   │   ├── NextDoseWidget.swift
│   │   ├── AdherenceWidget.swift
│   │   └── WidgetTimelineProvider.swift
│   │
│   └── Watch/
│       ├── WatchApp.swift
│       ├── WatchConnectivityManager.swift
│       ├── ComplicationProvider.swift
│       └── WatchViews/
│           ├── WatchHomeView.swift
│           └── WatchDoseView.swift
├── Resources/
│   ├── Assets.xcassets/
│   ├── Localizable.strings
│   ├── InfoPlist.strings
│   └── PrivacyInfo.xcprivacy
└── Tests/
    ├── UnitTests/
    │   ├── Domain/
    │   ├── Data/
    │   └── Presentation/
    ├── UITests/
    │   └── Flows/
    └── SnapshotTests/
        └── Screens/

2.3. Dependency Injection

MedTime utiliza Factory pattern para DI (ligero, type-safe, SwiftUI-friendly):

// DependencyContainer.swift
import Factory

extension Container {
    // MARK: - Data Sources

    var swiftDataSource: Factory<SwiftDataSourceProtocol> {
        self { SwiftDataSource.shared }
            .singleton
    }

    var keyChainDataSource: Factory<KeyChainDataSourceProtocol> {
        self { KeyChainDataSource() }
            .singleton
    }

    var apiClient: Factory<APIClientProtocol> {
        self { APIClient(baseURL: Config.apiBaseURL) }
            .singleton
    }

    // MARK: - Repositories

    var medicationRepository: Factory<MedicationRepositoryProtocol> {
        self {
            MedicationRepository(
                localDataSource: self.swiftDataSource(),
                remoteDataSource: MedicationAPIDataSource(client: self.apiClient()),
                cryptoManager: self.cryptoManager()
            )
        }
    }

    var syncRepository: Factory<SyncRepositoryProtocol> {
        self {
            SyncRepository(
                localDataSource: self.swiftDataSource(),
                remoteDataSource: SyncAPIDataSource(client: self.apiClient()),
                cryptoManager: self.cryptoManager()
            )
        }
    }

    // MARK: - Use Cases

    var getMedicationsUseCase: Factory<GetMedicationsUseCaseProtocol> {
        self { GetMedicationsUseCase(repository: self.medicationRepository()) }
    }

    var syncDataUseCase: Factory<SyncDataUseCaseProtocol> {
        self {
            SyncDataUseCase(
                syncRepository: self.syncRepository(),
                conflictResolver: self.conflictResolver()
            )
        }
    }

    // MARK: - Core Services

    var cryptoManager: Factory<CryptoManagerProtocol> {
        self {
            CryptoManager(keyChain: self.keyChainDataSource())
        }
        .singleton
    }

    var syncManager: Factory<SyncManagerProtocol> {
        self {
            SyncManager(
                syncUseCase: self.syncDataUseCase(),
                networkMonitor: self.networkMonitor()
            )
        }
        .singleton
    }

    var networkMonitor: Factory<NetworkMonitorProtocol> {
        self { NetworkMonitor() }
            .singleton
    }
}

// Usage in ViewModel
final class MedicationViewModel: ObservableObject {
    @Injected(\.getMedicationsUseCase) private var getMedicationsUseCase
    @Injected(\.syncManager) private var syncManager

    @Published var medications: [Medication] = []
    @Published var isLoading = false
    @Published var error: DomainError?

    @MainActor
    func loadMedications() async {
        isLoading = true
        defer { isLoading = false }

        do {
            medications = try await getMedicationsUseCase.execute()
        } catch let error as DomainError {
            self.error = error
        } catch {
            self.error = .unknown(error)
        }
    }
}

2.4. Principios SOLID

Principio Aplicacion en MedTime
Single Responsibility Cada UseCase tiene una sola responsabilidad
Open/Closed Protocols permiten extension sin modificacion
Liskov Substitution Repositories son intercambiables (mock/real)
Interface Segregation Protocols pequenos y especificos
Dependency Inversion Domain define protocols, Data implementa

3. Capa Domain

3.1. Entities

Las entities son modelos de negocio puros, sin dependencias de frameworks:

// Domain/Entities/Medication.swift
import Foundation

struct Medication: Identifiable, Equatable, Hashable {
    let id: UUID
    var name: String
    var genericName: String?
    var dosage: Dosage
    var form: MedicationForm
    var route: AdministrationRoute
    var frequency: DosageFrequency
    var instructions: String?
    var startDate: Date
    var endDate: Date?
    var isActive: Bool
    var requiresPrescription: Bool
    var inventory: Inventory?
    var reminders: [Reminder]
    let createdAt: Date
    var updatedAt: Date
    var version: Int64

    struct Dosage: Equatable, Hashable {
        var amount: Double
        var unit: DosageUnit

        var displayString: String {
            "\(amount.formatted()) \(unit.abbreviation)"
        }
    }

    struct Inventory: Equatable, Hashable {
        var currentQuantity: Double
        var minimumQuantity: Double
        var packageSize: Int?
        var expirationDate: Date?

        var isLow: Bool {
            currentQuantity <= minimumQuantity
        }

        var daysRemaining: Int? {
            // Calculado basado en frecuencia
            nil
        }
    }
}

// Domain/Entities/DoseLog.swift
struct DoseLog: Identifiable, Equatable {
    let id: UUID
    let medicationId: UUID
    let scheduledTime: Date
    var takenTime: Date?
    var status: DoseStatus
    var skipReason: SkipReason?
    var notes: String?
    let createdAt: Date
    var updatedAt: Date
    var version: Int64

    enum DoseStatus: String, CaseIterable {
        case pending
        case taken
        case skipped
        case missed
        case late
    }

    enum SkipReason: String, CaseIterable {
        case sideEffects
        case forgotToTake
        case outOfMedication
        case doctorAdvised
        case feltBetter
        case other
    }
}

// Domain/Entities/Schedule.swift
struct Schedule: Identifiable, Equatable {
    let id: UUID
    let medicationId: UUID
    var times: [ScheduleTime]
    var daysOfWeek: Set<DayOfWeek>?  // nil = every day
    var isActive: Bool
    let createdAt: Date
    var updatedAt: Date
    var version: Int64

    struct ScheduleTime: Equatable, Hashable {
        var hour: Int
        var minute: Int
        var label: String?  // "Morning", "With breakfast"

        var date: Date {
            Calendar.current.date(
                from: DateComponents(hour: hour, minute: minute)
            ) ?? Date()
        }
    }
}

// Enums compartidos
enum MedicationForm: String, CaseIterable, Codable {
    case tablet, capsule, liquid, injection, cream, patch, inhaler, drops, other

    var icon: String {
        switch self {
        case .tablet: return "pill"
        case .capsule: return "capsule"
        case .liquid: return "drop"
        case .injection: return "syringe"
        case .cream: return "tube"
        case .patch: return "bandage"
        case .inhaler: return "wind"
        case .drops: return "drop"
        case .other: return "pills"
        }
    }
}

enum DosageUnit: String, CaseIterable, Codable {
    case mg, g, ml, units, drops, puffs, tablets, capsules

    var abbreviation: String { rawValue }
}

enum AdministrationRoute: String, CaseIterable, Codable {
    case oral, sublingual, topical, intravenous, intramuscular
    case subcutaneous, inhaled, rectal, ophthalmic, otic, nasal
}

enum DosageFrequency: Equatable, Hashable, Codable {
    case timesPerDay(Int)
    case everyXHours(Int)
    case custom(times: [Date])
    case asNeeded

    var displayString: String {
        switch self {
        case .timesPerDay(let n):
            return "\(n)x al dia"
        case .everyXHours(let h):
            return "Cada \(h) horas"
        case .custom:
            return "Horario personalizado"
        case .asNeeded:
            return "Segun necesidad"
        }
    }
}

enum DayOfWeek: Int, CaseIterable, Codable {
    case sunday = 1, monday, tuesday, wednesday, thursday, friday, saturday
}

3.2. Use Cases

Los Use Cases encapsulan logica de negocio:

// Domain/UseCases/Medications/GetMedicationsUseCase.swift
protocol GetMedicationsUseCaseProtocol {
    func execute(filter: MedicationFilter?) async throws -> [Medication]
}

struct MedicationFilter {
    var isActive: Bool?
    var searchText: String?
    var form: MedicationForm?
}

final class GetMedicationsUseCase: GetMedicationsUseCaseProtocol {
    private let repository: MedicationRepositoryProtocol

    init(repository: MedicationRepositoryProtocol) {
        self.repository = repository
    }

    func execute(filter: MedicationFilter? = nil) async throws -> [Medication] {
        var medications = try await repository.getAllMedications()

        if let filter {
            if let isActive = filter.isActive {
                medications = medications.filter { $0.isActive == isActive }
            }
            if let searchText = filter.searchText, !searchText.isEmpty {
                medications = medications.filter {
                    $0.name.localizedCaseInsensitiveContains(searchText) ||
                    ($0.genericName?.localizedCaseInsensitiveContains(searchText) ?? false)
                }
            }
            if let form = filter.form {
                medications = medications.filter { $0.form == form }
            }
        }

        return medications.sorted { $0.name < $1.name }
    }
}

// Domain/UseCases/DoseLogs/LogDoseUseCase.swift
protocol LogDoseUseCaseProtocol {
    func execute(
        medicationId: UUID,
        scheduledTime: Date,
        status: DoseLog.DoseStatus,
        takenTime: Date?,
        skipReason: DoseLog.SkipReason?,
        notes: String?
    ) async throws -> DoseLog
}

final class LogDoseUseCase: LogDoseUseCaseProtocol {
    private let doseLogRepository: DoseLogRepositoryProtocol
    private let medicationRepository: MedicationRepositoryProtocol
    private let syncManager: SyncManagerProtocol

    init(
        doseLogRepository: DoseLogRepositoryProtocol,
        medicationRepository: MedicationRepositoryProtocol,
        syncManager: SyncManagerProtocol
    ) {
        self.doseLogRepository = doseLogRepository
        self.medicationRepository = medicationRepository
        self.syncManager = syncManager
    }

    func execute(
        medicationId: UUID,
        scheduledTime: Date,
        status: DoseLog.DoseStatus,
        takenTime: Date?,
        skipReason: DoseLog.SkipReason?,
        notes: String?
    ) async throws -> DoseLog {
        // Validar que el medicamento existe
        guard let medication = try await medicationRepository.getMedication(by: medicationId) else {
            throw DomainError.medicationNotFound(medicationId)
        }

        // Crear el log
        let doseLog = DoseLog(
            id: UUID(),
            medicationId: medicationId,
            scheduledTime: scheduledTime,
            takenTime: takenTime ?? (status == .taken ? Date() : nil),
            status: status,
            skipReason: skipReason,
            notes: notes,
            createdAt: Date(),
            updatedAt: Date(),
            version: 1
        )

        // Guardar localmente
        let savedLog = try await doseLogRepository.save(doseLog)

        // Actualizar inventario si se tomo la dosis
        if status == .taken, var inventory = medication.inventory {
            inventory.currentQuantity -= medication.dosage.amount
            var updatedMedication = medication
            updatedMedication.inventory = inventory
            updatedMedication.updatedAt = Date()
            updatedMedication.version += 1
            try await medicationRepository.save(updatedMedication)
        }

        // Encolar para sync
        await syncManager.enqueue(.doseLog(savedLog))

        return savedLog
    }
}

// Domain/UseCases/Sync/SyncDataUseCase.swift
protocol SyncDataUseCaseProtocol {
    func execute() async throws -> SyncResult
    func resolveConflict(_ conflict: SyncConflict, resolution: ConflictResolution) async throws
}

struct SyncResult {
    let uploaded: Int
    let downloaded: Int
    let conflicts: [SyncConflict]
    let errors: [SyncError]
}

struct SyncConflict {
    let entityType: EntityType
    let entityId: UUID
    let localVersion: Int64
    let serverVersion: Int64
    let localData: Data
    let serverData: Data
}

enum ConflictResolution {
    case keepLocal
    case keepServer
    case merge(Data)
}

final class SyncDataUseCase: SyncDataUseCaseProtocol {
    private let syncRepository: SyncRepositoryProtocol
    private let conflictResolver: ConflictResolverProtocol

    init(
        syncRepository: SyncRepositoryProtocol,
        conflictResolver: ConflictResolverProtocol
    ) {
        self.syncRepository = syncRepository
        self.conflictResolver = conflictResolver
    }

    func execute() async throws -> SyncResult {
        // 1. Obtener cambios locales pendientes
        let pendingChanges = try await syncRepository.getPendingChanges()

        // 2. Obtener version del servidor
        let serverVersion = try await syncRepository.getServerVersion()
        let localVersion = try await syncRepository.getLocalVersion()

        var uploaded = 0
        var downloaded = 0
        var conflicts: [SyncConflict] = []
        var errors: [SyncError] = []

        // 3. Subir cambios locales
        for change in pendingChanges {
            do {
                try await syncRepository.push(change)
                uploaded += 1
            } catch let error as SyncError {
                if case .conflict(let conflict) = error {
                    conflicts.append(conflict)
                } else {
                    errors.append(error)
                }
            }
        }

        // 4. Descargar cambios del servidor
        if serverVersion > localVersion {
            let serverChanges = try await syncRepository.pull(since: localVersion)
            for change in serverChanges {
                do {
                    try await syncRepository.applyServerChange(change)
                    downloaded += 1
                } catch let error as SyncError {
                    errors.append(error)
                }
            }
        }

        // 5. Auto-resolver conflictos simples (LWW)
        for conflict in conflicts {
            if await conflictResolver.canAutoResolve(conflict) {
                let resolution = await conflictResolver.autoResolve(conflict)
                try await resolveConflict(conflict, resolution: resolution)
            }
        }

        return SyncResult(
            uploaded: uploaded,
            downloaded: downloaded,
            conflicts: conflicts.filter { !conflictResolver.canAutoResolve($0) },
            errors: errors
        )
    }

    func resolveConflict(_ conflict: SyncConflict, resolution: ConflictResolution) async throws {
        try await syncRepository.resolveConflict(conflict, resolution: resolution)
    }
}

3.3. Repository Protocols

Los Repository Protocols definen contratos que la capa Data debe implementar:

// Domain/Repositories/MedicationRepositoryProtocol.swift
protocol MedicationRepositoryProtocol {
    func getAllMedications() async throws -> [Medication]
    func getMedication(by id: UUID) async throws -> Medication?
    func save(_ medication: Medication) async throws -> Medication
    func delete(_ medication: Medication) async throws
    func search(query: String) async throws -> [Medication]
}

// Domain/Repositories/DoseLogRepositoryProtocol.swift
protocol DoseLogRepositoryProtocol {
    func getLogs(for medicationId: UUID, dateRange: DateInterval?) async throws -> [DoseLog]
    func getLog(by id: UUID) async throws -> DoseLog?
    func save(_ log: DoseLog) async throws -> DoseLog
    func delete(_ log: DoseLog) async throws
    func getPendingDoses(for date: Date) async throws -> [DoseLog]
    func getAdherenceStats(for medicationId: UUID, days: Int) async throws -> AdherenceStats
}

struct AdherenceStats {
    let totalDoses: Int
    let takenDoses: Int
    let missedDoses: Int
    let skippedDoses: Int

    var adherenceRate: Double {
        guard totalDoses > 0 else { return 0 }
        return Double(takenDoses) / Double(totalDoses)
    }
}

// Domain/Repositories/SyncRepositoryProtocol.swift
protocol SyncRepositoryProtocol {
    func getPendingChanges() async throws -> [SyncChange]
    func getServerVersion() async throws -> Int64
    func getLocalVersion() async throws -> Int64
    func push(_ change: SyncChange) async throws
    func pull(since version: Int64) async throws -> [SyncChange]
    func applyServerChange(_ change: SyncChange) async throws
    func resolveConflict(_ conflict: SyncConflict, resolution: ConflictResolution) async throws
}

struct SyncChange {
    let entityType: EntityType
    let entityId: UUID
    let operation: SyncOperation
    let encryptedBlob: Data
    let version: Int64
    let timestamp: Date
}

enum SyncOperation: String {
    case create, update, delete
}

enum EntityType: String, CaseIterable {
    case medication, schedule, doseLog, clinicalAnalysis, prescription
}

3.4. Error Handling

// Domain/Errors/DomainError.swift
enum DomainError: Error, LocalizedError {
    // Entity errors
    case medicationNotFound(UUID)
    case scheduleNotFound(UUID)
    case doseLogNotFound(UUID)
    case userNotFound

    // Validation errors
    case invalidDosage(String)
    case invalidSchedule(String)
    case invalidDateRange

    // Business rule violations
    case medicationAlreadyExists(name: String)
    case cannotDeleteActiveMedication
    case scheduleConflict(existingTime: Date)

    // Sync errors
    case syncFailed(underlying: Error)
    case conflictDetected(SyncConflict)
    case offlineOperationFailed

    // Crypto errors
    case encryptionFailed
    case decryptionFailed
    case keyNotFound

    // Generic
    case unknown(Error)

    var errorDescription: String? {
        switch self {
        case .medicationNotFound(let id):
            return "Medicamento no encontrado: \(id)"
        case .invalidDosage(let reason):
            return "Dosificacion invalida: \(reason)"
        case .syncFailed(let error):
            return "Error de sincronizacion: \(error.localizedDescription)"
        case .encryptionFailed:
            return "Error al cifrar datos"
        case .decryptionFailed:
            return "Error al descifrar datos"
        // ... otros casos
        default:
            return "Error desconocido"
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .syncFailed:
            return "Verifica tu conexion a internet e intenta de nuevo"
        case .offlineOperationFailed:
            return "Esta operacion se completara cuando tengas conexion"
        case .decryptionFailed:
            return "Verifica que tu sesion este activa"
        default:
            return nil
        }
    }
}

4. Capa Data

4.1. Repository Implementations

// Data/Repositories/MedicationRepository.swift
final class MedicationRepository: MedicationRepositoryProtocol {
    private let localDataSource: SwiftDataSourceProtocol
    private let remoteDataSource: MedicationAPIDataSourceProtocol
    private let cryptoManager: CryptoManagerProtocol
    private let mapper: MedicationMapper

    init(
        localDataSource: SwiftDataSourceProtocol,
        remoteDataSource: MedicationAPIDataSourceProtocol,
        cryptoManager: CryptoManagerProtocol,
        mapper: MedicationMapper = MedicationMapper()
    ) {
        self.localDataSource = localDataSource
        self.remoteDataSource = remoteDataSource
        self.cryptoManager = cryptoManager
        self.mapper = mapper
    }

    func getAllMedications() async throws -> [Medication] {
        // Siempre lee de local (offline-first)
        let entities = try await localDataSource.fetchAll(MedicationEntity.self)
        return try entities.map { entity in
            // Descifrar blob
            let decryptedData = try cryptoManager.decrypt(entity.encryptedBlob)
            return try mapper.toDomain(entity, decryptedData: decryptedData)
        }
    }

    func getMedication(by id: UUID) async throws -> Medication? {
        guard let entity = try await localDataSource.fetch(
            MedicationEntity.self,
            predicate: #Predicate { $0.id == id }
        ).first else {
            return nil
        }

        let decryptedData = try cryptoManager.decrypt(entity.encryptedBlob)
        return try mapper.toDomain(entity, decryptedData: decryptedData)
    }

    func save(_ medication: Medication) async throws -> Medication {
        // 1. Cifrar datos sensibles
        let sensitiveData = try mapper.toSensitiveData(medication)
        let encryptedBlob = try cryptoManager.encrypt(sensitiveData)

        // 2. Crear/actualizar entity
        let entity = mapper.toEntity(medication, encryptedBlob: encryptedBlob)

        // 3. Guardar localmente
        try await localDataSource.save(entity)

        // 4. Marcar para sync
        try await localDataSource.addToSyncQueue(
            entityType: .medication,
            entityId: medication.id,
            operation: .update,
            encryptedBlob: encryptedBlob
        )

        return medication
    }

    func delete(_ medication: Medication) async throws {
        // 1. Soft delete local
        try await localDataSource.delete(
            MedicationEntity.self,
            predicate: #Predicate { $0.id == medication.id }
        )

        // 2. Marcar para sync
        try await localDataSource.addToSyncQueue(
            entityType: .medication,
            entityId: medication.id,
            operation: .delete,
            encryptedBlob: Data()  // Vacio para delete
        )
    }

    func search(query: String) async throws -> [Medication] {
        // Busqueda local en campos no cifrados
        let entities = try await localDataSource.fetch(
            MedicationEntity.self,
            predicate: #Predicate {
                $0.nameSearchable.localizedStandardContains(query)
            }
        )

        return try entities.map { entity in
            let decryptedData = try cryptoManager.decrypt(entity.encryptedBlob)
            return try mapper.toDomain(entity, decryptedData: decryptedData)
        }
    }
}

4.2. Data Sources

// Data/DataSources/Local/SwiftDataSource.swift
protocol SwiftDataSourceProtocol {
    func fetchAll<T: PersistentModel>(_ type: T.Type) async throws -> [T]
    func fetch<T: PersistentModel>(_ type: T.Type, predicate: Predicate<T>?) async throws -> [T]
    func save<T: PersistentModel>(_ entity: T) async throws
    func delete<T: PersistentModel>(_ type: T.Type, predicate: Predicate<T>) async throws
    func addToSyncQueue(entityType: EntityType, entityId: UUID, operation: SyncOperation, encryptedBlob: Data) async throws
}

@MainActor
final class SwiftDataSource: SwiftDataSourceProtocol {
    static let shared = SwiftDataSource()

    private let modelContainer: ModelContainer
    private let modelContext: ModelContext

    private init() {
        let schema = Schema([
            MedicationEntity.self,
            ScheduleEntity.self,
            DoseLogEntity.self,
            SyncQueueEntity.self,
            CacheEntity.self
        ])

        let modelConfiguration = ModelConfiguration(
            schema: schema,
            isStoredInMemoryOnly: false,
            cloudKitDatabase: .none  // No iCloud - usamos nuestro sync
        )

        do {
            modelContainer = try ModelContainer(
                for: schema,
                configurations: [modelConfiguration]
            )
            modelContext = modelContainer.mainContext
        } catch {
            fatalError("Failed to create ModelContainer: \(error)")
        }
    }

    func fetchAll<T: PersistentModel>(_ type: T.Type) async throws -> [T] {
        let descriptor = FetchDescriptor<T>()
        return try modelContext.fetch(descriptor)
    }

    func fetch<T: PersistentModel>(_ type: T.Type, predicate: Predicate<T>?) async throws -> [T] {
        var descriptor = FetchDescriptor<T>()
        descriptor.predicate = predicate
        return try modelContext.fetch(descriptor)
    }

    func save<T: PersistentModel>(_ entity: T) async throws {
        modelContext.insert(entity)
        try modelContext.save()
    }

    func delete<T: PersistentModel>(_ type: T.Type, predicate: Predicate<T>) async throws {
        let entities = try await fetch(type, predicate: predicate)
        for entity in entities {
            modelContext.delete(entity)
        }
        try modelContext.save()
    }

    func addToSyncQueue(
        entityType: EntityType,
        entityId: UUID,
        operation: SyncOperation,
        encryptedBlob: Data
    ) async throws {
        let queueItem = SyncQueueEntity(
            id: UUID(),
            entityType: entityType.rawValue,
            entityId: entityId,
            operation: operation.rawValue,
            encryptedBlob: encryptedBlob,
            createdAt: Date(),
            retryCount: 0
        )
        modelContext.insert(queueItem)
        try modelContext.save()
    }
}

// Data/DataSources/Remote/APIClient.swift
protocol APIClientProtocol {
    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
    func request(_ endpoint: Endpoint) async throws
    func upload(_ data: Data, to endpoint: Endpoint) async throws -> UploadResponse
}

final class APIClient: APIClientProtocol {
    private let baseURL: URL
    private let session: URLSession
    private let authInterceptor: AuthInterceptorProtocol
    private let retryInterceptor: RetryInterceptorProtocol
    private let decoder: JSONDecoder
    private let encoder: JSONEncoder

    init(
        baseURL: URL,
        session: URLSession = .shared,
        authInterceptor: AuthInterceptorProtocol = AuthInterceptor(),
        retryInterceptor: RetryInterceptorProtocol = RetryInterceptor()
    ) {
        self.baseURL = baseURL
        self.session = session
        self.authInterceptor = authInterceptor
        self.retryInterceptor = retryInterceptor
        self.decoder = JSONDecoder()
        self.decoder.dateDecodingStrategy = .iso8601
        self.encoder = JSONEncoder()
        self.encoder.dateEncodingStrategy = .iso8601
    }

    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
        var request = try buildRequest(for: endpoint)
        request = try await authInterceptor.intercept(request)

        let (data, response) = try await retryInterceptor.execute {
            try await self.session.data(for: request)
        }

        try validateResponse(response)
        return try decoder.decode(T.self, from: data)
    }

    func request(_ endpoint: Endpoint) async throws {
        var request = try buildRequest(for: endpoint)
        request = try await authInterceptor.intercept(request)

        let (_, response) = try await retryInterceptor.execute {
            try await self.session.data(for: request)
        }

        try validateResponse(response)
    }

    func upload(_ data: Data, to endpoint: Endpoint) async throws -> UploadResponse {
        var request = try buildRequest(for: endpoint)
        request = try await authInterceptor.intercept(request)
        request.httpBody = data

        let (responseData, response) = try await session.data(for: request)
        try validateResponse(response)

        return try decoder.decode(UploadResponse.self, from: responseData)
    }

    private func buildRequest(for endpoint: Endpoint) throws -> URLRequest {
        guard let url = URL(string: endpoint.path, relativeTo: baseURL) else {
            throw APIError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        request.allHTTPHeaderFields = endpoint.headers

        if let body = endpoint.body {
            request.httpBody = try encoder.encode(body)
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        return request
    }

    private func validateResponse(_ response: URLResponse) throws {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }

        switch httpResponse.statusCode {
        case 200...299:
            return
        case 401:
            throw APIError.unauthorized
        case 403:
            throw APIError.forbidden
        case 404:
            throw APIError.notFound
        case 409:
            throw APIError.conflict
        case 429:
            throw APIError.rateLimited(retryAfter: httpResponse.value(forHTTPHeaderField: "Retry-After"))
        case 500...599:
            throw APIError.serverError(httpResponse.statusCode)
        default:
            throw APIError.httpError(httpResponse.statusCode)
        }
    }
}

// Endpoint definition
struct Endpoint {
    let path: String
    let method: HTTPMethod
    let headers: [String: String]
    let body: Encodable?

    enum HTTPMethod: String {
        case GET, POST, PUT, PATCH, DELETE
    }
}

// API-specific data sources
final class SyncAPIDataSource {
    private let client: APIClientProtocol

    init(client: APIClientProtocol) {
        self.client = client
    }

    func push(blobs: [SyncBlobDTO]) async throws -> PushResponse {
        let endpoint = Endpoint(
            path: "/v1/sync/push",
            method: .POST,
            headers: [:],
            body: PushRequest(blobs: blobs)
        )
        return try await client.request(endpoint)
    }

    func pull(since version: Int64, deviceId: UUID) async throws -> PullResponse {
        let endpoint = Endpoint(
            path: "/v1/sync/pull?since_version=\(version)&device_id=\(deviceId)",
            method: .GET,
            headers: [:],
            body: nil
        )
        return try await client.request(endpoint)
    }
}

4.3. Mappers

// Data/Mappers/MedicationMapper.swift
struct MedicationMapper {

    func toDomain(_ entity: MedicationEntity, decryptedData: Data) throws -> Medication {
        // Decodificar datos sensibles del blob descifrado
        let sensitiveData = try JSONDecoder().decode(
            MedicationSensitiveData.self,
            from: decryptedData
        )

        return Medication(
            id: entity.id,
            name: sensitiveData.name,
            genericName: sensitiveData.genericName,
            dosage: Medication.Dosage(
                amount: sensitiveData.dosageAmount,
                unit: DosageUnit(rawValue: sensitiveData.dosageUnit) ?? .mg
            ),
            form: MedicationForm(rawValue: entity.form) ?? .tablet,
            route: AdministrationRoute(rawValue: entity.route) ?? .oral,
            frequency: decodeFrequency(sensitiveData.frequency),
            instructions: sensitiveData.instructions,
            startDate: entity.startDate,
            endDate: entity.endDate,
            isActive: entity.isActive,
            requiresPrescription: sensitiveData.requiresPrescription,
            inventory: sensitiveData.inventory.map { inv in
                Medication.Inventory(
                    currentQuantity: inv.currentQuantity,
                    minimumQuantity: inv.minimumQuantity,
                    packageSize: inv.packageSize,
                    expirationDate: inv.expirationDate
                )
            },
            reminders: [],  // Loaded separately
            createdAt: entity.createdAt,
            updatedAt: entity.updatedAt,
            version: entity.version
        )
    }

    func toEntity(_ medication: Medication, encryptedBlob: Data) -> MedicationEntity {
        MedicationEntity(
            id: medication.id,
            nameSearchable: medication.name.lowercased(),  // Para busqueda
            form: medication.form.rawValue,
            route: medication.route.rawValue,
            startDate: medication.startDate,
            endDate: medication.endDate,
            isActive: medication.isActive,
            encryptedBlob: encryptedBlob,
            createdAt: medication.createdAt,
            updatedAt: medication.updatedAt,
            version: medication.version
        )
    }

    func toSensitiveData(_ medication: Medication) throws -> Data {
        let sensitiveData = MedicationSensitiveData(
            name: medication.name,
            genericName: medication.genericName,
            dosageAmount: medication.dosage.amount,
            dosageUnit: medication.dosage.unit.rawValue,
            frequency: encodeFrequency(medication.frequency),
            instructions: medication.instructions,
            requiresPrescription: medication.requiresPrescription,
            inventory: medication.inventory.map { inv in
                MedicationSensitiveData.InventoryData(
                    currentQuantity: inv.currentQuantity,
                    minimumQuantity: inv.minimumQuantity,
                    packageSize: inv.packageSize,
                    expirationDate: inv.expirationDate
                )
            }
        )
        return try JSONEncoder().encode(sensitiveData)
    }

    private func encodeFrequency(_ frequency: DosageFrequency) -> String {
        // Encode to JSON string for storage
        switch frequency {
        case .timesPerDay(let n):
            return "times_per_day:\(n)"
        case .everyXHours(let h):
            return "every_x_hours:\(h)"
        case .asNeeded:
            return "as_needed"
        case .custom(let times):
            let timestamps = times.map { $0.timeIntervalSince1970 }
            return "custom:\(timestamps.map(String.init).joined(separator: ","))"
        }
    }

    private func decodeFrequency(_ encoded: String) -> DosageFrequency {
        let parts = encoded.split(separator: ":")
        guard let type = parts.first else { return .asNeeded }

        switch type {
        case "times_per_day":
            return .timesPerDay(Int(parts[1]) ?? 1)
        case "every_x_hours":
            return .everyXHours(Int(parts[1]) ?? 8)
        case "as_needed":
            return .asNeeded
        case "custom":
            // Parse timestamps
            return .asNeeded  // Simplified
        default:
            return .asNeeded
        }
    }
}

// Internal DTO for sensitive data
private struct MedicationSensitiveData: Codable {
    let name: String
    let genericName: String?
    let dosageAmount: Double
    let dosageUnit: String
    let frequency: String
    let instructions: String?
    let requiresPrescription: Bool
    let inventory: InventoryData?

    struct InventoryData: Codable {
        let currentQuantity: Double
        let minimumQuantity: Double
        let packageSize: Int?
        let expirationDate: Date?
    }
}

4.4. Cache Strategy

// Data/Cache/CacheManager.swift
protocol CacheManagerProtocol {
    func get<T: Codable>(_ key: String) -> T?
    func set<T: Codable>(_ value: T, for key: String, ttl: TimeInterval?)
    func remove(_ key: String)
    func clear()
}

final class CacheManager: CacheManagerProtocol {
    private let localDataSource: SwiftDataSourceProtocol
    private let memoryCache = NSCache<NSString, CacheEntry>()

    init(localDataSource: SwiftDataSourceProtocol) {
        self.localDataSource = localDataSource
        memoryCache.countLimit = 100
        memoryCache.totalCostLimit = 50 * 1024 * 1024  // 50MB
    }

    func get<T: Codable>(_ key: String) -> T? {
        // 1. Check memory cache
        if let entry = memoryCache.object(forKey: key as NSString),
           !entry.isExpired {
            return try? JSONDecoder().decode(T.self, from: entry.data)
        }

        // 2. Check disk cache (async wrapper needed in real impl)
        // Simplified for illustration
        return nil
    }

    func set<T: Codable>(_ value: T, for key: String, ttl: TimeInterval? = nil) {
        guard let data = try? JSONEncoder().encode(value) else { return }

        let entry = CacheEntry(
            data: data,
            expiresAt: ttl.map { Date().addingTimeInterval($0) }
        )

        memoryCache.setObject(entry, forKey: key as NSString, cost: data.count)

        // Also persist to disk for offline
        Task {
            try? await localDataSource.save(
                CacheEntity(key: key, data: data, expiresAt: entry.expiresAt)
            )
        }
    }

    func remove(_ key: String) {
        memoryCache.removeObject(forKey: key as NSString)
    }

    func clear() {
        memoryCache.removeAllObjects()
    }
}

private class CacheEntry: NSObject {
    let data: Data
    let expiresAt: Date?

    init(data: Data, expiresAt: Date?) {
        self.data = data
        self.expiresAt = expiresAt
    }

    var isExpired: Bool {
        guard let expiresAt else { return false }
        return Date() > expiresAt
    }
}

5. Capa Presentation

5.1. SwiftUI Views

// Presentation/Screens/Home/HomeView.swift
import SwiftUI

struct HomeView: View {
    @StateObject private var viewModel = HomeViewModel()
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: Spacing.large) {
                    // Header con saludo
                    GreetingHeader(userName: viewModel.userName)

                    // Siguiente dosis
                    if let nextDose = viewModel.nextDose {
                        NextDoseCard(
                            dose: nextDose,
                            onTakeDose: { viewModel.takeDose(nextDose) },
                            onSkipDose: { viewModel.showSkipSheet = true }
                        )
                    }

                    // Dosis del dia
                    TodayDosesSection(
                        doses: viewModel.todayDoses,
                        onTakeDose: viewModel.takeDose,
                        onViewAll: { viewModel.showAllDoses = true }
                    )

                    // Adherencia semanal
                    AdherenceCard(stats: viewModel.weeklyAdherence)

                    // Alertas
                    if !viewModel.alerts.isEmpty {
                        AlertsSection(alerts: viewModel.alerts)
                    }
                }
                .padding()
            }
            .navigationTitle("MedTime")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        viewModel.showProfile = true
                    } label: {
                        Image(systemName: "person.circle")
                    }
                }
            }
            .refreshable {
                await viewModel.refresh()
            }
            .sheet(isPresented: $viewModel.showSkipSheet) {
                SkipDoseSheet(
                    onSkip: viewModel.skipDose
                )
            }
            .sheet(isPresented: $viewModel.showProfile) {
                ProfileView()
            }
            .onChange(of: scenePhase) { _, newPhase in
                if newPhase == .active {
                    Task { await viewModel.refresh() }
                }
            }
        }
        .task {
            await viewModel.loadInitialData()
        }
    }
}

// Presentation/Screens/Home/Components/NextDoseCard.swift
struct NextDoseCard: View {
    let dose: ScheduledDose
    let onTakeDose: () -> Void
    let onSkipDose: () -> Void

    @State private var timeRemaining: TimeInterval = 0
    let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack(alignment: .leading, spacing: Spacing.medium) {
            HStack {
                Text("Siguiente dosis")
                    .font(.headline)
                    .foregroundStyle(.secondary)

                Spacer()

                Text(timeRemainingText)
                    .font(.caption)
                    .foregroundStyle(timeRemaining < 300 ? .red : .secondary)
            }

            HStack(spacing: Spacing.medium) {
                // Medication icon
                Image(systemName: dose.medication.form.icon)
                    .font(.title)
                    .foregroundStyle(.accent)
                    .frame(width: 50, height: 50)
                    .background(Color.accent.opacity(0.1))
                    .clipShape(Circle())

                VStack(alignment: .leading, spacing: 4) {
                    Text(dose.medication.name)
                        .font(.title3)
                        .fontWeight(.semibold)

                    Text(dose.medication.dosage.displayString)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)

                    Text(dose.scheduledTime, style: .time)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }

                Spacer()
            }

            HStack(spacing: Spacing.medium) {
                Button(action: onSkipDose) {
                    Label("Omitir", systemImage: "xmark")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.bordered)

                Button(action: onTakeDose) {
                    Label("Tomar", systemImage: "checkmark")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
        .background(Color(.systemBackground))
        .clipShape(RoundedRectangle(cornerRadius: 16))
        .shadow(color: .black.opacity(0.05), radius: 10, y: 4)
        .onReceive(timer) { _ in
            updateTimeRemaining()
        }
        .onAppear {
            updateTimeRemaining()
        }
    }

    private var timeRemainingText: String {
        if timeRemaining < 0 {
            return "Atrasado \(abs(Int(timeRemaining / 60))) min"
        } else if timeRemaining < 3600 {
            return "En \(Int(timeRemaining / 60)) min"
        } else {
            return "En \(Int(timeRemaining / 3600)) h"
        }
    }

    private func updateTimeRemaining() {
        timeRemaining = dose.scheduledTime.timeIntervalSinceNow
    }
}

// Presentation/Screens/Medications/MedicationListView.swift
struct MedicationListView: View {
    @StateObject private var viewModel = MedicationListViewModel()
    @State private var searchText = ""
    @State private var showAddMedication = false

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView()
                } else if viewModel.medications.isEmpty {
                    EmptyMedicationsView(onAdd: { showAddMedication = true })
                } else {
                    List {
                        ForEach(filteredMedications) { medication in
                            NavigationLink(value: medication) {
                                MedicationRow(medication: medication)
                            }
                        }
                        .onDelete(perform: viewModel.deleteMedications)
                    }
                    .listStyle(.insetGrouped)
                }
            }
            .navigationTitle("Medicamentos")
            .navigationDestination(for: Medication.self) { medication in
                MedicationDetailView(medication: medication)
            }
            .searchable(text: $searchText, prompt: "Buscar medicamento")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        showAddMedication = true
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $showAddMedication) {
                AddMedicationView()
            }
            .alert(
                "Error",
                isPresented: $viewModel.showError,
                presenting: viewModel.error
            ) { _ in
                Button("OK") {}
            } message: { error in
                Text(error.localizedDescription)
            }
        }
        .task {
            await viewModel.loadMedications()
        }
    }

    private var filteredMedications: [Medication] {
        if searchText.isEmpty {
            return viewModel.medications
        }
        return viewModel.medications.filter {
            $0.name.localizedCaseInsensitiveContains(searchText) ||
            ($0.genericName?.localizedCaseInsensitiveContains(searchText) ?? false)
        }
    }
}

5.2. ViewModels

// Presentation/Screens/Home/HomeViewModel.swift
import Foundation
import Combine
import Factory

@MainActor
final class HomeViewModel: ObservableObject {
    // Dependencies
    @Injected(\.getMedicationsUseCase) private var getMedicationsUseCase
    @Injected(\.getSchedulesUseCase) private var getSchedulesUseCase
    @Injected(\.logDoseUseCase) private var logDoseUseCase
    @Injected(\.getAdherenceUseCase) private var getAdherenceUseCase
    @Injected(\.syncManager) private var syncManager

    // Published state
    @Published var userName: String = ""
    @Published var nextDose: ScheduledDose?
    @Published var todayDoses: [ScheduledDose] = []
    @Published var weeklyAdherence: AdherenceStats?
    @Published var alerts: [Alert] = []
    @Published var isLoading = false
    @Published var error: DomainError?
    @Published var showError = false
    @Published var showSkipSheet = false
    @Published var showAllDoses = false
    @Published var showProfile = false

    private var cancellables = Set<AnyCancellable>()

    init() {
        setupBindings()
    }

    private func setupBindings() {
        // Escuchar cambios de sync
        syncManager.syncStatePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] state in
                if case .completed = state {
                    Task { await self?.refresh() }
                }
            }
            .store(in: &cancellables)
    }

    func loadInitialData() async {
        isLoading = true
        defer { isLoading = false }

        do {
            async let medicationsTask = getMedicationsUseCase.execute(filter: .init(isActive: true))
            async let schedulesTask = getSchedulesUseCase.execute()
            async let adherenceTask = getAdherenceUseCase.execute(days: 7)

            let (medications, schedules, adherence) = try await (
                medicationsTask,
                schedulesTask,
                adherenceTask
            )

            // Combinar medicamentos y schedules para crear dosis programadas
            todayDoses = buildTodayDoses(medications: medications, schedules: schedules)
            nextDose = todayDoses.first { $0.status == .pending && $0.scheduledTime > Date() }
            weeklyAdherence = adherence

            // Cargar alertas
            alerts = buildAlerts(medications: medications)

        } catch let domainError as DomainError {
            self.error = domainError
            self.showError = true
        } catch {
            self.error = .unknown(error)
            self.showError = true
        }
    }

    func refresh() async {
        // Trigger sync primero
        await syncManager.sync()

        // Recargar datos
        await loadInitialData()
    }

    func takeDose(_ dose: ScheduledDose) {
        Task {
            do {
                _ = try await logDoseUseCase.execute(
                    medicationId: dose.medication.id,
                    scheduledTime: dose.scheduledTime,
                    status: .taken,
                    takenTime: Date(),
                    skipReason: nil,
                    notes: nil
                )

                // Actualizar UI
                await loadInitialData()

            } catch let domainError as DomainError {
                self.error = domainError
                self.showError = true
            } catch {
                self.error = .unknown(error)
                self.showError = true
            }
        }
    }

    func skipDose(reason: DoseLog.SkipReason, notes: String?) {
        guard let dose = nextDose else { return }

        Task {
            do {
                _ = try await logDoseUseCase.execute(
                    medicationId: dose.medication.id,
                    scheduledTime: dose.scheduledTime,
                    status: .skipped,
                    takenTime: nil,
                    skipReason: reason,
                    notes: notes
                )

                showSkipSheet = false
                await loadInitialData()

            } catch let domainError as DomainError {
                self.error = domainError
                self.showError = true
            } catch {
                self.error = .unknown(error)
                self.showError = true
            }
        }
    }

    private func buildTodayDoses(medications: [Medication], schedules: [Schedule]) -> [ScheduledDose] {
        // Combinar medicamentos con sus schedules para el dia actual
        var doses: [ScheduledDose] = []
        let today = Calendar.current.startOfDay(for: Date())

        for medication in medications {
            guard let schedule = schedules.first(where: { $0.medicationId == medication.id }),
                  schedule.isActive else { continue }

            for time in schedule.times {
                let scheduledTime = Calendar.current.date(
                    bySettingHour: time.hour,
                    minute: time.minute,
                    second: 0,
                    of: today
                ) ?? today

                doses.append(ScheduledDose(
                    id: UUID(),
                    medication: medication,
                    scheduledTime: scheduledTime,
                    status: .pending  // Would check against dose logs
                ))
            }
        }

        return doses.sorted { $0.scheduledTime < $1.scheduledTime }
    }

    private func buildAlerts(medications: [Medication]) -> [Alert] {
        var alerts: [Alert] = []

        for medication in medications {
            // Low inventory
            if let inventory = medication.inventory, inventory.isLow {
                alerts.append(Alert(
                    type: .lowInventory,
                    medication: medication,
                    message: "Quedan \(Int(inventory.currentQuantity)) \(medication.dosage.unit.abbreviation)"
                ))
            }

            // Expiring soon
            if let expirationDate = medication.inventory?.expirationDate,
               expirationDate < Date().addingTimeInterval(30 * 24 * 3600) {
                alerts.append(Alert(
                    type: .expiringSoon,
                    medication: medication,
                    message: "Expira el \(expirationDate.formatted(date: .abbreviated, time: .omitted))"
                ))
            }
        }

        return alerts
    }
}

struct ScheduledDose: Identifiable {
    let id: UUID
    let medication: Medication
    let scheduledTime: Date
    var status: DoseLog.DoseStatus
}

struct Alert: Identifiable {
    let id = UUID()
    let type: AlertType
    let medication: Medication
    let message: String

    enum AlertType {
        case lowInventory
        case expiringSoon
        case missedDose
        case interaction
    }
}

5.3. Navigation

// Presentation/Navigation/AppCoordinator.swift
import SwiftUI

@MainActor
final class AppCoordinator: ObservableObject {
    @Published var selectedTab: Tab = .home
    @Published var authState: AuthState = .loading
    @Published var path = NavigationPath()

    @Injected(\.authUseCase) private var authUseCase

    enum Tab {
        case home, medications, calendar, profile
    }

    enum AuthState {
        case loading
        case authenticated(User)
        case unauthenticated
    }

    init() {
        checkAuthState()
    }

    func checkAuthState() {
        Task {
            do {
                if let user = try await authUseCase.getCurrentUser() {
                    authState = .authenticated(user)
                } else {
                    authState = .unauthenticated
                }
            } catch {
                authState = .unauthenticated
            }
        }
    }

    func logout() {
        Task {
            try? await authUseCase.logout()
            authState = .unauthenticated
        }
    }
}

// Root View
struct RootView: View {
    @StateObject private var coordinator = AppCoordinator()

    var body: some View {
        Group {
            switch coordinator.authState {
            case .loading:
                LoadingView()

            case .authenticated:
                MainTabView()
                    .environmentObject(coordinator)

            case .unauthenticated:
                AuthView()
                    .environmentObject(coordinator)
            }
        }
    }
}

struct MainTabView: View {
    @EnvironmentObject var coordinator: AppCoordinator

    var body: some View {
        TabView(selection: $coordinator.selectedTab) {
            HomeView()
                .tabItem {
                    Label("Inicio", systemImage: "house")
                }
                .tag(AppCoordinator.Tab.home)

            MedicationListView()
                .tabItem {
                    Label("Medicamentos", systemImage: "pills")
                }
                .tag(AppCoordinator.Tab.medications)

            CalendarView()
                .tabItem {
                    Label("Calendario", systemImage: "calendar")
                }
                .tag(AppCoordinator.Tab.calendar)

            ProfileView()
                .tabItem {
                    Label("Perfil", systemImage: "person")
                }
                .tag(AppCoordinator.Tab.profile)
        }
    }
}

5.4. UI State Management

// Presentation/State/ViewState.swift
enum ViewState<T> {
    case idle
    case loading
    case loaded(T)
    case error(Error)

    var isLoading: Bool {
        if case .loading = self { return true }
        return false
    }

    var value: T? {
        if case .loaded(let value) = self { return value }
        return nil
    }

    var error: Error? {
        if case .error(let error) = self { return error }
        return nil
    }
}

// Usage in ViewModel
@MainActor
final class MedicationDetailViewModel: ObservableObject {
    @Published var state: ViewState<MedicationDetail> = .idle

    func load(medicationId: UUID) async {
        state = .loading

        do {
            let detail = try await loadMedicationDetail(medicationId)
            state = .loaded(detail)
        } catch {
            state = .error(error)
        }
    }
}

// Generic StateView
struct StateView<T, Content: View, LoadingView: View, ErrorView: View>: View {
    let state: ViewState<T>
    @ViewBuilder let content: (T) -> Content
    @ViewBuilder let loading: () -> LoadingView
    @ViewBuilder let error: (Error) -> ErrorView

    var body: some View {
        switch state {
        case .idle:
            EmptyView()
        case .loading:
            loading()
        case .loaded(let value):
            content(value)
        case .error(let err):
            error(err)
        }
    }
}

6. Persistencia Local

6.1. SwiftData Models

// Data/Models/Persistence/MedicationEntity.swift
import SwiftData

@Model
final class MedicationEntity {
    @Attribute(.unique) var id: UUID
    var nameSearchable: String  // Para busqueda (no cifrado)
    var form: String
    var route: String
    var startDate: Date
    var endDate: Date?
    var isActive: Bool
    var encryptedBlob: Data  // Datos sensibles cifrados
    var createdAt: Date
    var updatedAt: Date
    var version: Int64

    // Relationships
    @Relationship(deleteRule: .cascade, inverse: \ScheduleEntity.medication)
    var schedules: [ScheduleEntity]?

    @Relationship(deleteRule: .cascade, inverse: \DoseLogEntity.medication)
    var doseLogs: [DoseLogEntity]?

    init(
        id: UUID,
        nameSearchable: String,
        form: String,
        route: String,
        startDate: Date,
        endDate: Date?,
        isActive: Bool,
        encryptedBlob: Data,
        createdAt: Date,
        updatedAt: Date,
        version: Int64
    ) {
        self.id = id
        self.nameSearchable = nameSearchable
        self.form = form
        self.route = route
        self.startDate = startDate
        self.endDate = endDate
        self.isActive = isActive
        self.encryptedBlob = encryptedBlob
        self.createdAt = createdAt
        self.updatedAt = updatedAt
        self.version = version
    }
}

@Model
final class ScheduleEntity {
    @Attribute(.unique) var id: UUID
    var medicationId: UUID
    var timesJson: Data  // Serialized [ScheduleTime]
    var daysOfWeekJson: Data?  // Serialized Set<DayOfWeek>
    var isActive: Bool
    var createdAt: Date
    var updatedAt: Date
    var version: Int64

    var medication: MedicationEntity?

    init(
        id: UUID,
        medicationId: UUID,
        timesJson: Data,
        daysOfWeekJson: Data?,
        isActive: Bool,
        createdAt: Date,
        updatedAt: Date,
        version: Int64
    ) {
        self.id = id
        self.medicationId = medicationId
        self.timesJson = timesJson
        self.daysOfWeekJson = daysOfWeekJson
        self.isActive = isActive
        self.createdAt = createdAt
        self.updatedAt = updatedAt
        self.version = version
    }
}

@Model
final class DoseLogEntity {
    @Attribute(.unique) var id: UUID
    var medicationId: UUID
    var scheduledTime: Date
    var takenTime: Date?
    var status: String
    var skipReason: String?
    var encryptedNotes: Data?  // Notes cifradas
    var createdAt: Date
    var updatedAt: Date
    var version: Int64

    var medication: MedicationEntity?

    init(
        id: UUID,
        medicationId: UUID,
        scheduledTime: Date,
        takenTime: Date?,
        status: String,
        skipReason: String?,
        encryptedNotes: Data?,
        createdAt: Date,
        updatedAt: Date,
        version: Int64
    ) {
        self.id = id
        self.medicationId = medicationId
        self.scheduledTime = scheduledTime
        self.takenTime = takenTime
        self.status = status
        self.skipReason = skipReason
        self.encryptedNotes = encryptedNotes
        self.createdAt = createdAt
        self.updatedAt = updatedAt
        self.version = version
    }
}

@Model
final class SyncQueueEntity {
    @Attribute(.unique) var id: UUID
    var entityType: String
    var entityId: UUID
    var operation: String
    var encryptedBlob: Data
    var createdAt: Date
    var retryCount: Int
    var lastError: String?

    init(
        id: UUID,
        entityType: String,
        entityId: UUID,
        operation: String,
        encryptedBlob: Data,
        createdAt: Date,
        retryCount: Int,
        lastError: String? = nil
    ) {
        self.id = id
        self.entityType = entityType
        self.entityId = entityId
        self.operation = operation
        self.encryptedBlob = encryptedBlob
        self.createdAt = createdAt
        self.retryCount = retryCount
        self.lastError = lastError
    }
}

6.2. KeyChain Storage

// Core/Crypto/KeyChainDataSource.swift
import Security

protocol KeyChainDataSourceProtocol {
    func save(_ data: Data, for key: String) throws
    func get(_ key: String) throws -> Data?
    func delete(_ key: String) throws
    func exists(_ key: String) -> Bool
}

final class KeyChainDataSource: KeyChainDataSourceProtocol {
    private let service: String
    private let accessGroup: String?

    init(
        service: String = Bundle.main.bundleIdentifier ?? "com.medtime",
        accessGroup: String? = nil
    ) {
        self.service = service
        self.accessGroup = accessGroup
    }

    func save(_ data: Data, for key: String) throws {
        // Eliminar existente primero
        try? delete(key)

        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]

        if let accessGroup {
            query[kSecAttrAccessGroup as String] = accessGroup
        }

        let status = SecItemAdd(query as CFDictionary, nil)

        guard status == errSecSuccess else {
            throw KeyChainError.saveFailed(status)
        }
    }

    func get(_ key: String) throws -> Data? {
        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        if let accessGroup {
            query[kSecAttrAccessGroup as String] = accessGroup
        }

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        switch status {
        case errSecSuccess:
            return result as? Data
        case errSecItemNotFound:
            return nil
        default:
            throw KeyChainError.readFailed(status)
        }
    }

    func delete(_ key: String) throws {
        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key
        ]

        if let accessGroup {
            query[kSecAttrAccessGroup as String] = accessGroup
        }

        let status = SecItemDelete(query as CFDictionary)

        guard status == errSecSuccess || status == errSecItemNotFound else {
            throw KeyChainError.deleteFailed(status)
        }
    }

    func exists(_ key: String) -> Bool {
        do {
            return try get(key) != nil
        } catch {
            return false
        }
    }
}

enum KeyChainError: Error {
    case saveFailed(OSStatus)
    case readFailed(OSStatus)
    case deleteFailed(OSStatus)
}

// Keys used in KeyChain
enum KeyChainKey: String {
    case masterKey = "medtime.master_key"
    case authToken = "medtime.auth_token"
    case refreshToken = "medtime.refresh_token"
    case deviceId = "medtime.device_id"
    case biometricKey = "medtime.biometric_key"
}

6.3. UserDefaults

// Data/DataSources/Local/UserDefaultsDataSource.swift
import Foundation

protocol UserDefaultsDataSourceProtocol {
    func get<T: Codable>(_ key: UserDefaultsKey) -> T?
    func set<T: Codable>(_ value: T, for key: UserDefaultsKey)
    func remove(_ key: UserDefaultsKey)
}

enum UserDefaultsKey: String {
    // Preferences
    case notificationsEnabled = "notifications_enabled"
    case soundEnabled = "sound_enabled"
    case hapticEnabled = "haptic_enabled"
    case reminderMinutesBefore = "reminder_minutes_before"

    // App State
    case lastSyncVersion = "last_sync_version"
    case lastSyncDate = "last_sync_date"
    case onboardingCompleted = "onboarding_completed"
    case privacyConsentVersion = "privacy_consent_version"

    // Cache
    case cachedUserName = "cached_user_name"
    case cachedTier = "cached_tier"
}

final class UserDefaultsDataSource: UserDefaultsDataSourceProtocol {
    private let defaults: UserDefaults
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    init(suiteName: String? = nil) {
        if let suiteName {
            self.defaults = UserDefaults(suiteName: suiteName) ?? .standard
        } else {
            self.defaults = .standard
        }
    }

    func get<T: Codable>(_ key: UserDefaultsKey) -> T? {
        guard let data = defaults.data(forKey: key.rawValue) else {
            return nil
        }
        return try? decoder.decode(T.self, from: data)
    }

    func set<T: Codable>(_ value: T, for key: UserDefaultsKey) {
        guard let data = try? encoder.encode(value) else { return }
        defaults.set(data, forKey: key.rawValue)
    }

    func remove(_ key: UserDefaultsKey) {
        defaults.removeObject(forKey: key.rawValue)
    }
}

// Convenience property wrappers
@propertyWrapper
struct UserDefault<T: Codable> {
    let key: UserDefaultsKey
    let defaultValue: T
    let dataSource: UserDefaultsDataSourceProtocol

    var wrappedValue: T {
        get { dataSource.get(key) ?? defaultValue }
        set { dataSource.set(newValue, for: key) }
    }
}

6.4. Migraciones

// Data/Migrations/MigrationManager.swift
import SwiftData

final class MigrationManager {
    static let currentSchemaVersion = 1

    static func migrate(container: ModelContainer) async throws {
        let storedVersion = UserDefaults.standard.integer(forKey: "schema_version")

        guard storedVersion < currentSchemaVersion else { return }

        // Run migrations sequentially
        if storedVersion < 1 {
            try await migrateToV1(container: container)
        }

        // Update stored version
        UserDefaults.standard.set(currentSchemaVersion, forKey: "schema_version")
    }

    private static func migrateToV1(container: ModelContainer) async throws {
        // Initial schema - no migration needed
    }
}

// Schema versioning with SwiftData
enum SchemaV1: VersionedSchema {
    static var versionIdentifier: Schema.Version = .init(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [
            MedicationEntity.self,
            ScheduleEntity.self,
            DoseLogEntity.self,
            SyncQueueEntity.self,
            CacheEntity.self
        ]
    }
}

// Future migrations would add SchemaV2, etc.

7. Networking

7.1. API Client

(Ver seccion 4.2 para implementacion completa de APIClient)

7.2. Request/Response

// Data/DataSources/Remote/Endpoints.swift
enum APIEndpoint {
    // Auth
    case login(email: String, password: String)
    case register(email: String, password: String, name: String)
    case refreshToken
    case logout

    // Sync
    case syncPush(blobs: [SyncBlobDTO])
    case syncPull(sinceVersion: Int64, deviceId: UUID)

    // Catalog
    case searchDrugs(query: String, page: Int, pageSize: Int)
    case getDrug(id: UUID)
    case getCatalogBundle

    // Studies
    case getStudiesCatalog
    case getStudyUpdates(sinceVersion: String)

    var endpoint: Endpoint {
        switch self {
        case .login(let email, let password):
            return Endpoint(
                path: "/v1/auth/login",
                method: .POST,
                headers: [:],
                body: LoginRequest(email: email, password: password)
            )

        case .register(let email, let password, let name):
            return Endpoint(
                path: "/v1/auth/register",
                method: .POST,
                headers: [:],
                body: RegisterRequest(email: email, password: password, name: name)
            )

        case .refreshToken:
            return Endpoint(
                path: "/v1/auth/refresh",
                method: .POST,
                headers: [:],
                body: nil
            )

        case .logout:
            return Endpoint(
                path: "/v1/auth/logout",
                method: .POST,
                headers: [:],
                body: nil
            )

        case .syncPush(let blobs):
            return Endpoint(
                path: "/v1/sync/push",
                method: .POST,
                headers: [:],
                body: PushRequest(blobs: blobs)
            )

        case .syncPull(let sinceVersion, let deviceId):
            return Endpoint(
                path: "/v1/sync/pull?since_version=\(sinceVersion)&device_id=\(deviceId)",
                method: .GET,
                headers: [:],
                body: nil
            )

        case .searchDrugs(let query, let page, let pageSize):
            let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query
            return Endpoint(
                path: "/v1/catalog/drugs?q=\(encodedQuery)&page=\(page)&page_size=\(pageSize)",
                method: .GET,
                headers: ["X-Privacy-Consent": "accepted"],
                body: nil
            )

        case .getDrug(let id):
            return Endpoint(
                path: "/v1/catalog/drugs/\(id)",
                method: .GET,
                headers: [:],
                body: nil
            )

        case .getCatalogBundle:
            return Endpoint(
                path: "/v1/catalog/bundle",
                method: .GET,
                headers: [:],
                body: nil
            )

        case .getStudiesCatalog:
            return Endpoint(
                path: "/v1/catalog/studies",
                method: .GET,
                headers: [:],
                body: nil
            )

        case .getStudyUpdates(let sinceVersion):
            return Endpoint(
                path: "/v1/catalog/studies/updates?since_version=\(sinceVersion)",
                method: .GET,
                headers: [:],
                body: nil
            )
        }
    }
}

// DTOs
struct LoginRequest: Encodable {
    let email: String
    let password: String
}

struct RegisterRequest: Encodable {
    let email: String
    let password: String
    let name: String
}

struct PushRequest: Encodable {
    let blobs: [SyncBlobDTO]
}

struct SyncBlobDTO: Codable {
    let entityId: UUID
    let entityType: String
    let encryptedBlob: Data
    let clientVersion: Int64
    let isDeleted: Bool

    enum CodingKeys: String, CodingKey {
        case entityId = "entity_id"
        case entityType = "entity_type"
        case encryptedBlob = "encrypted_blob"
        case clientVersion = "client_version"
        case isDeleted = "is_deleted"
    }
}

struct PullResponse: Decodable {
    let blobs: [SyncBlobDTO]
    let serverVersion: Int64
    let hasMore: Bool

    enum CodingKeys: String, CodingKey {
        case blobs
        case serverVersion = "server_version"
        case hasMore = "has_more"
    }
}

struct PushResponse: Decodable {
    let accepted: [AcceptedBlob]
    let rejected: [RejectedBlob]
    let serverVersion: Int64

    struct AcceptedBlob: Decodable {
        let entityId: UUID
        let newVersion: Int64

        enum CodingKeys: String, CodingKey {
            case entityId = "entity_id"
            case newVersion = "new_version"
        }
    }

    struct RejectedBlob: Decodable {
        let entityId: UUID
        let reason: String
        let serverVersion: Int64

        enum CodingKeys: String, CodingKey {
            case entityId = "entity_id"
            case reason
            case serverVersion = "server_version"
        }
    }

    enum CodingKeys: String, CodingKey {
        case accepted
        case rejected
        case serverVersion = "server_version"
    }
}

7.3. Interceptors

// Data/DataSources/Remote/Interceptors/AuthInterceptor.swift
protocol AuthInterceptorProtocol {
    func intercept(_ request: URLRequest) async throws -> URLRequest
}

final class AuthInterceptor: AuthInterceptorProtocol {
    private let keyChain: KeyChainDataSourceProtocol
    private let tokenRefresher: TokenRefresherProtocol

    init(
        keyChain: KeyChainDataSourceProtocol = KeyChainDataSource(),
        tokenRefresher: TokenRefresherProtocol? = nil
    ) {
        self.keyChain = keyChain
        self.tokenRefresher = tokenRefresher ?? TokenRefresher(keyChain: keyChain)
    }

    func intercept(_ request: URLRequest) async throws -> URLRequest {
        var request = request

        // Verificar si el endpoint requiere auth
        guard !isPublicEndpoint(request.url) else {
            return request
        }

        // Obtener token
        guard let tokenData = try keyChain.get(KeyChainKey.authToken.rawValue),
              let token = String(data: tokenData, encoding: .utf8) else {
            throw APIError.unauthorized
        }

        // Verificar expiracion
        if isTokenExpired(token) {
            let newToken = try await tokenRefresher.refresh()
            request.setValue("Bearer \(newToken)", forHTTPHeaderField: "Authorization")
        } else {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }

        return request
    }

    private func isPublicEndpoint(_ url: URL?) -> Bool {
        guard let path = url?.path else { return false }
        let publicPaths = [
            "/v1/auth/login",
            "/v1/auth/register",
            "/v1/catalog/bundle"
        ]
        return publicPaths.contains { path.hasPrefix($0) }
    }

    private func isTokenExpired(_ token: String) -> Bool {
        // Decode JWT and check exp claim
        guard let payload = decodeJWTPayload(token),
              let exp = payload["exp"] as? TimeInterval else {
            return true
        }
        return Date(timeIntervalSince1970: exp) < Date()
    }

    private func decodeJWTPayload(_ token: String) -> [String: Any]? {
        let parts = token.split(separator: ".")
        guard parts.count == 3,
              let payloadData = Data(base64Encoded: String(parts[1]).base64Padded) else {
            return nil
        }
        return try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
    }
}

// Data/DataSources/Remote/Interceptors/RetryInterceptor.swift
protocol RetryInterceptorProtocol {
    func execute<T>(_ operation: () async throws -> T) async throws -> T
}

final class RetryInterceptor: RetryInterceptorProtocol {
    private let maxRetries: Int
    private let retryDelay: TimeInterval

    init(maxRetries: Int = 3, retryDelay: TimeInterval = 1.0) {
        self.maxRetries = maxRetries
        self.retryDelay = retryDelay
    }

    func execute<T>(_ operation: () async throws -> T) async throws -> T {
        var lastError: Error?

        for attempt in 0..<maxRetries {
            do {
                return try await operation()
            } catch let error as APIError {
                lastError = error

                // No retry for these errors
                switch error {
                case .unauthorized, .forbidden, .notFound, .conflict:
                    throw error
                case .rateLimited(let retryAfter):
                    if let retryAfter, let seconds = Int(retryAfter) {
                        try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000)
                    }
                    continue
                case .serverError, .httpError, .networkError:
                    // Exponential backoff
                    let delay = retryDelay * pow(2.0, Double(attempt))
                    try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
                    continue
                default:
                    throw error
                }
            } catch {
                lastError = error
                // Exponential backoff for unknown errors
                let delay = retryDelay * pow(2.0, Double(attempt))
                try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
            }
        }

        throw lastError ?? APIError.unknown
    }
}

7.4. Error Mapping

// Data/DataSources/Remote/APIError.swift
enum APIError: Error, LocalizedError {
    case invalidURL
    case invalidResponse
    case unauthorized
    case forbidden
    case notFound
    case conflict
    case rateLimited(retryAfter: String?)
    case serverError(Int)
    case httpError(Int)
    case networkError(Error)
    case decodingError(Error)
    case unknown

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "URL invalida"
        case .invalidResponse:
            return "Respuesta invalida del servidor"
        case .unauthorized:
            return "Sesion expirada. Inicia sesion nuevamente."
        case .forbidden:
            return "No tienes permiso para esta accion"
        case .notFound:
            return "Recurso no encontrado"
        case .conflict:
            return "Conflicto de datos. Sincroniza e intenta de nuevo."
        case .rateLimited:
            return "Demasiadas solicitudes. Espera un momento."
        case .serverError(let code):
            return "Error del servidor (\(code))"
        case .httpError(let code):
            return "Error HTTP (\(code))"
        case .networkError:
            return "Error de red. Verifica tu conexion."
        case .decodingError:
            return "Error al procesar datos"
        case .unknown:
            return "Error desconocido"
        }
    }

    var isRetryable: Bool {
        switch self {
        case .serverError, .networkError, .rateLimited:
            return true
        default:
            return false
        }
    }
}

// Mapping to DomainError
extension APIError {
    func toDomainError() -> DomainError {
        switch self {
        case .unauthorized:
            return .userNotFound
        case .conflict:
            return .syncFailed(underlying: self)
        case .networkError:
            return .offlineOperationFailed
        default:
            return .unknown(self)
        }
    }
}

8. Cifrado E2E

8.1. CryptoKit Implementation

// Core/Crypto/CryptoManager.swift
import CryptoKit
import Foundation

protocol CryptoManagerProtocol {
    func generateMasterKey() throws -> SymmetricKey
    func deriveMasterKey(from password: String, salt: Data) throws -> SymmetricKey
    func encrypt(_ data: Data) throws -> Data
    func decrypt(_ encryptedData: Data) throws -> Data
    func hash(_ data: Data) -> String
}

final class CryptoManager: CryptoManagerProtocol {
    private let keyChain: KeyChainDataSourceProtocol
    private var masterKey: SymmetricKey?

    // Crypto constants
    private let keySize = SymmetricKeySize.bits256
    private let saltSize = 32
    private let iterationCount = 100_000

    init(keyChain: KeyChainDataSourceProtocol) {
        self.keyChain = keyChain
        loadMasterKey()
    }

    // MARK: - Key Generation

    func generateMasterKey() throws -> SymmetricKey {
        let key = SymmetricKey(size: keySize)
        try saveMasterKey(key)
        self.masterKey = key
        return key
    }

    func deriveMasterKey(from password: String, salt: Data) throws -> SymmetricKey {
        guard let passwordData = password.data(using: .utf8) else {
            throw CryptoError.invalidPassword
        }

        // PBKDF2 derivation
        var derivedKey = Data(count: 32)
        let result = derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
            salt.withUnsafeBytes { saltBytes in
                passwordData.withUnsafeBytes { passwordBytes in
                    CCKeyDerivationPBKDF(
                        CCPBKDFAlgorithm(kCCPBKDF2),
                        passwordBytes.baseAddress?.assumingMemoryBound(to: Int8.self),
                        passwordData.count,
                        saltBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
                        salt.count,
                        CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                        UInt32(iterationCount),
                        derivedKeyBytes.baseAddress?.assumingMemoryBound(to: UInt8.self),
                        32
                    )
                }
            }
        }

        guard result == kCCSuccess else {
            throw CryptoError.keyDerivationFailed
        }

        let key = SymmetricKey(data: derivedKey)
        try saveMasterKey(key)
        self.masterKey = key
        return key
    }

    // MARK: - Encryption

    func encrypt(_ data: Data) throws -> Data {
        guard let key = masterKey else {
            throw CryptoError.keyNotFound
        }

        // Generate random nonce
        let nonce = try AES.GCM.Nonce()

        // Encrypt with AES-GCM
        let sealedBox = try AES.GCM.seal(data, using: key, nonce: nonce)

        // Return combined: nonce + ciphertext + tag
        guard let combined = sealedBox.combined else {
            throw CryptoError.encryptionFailed
        }

        return combined
    }

    func decrypt(_ encryptedData: Data) throws -> Data {
        guard let key = masterKey else {
            throw CryptoError.keyNotFound
        }

        // Parse sealed box from combined data
        let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)

        // Decrypt
        let decryptedData = try AES.GCM.open(sealedBox, using: key)

        return decryptedData
    }

    // MARK: - Hashing

    func hash(_ data: Data) -> String {
        let digest = SHA256.hash(data: data)
        return digest.map { String(format: "%02x", $0) }.joined()
    }

    // MARK: - Private

    private func loadMasterKey() {
        guard let keyData = try? keyChain.get(KeyChainKey.masterKey.rawValue) else {
            return
        }
        masterKey = SymmetricKey(data: keyData)
    }

    private func saveMasterKey(_ key: SymmetricKey) throws {
        let keyData = key.withUnsafeBytes { Data($0) }
        try keyChain.save(keyData, for: KeyChainKey.masterKey.rawValue)
    }
}

enum CryptoError: Error {
    case keyNotFound
    case keyDerivationFailed
    case encryptionFailed
    case decryptionFailed
    case invalidPassword
    case invalidData
    case notInitialized
}

8.2. Patron de Inicializacion Segura (Actor-Based)

CRITICO: El CryptoManager debe garantizar inicializacion thread-safe para evitar race conditions durante acceso concurrente. Usar patron actor con ensureInitialized().

// Core/Crypto/CryptoManagerActor.swift
import CryptoKit
import Foundation
import Swift_Argon2  // Swift-Argon2 library

/// Actor-based CryptoManager para inicializacion thread-safe
/// Resuelve PERF-004: race condition en inicializacion concurrente
actor CryptoManagerActor: CryptoManagerProtocol {

    // MARK: - State

    private enum InitializationState {
        case uninitialized
        case initializing(Task<Void, Error>)
        case initialized(SymmetricKey)
        case failed(Error)
    }

    private var state: InitializationState = .uninitialized
    private let keyChain: KeyChainDataSourceProtocol

    // Argon2id parameters (Decision del Director #2 - DV2)
    private let argon2Memory: UInt32 = 65536     // 64 MiB
    private let argon2Iterations: UInt32 = 3     // t=3
    private let argon2Parallelism: UInt32 = 4    // p=4
    private let keySize = 32                      // 256 bits

    // MARK: - Init

    init(keyChain: KeyChainDataSourceProtocol) {
        self.keyChain = keyChain
    }

    // MARK: - Public API

    /// Garantiza que el CryptoManager esta inicializado antes de cualquier operacion
    /// Patron: ensureInitialized() - llamar antes de encrypt/decrypt
    func ensureInitialized() async throws {
        switch state {
        case .uninitialized:
            // Iniciar inicializacion
            let task = Task {
                try await self.performInitialization()
            }
            state = .initializing(task)
            try await task.value

        case .initializing(let task):
            // Esperar inicializacion en progreso (evita race condition)
            try await task.value

        case .initialized:
            // Ya inicializado, no-op
            return

        case .failed(let error):
            // Re-throw error previo
            throw error
        }
    }

    func encrypt(_ data: Data) async throws -> Data {
        try await ensureInitialized()

        guard case .initialized(let key) = state else {
            throw CryptoError.notInitialized
        }

        let nonce = try AES.GCM.Nonce()
        let sealedBox = try AES.GCM.seal(data, using: key, nonce: nonce)

        guard let combined = sealedBox.combined else {
            throw CryptoError.encryptionFailed
        }

        return combined
    }

    func decrypt(_ encryptedData: Data) async throws -> Data {
        try await ensureInitialized()

        guard case .initialized(let key) = state else {
            throw CryptoError.notInitialized
        }

        let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
        return try AES.GCM.open(sealedBox, using: key)
    }

    func deriveMasterKey(from password: String, salt: Data) async throws -> SymmetricKey {
        guard let passwordData = password.data(using: .utf8) else {
            throw CryptoError.invalidPassword
        }

        // Argon2id key derivation (reemplaza PBKDF2)
        let derivedKey = try Argon2.hash(
            password: passwordData,
            salt: salt,
            iterations: argon2Iterations,
            memory: argon2Memory,
            parallelism: argon2Parallelism,
            length: UInt32(keySize),
            variant: .id
        )

        let key = SymmetricKey(data: derivedKey)
        try await saveMasterKey(key)
        state = .initialized(key)

        return key
    }

    func generateMasterKey() async throws -> SymmetricKey {
        let key = SymmetricKey(size: .bits256)
        try await saveMasterKey(key)
        state = .initialized(key)
        return key
    }

    nonisolated func hash(_ data: Data) -> String {
        let digest = SHA256.hash(data: data)
        return digest.map { String(format: "%02x", $0) }.joined()
    }

    // MARK: - Private

    private func performInitialization() async throws {
        do {
            if let keyData = try? keyChain.get(KeyChainKey.masterKey.rawValue) {
                let key = SymmetricKey(data: keyData)
                state = .initialized(key)
            } else {
                // No key found - waiting for setup or derivation
                state = .uninitialized
            }
        } catch {
            state = .failed(error)
            throw error
        }
    }

    private func saveMasterKey(_ key: SymmetricKey) async throws {
        let keyData = key.withUnsafeBytes { Data($0) }
        try keyChain.save(keyData, for: KeyChainKey.masterKey.rawValue)
    }
}

// MARK: - Usage Example

/*
 // Uso correcto del CryptoManagerActor

 class MedicationRepository {
     private let crypto: CryptoManagerActor

     func saveMedication(_ med: Medication) async throws {
         // ensureInitialized() es llamado automaticamente por encrypt()
         let encrypted = try await crypto.encrypt(med.toData())
         try await database.save(encrypted)
     }
 }

 // Multiples llamadas concurrentes son seguras:
 async let result1 = crypto.encrypt(data1)
 async let result2 = crypto.encrypt(data2)
 async let result3 = crypto.encrypt(data3)
 // Todas esperan la misma inicializacion sin race condition
*/

8.3. Key Management

// Core/Crypto/KeyManager.swift
protocol KeyManagerProtocol {
    func setupKeys(password: String) async throws
    func unlockWithBiometrics() async throws -> Bool
    func changePassword(from oldPassword: String, to newPassword: String) async throws
    func deleteAllKeys() throws
}

final class KeyManager: KeyManagerProtocol {
    private let cryptoManager: CryptoManagerProtocol
    private let keyChain: KeyChainDataSourceProtocol
    private let biometricAuth: BiometricAuthProtocol

    init(
        cryptoManager: CryptoManagerProtocol,
        keyChain: KeyChainDataSourceProtocol,
        biometricAuth: BiometricAuthProtocol
    ) {
        self.cryptoManager = cryptoManager
        self.keyChain = keyChain
        self.biometricAuth = biometricAuth
    }

    func setupKeys(password: String) async throws {
        // Generate random salt
        var salt = Data(count: 32)
        let result = salt.withUnsafeMutableBytes {
            SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
        }
        guard result == errSecSuccess else {
            throw CryptoError.keyDerivationFailed
        }

        // Save salt
        try keyChain.save(salt, for: "master_key_salt")

        // Derive and save master key
        _ = try cryptoManager.deriveMasterKey(from: password, salt: salt)

        // Optionally enable biometrics
        if await biometricAuth.isAvailable() {
            try await enableBiometrics(password: password)
        }
    }

    func unlockWithBiometrics() async throws -> Bool {
        guard await biometricAuth.isAvailable() else {
            return false
        }

        let success = await biometricAuth.authenticate(reason: "Desbloquear MedTime")

        if success {
            // Load encrypted password from keychain (protected by biometric)
            if let encryptedPassword = try keyChain.get(KeyChainKey.biometricKey.rawValue),
               let salt = try keyChain.get("master_key_salt") {
                // The biometric key is the password itself, stored with biometric protection
                let password = String(data: encryptedPassword, encoding: .utf8) ?? ""
                _ = try cryptoManager.deriveMasterKey(from: password, salt: salt)
                return true
            }
        }

        return false
    }

    func changePassword(from oldPassword: String, to newPassword: String) async throws {
        // Verify old password
        guard let salt = try keyChain.get("master_key_salt") else {
            throw CryptoError.keyNotFound
        }

        // Re-derive with old password to verify
        _ = try cryptoManager.deriveMasterKey(from: oldPassword, salt: salt)

        // Generate new salt
        var newSalt = Data(count: 32)
        _ = newSalt.withUnsafeMutableBytes {
            SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
        }

        // Re-encrypt all data with new key
        // This would involve:
        // 1. Decrypt all blobs with old key
        // 2. Generate new key
        // 3. Re-encrypt all blobs with new key
        // 4. Save new salt

        try keyChain.save(newSalt, for: "master_key_salt")
        _ = try cryptoManager.deriveMasterKey(from: newPassword, salt: newSalt)

        // Update biometric key if enabled
        if await biometricAuth.isAvailable() {
            try await enableBiometrics(password: newPassword)
        }
    }

    func deleteAllKeys() throws {
        try keyChain.delete(KeyChainKey.masterKey.rawValue)
        try keyChain.delete("master_key_salt")
        try keyChain.delete(KeyChainKey.biometricKey.rawValue)
    }

    private func enableBiometrics(password: String) async throws {
        // Store password in keychain with biometric protection
        guard let passwordData = password.data(using: .utf8) else {
            throw CryptoError.invalidPassword
        }

        // This requires special keychain access control
        var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: KeyChainKey.biometricKey.rawValue,
            kSecValueData as String: passwordData,
            kSecAttrAccessControl as String: try createBiometricAccessControl()
        ]

        SecItemDelete(query as CFDictionary)
        let status = SecItemAdd(query as CFDictionary, nil)

        guard status == errSecSuccess else {
            throw KeyChainError.saveFailed(status)
        }
    }

    private func createBiometricAccessControl() throws -> SecAccessControl {
        var error: Unmanaged<CFError>?
        guard let access = SecAccessControlCreateWithFlags(
            kCFAllocatorDefault,
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
            .biometryCurrentSet,
            &error
        ) else {
            throw error?.takeRetainedValue() ?? CryptoError.keyDerivationFailed
        }
        return access
    }
}

8.4. Blob Encryption

// Core/Crypto/BlobEncryption.swift
protocol BlobEncryptionProtocol {
    func encryptBlob<T: Encodable>(_ object: T) throws -> Data
    func decryptBlob<T: Decodable>(_ data: Data, as type: T.Type) throws -> T
}

final class BlobEncryption: BlobEncryptionProtocol {
    private let cryptoManager: CryptoManagerProtocol
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    init(cryptoManager: CryptoManagerProtocol) {
        self.cryptoManager = cryptoManager
        encoder.dateEncodingStrategy = .iso8601
        decoder.dateDecodingStrategy = .iso8601
    }

    func encryptBlob<T: Encodable>(_ object: T) throws -> Data {
        // 1. Serialize to JSON
        let jsonData = try encoder.encode(object)

        // 2. Compress (optional, for large blobs)
        let compressedData = try compress(jsonData)

        // 3. Encrypt
        let encryptedData = try cryptoManager.encrypt(compressedData)

        return encryptedData
    }

    func decryptBlob<T: Decodable>(_ data: Data, as type: T.Type) throws -> T {
        // 1. Decrypt
        let decryptedData = try cryptoManager.decrypt(data)

        // 2. Decompress
        let decompressedData = try decompress(decryptedData)

        // 3. Deserialize
        let object = try decoder.decode(type, from: decompressedData)

        return object
    }

    private func compress(_ data: Data) throws -> Data {
        // Use built-in compression
        guard let compressed = try? (data as NSData).compressed(using: .lzfse) else {
            return data  // Return uncompressed if compression fails
        }
        return compressed as Data
    }

    private func decompress(_ data: Data) throws -> Data {
        // Try to decompress, return original if fails (might not be compressed)
        guard let decompressed = try? (data as NSData).decompressed(using: .lzfse) else {
            return data
        }
        return decompressed as Data
    }
}

8.5. Zero-Knowledge Flows

// Core/Crypto/ZeroKnowledgeManager.swift
/// Manages Zero-Knowledge architecture flows
/// The server NEVER sees plaintext PHI data
final class ZeroKnowledgeManager {
    private let blobEncryption: BlobEncryptionProtocol
    private let syncRepository: SyncRepositoryProtocol

    init(
        blobEncryption: BlobEncryptionProtocol,
        syncRepository: SyncRepositoryProtocol
    ) {
        self.blobEncryption = blobEncryption
        self.syncRepository = syncRepository
    }

    /// Prepare data for server sync (encrypt locally first)
    func prepareForSync<T: Encodable & Identifiable>(
        _ entity: T,
        entityType: EntityType,
        operation: SyncOperation
    ) throws -> SyncBlobDTO where T.ID == UUID {
        // Encrypt the entire entity
        let encryptedBlob = try blobEncryption.encryptBlob(entity)

        return SyncBlobDTO(
            entityId: entity.id,
            entityType: entityType.rawValue,
            encryptedBlob: encryptedBlob,
            clientVersion: 1,
            isDeleted: operation == .delete
        )
    }

    /// Process data received from server (decrypt locally)
    func processFromServer<T: Decodable>(
        _ blob: SyncBlobDTO,
        as type: T.Type
    ) throws -> T {
        try blobEncryption.decryptBlob(blob.encryptedBlob, as: type)
    }

    /// Verify that server cannot read data
    /// This is called during development/testing to ensure compliance
    func verifyZeroKnowledge(blob: SyncBlobDTO) -> ZeroKnowledgeVerification {
        // Server should only see:
        // - entityId (UUID)
        // - entityType (string)
        // - encryptedBlob (opaque bytes)
        // - timestamps
        // - version numbers

        let serverVisibleFields: Set<String> = [
            "entity_id",
            "entity_type",
            "encrypted_blob",
            "client_version",
            "is_deleted"
        ]

        // Verify blob is encrypted (starts with valid AES-GCM header)
        let isEncrypted = blob.encryptedBlob.count >= 28  // Minimum: 12 nonce + 16 tag

        return ZeroKnowledgeVerification(
            isCompliant: isEncrypted,
            serverVisibleFields: serverVisibleFields,
            phiExposed: !isEncrypted
        )
    }
}

struct ZeroKnowledgeVerification {
    let isCompliant: Bool
    let serverVisibleFields: Set<String>
    let phiExposed: Bool
}

9. Offline-First Engine

9.1. Sync Manager

// Core/Sync/SyncManager.swift
import Combine

protocol SyncManagerProtocol {
    var syncStatePublisher: AnyPublisher<SyncState, Never> { get }
    func sync() async
    func enqueue(_ operation: SyncOperation) async
    func retryFailed() async
}

enum SyncState {
    case idle
    case syncing(progress: Double)
    case completed(result: SyncResult)
    case failed(error: Error)
    case offline
}

final class SyncManager: SyncManagerProtocol {
    private let syncUseCase: SyncDataUseCaseProtocol
    private let networkMonitor: NetworkMonitorProtocol
    private let syncQueue: SyncQueueProtocol

    private let stateSubject = CurrentValueSubject<SyncState, Never>(.idle)
    var syncStatePublisher: AnyPublisher<SyncState, Never> {
        stateSubject.eraseToAnyPublisher()
    }

    private var cancellables = Set<AnyCancellable>()
    private var syncTask: Task<Void, Never>?

    init(
        syncUseCase: SyncDataUseCaseProtocol,
        networkMonitor: NetworkMonitorProtocol,
        syncQueue: SyncQueueProtocol = SyncQueue()
    ) {
        self.syncUseCase = syncUseCase
        self.networkMonitor = networkMonitor
        self.syncQueue = syncQueue

        setupNetworkObserver()
    }

    private func setupNetworkObserver() {
        networkMonitor.isConnectedPublisher
            .removeDuplicates()
            .sink { [weak self] isConnected in
                if isConnected {
                    Task { await self?.sync() }
                } else {
                    self?.stateSubject.send(.offline)
                }
            }
            .store(in: &cancellables)
    }

    func sync() async {
        // Cancel existing sync
        syncTask?.cancel()

        // Check network
        guard networkMonitor.isConnected else {
            stateSubject.send(.offline)
            return
        }

        stateSubject.send(.syncing(progress: 0))

        syncTask = Task {
            do {
                let result = try await syncUseCase.execute()
                stateSubject.send(.completed(result: result))
            } catch {
                stateSubject.send(.failed(error: error))
            }
        }

        await syncTask?.value
    }

    func enqueue(_ operation: SyncOperation) async {
        await syncQueue.add(operation)

        // Auto-sync if connected
        if networkMonitor.isConnected {
            await sync()
        }
    }

    func retryFailed() async {
        await syncQueue.retryFailed()
        await sync()
    }
}

9.2. Conflict Resolution

// Core/Sync/ConflictResolver.swift
protocol ConflictResolverProtocol {
    func canAutoResolve(_ conflict: SyncConflict) -> Bool
    func autoResolve(_ conflict: SyncConflict) async -> ConflictResolution
    func merge(_ conflict: SyncConflict) async throws -> Data
}

/// Last-Write-Wins conflict resolver with smart merging
final class ConflictResolver: ConflictResolverProtocol {

    func canAutoResolve(_ conflict: SyncConflict) -> Bool {
        // Auto-resolve if:
        // 1. Version difference is 1 (simple sequential edit)
        // 2. Changes are in different fields
        // 3. One side is a delete

        let versionDiff = abs(conflict.serverVersion - conflict.localVersion)
        return versionDiff <= 1
    }

    func autoResolve(_ conflict: SyncConflict) async -> ConflictResolution {
        // Last-Write-Wins by default
        // Compare timestamps if available

        // For now, prefer server version to ensure consistency
        return .keepServer
    }

    func merge(_ conflict: SyncConflict) async throws -> Data {
        // Attempt field-level merge
        // This is entity-specific

        switch conflict.entityType {
        case .medication:
            return try await mergeMedication(conflict)
        case .doseLog:
            // Dose logs are append-only, shouldn't conflict
            return conflict.serverData
        default:
            // Default to server
            return conflict.serverData
        }
    }

    private func mergeMedication(_ conflict: SyncConflict) async throws -> Data {
        // Decode both versions
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601

        let local = try decoder.decode(MedicationMergeDTO.self, from: conflict.localData)
        let server = try decoder.decode(MedicationMergeDTO.self, from: conflict.serverData)

        // Merge strategy: take most recent value for each field
        var merged = MedicationMergeDTO(
            name: local.nameUpdatedAt > server.nameUpdatedAt ? local.name : server.name,
            nameUpdatedAt: max(local.nameUpdatedAt, server.nameUpdatedAt),
            dosage: local.dosageUpdatedAt > server.dosageUpdatedAt ? local.dosage : server.dosage,
            dosageUpdatedAt: max(local.dosageUpdatedAt, server.dosageUpdatedAt),
            // ... other fields
            version: max(local.version, server.version) + 1
        )

        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return try encoder.encode(merged)
    }
}

// Internal DTO for merging
private struct MedicationMergeDTO: Codable {
    var name: String
    var nameUpdatedAt: Date
    var dosage: String
    var dosageUpdatedAt: Date
    var version: Int64
}

9.3. Operation Queue

// Core/Sync/SyncQueue.swift
protocol SyncQueueProtocol {
    func add(_ operation: SyncOperation) async
    func getAll() async -> [SyncOperation]
    func remove(_ id: UUID) async
    func retryFailed() async
    func clear() async
}

actor SyncQueue: SyncQueueProtocol {
    private let localDataSource: SwiftDataSourceProtocol
    private let maxRetries = 3

    init(localDataSource: SwiftDataSourceProtocol = SwiftDataSource.shared) {
        self.localDataSource = localDataSource
    }

    func add(_ operation: SyncOperation) async {
        let entity = SyncQueueEntity(
            id: UUID(),
            entityType: operation.entityType.rawValue,
            entityId: operation.entityId,
            operation: operation.operation.rawValue,
            encryptedBlob: operation.encryptedBlob,
            createdAt: Date(),
            retryCount: 0
        )

        try? await localDataSource.save(entity)
    }

    func getAll() async -> [SyncOperation] {
        let entities = (try? await localDataSource.fetchAll(SyncQueueEntity.self)) ?? []

        return entities
            .filter { $0.retryCount < maxRetries }
            .sorted { $0.createdAt < $1.createdAt }
            .compactMap { entity in
                guard let entityType = EntityType(rawValue: entity.entityType),
                      let operation = SyncOperation(rawValue: entity.operation) else {
                    return nil
                }
                return SyncOperation(
                    entityType: entityType,
                    entityId: entity.entityId,
                    operation: operation,
                    encryptedBlob: entity.encryptedBlob
                )
            }
    }

    func remove(_ id: UUID) async {
        try? await localDataSource.delete(
            SyncQueueEntity.self,
            predicate: #Predicate { $0.entityId == id }
        )
    }

    func retryFailed() async {
        // Reset retry counts for failed operations
        let entities = (try? await localDataSource.fetchAll(SyncQueueEntity.self)) ?? []
        for var entity in entities where entity.retryCount >= maxRetries {
            entity.retryCount = 0
            try? await localDataSource.save(entity)
        }
    }

    func clear() async {
        let entities = (try? await localDataSource.fetchAll(SyncQueueEntity.self)) ?? []
        for entity in entities {
            try? await localDataSource.delete(
                SyncQueueEntity.self,
                predicate: #Predicate { $0.id == entity.id }
            )
        }
    }
}

struct SyncOperation: Equatable {
    let entityType: EntityType
    let entityId: UUID
    let operation: OperationType
    let encryptedBlob: Data

    enum OperationType: String {
        case create, update, delete
    }
}

9.4. Network Monitor

// Core/Sync/NetworkMonitor.swift
import Network
import Combine

protocol NetworkMonitorProtocol {
    var isConnected: Bool { get }
    var isConnectedPublisher: AnyPublisher<Bool, Never> { get }
    var connectionType: ConnectionType { get }
}

enum ConnectionType {
    case wifi
    case cellular
    case ethernet
    case unknown
    case none
}

final class NetworkMonitor: NetworkMonitorProtocol {
    private let monitor: NWPathMonitor
    private let queue = DispatchQueue(label: "NetworkMonitor")

    private let connectedSubject = CurrentValueSubject<Bool, Never>(false)
    private var _connectionType: ConnectionType = .none

    var isConnected: Bool { connectedSubject.value }
    var isConnectedPublisher: AnyPublisher<Bool, Never> {
        connectedSubject.eraseToAnyPublisher()
    }
    var connectionType: ConnectionType { _connectionType }

    init() {
        monitor = NWPathMonitor()
        startMonitoring()
    }

    deinit {
        monitor.cancel()
    }

    private func startMonitoring() {
        monitor.pathUpdateHandler = { [weak self] path in
            let isConnected = path.status == .satisfied
            self?.connectedSubject.send(isConnected)
            self?._connectionType = self?.getConnectionType(path) ?? .none
        }
        monitor.start(queue: queue)
    }

    private func getConnectionType(_ path: NWPath) -> ConnectionType {
        if path.usesInterfaceType(.wifi) {
            return .wifi
        } else if path.usesInterfaceType(.cellular) {
            return .cellular
        } else if path.usesInterfaceType(.wiredEthernet) {
            return .ethernet
        } else if path.status == .satisfied {
            return .unknown
        }
        return .none
    }
}

10. Push Notifications

10.1. APNs Registration

// Core/Notifications/NotificationManager.swift
import UserNotifications
import UIKit

protocol NotificationManagerProtocol {
    func requestAuthorization() async throws -> Bool
    func registerForRemoteNotifications() async throws -> String
    func scheduleLocalNotification(_ notification: MedTimeNotification) async throws
    func cancelNotification(id: String) async
    func cancelAllNotifications() async
}

final class NotificationManager: NSObject, NotificationManagerProtocol {
    private let center = UNUserNotificationCenter.current()
    private var deviceTokenContinuation: CheckedContinuation<String, Error>?

    override init() {
        super.init()
        center.delegate = self
    }

    func requestAuthorization() async throws -> Bool {
        let options: UNAuthorizationOptions = [.alert, .sound, .badge, .criticalAlert]
        return try await center.requestAuthorization(options: options)
    }

    func registerForRemoteNotifications() async throws -> String {
        return try await withCheckedThrowingContinuation { continuation in
            self.deviceTokenContinuation = continuation

            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }

            // Timeout after 30 seconds
            Task {
                try await Task.sleep(nanoseconds: 30_000_000_000)
                if self.deviceTokenContinuation != nil {
                    self.deviceTokenContinuation?.resume(throwing: NotificationError.registrationTimeout)
                    self.deviceTokenContinuation = nil
                }
            }
        }
    }

    // Called from AppDelegate
    func didRegisterForRemoteNotifications(deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
        deviceTokenContinuation?.resume(returning: token)
        deviceTokenContinuation = nil
    }

    func didFailToRegisterForRemoteNotifications(error: Error) {
        deviceTokenContinuation?.resume(throwing: error)
        deviceTokenContinuation = nil
    }

    func scheduleLocalNotification(_ notification: MedTimeNotification) async throws {
        let content = UNMutableNotificationContent()
        content.title = notification.title
        content.body = notification.body
        content.sound = notification.isCritical ? .defaultCritical : .default
        content.categoryIdentifier = notification.category.rawValue
        content.userInfo = notification.userInfo

        if let badge = notification.badge {
            content.badge = NSNumber(value: badge)
        }

        let trigger: UNNotificationTrigger
        switch notification.trigger {
        case .date(let date):
            let components = Calendar.current.dateComponents(
                [.year, .month, .day, .hour, .minute],
                from: date
            )
            trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)

        case .interval(let seconds):
            trigger = UNTimeIntervalNotificationTrigger(timeInterval: seconds, repeats: false)

        case .daily(let hour, let minute):
            var components = DateComponents()
            components.hour = hour
            components.minute = minute
            trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
        }

        let request = UNNotificationRequest(
            identifier: notification.id,
            content: content,
            trigger: trigger
        )

        try await center.add(request)
    }

    func cancelNotification(id: String) async {
        center.removePendingNotificationRequests(withIdentifiers: [id])
        center.removeDeliveredNotifications(withIdentifiers: [id])
    }

    func cancelAllNotifications() async {
        center.removeAllPendingNotificationRequests()
        center.removeAllDeliveredNotifications()
    }
}

extension NotificationManager: UNUserNotificationCenterDelegate {
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        // Show notification even when app is in foreground
        return [.banner, .sound, .badge]
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let userInfo = response.notification.request.content.userInfo
        let actionId = response.actionIdentifier

        // Handle notification action
        await handleNotificationAction(actionId: actionId, userInfo: userInfo)
    }

    private func handleNotificationAction(actionId: String, userInfo: [AnyHashable: Any]) async {
        // Post notification for app to handle
        NotificationCenter.default.post(
            name: .didReceiveNotificationAction,
            object: nil,
            userInfo: [
                "actionId": actionId,
                "userInfo": userInfo
            ]
        )
    }
}

struct MedTimeNotification {
    let id: String
    let title: String
    let body: String
    let category: NotificationCategory
    let trigger: NotificationTrigger
    let isCritical: Bool
    let badge: Int?
    let userInfo: [String: Any]

    enum NotificationCategory: String {
        case doseReminder = "DOSE_REMINDER"
        case missedDose = "MISSED_DOSE"
        case lowInventory = "LOW_INVENTORY"
        case refillReminder = "REFILL_REMINDER"
        case interaction = "INTERACTION_ALERT"
    }

    enum NotificationTrigger {
        case date(Date)
        case interval(TimeInterval)
        case daily(hour: Int, minute: Int)
    }
}

enum NotificationError: Error {
    case registrationTimeout
    case authorizationDenied
}

extension Notification.Name {
    static let didReceiveNotificationAction = Notification.Name("didReceiveNotificationAction")
}

10.2. Notification Handling

// Core/Notifications/NotificationScheduler.swift
protocol NotificationSchedulerProtocol {
    func scheduleReminders(for medication: Medication, schedule: Schedule) async throws
    func cancelReminders(for medicationId: UUID) async
    func rescheduleAllReminders() async throws
}

final class NotificationScheduler: NotificationSchedulerProtocol {
    private let notificationManager: NotificationManagerProtocol
    private let scheduleRepository: ScheduleRepositoryProtocol
    private let medicationRepository: MedicationRepositoryProtocol
    private let userDefaults: UserDefaultsDataSourceProtocol

    init(
        notificationManager: NotificationManagerProtocol,
        scheduleRepository: ScheduleRepositoryProtocol,
        medicationRepository: MedicationRepositoryProtocol,
        userDefaults: UserDefaultsDataSourceProtocol
    ) {
        self.notificationManager = notificationManager
        self.scheduleRepository = scheduleRepository
        self.medicationRepository = medicationRepository
        self.userDefaults = userDefaults
    }

    func scheduleReminders(for medication: Medication, schedule: Schedule) async throws {
        // Cancel existing reminders
        await cancelReminders(for: medication.id)

        guard schedule.isActive else { return }

        let reminderMinutes: Int = userDefaults.get(.reminderMinutesBefore) ?? 5

        for time in schedule.times {
            // Schedule for next 7 days
            for dayOffset in 0..<7 {
                guard let doseDate = calculateDoseDate(
                    time: time,
                    dayOffset: dayOffset,
                    daysOfWeek: schedule.daysOfWeek
                ) else { continue }

                // Reminder notification
                let reminderDate = doseDate.addingTimeInterval(-Double(reminderMinutes * 60))
                if reminderDate > Date() {
                    let notification = MedTimeNotification(
                        id: "reminder-\(medication.id)-\(doseDate.timeIntervalSince1970)",
                        title: "Hora de tomar \(medication.name)",
                        body: "\(medication.dosage.displayString) en \(reminderMinutes) minutos",
                        category: .doseReminder,
                        trigger: .date(reminderDate),
                        isCritical: false,
                        badge: 1,
                        userInfo: [
                            "medicationId": medication.id.uuidString,
                            "scheduledTime": doseDate.timeIntervalSince1970,
                            "type": "reminder"
                        ]
                    )
                    try await notificationManager.scheduleLocalNotification(notification)
                }

                // Missed dose notification (15 min after scheduled time)
                let missedDate = doseDate.addingTimeInterval(15 * 60)
                if missedDate > Date() {
                    let notification = MedTimeNotification(
                        id: "missed-\(medication.id)-\(doseDate.timeIntervalSince1970)",
                        title: "Dosis pendiente",
                        body: "No has registrado \(medication.name)",
                        category: .missedDose,
                        trigger: .date(missedDate),
                        isCritical: true,
                        badge: 1,
                        userInfo: [
                            "medicationId": medication.id.uuidString,
                            "scheduledTime": doseDate.timeIntervalSince1970,
                            "type": "missed"
                        ]
                    )
                    try await notificationManager.scheduleLocalNotification(notification)
                }
            }
        }
    }

    func cancelReminders(for medicationId: UUID) async {
        // Cancel all notifications for this medication
        // We'd need to track scheduled notification IDs
        // For now, pattern match on ID prefix
        let center = UNUserNotificationCenter.current()
        let requests = await center.pendingNotificationRequests()

        let idsToCancel = requests
            .filter { $0.identifier.contains(medicationId.uuidString) }
            .map { $0.identifier }

        center.removePendingNotificationRequests(withIdentifiers: idsToCancel)
    }

    func rescheduleAllReminders() async throws {
        // Cancel all existing
        await notificationManager.cancelAllNotifications()

        // Get all active medications and schedules
        let medications = try await medicationRepository.getAllMedications()
            .filter { $0.isActive }

        for medication in medications {
            if let schedule = try await scheduleRepository.getSchedule(for: medication.id) {
                try await scheduleReminders(for: medication, schedule: schedule)
            }
        }
    }

    private func calculateDoseDate(
        time: Schedule.ScheduleTime,
        dayOffset: Int,
        daysOfWeek: Set<DayOfWeek>?
    ) -> Date? {
        let calendar = Calendar.current
        var date = calendar.startOfDay(for: Date())
        date = calendar.date(byAdding: .day, value: dayOffset, to: date)!
        date = calendar.date(bySettingHour: time.hour, minute: time.minute, second: 0, of: date)!

        // Check day of week filter
        if let daysOfWeek {
            let weekday = calendar.component(.weekday, from: date)
            guard let dayOfWeek = DayOfWeek(rawValue: weekday),
                  daysOfWeek.contains(dayOfWeek) else {
                return nil
            }
        }

        return date
    }
}

10.3. Rich Notifications

// Core/Notifications/NotificationActions.swift
import UserNotifications

enum NotificationActionIdentifier: String {
    case takeDose = "TAKE_DOSE_ACTION"
    case skipDose = "SKIP_DOSE_ACTION"
    case snooze = "SNOOZE_ACTION"
    case viewDetails = "VIEW_DETAILS_ACTION"
}

final class NotificationActionsSetup {
    static func registerCategories() {
        let center = UNUserNotificationCenter.current()

        // Dose Reminder Category
        let takeAction = UNNotificationAction(
            identifier: NotificationActionIdentifier.takeDose.rawValue,
            title: "Tomar",
            options: [.foreground]
        )

        let skipAction = UNNotificationAction(
            identifier: NotificationActionIdentifier.skipDose.rawValue,
            title: "Omitir",
            options: [.destructive]
        )

        let snoozeAction = UNNotificationAction(
            identifier: NotificationActionIdentifier.snooze.rawValue,
            title: "Recordar en 10 min",
            options: []
        )

        let doseReminderCategory = UNNotificationCategory(
            identifier: MedTimeNotification.NotificationCategory.doseReminder.rawValue,
            actions: [takeAction, snoozeAction, skipAction],
            intentIdentifiers: [],
            options: [.customDismissAction]
        )

        // Missed Dose Category
        let viewAction = UNNotificationAction(
            identifier: NotificationActionIdentifier.viewDetails.rawValue,
            title: "Ver detalles",
            options: [.foreground]
        )

        let missedDoseCategory = UNNotificationCategory(
            identifier: MedTimeNotification.NotificationCategory.missedDose.rawValue,
            actions: [takeAction, viewAction, skipAction],
            intentIdentifiers: [],
            options: [.customDismissAction]
        )

        // Register all categories
        center.setNotificationCategories([
            doseReminderCategory,
            missedDoseCategory
        ])
    }
}

10.4. Actions

// Core/Notifications/NotificationActionHandler.swift
protocol NotificationActionHandlerProtocol {
    func handle(actionId: String, userInfo: [AnyHashable: Any]) async
}

final class NotificationActionHandler: NotificationActionHandlerProtocol {
    private let logDoseUseCase: LogDoseUseCaseProtocol
    private let notificationScheduler: NotificationSchedulerProtocol

    init(
        logDoseUseCase: LogDoseUseCaseProtocol,
        notificationScheduler: NotificationSchedulerProtocol
    ) {
        self.logDoseUseCase = logDoseUseCase
        self.notificationScheduler = notificationScheduler
    }

    func handle(actionId: String, userInfo: [AnyHashable: Any]) async {
        guard let medicationIdString = userInfo["medicationId"] as? String,
              let medicationId = UUID(uuidString: medicationIdString),
              let scheduledTimestamp = userInfo["scheduledTime"] as? TimeInterval else {
            return
        }

        let scheduledTime = Date(timeIntervalSince1970: scheduledTimestamp)

        switch NotificationActionIdentifier(rawValue: actionId) {
        case .takeDose:
            do {
                _ = try await logDoseUseCase.execute(
                    medicationId: medicationId,
                    scheduledTime: scheduledTime,
                    status: .taken,
                    takenTime: Date(),
                    skipReason: nil,
                    notes: nil
                )
            } catch {
                // Handle error - maybe show local notification
            }

        case .skipDose:
            do {
                _ = try await logDoseUseCase.execute(
                    medicationId: medicationId,
                    scheduledTime: scheduledTime,
                    status: .skipped,
                    takenTime: nil,
                    skipReason: .other,
                    notes: nil
                )
            } catch {
                // Handle error
            }

        case .snooze:
            // Reschedule notification for 10 minutes later
            // Would need medication info to do this properly
            break

        case .viewDetails:
            // Navigate to medication detail
            // Post notification for navigation
            NotificationCenter.default.post(
                name: .navigateToMedication,
                object: nil,
                userInfo: ["medicationId": medicationId]
            )

        case .none:
            break
        }
    }
}

extension Notification.Name {
    static let navigateToMedication = Notification.Name("navigateToMedication")
}

11. Widgets

11.1. WidgetKit Integration

// Extensions/Widget/MedTimeWidget.swift
import WidgetKit
import SwiftUI

@main
struct MedTimeWidgetBundle: WidgetBundle {
    var body: some Widget {
        NextDoseWidget()
        AdherenceWidget()
    }
}

struct NextDoseWidget: Widget {
    let kind: String = "NextDoseWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: NextDoseTimelineProvider()) { entry in
            NextDoseWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Siguiente Dosis")
        .description("Muestra tu proxima dosis programada")
        .supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
    }
}

struct AdherenceWidget: Widget {
    let kind: String = "AdherenceWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: AdherenceTimelineProvider()) { entry in
            AdherenceWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Adherencia")
        .description("Tu progreso de adherencia semanal")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

11.2. Timeline Provider

// Extensions/Widget/NextDoseTimelineProvider.swift
import WidgetKit

struct NextDoseEntry: TimelineEntry {
    let date: Date
    let medication: WidgetMedication?
    let scheduledTime: Date?
    let configuration: ConfigurationIntent?
}

struct WidgetMedication {
    let name: String
    let dosage: String
    let form: String
}

struct NextDoseTimelineProvider: TimelineProvider {
    private let sharedDataManager = WidgetSharedDataManager()

    func placeholder(in context: Context) -> NextDoseEntry {
        NextDoseEntry(
            date: Date(),
            medication: WidgetMedication(name: "Medicamento", dosage: "100mg", form: "tablet"),
            scheduledTime: Date().addingTimeInterval(3600),
            configuration: nil
        )
    }

    func getSnapshot(in context: Context, completion: @escaping (NextDoseEntry) -> Void) {
        let entry = getLatestEntry()
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<NextDoseEntry>) -> Void) {
        var entries: [NextDoseEntry] = []
        let currentDate = Date()

        // Generate entries for the next 6 hours, updating hourly
        for hourOffset in 0..<6 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = getEntryFor(date: entryDate)
            entries.append(entry)
        }

        // Update every 30 minutes
        let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
        let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))
        completion(timeline)
    }

    private func getLatestEntry() -> NextDoseEntry {
        getEntryFor(date: Date())
    }

    private func getEntryFor(date: Date) -> NextDoseEntry {
        guard let data = sharedDataManager.getNextDose() else {
            return NextDoseEntry(
                date: date,
                medication: nil,
                scheduledTime: nil,
                configuration: nil
            )
        }

        return NextDoseEntry(
            date: date,
            medication: WidgetMedication(
                name: data.medicationName,
                dosage: data.dosage,
                form: data.form
            ),
            scheduledTime: data.scheduledTime,
            configuration: nil
        )
    }
}

// Adherence Timeline Provider
struct AdherenceEntry: TimelineEntry {
    let date: Date
    let adherenceRate: Double
    let takenCount: Int
    let totalCount: Int
    let streakDays: Int
}

struct AdherenceTimelineProvider: TimelineProvider {
    private let sharedDataManager = WidgetSharedDataManager()

    func placeholder(in context: Context) -> AdherenceEntry {
        AdherenceEntry(date: Date(), adherenceRate: 0.85, takenCount: 17, totalCount: 20, streakDays: 7)
    }

    func getSnapshot(in context: Context, completion: @escaping (AdherenceEntry) -> Void) {
        let entry = getLatestEntry()
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<AdherenceEntry>) -> Void) {
        let entry = getLatestEntry()

        // Update every hour
        let nextUpdateDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdateDate))
        completion(timeline)
    }

    private func getLatestEntry() -> AdherenceEntry {
        guard let data = sharedDataManager.getAdherenceData() else {
            return AdherenceEntry(date: Date(), adherenceRate: 0, takenCount: 0, totalCount: 0, streakDays: 0)
        }

        return AdherenceEntry(
            date: Date(),
            adherenceRate: data.rate,
            takenCount: data.taken,
            totalCount: data.total,
            streakDays: data.streak
        )
    }
}

11.3. App Groups

// Extensions/Widget/WidgetSharedDataManager.swift
import Foundation

struct WidgetSharedDataManager {
    private let suiteName = "group.com.medtime.shared"
    private let nextDoseKey = "widget_next_dose"
    private let adherenceKey = "widget_adherence"

    private var sharedDefaults: UserDefaults? {
        UserDefaults(suiteName: suiteName)
    }

    // MARK: - Write (from main app)

    func updateNextDose(_ data: NextDoseData) {
        guard let encoded = try? JSONEncoder().encode(data) else { return }
        sharedDefaults?.set(encoded, forKey: nextDoseKey)

        // Reload widget timelines
        WidgetCenter.shared.reloadTimelines(ofKind: "NextDoseWidget")
    }

    func updateAdherence(_ data: AdherenceData) {
        guard let encoded = try? JSONEncoder().encode(data) else { return }
        sharedDefaults?.set(encoded, forKey: adherenceKey)

        WidgetCenter.shared.reloadTimelines(ofKind: "AdherenceWidget")
    }

    // MARK: - Read (from widget)

    func getNextDose() -> NextDoseData? {
        guard let data = sharedDefaults?.data(forKey: nextDoseKey) else { return nil }
        return try? JSONDecoder().decode(NextDoseData.self, from: data)
    }

    func getAdherenceData() -> AdherenceData? {
        guard let data = sharedDefaults?.data(forKey: adherenceKey) else { return nil }
        return try? JSONDecoder().decode(AdherenceData.self, from: data)
    }
}

struct NextDoseData: Codable {
    let medicationName: String
    let dosage: String
    let form: String
    let scheduledTime: Date
}

struct AdherenceData: Codable {
    let rate: Double
    let taken: Int
    let total: Int
    let streak: Int
    let lastUpdated: Date
}

11.4. Widget Types

// Extensions/Widget/Views/NextDoseWidgetView.swift
import SwiftUI
import WidgetKit

struct NextDoseWidgetView: View {
    @Environment(\.widgetFamily) var family
    var entry: NextDoseEntry

    var body: some View {
        switch family {
        case .systemSmall:
            SmallNextDoseView(entry: entry)
        case .systemMedium:
            MediumNextDoseView(entry: entry)
        case .accessoryRectangular:
            AccessoryNextDoseView(entry: entry)
        default:
            SmallNextDoseView(entry: entry)
        }
    }
}

struct SmallNextDoseView: View {
    let entry: NextDoseEntry

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Image(systemName: "pills.fill")
                    .foregroundStyle(.accent)
                Text("Siguiente")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            if let medication = entry.medication,
               let time = entry.scheduledTime {
                Text(medication.name)
                    .font(.headline)
                    .lineLimit(1)

                Text(medication.dosage)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)

                Spacer()

                Text(time, style: .time)
                    .font(.title2)
                    .fontWeight(.bold)
            } else {
                Text("No hay dosis programadas")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
        }
        .padding()
    }
}

struct MediumNextDoseView: View {
    let entry: NextDoseEntry

    var body: some View {
        HStack(spacing: 16) {
            // Icon
            VStack {
                Image(systemName: entry.medication?.form == "tablet" ? "pill.fill" : "cross.vial.fill")
                    .font(.largeTitle)
                    .foregroundStyle(.accent)
            }
            .frame(width: 60)

            // Info
            VStack(alignment: .leading, spacing: 4) {
                Text("Siguiente dosis")
                    .font(.caption)
                    .foregroundStyle(.secondary)

                if let medication = entry.medication {
                    Text(medication.name)
                        .font(.headline)

                    Text(medication.dosage)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                } else {
                    Text("Sin dosis programadas")
                        .font(.subheadline)
                }
            }

            Spacer()

            // Time
            if let time = entry.scheduledTime {
                VStack {
                    Text(time, style: .time)
                        .font(.title)
                        .fontWeight(.bold)

                    Text(timeUntil(time))
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
        }
        .padding()
    }

    private func timeUntil(_ date: Date) -> String {
        let interval = date.timeIntervalSinceNow
        if interval < 0 {
            return "Ahora"
        } else if interval < 3600 {
            return "En \(Int(interval / 60)) min"
        } else {
            return "En \(Int(interval / 3600)) h"
        }
    }
}

struct AccessoryNextDoseView: View {
    let entry: NextDoseEntry

    var body: some View {
        HStack {
            Image(systemName: "pills")

            VStack(alignment: .leading) {
                if let medication = entry.medication,
                   let time = entry.scheduledTime {
                    Text(medication.name)
                        .font(.headline)
                    Text(time, style: .time)
                        .font(.caption)
                } else {
                    Text("Sin dosis")
                }
            }
        }
    }
}

// Extensions/Widget/Views/AdherenceWidgetView.swift
struct AdherenceWidgetView: View {
    @Environment(\.widgetFamily) var family
    var entry: AdherenceEntry

    var body: some View {
        switch family {
        case .systemSmall:
            SmallAdherenceView(entry: entry)
        case .systemMedium:
            MediumAdherenceView(entry: entry)
        default:
            SmallAdherenceView(entry: entry)
        }
    }
}

struct SmallAdherenceView: View {
    let entry: AdherenceEntry

    var body: some View {
        VStack(spacing: 8) {
            Text("Adherencia")
                .font(.caption)
                .foregroundStyle(.secondary)

            ZStack {
                Circle()
                    .stroke(Color.gray.opacity(0.3), lineWidth: 8)

                Circle()
                    .trim(from: 0, to: entry.adherenceRate)
                    .stroke(adherenceColor, style: StrokeStyle(lineWidth: 8, lineCap: .round))
                    .rotationEffect(.degrees(-90))

                VStack {
                    Text("\(Int(entry.adherenceRate * 100))%")
                        .font(.title2)
                        .fontWeight(.bold)

                    Text("\(entry.takenCount)/\(entry.totalCount)")
                        .font(.caption2)
                        .foregroundStyle(.secondary)
                }
            }
            .padding(8)

            if entry.streakDays > 0 {
                Label("\(entry.streakDays) dias", systemImage: "flame.fill")
                    .font(.caption2)
                    .foregroundStyle(.orange)
            }
        }
        .padding()
    }

    private var adherenceColor: Color {
        if entry.adherenceRate >= 0.9 { return .green }
        if entry.adherenceRate >= 0.7 { return .yellow }
        return .red
    }
}

struct MediumAdherenceView: View {
    let entry: AdherenceEntry

    var body: some View {
        HStack(spacing: 20) {
            // Ring chart
            ZStack {
                Circle()
                    .stroke(Color.gray.opacity(0.3), lineWidth: 10)

                Circle()
                    .trim(from: 0, to: entry.adherenceRate)
                    .stroke(
                        entry.adherenceRate >= 0.9 ? Color.green : (entry.adherenceRate >= 0.7 ? Color.yellow : Color.red),
                        style: StrokeStyle(lineWidth: 10, lineCap: .round)
                    )
                    .rotationEffect(.degrees(-90))

                Text("\(Int(entry.adherenceRate * 100))%")
                    .font(.title)
                    .fontWeight(.bold)
            }
            .frame(width: 80, height: 80)

            // Stats
            VStack(alignment: .leading, spacing: 8) {
                Text("Esta semana")
                    .font(.headline)

                HStack {
                    StatItem(value: "\(entry.takenCount)", label: "Tomadas", color: .green)
                    StatItem(value: "\(entry.totalCount - entry.takenCount)", label: "Pendientes", color: .orange)
                }

                if entry.streakDays > 0 {
                    Label("\(entry.streakDays) dias de racha", systemImage: "flame.fill")
                        .font(.caption)
                        .foregroundStyle(.orange)
                }
            }
        }
        .padding()
    }
}

struct StatItem: View {
    let value: String
    let label: String
    let color: Color

    var body: some View {
        VStack(alignment: .leading) {
            Text(value)
                .font(.title3)
                .fontWeight(.bold)
                .foregroundStyle(color)
            Text(label)
                .font(.caption2)
                .foregroundStyle(.secondary)
        }
    }
}

12. Apple Watch

12.1. WatchConnectivity

// Extensions/Watch/WatchConnectivityManager.swift
import WatchConnectivity

protocol WatchConnectivityManagerProtocol {
    var isReachable: Bool { get }
    func sendMessage(_ message: [String: Any]) async throws
    func transferUserInfo(_ userInfo: [String: Any])
    func updateApplicationContext(_ context: [String: Any]) throws
}

final class WatchConnectivityManager: NSObject, WatchConnectivityManagerProtocol {
    static let shared = WatchConnectivityManager()

    private let session: WCSession
    private var messageHandlers: [(String, ([String: Any]) -> Void)] = []

    var isReachable: Bool {
        session.isReachable
    }

    private override init() {
        session = WCSession.default
        super.init()

        if WCSession.isSupported() {
            session.delegate = self
            session.activate()
        }
    }

    func sendMessage(_ message: [String: Any]) async throws {
        guard session.isReachable else {
            throw WatchError.notReachable
        }

        return try await withCheckedThrowingContinuation { continuation in
            session.sendMessage(message, replyHandler: { _ in
                continuation.resume()
            }, errorHandler: { error in
                continuation.resume(throwing: error)
            })
        }
    }

    func transferUserInfo(_ userInfo: [String: Any]) {
        session.transferUserInfo(userInfo)
    }

    func updateApplicationContext(_ context: [String: Any]) throws {
        try session.updateApplicationContext(context)
    }

    func registerHandler(for type: String, handler: @escaping ([String: Any]) -> Void) {
        messageHandlers.append((type, handler))
    }
}

extension WatchConnectivityManager: WCSessionDelegate {
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        // Handle activation
    }

    #if os(iOS)
    func sessionDidBecomeInactive(_ session: WCSession) {}
    func sessionDidDeactivate(_ session: WCSession) {
        session.activate()
    }
    #endif

    func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
        guard let type = message["type"] as? String else { return }

        for (handlerType, handler) in messageHandlers where handlerType == type {
            handler(message)
        }
    }

    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        // Handle user info transfer
        NotificationCenter.default.post(
            name: .didReceiveWatchUserInfo,
            object: nil,
            userInfo: userInfo
        )
    }
}

enum WatchError: Error {
    case notReachable
    case notSupported
}

extension Notification.Name {
    static let didReceiveWatchUserInfo = Notification.Name("didReceiveWatchUserInfo")
}

12.2. Complications

// Extensions/Watch/ComplicationProvider.swift
import ClockKit
import SwiftUI

final class ComplicationController: NSObject, CLKComplicationDataSource {

    func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
        let descriptors = [
            CLKComplicationDescriptor(
                identifier: "MedTimeNextDose",
                displayName: "Siguiente Dosis",
                supportedFamilies: [
                    .modularSmall,
                    .modularLarge,
                    .utilitarianSmall,
                    .utilitarianSmallFlat,
                    .utilitarianLarge,
                    .circularSmall,
                    .graphicCorner,
                    .graphicCircular,
                    .graphicRectangular,
                    .graphicExtraLarge
                ]
            )
        ]
        handler(descriptors)
    }

    func getCurrentTimelineEntry(
        for complication: CLKComplication,
        withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void
    ) {
        let entry = createTimelineEntry(for: complication, date: Date())
        handler(entry)
    }

    func getTimelineEntries(
        for complication: CLKComplication,
        after date: Date,
        limit: Int,
        withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void
    ) {
        var entries: [CLKComplicationTimelineEntry] = []

        // Generate entries for upcoming doses
        // Would fetch from shared data

        handler(entries)
    }

    private func createTimelineEntry(
        for complication: CLKComplication,
        date: Date
    ) -> CLKComplicationTimelineEntry? {
        guard let template = createTemplate(for: complication.family) else {
            return nil
        }
        return CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
    }

    private func createTemplate(for family: CLKComplicationFamily) -> CLKComplicationTemplate? {
        // Get data from shared storage
        let data = WatchSharedData.shared.getNextDose()

        switch family {
        case .graphicCircular:
            return CLKComplicationTemplateGraphicCircularStackText(
                line1TextProvider: CLKSimpleTextProvider(text: data?.medicationName ?? "MedTime"),
                line2TextProvider: CLKTimeTextProvider(date: data?.scheduledTime ?? Date())
            )

        case .graphicRectangular:
            return CLKComplicationTemplateGraphicRectangularStandardBody(
                headerTextProvider: CLKSimpleTextProvider(text: "Siguiente dosis"),
                body1TextProvider: CLKSimpleTextProvider(text: data?.medicationName ?? "Sin dosis"),
                body2TextProvider: data != nil ? CLKTimeTextProvider(date: data!.scheduledTime) : nil
            )

        case .modularSmall:
            return CLKComplicationTemplateModularSmallStackText(
                line1TextProvider: CLKSimpleTextProvider(text: "💊"),
                line2TextProvider: data != nil ? CLKTimeTextProvider(date: data!.scheduledTime) : CLKSimpleTextProvider(text: "--:--")
            )

        default:
            return nil
        }
    }
}

12.3. Independent Mode

// Extensions/Watch/WatchApp.swift
import SwiftUI
import WatchKit

@main
struct MedTimeWatchApp: App {
    @StateObject private var sessionManager = WatchSessionManager()

    var body: some Scene {
        WindowGroup {
            WatchContentView()
                .environmentObject(sessionManager)
        }
    }
}

struct WatchContentView: View {
    @EnvironmentObject var sessionManager: WatchSessionManager
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            WatchHomeView()
                .tag(0)

            WatchMedicationsView()
                .tag(1)

            WatchHistoryView()
                .tag(2)
        }
        .tabViewStyle(.page)
    }
}

// Watch Home View
struct WatchHomeView: View {
    @EnvironmentObject var sessionManager: WatchSessionManager

    var body: some View {
        ScrollView {
            VStack(spacing: 12) {
                if let nextDose = sessionManager.nextDose {
                    NextDoseWatchCard(dose: nextDose)
                } else {
                    Text("No hay dosis pendientes")
                        .foregroundStyle(.secondary)
                }

                // Today's progress
                HStack {
                    Text("Hoy")
                        .font(.headline)
                    Spacer()
                    Text("\(sessionManager.takenToday)/\(sessionManager.totalToday)")
                        .foregroundStyle(.secondary)
                }

                ProgressView(value: Double(sessionManager.takenToday), total: Double(sessionManager.totalToday))
                    .tint(.green)
            }
            .padding()
        }
        .navigationTitle("MedTime")
    }
}

struct NextDoseWatchCard: View {
    let dose: WatchDose
    @State private var showingConfirmation = false

    var body: some View {
        VStack(spacing: 8) {
            Text(dose.medicationName)
                .font(.headline)

            Text(dose.dosage)
                .font(.caption)
                .foregroundStyle(.secondary)

            Text(dose.scheduledTime, style: .time)
                .font(.title2)

            Button("Tomar") {
                showingConfirmation = true
            }
            .buttonStyle(.borderedProminent)
            .tint(.green)
        }
        .padding()
        .background(Color(.darkGray).opacity(0.5))
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .confirmationDialog("Confirmar dosis", isPresented: $showingConfirmation) {
            Button("Confirmar") {
                // Log dose
            }
            Button("Cancelar", role: .cancel) {}
        }
    }
}

// Session Manager for Watch
@MainActor
final class WatchSessionManager: ObservableObject {
    @Published var nextDose: WatchDose?
    @Published var takenToday: Int = 0
    @Published var totalToday: Int = 0
    @Published var medications: [WatchMedication] = []

    private let connectivityManager = WatchConnectivityManager.shared
    private let localStorage = WatchLocalStorage()

    init() {
        loadLocalData()
        setupConnectivity()
    }

    private func loadLocalData() {
        // Load from local storage (for standalone mode)
        if let data = localStorage.getNextDose() {
            nextDose = data
        }

        let stats = localStorage.getTodayStats()
        takenToday = stats.taken
        totalToday = stats.total

        medications = localStorage.getMedications()
    }

    private func setupConnectivity() {
        connectivityManager.registerHandler(for: "sync") { [weak self] message in
            Task { @MainActor in
                self?.handleSyncMessage(message)
            }
        }
    }

    private func handleSyncMessage(_ message: [String: Any]) {
        // Update local data from phone
        if let doseData = message["nextDose"] as? [String: Any] {
            nextDose = WatchDose(from: doseData)
            localStorage.saveNextDose(nextDose!)
        }

        if let taken = message["takenToday"] as? Int,
           let total = message["totalToday"] as? Int {
            takenToday = taken
            totalToday = total
            localStorage.saveTodayStats(taken: taken, total: total)
        }
    }

    func logDose(_ dose: WatchDose) async {
        // Save locally
        localStorage.logDose(dose)
        takenToday += 1

        // Sync to phone if reachable
        if connectivityManager.isReachable {
            try? await connectivityManager.sendMessage([
                "type": "doseLogged",
                "medicationId": dose.medicationId,
                "scheduledTime": dose.scheduledTime.timeIntervalSince1970,
                "takenTime": Date().timeIntervalSince1970
            ])
        }

        // Update next dose
        loadLocalData()
    }
}

struct WatchDose {
    let medicationId: String
    let medicationName: String
    let dosage: String
    let scheduledTime: Date

    init(from dict: [String: Any]) {
        medicationId = dict["medicationId"] as? String ?? ""
        medicationName = dict["medicationName"] as? String ?? ""
        dosage = dict["dosage"] as? String ?? ""
        scheduledTime = Date(timeIntervalSince1970: dict["scheduledTime"] as? TimeInterval ?? 0)
    }
}

struct WatchMedication: Identifiable {
    let id: String
    let name: String
    let dosage: String
    let nextDoseTime: Date?
}

12.4. Health Sync

// Extensions/Watch/HealthKitManager.swift
import HealthKit

protocol HealthKitManagerProtocol {
    func requestAuthorization() async throws -> Bool
    func saveMedicationLog(medication: String, date: Date) async throws
    func syncWithHealth() async throws
}

final class HealthKitManager: HealthKitManagerProtocol {
    private let healthStore = HKHealthStore()

    func requestAuthorization() async throws -> Bool {
        guard HKHealthStore.isHealthDataAvailable() else {
            throw HealthKitError.notAvailable
        }

        // Types we want to write
        let writeTypes: Set<HKSampleType> = [
            // Medication logs would go here if HealthKit supported them
            // For now, we could use workout or other types
        ]

        // Types we want to read
        let readTypes: Set<HKObjectType> = [
            HKObjectType.quantityType(forIdentifier: .heartRate)!,
            HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
        ]

        try await healthStore.requestAuthorization(toShare: writeTypes, read: readTypes)
        return true
    }

    func saveMedicationLog(medication: String, date: Date) async throws {
        // HealthKit doesn't have a native medication type
        // We could use HKCDADocument for clinical data
        // Or integrate with Apple's medication tracking when available
    }

    func syncWithHealth() async throws {
        // Sync relevant health data that might affect medication
        // e.g., sleep patterns for sleep aids
        // e.g., heart rate for cardiac medications
    }
}

enum HealthKitError: Error {
    case notAvailable
    case authorizationDenied
}

13. Testing

13.1. Unit Tests

// Tests/UnitTests/Domain/GetMedicationsUseCaseTests.swift
import XCTest
@testable import MedTime

final class GetMedicationsUseCaseTests: XCTestCase {
    var sut: GetMedicationsUseCase!
    var mockRepository: MockMedicationRepository!

    override func setUp() {
        super.setUp()
        mockRepository = MockMedicationRepository()
        sut = GetMedicationsUseCase(repository: mockRepository)
    }

    override func tearDown() {
        sut = nil
        mockRepository = nil
        super.tearDown()
    }

    func test_execute_returnsMedications() async throws {
        // Given
        let medications = [
            Medication.stub(name: "Aspirin"),
            Medication.stub(name: "Ibuprofen")
        ]
        mockRepository.medicationsToReturn = medications

        // When
        let result = try await sut.execute(filter: nil)

        // Then
        XCTAssertEqual(result.count, 2)
        XCTAssertEqual(result[0].name, "Aspirin")
        XCTAssertEqual(result[1].name, "Ibuprofen")
    }

    func test_execute_withActiveFilter_returnsOnlyActive() async throws {
        // Given
        let medications = [
            Medication.stub(name: "Active Med", isActive: true),
            Medication.stub(name: "Inactive Med", isActive: false)
        ]
        mockRepository.medicationsToReturn = medications

        // When
        let result = try await sut.execute(filter: MedicationFilter(isActive: true))

        // Then
        XCTAssertEqual(result.count, 1)
        XCTAssertEqual(result[0].name, "Active Med")
    }

    func test_execute_withSearchFilter_filtersbyName() async throws {
        // Given
        let medications = [
            Medication.stub(name: "Aspirin"),
            Medication.stub(name: "Ibuprofen"),
            Medication.stub(name: "Paracetamol")
        ]
        mockRepository.medicationsToReturn = medications

        // When
        let result = try await sut.execute(filter: MedicationFilter(searchText: "ibu"))

        // Then
        XCTAssertEqual(result.count, 1)
        XCTAssertEqual(result[0].name, "Ibuprofen")
    }

    func test_execute_repositoryThrows_propagatesError() async {
        // Given
        mockRepository.errorToThrow = DomainError.unknown(NSError(domain: "", code: 0))

        // When/Then
        do {
            _ = try await sut.execute(filter: nil)
            XCTFail("Expected error to be thrown")
        } catch {
            XCTAssertTrue(error is DomainError)
        }
    }
}

// Tests/UnitTests/Data/MedicationRepositoryTests.swift
final class MedicationRepositoryTests: XCTestCase {
    var sut: MedicationRepository!
    var mockLocalDataSource: MockSwiftDataSource!
    var mockRemoteDataSource: MockMedicationAPIDataSource!
    var mockCryptoManager: MockCryptoManager!

    override func setUp() {
        super.setUp()
        mockLocalDataSource = MockSwiftDataSource()
        mockRemoteDataSource = MockMedicationAPIDataSource()
        mockCryptoManager = MockCryptoManager()

        sut = MedicationRepository(
            localDataSource: mockLocalDataSource,
            remoteDataSource: mockRemoteDataSource,
            cryptoManager: mockCryptoManager
        )
    }

    func test_getAllMedications_decryptsBlobs() async throws {
        // Given
        let entity = MedicationEntity.stub(
            id: UUID(),
            encryptedBlob: "encrypted".data(using: .utf8)!
        )
        mockLocalDataSource.entitiesToReturn = [entity]
        mockCryptoManager.decryptedData = validMedicationJSON()

        // When
        let result = try await sut.getAllMedications()

        // Then
        XCTAssertEqual(result.count, 1)
        XCTAssertTrue(mockCryptoManager.decryptCalled)
    }

    func test_save_encryptsAndSavesLocally() async throws {
        // Given
        let medication = Medication.stub()
        mockCryptoManager.encryptedData = "encrypted".data(using: .utf8)!

        // When
        _ = try await sut.save(medication)

        // Then
        XCTAssertTrue(mockCryptoManager.encryptCalled)
        XCTAssertTrue(mockLocalDataSource.saveCalled)
    }

    func test_save_addsToSyncQueue() async throws {
        // Given
        let medication = Medication.stub()
        mockCryptoManager.encryptedData = "encrypted".data(using: .utf8)!

        // When
        _ = try await sut.save(medication)

        // Then
        XCTAssertTrue(mockLocalDataSource.addToSyncQueueCalled)
    }

    private func validMedicationJSON() -> Data {
        """
        {
            "name": "Test Med",
            "genericName": null,
            "dosageAmount": 100,
            "dosageUnit": "mg",
            "frequency": "times_per_day:2",
            "instructions": null,
            "requiresPrescription": false,
            "inventory": null
        }
        """.data(using: .utf8)!
    }
}

13.2. UI Tests

// Tests/UITests/Flows/MedicationFlowUITests.swift
import XCTest

final class MedicationFlowUITests: XCTestCase {
    var app: XCUIApplication!

    override func setUp() {
        super.setUp()
        continueAfterFailure = false

        app = XCUIApplication()
        app.launchArguments = ["--uitesting"]
        app.launch()
    }

    func test_addMedication_completesSuccessfully() {
        // Navigate to medications tab
        app.tabBars.buttons["Medicamentos"].tap()

        // Tap add button
        app.navigationBars.buttons["plus"].tap()

        // Fill in medication name
        let nameField = app.textFields["medication_name"]
        nameField.tap()
        nameField.typeText("Aspirina")

        // Select dosage
        app.buttons["dosage_field"].tap()
        app.textFields["dosage_amount"].tap()
        app.textFields["dosage_amount"].typeText("100")
        app.buttons["mg"].tap()
        app.buttons["Listo"].tap()

        // Select form
        app.buttons["form_picker"].tap()
        app.buttons["Tableta"].tap()

        // Select frequency
        app.buttons["frequency_picker"].tap()
        app.buttons["2 veces al dia"].tap()

        // Save
        app.buttons["Guardar"].tap()

        // Verify medication appears in list
        XCTAssertTrue(app.staticTexts["Aspirina"].exists)
    }

    func test_takeDose_updatesUI() {
        // Navigate to home
        app.tabBars.buttons["Inicio"].tap()

        // Find next dose card
        let doseCard = app.otherElements["next_dose_card"]
        XCTAssertTrue(doseCard.exists)

        // Tap take button
        doseCard.buttons["Tomar"].tap()

        // Verify success feedback
        XCTAssertTrue(app.staticTexts["Dosis registrada"].waitForExistence(timeout: 2))
    }

    func test_skipDose_showsReasonPicker() {
        // Navigate to home
        app.tabBars.buttons["Inicio"].tap()

        // Find next dose card
        let doseCard = app.otherElements["next_dose_card"]

        // Tap skip button
        doseCard.buttons["Omitir"].tap()

        // Verify reason picker appears
        XCTAssertTrue(app.sheets.element.exists)
        XCTAssertTrue(app.buttons["Efectos secundarios"].exists)
    }
}

// Tests/UITests/Flows/AuthFlowUITests.swift
final class AuthFlowUITests: XCTestCase {
    var app: XCUIApplication!

    override func setUp() {
        super.setUp()
        continueAfterFailure = false

        app = XCUIApplication()
        app.launchArguments = ["--uitesting", "--logged-out"]
        app.launch()
    }

    func test_login_withValidCredentials_navigatesToHome() {
        // Enter email
        let emailField = app.textFields["email_field"]
        emailField.tap()
        emailField.typeText("test@example.com")

        // Enter password
        let passwordField = app.secureTextFields["password_field"]
        passwordField.tap()
        passwordField.typeText("password123")

        // Tap login
        app.buttons["Iniciar sesion"].tap()

        // Verify navigation to home
        XCTAssertTrue(app.tabBars.buttons["Inicio"].waitForExistence(timeout: 5))
    }

    func test_login_withInvalidCredentials_showsError() {
        // Enter email
        let emailField = app.textFields["email_field"]
        emailField.tap()
        emailField.typeText("invalid@example.com")

        // Enter password
        let passwordField = app.secureTextFields["password_field"]
        passwordField.tap()
        passwordField.typeText("wrongpassword")

        // Tap login
        app.buttons["Iniciar sesion"].tap()

        // Verify error message
        XCTAssertTrue(app.staticTexts["Credenciales invalidas"].waitForExistence(timeout: 5))
    }
}

13.3. Snapshot Tests

// Tests/SnapshotTests/Screens/HomeViewSnapshotTests.swift
import XCTest
import SnapshotTesting
@testable import MedTime

final class HomeViewSnapshotTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // isRecording = true  // Uncomment to record new snapshots
    }

    func test_homeView_withDoses() {
        let viewModel = HomeViewModel()
        viewModel.nextDose = .stub()
        viewModel.todayDoses = [.stub(), .stub(), .stub()]
        viewModel.weeklyAdherence = AdherenceStats(totalDoses: 20, takenDoses: 18, missedDoses: 1, skippedDoses: 1)

        let view = HomeView()
            .environmentObject(viewModel)

        assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13)))
    }

    func test_homeView_empty() {
        let viewModel = HomeViewModel()
        viewModel.nextDose = nil
        viewModel.todayDoses = []

        let view = HomeView()
            .environmentObject(viewModel)

        assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13)))
    }

    func test_medicationCard_variants() {
        let medications = [
            Medication.stub(name: "Aspirina", form: .tablet),
            Medication.stub(name: "Jarabe para la tos", form: .liquid),
            Medication.stub(name: "Insulina", form: .injection)
        ]

        for medication in medications {
            let view = MedicationCard(medication: medication)
            assertSnapshot(
                matching: view,
                as: .image(layout: .fixed(width: 350, height: 100)),
                named: medication.form.rawValue
            )
        }
    }

    func test_adherenceWidget_allSizes() {
        let entry = AdherenceEntry(
            date: Date(),
            adherenceRate: 0.85,
            takenCount: 17,
            totalCount: 20,
            streakDays: 7
        )

        // Small
        assertSnapshot(
            matching: SmallAdherenceView(entry: entry),
            as: .image(layout: .fixed(width: 155, height: 155)),
            named: "small"
        )

        // Medium
        assertSnapshot(
            matching: MediumAdherenceView(entry: entry),
            as: .image(layout: .fixed(width: 329, height: 155)),
            named: "medium"
        )
    }
}

13.4. Mock Strategies

// Tests/Mocks/MockMedicationRepository.swift
@testable import MedTime

final class MockMedicationRepository: MedicationRepositoryProtocol {
    var medicationsToReturn: [Medication] = []
    var medicationToReturn: Medication?
    var errorToThrow: Error?
    var savedMedications: [Medication] = []
    var deletedMedications: [Medication] = []

    func getAllMedications() async throws -> [Medication] {
        if let error = errorToThrow { throw error }
        return medicationsToReturn
    }

    func getMedication(by id: UUID) async throws -> Medication? {
        if let error = errorToThrow { throw error }
        return medicationToReturn ?? medicationsToReturn.first { $0.id == id }
    }

    func save(_ medication: Medication) async throws -> Medication {
        if let error = errorToThrow { throw error }
        savedMedications.append(medication)
        return medication
    }

    func delete(_ medication: Medication) async throws {
        if let error = errorToThrow { throw error }
        deletedMedications.append(medication)
    }

    func search(query: String) async throws -> [Medication] {
        if let error = errorToThrow { throw error }
        return medicationsToReturn.filter { $0.name.contains(query) }
    }
}

// Tests/Mocks/MockCryptoManager.swift
final class MockCryptoManager: CryptoManagerProtocol {
    var encryptCalled = false
    var decryptCalled = false
    var encryptedData: Data = Data()
    var decryptedData: Data = Data()
    var errorToThrow: Error?

    func generateMasterKey() throws -> SymmetricKey {
        SymmetricKey(size: .bits256)
    }

    func deriveMasterKey(from password: String, salt: Data) throws -> SymmetricKey {
        SymmetricKey(size: .bits256)
    }

    func encrypt(_ data: Data) throws -> Data {
        encryptCalled = true
        if let error = errorToThrow { throw error }
        return encryptedData
    }

    func decrypt(_ encryptedData: Data) throws -> Data {
        decryptCalled = true
        if let error = errorToThrow { throw error }
        return decryptedData
    }

    func hash(_ data: Data) -> String {
        "mock_hash"
    }
}

// Tests/Mocks/MockSwiftDataSource.swift
final class MockSwiftDataSource: SwiftDataSourceProtocol {
    var entitiesToReturn: [any PersistentModel] = []
    var saveCalled = false
    var deleteCalled = false
    var addToSyncQueueCalled = false
    var errorToThrow: Error?

    func fetchAll<T: PersistentModel>(_ type: T.Type) async throws -> [T] {
        if let error = errorToThrow { throw error }
        return entitiesToReturn as? [T] ?? []
    }

    func fetch<T: PersistentModel>(_ type: T.Type, predicate: Predicate<T>?) async throws -> [T] {
        if let error = errorToThrow { throw error }
        return entitiesToReturn as? [T] ?? []
    }

    func save<T: PersistentModel>(_ entity: T) async throws {
        saveCalled = true
        if let error = errorToThrow { throw error }
    }

    func delete<T: PersistentModel>(_ type: T.Type, predicate: Predicate<T>) async throws {
        deleteCalled = true
        if let error = errorToThrow { throw error }
    }

    func addToSyncQueue(entityType: EntityType, entityId: UUID, operation: SyncOperation, encryptedBlob: Data) async throws {
        addToSyncQueueCalled = true
        if let error = errorToThrow { throw error }
    }
}

// Tests/Stubs/Medication+Stub.swift
extension Medication {
    static func stub(
        id: UUID = UUID(),
        name: String = "Test Medication",
        genericName: String? = nil,
        dosageAmount: Double = 100,
        dosageUnit: DosageUnit = .mg,
        form: MedicationForm = .tablet,
        route: AdministrationRoute = .oral,
        frequency: DosageFrequency = .timesPerDay(2),
        isActive: Bool = true
    ) -> Medication {
        Medication(
            id: id,
            name: name,
            genericName: genericName,
            dosage: Dosage(amount: dosageAmount, unit: dosageUnit),
            form: form,
            route: route,
            frequency: frequency,
            instructions: nil,
            startDate: Date(),
            endDate: nil,
            isActive: isActive,
            requiresPrescription: false,
            inventory: nil,
            reminders: [],
            createdAt: Date(),
            updatedAt: Date(),
            version: 1
        )
    }
}

14. Dependencies

14.1. Swift Package Manager

// Package.swift (or via Xcode Project Settings)
// swift-tools-version:5.9

import PackageDescription

let package = Package(
    name: "MedTime",
    platforms: [
        .iOS(.v15),
        .watchOS(.v9)
    ],
    dependencies: [
        // Dependency Injection
        .package(url: "https://github.com/hmlongco/Factory", from: "2.3.0"),

        // Networking (optional, for advanced features)
        // Using native URLSession primarily

        // Testing
        .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.0"),

        // Linting (development only)
        // SwiftLint via build phase
    ],
    targets: [
        .target(
            name: "MedTime",
            dependencies: [
                .product(name: "Factory", package: "Factory")
            ]
        ),
        .testTarget(
            name: "MedTimeTests",
            dependencies: [
                "MedTime",
                .product(name: "SnapshotTesting", package: "swift-snapshot-testing")
            ]
        )
    ]
)

14.2. Version Constraints

Dependency Version Razon
Factory 2.3+ DI container ligero
SnapshotTesting 1.15+ Testing visual
SwiftLint 0.54+ Code quality (dev)

Politica de Dependencias:

  1. Minimizar dependencias externas - Preferir APIs nativas de Apple
  2. No incluir networking libraries - URLSession es suficiente
  3. No incluir Realm/CoreData wrappers - SwiftData es nativo
  4. No incluir crypto libraries - CryptoKit es nativo
  5. Auditar seguridad - Revisar CVEs antes de adoptar

14.3. Jailbreak Detection (DV2-P3: SEC-BAJO-003)

// Core/Security/JailbreakDetector.swift
import Foundation
import UIKit

/// Detecta si el dispositivo esta jailbroken
/// Comportamiento por defecto: BLOCK (bloquear app si detectado)
final class JailbreakDetector {

    enum DetectionResult {
        case secure           // No jailbreak detectado
        case jailbroken       // Jailbreak detectado
        case suspicious       // Indicadores sospechosos
    }

    enum SecurityAction {
        case block            // Bloquear app completamente (DEFAULT)
        case warn             // Mostrar warning, permitir continuar
        case log              // Solo log, para debug
        case none             // No action (solo para testing)
    }

    // CONFIGURACION POR DEFECTO: BLOCK
    static let defaultAction: SecurityAction = .block

    /// Detecta jailbreak con multiple heuristicas
    static func detect() -> DetectionResult {
        var score = 0
        let checks: [(String, Bool)] = [
            ("Cydia check", checkCydiaApp()),
            ("Suspicious paths", checkSuspiciousPaths()),
            ("Dylib injection", checkDylibInjection()),
            ("Fork check", checkForkAbility()),
            ("Symlink check", checkSymlinks()),
            ("Sandbox check", checkSandboxIntegrity())
        ]

        for (name, detected) in checks {
            if detected {
                score += 1
                Logger.security.warning("Jailbreak indicator: \(name)")
            }
        }

        // Clasificacion por score
        if score >= 3 {
            return .jailbroken
        } else if score > 0 {
            return .suspicious
        } else {
            return .secure
        }
    }

    // MARK: - Detection Methods

    private static func checkCydiaApp() -> Bool {
        // Check Cydia y otros package managers
        let apps = [
            "/Applications/Cydia.app",
            "/Applications/blackra1n.app",
            "/Applications/FakeCarrier.app",
            "/Applications/Icy.app",
            "/Applications/IntelliScreen.app",
            "/Applications/MxTube.app",
            "/Applications/RockApp.app",
            "/Applications/SBSettings.app",
            "/Applications/WinterBoard.app"
        ]

        return apps.contains { FileManager.default.fileExists(atPath: $0) }
    }

    private static func checkSuspiciousPaths() -> Bool {
        // Paths comunes en dispositivos jailbroken
        let paths = [
            "/usr/sbin/sshd",
            "/bin/bash",
            "/etc/apt",
            "/private/var/lib/apt/",
            "/private/var/lib/cydia",
            "/private/var/stash",
            "/private/var/mobile/Library/SBSettings/Themes",
            "/Library/MobileSubstrate/MobileSubstrate.dylib",
            "/var/cache/apt",
            "/var/lib/apt",
            "/var/lib/cydia",
            "/usr/libexec/sftp-server",
            "/usr/bin/sshd",
            "/usr/libexec/ssh-keysign"
        ]

        return paths.contains { FileManager.default.fileExists(atPath: $0) }
    }

    private static func checkDylibInjection() -> Bool {
        // Verificar si MobileSubstrate esta cargado
        #if !targetEnvironment(simulator)
        let libraries = [
            "MobileSubstrate",
            "SubstrateLoader",
            "SubstrateInserter"
        ]

        for library in libraries {
            if let handle = dlopen("/usr/lib/\(library).dylib", RTLD_LAZY) {
                dlclose(handle)
                return true
            }
        }
        #endif

        return false
    }

    private static func checkForkAbility() -> Bool {
        // En sandbox normal, fork() debe fallar
        #if !targetEnvironment(simulator)
        let pid = fork()
        if pid >= 0 {
            // fork() tuvo exito - NOT NORMAL en iOS
            return true
        }
        #endif
        return false
    }

    private static func checkSymlinks() -> Bool {
        // Check symlinks sospechosos
        do {
            let path = "/Applications"
            let attributes = try FileManager.default.attributesOfItem(atPath: path)
            if attributes[.type] as? FileAttributeType == .typeSymbolicLink {
                return true
            }
        } catch {
            // Si no podemos leer, asumir seguro
        }
        return false
    }

    private static func checkSandboxIntegrity() -> Bool {
        // Intento de escribir fuera del sandbox
        let testPath = "/private/test_jailbreak.txt"
        do {
            try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
            try? FileManager.default.removeItem(atPath: testPath)
            return true  // Escritura exitosa = jailbreak
        } catch {
            return false  // Escritura fallo = sandbox OK
        }
    }

    // MARK: - Action Handler

    static func enforcePolicy(result: DetectionResult, action: SecurityAction = defaultAction) {
        switch (result, action) {
        case (.secure, _):
            Logger.security.info("Device integrity: SECURE")
            return

        case (.jailbroken, .block):
            Logger.security.critical("Jailbreak detected - BLOCKING APP")
            showBlockScreen()

        case (.jailbroken, .warn):
            Logger.security.warning("Jailbreak detected - WARNING USER")
            showWarning()

        case (.jailbroken, .log):
            Logger.security.warning("Jailbreak detected - LOGGED ONLY")

        case (.suspicious, .block):
            Logger.security.error("Suspicious indicators - BLOCKING APP")
            showBlockScreen()

        case (.suspicious, .warn):
            Logger.security.warning("Suspicious indicators - WARNING USER")
            showWarning()

        case (.suspicious, .log):
            Logger.security.info("Suspicious indicators - LOGGED ONLY")

        case (_, .none):
            Logger.security.debug("Detection disabled for testing")
        }
    }

    private static func showBlockScreen() {
        // Mostrar pantalla de bloqueo permanente
        DispatchQueue.main.async {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let window = windowScene.windows.first {
                let blockVC = JailbreakBlockViewController()
                window.rootViewController = blockVC
                window.makeKeyAndVisible()
            }
        }
    }

    private static func showWarning() {
        // Mostrar alerta de advertencia
        DispatchQueue.main.async {
            let alert = UIAlertController(
                title: "Security Warning",
                message: "This device appears to be jailbroken. MedTime cannot guarantee the security of your health data on modified devices.",
                preferredStyle: .alert
            )
            alert.addAction(UIAlertAction(title: "I Understand", style: .destructive))

            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let rootVC = windowScene.windows.first?.rootViewController {
                rootVC.present(alert, animated: true)
            }
        }
    }
}

// UI para pantalla de bloqueo
final class JailbreakBlockViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemRed

        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 20
        stackView.alignment = .center
        stackView.translatesAutoresizingMaskIntoConstraints = false

        let icon = UIImageView(image: UIImage(systemName: "exclamationmark.shield.fill"))
        icon.tintColor = .white
        icon.contentMode = .scaleAspectFit
        icon.translatesAutoresizingMaskIntoConstraints = false
        icon.heightAnchor.constraint(equalToConstant: 100).isActive = true

        let title = UILabel()
        title.text = "Security Alert"
        title.font = .systemFont(ofSize: 28, weight: .bold)
        title.textColor = .white
        title.textAlignment = .center

        let message = UILabel()
        message.text = "This device has been modified (jailbroken). For your safety, MedTime cannot run on modified devices as it may compromise your health data security."
        message.font = .systemFont(ofSize: 16)
        message.textColor = .white
        message.textAlignment = .center
        message.numberOfLines = 0

        stackView.addArrangedSubview(icon)
        stackView.addArrangedSubview(title)
        stackView.addArrangedSubview(message)

        view.addSubview(stackView)
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40)
        ])
    }
}

// INTEGRACION EN APP LAUNCH
// App/MedTimeApp.swift
@main
struct MedTimeApp: App {
    init() {
        // JAILBREAK CHECK AL INICIO
        let detectionResult = JailbreakDetector.detect()
        JailbreakDetector.enforcePolicy(result: detectionResult)

        // Continue con inicializacion normal...
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Configuracion de Comportamiento:

Ambiente Action Justificacion
Production BLOCK PHI critico, zero-tolerance
Staging WARN Permitir testing en dev devices
Debug LOG Facilitar desarrollo
Unit Tests NONE No interferir con tests

Variables de Ambiente (en xcconfig):

// Production.xcconfig
JAILBREAK_ACTION = BLOCK

// Staging.xcconfig
JAILBREAK_ACTION = WARN

// Debug.xcconfig
JAILBREAK_ACTION = LOG

Justificacion de BLOCK como Default:

  1. HIPAA Compliance: Dispositivos modificados no garantizan integridad
  2. Keychain Vulnerabilities: Jailbreak permite acceso a Keychain
  3. SSL Pinning Bypass: Ataques MITM posibles
  4. Zero-Knowledge Compromise: Cliente comprometido = arquitectura falla

Metricas de Telemetria:

// Log cada deteccion (sin PII)
struct JailbreakTelemetry: Codable {
    let result: String  // "secure", "suspicious", "jailbroken"
    let action: String  // "block", "warn", "log"
    let timestamp: Date
    let osVersion: String
    let deviceModel: String
    // NO incluir UDID ni identificadores
}

14.4. Security Audit

// Scripts/audit_dependencies.sh
#!/bin/bash

# Check for known vulnerabilities in dependencies
# Run: ./Scripts/audit_dependencies.sh

echo "Auditing Swift Package dependencies..."

# Export resolved packages
swift package show-dependencies --format json > dependencies.json

# Check each dependency against known CVEs
# (In production, use tools like Snyk, Dependabot, or OSV)

echo "Dependencies audit complete."
echo "Review dependencies.json for detailed list."

# Verify no private/internal APIs are used
echo "Checking for private API usage..."
grep -r "UIApplication.shared" --include="*.swift" | grep -v "// MARK: - Allowed"

echo "Audit complete."

15. Build y CI/CD

15.1. Configuraciones

# .xcconfig files for different environments

# Development.xcconfig
PRODUCT_BUNDLE_IDENTIFIER = com.medtime.dev
DISPLAY_NAME = MedTime Dev
API_BASE_URL = https://api-dev.medtime.app/v1
ENABLE_DEBUG_LOGGING = YES

# Staging.xcconfig
PRODUCT_BUNDLE_IDENTIFIER = com.medtime.staging
DISPLAY_NAME = MedTime Staging
API_BASE_URL = https://api-staging.medtime.app/v1
ENABLE_DEBUG_LOGGING = YES

# Production.xcconfig
PRODUCT_BUNDLE_IDENTIFIER = com.medtime
DISPLAY_NAME = MedTime
API_BASE_URL = https://api.medtime.app/v1
ENABLE_DEBUG_LOGGING = NO

15.2. Fastlane

# fastlane/Fastfile
default_platform(:ios)

platform :ios do

  desc "Run tests"
  lane :test do
    run_tests(
      scheme: "MedTime",
      device: "iPhone 15",
      code_coverage: true,
      output_directory: "./fastlane/test_output"
    )
  end

  desc "Build for TestFlight"
  lane :beta do
    setup_ci if ENV['CI']

    match(type: "appstore", readonly: true)

    increment_build_number(
      build_number: ENV['BUILD_NUMBER'] || Time.now.strftime("%Y%m%d%H%M")
    )

    build_app(
      scheme: "MedTime",
      configuration: "Release",
      export_method: "app-store"
    )

    upload_to_testflight(
      skip_waiting_for_build_processing: true
    )

    slack(
      message: "MedTime Beta #{get_version_number} (#{get_build_number}) uploaded to TestFlight",
      success: true
    )
  end

  desc "Release to App Store"
  lane :release do
    setup_ci if ENV['CI']

    match(type: "appstore", readonly: true)

    build_app(
      scheme: "MedTime",
      configuration: "Release",
      export_method: "app-store"
    )

    upload_to_app_store(
      submit_for_review: true,
      automatic_release: false,
      force: true,
      precheck_include_in_app_purchases: false,
      submission_information: {
        add_id_info_uses_idfa: false
      }
    )
  end

  desc "Build for screenshots"
  lane :screenshots do
    capture_screenshots(scheme: "MedTimeUITests")
    frame_screenshots
  end

  error do |lane, exception|
    slack(
      message: "Error in lane #{lane}: #{exception.message}",
      success: false
    )
  end
end

15.3. App Store

# fastlane/metadata/en-US/description.txt
MedTime - Your intelligent medication companion

Never miss a dose again. MedTime helps you manage your medications with:

- Smart reminders that adapt to your schedule
- Easy dose tracking with one tap
- Family sharing for caregivers
- Offline mode - works without internet
- Complete privacy - your data stays encrypted on your device

Key Features:
- Add unlimited medications
- Customizable reminder schedules
- Interaction warnings
- Adherence statistics and streaks
- Apple Watch companion app
- Home Screen widgets
- Export reports for your doctor

Privacy First:
MedTime uses end-to-end encryption. Your health data is encrypted on your device and never visible to our servers. Your privacy is our priority.

# fastlane/metadata/en-US/keywords.txt
medication,reminder,pill,health,tracking,adherence,prescription,medicine,drug,pharmacy

# fastlane/metadata/en-US/privacy_url.txt
https://medtime.app/privacy

# fastlane/metadata/en-US/support_url.txt
https://medtime.app/support

16. Anexo A: Diagrama de Arquitectura

┌──────────────────────────────────────────────────────────────────────────┐
│                              MedTime iOS                                  │
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │                      PRESENTATION LAYER                            │ │
│  │  ┌──────────────┐  ┌───────────────┐  ┌─────────────────────────┐ │ │
│  │  │   SwiftUI    │  │   ViewModels  │  │     Coordinators        │ │ │
│  │  │    Views     │──│    (MVVM)     │──│    (Navigation)         │ │ │
│  │  └──────────────┘  └───────────────┘  └─────────────────────────┘ │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                    │                                     │
│                                    ▼                                     │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │                        DOMAIN LAYER                                │ │
│  │  ┌──────────────┐  ┌───────────────┐  ┌─────────────────────────┐ │ │
│  │  │   Entities   │  │   Use Cases   │  │  Repository Protocols   │ │ │
│  │  │   (Models)   │  │  (Interactor) │  │       (Ports)           │ │ │
│  │  └──────────────┘  └───────────────┘  └─────────────────────────┘ │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                    │                                     │
│                                    ▼                                     │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │                         DATA LAYER                                 │ │
│  │  ┌──────────────┐  ┌───────────────┐  ┌─────────────────────────┐ │ │
│  │  │ Repositories │  │ Data Sources  │  │        Mappers          │ │ │
│  │  │  (Adapters)  │  │ Local/Remote  │  │    (DTO <-> Entity)     │ │ │
│  │  └──────────────┘  └───────────────┘  └─────────────────────────┘ │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                           │              │                               │
│                           ▼              ▼                               │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │                         CORE LAYER                                 │ │
│  │  ┌───────────┐  ┌────────────┐  ┌───────────┐  ┌────────────────┐ │ │
│  │  │  Crypto   │  │   Sync     │  │ Network   │  │ Notifications  │ │ │
│  │  │ (E2E/ZK)  │  │  Manager   │  │ Monitor   │  │    Manager     │ │ │
│  │  └───────────┘  └────────────┘  └───────────┘  └────────────────┘ │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                           │              │              │                │
│              ┌────────────┴──────────────┴──────────────┘                │
│              ▼                           ▼                               │
│  ┌──────────────────────┐    ┌──────────────────────────────────────┐   │
│  │   LOCAL STORAGE      │    │         REMOTE (Server)              │   │
│  │  ┌────────────────┐  │    │  ┌─────────────────────────────────┐ │   │
│  │  │   SwiftData    │  │    │  │      API Client (URLSession)    │ │   │
│  │  │   (SQLite)     │  │    │  │  ┌────────────────────────────┐ │ │   │
│  │  └────────────────┘  │    │  │  │   Encrypted Blobs Only     │ │ │   │
│  │  ┌────────────────┐  │    │  │  │   (Zero-Knowledge)         │ │ │   │
│  │  │    KeyChain    │  │    │  │  └────────────────────────────┘ │ │   │
│  │  │  (Encryption   │  │    │  └─────────────────────────────────┘ │   │
│  │  │     Keys)      │  │    └──────────────────────────────────────┘   │
│  │  └────────────────┘  │                                               │
│  │  ┌────────────────┐  │                                               │
│  │  │  UserDefaults  │  │                                               │
│  │  │  (Settings)    │  │                                               │
│  │  └────────────────┘  │                                               │
│  └──────────────────────┘                                               │
│                                                                          │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │                       EXTENSIONS                                   │ │
│  │  ┌──────────────┐  ┌───────────────┐  ┌─────────────────────────┐ │ │
│  │  │   Widgets    │  │  Apple Watch  │  │     Notifications       │ │ │
│  │  │ (WidgetKit)  │  │ (WatchKit)    │  │       (UNUserNotif)     │ │ │
│  │  └──────────────┘  └───────────────┘  └─────────────────────────┘ │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

17. Anexo B: Checklist de Implementacion

17.1. Fase 1: Setup Inicial

  • Crear proyecto Xcode con targets (iOS, Widget, Watch)
  • Configurar SPM dependencies
  • Setup SwiftData schema
  • Configurar KeyChain wrapper
  • Implementar DI container

17.2. Fase 2: Domain Layer

  • Definir entities
  • Implementar use cases core
  • Definir repository protocols
  • Implementar error handling

17.3. Fase 3: Data Layer

  • Implementar SwiftData data source
  • Implementar API client
  • Crear mappers
  • Implementar repositories

17.4. Fase 4: Crypto

  • Implementar CryptoManager
  • Key derivation (Argon2id)
  • Blob encryption/decryption
  • KeyChain integration

17.5. Fase 5: Sync Engine

  • Implementar SyncManager
  • Queue de operaciones
  • Conflict resolution
  • Network monitor

17.6. Fase 6: Presentation

  • Implementar navigation
  • Crear ViewModels
  • Diseñar screens principales
  • Implementar components

17.7. Fase 7: Notifications

  • APNs registration
  • Local notifications
  • Notification actions
  • Background refresh

17.8. Fase 8: Extensions

  • Widget timeline providers
  • Widget views
  • Watch app
  • Watch connectivity

17.9. Fase 9: Testing

  • Unit tests (80%+ coverage)
  • UI tests
  • Snapshot tests
  • Performance tests

17.10. Fase 10: Release

  • App Store metadata
  • Screenshots
  • Privacy policy
  • Beta testing
  • Export Compliance validation

18. Anexo C: Export Compliance (DV2-P2)

Agregado: OPS-MEDIO-001 - Validacion de Export Compliance para App Store.

18.1. Declaracion de Uso de Criptografia

MedTime utiliza criptografia para proteger datos de salud (PHI). Esta seccion documenta el cumplimiento con las regulaciones de exportacion de EE.UU. (BIS/EAR) y los requisitos de App Store Connect.

18.1.1. Algoritmos Criptograficos Utilizados

Algoritmo Proposito Categoria EAR Exempt
AES-256-GCM Cifrado de blobs de datos 5A002 Si*
Argon2id Derivacion de claves 5A002 Si*
SHA-256 Checksums, HKDF No controlado N/A
ECDH P-256 Intercambio de claves 5A002 Si*
ECDSA P-256 Firmas digitales 5A002 Si*

*Exento bajo EAR 740.17(b)(1) - Proteccion de datos del usuario.

18.1.2. App Store Connect - Export Compliance

<!-- Info.plist - Export Compliance Settings -->
<key>ITSAppUsesNonExemptEncryption</key>
<false/>

<!-- Alternativa si se require clasificacion -->
<key>ITSEncryptionExportComplianceCode</key>
<string>XXXXXXXXXX</string>

Justificacion de Exencion (EAR 740.17(b)(1)):

La criptografia en MedTime esta exenta de requisitos de clasificacion porque:

  1. Uso exclusivo para proteccion de datos del usuario: El cifrado E2E protege

PHI del usuario y no se utiliza para propositos gubernamentales, militares o PHI del usuario y no se utiliza para propositos gubernamentales, militares o de inteligencia.

  1. Implementacion estandar: Utilizamos CryptoKit de Apple que ya esta aprobado

y distribuido en iOS. y distribuido en iOS.

  1. Sin backdoors: No existe acceso administrativo a datos cifrados.

  2. Datos en transito y reposo: El cifrado protege datos personales de salud

durante almacenamiento y transmision. durante almacenamiento y transmision.

18.2. Checklist de Compliance Pre-Submission

# export-compliance-checklist.yml
export_compliance:
  version: "1.0"
  app: "MedTime"
  date_reviewed: "YYYY-MM-DD"

  questionnaire:
    - question: "Does your app use encryption?"
      answer: true
      justification: "AES-256-GCM for user data protection"

    - question: "Does your app qualify for any exemptions?"
      answer: true
      exemption: "EAR 740.17(b)(1)"
      reason: "Personal data protection only"

    - question: "Is the encryption available to the user?"
      answer: false
      reason: "Encryption is transparent, user cannot modify algorithms"

    - question: "Is your app designed for government/military use?"
      answer: false

    - question: "Does your app use third-party encryption?"
      answer: true
      libraries:
        - name: "Apple CryptoKit"
          purpose: "AES-256-GCM, SHA-256, ECDH"
          pre_approved: true
        - name: "swift-crypto"
          purpose: "Argon2id key derivation"
          pre_approved: true

  declaration:
    uses_non_exempt_encryption: false
    exempt_under: "EAR 740.17(b)(1)"
    documentation_complete: true

  approvals:
    - role: "Security Lead"
      approved: false
      date: null
    - role: "Legal Counsel"
      approved: false
      date: null

18.3. Proceso de Validacion

flowchart TD
    subgraph PreSubmit["PRE-SUBMISSION VALIDATION"]
        A[Review Encryption Usage] --> B{Uses Non-Exempt?}
        B --> |No| C[Set ITSAppUsesNonExemptEncryption = false]
        B --> |Yes| D[Obtain ECCN Classification]
        D --> E[Submit Annual Self-Classification]
        C --> F[Document Exemption Justification]
    end

    subgraph Submit["APP STORE SUBMISSION"]
        F --> G[Upload to App Store Connect]
        E --> G
        G --> H[Answer Export Compliance Questions]
        H --> I[Submit for Review]
    end

    subgraph Annual["ANNUAL REQUIREMENTS"]
        J[Jan 1-Feb 1: Self-Classification Report] --> K[BIS SNAP-R System]
        K --> L[Document Submission]
    end

    I --> |If Exempt| Annual

18.4. Integracion con CI/CD

// Scripts/validate-export-compliance.swift
import Foundation

/// Validates export compliance settings before App Store submission
struct ExportComplianceValidator {

    struct ValidationResult {
        let isValid: Bool
        let issues: [String]
        let warnings: [String]
    }

    func validate() -> ValidationResult {
        var issues: [String] = []
        var warnings: [String] = []

        // Check Info.plist
        if let infoPlist = readInfoPlist() {
            // Verify ITSAppUsesNonExemptEncryption
            if let usesEncryption = infoPlist["ITSAppUsesNonExemptEncryption"] as? Bool {
                if usesEncryption {
                    // Must have ECCN code
                    if infoPlist["ITSEncryptionExportComplianceCode"] == nil {
                        issues.append("ITSEncryptionExportComplianceCode required when ITSAppUsesNonExemptEncryption is true")
                    }
                }
            } else {
                issues.append("Missing ITSAppUsesNonExemptEncryption in Info.plist")
            }
        } else {
            issues.append("Could not read Info.plist")
        }

        // Check for encryption usage
        let encryptionAPIs = findEncryptionUsage()
        if encryptionAPIs.isEmpty {
            warnings.append("No encryption APIs detected - verify if ITSAppUsesNonExemptEncryption should be true")
        }

        // Verify exemption documentation
        if !FileManager.default.fileExists(atPath: "Docs/ExportCompliance.md") {
            warnings.append("Missing ExportCompliance.md documentation")
        }

        return ValidationResult(
            isValid: issues.isEmpty,
            issues: issues,
            warnings: warnings
        )
    }

    private func findEncryptionUsage() -> [String] {
        // Scan for CryptoKit usage
        let patterns = [
            "AES.GCM",
            "ChaChaPoly",
            "SecKeyCreateEncryptedData",
            "CCCrypt",
            "Argon2"
        ]
        // Implementation to scan source files
        return patterns
    }

    private func readInfoPlist() -> [String: Any]? {
        // Read and parse Info.plist
        return nil
    }
}

// MARK: - Fastlane Integration
// Add to Fastfile:
//
// lane :validate_export do
//   sh("swift Scripts/validate-export-compliance.swift")
// end
//
// before_all do
//   validate_export
// end
# .github/workflows/export-compliance.yml
name: Export Compliance Check

on:
  push:
    branches: [main, develop]
    paths:
      - 'ios/**/*.swift'
      - 'ios/**/Info.plist'

jobs:
  validate-export:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check Info.plist Settings
        run: |
          PLIST="ios/MedTime/Info.plist"
          USES_ENCRYPTION=$(/usr/libexec/PlistBuddy -c "Print :ITSAppUsesNonExemptEncryption" "$PLIST" 2>/dev/null || echo "MISSING")

          if [ "$USES_ENCRYPTION" = "MISSING" ]; then
            echo "::error::ITSAppUsesNonExemptEncryption key missing from Info.plist"
            exit 1
          fi

          if [ "$USES_ENCRYPTION" = "true" ]; then
            ECCN=$(/usr/libexec/PlistBuddy -c "Print :ITSEncryptionExportComplianceCode" "$PLIST" 2>/dev/null || echo "MISSING")
            if [ "$ECCN" = "MISSING" ]; then
              echo "::error::ITSEncryptionExportComplianceCode required when using non-exempt encryption"
              exit 1
            fi
          fi

          echo "Export compliance validation passed"

      - name: Verify Documentation
        run: |
          if [ ! -f "docs/ExportCompliance.md" ]; then
            echo "::warning::Export compliance documentation not found"
          fi

18.5. Documentacion Requerida

Documento Ubicacion Responsable Frecuencia
Export Compliance Declaration docs/ExportCompliance.md Security Lead Por release
Self-Classification Report BIS SNAP-R Legal Anual (Enero)
Encryption Audit Trail docs/crypto/audit.log Dev Team Continuo
App Store Questionnaire App Store Connect Release Manager Por submission

Documento generado por SpecQueen Technical Division - IT-12 "La resistencia es futil cuando la arquitectura es perfecta."