12 KiB
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
}
}
}