Initial commit
This commit is contained in:
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