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¶
- ✅ Add DailyCheckIn to Core Data model (Xcode)
- Create DailyCheckInService.swift
- Create CheckInView.swift
- Integrate prompt in DoseTrackingView
- Create SupplementImpactView for analytics
- Add unit tests
- Test with real data
- 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