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

422 lines
12 KiB
Markdown

# Complete ViewModel Implementation
This reference provides full implementation examples for iOS sleep tracking using Combine and SwiftUI.
## Full ViewModel with Combine
```swift
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
```swift
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
```swift
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
}
}
}
```