// ── Routine Runner View ─────────────────────────────────────── // Full-screen view for executing a routine step-by-step import SwiftUI struct RoutineRunnerView: View { @State var routine: CMRoutine let onComplete: (CMRoutine) -> Void let onCancel: () -> Void @State private var now = Date() @State private var timer: Timer? var body: some View { ZStack { CMColors.bg.ignoresSafeArea() VStack(spacing: 0) { // Header header // Progress bar progressBar // Current step if let step = routine.currentStep { currentStepCard(step) } else if routine.status == .completed { completedView } Spacer() // Step list stepList // Controls controls } } .onAppear { startTicking() } .onDisappear { stopTicking() } } // MARK: - Header private var header: some View { HStack { Button { routine.cancel() onCancel() } label: { Image(systemName: "xmark") .font(.title3) .foregroundColor(CMColors.textSecondary) } Spacer() VStack(spacing: 2) { Text(routine.name) .font(.headline) .foregroundColor(CMColors.text) Text("Step \(min(routine.currentStepIndex + 1, routine.steps.count)) of \(routine.steps.count)") .font(.caption) .foregroundColor(CMColors.textSecondary) } Spacer() // Spacer for symmetry Color.clear.frame(width: 28, height: 28) } .padding() } // MARK: - Progress Bar private var progressBar: some View { GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 2) .fill(CMColors.surfaceHover) .frame(height: 4) RoundedRectangle(cornerRadius: 2) .fill(CMColors.accent) .frame(width: geo.size.width * routine.progress, height: 4) .animation(.easeInOut(duration: 0.3), value: routine.progress) } } .frame(height: 4) .padding(.horizontal) } // MARK: - Current Step Card @ViewBuilder private func currentStepCard(_ step: CMRoutineStep) -> some View { VStack(spacing: 16) { Spacer().frame(height: 20) // Step label Text(step.label) .font(.largeTitle.bold()) .foregroundColor(CMColors.text) .multilineTextAlignment(.center) // Remaining time let remaining = routine.remainingStepSeconds(now: now) Text(formatRemaining(remaining)) .font(.system(size: 60, weight: .thin, design: .monospaced)) .foregroundColor(remaining <= 30 ? CMColors.critical : CMColors.accent) // Notes if let notes = step.notes, !notes.isEmpty { Text(notes) .font(.subheadline) .foregroundColor(CMColors.textSecondary) .multilineTextAlignment(.center) .padding(.horizontal, 32) } // Next step preview if let next = routine.nextStep { HStack(spacing: 4) { Text("Next:") .font(.caption) .foregroundColor(CMColors.textMuted) Text(next.label) .font(.caption.bold()) .foregroundColor(CMColors.textSecondary) if step.transition != .immediate { Text("(\(step.transition.label))") .font(.caption2) .foregroundColor(CMColors.textMuted) } } .padding(.top, 8) } Spacer() } } // MARK: - Completed View private var completedView: some View { VStack(spacing: 16) { Spacer() Image(systemName: "checkmark.circle.fill") .font(.system(size: 80)) .foregroundColor(CMColors.gentle) Text("Routine Complete!") .font(.largeTitle.bold()) .foregroundColor(CMColors.text) HStack(spacing: 24) { VStack { Text("\(routine.completedStepCount)") .font(.title.bold()) .foregroundColor(CMColors.accent) Text("Completed") .font(.caption) .foregroundColor(CMColors.textSecondary) } if routine.skippedStepCount > 0 { VStack { Text("\(routine.skippedStepCount)") .font(.title.bold()) .foregroundColor(CMColors.important) Text("Skipped") .font(.caption) .foregroundColor(CMColors.textSecondary) } } } Spacer() Button { onComplete(routine) } label: { Text("Done") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() .background(CMColors.accent) .cornerRadius(12) } .padding(.horizontal, 32) .padding(.bottom, 32) } } // MARK: - Step List private var stepList: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Array(routine.steps.enumerated()), id: \.element.id) { idx, step in stepPill(step, index: idx) } } .padding(.horizontal) } .padding(.vertical, 8) } @ViewBuilder private func stepPill(_ step: CMRoutineStep, index: Int) -> some View { let isCurrent = index == routine.currentStepIndex && routine.status == .active HStack(spacing: 4) { statusIcon(step.status) .font(.caption2) Text(step.label) .font(.caption2) .lineLimit(1) } .padding(.horizontal, 10) .padding(.vertical, 6) .background(isCurrent ? CMColors.accent.opacity(0.2) : CMColors.surfaceHover) .cornerRadius(16) .foregroundColor(isCurrent ? CMColors.accent : statusColor(step.status)) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(isCurrent ? CMColors.accent : Color.clear, lineWidth: 1) ) } @ViewBuilder private func statusIcon(_ status: StepStatus) -> some View { switch status { case .completed: Image(systemName: "checkmark.circle.fill") .foregroundColor(CMColors.gentle) case .skipped: Image(systemName: "forward.fill") .foregroundColor(CMColors.important) case .active: Image(systemName: "play.circle.fill") .foregroundColor(CMColors.accent) case .pending: Image(systemName: "circle") .foregroundColor(CMColors.textMuted) } } private func statusColor(_ status: StepStatus) -> Color { switch status { case .completed: return CMColors.gentle case .skipped: return CMColors.important case .active: return CMColors.accent case .pending: return CMColors.textMuted } } // MARK: - Controls private var controls: some View { HStack(spacing: 16) { if routine.status == .active { // Skip button Button { routine.skipCurrentStep() checkCompletion() } label: { Image(systemName: "forward.fill") .font(.title2) .foregroundColor(CMColors.textSecondary) .frame(width: 56, height: 56) .background(CMColors.surfaceHover) .clipShape(Circle()) } // Complete step button Button { routine.completeCurrentStep() checkCompletion() } label: { Image(systemName: "checkmark") .font(.title.bold()) .foregroundColor(.white) .frame(width: 72, height: 72) .background(CMColors.accent) .clipShape(Circle()) } // Pause button Button { routine.pause() } label: { Image(systemName: "pause.fill") .font(.title2) .foregroundColor(CMColors.textSecondary) .frame(width: 56, height: 56) .background(CMColors.surfaceHover) .clipShape(Circle()) } } else if routine.status == .paused { Button { routine.resume() } label: { Image(systemName: "play.fill") .font(.title.bold()) .foregroundColor(.white) .frame(width: 72, height: 72) .background(CMColors.accent) .clipShape(Circle()) } } } .padding(.bottom, 32) } // MARK: - Helpers private func formatRemaining(_ seconds: TimeInterval) -> String { let total = Int(max(0, seconds)) let m = total / 60 let s = total % 60 return String(format: "%d:%02d", m, s) } private func startTicking() { timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in now = Date() if routine.shouldStepComplete(now: now) { routine.completeCurrentStep() checkCompletion() } } } private func stopTicking() { timer?.invalidate() timer = nil } private func checkCompletion() { if routine.status == .completed { stopTicking() } } }