Files
gh-asleep-ai-sleeptrack-ski…/skills/sleeptrack-ios/references/advanced_patterns.md
2025-11-29 17:58:23 +08:00

11 KiB

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.

@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.

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

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.

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

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

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

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

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

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

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