Files
gh-asleep-ai-sleeptrack-skills/skills/sleeptrack-ios/references/complete_viewmodel_implementation.md
2025-11-29 17:58:20 +08:00

12 KiB

Complete ViewModel Implementation

This reference provides full implementation examples for iOS sleep tracking using Combine and SwiftUI.

Full ViewModel with Combine

import Foundation
import Combine
import AsleepSDK

final class SleepTrackingViewModel: ObservableObject {
    // MARK: - SDK Components
    private(set) var trackingManager: Asleep.SleepTrackingManager?
    private(set) var reports: Asleep.Reports?

    // MARK: - Published State
    @Published var userId: String?
    @Published var sessionId: String?
    @Published var sequenceNumber: Int?
    @Published var error: String?
    @Published var isTracking = false
    @Published var currentReport: Asleep.Model.Report?
    @Published var reportList: [Asleep.Model.SleepSession]?
    @Published private(set) var config: Asleep.Config?

    // MARK: - Initialization
    func initAsleepConfig(
        apiKey: String,
        userId: String,
        baseUrl: URL? = nil,
        callbackUrl: URL? = nil
    ) {
        Asleep.initAsleepConfig(
            apiKey: apiKey,
            userId: userId.isEmpty ? nil : userId,
            baseUrl: baseUrl,
            callbackUrl: callbackUrl,
            delegate: self
        )

        // Optional: Enable debug logging
        Asleep.setDebugLoggerDelegate(self)
    }

    func initSleepTrackingManager() {
        guard let config else { return }
        trackingManager = Asleep.createSleepTrackingManager(
            config: config,
            delegate: self
        )
    }

    func initReports() {
        guard let config else { return }
        reports = Asleep.createReports(config: config)
    }

    // MARK: - Tracking Control
    func startTracking() {
        trackingManager?.startTracking()
    }

    func stopTracking() {
        trackingManager?.stopTracking()
        initReports()
    }
}

// MARK: - AsleepConfigDelegate
extension SleepTrackingViewModel: AsleepConfigDelegate {
    func userDidJoin(userId: String, config: Asleep.Config) {
        Task { @MainActor in
            self.config = config
            self.userId = userId
            initSleepTrackingManager()
        }
    }

    func didFailUserJoin(error: Asleep.AsleepError) {
        Task { @MainActor in
            self.error = "Failed to join: \(error.localizedDescription)"
        }
    }

    func userDidDelete(userId: String) {
        Task { @MainActor in
            self.userId = nil
            self.config = nil
        }
    }
}

// MARK: - AsleepSleepTrackingManagerDelegate
extension SleepTrackingViewModel: AsleepSleepTrackingManagerDelegate {
    func didCreate() {
        Task { @MainActor in
            self.isTracking = true
            self.error = nil
        }
    }

    func didUpload(sequence: Int) {
        Task { @MainActor in
            self.sequenceNumber = sequence
        }
    }

    func didClose(sessionId: String) {
        Task { @MainActor in
            self.isTracking = false
            self.sessionId = sessionId
        }
    }

    func didFail(error: Asleep.AsleepError) {
        switch error {
        case let .httpStatus(code, _, message) where code == 403 || code == 404:
            Task { @MainActor in
                self.isTracking = false
                self.error = "\(code): \(message ?? "Unknown error")"
            }
        default:
            Task { @MainActor in
                self.error = error.localizedDescription
            }
        }
    }

    func didInterrupt() {
        Task { @MainActor in
            self.error = "Tracking interrupted (e.g., phone call)"
        }
    }

    func didResume() {
        Task { @MainActor in
            self.error = nil
        }
    }

    func micPermissionWasDenied() {
        Task { @MainActor in
            self.isTracking = false
            self.error = "Microphone permission denied. Please enable in Settings."
        }
    }

    func analysing(session: Asleep.Model.Session) {
        // Optional: Handle real-time analysis data
        print("Real-time analysis:", session)
    }
}

// MARK: - AsleepDebugLoggerDelegate (Optional)
extension SleepTrackingViewModel: AsleepDebugLoggerDelegate {
    func didPrint(message: String) {
        print("[Asleep SDK]", message)
    }
}

Complete SwiftUI View

import SwiftUI

struct SleepTrackingView: View {
    @StateObject private var viewModel = SleepTrackingViewModel()
    @AppStorage("sampleapp+apikey") private var apiKey = ""
    @AppStorage("sampleapp+userid") private var userId = ""

    @State private var startTime: Date?
    @State private var showingReport = false

    var body: some View {
        VStack(spacing: 20) {
            // Configuration Section
            VStack(alignment: .leading, spacing: 8) {
                Text("Configuration")
                    .font(.headline)

                TextField("API Key", text: $apiKey)
                    .textFieldStyle(.roundedBorder)
                    .disabled(viewModel.isTracking)

                TextField("User ID", text: $userId)
                    .textFieldStyle(.roundedBorder)
                    .disabled(viewModel.isTracking)
            }
            .padding()

            // Status Section
            VStack(alignment: .leading, spacing: 8) {
                Text("Status")
                    .font(.headline)

                if let error = viewModel.error {
                    Text("Error: \(error)")
                        .foregroundColor(.red)
                        .font(.caption)
                }

                if viewModel.isTracking {
                    HStack {
                        ProgressView()
                        Text("Tracking...")
                    }

                    if let sequence = viewModel.sequenceNumber {
                        Text("Sequence: \(sequence)")
                            .font(.caption)
                    }
                }

                if let sessionId = viewModel.sessionId {
                    Text("Session ID: \(sessionId)")
                        .font(.caption)
                        .lineLimit(1)
                }
            }
            .padding()

            // Tracking Control
            Button(action: {
                if viewModel.isTracking {
                    stopTracking()
                } else {
                    startTracking()
                }
            }) {
                Text(viewModel.isTracking ? "Stop Tracking" : "Start Tracking")
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(viewModel.isTracking ? Color.red : Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .disabled(apiKey.isEmpty || userId.isEmpty)
            .padding(.horizontal)

            // Report Access
            if viewModel.sessionId != nil {
                Button("View Report") {
                    fetchReport()
                }
                .padding()
            }

            Spacer()
        }
        .padding()
        .sheet(isPresented: $showingReport) {
            ReportView(report: viewModel.currentReport)
        }
    }

    private func startTracking() {
        viewModel.sessionId = nil
        viewModel.sequenceNumber = nil

        if viewModel.config == nil {
            viewModel.initAsleepConfig(
                apiKey: apiKey,
                userId: userId
            )
        } else {
            viewModel.startTracking()
        }

        startTime = Date()
    }

    private func stopTracking() {
        viewModel.stopTracking()
    }

    private func fetchReport() {
        guard let sessionId = viewModel.sessionId else { return }

        Task {
            do {
                let report = try await viewModel.reports?.report(sessionId: sessionId)
                await MainActor.run {
                    viewModel.currentReport = report
                    showingReport = true
                }
            } catch {
                await MainActor.run {
                    viewModel.error = "Failed to fetch report: \(error.localizedDescription)"
                }
            }
        }
    }
}

Complete Report View

import SwiftUI
import AsleepSDK

struct ReportView: View {
    @Environment(\.dismiss) private var dismiss
    let report: Asleep.Model.Report?

    var body: some View {
        NavigationView {
            ScrollView {
                if let report = report {
                    VStack(alignment: .leading, spacing: 16) {
                        // Session Information
                        Section("Session Information") {
                            InfoRow(label: "Session ID", value: report.session.id)
                            InfoRow(label: "Start Time", value: report.session.startTime.formatted())
                            if let endTime = report.session.endTime {
                                InfoRow(label: "End Time", value: endTime.formatted())
                            }
                            InfoRow(label: "State", value: report.session.state.rawValue)
                        }

                        Divider()

                        // Sleep Statistics
                        if let stat = report.stat {
                            Section("Sleep Statistics") {
                                StatRow(label: "Sleep Efficiency", value: stat.sleepEfficiency, unit: "%")
                                StatRow(label: "Sleep Latency", value: stat.sleepLatency, unit: "min")
                                StatRow(label: "Total Sleep Time", value: stat.sleepTime, unit: "min")
                                StatRow(label: "Time in Bed", value: stat.timeInBed, unit: "min")
                            }

                            Divider()

                            Section("Sleep Stages") {
                                StatRow(label: "Deep Sleep", value: stat.timeInDeep, unit: "min")
                                StatRow(label: "Light Sleep", value: stat.timeInLight, unit: "min")
                                StatRow(label: "REM Sleep", value: stat.timeInRem, unit: "min")
                                StatRow(label: "Wake Time", value: stat.timeInWake, unit: "min")
                            }

                            Divider()

                            Section("Snoring Analysis") {
                                StatRow(label: "Time Snoring", value: stat.timeInSnoring, unit: "min")
                                StatRow(label: "Snoring Count", value: stat.snoringCount, unit: "times")
                            }
                        }
                    }
                    .padding()
                } else {
                    Text("No report available")
                        .foregroundColor(.secondary)
                }
            }
            .navigationTitle("Sleep Report")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Done") {
                        dismiss()
                    }
                }
            }
        }
    }
}

struct InfoRow: View {
    let label: String
    let value: String

    var body: some View {
        HStack {
            Text(label)
                .foregroundColor(.secondary)
            Spacer()
            Text(value)
        }
    }
}

struct StatRow: View {
    let label: String
    let value: Int?
    let unit: String

    var body: some View {
        HStack {
            Text(label)
                .foregroundColor(.secondary)
            Spacer()
            if let value = value {
                Text("\(value) \(unit)")
            } else {
                Text("N/A")
                    .foregroundColor(.secondary)
            }
        }
    }
}

struct Section<Content: View>: View {
    let title: String
    let content: Content

    init(_ title: String, @ViewBuilder content: () -> Content) {
        self.title = title
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(title)
                .font(.headline)
            content
        }
    }
}