Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:23 +08:00
commit 9faa5d88f3
22 changed files with 9600 additions and 0 deletions

View File

@@ -0,0 +1,460 @@
# Advanced Integration Patterns
Comprehensive patterns for different app architectures and advanced error handling strategies.
## Pattern 1: Simple Single-View App
Best for: Basic sleep tracking app with minimal features.
```swift
@main
struct SimpleSleepApp: App {
var body: some Scene {
WindowGroup {
SleepTrackingView()
}
}
}
```
### Advantages
- Simple architecture
- Easy to understand and maintain
- Minimal code overhead
- Fast development
### Use Cases
- MVP or prototype apps
- Single-purpose sleep trackers
- Learning/demo applications
## Pattern 2: Multi-View App with Navigation
Best for: Apps with reports, settings, and history.
```swift
struct ContentView: View {
var body: some View {
TabView {
SleepTrackingView()
.tabItem {
Label("Track", systemImage: "moon.zzz")
}
ReportHistoryView()
.tabItem {
Label("History", systemImage: "chart.bar")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gear")
}
}
}
}
```
### Supporting Views
```swift
struct ReportHistoryView: View {
@StateObject private var viewModel = ReportHistoryViewModel()
var body: some View {
NavigationView {
List(viewModel.sessions) { session in
NavigationLink {
ReportDetailView(sessionId: session.id)
} label: {
SessionRow(session: session)
}
}
.navigationTitle("Sleep History")
.onAppear {
viewModel.loadSessions()
}
}
}
}
struct SettingsView: View {
@AppStorage("sleepapp+apikey") private var apiKey = ""
@AppStorage("sleepapp+userid") private var userId = ""
@AppStorage("sleepapp+notifications") private var notificationsEnabled = false
var body: some View {
NavigationView {
Form {
Section("Account") {
TextField("User ID", text: $userId)
SecureField("API Key", text: $apiKey)
}
Section("Preferences") {
Toggle("Enable Notifications", isOn: $notificationsEnabled)
}
}
.navigationTitle("Settings")
}
}
}
```
### Advantages
- Clear separation of concerns
- Scalable for additional features
- Familiar tab-based navigation
- Easy to add new sections
## Pattern 3: Centralized SDK Manager
Best for: Complex apps sharing SDK instance across views.
```swift
final class AsleepSDKManager: ObservableObject {
static let shared = AsleepSDKManager()
@Published var config: Asleep.Config?
@Published var isInitialized = false
@Published var error: String?
private var trackingManager: Asleep.SleepTrackingManager?
private var reports: Asleep.Reports?
private init() {}
func initialize(apiKey: String, userId: String) {
Asleep.initAsleepConfig(
apiKey: apiKey,
userId: userId,
delegate: self
)
}
func getTrackingManager() -> Asleep.SleepTrackingManager? {
guard let config else { return nil }
if trackingManager == nil {
trackingManager = Asleep.createSleepTrackingManager(
config: config,
delegate: self
)
}
return trackingManager
}
func getReports() -> Asleep.Reports? {
guard let config else { return nil }
if reports == nil {
reports = Asleep.createReports(config: config)
}
return reports
}
func reset() {
trackingManager = nil
reports = nil
config = nil
isInitialized = false
}
}
// MARK: - Delegates
extension AsleepSDKManager: AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config) {
Task { @MainActor in
self.config = config
self.isInitialized = true
}
}
func didFailUserJoin(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = "Failed to join: \(error.localizedDescription)"
}
}
func userDidDelete(userId: String) {
Task { @MainActor in
reset()
}
}
}
// Usage in views
struct SleepTrackingView: View {
@ObservedObject var sdkManager = AsleepSDKManager.shared
@StateObject private var trackingState = TrackingStateViewModel()
var body: some View {
VStack {
if sdkManager.isInitialized {
TrackingControls(manager: sdkManager.getTrackingManager())
} else {
ConfigurationView()
}
}
}
}
struct ReportHistoryView: View {
@ObservedObject var sdkManager = AsleepSDKManager.shared
var body: some View {
NavigationView {
if sdkManager.isInitialized {
ReportList(reports: sdkManager.getReports())
} else {
Text("Please configure the app first")
}
}
}
}
```
### Advantages
- Single source of truth for SDK state
- Prevents duplicate SDK instances
- Centralized error handling
- Easy to manage lifecycle across app
### Testing Strategy
```swift
protocol AsleepSDKManagerProtocol {
var config: Asleep.Config? { get }
var isInitialized: Bool { get }
func initialize(apiKey: String, userId: String)
func getTrackingManager() -> Asleep.SleepTrackingManager?
}
// For testing
class MockSDKManager: AsleepSDKManagerProtocol, ObservableObject {
@Published var config: Asleep.Config?
@Published var isInitialized = false
func initialize(apiKey: String, userId: String) {
isInitialized = true
}
func getTrackingManager() -> Asleep.SleepTrackingManager? {
nil
}
}
```
## Error Recovery Patterns
### Automatic Retry with Exponential Backoff
```swift
final class SleepTrackingViewModel: ObservableObject {
private var retryCount = 0
private let maxRetries = 3
func startTrackingWithRetry() {
trackingManager?.startTracking()
}
func didFail(error: Asleep.AsleepError) {
if isTransientError(error) && retryCount < maxRetries {
retryCount += 1
let delay = pow(2.0, Double(retryCount))
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.startTrackingWithRetry()
}
} else {
retryCount = 0
handleError(error)
}
}
private func isTransientError(_ error: Asleep.AsleepError) -> Bool {
switch error {
case .networkError, .uploadFailed:
return true
default:
return false
}
}
}
```
### Error Categorization and Handling
```swift
extension SleepTrackingViewModel {
func handleError(_ error: Asleep.AsleepError) {
switch error {
case .micPermission:
showAlert(
title: "Microphone Access Required",
message: "Please enable microphone access in Settings to track sleep.",
action: openSettings
)
case .audioSessionError:
showAlert(
title: "Audio Unavailable",
message: "Another app is using the microphone. Please close it and try again."
)
case let .httpStatus(code, _, message):
switch code {
case 403:
showAlert(
title: "Session Already Active",
message: "Another device is tracking with this user ID."
)
case 404:
showAlert(
title: "Session Not Found",
message: "The tracking session could not be found."
)
default:
showAlert(
title: "Error \(code)",
message: message ?? "An unknown error occurred"
)
}
default:
showAlert(
title: "Error",
message: error.localizedDescription
)
}
}
private func openSettings() {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
```
### Report Fetching with Retry
```swift
func fetchReportWithRetry(sessionId: String, maxAttempts: Int = 5) async {
for attempt in 1...maxAttempts {
do {
let report = try await reports?.report(sessionId: sessionId)
await MainActor.run {
self.currentReport = report
}
return
} catch {
if attempt < maxAttempts {
// Wait before retrying (exponential backoff)
let delay = UInt64(pow(2.0, Double(attempt))) * 1_000_000_000
try? await Task.sleep(nanoseconds: delay)
} else {
await MainActor.run {
self.error = "Report not ready. Please try again later."
}
}
}
}
}
```
### Graceful Degradation
```swift
struct SleepTrackingView: View {
@StateObject private var viewModel = SleepTrackingViewModel()
@State private var offlineMode = false
var body: some View {
VStack {
if offlineMode {
OfflineModeView()
} else {
OnlineModeView(viewModel: viewModel)
}
}
.onReceive(viewModel.$error) { error in
if let error = error, isNetworkError(error) {
offlineMode = true
}
}
}
private func isNetworkError(_ error: String) -> Bool {
error.contains("network") || error.contains("connection")
}
}
struct OfflineModeView: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "wifi.slash")
.font(.system(size: 60))
.foregroundColor(.secondary)
Text("Offline Mode")
.font(.headline)
Text("Sleep tracking data will sync when connection is restored")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
}
```
## Testing Patterns
### Dependency Injection for Testing
```swift
protocol SleepTrackingManagerProtocol {
func startTracking()
func stopTracking()
}
final class SleepTrackingViewModel: ObservableObject {
private let trackingManager: SleepTrackingManagerProtocol
init(trackingManager: SleepTrackingManagerProtocol) {
self.trackingManager = trackingManager
}
func startTracking() {
trackingManager.startTracking()
}
}
// For testing
class MockTrackingManager: SleepTrackingManagerProtocol {
var startTrackingCalled = false
var stopTrackingCalled = false
func startTracking() {
startTrackingCalled = true
}
func stopTracking() {
stopTrackingCalled = true
}
}
// Usage in tests
func testStartTracking() {
let mockManager = MockTrackingManager()
let viewModel = SleepTrackingViewModel(trackingManager: mockManager)
viewModel.startTracking()
XCTAssertTrue(mockManager.startTrackingCalled)
}
```

View File

@@ -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
}
}
}
```

View File

@@ -0,0 +1,284 @@
# iOS-Specific Features
Detailed implementation guides for iOS platform features including Siri Shortcuts, background modes, permissions, lifecycle management, and persistent storage.
## Siri Shortcuts Integration
Enable voice-activated tracking with App Intents (iOS 16+):
```swift
// StartSleepIntent.swift
import AppIntents
@available(iOS 16, *)
struct StartSleepIntent: AppIntent {
static var title: LocalizedStringResource = "Start Sleep"
static var description = IntentDescription("Start Sleep Tracking")
func perform() async throws -> some IntentResult {
NotificationCenter.default.post(name: .startSleep, object: nil)
return .result()
}
}
// StopSleepIntent.swift
@available(iOS 16, *)
struct StopSleepIntent: AppIntent {
static var title: LocalizedStringResource = "Stop Sleep"
static var description = IntentDescription("Stop Sleep Tracking")
func perform() async throws -> some IntentResult {
NotificationCenter.default.post(name: .stopSleep, object: nil)
return .result()
}
}
// Notification extensions
extension Notification.Name {
static let startSleep = Notification.Name("startSleep")
static let stopSleep = Notification.Name("stopSleep")
}
```
### Handling Shortcuts in Views
```swift
struct SleepTrackingView: View {
@StateObject private var viewModel = SleepTrackingViewModel()
var body: some View {
// ... view content ...
.onReceive(NotificationCenter.default.publisher(for: .startSleep)) { _ in
if !viewModel.isTracking {
startTracking()
}
}
.onReceive(NotificationCenter.default.publisher(for: .stopSleep)) { _ in
if viewModel.isTracking {
stopTracking()
}
}
}
}
```
Users can then say: "Hey Siri, start sleep" or "Hey Siri, stop sleep"
## Background Audio Mode
Configure background audio to maintain tracking during sleep.
### Info.plist Configuration
```xml
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
```
### Implementation Notes
- iOS automatically maintains background audio session during tracking
- App remains active in background while microphone is in use
- User sees audio indicator (red bar/pill) showing active recording
- No additional code needed beyond Info.plist configuration
- System handles audio session management automatically
### Best Practices
1. Inform users why the app needs background audio mode
2. Display clear status indicators when tracking is active
3. Handle audio interruptions gracefully (phone calls, other apps)
4. Test background behavior thoroughly on physical devices
## Microphone Permission Handling
Request and handle microphone permission properly:
```swift
import AVFoundation
func requestMicrophonePermission() async -> Bool {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted:
return true
case .denied:
return false
case .undetermined:
return await AVAudioSession.sharedInstance().requestRecordPermission()
@unknown default:
return false
}
}
// Usage in SwiftUI
Button("Start Tracking") {
Task {
let hasPermission = await requestMicrophonePermission()
if hasPermission {
startTracking()
} else {
showPermissionAlert = true
}
}
}
```
### Permission Alert Handling
```swift
struct PermissionAlert: ViewModifier {
@Binding var showAlert: Bool
func body(content: Content) -> some View {
content
.alert("Microphone Access Required", isPresented: $showAlert) {
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Please enable microphone access in Settings to track sleep.")
}
}
}
```
### Checking Permission Status
```swift
func checkMicrophonePermission() -> Bool {
let status = AVAudioSession.sharedInstance().recordPermission
return status == .granted
}
```
## App Lifecycle Management
Handle app state transitions gracefully:
```swift
struct SleepTrackingView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
// ... view content ...
.onChange(of: scenePhase) { newPhase in
switch newPhase {
case .active:
print("App is active")
// Refresh UI state if needed
case .inactive:
print("App is inactive")
// Prepare for potential backgrounding
case .background:
// Tracking continues in background with audio mode
print("App is in background")
// Minimal operations only
@unknown default:
break
}
}
}
}
```
### Advanced Lifecycle Handling
```swift
class AppLifecycleObserver: ObservableObject {
@Published var isActive = true
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.handleEnterBackground()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.handleEnterForeground()
}
.store(in: &cancellables)
}
private func handleEnterBackground() {
isActive = false
// Save state, reduce operations
}
private func handleEnterForeground() {
isActive = true
// Refresh state, resume operations
}
}
```
## Persistent Storage with AppStorage
Store configuration persistently across app launches:
```swift
struct SleepTrackingView: View {
@AppStorage("sleepapp+apikey") private var apiKey = ""
@AppStorage("sleepapp+userid") private var userId = ""
@AppStorage("sleepapp+baseurl") private var baseUrl = ""
// Values automatically persist across app launches
// Uses UserDefaults under the hood
}
```
### Custom Storage Keys
```swift
extension String {
static let apiKeyStorage = "sleepapp+apikey"
static let userIdStorage = "sleepapp+userid"
static let baseUrlStorage = "sleepapp+baseurl"
}
struct SleepTrackingView: View {
@AppStorage(.apiKeyStorage) private var apiKey = ""
@AppStorage(.userIdStorage) private var userId = ""
@AppStorage(.baseUrlStorage) private var baseUrl = ""
}
```
### Advanced Persistent Storage
```swift
class PersistentSettings: ObservableObject {
@AppStorage("sleepapp+apikey") var apiKey = ""
@AppStorage("sleepapp+userid") var userId = ""
@AppStorage("sleepapp+baseurl") var baseUrl = ""
@AppStorage("sleepapp+notifications") var notificationsEnabled = false
@AppStorage("sleepapp+lastSessionId") var lastSessionId = ""
func clearAll() {
apiKey = ""
userId = ""
baseUrl = ""
notificationsEnabled = false
lastSessionId = ""
}
}
// Usage
struct SettingsView: View {
@StateObject private var settings = PersistentSettings()
var body: some View {
Form {
TextField("API Key", text: $settings.apiKey)
TextField("User ID", text: $settings.userId)
Toggle("Notifications", isOn: $settings.notificationsEnabled)
}
}
}
```