422 lines
12 KiB
Markdown
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
|
|
}
|
|
}
|
|
}
|
|
```
|