Initial commit
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user