Skip to content

Daily Check-In Feature - Implementation Plan

Priority: HIGH (Value Loop Creation) Timeline: 2-3 weeks Goal: Create feedback loop showing users the IMPACT of their supplements


Core Data Model

DailyCheckIn Entity

@objc(DailyCheckIn)
public class DailyCheckIn: NSManagedObject {
    @NSManaged public var id: UUID
    @NSManaged public var date: Date
    @NSManaged public var focusLevel: Int16      // 1-5 (Very Unfocused to Very Focused)
    @NSManaged public var sleepQuality: Int16    // 1-5 (Very Poor to Excellent)
    @NSManaged public var energyLevel: Int16     // 1-5 (Exhausted to Energized)
    @NSManaged public var stressLevel: Int16     // 1-5 (Very Stressed to Very Calm)
    @NSManaged public var mood: String?          // Optional notes
    @NSManaged public var createdAt: Date
}

Attributes: - id: UUID (primary key) - date: Date (indexed, unique per day) - focusLevel: Int16 (1-5 scale) - sleepQuality: Int16 (1-5 scale) - energyLevel: Int16 (1-5 scale) - stressLevel: Int16 (1-5 scale) - mood: String (optional text notes) - createdAt: Date (timestamp)

Constraints: - Unique constraint on date (one check-in per day) - All levels must be 1-5 range


Service Layer: DailyCheckInService.swift

import Foundation
import CoreData

class DailyCheckInService {
    static let shared = DailyCheckInService()
    private let viewContext = PersistenceController.shared.container.viewContext

    // MARK: - CRUD Operations

    /// Create or update today's check-in
    func saveCheckIn(focus: Int, sleep: Int, energy: Int, stress: Int, mood: String? = nil) {
        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date())

        // Check if today's check-in already exists
        let fetchRequest: NSFetchRequest<DailyCheckIn> = DailyCheckIn.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "date == %@", today as CVarArg)

        do {
            let results = try viewContext.fetch(fetchRequest)
            let checkIn = results.first ?? DailyCheckIn(context: viewContext)

            checkIn.id = checkIn.id ?? UUID()
            checkIn.date = today
            checkIn.focusLevel = Int16(focus)
            checkIn.sleepQuality = Int16(sleep)
            checkIn.energyLevel = Int16(energy)
            checkIn.stressLevel = Int16(stress)
            checkIn.mood = mood
            checkIn.createdAt = checkIn.createdAt ?? Date()

            DataManager.shared.save()
        } catch {
            print("Error saving check-in: \\(error)")
        }
    }

    /// Get today's check-in (if exists)
    func getTodayCheckIn() -> DailyCheckIn? {
        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date())

        let fetchRequest: NSFetchRequest<DailyCheckIn> = DailyCheckIn.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "date == %@", today as CVarArg)

        return try? viewContext.fetch(fetchRequest).first
    }

    /// Check if user has checked in today
    func hasCheckedInToday() -> Bool {
        return getTodayCheckIn() != nil
    }

    /// Get check-ins for date range
    func getCheckIns(from startDate: Date, to endDate: Date) -> [DailyCheckIn] {
        let fetchRequest: NSFetchRequest<DailyCheckIn> = DailyCheckIn.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "date >= %@ AND date <= %@",
                                            startDate as CVarArg, endDate as CVarArg)
        fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \\DailyCheckIn.date, ascending: true)]

        return (try? viewContext.fetch(fetchRequest)) ?? []
    }

    // MARK: - Analytics

    /// Calculate average wellness metric for date range
    func getAverageMetric(_ keyPath: KeyPath<DailyCheckIn, Int16>,
                         from startDate: Date, to endDate: Date) -> Double {
        let checkIns = getCheckIns(from: startDate, to: endDate)
        guard !checkIns.isEmpty else { return 0 }

        let sum = checkIns.reduce(0) { $0 + Double($1[keyPath: keyPath]) }
        return sum / Double(checkIns.count)
    }

    /// Calculate wellness improvement % for a supplement
    func getSupplementImpact(supplement: Supplement, metric: WellnessMetric) -> Double? {
        // Find first dose date for this supplement
        guard let firstDose = getFirstDoseDate(for: supplement) else { return nil }

        let calendar = Calendar.current

        // Before period: 7 days before first dose
        guard let beforeStart = calendar.date(byAdding: .day, value: -7, to: firstDose) else { return nil }
        let beforeEnd = calendar.date(byAdding: .day, value: -1, to: firstDose) ?? firstDose

        // After period: 7-14 days after first dose (skip first week for supplement to take effect)
        guard let afterStart = calendar.date(byAdding: .day, value: 7, to: firstDose),
              let afterEnd = calendar.date(byAdding: .day, value: 14, to: firstDose) else { return nil }

        let keyPath: KeyPath<DailyCheckIn, Int16> = metric.keyPath

        let beforeAvg = getAverageMetric(keyPath, from: beforeStart, to: beforeEnd)
        let afterAvg = getAverageMetric(keyPath, from: afterStart, to: afterEnd)

        guard beforeAvg > 0 else { return nil }

        let improvement = ((afterAvg - beforeAvg) / beforeAvg) * 100
        return improvement.isFinite ? improvement : nil
    }

    /// Get first dose date for supplement
    private func getFirstDoseDate(for supplement: Supplement) -> Date? {
        let fetchRequest: NSFetchRequest<SupplementDose> = SupplementDose.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "supplement == %@", supplement)
        fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \\SupplementDose.takenAt, ascending: true)]
        fetchRequest.fetchLimit = 1

        return try? viewContext.fetch(fetchRequest).first?.takenAt
    }

    /// Get trending metrics (7-day moving average)
    func getWeeklyTrends() -> [WellnessMetric: [Double]] {
        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date())
        guard let weekAgo = calendar.date(byAdding: .day, value: -7, to: today) else {
            return [:]
        }

        let checkIns = getCheckIns(from: weekAgo, to: today)

        var trends: [WellnessMetric: [Double]] = [:]

        for metric in WellnessMetric.allCases {
            trends[metric] = checkIns.map { Double($0[keyPath: metric.keyPath]) }
        }

        return trends
    }
}

// MARK: - Wellness Metrics

enum WellnessMetric: String, CaseIterable {
    case focus = "Focus"
    case sleep = "Sleep"
    case energy = "Energy"
    case stress = "Stress"

    var keyPath: KeyPath<DailyCheckIn, Int16> {
        switch self {
        case .focus: return \\.focusLevel
        case .sleep: return \\.sleepQuality
        case .energy: return \\.energyLevel
        case .stress: return \\.stressLevel
        }
    }

    var emoji: String {
        switch self {
        case .focus: return "🧠"
        case .sleep: return "😴"
        case .energy: return "⚡"
        case .stress: return "🧘"
        }
    }

    var lowLabel: String {
        switch self {
        case .focus: return "Very Unfocused"
        case .sleep: return "Poor Sleep"
        case .energy: return "Exhausted"
        case .stress: return "Very Stressed"
        }
    }

    var highLabel: String {
        switch self {
        case .focus: return "Very Focused"
        case .sleep: return "Excellent Sleep"
        case .energy: return "Energized"
        case .stress: return "Very Calm"
        }
    }
}

UI Layer: CheckInView.swift

import SwiftUI

struct CheckInView: View {
    @Environment(\\.dismiss) private var dismiss

    @State private var focusLevel: Double = 3
    @State private var sleepQuality: Double = 3
    @State private var energyLevel: Double = 3
    @State private var stressLevel: Double = 3
    @State private var mood: String = ""
    @State private var showThankYou = false

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 30) {
                    // Header
                    VStack(spacing: 10) {
                        Text("Daily Check-In")
                            .font(.title)
                            .fontWeight(.bold)

                        Text("How are you feeling today?")
                            .font(.subheadline)
                            .foregroundColor(.secondary)
                    }
                    .padding(.top)

                    // Wellness Metrics
                    VStack(spacing: 25) {
                        WellnessSlider(
                            metric: .focus,
                            value: $focusLevel
                        )

                        WellnessSlider(
                            metric: .sleep,
                            value: $sleepQuality
                        )

                        WellnessSlider(
                            metric: .energy,
                            value: $energyLevel
                        )

                        WellnessSlider(
                            metric: .stress,
                            value: $stressLevel
                        )
                    }
                    .padding(.horizontal)

                    // Optional Mood Notes
                    VStack(alignment: .leading, spacing: 10) {
                        Text("Notes (optional)")
                            .font(.headline)

                        TextField("How are you feeling?", text: $mood, axis: .vertical)
                            .textFieldStyle(.roundedBorder)
                            .lineLimit(3...6)
                    }
                    .padding(.horizontal)

                    // Save Button
                    Button(action: saveCheckIn) {
                        Text("Save Check-In")
                            .font(.headline)
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.blue)
                            .cornerRadius(10)
                    }
                    .padding(.horizontal)
                    .padding(.bottom)
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Skip") {
                        dismiss()
                    }
                }
            }
            .alert("Thank You! 🙏", isPresented: $showThankYou) {
                Button("Done") {
                    dismiss()
                }
            } message: {
                Text("Your check-in helps us show you the impact of your supplements over time.")
            }
        }
    }

    private func saveCheckIn() {
        DailyCheckInService.shared.saveCheckIn(
            focus: Int(focusLevel),
            sleep: Int(sleepQuality),
            energy: Int(energyLevel),
            stress: Int(stressLevel),
            mood: mood.isEmpty ? nil : mood
        )

        showThankYou = true
    }
}

// MARK: - Wellness Slider Component

struct WellnessSlider: View {
    let metric: WellnessMetric
    @Binding var value: Double

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text(metric.emoji)
                    .font(.title2)
                Text(metric.rawValue)
                    .font(.headline)
                Spacer()
                Text(currentLabel)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }

            Slider(value: $value, in: 1...5, step: 1)
                .tint(sliderColor)

            HStack {
                Text(metric.lowLabel)
                    .font(.caption)
                    .foregroundColor(.secondary)
                Spacer()
                Text(metric.highLabel)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .padding()
        .background(Color(.secondarySystemBackground))
        .cornerRadius(10)
    }

    private var currentLabel: String {
        let level = Int(value)
        switch level {
        case 1: return "😔"
        case 2: return "😕"
        case 3: return "😐"
        case 4: return "🙂"
        case 5: return "😄"
        default: return "😐"
        }
    }

    private var sliderColor: Color {
        let level = Int(value)
        switch level {
        case 1, 2: return .red
        case 3: return .orange
        case 4, 5: return .green
        default: return .blue
        }
    }
}

#Preview {
    CheckInView()
}

Integration Points

1. Prompt After Dose Logging (DoseTrackingView.swift)

Add check-in prompt after user logs a dose:

// In DoseTrackingView.swift, after successful dose logging:

.onChange(of: doseLogged) { oldValue, newValue in
    if newValue {
        // Show success message
        showingSuccessAlert = true

        // Check if user should be prompted for daily check-in
        if !DailyCheckInService.shared.hasCheckedInToday() {
            // Random chance (50%) or once per day after first dose
            if shouldPromptCheckIn() {
                showingCheckIn = true
            }
        }
    }
}
.sheet(isPresented: $showingCheckIn) {
    CheckInView()
}

private func shouldPromptCheckIn() -> Bool {
    // Only prompt once per day, after first dose of the day
    let calendar = Calendar.current
    let today = calendar.startOfDay(for: Date())

    // Get today's doses
    let todayDoses = supplement?.dosesArray?
        .filter { calendar.isDate($0.takenAt ?? Date(), inSameDayAs: today) }

    // Prompt if this is the first dose of the day
    return todayDoses?.count == 1
}

2. Analytics Integration (AnalyticsView.swift)

Add "Supplement Impact" tab showing correlations:

// New tab in AnalyticsView

TabView {
    // ... existing tabs ...

    SupplementImpactView()
        .tabItem {
            Label("Impact", systemImage: "chart.line.uptrend.xyaxis")
        }
}

// New view: SupplementImpactView.swift

struct SupplementImpactView: View {
    @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \\Supplement.name, ascending: true)])
    private var supplements: FetchedResults<Supplement>

    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                Text("Supplement Impact")
                    .font(.title)
                    .fontWeight(.bold)
                    .padding()

                ForEach(supplements, id: \\.id) { supplement in
                    SupplementImpactCard(supplement: supplement)
                }
            }
        }
    }
}

struct SupplementImpactCard: View {
    let supplement: Supplement

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text(supplement.name ?? "Unknown")
                .font(.headline)

            ForEach(WellnessMetric.allCases, id: \\.self) { metric in
                if let impact = DailyCheckInService.shared.getSupplementImpact(
                    supplement: supplement,
                    metric: metric
                ) {
                    ImpactRow(metric: metric, impact: impact)
                }
            }
        }
        .padding()
        .background(Color(.secondarySystemBackground))
        .cornerRadius(10)
        .padding(.horizontal)
    }
}

struct ImpactRow: View {
    let metric: WellnessMetric
    let impact: Double

    var body: some View {
        HStack {
            Text(metric.emoji)
            Text(metric.rawValue)
            Spacer()
            Text(String(format: "%+.0f%%", impact))
                .fontWeight(.bold)
                .foregroundColor(impact > 0 ? .green : .red)
        }
    }
}

Testing Checklist

  • [ ] User can create daily check-in
  • [ ] Check-in persists to Core Data
  • [ ] Only one check-in per day (unique constraint)
  • [ ] Check-in prompt appears after first dose
  • [ ] Can skip check-in
  • [ ] Analytics show supplement impact correctly
  • [ ] Correlation calculation handles edge cases (no before/after data)
  • [ ] UI displays emoji feedback correctly
  • [ ] Slider values save correctly (1-5 range)

Next Steps

  1. ✅ Add DailyCheckIn to Core Data model (Xcode)
  2. Create DailyCheckInService.swift
  3. Create CheckInView.swift
  4. Integrate prompt in DoseTrackingView
  5. Create SupplementImpactView for analytics
  6. Add unit tests
  7. Test with real data
  8. Deploy to TestFlight for user feedback

Success Metrics

Before Implementation: - Average user retention: X% - Daily active users: Y

After Implementation (Target): - 30% increase in user retention (users want to see results) - 40% increase in daily check-ins (engagement) - Users see value: "I can finally see if supplements work!"

Key Metric: Correlation between check-ins and continued app usage