Initial commit
This commit is contained in:
526
skills/sleeptrack-ios/SKILL.md
Normal file
526
skills/sleeptrack-ios/SKILL.md
Normal 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
|
||||
460
skills/sleeptrack-ios/references/advanced_patterns.md
Normal file
460
skills/sleeptrack-ios/references/advanced_patterns.md
Normal 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)
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
284
skills/sleeptrack-ios/references/ios_specific_features.md
Normal file
284
skills/sleeptrack-ios/references/ios_specific_features.md
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user