Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:20 +08:00
commit b1b19cb098
22 changed files with 9598 additions and 0 deletions

View File

@@ -0,0 +1,526 @@
---
name: sleeptrack-ios
description: This skill helps iOS developers integrate the Asleep SDK for sleep tracking functionality. Use this skill when building native iOS apps with Swift/SwiftUI that need sleep tracking capabilities, implementing delegate patterns, configuring iOS permissions (microphone, notifications, background modes), managing tracking lifecycle, integrating Siri Shortcuts, or working with Combine framework for reactive state management.
---
# Sleeptrack iOS
## Overview
This skill provides comprehensive guidance for integrating the Asleep SDK into native iOS applications using Swift and SwiftUI. It covers SDK setup, iOS-specific permissions, delegate-based architecture, tracking lifecycle management, Combine framework integration, and Siri Shortcuts support.
Use this skill when:
- Building native iOS sleep tracking applications
- Implementing SwiftUI-based tracking interfaces
- Managing iOS permissions and background modes
- Working with delegate patterns for SDK callbacks
- Integrating Siri Shortcuts for voice-activated tracking
- Using Combine framework for reactive state management
**Prerequisites**: Developers should first review the `sleeptrack-foundation` skill to understand core Asleep concepts, authentication, data structures, and error handling before implementing iOS-specific integration.
## Quick Start
### 1. Installation
Add AsleepSDK to your Xcode project using Swift Package Manager:
```swift
// In Xcode: File Add Packages
// Enter package URL: https://github.com/asleep-ai/asleep-sdk-ios
```
Or add to `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/asleep-ai/asleep-sdk-ios", from: "2.0.0")
]
```
### 2. Configure iOS Permissions
Add required permissions to `Info.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Microphone access for audio-based sleep tracking -->
<key>NSMicrophoneUsageDescription</key>
<string>This app uses your microphone to track sleep stages and detect snoring during sleep.</string>
<!-- Background audio mode for continuous tracking -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<!-- Optional: For notification reminders -->
<key>NSUserNotificationsUsageDescription</key>
<string>Get reminders to start and stop sleep tracking.</string>
</dict>
</plist>
```
### 3. Basic Setup
```swift
import SwiftUI
import AsleepSDK
@main
struct SleepTrackerApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
}
}
```
## SDK Architecture
The Asleep iOS SDK follows a delegate-based architecture with three main components:
### 1. AsleepConfig - Configuration and User Management
**Purpose**: Initialize SDK with API credentials and manage user lifecycle.
**Key Delegate**: `AsleepConfigDelegate`
```swift
protocol AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config)
func didFailUserJoin(error: Asleep.AsleepError)
func userDidDelete(userId: String)
}
```
### 2. SleepTrackingManager - Tracking Lifecycle
**Purpose**: Control sleep tracking start, stop, and monitor session state.
**Key Delegate**: `AsleepSleepTrackingManagerDelegate`
```swift
protocol AsleepSleepTrackingManagerDelegate {
func didCreate() // Session created
func didUpload(sequence: Int) // Data uploaded
func didClose(sessionId: String) // Tracking stopped
func didFail(error: Asleep.AsleepError) // Error occurred
func didInterrupt() // Interrupted (e.g., phone call)
func didResume() // Resumed after interruption
func micPermissionWasDenied() // Mic permission denied
func analysing(session: Asleep.Model.Session) // Real-time data (optional)
}
```
### 3. Reports - Retrieving Sleep Data
**Purpose**: Fetch sleep reports and session lists after tracking completes.
```swift
// Reports API is async/await based, not delegate-driven
let reports = Asleep.createReports(config: config)
// Get single report
let report = try await reports.report(sessionId: "session_id")
// Get report list
let reportList = try await reports.reports(
fromDate: "2024-01-01",
toDate: "2024-01-31"
)
```
## Implementation Overview
### Minimal ViewModel Example
```swift
import Foundation
import Combine
import AsleepSDK
final class SleepTrackingViewModel: ObservableObject {
private(set) var trackingManager: Asleep.SleepTrackingManager?
private(set) var reports: Asleep.Reports?
@Published var isTracking = false
@Published var error: String?
@Published private(set) var config: Asleep.Config?
func initAsleepConfig(apiKey: String, userId: String) {
Asleep.initAsleepConfig(
apiKey: apiKey,
userId: userId,
delegate: self
)
}
func startTracking() {
trackingManager?.startTracking()
}
func stopTracking() {
trackingManager?.stopTracking()
}
}
// Implement delegates
extension SleepTrackingViewModel: AsleepConfigDelegate {
func userDidJoin(userId: String, config: Asleep.Config) {
Task { @MainActor in
self.config = config
self.trackingManager = Asleep.createSleepTrackingManager(
config: config,
delegate: self
)
}
}
func didFailUserJoin(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = error.localizedDescription
}
}
func userDidDelete(userId: String) {
// Handle user deletion
}
}
extension SleepTrackingViewModel: AsleepSleepTrackingManagerDelegate {
func didCreate() {
Task { @MainActor in
self.isTracking = true
}
}
func didClose(sessionId: String) {
Task { @MainActor in
self.isTracking = false
// Initialize reports to fetch session data
self.reports = Asleep.createReports(config: config!)
}
}
func didFail(error: Asleep.AsleepError) {
Task { @MainActor in
self.error = error.localizedDescription
}
}
// Implement other delegate methods as needed
}
```
For complete ViewModel implementation with all delegate methods, see [references/complete_viewmodel_implementation.md](references/complete_viewmodel_implementation.md)
## iOS-Specific Features
### 1. Siri Shortcuts
Enable voice-activated tracking with App Intents (iOS 16+). Users can say "Hey Siri, start sleep" or "Hey Siri, stop sleep".
For complete Siri Shortcuts implementation, see [references/ios_specific_features.md](references/ios_specific_features.md#siri-shortcuts-integration)
### 2. Background Audio Mode
Configure background audio to maintain tracking during sleep. Simply add `audio` to `UIBackgroundModes` in Info.plist - iOS handles the rest automatically.
For details, see [references/ios_specific_features.md](references/ios_specific_features.md#background-audio-mode)
### 3. Microphone Permission
Request microphone permission before starting tracking:
```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
}
}
```
For complete permission handling, see [references/ios_specific_features.md](references/ios_specific_features.md#microphone-permission-handling)
### 4. App Lifecycle Management
Handle app state transitions using SwiftUI's `scenePhase`:
```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")
case .inactive: print("App is inactive")
case .background: print("App in background - tracking continues")
@unknown default: break
}
}
}
}
```
For advanced lifecycle patterns, see [references/ios_specific_features.md](references/ios_specific_features.md#app-lifecycle-management)
### 5. Persistent Storage
Store configuration using AppStorage:
```swift
struct SleepTrackingView: View {
@AppStorage("sleepapp+apikey") private var apiKey = ""
@AppStorage("sleepapp+userid") private var userId = ""
// Values automatically persist across app launches
}
```
## Error Handling
### Common Error Patterns
```swift
func handleError(_ error: Asleep.AsleepError) {
switch error {
case .micPermission:
// Guide user to Settings
showMicPermissionAlert()
case .audioSessionError:
// Another app is using microphone
showAudioUnavailableAlert()
case let .httpStatus(code, _, message):
switch code {
case 403: // Session already active on another device
case 404: // Session not found
default: break
}
default:
showGenericError(error.localizedDescription)
}
}
```
### Retry with Exponential Backoff
```swift
func startTrackingWithRetry() {
trackingManager?.startTracking()
}
func didFail(error: Asleep.AsleepError) {
if isTransientError(error) && retryCount < maxRetries {
retryCount += 1
DispatchQueue.main.asyncAfter(deadline: .now() + pow(2.0, Double(retryCount))) {
self.startTrackingWithRetry()
}
} else {
handleError(error)
}
}
```
For comprehensive error handling patterns, see [references/advanced_patterns.md](references/advanced_patterns.md#error-recovery-patterns)
## Best Practices
### 1. State Management
Use `@Published` properties for reactive UI updates:
```swift
final class SleepTrackingViewModel: ObservableObject {
@Published var isTracking = false
@Published var error: String?
// UI automatically updates when values change
}
```
### 2. Main Thread Safety
Always update UI on main thread:
```swift
func didCreate() {
Task { @MainActor in // Ensures main thread
self.isTracking = true
}
}
```
### 3. Resource Cleanup
```swift
final class SleepTrackingViewModel: ObservableObject {
deinit {
trackingManager = nil
reports = nil
}
}
```
### 4. User Experience
Provide clear visual feedback with loading states, progress indicators, and error messages. Disable controls appropriately during tracking.
### 5. Testing Considerations
Use dependency injection for testable code:
```swift
protocol SleepTrackingManagerProtocol {
func startTracking()
func stopTracking()
}
// Production and mock implementations
```
For complete testing patterns, see [references/advanced_patterns.md](references/advanced_patterns.md#testing-patterns)
## Common Integration Patterns
### Pattern 1: Simple Single-View App
Best for basic sleep tracking with minimal features. Single view with tracking controls.
### Pattern 2: Multi-View App with Navigation
Best for apps with reports, settings, and history. Uses TabView for navigation between Track, History, and Settings.
### Pattern 3: Centralized SDK Manager
Best for complex apps sharing SDK instance across views. Single source of truth with `AsleepSDKManager.shared`.
For complete implementation of all patterns, see [references/advanced_patterns.md](references/advanced_patterns.md)
## Real-time Data Access
Access preliminary sleep data during tracking (available after sequence 10):
```swift
func analysing(session: Asleep.Model.Session) {
Task { @MainActor in
if let sleepStages = session.sleepStages {
updateRealtimeChart(stages: sleepStages)
}
}
}
func didUpload(sequence: Int) {
// Real-time data available every 10 sequences after sequence 10
if sequence >= 10 && sequence % 10 == 0 {
// SDK automatically calls analysing() delegate
}
}
```
## Fetching Reports
Retrieve sleep session data after tracking:
```swift
func fetchReport(sessionId: String) async {
do {
let report = try await reports?.report(sessionId: sessionId)
// Process report data
} catch {
// Handle error
}
}
// Fetch multiple sessions
func fetchReportList() async {
let reportList = try await reports?.reports(
fromDate: "2024-01-01",
toDate: "2024-01-31"
)
}
```
## Troubleshooting
### Tracking Doesn't Start
**Causes**: Missing microphone permission, empty API key/user ID, another app using microphone
**Solution**: Validate configuration and check microphone permission before starting
### Background Tracking Stops
**Causes**: Background audio mode not configured, memory pressure, force-closed app
**Solution**: Ensure `UIBackgroundModes` includes `audio` in Info.plist
### Reports Not Available
**Causes**: Session processing incomplete (takes time), minimum duration not met (5 minutes), network issues
**Solution**: Implement retry logic with exponential backoff when fetching reports
For detailed troubleshooting, see the complete implementation examples in references/
## Sample Code Reference
This skill is based on the official Asleep iOS sample app:
- **MainViewModel.swift**: Complete ViewModel with all delegates
- **MainView.swift**: SwiftUI view with tracking controls
- **StartSleepIntent.swift / StopSleepIntent.swift**: Siri Shortcuts
- **ReportView.swift**: Sleep report display
- **Info.plist**: Required iOS permissions
Sample app: [Asleep iOS Sample App](https://github.com/asleep-ai/asleep-sdk-ios-sampleapp-public)
## Resources
### Official Documentation
- **iOS Get Started**: https://docs-en.asleep.ai/docs/ios-get-started.md
- **iOS Error Codes**: https://docs-en.asleep.ai/docs/ios-error-codes.md
- **AsleepConfig Reference**: https://docs-en.asleep.ai/docs/ios-asleep-config.md
- **SleepTrackingManager Reference**: https://docs-en.asleep.ai/docs/ios-sleep-tracking-manager.md
- **Sample App Guide**: https://docs-en.asleep.ai/docs/sample-app.md
### Apple Documentation
- **SwiftUI**: https://developer.apple.com/documentation/swiftui
- **Combine**: https://developer.apple.com/documentation/combine
- **AVAudioSession**: https://developer.apple.com/documentation/avfaudio/avaudiosession
- **App Intents**: https://developer.apple.com/documentation/appintents
- **Background Modes**: https://developer.apple.com/documentation/xcode/configuring-background-execution-modes
### Related Skills
- **sleeptrack-foundation**: Core Asleep concepts, authentication, and data structures
- **sleeptrack-android**: Android-specific implementation guide
- **sleeptrack-be**: Backend API integration
## Next Steps
After integrating the iOS SDK:
1. Test thoroughly across different iOS devices and versions
2. Implement proper error handling for all edge cases
3. Add user-friendly error messages and recovery flows
4. Consider HealthKit integration for data export
5. Implement notification reminders for tracking
6. Add data visualization for sleep trends
7. Consider Apple Watch companion app
8. Submit to App Store with proper privacy declarations

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