From 82fe3a43f2c7d0eef5d9832448e5db71e81c535a Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:45:29 +0300 Subject: [PATCH 01/59] chore: remove sources, tests and docs that were never part of a build The Enterprise, Integration, Analytics, Widgets and LiveData folders, the demo app, examples, templates and the Documentation folder were not included by any target in Package.swift, and much of the code referenced types that do not exist. The Quick/Nimble tests in Tests/Unit belonged to the only test target, which did not compile because Quick and Nimble were never declared as dependencies; that test target is removed with them and a new one is added together with the first new tests. The podspec, Turkish README stub and support files described APIs that are not in the package. --- .github/stale.yml | 14 - ACKNOWLEDGMENTS.md | 25 - Assets/.gitkeep | 5 - Assets/social-preview.html | 20 - Benchmarks/README.md | 36 - .../Managers/WidgetKitManager.swift | 379 ----- Demo/WidgetKitDemo/WidgetKitDemoApp.swift | 743 --------- Documentation/API-Reference.md | 851 ---------- Documentation/API.md | 329 ---- Documentation/APIReference.md | 591 ------- Documentation/Architecture.md | 356 ----- Documentation/ArchitectureGuide.md | 469 ------ Documentation/Best-Practices.md | 1078 ------------- Documentation/ConfigurationAPI.md | 558 ------- Documentation/CustomizationAPI.md | 695 -------- Documentation/CustomizationGuide.md | 649 -------- Documentation/DataIntegrationAPI.md | 672 -------- Documentation/DataIntegrationGuide.md | 785 --------- Documentation/DynamicIslandAPI.md | 458 ------ Documentation/DynamicIslandGuide.md | 774 --------- Documentation/GettingStarted.md | 35 - Documentation/HomeScreenWidgetAPI.md | 439 ------ Documentation/HomeScreenWidgetGuide.md | 592 ------- Documentation/InstallationGuide.md | 122 -- Documentation/LiveActivityAPI.md | 446 ------ Documentation/LiveActivityGuide.md | 772 --------- Documentation/LockScreenWidgetAPI.md | 349 ----- Documentation/LockScreenWidgetGuide.md | 589 ------- Documentation/Performance.md | 536 ------- Documentation/PerformanceGuide.md | 358 ----- Documentation/README.md | 28 - Documentation/Security.md | 729 --------- Documentation/Testing.md | 644 -------- Documentation/Troubleshooting.md | 955 ----------- Documentation/UsageGuide.md | 258 --- Documentation/WidgetDevelopmentManagerAPI.md | 332 ---- Examples/AdvancedExample.swift | 45 - Examples/AdvancedExamples/AdvancedDemo.swift | 20 - .../AdvancedWidgetExample.swift | 365 ----- Examples/AdvancedExamples/README.md | 6 - Examples/BasicExample.swift | 87 - .../BasicExamples/BasicWidgetExample.swift | 211 --- .../DynamicIslandExample.swift | 195 --- .../HomeScreenWidgetExample.swift | 89 -- .../WeatherWidgetExample.swift | 357 ----- .../FitnessTrackerExample.swift | 528 ------- .../LiveActivityExample.swift | 159 -- .../CalendarWidgetExample.swift | 418 ----- .../LockScreenWidgetExample.swift | 81 - Examples/README.md | 13 - Package.swift | 5 - README_TR.md | 36 - SUPPORT.md | 25 - Sources/Analytics/WidgetAnalytics.swift | 515 ------ Sources/Analytics/WidgetErrorHandler.swift | 208 --- Sources/Enterprise/AIManager.swift | 1013 ------------ Sources/Enterprise/AdvancedAnalytics.swift | 1314 ---------------- Sources/Enterprise/MLAnalyticsEngine.swift | 1275 --------------- Sources/Integration/APIManager.swift | 1162 -------------- Sources/Integration/CloudIntegration.swift | 1396 ----------------- Sources/Integration/DataSourceManager.swift | 1281 --------------- Sources/LiveData/LiveDataIntegration.swift | 590 ------- Sources/Widgets/WidgetContentView.swift | 881 ----------- Sources/Widgets/WidgetTemplate.swift | 417 ----- Sources/Widgets/WidgetViews.swift | 506 ------ Templates/ServiceTemplate.swift | 126 -- Templates/TestTemplate.swift | 152 -- Templates/ViewModelTemplate.swift | 138 -- .../Integration/WidgetIntegrationTests.swift | 231 --- Tests/Integration/WidgetTemplatesTests.swift | 492 ------ Tests/LinuxMain.swift | 7 - .../LiveDataIntegrationTests.swift | 333 ---- .../Performance/WidgetPerformanceTests.swift | 270 ---- Tests/Unit/WidgetAnalyticsTests.swift | 314 ---- Tests/Unit/WidgetDataManagerTests.swift | 349 ----- Tests/Unit/WidgetEngineTests.swift | 190 --- Tests/UnitTests.swift | 44 - Tests/iOS-Widget-Development-KitTests.swift | 45 - WidgetDevelopmentKit.podspec | 55 - 79 files changed, 32615 deletions(-) delete mode 100644 .github/stale.yml delete mode 100644 ACKNOWLEDGMENTS.md delete mode 100644 Assets/.gitkeep delete mode 100644 Assets/social-preview.html delete mode 100644 Benchmarks/README.md delete mode 100644 Demo/WidgetKitDemo/Managers/WidgetKitManager.swift delete mode 100644 Demo/WidgetKitDemo/WidgetKitDemoApp.swift delete mode 100644 Documentation/API-Reference.md delete mode 100644 Documentation/API.md delete mode 100644 Documentation/APIReference.md delete mode 100644 Documentation/Architecture.md delete mode 100644 Documentation/ArchitectureGuide.md delete mode 100644 Documentation/Best-Practices.md delete mode 100644 Documentation/ConfigurationAPI.md delete mode 100644 Documentation/CustomizationAPI.md delete mode 100644 Documentation/CustomizationGuide.md delete mode 100644 Documentation/DataIntegrationAPI.md delete mode 100644 Documentation/DataIntegrationGuide.md delete mode 100644 Documentation/DynamicIslandAPI.md delete mode 100644 Documentation/DynamicIslandGuide.md delete mode 100644 Documentation/GettingStarted.md delete mode 100644 Documentation/HomeScreenWidgetAPI.md delete mode 100644 Documentation/HomeScreenWidgetGuide.md delete mode 100644 Documentation/InstallationGuide.md delete mode 100644 Documentation/LiveActivityAPI.md delete mode 100644 Documentation/LiveActivityGuide.md delete mode 100644 Documentation/LockScreenWidgetAPI.md delete mode 100644 Documentation/LockScreenWidgetGuide.md delete mode 100644 Documentation/Performance.md delete mode 100644 Documentation/PerformanceGuide.md delete mode 100644 Documentation/README.md delete mode 100644 Documentation/Security.md delete mode 100644 Documentation/Testing.md delete mode 100644 Documentation/Troubleshooting.md delete mode 100644 Documentation/UsageGuide.md delete mode 100644 Documentation/WidgetDevelopmentManagerAPI.md delete mode 100644 Examples/AdvancedExample.swift delete mode 100644 Examples/AdvancedExamples/AdvancedDemo.swift delete mode 100644 Examples/AdvancedExamples/AdvancedWidgetExample.swift delete mode 100644 Examples/AdvancedExamples/README.md delete mode 100644 Examples/BasicExample.swift delete mode 100644 Examples/BasicExamples/BasicWidgetExample.swift delete mode 100644 Examples/DynamicIslandExamples/DynamicIslandExample.swift delete mode 100644 Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift delete mode 100644 Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift delete mode 100644 Examples/LiveActivityExamples/FitnessTrackerExample.swift delete mode 100644 Examples/LiveActivityExamples/LiveActivityExample.swift delete mode 100644 Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift delete mode 100644 Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift delete mode 100644 Examples/README.md delete mode 100644 README_TR.md delete mode 100644 SUPPORT.md delete mode 100644 Sources/Analytics/WidgetAnalytics.swift delete mode 100644 Sources/Analytics/WidgetErrorHandler.swift delete mode 100644 Sources/Enterprise/AIManager.swift delete mode 100644 Sources/Enterprise/AdvancedAnalytics.swift delete mode 100644 Sources/Enterprise/MLAnalyticsEngine.swift delete mode 100644 Sources/Integration/APIManager.swift delete mode 100644 Sources/Integration/CloudIntegration.swift delete mode 100644 Sources/Integration/DataSourceManager.swift delete mode 100644 Sources/LiveData/LiveDataIntegration.swift delete mode 100644 Sources/Widgets/WidgetContentView.swift delete mode 100644 Sources/Widgets/WidgetTemplate.swift delete mode 100644 Sources/Widgets/WidgetViews.swift delete mode 100644 Templates/ServiceTemplate.swift delete mode 100644 Templates/TestTemplate.swift delete mode 100644 Templates/ViewModelTemplate.swift delete mode 100644 Tests/Integration/WidgetIntegrationTests.swift delete mode 100644 Tests/Integration/WidgetTemplatesTests.swift delete mode 100644 Tests/LinuxMain.swift delete mode 100644 Tests/Performance/LiveDataIntegrationTests.swift delete mode 100644 Tests/Performance/WidgetPerformanceTests.swift delete mode 100644 Tests/Unit/WidgetAnalyticsTests.swift delete mode 100644 Tests/Unit/WidgetDataManagerTests.swift delete mode 100644 Tests/Unit/WidgetEngineTests.swift delete mode 100644 Tests/UnitTests.swift delete mode 100644 Tests/iOS-Widget-Development-KitTests.swift delete mode 100644 WidgetDevelopmentKit.podspec diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 50305d1..0000000 --- a/.github/stale.yml +++ /dev/null @@ -1,14 +0,0 @@ -daysUntilStale: 60 -daysUntilClose: 7 -staleLabel: stale -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. -closeComment: > - This issue has been automatically closed due to inactivity. - Feel free to reopen if you still experience this problem. -exemptLabels: - - pinned - - security - - "priority: critical" -only: issues diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md deleted file mode 100644 index a8300b9..0000000 --- a/ACKNOWLEDGMENTS.md +++ /dev/null @@ -1,25 +0,0 @@ -# Acknowledgments - -## Contributors - -Thanks to everyone who has contributed to this project! - - - - - -## Built With - -- [Swift](https://swift.org) - The Swift programming language -- [SwiftUI](https://developer.apple.com/swiftui/) - Declarative UI framework -- [Swift Package Manager](https://swift.org/package-manager/) - Dependency management - -## Inspiration - -- [Alamofire](https://github.com/Alamofire/Alamofire) - Elegant HTTP networking -- [Kingfisher](https://github.com/onevcat/Kingfisher) - Image downloading and caching -- [SnapKit](https://github.com/SnapKit/SnapKit) - Auto Layout DSL - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/Assets/.gitkeep b/Assets/.gitkeep deleted file mode 100644 index 8db8e31..0000000 --- a/Assets/.gitkeep +++ /dev/null @@ -1,5 +0,0 @@ -# Widget Assets - -Add these files: -- banner.png (800x400) -- widgets-preview.png (widget screenshots) diff --git a/Assets/social-preview.html b/Assets/social-preview.html deleted file mode 100644 index 09ecba5..0000000 --- a/Assets/social-preview.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - -

iOS-Widget-Development-Kit

-

📲 WidgetKit development kit with Live Activities and dynamic widgets

-
- Swift 5.9+ - iOS 15+ - SPM - MIT -
- - diff --git a/Benchmarks/README.md b/Benchmarks/README.md deleted file mode 100644 index 0ab2879..0000000 --- a/Benchmarks/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Performance Benchmarks - -> Last updated: 2026-02-03 -> Environment: MacBook Pro M3, Xcode 15.4, Swift 5.9 - -## Methodology - -All benchmarks run on a clean build with release optimizations enabled. -Each test executes 1000 iterations and reports the median value. - -## Results - -| Operation | Time (ms) | Memory (MB) | Allocations | -|-----------|-----------|-------------|-------------| -| Initialize | 0.02 | 0.5 | 12 | -| Process (small) | 0.15 | 1.2 | 45 | -| Process (medium) | 1.8 | 4.5 | 230 | -| Process (large) | 12.3 | 18.0 | 1,200 | - -## Comparison - -| Framework | Init (ms) | Process (ms) | Memory (MB) | -|-----------|-----------|--------------|-------------| -| **This Library** | **0.02** | **1.8** | **4.5** | -| Competitor A | 0.05 | 3.2 | 8.1 | -| Competitor B | 0.08 | 2.9 | 6.3 | - -## How to Run - -```bash -swift run -c release Benchmarks --iterations 1000 -``` - ---- - -*Benchmarks are run on every release to ensure no performance regressions.* diff --git a/Demo/WidgetKitDemo/Managers/WidgetKitManager.swift b/Demo/WidgetKitDemo/Managers/WidgetKitManager.swift deleted file mode 100644 index a650a81..0000000 --- a/Demo/WidgetKitDemo/Managers/WidgetKitManager.swift +++ /dev/null @@ -1,379 +0,0 @@ -import Foundation -import SwiftUI -import WidgetKit -import Combine - -/// Manager class for handling all widget-related operations in the demo app -@available(iOS 16.0, *) -@MainActor -public class WidgetKitManager: ObservableObject { - - // MARK: - Published Properties - - @Published public var availableWidgets: [WidgetModel] = [] - @Published public var activeWidgets: [WidgetModel] = [] - @Published public var enableHomeScreen: Bool = true - @Published public var enableLockScreen: Bool = true - @Published public var enableLiveActivities: Bool = true - @Published public var enableDynamicIsland: Bool = true - @Published public var isConfigured: Bool = false - - // MARK: - Private Properties - - private var configuration = WidgetConfiguration() - private var cancellables = Set() - - // MARK: - Initialization - - public init() { - loadAvailableWidgets() - setupObservers() - } - - // MARK: - Public Methods - - /// Configures the widget manager with custom settings - public func configure(_ block: (inout WidgetConfiguration) -> Void) { - block(&configuration) - isConfigured = true - applyConfiguration() - } - - /// Creates a new widget with specified parameters - public func createWidget(type: WidgetType, size: WidgetSize, content: AnyView) async throws -> WidgetModel { - let widget = WidgetModel( - id: UUID().uuidString, - type: type, - size: size, - title: "New Widget", - subtitle: "Custom widget", - icon: "square.grid.2x2", - isActive: false, - lastUpdated: Date() - ) - - availableWidgets.append(widget) - - // Request widget timeline reload - WidgetCenter.shared.reloadAllTimelines() - - return widget - } - - /// Activates a widget - public func activateWidget(_ widget: WidgetModel) async throws { - guard let index = availableWidgets.firstIndex(where: { $0.id == widget.id }) else { - throw WidgetError.widgetNotFound - } - - availableWidgets[index].isActive = true - activeWidgets.append(availableWidgets[index]) - - // Update widget timeline - WidgetCenter.shared.reloadTimelines(ofKind: widget.kind) - } - - /// Deactivates a widget - public func deactivateWidget(_ widget: WidgetModel) async throws { - guard let availableIndex = availableWidgets.firstIndex(where: { $0.id == widget.id }) else { - throw WidgetError.widgetNotFound - } - - availableWidgets[availableIndex].isActive = false - activeWidgets.removeAll { $0.id == widget.id } - - // Update widget timeline - WidgetCenter.shared.reloadTimelines(ofKind: widget.kind) - } - - /// Updates widget content - public func updateWidget(_ widget: WidgetModel, with data: WidgetData) async throws { - guard let index = availableWidgets.firstIndex(where: { $0.id == widget.id }) else { - throw WidgetError.widgetNotFound - } - - availableWidgets[index].lastUpdated = Date() - - // Store widget data for timeline provider - UserDefaults.standard.set(try? JSONEncoder().encode(data), forKey: "widget_data_\(widget.id)") - - // Reload widget timeline - WidgetCenter.shared.reloadTimelines(ofKind: widget.kind) - } - - /// Refreshes all widgets - public func refreshAllWidgets() async { - WidgetCenter.shared.reloadAllTimelines() - - for index in availableWidgets.indices { - availableWidgets[index].lastUpdated = Date() - } - } - - /// Gets widget statistics - public func getWidgetStatistics() -> WidgetStatistics { - return WidgetStatistics( - totalWidgets: availableWidgets.count, - activeWidgets: activeWidgets.count, - homeScreenWidgets: availableWidgets.filter { $0.type == .homeScreen }.count, - lockScreenWidgets: availableWidgets.filter { $0.type == .lockScreen }.count, - liveActivities: availableWidgets.filter { $0.type == .liveActivity }.count - ) - } - - // MARK: - Private Methods - - private func loadAvailableWidgets() { - // Load sample widgets for demo - availableWidgets = [ - WidgetModel( - id: "weather", - type: .homeScreen, - size: .medium, - title: "Weather Widget", - subtitle: "Current weather conditions", - icon: "cloud.sun.fill", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "calendar", - type: .homeScreen, - size: .small, - title: "Calendar Widget", - subtitle: "Upcoming events", - icon: "calendar", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "fitness", - type: .lockScreen, - size: .small, - title: "Fitness Widget", - subtitle: "Activity rings", - icon: "figure.walk", - isActive: false, - lastUpdated: Date() - ), - WidgetModel( - id: "stocks", - type: .homeScreen, - size: .large, - title: "Stocks Widget", - subtitle: "Market overview", - icon: "chart.line.uptrend.xyaxis", - isActive: false, - lastUpdated: Date() - ), - WidgetModel( - id: "music", - type: .lockScreen, - size: .small, - title: "Music Widget", - subtitle: "Now playing", - icon: "music.note", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "news", - type: .homeScreen, - size: .medium, - title: "News Widget", - subtitle: "Top headlines", - icon: "newspaper", - isActive: false, - lastUpdated: Date() - ), - WidgetModel( - id: "battery", - type: .lockScreen, - size: .small, - title: "Battery Widget", - subtitle: "Device battery status", - icon: "battery.75", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "reminders", - type: .homeScreen, - size: .small, - title: "Reminders Widget", - subtitle: "Today's tasks", - icon: "checklist", - isActive: false, - lastUpdated: Date() - ) - ] - - // Filter active widgets - activeWidgets = availableWidgets.filter { $0.isActive } - } - - private func setupObservers() { - // Observe widget configuration changes - NotificationCenter.default.publisher(for: .widgetConfigurationChanged) - .sink { [weak self] _ in - Task { @MainActor in - await self?.refreshAllWidgets() - } - } - .store(in: &cancellables) - } - - private func applyConfiguration() { - enableHomeScreen = configuration.enableHomeScreenWidgets - enableLockScreen = configuration.enableLockScreenWidgets - enableLiveActivities = configuration.enableLiveActivities - enableDynamicIsland = configuration.enableDynamicIsland - } -} - -// MARK: - Supporting Types - -@available(iOS 16.0, *) -public struct WidgetModel: Identifiable, Codable { - public let id: String - public let type: WidgetType - public let size: WidgetSize - public let title: String - public let subtitle: String - public let icon: String - public var isActive: Bool - public var lastUpdated: Date - - public var kind: String { - return "com.widgetkit.demo.\(id)" - } -} - -public enum WidgetType: String, Codable, CaseIterable { - case homeScreen = "home_screen" - case lockScreen = "lock_screen" - case standBy = "stand_by" - case liveActivity = "live_activity" - case dynamicIsland = "dynamic_island" - - public var displayName: String { - switch self { - case .homeScreen: return "Home Screen" - case .lockScreen: return "Lock Screen" - case .standBy: return "StandBy" - case .liveActivity: return "Live Activity" - case .dynamicIsland: return "Dynamic Island" - } - } -} - -public enum WidgetSize: String, Codable, CaseIterable { - case small = "small" - case medium = "medium" - case large = "large" - case extraLarge = "extra_large" - - public var displayName: String { - switch self { - case .small: return "Small" - case .medium: return "Medium" - case .large: return "Large" - case .extraLarge: return "Extra Large" - } - } - - public var dimensions: CGSize { - switch self { - case .small: return CGSize(width: 155, height: 155) - case .medium: return CGSize(width: 329, height: 155) - case .large: return CGSize(width: 329, height: 345) - case .extraLarge: return CGSize(width: 329, height: 382) - } - } -} - -public struct WidgetConfiguration { - public var enableHomeScreenWidgets: Bool = true - public var enableLockScreenWidgets: Bool = true - public var enableLiveActivities: Bool = true - public var enableDynamicIsland: Bool = true - public var refreshInterval: TimeInterval = 300 // 5 minutes - public var enableBackgroundRefresh: Bool = true - public var enableInteractions: Bool = true - public var enableDeepLinking: Bool = true -} - -public struct WidgetData: Codable { - public let content: [String: Any] - public let timestamp: Date - - enum CodingKeys: String, CodingKey { - case content - case timestamp - } - - public init(content: [String: Any], timestamp: Date = Date()) { - self.content = content - self.timestamp = timestamp - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - timestamp = try container.decode(Date.self, forKey: .timestamp) - - // Decode content as Data and convert to dictionary - if let data = try? container.decode(Data.self, forKey: .content), - let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - content = dict - } else { - content = [:] - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(timestamp, forKey: .timestamp) - - // Encode content dictionary as Data - if let data = try? JSONSerialization.data(withJSONObject: content) { - try container.encode(data, forKey: .content) - } - } -} - -public struct WidgetStatistics { - public let totalWidgets: Int - public let activeWidgets: Int - public let homeScreenWidgets: Int - public let lockScreenWidgets: Int - public let liveActivities: Int -} - -public enum WidgetError: LocalizedError { - case widgetNotFound - case invalidConfiguration - case activationFailed - case updateFailed - - public var errorDescription: String? { - switch self { - case .widgetNotFound: - return "Widget not found" - case .invalidConfiguration: - return "Invalid widget configuration" - case .activationFailed: - return "Failed to activate widget" - case .updateFailed: - return "Failed to update widget" - } - } -} - -// MARK: - Notifications - -extension Notification.Name { - static let widgetConfigurationChanged = Notification.Name("widgetConfigurationChanged") - static let widgetActivated = Notification.Name("widgetActivated") - static let widgetDeactivated = Notification.Name("widgetDeactivated") - static let widgetUpdated = Notification.Name("widgetUpdated") -} \ No newline at end of file diff --git a/Demo/WidgetKitDemo/WidgetKitDemoApp.swift b/Demo/WidgetKitDemo/WidgetKitDemoApp.swift deleted file mode 100644 index 191e025..0000000 --- a/Demo/WidgetKitDemo/WidgetKitDemoApp.swift +++ /dev/null @@ -1,743 +0,0 @@ -import SwiftUI -import WidgetKit - -/// Main demo application showcasing all iOS Widget Development Kit features -@main -@available(iOS 16.0, *) -struct WidgetKitDemoApp: App { - @StateObject private var widgetManager = WidgetKitManager() - @StateObject private var themeManager = ThemeManager() - @StateObject private var dataManager = DataManager() - @StateObject private var performanceMonitor = PerformanceMonitor.shared - - init() { - setupApplication() - } - - var body: some Scene { - WindowGroup { - ContentView() - .environmentObject(widgetManager) - .environmentObject(themeManager) - .environmentObject(dataManager) - .environmentObject(performanceMonitor) - .onAppear { - Task { - await initializeFramework() - } - } - } - } - - private func setupApplication() { - // Configure appearance - UINavigationBar.appearance().largeTitleTextAttributes = [ - .foregroundColor: UIColor.label, - .font: UIFont.systemFont(ofSize: 34, weight: .bold) - ] - - // Configure tab bar - UITabBar.appearance().backgroundColor = UIColor.systemBackground - UITabBar.appearance().tintColor = UIColor.systemBlue - } - - private func initializeFramework() async { - // Initialize security - let encryptionManager = EncryptionManager() - let authManager = AuthenticationManager() - let privacyManager = PrivacyManager(encryptionManager: encryptionManager) - - // Initialize performance monitoring - do { - try await performanceMonitor.startMonitoring() - } catch { - print("Failed to start performance monitoring: \(error)") - } - - // Configure widget manager - widgetManager.configure { config in - config.enableHomeScreenWidgets = true - config.enableLockScreenWidgets = true - config.enableLiveActivities = true - config.enableDynamicIsland = true - config.refreshInterval = 300 // 5 minutes - } - - // Setup data sources - await dataManager.setupDataSources() - - // Request necessary permissions - await requestPermissions() - - // Register for remote notifications - await registerForNotifications() - } - - private func requestPermissions() async { - // Request privacy permissions - let privacyManager = PrivacyManager(encryptionManager: EncryptionManager()) - - let purposes: [PrivacyManager.DataPurpose] = [ - .widgetFunctionality, - .performanceAnalytics, - .usageAnalytics, - .personalization - ] - - do { - _ = try await privacyManager.requestConsent(for: purposes) - } catch { - print("Consent request failed: \(error)") - } - } - - private func registerForNotifications() async { - let center = UNUserNotificationCenter.current() - - do { - let granted = try await center.requestAuthorization(options: [.alert, .badge, .sound]) - if granted { - await UIApplication.shared.registerForRemoteNotifications() - } - } catch { - print("Notification authorization failed: \(error)") - } - } -} - -// MARK: - Content View - -@available(iOS 16.0, *) -struct ContentView: View { - @State private var selectedTab = 0 - @EnvironmentObject var widgetManager: WidgetKitManager - @EnvironmentObject var themeManager: ThemeManager - - var body: some View { - TabView(selection: $selectedTab) { - HomeView() - .tabItem { - Label("Home", systemImage: "house.fill") - } - .tag(0) - - WidgetsView() - .tabItem { - Label("Widgets", systemImage: "square.grid.2x2.fill") - } - .tag(1) - - LiveActivitiesView() - .tabItem { - Label("Live", systemImage: "bolt.circle.fill") - } - .tag(2) - - AnalyticsView() - .tabItem { - Label("Analytics", systemImage: "chart.line.uptrend.xyaxis") - } - .tag(3) - - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } - .tag(4) - } - .accentColor(themeManager.currentTheme.accentColor) - } -} - -// MARK: - Home View - -@available(iOS 16.0, *) -struct HomeView: View { - @EnvironmentObject var widgetManager: WidgetKitManager - @State private var showingWelcome = true - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 20) { - // Welcome Card - WelcomeCard() - .padding(.horizontal) - - // Quick Actions - QuickActionsSection() - .padding(.horizontal) - - // Featured Widgets - FeaturedWidgetsSection() - .padding(.horizontal) - - // Recent Activity - RecentActivitySection() - .padding(.horizontal) - - // Performance Overview - PerformanceOverviewCard() - .padding(.horizontal) - } - .padding(.vertical) - } - .navigationTitle("Widget Kit Demo") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - // Show notifications - }) { - Image(systemName: "bell.badge") - } - } - } - } - } -} - -// MARK: - Widgets View - -@available(iOS 16.0, *) -struct WidgetsView: View { - @EnvironmentObject var widgetManager: WidgetKitManager - @State private var selectedCategory = "All" - - let categories = ["All", "Home Screen", "Lock Screen", "StandBy", "Custom"] - - var body: some View { - NavigationView { - VStack { - // Category Picker - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(categories, id: \.self) { category in - CategoryChip( - title: category, - isSelected: selectedCategory == category - ) { - selectedCategory = category - } - } - } - .padding(.horizontal) - } - .padding(.vertical) - - // Widget Grid - ScrollView { - LazyVGrid(columns: [ - GridItem(.flexible()), - GridItem(.flexible()) - ], spacing: 16) { - ForEach(widgetManager.availableWidgets) { widget in - WidgetCard(widget: widget) - } - } - .padding() - } - } - .navigationTitle("Widgets") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Add") { - // Show widget creation - } - } - } - } - } -} - -// MARK: - Live Activities View - -@available(iOS 16.0, *) -struct LiveActivitiesView: View { - @State private var activeLiveActivities: [LiveActivityItem] = [] - @State private var showingCreateActivity = false - - var body: some View { - NavigationView { - VStack { - if activeLiveActivities.isEmpty { - EmptyStateView( - icon: "bolt.circle", - title: "No Active Live Activities", - description: "Start a Live Activity to see real-time updates here" - ) { - showingCreateActivity = true - } - } else { - ScrollView { - VStack(spacing: 16) { - ForEach(activeLiveActivities) { activity in - LiveActivityCard(activity: activity) - .padding(.horizontal) - } - } - .padding(.vertical) - } - } - } - .navigationTitle("Live Activities") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - showingCreateActivity = true - }) { - Image(systemName: "plus.circle.fill") - } - } - } - .sheet(isPresented: $showingCreateActivity) { - CreateLiveActivityView() - } - } - } -} - -// MARK: - Analytics View - -@available(iOS 16.0, *) -struct AnalyticsView: View { - @EnvironmentObject var performanceMonitor: PerformanceMonitor - @State private var selectedTimeRange = "Today" - @State private var selectedMetric = "All" - - let timeRanges = ["Today", "Week", "Month", "Year"] - let metrics = ["All", "CPU", "Memory", "Battery", "Network"] - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 20) { - // Time Range Selector - Picker("Time Range", selection: $selectedTimeRange) { - ForEach(timeRanges, id: \.self) { range in - Text(range).tag(range) - } - } - .pickerStyle(SegmentedPickerStyle()) - .padding(.horizontal) - - // Performance Score Card - PerformanceScoreCard(score: performanceMonitor.performanceScore) - .padding(.horizontal) - - // Metrics Charts - MetricsChartsSection(selectedMetric: selectedMetric) - .padding(.horizontal) - - // Detailed Metrics - DetailedMetricsSection() - .padding(.horizontal) - - // Recommendations - RecommendationsSection() - .padding(.horizontal) - } - .padding(.vertical) - } - .navigationTitle("Analytics") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Export") { - // Export analytics data - } - } - } - } - } -} - -// MARK: - Settings View - -@available(iOS 16.0, *) -struct SettingsView: View { - @EnvironmentObject var themeManager: ThemeManager - @EnvironmentObject var widgetManager: WidgetKitManager - @State private var enableNotifications = true - @State private var enableAnalytics = true - @State private var enableAutoUpdate = true - @State private var selectedTheme = "System" - - var body: some View { - NavigationView { - Form { - // Appearance Section - Section("Appearance") { - Picker("Theme", selection: $selectedTheme) { - Text("System").tag("System") - Text("Light").tag("Light") - Text("Dark").tag("Dark") - } - - ColorPicker("Accent Color", selection: $themeManager.accentColor) - - Toggle("Dynamic Colors", isOn: $themeManager.enableDynamicColors) - } - - // Widget Settings - Section("Widget Settings") { - Toggle("Home Screen Widgets", isOn: $widgetManager.enableHomeScreen) - Toggle("Lock Screen Widgets", isOn: $widgetManager.enableLockScreen) - Toggle("Live Activities", isOn: $widgetManager.enableLiveActivities) - Toggle("Dynamic Island", isOn: $widgetManager.enableDynamicIsland) - - HStack { - Text("Refresh Interval") - Spacer() - Text("5 min") - .foregroundColor(.secondary) - } - } - - // Privacy & Security - Section("Privacy & Security") { - NavigationLink("Privacy Settings") { - PrivacySettingsView() - } - - NavigationLink("Security Settings") { - SecuritySettingsView() - } - - Toggle("Analytics", isOn: $enableAnalytics) - } - - // Notifications - Section("Notifications") { - Toggle("Enable Notifications", isOn: $enableNotifications) - - NavigationLink("Notification Preferences") { - NotificationPreferencesView() - } - } - - // Data Management - Section("Data Management") { - NavigationLink("Cache Management") { - CacheManagementView() - } - - Toggle("Auto Update", isOn: $enableAutoUpdate) - - Button("Clear All Data") { - // Clear data action - } - .foregroundColor(.red) - } - - // About - Section("About") { - HStack { - Text("Version") - Spacer() - Text("1.0.0") - .foregroundColor(.secondary) - } - - NavigationLink("Documentation") { - DocumentationView() - } - - NavigationLink("Support") { - SupportView() - } - - NavigationLink("License") { - LicenseView() - } - } - } - .navigationTitle("Settings") - } - } -} - -// MARK: - Supporting Views - -@available(iOS 16.0, *) -struct WelcomeCard: View { - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Welcome to") - .font(.headline) - .foregroundColor(.secondary) - Text("Widget Kit Demo") - .font(.largeTitle) - .fontWeight(.bold) - } - Spacer() - Image(systemName: "sparkles") - .font(.system(size: 40)) - .foregroundColor(.blue) - } - - Text("Explore the most advanced widget development framework for iOS") - .font(.body) - .foregroundColor(.secondary) - - HStack(spacing: 20) { - StatItem(value: "50+", label: "Widgets") - StatItem(value: "15K+", label: "Lines") - StatItem(value: "95%", label: "Coverage") - } - } - .padding() - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(16) - } -} - -@available(iOS 16.0, *) -struct QuickActionsSection: View { - let actions = [ - QuickAction(icon: "plus.app", title: "Create Widget", color: .blue), - QuickAction(icon: "bolt.fill", title: "Start Live Activity", color: .orange), - QuickAction(icon: "chart.line.uptrend.xyaxis", title: "View Analytics", color: .green), - QuickAction(icon: "gear", title: "Settings", color: .gray) - ] - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("Quick Actions") - .font(.headline) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(actions) { action in - QuickActionButton(action: action) - } - } - } - } - } -} - -@available(iOS 16.0, *) -struct FeaturedWidgetsSection: View { - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Featured Widgets") - .font(.headline) - Spacer() - Button("See All") { - // Navigate to widgets - } - .font(.caption) - } - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { - ForEach(0..<5) { index in - FeaturedWidgetCard(index: index) - } - } - } - } - } -} - -@available(iOS 16.0, *) -struct RecentActivitySection: View { - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("Recent Activity") - .font(.headline) - - VStack(spacing: 8) { - ActivityRow(icon: "square.grid.2x2", title: "Weather Widget Updated", time: "2 min ago") - ActivityRow(icon: "bolt.circle", title: "Delivery Tracking Started", time: "15 min ago") - ActivityRow(icon: "chart.bar", title: "Analytics Report Generated", time: "1 hour ago") - } - .padding() - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(12) - } - } -} - -// MARK: - Helper Views - -@available(iOS 16.0, *) -struct StatItem: View { - let value: String - let label: String - - var body: some View { - VStack(spacing: 4) { - Text(value) - .font(.title2) - .fontWeight(.bold) - Text(label) - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -struct QuickAction: Identifiable { - let id = UUID() - let icon: String - let title: String - let color: Color -} - -@available(iOS 16.0, *) -struct QuickActionButton: View { - let action: QuickAction - - var body: some View { - VStack(spacing: 8) { - Image(systemName: action.icon) - .font(.system(size: 24)) - .foregroundColor(.white) - .frame(width: 50, height: 50) - .background(action.color) - .cornerRadius(12) - - Text(action.title) - .font(.caption) - .multilineTextAlignment(.center) - .frame(width: 70) - } - } -} - -@available(iOS 16.0, *) -struct FeaturedWidgetCard: View { - let index: Int - - var body: some View { - VStack { - RoundedRectangle(cornerRadius: 12) - .fill(Color.blue.opacity(0.2)) - .frame(width: 150, height: 150) - .overlay( - Image(systemName: "square.grid.2x2") - .font(.system(size: 40)) - .foregroundColor(.blue) - ) - - Text("Widget \(index + 1)") - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -struct ActivityRow: View { - let icon: String - let title: String - let time: String - - var body: some View { - HStack { - Image(systemName: icon) - .foregroundColor(.blue) - .frame(width: 30) - - Text(title) - .font(.subheadline) - - Spacer() - - Text(time) - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Supporting Types - -@available(iOS 16.0, *) -struct LiveActivityItem: Identifiable { - let id = UUID() - let title: String - let status: String - let progress: Double -} - -@available(iOS 16.0, *) -struct CategoryChip: View { - let title: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - Text(title) - .font(.subheadline) - .fontWeight(isSelected ? .semibold : .regular) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(isSelected ? Color.blue : Color(UIColor.secondarySystemBackground)) - .foregroundColor(isSelected ? .white : .primary) - .cornerRadius(20) - } - } -} - -// Additional placeholder views for navigation destinations -@available(iOS 16.0, *) -struct CreateLiveActivityView: View { - var body: some View { - Text("Create Live Activity") - } -} - -@available(iOS 16.0, *) -struct PrivacySettingsView: View { - var body: some View { - Text("Privacy Settings") - } -} - -@available(iOS 16.0, *) -struct SecuritySettingsView: View { - var body: some View { - Text("Security Settings") - } -} - -@available(iOS 16.0, *) -struct NotificationPreferencesView: View { - var body: some View { - Text("Notification Preferences") - } -} - -@available(iOS 16.0, *) -struct CacheManagementView: View { - var body: some View { - Text("Cache Management") - } -} - -@available(iOS 16.0, *) -struct DocumentationView: View { - var body: some View { - Text("Documentation") - } -} - -@available(iOS 16.0, *) -struct SupportView: View { - var body: some View { - Text("Support") - } -} - -@available(iOS 16.0, *) -struct LicenseView: View { - var body: some View { - Text("License") - } -} \ No newline at end of file diff --git a/Documentation/API-Reference.md b/Documentation/API-Reference.md deleted file mode 100644 index 167ca79..0000000 --- a/Documentation/API-Reference.md +++ /dev/null @@ -1,851 +0,0 @@ -# Comprehensive API Reference — iOS Widget Development Kit - -## Overview - -This comprehensive API reference covers all classes, methods, properties, and protocols available in the iOS Widget Development Kit. The framework provides enterprise-grade functionality for creating, managing, and optimizing iOS widgets with advanced features including AI/ML integration, security, performance monitoring, and multi-cloud support. - -## Architecture Overview - -The iOS Widget Development Kit is organized into distinct layers: - -- **Core Layer**: Fundamental widget management and development tools -- **Security Layer**: Authentication, encryption, and privacy management -- **Performance Layer**: Monitoring, optimization, and caching systems -- **Enterprise Layer**: AI/ML, analytics, and business intelligence -- **Integration Layer**: APIs, cloud services, and data source management - -## Core API - -### WidgetDevelopmentManager - -The primary entry point for all widget development operations. - -```swift -@available(iOS 16.0, *) -public class WidgetDevelopmentManager: ObservableObject -``` - -#### Properties - -```swift -public static let shared: WidgetDevelopmentManager -public private(set) var isInitialized: Bool -public private(set) var activeWidgets: [Widget] -public private(set) var widgetTemplates: [WidgetTemplate] -public private(set) var configuration: WidgetConfiguration -``` - -#### Methods - -##### Initialization - -```swift -public func initialize(configuration: WidgetConfiguration) async throws -``` -Initializes the widget development manager with the specified configuration. - -**Parameters:** -- `configuration`: The configuration object containing setup parameters - -**Throws:** `WidgetError.configurationInvalid` if configuration is invalid - -##### Widget Creation - -```swift -public func createWidget( - with configuration: WidgetConfiguration, - template: WidgetTemplate? = nil -) async throws -> Widget -``` -Creates a new widget with the specified configuration and optional template. - -**Parameters:** -- `configuration`: Widget configuration specifying type, size, and behavior -- `template`: Optional template to use as a starting point - -**Returns:** The created `Widget` instance - -**Throws:** -- `WidgetError.invalidConfiguration` -- `WidgetError.templateNotFound` -- `WidgetError.creationFailed` - -##### Widget Management - -```swift -public func updateWidget( - id: String, - data: WidgetData, - options: UpdateOptions = UpdateOptions() -) async throws - -public func deleteWidget(id: String) async throws - -public func getWidget(id: String) -> Widget? - -public func getAllWidgets() -> [Widget] -``` - -##### Template Management - -```swift -public func registerTemplate(_ template: WidgetTemplate) async throws - -public func getAvailableTemplates() -> [WidgetTemplate] - -public func getTemplate(named name: String) -> WidgetTemplate? -``` - -### Widget - -The core widget class representing individual widget instances. - -```swift -@available(iOS 16.0, *) -public class Widget: ObservableObject, Identifiable -``` - -#### Properties - -```swift -public let id: String -public let type: WidgetType -public let size: WidgetSize -public private(set) var configuration: WidgetConfiguration -public private(set) var data: WidgetData? -public private(set) var state: WidgetState -public private(set) var lastUpdated: Date -public private(set) var metrics: WidgetMetrics -``` - -#### Methods - -```swift -public func loadData() async throws -> WidgetData -public func refreshData() async throws -public func render() async throws -public func updateConfiguration(_ newConfiguration: WidgetConfiguration) async throws -public func getMetrics() async -> WidgetMetrics -``` - -### WidgetConfiguration - -Configuration object for widget creation and customization. - -```swift -public struct WidgetConfiguration: Codable, Equatable -``` - -#### Properties - -```swift -public var type: WidgetType -public var size: WidgetSize -public var updateInterval: TimeInterval -public var displayName: String -public var backgroundColor: UIColor -public var textColor: UIColor -public var enableAnimations: Bool -public var enableInteractions: Bool -public var dataSource: DataSourceConfiguration? -public var customProperties: [String: Any] -``` - -#### Enumerations - -##### WidgetType - -```swift -public enum WidgetType: String, CaseIterable, Codable { - case weather = "weather" - case calendar = "calendar" - case news = "news" - case fitness = "fitness" - case social = "social" - case productivity = "productivity" - case entertainment = "entertainment" - case custom = "custom" -} -``` - -##### WidgetSize - -```swift -public enum WidgetSize: String, CaseIterable, Codable { - case small = "small" - case medium = "medium" - case large = "large" - case extraLarge = "extra_large" - case accessoryCircular = "accessory_circular" - case accessoryRectangular = "accessory_rectangular" - case accessoryInline = "accessory_inline" -} -``` - -##### WidgetState - -```swift -public enum WidgetState: String, Codable { - case inactive = "inactive" - case loading = "loading" - case active = "active" - case error = "error" - case updating = "updating" -} -``` - -## Security API - -### EncryptionManager - -Enterprise-grade encryption management with AES-256-GCM and Secure Enclave integration. - -```swift -@available(iOS 16.0, *) -public class EncryptionManager: ObservableObject -``` - -#### Methods - -```swift -public func encryptData( - _ data: Data, - keyIdentifier: String? = nil, - requireBiometric: Bool = false -) async throws -> EncryptedData - -public func decryptData( - _ encryptedData: EncryptedData, - requireBiometric: Bool = false -) async throws -> Data - -public func generateSecureKey(identifier: String) async throws -> SecretKey - -public func rotateKeys() async throws -``` - -### AuthenticationManager - -Multi-factor authentication with biometric support. - -```swift -@available(iOS 16.0, *) -public class AuthenticationManager: ObservableObject -``` - -#### Methods - -```swift -public func authenticate( - reason: String? = nil, - allowFallback: Bool = true -) async throws -> AuthenticationResult - -public func validateSession(_ sessionId: String? = nil) async throws -> Bool - -public func createSession( - for userId: String, - permissions: [Permission] = [] -) async throws -> Session - -public func revokeSession(_ sessionId: String) async throws -``` - -### PrivacyManager - -GDPR, CCPA, and HIPAA compliance management. - -```swift -@available(iOS 16.0, *) -public class PrivacyManager: ObservableObject -``` - -#### Methods - -```swift -public func processData( - _ data: T, - for purpose: DataPurpose, - subjectId: String? = nil -) async throws -> ProcessedData - -public func processDataSubjectRequest( - _ request: DataSubjectRequest -) async throws -> DataSubjectResponse - -public func generateConsentForm( - for purposes: [DataPurpose] -) async -> ConsentForm - -public func validateGDPRCompliance() async throws -> ComplianceResult -public func validateCCPACompliance() async throws -> ComplianceResult -public func validateHIPAACompliance() async throws -> ComplianceResult -``` - -## Performance API - -### PerformanceMonitor - -Real-time performance metrics collection and monitoring. - -```swift -@available(iOS 16.0, *) -public class PerformanceMonitor: ObservableObject -``` - -#### Methods - -```swift -public func measureExecutionTime( - _ operation: () async throws -> T, - metricType: MetricType = .renderTime -) async throws -> T - -public func getCurrentMetrics() -> [String: PerformanceMetric] - -public func generateReport( - for period: DateInterval -) async throws -> PerformanceReport - -public func setThreshold( - _ threshold: Double, - for metricType: MetricType -) async - -public func enableAlerts(for metricTypes: [MetricType]) async -``` - -### CacheManager - -Multi-level caching system with intelligent cache policies. - -```swift -@available(iOS 16.0, *) -public class CacheManager: ObservableObject -``` - -#### Methods - -```swift -public func store( - _ object: T, - forKey key: String, - ttl: TimeInterval? = nil, - policy: CachePolicy = .cacheFirst -) async throws - -public func retrieve( - _ type: T.Type, - forKey key: String, - policy: CachePolicy = .cacheFirst -) async throws -> T - -public func removeObject(forKey key: String) async - -public func clearCache(policy: ClearPolicy = .expired) async - -public func getCacheStatistics() async -> CacheStatistics -``` - -### OptimizationEngine - -AI-driven performance optimization with automated tuning. - -```swift -@available(iOS 16.0, *) -public class OptimizationEngine: ObservableObject -``` - -#### Methods - -```swift -public func analyzeAndOptimize() async throws -> [Optimization] - -public func executeOptimization(_ optimization: Optimization) async throws -> OptimizationResult - -public func optimizeMemory() async throws -public func optimizeCPU() async throws -public func optimizeBattery() async throws - -public func getOptimizationInsights() async -> OptimizationInsights -``` - -## Enterprise API - -### AIManager - -Core ML integration for intelligent widget optimization and personalization. - -```swift -@available(iOS 16.0, *) -public class AIManager: ObservableObject -``` - -#### Methods - -```swift -public func predict( - modelType: ModelType, - features: [String: Double], - metadata: [String: String] = [:] -) async throws -> PredictionResult - -public func analyzeUserBehavior( - userId: String, - interactions: [UserInteraction], - context: [String: String] = [:] -) async throws -> UserBehaviorProfile - -public func getContentRecommendations( - for userId: String, - contentTypes: [String] = [], - limit: Int = 10 -) async throws -> [ContentRecommendation] - -public func trainModel( - modelType: ModelType, - trainingData: [[String: Double]], - labels: [Double] -) async throws -``` - -### MLAnalyticsEngine - -Advanced analytics with user segmentation and forecasting. - -```swift -@available(iOS 16.0, *) -public class MLAnalyticsEngine: ObservableObject -``` - -#### Methods - -```swift -public func performUserSegmentation( - criteria: [String: Any] = [:] -) async throws -> [UserSegment] - -public func forecastMetric( - metric: String, - forecastPeriod: TimeInterval -) async throws -> TimeSeriesForecast - -public func detectAnomalies( - in data: [DataPoint], - sensitivity: Double = 0.95 -) async throws -> [AnomalyDetection] - -public func generateInsights( - for segment: UserSegment -) async throws -> [BusinessInsight] -``` - -### AdvancedAnalytics - -Executive dashboards and business intelligence. - -```swift -@available(iOS 16.0, *) -public class AdvancedAnalytics: ObservableObject -``` - -#### Methods - -```swift -public func createDashboard( - name: String, - type: DashboardType, - description: String -) async throws -> Dashboard - -public func generateReport( - type: AnalyticsReport.ReportType, - period: DateInterval -) async throws -> AnalyticsReport - -public func executeQuery( - _ query: AnalyticsQuery -) async throws -> QueryResult - -public func scheduleReport( - report: AnalyticsReport, - schedule: ReportSchedule -) async throws -> ScheduledReport -``` - -## Integration API - -### APIManager - -Enterprise-grade API management with comprehensive security and monitoring. - -```swift -@available(iOS 16.0, *) -public class APIManager: ObservableObject -``` - -#### Methods - -```swift -public func request( - _ request: APIRequest, - responseType: T.Type -) async throws -> APIResponse - -public func graphQL( - _ request: GraphQLRequest, - responseType: T.Type -) async throws -> APIResponse - -public func connectWebSocket( - config: WebSocketConfig, - onMessage: @escaping (Data) -> Void -) async throws -> WebSocketConnection - -public func uploadFile( - data: Data, - to endpoint: String, - metadata: [String: String] = [:] -) async throws -> FileUploadResponse -``` - -### CloudIntegration - -Multi-cloud support with hybrid deployment strategies. - -```swift -@available(iOS 16.0, *) -public class CloudIntegration: ObservableObject -``` - -#### Methods - -```swift -public func upload( - data: Data, - to path: String, - provider: CloudProvider? = nil, - metadata: [String: String] = [:] -) async throws -> CloudFile - -public func download( - path: String, - provider: CloudProvider? = nil -) async throws -> Data - -public func synchronize(paths: [String] = []) async throws - -public func migrate( - from sourceProvider: CloudProvider, - to targetProvider: CloudProvider, - paths: [String] = [] -) async throws -``` - -### DataSourceManager - -Unified data source management with real-time streaming. - -```swift -@available(iOS 16.0, *) -public class DataSourceManager: ObservableObject -``` - -#### Methods - -```swift -public func registerDataSource(_ configuration: DataSourceConfiguration) async throws - -public func executeQuery( - _ query: DataQuery, - responseType: T.Type -) async throws -> DataResponse - -public func streamData( - from dataSourceName: String, - query: String? = nil, - responseType: T.Type -) -> AsyncThrowingStream, Error> - -public func writeData( - _ data: T, - to dataSourceName: String, - path: String? = nil, - options: WriteOptions = WriteOptions() -) async throws -> WriteResult -``` - -## Data Models - -### Core Data Models - -#### WidgetData - -Base protocol for all widget data types. - -```swift -public protocol WidgetData: Codable, Equatable { - var id: String { get } - var timestamp: Date { get } - var metadata: [String: String] { get } -} -``` - -#### WeatherData - -Weather-specific data model with comprehensive meteorological information. - -```swift -public struct WeatherData: WidgetData { - public let id: String - public let timestamp: Date - public let metadata: [String: String] - - public let temperature: Temperature - public let humidity: Double - public let pressure: Double - public let windSpeed: Double - public let windDirection: Double - public let conditions: WeatherCondition - public let forecast: [WeatherForecast] - public let location: Location - public let alerts: [WeatherAlert] -} -``` - -#### CalendarData - -Calendar and event data model with meeting and scheduling information. - -```swift -public struct CalendarData: WidgetData { - public let id: String - public let timestamp: Date - public let metadata: [String: String] - - public let events: [CalendarEvent] - public let upcomingMeetings: [Meeting] - public let deadlines: [Deadline] - public let reminders: [Reminder] - public let availability: AvailabilityStatus -} -``` - -### Performance Data Models - -#### PerformanceMetric - -Performance measurement data structure. - -```swift -public struct PerformanceMetric: Codable, Identifiable { - public let id: String - public let type: MetricType - public let value: Double - public let threshold: Double - public let timestamp: Date - public let severity: Severity - public let context: [String: String] -} -``` - -### Security Data Models - -#### EncryptedData - -Container for encrypted data with metadata. - -```swift -public struct EncryptedData: Codable { - public let data: Data - public let algorithm: EncryptionAlgorithm - public let keyIdentifier: String - public let initializationVector: Data - public let authenticationTag: Data - public let timestamp: Date -} -``` - -#### AuthenticationResult - -Result of authentication operations. - -```swift -public struct AuthenticationResult: Codable { - public let success: Bool - public let method: AuthenticationMethod - public let sessionId: String? - public let permissions: [Permission] - public let expiresAt: Date? - public let deviceInfo: DeviceInfo -} -``` - -## Error Handling - -### WidgetError - -Comprehensive error enumeration for widget operations. - -```swift -public enum WidgetError: LocalizedError { - case configurationInvalid - case templateNotFound - case creationFailed(String) - case updateFailed(String) - case dataLoadingFailed(String) - case renderingFailed(String) - case permissionDenied - case resourceUnavailable - case networkError(Error) - case storageError(Error) -} -``` - -### Security Errors - -```swift -public enum SecurityError: LocalizedError { - case encryptionFailed(String) - case decryptionFailed(String) - case authenticationFailed(String) - case biometricUnavailable - case keyGenerationFailed - case invalidCredentials - case sessionExpired - case permissionDenied -} -``` - -### Performance Errors - -```swift -public enum PerformanceError: LocalizedError { - case thresholdExceeded(MetricType, Double) - case monitoringFailed(String) - case optimizationFailed(String) - case cacheError(String) - case metricsUnavailable -} -``` - -## Protocols - -### Widget Development Protocols - -#### WidgetRenderable - -Protocol for renderable widget components. - -```swift -public protocol WidgetRenderable { - associatedtype Content: View - - func render() async throws -> Content - func updateContent(_ data: WidgetData) async throws - func handleInteraction(_ interaction: WidgetInteraction) async -} -``` - -#### DataSourceProtocol - -Protocol for custom data sources. - -```swift -public protocol DataSourceProtocol { - associatedtype DataType: Codable - - func connect() async throws - func disconnect() async - func fetch() async throws -> DataType - func subscribe(handler: @escaping (DataType) -> Void) async throws -} -``` - -### Security Protocols - -#### AuthenticationProvider - -Protocol for custom authentication providers. - -```swift -public protocol AuthenticationProvider { - func authenticate(request: AuthenticationRequest) async throws -> AuthenticationResult - func validateCredentials(_ credentials: Credentials) async throws -> Bool - func refreshToken(_ token: String) async throws -> String -} -``` - -## Constants and Configurations - -### Default Values - -```swift -public struct WidgetDefaults { - public static let updateInterval: TimeInterval = 300 // 5 minutes - public static let maxCacheSize: Int = 100 * 1024 * 1024 // 100MB - public static let defaultTimeout: TimeInterval = 30 - public static let maxRetryAttempts: Int = 3 - public static let performanceThreshold: TimeInterval = 0.1 // 100ms -} -``` - -### Configuration Keys - -```swift -public struct ConfigurationKeys { - public static let apiBaseURL = "api_base_url" - public static let encryptionEnabled = "encryption_enabled" - public static let performanceMonitoring = "performance_monitoring" - public static let analyticsEnabled = "analytics_enabled" - public static let debugMode = "debug_mode" -} -``` - -## Usage Examples - -### Basic Widget Creation - -```swift -// Initialize the manager -let widgetManager = WidgetDevelopmentManager.shared -try await widgetManager.initialize(configuration: .default) - -// Create a weather widget -let configuration = WidgetConfiguration( - type: .weather, - size: .medium, - updateInterval: 300 -) - -let widget = try await widgetManager.createWidget(with: configuration) -``` - -### Security Implementation - -```swift -// Encrypt sensitive data -let encryptionManager = EncryptionManager.shared -let sensitiveData = "User personal information".data(using: .utf8)! - -let encryptedData = try await encryptionManager.encryptData( - sensitiveData, - keyIdentifier: "user-data-key", - requireBiometric: true -) -``` - -### Performance Monitoring - -```swift -// Monitor widget rendering performance -let performanceMonitor = PerformanceMonitor.shared - -let renderTime = try await performanceMonitor.measureExecutionTime { - try await widget.render() -} - -print("Widget rendered in \(renderTime)ms") -``` - -This comprehensive API reference provides complete documentation for all major components and features of the iOS Widget Development Kit, enabling developers to effectively utilize the enterprise-grade capabilities of the framework. \ No newline at end of file diff --git a/Documentation/API.md b/Documentation/API.md deleted file mode 100644 index 861334a..0000000 --- a/Documentation/API.md +++ /dev/null @@ -1,329 +0,0 @@ -# API Reference - -## Table of Contents - -- [Overview](#overview) -- [WidgetKit Integration](#widgetkit-integration) -- [Timeline Providers](#timeline-providers) -- [Widget Views](#widget-views) -- [Data Services](#data-services) -- [Configuration](#configuration) -- [Utilities](#utilities) - ---- - -## Overview - -iOS Widget Development Kit provides a comprehensive set of tools for building beautiful, performant widgets for iOS 14+. - -### Module Import - -```swift -import iOSWidgetDevelopmentKit -``` - ---- - -## WidgetKit Integration - -### WidgetBuilder - -A fluent API for constructing widgets with minimal boilerplate. - -```swift -public struct WidgetBuilder { - public init(@ViewBuilder content: @escaping () -> Content) - public func supportedFamilies(_ families: [WidgetFamily]) -> Self - public func configurationDisplayName(_ name: String) -> Self - public func description(_ description: String) -> Self -} -``` - -**Example:** - -```swift -WidgetBuilder { - MyWidgetView() -} -.supportedFamilies([.systemSmall, .systemMedium]) -.configurationDisplayName("My Widget") -.description("Displays important information") -``` - ---- - -## Timeline Providers - -### SimpleTimelineProvider - -A simplified timeline provider with automatic refresh management. - -```swift -public protocol SimpleTimelineProvider: TimelineProvider { - associatedtype DataType - - func fetchData() async throws -> DataType - var refreshInterval: TimeInterval { get } -} -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `refreshInterval` | `TimeInterval` | Time between updates (default: 3600) | - -**Example:** - -```swift -struct WeatherProvider: SimpleTimelineProvider { - typealias DataType = WeatherData - - var refreshInterval: TimeInterval { 1800 } // 30 minutes - - func fetchData() async throws -> WeatherData { - try await WeatherService.shared.getCurrentWeather() - } -} -``` - -### IntentTimelineProvider - -For configurable widgets with user intents. - -```swift -public struct IntentWidgetConfiguration { - public let provider: IntentTimelineProvider - public let content: (Entry) -> WidgetView -} -``` - ---- - -## Widget Views - -### WidgetContainerView - -A container that handles all widget families with appropriate layouts. - -```swift -public struct WidgetContainerView: View { - public init( - @ViewBuilder small: @escaping () -> Content, - @ViewBuilder medium: @escaping () -> Content, - @ViewBuilder large: @escaping () -> Content - ) -} -``` - -### GradientBackground - -Pre-built gradient backgrounds for widgets. - -```swift -public enum GradientBackground { - case sunrise - case sunset - case ocean - case forest - case custom(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) - - public var gradient: LinearGradient { get } -} -``` - -### WidgetText - -Optimized text component for widgets. - -```swift -public struct WidgetText: View { - public init(_ text: String, style: TextStyle = .body) - - public enum TextStyle { - case title - case headline - case body - case caption - case value - } -} -``` - ---- - -## Data Services - -### WidgetDataService - -Protocol for widget data fetching. - -```swift -public protocol WidgetDataService { - associatedtype DataType: Codable - - func fetch() async throws -> DataType - func getCached() -> DataType? - func invalidateCache() -} -``` - -### SharedDataContainer - -For sharing data between app and widget. - -```swift -public final class SharedDataContainer { - public static func configure(appGroupIdentifier: String) - - public static func save(_ value: T, forKey key: String) throws - public static func load(_ type: T.Type, forKey key: String) throws -> T? - public static func remove(forKey key: String) -} -``` - -**Example:** - -```swift -// In main app -SharedDataContainer.configure(appGroupIdentifier: "group.com.yourapp") -try SharedDataContainer.save(userData, forKey: "userData") - -// In widget -let userData = try SharedDataContainer.load(UserData.self, forKey: "userData") -``` - ---- - -## Configuration - -### WidgetConfiguration - -Global widget configuration options. - -```swift -public struct WidgetConfiguration { - public static var shared: WidgetConfiguration - - public var defaultRefreshInterval: TimeInterval - public var cacheExpirationInterval: TimeInterval - public var maxCacheSize: Int - public var enableDebugMode: Bool -} -``` - -### WidgetPreferences - -User preferences storage for widgets. - -```swift -public final class WidgetPreferences { - public static let shared: WidgetPreferences - - public subscript(key: String) -> T? { get set } - public func register(defaults: [String: Any]) -} -``` - ---- - -## Utilities - -### WidgetCenter Extensions - -```swift -extension WidgetCenter { - /// Reloads specific widget timeline - public func reload(kind: String) - - /// Reloads all widget timelines - public func reloadAll() - - /// Gets current widget configurations - public func getConfigurations() async -> [WidgetInfo] -} -``` - -### DateFormatting - -Widget-optimized date formatters. - -```swift -public enum WidgetDateFormatter { - case relative // "2 hours ago" - case compact // "2h" - case time // "3:45 PM" - case date // "Jan 15" - case full // "January 15, 2024" - - public func string(from date: Date) -> String -} -``` - -### DeepLinking - -Handle widget tap actions. - -```swift -public struct WidgetDeepLink { - public static func url(for action: String, parameters: [String: String] = [:]) -> URL - - public static func parse(_ url: URL) -> (action: String, parameters: [String: String])? -} -``` - -**Example:** - -```swift -// In widget view -Link(destination: WidgetDeepLink.url(for: "showDetails", parameters: ["id": "123"])) { - WidgetContent() -} - -// In main app -func application(_ app: UIApplication, open url: URL) -> Bool { - if let (action, params) = WidgetDeepLink.parse(url) { - handleWidgetAction(action, parameters: params) - return true - } - return false -} -``` - ---- - -## Error Handling - -### WidgetError - -```swift -public enum WidgetError: LocalizedError { - case dataFetchFailed(underlying: Error) - case cacheMiss - case invalidConfiguration - case networkUnavailable - - public var errorDescription: String? { get } - public var recoverySuggestion: String? { get } -} -``` - ---- - -## Best Practices - -1. **Keep widgets lightweight** - Minimize memory usage -2. **Cache aggressively** - Network calls are expensive -3. **Handle errors gracefully** - Always show useful content -4. **Use appropriate refresh intervals** - Balance freshness vs battery -5. **Test all widget families** - Each size has different requirements - ---- - -## Version History - -| Version | Changes | -|---------|---------| -| 2.0.0 | iOS 17 support, Interactive widgets | -| 1.5.0 | Live Activities support | -| 1.0.0 | Initial release | diff --git a/Documentation/APIReference.md b/Documentation/APIReference.md deleted file mode 100644 index 1a30044..0000000 --- a/Documentation/APIReference.md +++ /dev/null @@ -1,591 +0,0 @@ -# API Reference - - -## Table of Contents -- [API Reference](#api-reference) -- [WidgetEngine](#widgetengine) - - [Properties](#properties) - - [Methods](#methods) - - [registerTemplate(_:)](#registertemplate) - - [createWidget(with:)](#createwidgetwith) - - [updateWidget(_:with:)](#updatewidgetwith) - - [configurePerformance(_:)](#configureperformance) - - [getAnalytics()](#getanalytics) -- [WidgetTemplate](#widgettemplate) - - [Properties](#properties) - - [Methods](#methods) - - [createWidget(configuration:)](#createwidgetconfiguration) - - [validateConfiguration(_:)](#validateconfiguration) - - [getDefaultConfiguration()](#getdefaultconfiguration) -- [WidgetConfiguration](#widgetconfiguration) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetDefinition](#widgetdefinition) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetCustomization](#widgetcustomization) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetSettings](#widgetsettings) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetData](#widgetdata) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetType](#widgettype) -- [LiveDataIntegration](#livedataintegration) - - [Methods](#methods) - - [registerDataSource(_:dataSource:)](#registerdatasourcedatasource) - - [connectWebSocket(identifier:url:headers:)](#connectwebsocketidentifierurlheaders) - - [disconnectWebSocket(identifier:)](#disconnectwebsocketidentifier) - - [getDataPublisher(for:)](#getdatapublisherfor) - - [sendData(to:data:)](#senddatatodata) - - [configureDataSource(_:settings:)](#configuredatasourcesettings) - - [getConnectionStatus(for:)](#getconnectionstatusfor) -- [WidgetDataSource](#widgetdatasource) - - [Properties](#properties) - - [Methods](#methods) - - [refresh()](#refresh) - - [configure(_:)](#configure) -- [DataSourceSettings](#datasourcesettings) - - [Properties](#properties) - - [Initializer](#initializer) -- [ConnectionStatus](#connectionstatus) -- [WidgetAnalyticsService](#widgetanalyticsservice) - - [Methods](#methods) - - [getAnalytics()](#getanalytics) - - [configure(_:)](#configure) - - [exportAnalytics()](#exportanalytics) - - [clearAnalytics()](#clearanalytics) -- [WidgetAnalytics](#widgetanalytics) - - [Properties](#properties) -- [AnalyticsConfiguration](#analyticsconfiguration) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetPerformanceMetrics](#widgetperformancemetrics) - - [Properties](#properties) - - [Methods](#methods) - - [update(_:)](#update) - - [updateMemoryUsage(_:)](#updatememoryusage) - - [updateBatteryLevel(_:)](#updatebatterylevel) - - [updateSettings(_:)](#updatesettings) - - [updateBackgroundRefresh()](#updatebackgroundrefresh) -- [WidgetPerformanceSettings](#widgetperformancesettings) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetError](#widgeterror) -- [Notification Names](#notification-names) - - - -## WidgetEngine - -The core widget engine that manages widget lifecycle and performance. - -### Properties - -```swift -public static let shared: WidgetEngine -public var currentConfiguration: WidgetConfiguration? -public var performanceMetrics: WidgetPerformanceMetrics -``` - -### Methods - -#### registerTemplate(_:) -Registers a widget template with the engine. - -```swift -public func registerTemplate(_ template: WidgetTemplate) -``` - -**Parameters:** -- `template`: The widget template to register - -#### createWidget(with:) -Creates a widget view with the specified configuration. - -```swift -public func createWidget(with configuration: WidgetConfiguration) -> some View -``` - -**Parameters:** -- `configuration`: Widget configuration - -**Returns:** Widget view - -#### updateWidget(_:with:) -Updates a widget with new data. - -```swift -public func updateWidget(_ widgetId: String, with data: WidgetData) -``` - -**Parameters:** -- `widgetId`: Widget identifier -- `data`: New data to update - -#### configurePerformance(_:) -Configures widget performance settings. - -```swift -public func configurePerformance(_ settings: WidgetPerformanceSettings) -``` - -**Parameters:** -- `settings`: Performance configuration - -#### getAnalytics() -Returns analytics data. - -```swift -public func getAnalytics() -> WidgetAnalytics -``` - -**Returns:** Analytics data - -## WidgetTemplate - -Base protocol for all widget templates. - -### Properties - -```swift -var identifier: String { get } -var displayName: String { get } -var description: String { get } -var supportedSizes: [WidgetFamily] { get } -``` - -### Methods - -#### createWidget(configuration:) -Creates widget view with configuration. - -```swift -func createWidget(configuration: WidgetConfiguration) -> AnyView -``` - -#### validateConfiguration(_:) -Validates configuration. - -```swift -func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool -``` - -#### getDefaultConfiguration() -Returns default configuration. - -```swift -func getDefaultConfiguration() -> WidgetConfiguration -``` - -## WidgetConfiguration - -Configuration for widget setup. - -### Properties - -```swift -public let id: String -public let templateIdentifier: String -public let widgets: [WidgetDefinition] -public let settings: WidgetSettings -``` - -### Initializer - -```swift -public init(id: String, templateIdentifier: String, widgets: [WidgetDefinition], settings: WidgetSettings) -``` - -## WidgetDefinition - -Definition of a widget instance. - -### Properties - -```swift -public let id: String -public let type: WidgetType -public let dataSourceIdentifier: String? -public let customization: WidgetCustomization -``` - -### Initializer - -```swift -public init(id: String, type: WidgetType, dataSourceIdentifier: String? = nil, customization: WidgetCustomization) -``` - -## WidgetCustomization - -Customization options for widget appearance. - -### Properties - -```swift -public let backgroundColor: Color -public let textColor: Color -public let accentColor: Color -public let cornerRadius: CGFloat -public let padding: EdgeInsets -public let font: Font -``` - -### Initializer - -```swift -public init(backgroundColor: Color = .clear, textColor: Color = .primary, accentColor: Color = .blue, cornerRadius: CGFloat = 12, padding: EdgeInsets = EdgeInsets(), font: Font = .body) -``` - -## WidgetSettings - -Settings for widget behavior. - -### Properties - -```swift -public let refreshInterval: TimeInterval -public let enableAnimations: Bool -public let enableHaptics: Bool -public let enableSound: Bool -``` - -### Initializer - -```swift -public init(refreshInterval: TimeInterval = 30.0, enableAnimations: Bool = true, enableHaptics: Bool = true, enableSound: Bool = false) -``` - -## WidgetData - -Data structure for widget content. - -### Properties - -```swift -public let id: String -public let type: WidgetType -public let content: [String: Any] -public let timestamp: Date -``` - -### Initializer - -```swift -public init(id: String, type: WidgetType, content: [String: Any], timestamp: Date = Date()) -``` - -## WidgetType - -Enumeration of supported widget types. - -```swift -public enum WidgetType: String, CaseIterable { - case weather = "weather" - case calendar = "calendar" - case fitness = "fitness" - case news = "news" - case social = "social" - case productivity = "productivity" - case entertainment = "entertainment" - case finance = "finance" - case health = "health" - case travel = "travel" -} -``` - -## LiveDataIntegration - -Framework for real-time data integration. - -### Methods - -#### registerDataSource(_:dataSource:) -Registers a data source for live updates. - -```swift -public func registerDataSource(_ identifier: String, dataSource: WidgetDataSource) -``` - -#### connectWebSocket(identifier:url:headers:) -Connects to WebSocket for real-time data. - -```swift -public func connectWebSocket(identifier: String, url: URL, headers: [String: String] = [:]) -``` - -#### disconnectWebSocket(identifier:) -Disconnects WebSocket. - -```swift -public func disconnectWebSocket(identifier: String) -``` - -#### getDataPublisher(for:) -Gets data publisher for specific source. - -```swift -public func getDataPublisher(for identifier: String) -> AnyPublisher? -``` - -#### sendData(to:data:) -Sends data to specific widget. - -```swift -public func sendData(to widgetId: String, data: WidgetData) -``` - -#### configureDataSource(_:settings:) -Configures data source settings. - -```swift -public func configureDataSource(_ identifier: String, settings: DataSourceSettings) -``` - -#### getConnectionStatus(for:) -Gets connection status. - -```swift -public func getConnectionStatus(for identifier: String) -> ConnectionStatus -``` - -## WidgetDataSource - -Protocol for data sources. - -### Properties - -```swift -var identifier: String { get } -var dataPublisher: AnyPublisher { get } -``` - -### Methods - -#### refresh() -Refreshes data source. - -```swift -func refresh() -``` - -#### configure(_:) -Configures data source settings. - -```swift -func configure(_ settings: DataSourceSettings) -``` - -## DataSourceSettings - -Settings for data sources. - -### Properties - -```swift -public let refreshInterval: TimeInterval -public let enableCaching: Bool -public let maxRetries: Int -public let timeout: TimeInterval -``` - -### Initializer - -```swift -public init(refreshInterval: TimeInterval = 30.0, enableCaching: Bool = true, maxRetries: Int = 3, timeout: TimeInterval = 10.0) -``` - -## ConnectionStatus - -Enumeration of connection statuses. - -```swift -public enum ConnectionStatus { - case connected - case connecting - case disconnected - case unknown -} -``` - -## WidgetAnalyticsService - -Service for analytics and performance tracking. - -### Methods - -#### getAnalytics() -Returns analytics data. - -```swift -public func getAnalytics() -> WidgetAnalytics -``` - -#### configure(_:) -Configures analytics service. - -```swift -public func configure(_ config: AnalyticsConfiguration) -``` - -#### exportAnalytics() -Exports analytics data. - -```swift -public func exportAnalytics() -> Data? -``` - -#### clearAnalytics() -Clears analytics data. - -```swift -public func clearAnalytics() -``` - -## WidgetAnalytics - -Analytics data structure. - -### Properties - -```swift -public var totalWidgetUpdates: Int -public var widgetUpdates: [String: Int] -public var templateRegistrations: Int -public var dataSourceRegistrations: Int -public var webSocketConnections: Int -public var webSocketDisconnections: Int -public var dataSentCount: Int -public var dataSourceUpdates: Int -public var dataSourceConfigurations: Int -public var connectionsEstablished: Int -public var connectionFailures: Int -public var connectionsCancelled: Int -public var connectionRetries: Int -public var reconnectionAttempts: Int -public var dataReceiveErrors: Int -public var dataDecodeErrors: Int -public var networkUnavailableEvents: Int -public var memoryCleanups: Int -public var batteryOptimizations: Int -public var errors: Int -public var engineInitializations: Int -public var sessionStartTime: Date -public var lastUpdateTime: Date -``` - -## AnalyticsConfiguration - -Configuration for analytics service. - -### Properties - -```swift -public let enableExternalAnalytics: Bool -public let maxEventLogSize: Int -public let enablePerformanceTracking: Bool -public let enableErrorTracking: Bool -``` - -### Initializer - -```swift -public init(enableExternalAnalytics: Bool = false, maxEventLogSize: Int = 1000, enablePerformanceTracking: Bool = true, enableErrorTracking: Bool = true) -``` - -## WidgetPerformanceMetrics - -Performance metrics for widgets. - -### Properties - -```swift -@Published public var memoryUsage: UInt64 -@Published public var batteryLevel: Float -@Published public var refreshCount: Int -@Published public var errorCount: Int -@Published public var settings: WidgetPerformanceSettings -``` - -### Methods - -#### update(_:) -Updates metrics with new data. - -```swift -public func update(_ metrics: WidgetPerformanceMetrics) -``` - -#### updateMemoryUsage(_:) -Updates memory usage. - -```swift -public func updateMemoryUsage(_ usage: UInt64) -``` - -#### updateBatteryLevel(_:) -Updates battery level. - -```swift -public func updateBatteryLevel(_ level: Float) -``` - -#### updateSettings(_:) -Updates performance settings. - -```swift -public func updateSettings(_ settings: WidgetPerformanceSettings) -``` - -#### updateBackgroundRefresh() -Updates background refresh count. - -```swift -public func updateBackgroundRefresh() -``` - -## WidgetPerformanceSettings - -Settings for performance optimization. - -### Properties - -```swift -public let maxMemoryUsage: UInt64 -public let refreshInterval: TimeInterval -public let enableBatteryOptimization: Bool -``` - -### Initializer - -```swift -public init(maxMemoryUsage: UInt64 = 100 * 1024 * 1024, refreshInterval: TimeInterval = 30.0, enableBatteryOptimization: Bool = true) -``` - -## WidgetError - -Error types for widgets. - -```swift -public enum WidgetError: Error, LocalizedError { - case templateNotFound(String) - case dataSourceNotFound(String) - case connectionFailed(String) - case dataDecodeFailed(String) - case networkUnavailable - case unknown -} -``` - -## Notification Names - -```swift -extension Notification.Name { - static let widgetDidUpdate = Notification.Name("widgetDidUpdate") - static let widgetErrorOccurred = Notification.Name("widgetErrorOccurred") - static let widgetDataReceived = Notification.Name("widgetDataReceived") - static let dataSourceUpdated = Notification.Name("dataSourceUpdated") -} -``` \ No newline at end of file diff --git a/Documentation/Architecture.md b/Documentation/Architecture.md deleted file mode 100644 index 918b7b7..0000000 --- a/Documentation/Architecture.md +++ /dev/null @@ -1,356 +0,0 @@ -# 🏗️ Architecture Guide - iOS Widget Development Kit - -## 📋 Table of Contents - -- [Overview](#overview) -- [Architectural Principles](#architectural-principles) -- [System Architecture](#system-architecture) -- [Layer Structure](#layer-structure) -- [Design Patterns](#design-patterns) -- [Data Flow](#data-flow) -- [Component Interaction](#component-interaction) -- [Security Architecture](#security-architecture) -- [Performance Architecture](#performance-architecture) -- [Scalability Considerations](#scalability-considerations) - ---- - -## 🌟 Overview - -The iOS Widget Development Kit follows a **layered, modular architecture** that emphasizes separation of concerns, testability, and maintainability. Our architecture is built on enterprise-grade principles ensuring scalability from small widgets to large-scale enterprise applications. - -### Core Architectural Goals - -- **🔒 Security First**: Zero-trust architecture with defense-in-depth -- **⚡ Performance Optimized**: Sub-100ms response times with intelligent caching -- **🧪 Testability**: 95%+ test coverage with comprehensive unit and integration tests -- **🔧 Maintainability**: Clean, documented code following SOLID principles -- **📈 Scalability**: Support for millions of widgets with horizontal scaling -- **🌐 Extensibility**: Plugin architecture for custom functionality - ---- - -## 🎯 Architectural Principles - -### 1. Clean Architecture -Following Uncle Bob's Clean Architecture principles: -- **Dependency Inversion**: High-level modules don't depend on low-level modules -- **Single Responsibility**: Each component has one reason to change -- **Interface Segregation**: No client should depend on methods it doesn't use -- **Open/Closed Principle**: Open for extension, closed for modification - -### 2. MVVM + Coordinator Pattern -- **Model**: Data models and business logic -- **View**: SwiftUI views and UI components -- **ViewModel**: View logic and state management -- **Coordinator**: Navigation and flow control - -### 3. Reactive Programming -- **Combine Framework**: For reactive data streams -- **Publisher/Subscriber**: Decoupled component communication -- **State Management**: Centralized, predictable state updates - ---- - -## 🏢 System Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ iOS Widget Development Kit │ -├─────────────────────────────────────────────────────────────┤ -│ Presentation Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ SwiftUI │ │ ViewModels │ │ Coordinators│ │ -│ │ Views │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ Business Logic Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Use Cases │ │ Services │ │ Validators │ │ -│ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ Data Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │Repositories │ │Data Sources │ │ Models │ │ -│ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ Infrastructure Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Network │ │ Cache │ │ Security │ │ -│ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📚 Layer Structure - -### 1. **Presentation Layer** (Sources/UI/) -- **SwiftUI Views**: Declarative UI components -- **ViewModels**: State management and view logic -- **Coordinators**: Navigation flow control -- **View Builders**: Reusable UI component builders - -**Responsibilities:** -- User interface rendering -- User interaction handling -- Navigation management -- State presentation - -### 2. **Business Logic Layer** (Sources/Core/, Sources/Features/) -- **Use Cases**: Application-specific business rules -- **Services**: Cross-cutting concerns (Analytics, Logging) -- **Validators**: Input validation and business rule enforcement -- **Managers**: Resource and lifecycle management - -**Responsibilities:** -- Business rule implementation -- Data transformation -- Workflow orchestration -- Cross-cutting services - -### 3. **Data Layer** (Sources/Analytics/, Sources/LiveData/) -- **Repositories**: Data access abstraction -- **Data Sources**: Concrete data implementations -- **Models**: Data structures and entities -- **DTOs**: Data transfer objects - -**Responsibilities:** -- Data persistence -- External API integration -- Data mapping and transformation -- Cache management - -### 4. **Infrastructure Layer** (Sources/Security/, Sources/Performance/) -- **Network Layer**: HTTP client and communication -- **Cache Layer**: Data caching strategies -- **Security Layer**: Authentication and authorization -- **Configuration**: App settings and environment - -**Responsibilities:** -- External service integration -- Infrastructure concerns -- Platform-specific implementations -- Configuration management - ---- - -## 🎨 Design Patterns - -### 1. **Repository Pattern** -```swift -protocol WidgetRepository { - func getWidget(id: String) async throws -> Widget - func saveWidget(_ widget: Widget) async throws - func deleteWidget(id: String) async throws -} - -class DefaultWidgetRepository: WidgetRepository { - private let localDataSource: LocalWidgetDataSource - private let remoteDataSource: RemoteWidgetDataSource - - // Implementation with caching strategy -} -``` - -### 2. **Factory Pattern** -```swift -protocol WidgetFactory { - func createWidget(type: WidgetType) -> Widget -} - -class DefaultWidgetFactory: WidgetFactory { - func createWidget(type: WidgetType) -> Widget { - switch type { - case .weather: return WeatherWidget() - case .calendar: return CalendarWidget() - case .fitness: return FitnessWidget() - } - } -} -``` - -### 3. **Observer Pattern** -```swift -class WidgetNotificationCenter { - static let shared = WidgetNotificationCenter() - private var observers: [WeakObserver] = [] - - func addObserver(_ observer: WidgetObserver) { - observers.append(WeakObserver(observer)) - } - - func notifyWidgetUpdated(_ widget: Widget) { - observers.forEach { $0.observer?.widgetDidUpdate(widget) } - } -} -``` - -### 4. **Strategy Pattern** -```swift -protocol CachingStrategy { - func cache(_ object: T, forKey key: String) - func retrieve(forKey key: String, type: T.Type) -> T? -} - -class MemoryCachingStrategy: CachingStrategy { /* Implementation */ } -class DiskCachingStrategy: CachingStrategy { /* Implementation */ } -class HybridCachingStrategy: CachingStrategy { /* Implementation */ } -``` - ---- - -## 🔄 Data Flow - -### Widget Update Flow -``` -User Action → ViewModel → Use Case → Repository → Data Source → API - ↑ ↓ -UI Update ← Combine Publisher ← Business Logic ← Data Mapping ← Response -``` - -### Live Activity Flow -``` -External Event → Push Notification → Activity Manager → Widget Engine - ↓ ↓ ↓ - Live Activity Update → Dynamic Island → Lock Screen -``` - -### Background Refresh Flow -``` -Background Task → Scheduler → Data Fetcher → Cache Update → Widget Refresh - ↓ ↓ ↓ ↓ - Battery Check → Network Check → Validation → UI Update -``` - ---- - -## 🔗 Component Interaction - -### Core Components -- **WidgetEngine**: Central orchestrator for all widget operations -- **DataManager**: Handles all data operations and caching -- **SecurityManager**: Manages authentication and encryption -- **PerformanceMonitor**: Tracks and optimizes performance metrics -- **AnalyticsService**: Collects usage and performance data - -### Communication Patterns -- **Async/Await**: For asynchronous operations -- **Combine**: For reactive data streams -- **Protocols**: For dependency injection and testability -- **Delegates**: For callback-based communication - ---- - -## 🛡️ Security Architecture - -### Authentication & Authorization -- **Biometric Authentication**: Touch ID / Face ID integration -- **Keychain Storage**: Secure credential storage -- **Certificate Pinning**: SSL/TLS security -- **Token Management**: JWT token handling - -### Data Protection -- **Encryption at Rest**: AES-256 encryption for stored data -- **Encryption in Transit**: TLS 1.3 for network communication -- **Data Sanitization**: Input validation and output encoding -- **Privacy Controls**: GDPR compliance and user consent - -### Security Monitoring -- **Threat Detection**: Real-time security monitoring -- **Audit Logging**: Comprehensive security event logging -- **Vulnerability Scanning**: Automated security assessments -- **Incident Response**: Security incident handling procedures - ---- - -## ⚡ Performance Architecture - -### Optimization Strategies -- **Lazy Loading**: Load resources only when needed -- **Memory Pooling**: Efficient memory management -- **Background Processing**: Non-blocking UI operations -- **Smart Caching**: Multi-level caching strategy - -### Performance Monitoring -- **Real-time Metrics**: Live performance monitoring -- **Performance Budgets**: Predefined performance thresholds -- **Automated Alerts**: Performance degradation notifications -- **Optimization Recommendations**: AI-powered performance insights - -### Resource Management -- **Memory Management**: Automatic memory optimization -- **Battery Optimization**: Power-efficient algorithms -- **Network Optimization**: Intelligent data fetching -- **CPU Optimization**: Efficient processing strategies - ---- - -## 📈 Scalability Considerations - -### Horizontal Scaling -- **Microservice Architecture**: Decomposed services -- **Load Balancing**: Distributed traffic handling -- **Auto-scaling**: Dynamic resource allocation -- **Service Mesh**: Inter-service communication - -### Vertical Scaling -- **Resource Optimization**: Efficient resource utilization -- **Performance Tuning**: Algorithm optimization -- **Memory Optimization**: Reduced memory footprint -- **CPU Optimization**: Parallel processing - -### Data Scaling -- **Database Sharding**: Distributed data storage -- **Caching Layers**: Multi-level caching -- **Data Partitioning**: Logical data separation -- **Read Replicas**: Distributed read operations - ---- - -## 🔧 Implementation Guidelines - -### Code Organization -``` -Sources/ -├── Core/ # Core framework components -│ ├── WidgetEngine.swift # Central widget orchestrator -│ ├── DataManager.swift # Data management -│ └── ConfigManager.swift # Configuration management -├── Features/ # Feature-specific implementations -│ ├── Weather/ # Weather widget feature -│ ├── Calendar/ # Calendar widget feature -│ └── Fitness/ # Fitness widget feature -├── Security/ # Security implementations -│ ├── Authentication/ # Authentication services -│ ├── Encryption/ # Encryption utilities -│ └── Compliance/ # Compliance frameworks -├── Performance/ # Performance optimizations -│ ├── Monitoring/ # Performance monitoring -│ ├── Optimization/ # Performance optimization -│ └── Caching/ # Caching strategies -└── UI/ # User interface components - ├── Views/ # SwiftUI views - ├── ViewModels/ # View models - └── Coordinators/ # Navigation coordinators -``` - -### Testing Strategy -- **Unit Tests**: Test individual components in isolation -- **Integration Tests**: Test component interactions -- **UI Tests**: Test user interface workflows -- **Performance Tests**: Test performance characteristics -- **Security Tests**: Test security implementations - -### Documentation Standards -- **Code Documentation**: Comprehensive inline documentation -- **Architecture Documentation**: System design documentation -- **API Documentation**: Public interface documentation -- **Setup Guides**: Implementation and setup guides - ---- - -This architecture ensures that the iOS Widget Development Kit remains maintainable, scalable, and secure while providing excellent performance and developer experience. diff --git a/Documentation/ArchitectureGuide.md b/Documentation/ArchitectureGuide.md deleted file mode 100644 index b445122..0000000 --- a/Documentation/ArchitectureGuide.md +++ /dev/null @@ -1,469 +0,0 @@ -# 🏗️ Widget Architecture Guide - - -## Table of Contents -- [🏗️ Widget Architecture Guide](#-widget-architecture-guide) -- [Overview](#overview) -- [🏛️ Architecture Overview](#-architecture-overview) - - [Core Architecture Principles](#core-architecture-principles) - - [Architecture Layers](#architecture-layers) -- [🧩 Core Components](#-core-components) - - [1. Widget Engine](#1-widget-engine) - - [2. Widget Managers](#2-widget-managers) - - [Home Screen Widget Manager](#home-screen-widget-manager) - - [Live Activity Manager](#live-activity-manager) - - [Dynamic Island Manager](#dynamic-island-manager) - - [3. Data Integration Layer](#3-data-integration-layer) - - [4. Analytics Engine](#4-analytics-engine) -- [🔄 Data Flow](#-data-flow) - - [Widget Update Flow](#widget-update-flow) - - [Live Activity Flow](#live-activity-flow) -- [🎨 UI Architecture](#-ui-architecture) - - [Widget View Hierarchy](#widget-view-hierarchy) - - [View Model Pattern](#view-model-pattern) -- [🔧 Configuration Architecture](#-configuration-architecture) - - [Widget Configuration System](#widget-configuration-system) - - [Theme System](#theme-system) -- [🚀 Performance Architecture](#-performance-architecture) - - [Memory Management](#memory-management) - - [Background Processing](#background-processing) -- [🔒 Security Architecture](#-security-architecture) - - [Data Security](#data-security) - - [Widget Security](#widget-security) -- [📊 Monitoring and Analytics](#-monitoring-and-analytics) - - [Performance Monitoring](#performance-monitoring) - - [Error Handling](#error-handling) -- [🔄 Lifecycle Management](#-lifecycle-management) - - [Widget Lifecycle](#widget-lifecycle) - - [Live Activity Lifecycle](#live-activity-lifecycle) -- [🧪 Testing Architecture](#-testing-architecture) - - [Unit Testing](#unit-testing) - - [Integration Testing](#integration-testing) - - [UI Testing](#ui-testing) -- [📈 Scalability Considerations](#-scalability-considerations) - - [Horizontal Scaling](#horizontal-scaling) - - [Vertical Scaling](#vertical-scaling) -- [🔮 Future Architecture](#-future-architecture) - - [Planned Enhancements](#planned-enhancements) - - [Architecture Evolution](#architecture-evolution) -- [📚 Best Practices](#-best-practices) - - [Code Organization](#code-organization) - - [Performance Optimization](#performance-optimization) - - [Security](#security) -- [🎯 Conclusion](#-conclusion) - - - -## Overview - -The iOS Widget Development Kit follows a clean, modular architecture designed for scalability, maintainability, and performance. This guide explains the architectural patterns, components, and design principles used throughout the framework. - -## 🏛️ Architecture Overview - -### Core Architecture Principles - -- **Clean Architecture**: Separation of concerns with clear boundaries -- **SOLID Principles**: Single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion -- **MVVM Pattern**: Model-View-ViewModel for UI components -- **Repository Pattern**: Data access abstraction -- **Factory Pattern**: Widget creation and configuration -- **Observer Pattern**: Real-time updates and notifications - -### Architecture Layers - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Widget │ │ Live Activity│ │ Dynamic │ │ -│ │ Views │ │ Views │ │ Island │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────────┐ -│ Business Logic Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Widget │ │ Data │ │ Analytics │ │ -│ │ Managers │ │ Processors │ │ Engine │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────────┐ -│ Data Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Network │ │ Cache │ │ Storage │ │ -│ │ Services │ │ Manager │ │ Manager │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Logging │ │ Error │ │ Security │ │ -│ │ System │ │ Handling │ │ Manager │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 🧩 Core Components - -### 1. Widget Engine - -The Widget Engine is the central orchestrator that manages widget lifecycle, configuration, and updates. - -```swift -class WidgetEngine { - // Core functionality - func register(_ widget: Widget) -> Result - func unregister(_ widget: Widget) -> Result - func update(_ widget: Widget, with data: WidgetData) -> Result - - // Configuration management - func configure(_ config: WidgetConfiguration) -> Result - func getConfiguration() -> WidgetConfiguration - - // Lifecycle management - func start() -> Result - func stop() -> Result - func pause() -> Result - func resume() -> Result -} -``` - -### 2. Widget Managers - -Specialized managers handle different widget types and functionalities: - -#### Home Screen Widget Manager -```swift -class HomeScreenWidgetManager { - func createWidget(kind: String, size: WidgetSize) -> HomeScreenWidget - func updateWidget(_ widget: HomeScreenWidget, with data: WidgetData) - func configureWidget(_ widget: HomeScreenWidget, config: HomeWidgetConfiguration) -} -``` - -#### Live Activity Manager -```swift -class LiveActivityManager { - func startActivity(_ activity: Activity) - func updateActivity(_ activity: Activity, with data: T.ContentState) - func endActivity(_ activity: Activity) -} -``` - -#### Dynamic Island Manager -```swift -class DynamicIslandManager { - func startIslandActivity(_ activity: DynamicIslandActivity) - func updateIslandActivity(_ activity: DynamicIslandActivity) - func endIslandActivity(_ activity: DynamicIslandActivity) -} -``` - -### 3. Data Integration Layer - -The data integration layer provides seamless data access and synchronization: - -```swift -protocol WidgetDataSource { - func fetch() async throws -> WidgetData - func subscribe(to updates: @escaping (WidgetData) -> Void) - func unsubscribe() -} - -class WidgetDataManager { - func connect(_ dataSource: WidgetDataSource) -> Result - func fetchData() async throws -> WidgetData - func cache(_ data: WidgetData) -> Result - func retrieve(id: String) -> Result -} -``` - -### 4. Analytics Engine - -The analytics engine tracks widget usage and performance: - -```swift -class WidgetAnalytics { - func trackWidgetView(_ widget: Widget) - func trackWidgetInteraction(_ widget: Widget, action: String) - func trackPerformance(_ widget: Widget, metrics: PerformanceMetrics) - func generateReport() -> AnalyticsReport -} -``` - -## 🔄 Data Flow - -### Widget Update Flow - -``` -1. Data Source → 2. Data Processor → 3. Widget Engine → 4. Widget View - ↓ ↓ ↓ ↓ - Raw Data → Processed Data → Widget Data → UI Update -``` - -### Live Activity Flow - -``` -1. Activity Start → 2. Real-time Updates → 3. UI Refresh → 4. Activity End - ↓ ↓ ↓ ↓ - Initialize → Data Updates → View Update → Cleanup -``` - -## 🎨 UI Architecture - -### Widget View Hierarchy - -``` -WidgetView (Base) -├── HomeScreenWidgetView -│ ├── SmallWidgetView -│ ├── MediumWidgetView -│ └── LargeWidgetView -├── LockScreenWidgetView -│ ├── SmallLockWidgetView -│ └── MediumLockWidgetView -└── LiveActivityView - ├── LockScreenActivityView - └── DynamicIslandView - ├── CompactView - ├── ExpandedView - └── MinimalView -``` - -### View Model Pattern - -```swift -class WidgetViewModel: ObservableObject { - @Published var widgetData: WidgetData? - @Published var isLoading: Bool = false - @Published var error: WidgetError? - - private let dataManager: WidgetDataManager - private let analytics: WidgetAnalytics - - func loadData() - func refreshData() - func handleInteraction(_ action: WidgetAction) -} -``` - -## 🔧 Configuration Architecture - -### Widget Configuration System - -```swift -struct WidgetConfiguration { - // Basic settings - var enableHomeScreenWidgets: Bool = true - var enableLockScreenWidgets: Bool = true - var enableLiveActivities: Bool = true - var enableDynamicIsland: Bool = true - - // Performance settings - var refreshInterval: TimeInterval = 300 - var enableBackgroundUpdates: Bool = true - var enableCaching: Bool = true - - // Styling settings - var enableDynamicColors: Bool = true - var enableDarkMode: Bool = true - var enableCustomFonts: Bool = true - var enableAnimations: Bool = true -} -``` - -### Theme System - -```swift -struct WidgetTheme { - let backgroundColor: Color - let textColor: Color - let accentColor: Color - let cornerRadius: CGFloat - let shadow: WidgetShadow? -} - -struct DynamicTheme { - let lightMode: WidgetTheme - let darkMode: WidgetTheme -} -``` - -## 🚀 Performance Architecture - -### Memory Management - -- **Widget Pooling**: Reuse widget instances for better performance -- **Lazy Loading**: Load data and views only when needed -- **Image Caching**: Efficient image loading and caching -- **Memory Monitoring**: Track and optimize memory usage - -### Background Processing - -- **Background Tasks**: Handle updates when app is in background -- **Data Synchronization**: Sync data efficiently -- **Battery Optimization**: Minimize battery impact -- **Network Optimization**: Efficient network requests - -## 🔒 Security Architecture - -### Data Security - -- **Encryption**: Encrypt sensitive data -- **Certificate Pinning**: Secure network communications -- **Data Validation**: Validate all incoming data -- **Access Control**: Control data access permissions - -### Widget Security - -- **Sandboxing**: Isolate widget processes -- **Permission Management**: Manage widget permissions -- **Secure Storage**: Store sensitive data securely -- **Audit Logging**: Log security events - -## 📊 Monitoring and Analytics - -### Performance Monitoring - -```swift -struct PerformanceMetrics { - let renderTime: TimeInterval - let memoryUsage: Int64 - let networkLatency: TimeInterval - let batteryImpact: Double -} - -class PerformanceMonitor { - func trackMetrics(_ metrics: PerformanceMetrics) - func generatePerformanceReport() -> PerformanceReport - func alertOnThreshold(_ threshold: PerformanceThreshold) -} -``` - -### Error Handling - -```swift -enum WidgetError: Error { - case initializationFailed - case dataFetchFailed - case renderFailed - case networkError - case permissionDenied - case invalidConfiguration -} - -class ErrorHandler { - func handle(_ error: WidgetError) - func logError(_ error: WidgetError) - func recoverFromError(_ error: WidgetError) -> Result -} -``` - -## 🔄 Lifecycle Management - -### Widget Lifecycle - -``` -1. Initialization → 2. Configuration → 3. Registration → 4. Activation - ↓ ↓ ↓ ↓ - Setup Engine → Load Config → Register → Start Updates - ↓ ↓ ↓ ↓ -5. Data Loading → 6. Rendering → 7. Interaction → 8. Cleanup - ↓ ↓ ↓ ↓ - Fetch Data → Update UI → Handle Event → Release Resources -``` - -### Live Activity Lifecycle - -``` -1. Request Activity → 2. Start Activity → 3. Update Activity → 4. End Activity - ↓ ↓ ↓ ↓ - Create Config → Initialize → Real-time → Cleanup - ↓ ↓ ↓ ↓ -5. Dynamic Island → 6. Lock Screen → 7. Notifications → 8. Analytics - ↓ ↓ ↓ ↓ - Island Views → Lock Views → Push Updates → Track Usage -``` - -## 🧪 Testing Architecture - -### Unit Testing - -- **Widget Engine Tests**: Test core functionality -- **Data Manager Tests**: Test data operations -- **Configuration Tests**: Test configuration system -- **Error Handling Tests**: Test error scenarios - -### Integration Testing - -- **Widget Integration Tests**: Test widget interactions -- **Live Activity Tests**: Test activity lifecycle -- **Data Integration Tests**: Test data flow -- **Performance Tests**: Test performance metrics - -### UI Testing - -- **Widget Rendering Tests**: Test UI rendering -- **Interaction Tests**: Test user interactions -- **Accessibility Tests**: Test accessibility features -- **Cross-device Tests**: Test on different devices - -## 📈 Scalability Considerations - -### Horizontal Scaling - -- **Widget Pooling**: Reuse widget instances -- **Data Caching**: Cache frequently accessed data -- **Background Processing**: Handle updates efficiently -- **Memory Management**: Optimize memory usage - -### Vertical Scaling - -- **Modular Architecture**: Add new features easily -- **Plugin System**: Extend functionality with plugins -- **Configuration System**: Customize behavior -- **Analytics System**: Monitor and optimize - -## 🔮 Future Architecture - -### Planned Enhancements - -- **AI Integration**: Smart widget recommendations -- **Machine Learning**: Predictive data loading -- **Advanced Analytics**: Deep insights into usage -- **Cross-platform Support**: Extend to other platforms - -### Architecture Evolution - -- **Microservices**: Break down into smaller services -- **Event-driven Architecture**: Improve responsiveness -- **GraphQL Integration**: Flexible data fetching -- **Real-time Collaboration**: Multi-user features - -## 📚 Best Practices - -### Code Organization - -- **Clear Separation**: Separate concerns clearly -- **Consistent Naming**: Use consistent naming conventions -- **Documentation**: Document all public APIs -- **Error Handling**: Handle errors gracefully - -### Performance Optimization - -- **Lazy Loading**: Load data only when needed -- **Caching**: Cache frequently accessed data -- **Background Processing**: Handle updates efficiently -- **Memory Management**: Optimize memory usage - -### Security - -- **Data Encryption**: Encrypt sensitive data -- **Input Validation**: Validate all inputs -- **Access Control**: Control data access -- **Audit Logging**: Log security events - -## 🎯 Conclusion - -The iOS Widget Development Kit architecture provides a solid foundation for building scalable, maintainable, and performant widget applications. By following the architectural patterns and principles outlined in this guide, developers can create robust widget solutions that meet the highest standards of quality and user experience. - -For more information about specific architectural components, refer to the individual documentation files for each module. \ No newline at end of file diff --git a/Documentation/Best-Practices.md b/Documentation/Best-Practices.md deleted file mode 100644 index 5ca70d9..0000000 --- a/Documentation/Best-Practices.md +++ /dev/null @@ -1,1078 +0,0 @@ -# Enterprise Best Practices Guide — iOS Widget Development Kit - -## Overview - -This comprehensive guide outlines enterprise-grade best practices for developing high-quality, performant, and secure iOS widgets using the iOS Widget Development Kit. These practices ensure optimal user experience, maintainability, and compliance with industry standards. - -## Table of Contents - -1. [Architecture & Design Principles](#architecture--design-principles) -2. [Performance Best Practices](#performance-best-practices) -3. [Security Best Practices](#security-best-practices) -4. [Code Quality Standards](#code-quality-standards) -5. [Testing Best Practices](#testing-best-practices) -6. [Accessibility Best Practices](#accessibility-best-practices) -7. [Localization Best Practices](#localization-best-practices) -8. [Deployment & Monitoring](#deployment--monitoring) -9. [Documentation Standards](#documentation-standards) - -## Architecture & Design Principles - -### 1. Clean Architecture Implementation - -**Layer Separation** -```swift -// ✅ Good: Clear separation of concerns -struct WeatherWidget { - private let dataLayer: WeatherDataRepository - private let businessLayer: WeatherBusinessLogic - private let presentationLayer: WeatherPresentation - - init() { - self.dataLayer = WeatherDataRepository() - self.businessLayer = WeatherBusinessLogic(repository: dataLayer) - self.presentationLayer = WeatherPresentation(logic: businessLayer) - } -} - -// ❌ Bad: Mixed responsibilities -struct WeatherWidget { - func loadWeatherAndUpdateUI() { - // Data loading, business logic, and UI updates mixed together - URLSession.shared.dataTask(with: url) { data, _, _ in - // Process data - // Update UI - // Business logic - } - } -} -``` - -**Dependency Injection** -```swift -// ✅ Good: Testable and flexible -protocol WeatherService { - func fetchWeather() async throws -> WeatherData -} - -class WeatherWidget { - private let weatherService: WeatherService - - init(weatherService: WeatherService) { - self.weatherService = weatherService - } -} - -// ❌ Bad: Tight coupling -class WeatherWidget { - private let weatherService = ConcreteWeatherService() -} -``` - -### 2. SOLID Principles Application - -**Single Responsibility Principle** -```swift -// ✅ Good: Each class has one responsibility -class WeatherDataLoader { - func loadWeather() async throws -> WeatherData { } -} - -class WeatherFormatter { - func format(_ weather: WeatherData) -> String { } -} - -class WeatherRenderer { - func render(_ formattedWeather: String) { } -} -``` - -**Open/Closed Principle** -```swift -// ✅ Good: Open for extension, closed for modification -protocol WidgetRenderer { - func render() async throws -} - -class WeatherRenderer: WidgetRenderer { - func render() async throws { - // Weather-specific rendering - } -} - -class CalendarRenderer: WidgetRenderer { - func render() async throws { - // Calendar-specific rendering - } -} -``` - -### 3. Design Patterns - -**Repository Pattern** -```swift -protocol WeatherRepository { - func getWeather(for location: Location) async throws -> WeatherData - func saveWeather(_ weather: WeatherData) async throws -} - -class WeatherRepositoryImpl: WeatherRepository { - private let remoteDataSource: RemoteDataSource - private let localDataSource: LocalDataSource - - func getWeather(for location: Location) async throws -> WeatherData { - // Try local first, fallback to remote - if let cachedWeather = try? await localDataSource.getWeather(for: location), - !cachedWeather.isExpired { - return cachedWeather - } - - let remoteWeather = try await remoteDataSource.getWeather(for: location) - try? await localDataSource.saveWeather(remoteWeather) - return remoteWeather - } -} -``` - -## Performance Best Practices - -### 1. Memory Management - -**Proper Resource Cleanup** -```swift -// ✅ Good: Proper cleanup and weak references -class WidgetManager { - private weak var delegate: WidgetDelegate? - private var cancellables = Set() - private var imageCache: NSCache - - init() { - imageCache = NSCache() - imageCache.totalCostLimit = 50 * 1024 * 1024 // 50MB - } - - deinit { - cancellables.removeAll() - cleanup() - } - - private func cleanup() { - imageCache.removeAllObjects() - // Release other resources - } -} -``` - -**Efficient Caching Strategy** -```swift -// ✅ Good: Multi-level caching -class DataCacheManager { - private let memoryCache = NSCache() - private let diskCache: DiskCache - - init() { - // Configure memory cache - memoryCache.totalCostLimit = 20 * 1024 * 1024 // 20MB - memoryCache.countLimit = 100 - - // Configure disk cache - diskCache = DiskCache(maxSize: 100 * 1024 * 1024) // 100MB - } - - func getData(for key: String) async -> Data? { - // Check memory cache first - if let data = memoryCache.object(forKey: NSString(string: key)) { - return data as Data - } - - // Check disk cache - if let data = await diskCache.getData(for: key) { - // Store in memory cache for faster access - memoryCache.setObject(data as NSData, forKey: NSString(string: key)) - return data - } - - return nil - } -} -``` - -### 2. Performance Monitoring - -**Execution Time Measurement** -```swift -// ✅ Good: Performance monitoring with thresholds -func renderWidget() async throws { - let startTime = CFAbsoluteTimeGetCurrent() - defer { - let executionTime = CFAbsoluteTimeGetCurrent() - startTime - PerformanceMonitor.shared.recordMetric(.renderTime, value: executionTime) - - if executionTime > 0.1 { // 100ms threshold - logger.warning("Widget rendering exceeded threshold: \(executionTime)s") - // Send alert to monitoring system - Analytics.shared.trackPerformanceIssue( - metric: "render_time", - value: executionTime, - threshold: 0.1 - ) - } - } - - // Rendering logic - try await performRenderingOperations() -} -``` - -**Battery Optimization** -```swift -// ✅ Good: Battery-conscious implementation -class WidgetUpdateManager { - private var updateTimer: Timer? - private let batteryMonitor = BatteryMonitor() - private let networkMonitor = NetworkMonitor() - - func scheduleUpdates() { - let updateInterval = calculateOptimalInterval() - - updateTimer = Timer.scheduledTimer( - withTimeInterval: updateInterval, - repeats: true - ) { [weak self] _ in - Task { - await self?.updateWidgetData() - } - } - } - - private func calculateOptimalInterval() -> TimeInterval { - var interval: TimeInterval = 300 // Default 5 minutes - - // Adjust based on battery level - if batteryMonitor.batteryLevel < 0.2 { - interval *= 3 // 15 minutes when battery is low - } else if batteryMonitor.isLowPowerModeEnabled { - interval *= 2 // 10 minutes in low power mode - } - - // Adjust based on network conditions - if networkMonitor.isExpensive { - interval *= 1.5 // Reduce frequency on cellular - } - - return interval - } -} -``` - -### 3. Lazy Loading and Prefetching - -```swift -// ✅ Good: Intelligent data loading -class WidgetDataManager { - private let prefetchQueue = OperationQueue() - - init() { - prefetchQueue.maxConcurrentOperationCount = 2 - prefetchQueue.qualityOfService = .utility - } - - func loadWidgetData() async throws -> WidgetData { - // Load essential data immediately - async let essentialData = loadEssentialData() - - // Prefetch secondary data in background - prefetchSecondaryData() - - return try await essentialData - } - - private func prefetchSecondaryData() { - prefetchQueue.addOperation { [weak self] in - Task { - // Prefetch images - await self?.prefetchImages() - - // Prefetch next day's weather - await self?.prefetchForecast() - } - } - } -} -``` - -## Security Best Practices - -### 1. Data Protection - -**Sensitive Data Handling** -```swift -// ✅ Good: Proper encryption for sensitive data -class SecureDataManager { - private let encryptionManager = EncryptionManager.shared - private let keychain = KeychainWrapper() - - func storeUserData(_ data: T, sensitive: Bool = false) async throws { - let jsonData = try JSONEncoder().encode(data) - - if sensitive { - // Encrypt sensitive data - let encryptedData = try await encryptionManager.encryptData( - jsonData, - keyIdentifier: "user-data-key", - requireBiometric: true - ) - - // Store in keychain - try keychain.set(encryptedData, forKey: "encrypted_user_data") - } else { - // Store non-sensitive data in UserDefaults - UserDefaults.standard.set(jsonData, forKey: "user_data") - } - } - - func retrieveUserData(_ type: T.Type, sensitive: Bool = false) async throws -> T { - if sensitive { - // Retrieve from keychain and decrypt - guard let encryptedData = keychain.getData("encrypted_user_data") else { - throw SecurityError.dataNotFound - } - - let decryptedData = try await encryptionManager.decryptData( - encryptedData, - requireBiometric: true - ) - - return try JSONDecoder().decode(type, from: decryptedData) - } else { - // Retrieve from UserDefaults - guard let data = UserDefaults.standard.data(forKey: "user_data") else { - throw SecurityError.dataNotFound - } - - return try JSONDecoder().decode(type, from: data) - } - } -} -``` - -**Network Security** -```swift -// ✅ Good: Secure network communication -class SecureNetworkManager { - private let session: URLSession - - init() { - let configuration = URLSessionConfiguration.default - - // Enable certificate pinning - configuration.tlsMinimumSupportedProtocolVersion = .TLSv12 - configuration.httpShouldUsePipelining = false - configuration.httpCookieAcceptPolicy = .never - - session = URLSession( - configuration: configuration, - delegate: self, - delegateQueue: nil - ) - } - - func fetchSecureData(from url: URL) async throws -> Data { - var request = URLRequest(url: url) - - // Add security headers - request.setValue("no-cache, no-store, must-revalidate", forHTTPHeaderField: "Cache-Control") - request.setValue("nosniff", forHTTPHeaderField: "X-Content-Type-Options") - request.setValue("1; mode=block", forHTTPHeaderField: "X-XSS-Protection") - - let (data, response) = try await session.data(for: request) - - // Validate response - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw NetworkError.invalidResponse - } - - return data - } -} - -extension SecureNetworkManager: URLSessionDelegate { - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) { - // Implement certificate pinning - guard let serverTrust = challenge.protectionSpace.serverTrust else { - challenge.sender?.cancelAuthenticationChallenge(challenge) - return - } - - // Validate server certificate - // ... certificate validation logic ... - } -} -``` - -### 2. Privacy Compliance - -**GDPR Compliance** -```swift -// ✅ Good: Privacy-conscious data processing -class PrivacyCompliantAnalytics { - private let privacyManager = PrivacyManager.shared - - func trackUserInteraction(_ interaction: UserInteraction) async { - // Check consent before processing - guard await privacyManager.hasConsent(for: .analytics) else { - logger.info("Analytics tracking skipped - no consent") - return - } - - // Anonymize data - let anonymizedData = await anonymizeInteraction(interaction) - - // Process with purpose limitation - try? await privacyManager.processData( - anonymizedData, - for: .analytics, - subjectId: nil // Anonymous - ) - } - - private func anonymizeInteraction(_ interaction: UserInteraction) async -> AnonymizedInteraction { - return AnonymizedInteraction( - eventType: interaction.eventType, - timestamp: interaction.timestamp.rounded(to: .hour), // Round to hour - deviceType: UIDevice.current.model, - // Remove all PII - userId: nil, - location: nil, - ipAddress: nil - ) - } -} -``` - -## Code Quality Standards - -### 1. Error Handling - -**Comprehensive Error Management** -```swift -// ✅ Good: Detailed error handling with recovery -enum WidgetError: LocalizedError { - case networkUnavailable - case dataCorrupted(String) - case rateLimitExceeded(retryAfter: TimeInterval) - case authenticationFailed(reason: String) - - var errorDescription: String? { - switch self { - case .networkUnavailable: - return "Network connection is unavailable" - case .dataCorrupted(let details): - return "Data corruption detected: \(details)" - case .rateLimitExceeded(let retryAfter): - return "Rate limit exceeded. Retry after \(retryAfter) seconds" - case .authenticationFailed(let reason): - return "Authentication failed: \(reason)" - } - } - - var recoverySuggestion: String? { - switch self { - case .networkUnavailable: - return "Check your internet connection and try again" - case .dataCorrupted: - return "Clear cache and reload data" - case .rateLimitExceeded: - return "Wait before making additional requests" - case .authenticationFailed: - return "Please sign in again" - } - } - - var shouldRetry: Bool { - switch self { - case .networkUnavailable, .rateLimitExceeded: - return true - case .dataCorrupted, .authenticationFailed: - return false - } - } -} - -class WidgetService { - func fetchData() async throws -> WidgetData { - do { - return try await performFetch() - } catch let error as WidgetError { - // Handle specific widget errors - if error.shouldRetry { - // Implement retry logic - return try await retryFetch() - } - - // Log error with context - logger.error("Widget fetch failed: \(error.localizedDescription)") - - // Send to crash reporting - CrashReporter.shared.recordError(error) - - throw error - } catch { - // Handle unexpected errors - logger.error("Unexpected error: \(error)") - throw WidgetError.dataCorrupted("Unexpected error occurred") - } - } -} -``` - -### 2. Async/Await Best Practices - -**Proper Concurrency Usage** -```swift -// ✅ Good: Efficient concurrent operations -class DataLoadingManager { - func loadAllData() async throws -> CombinedData { - // Load data concurrently - async let weatherData = loadWeatherData() - async let calendarData = loadCalendarData() - async let newsData = loadNewsData() - - // Use TaskGroup for dynamic concurrency - let additionalData = try await withThrowingTaskGroup(of: (String, Data).self) { group in - let endpoints = getAdditionalEndpoints() - - for endpoint in endpoints { - group.addTask { - let data = try await self.fetchData(from: endpoint) - return (endpoint.id, data) - } - } - - var results: [String: Data] = [:] - for try await (id, data) in group { - results[id] = data - } - return results - } - - // Combine all results - return try await CombinedData( - weather: weatherData, - calendar: calendarData, - news: newsData, - additional: additionalData - ) - } - - @MainActor - func updateUI(with data: CombinedData) { - // UI updates on main actor - weatherView.update(with: data.weather) - calendarView.update(with: data.calendar) - newsView.update(with: data.news) - } -} -``` - -### 3. Code Documentation - -**Comprehensive Documentation** -```swift -/// Manages weather data fetching and caching for weather widgets. -/// -/// This class provides a thread-safe interface for loading weather data -/// from various sources with intelligent caching and error recovery. -/// -/// Example usage: -/// ```swift -/// let manager = WeatherDataManager() -/// let weather = try await manager.fetchWeather(for: location) -/// ``` -/// -/// - Important: This class requires network permissions and location access. -/// - Note: All methods are thread-safe and can be called from any queue. -public class WeatherDataManager { - - /// Fetches weather data for the specified location. - /// - /// This method first checks the cache for valid data. If no cached data is available - /// or the cache has expired, it fetches fresh data from the network. - /// - /// - Parameters: - /// - location: The geographic location for weather data - /// - useCache: Whether to use cached data if available (default: true) - /// - forceRefresh: Force a network request even if cache is valid (default: false) - /// - /// - Returns: Current weather data for the location - /// - /// - Throws: - /// - `WeatherError.networkUnavailable` if no network connection - /// - `WeatherError.invalidLocation` if location is invalid - /// - `WeatherError.rateLimitExceeded` if API rate limit is hit - /// - /// - Complexity: O(1) if cached, O(n) for network request where n is payload size - /// - Performance: Typically completes in 50-200ms for cached, 200-1000ms for network - public func fetchWeather( - for location: CLLocation, - useCache: Bool = true, - forceRefresh: Bool = false - ) async throws -> WeatherData { - // Implementation - } -} -``` - -## Testing Best Practices - -### 1. Unit Testing - -**Testable Code Structure** -```swift -// ✅ Good: Testable with dependency injection -class WeatherViewModel { - private let weatherService: WeatherServiceProtocol - private let formatter: WeatherFormatterProtocol - private let cache: CacheProtocol - - init( - weatherService: WeatherServiceProtocol, - formatter: WeatherFormatterProtocol, - cache: CacheProtocol = CacheManager.shared - ) { - self.weatherService = weatherService - self.formatter = formatter - self.cache = cache - } - - func loadWeather() async throws -> String { - // Check cache first - if let cachedWeather = await cache.get(WeatherData.self, forKey: "weather") { - return formatter.format(cachedWeather) - } - - // Fetch from service - let weather = try await weatherService.fetchWeather() - - // Cache the result - await cache.set(weather, forKey: "weather") - - return formatter.format(weather) - } -} - -// Test -class WeatherViewModelTests: XCTestCase { - func testLoadWeatherFromCache() async throws { - // Given - let mockService = MockWeatherService() - let mockFormatter = MockWeatherFormatter() - let mockCache = MockCache() - - let cachedWeather = WeatherData.mock() - mockCache.mockData["weather"] = cachedWeather - mockFormatter.formattedString = "Sunny, 25°C" - - let viewModel = WeatherViewModel( - weatherService: mockService, - formatter: mockFormatter, - cache: mockCache - ) - - // When - let result = try await viewModel.loadWeather() - - // Then - XCTAssertEqual(result, "Sunny, 25°C") - XCTAssertFalse(mockService.fetchWeatherCalled) // Should use cache - XCTAssertTrue(mockFormatter.formatCalled) - } -} -``` - -### 2. Integration Testing - -**End-to-End Testing** -```swift -// ✅ Good: Comprehensive integration test -class WidgetIntegrationTests: XCTestCase { - var widgetManager: AdvancedWidgetManager! - - override func setUp() async throws { - try await super.setUp() - widgetManager = AdvancedWidgetManager.shared - try await widgetManager.initialize() - } - - func testCompleteWidgetLifecycle() async throws { - // Given - let configuration = WidgetConfiguration( - type: .weather, - size: .medium, - displayName: "Test Weather Widget", - updateInterval: 300 - ) - - // When - Create widget - let widget = try await widgetManager.createWidget(with: configuration) - - // Then - Verify creation - XCTAssertNotNil(widget) - XCTAssertEqual(widget.state, .active) - - // When - Update widget - let testData = WeatherData.mock() - try await widgetManager.updateWidget(widget.id, with: testData) - - // Then - Verify update - XCTAssertEqual(widget.data as? WeatherData, testData) - XCTAssertGreaterThan(widget.metrics.updateCount, 0) - - // When - Check health - let healthReport = await widgetManager.performHealthCheck() - - // Then - Verify health - XCTAssertGreaterThan(healthReport.totalScore, 0.8) - XCTAssertTrue(healthReport.unhealthyWidgets.isEmpty) - - // Performance assertions - XCTAssertLessThan(widget.metrics.loadTime, 0.5) // 500ms max - XCTAssertLessThan(widget.metrics.renderTime, 0.1) // 100ms max - XCTAssertLessThan(widget.metrics.memoryUsage, 50_000_000) // 50MB max - - // When - Delete widget - try await widgetManager.deleteWidget(widget.id) - - // Then - Verify deletion - XCTAssertNil(widgetManager.getWidget(widget.id)) - } -} -``` - -## Accessibility Best Practices - -### 1. VoiceOver Support - -```swift -// ✅ Good: Comprehensive accessibility -struct AccessibleWeatherWidget: View { - let weatherData: WeatherData - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - // Temperature with proper accessibility - HStack { - Image(systemName: weatherData.iconName) - .font(.largeTitle) - .accessibilityLabel(weatherData.conditionDescription) - - Text("\(weatherData.temperature)°") - .font(.system(size: 48, weight: .bold)) - .accessibilityLabel("Temperature \(weatherData.temperature) degrees") - .accessibilityHint("Current temperature in \(weatherData.location)") - } - .accessibilityElement(children: .combine) - - // Forecast with accessibility - HStack { - ForEach(weatherData.forecast, id: \.date) { day in - VStack { - Text(day.dayAbbreviation) - .font(.caption) - - Image(systemName: day.iconName) - .font(.title3) - - Text("\(day.high)°") - .font(.caption) - } - .accessibilityElement(children: .combine) - .accessibilityLabel("\(day.dayName): \(day.conditionDescription), high of \(day.high) degrees") - } - } - } - .accessibilityElement(children: .contain) - .accessibilityLabel("Weather widget") - .accessibilityHint("Shows current weather and forecast for \(weatherData.location)") - .accessibilityAddTraits(.isButton) - .accessibilityAction { - // Open weather app - openWeatherApp() - } - } -} -``` - -### 2. Dynamic Type Support - -```swift -// ✅ Good: Responsive to text size changes -struct DynamicTypeWidget: View { - @Environment(\.sizeCategory) var sizeCategory - let data: WidgetData - - var body: some View { - VStack(spacing: spacing) { - Text(data.title) - .font(.system(.title2, design: .rounded)) - .lineLimit(titleLineLimit) - .minimumScaleFactor(0.7) - - Text(data.description) - .font(.system(.body)) - .lineLimit(descriptionLineLimit) - .fixedSize(horizontal: false, vertical: true) - } - .padding(padding) - } - - private var spacing: CGFloat { - sizeCategory.isAccessibilityCategory ? 16 : 12 - } - - private var padding: CGFloat { - sizeCategory.isAccessibilityCategory ? 20 : 16 - } - - private var titleLineLimit: Int { - sizeCategory.isAccessibilityCategory ? 3 : 2 - } - - private var descriptionLineLimit: Int { - sizeCategory.isAccessibilityCategory ? 5 : 3 - } -} -``` - -## Localization Best Practices - -### 1. Internationalization - -```swift -// ✅ Good: Proper localization -struct LocalizedStrings { - static func temperature(_ value: Double, unit: TemperatureUnit = .celsius) -> String { - let measurement = Measurement(value: value, unit: unit.unitTemperature) - let formatter = MeasurementFormatter() - formatter.locale = Locale.current - formatter.unitStyle = .short - return formatter.string(from: measurement) - } - - static var loadingMessage: String { - NSLocalizedString( - "widget.loading.message", - tableName: "Widget", - bundle: .main, - value: "Loading...", - comment: "Message shown while widget is loading data" - ) - } - - static func errorMessage(_ error: WidgetError) -> String { - let key = "widget.error.\(error.errorCode)" - return NSLocalizedString( - key, - tableName: "Widget", - bundle: .main, - value: error.localizedDescription, - comment: "Error message for widget error \(error.errorCode)" - ) - } -} - -// Localizable.strings (en) -"widget.loading.message" = "Loading..."; -"widget.error.network" = "Network connection unavailable"; -"widget.error.data" = "Unable to load data"; - -// Localizable.strings (es) -"widget.loading.message" = "Cargando..."; -"widget.error.network" = "Conexión de red no disponible"; -"widget.error.data" = "No se pueden cargar los datos"; -``` - -## Deployment & Monitoring - -### 1. Production Configuration - -```swift -// ✅ Good: Environment-specific configuration -enum Environment { - case development - case staging - case production - - static var current: Environment { - #if DEBUG - return .development - #elseif STAGING - return .staging - #else - return .production - #endif - } - - var apiBaseURL: String { - switch self { - case .development: - return "https://dev-api.example.com" - case .staging: - return "https://staging-api.example.com" - case .production: - return "https://api.example.com" - } - } - - var configuration: WidgetConfiguration { - switch self { - case .development: - return WidgetConfiguration( - updateInterval: 60, // 1 minute for testing - enableDebugLogging: true, - enableCrashReporting: false - ) - case .staging: - return WidgetConfiguration( - updateInterval: 300, // 5 minutes - enableDebugLogging: true, - enableCrashReporting: true - ) - case .production: - return WidgetConfiguration( - updateInterval: 900, // 15 minutes - enableDebugLogging: false, - enableCrashReporting: true - ) - } - } -} -``` - -### 2. Monitoring & Analytics - -```swift -// ✅ Good: Comprehensive monitoring -class WidgetMonitor { - static let shared = WidgetMonitor() - private let analytics = AdvancedAnalytics.shared - - func trackWidgetLifecycle(_ widget: Widget, event: WidgetLifecycleEvent) { - let eventData = AnalyticsEvent( - name: "widget_lifecycle", - parameters: [ - "widget_id": widget.id, - "widget_type": widget.configuration.type.rawValue, - "event": event.rawValue, - "timestamp": Date().timeIntervalSince1970 - ] - ) - - Task { - await analytics.track(eventData) - } - } - - func trackPerformance(_ widget: Widget) { - guard widget.metrics.renderTime > 0 else { return } - - let performanceData = AnalyticsEvent( - name: "widget_performance", - parameters: [ - "widget_id": widget.id, - "load_time": widget.metrics.loadTime, - "render_time": widget.metrics.renderTime, - "memory_usage": widget.metrics.memoryUsage, - "update_count": widget.metrics.updateCount, - "error_count": widget.metrics.errorCount - ] - ) - - Task { - await analytics.track(performanceData) - - // Alert if performance degraded - if widget.metrics.renderTime > 0.1 { - await sendPerformanceAlert(widget) - } - } - } - - private func sendPerformanceAlert(_ widget: Widget) async { - let alert = PerformanceAlert( - widgetId: widget.id, - metric: "render_time", - value: widget.metrics.renderTime, - threshold: 0.1, - severity: .warning - ) - - await analytics.sendAlert(alert) - } -} -``` - -## Documentation Standards - -### 1. API Documentation - -```swift -/// A comprehensive widget management system for iOS applications. -/// -/// `AdvancedWidgetManager` provides enterprise-grade functionality for creating, -/// managing, and optimizing iOS widgets with support for templates, orchestration, -/// and performance monitoring. -/// -/// ## Overview -/// -/// The manager handles the complete lifecycle of widgets including: -/// - Creation and configuration -/// - Template management -/// - Performance optimization -/// - Health monitoring -/// - Orchestration of multiple widgets -/// -/// ## Usage -/// -/// ```swift -/// let manager = AdvancedWidgetManager.shared -/// try await manager.initialize() -/// -/// let configuration = WidgetConfiguration( -/// type: .weather, -/// size: .medium, -/// displayName: "My Weather" -/// ) -/// -/// let widget = try await manager.createWidget(with: configuration) -/// ``` -/// -/// ## Performance Considerations -/// -/// The manager automatically optimizes widget performance by: -/// - Implementing intelligent caching strategies -/// - Batching network requests -/// - Managing memory usage -/// - Scheduling updates based on system conditions -/// -/// - Note: All methods are thread-safe and can be called from any queue -/// - Important: Requires iOS 16.0 or later -/// - Warning: Widget quota is limited to 100 widgets per application -public class AdvancedWidgetManager { - // Implementation -} -``` - -## Conclusion - -Following these enterprise best practices ensures that your iOS widgets are: - -- **Performant**: Sub-100ms response times with optimized memory usage -- **Secure**: Enterprise-grade encryption and privacy compliance -- **Maintainable**: Clean architecture with comprehensive testing -- **Accessible**: Full VoiceOver support and dynamic type compatibility -- **Scalable**: Proper error handling and monitoring for production deployment -- **Compliant**: GDPR, HIPAA, and industry standard compliance - -Regular code reviews, automated testing, and continuous monitoring are essential for maintaining these standards in production environments. Always prioritize user experience, security, and performance in your widget development process. \ No newline at end of file diff --git a/Documentation/ConfigurationAPI.md b/Documentation/ConfigurationAPI.md deleted file mode 100644 index 9e6e2cf..0000000 --- a/Documentation/ConfigurationAPI.md +++ /dev/null @@ -1,558 +0,0 @@ -# Configuration API - - -## Table of Contents -- [Configuration API](#configuration-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [WidgetConfiguration](#widgetconfiguration) - - [DataIntegrationConfiguration](#dataintegrationconfiguration) - - [PerformanceConfiguration](#performanceconfiguration) -- [Widget Configuration](#widget-configuration) - - [Basic Widget Configuration](#basic-widget-configuration) - - [Advanced Widget Configuration](#advanced-widget-configuration) -- [Data Integration Configuration](#data-integration-configuration) - - [Real-Time Updates Configuration](#real-time-updates-configuration) - - [Caching Configuration](#caching-configuration) - - [Network Configuration](#network-configuration) -- [Performance Configuration](#performance-configuration) - - [Memory Management](#memory-management) - - [Rendering Configuration](#rendering-configuration) - - [Background Processing](#background-processing) -- [Security Configuration](#security-configuration) - - [Encryption Configuration](#encryption-configuration) - - [Authentication Configuration](#authentication-configuration) - - [Certificate Pinning](#certificate-pinning) -- [Analytics Configuration](#analytics-configuration) - - [Usage Analytics](#usage-analytics) - - [Error Reporting](#error-reporting) -- [Localization Configuration](#localization-configuration) - - [Language Support](#language-support) - - [Regional Settings](#regional-settings) -- [Accessibility Configuration](#accessibility-configuration) - - [Accessibility Settings](#accessibility-settings) - - [Custom Accessibility](#custom-accessibility) -- [Configuration Management](#configuration-management) - - [Configuration Persistence](#configuration-persistence) - - [Configuration Validation](#configuration-validation) -- [Error Handling](#error-handling) - - [Configuration Errors](#configuration-errors) - - [Error Handling Example](#error-handling-example) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Best Practices](#best-practices) - - [Configuration Management](#configuration-management) - - [Performance Optimization](#performance-optimization) - - [Security Considerations](#security-considerations) - - - -## Overview - -The Configuration API provides comprehensive functionality for configuring widgets, data sources, and system settings in iOS applications using the iOS Widget Development Kit. This API enables developers to customize widget behavior, appearance, and functionality through various configuration options. - -## Core Classes - -### WidgetConfiguration - -The main configuration class for widget settings. - -```swift -class WidgetConfiguration { - var enableHomeScreenWidgets: Bool - var enableLockScreenWidgets: Bool - var enableLiveActivities: Bool - var enableDynamicIsland: Bool - var defaultRefreshInterval: TimeInterval - var enableBackgroundUpdates: Bool - var enableInteractions: Bool - var enableDeepLinking: Bool - var enableDynamicColors: Bool - var enableDarkMode: Bool - var enableCustomFonts: Bool - var enableAnimations: Bool -} -``` - -### DataIntegrationConfiguration - -Configuration options for data integration. - -```swift -struct DataIntegrationConfiguration { - var enableRealTimeUpdates: Bool - var enableBackgroundSync: Bool - var enableCaching: Bool - var refreshInterval: TimeInterval - var cachePolicy: CachePolicy - var enableCompression: Bool - var enableEncryption: Bool -} -``` - -### PerformanceConfiguration - -Configuration options for performance optimization. - -```swift -struct PerformanceConfiguration { - var enableHardwareAcceleration: Bool - var enableMemoryOptimization: Bool - var enableRenderingOptimization: Bool - var maxMemoryUsage: Int64 - var enableBackgroundProcessing: Bool - var enableCaching: Bool -} -``` - -## Widget Configuration - -### Basic Widget Configuration - -```swift -// Create basic widget configuration -let widgetConfig = WidgetConfiguration() -widgetConfig.enableHomeScreenWidgets = true -widgetConfig.enableLockScreenWidgets = true -widgetConfig.enableLiveActivities = true -widgetConfig.enableDynamicIsland = true -widgetConfig.defaultRefreshInterval = 300 // 5 minutes -widgetConfig.enableBackgroundUpdates = true -widgetConfig.enableInteractions = true -widgetConfig.enableDeepLinking = true - -// Apply configuration -let widgetManager = WidgetDevelopmentManager.shared -widgetManager.configure(widgetConfig) -``` - -### Advanced Widget Configuration - -```swift -// Create advanced widget configuration -let advancedConfig = WidgetConfiguration() -advancedConfig.enableDynamicColors = true -advancedConfig.enableDarkMode = true -advancedConfig.enableCustomFonts = true -advancedConfig.enableAnimations = true -advancedConfig.enableAccessibility = true -advancedConfig.enableLocalization = true -advancedConfig.enableAnalytics = true -advancedConfig.enableErrorReporting = true - -// Apply advanced configuration -widgetManager.configureAdvanced(advancedConfig) -``` - -## Data Integration Configuration - -### Real-Time Updates Configuration - -```swift -// Configure real-time updates -let realTimeConfig = RealTimeConfiguration() -realTimeConfig.enableWebSocket = true -realTimeConfig.enablePushNotifications = true -realTimeConfig.enableBackgroundUpdates = true -realTimeConfig.updateInterval = 30 // 30 seconds -realTimeConfig.maxRetryAttempts = 3 -realTimeConfig.retryInterval = 5 // 5 seconds - -// Apply real-time configuration -let dataManager = WidgetDataManager.shared -dataManager.configureRealTimeUpdates(realTimeConfig) -``` - -### Caching Configuration - -```swift -// Configure caching -let cacheConfig = CacheConfiguration() -cacheConfig.enableCaching = true -cacheConfig.maxCacheSize = 100 * 1024 * 1024 // 100 MB -cacheConfig.cacheExpiration = 3600 // 1 hour -cacheConfig.enableCompression = true -cacheConfig.enableEncryption = true - -// Apply cache configuration -dataManager.configureCaching(cacheConfig) -``` - -### Network Configuration - -```swift -// Configure network settings -let networkConfig = NetworkConfiguration() -networkConfig.timeout = 30 // 30 seconds -networkConfig.retryCount = 3 -networkConfig.enableSSLVerification = true -networkConfig.enableCertificatePinning = true -networkConfig.enableCompression = true - -// Apply network configuration -dataManager.configureNetwork(networkConfig) -``` - -## Performance Configuration - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.maxMemoryUsage = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableMemoryMonitoring = true -memoryConfig.enableAutomaticCleanup = true -memoryConfig.cleanupInterval = 300 // 5 minutes - -// Apply memory configuration -let performanceManager = WidgetPerformanceManager.shared -performanceManager.configureMemory(memoryConfig) -``` - -### Rendering Configuration - -```swift -// Configure rendering -let renderingConfig = RenderingConfiguration() -renderingConfig.enableHardwareAcceleration = true -renderingConfig.enableLayerBacking = true -renderingConfig.enableOpaqueLayers = true -renderingConfig.enableAntiAliasing = true - -// Apply rendering configuration -performanceManager.configureRendering(renderingConfig) -``` - -### Background Processing - -```swift -// Configure background processing -let backgroundConfig = BackgroundProcessingConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes -backgroundConfig.enableBackgroundFetching = true -backgroundConfig.enableBackgroundUpdates = true - -// Apply background configuration -performanceManager.configureBackgroundProcessing(backgroundConfig) -``` - -## Security Configuration - -### Encryption Configuration - -```swift -// Configure encryption -let encryptionConfig = EncryptionConfiguration() -encryptionConfig.enableEncryption = true -encryptionConfig.algorithm = .aes256 -encryptionConfig.keySize = 256 -encryptionConfig.enableKeyRotation = true - -// Apply encryption configuration -let securityManager = WidgetSecurityManager.shared -securityManager.configureEncryption(encryptionConfig) -``` - -### Authentication Configuration - -```swift -// Configure authentication -let authConfig = AuthenticationConfiguration() -authConfig.enableAuthentication = true -authConfig.authenticationType = .bearer -authConfig.tokenRefreshInterval = 3600 // 1 hour -authConfig.enableAutoRefresh = true - -// Apply authentication configuration -securityManager.configureAuthentication(authConfig) -``` - -### Certificate Pinning - -```swift -// Configure certificate pinning -let certificateConfig = CertificateConfiguration() -certificateConfig.enableCertificatePinning = true -certificateConfig.pinnedCertificates = ["cert1", "cert2"] -certificateConfig.enableSSLVerification = true -certificateConfig.enableHostValidation = true - -// Apply certificate configuration -securityManager.configureCertificates(certificateConfig) -``` - -## Analytics Configuration - -### Usage Analytics - -```swift -// Configure usage analytics -let analyticsConfig = AnalyticsConfiguration() -analyticsConfig.enableUsageTracking = true -analyticsConfig.enablePerformanceTracking = true -analyticsConfig.enableErrorTracking = true -analyticsConfig.enableCrashReporting = true - -// Apply analytics configuration -let analyticsManager = WidgetAnalyticsManager.shared -analyticsManager.configure(analyticsConfig) -``` - -### Error Reporting - -```swift -// Configure error reporting -let errorConfig = ErrorReportingConfiguration() -errorConfig.enableErrorReporting = true -errorConfig.enableCrashReporting = true -errorConfig.enablePerformanceMonitoring = true -errorConfig.enableUserFeedback = true - -// Apply error reporting configuration -analyticsManager.configureErrorReporting(errorConfig) -``` - -## Localization Configuration - -### Language Support - -```swift -// Configure localization -let localizationConfig = LocalizationConfiguration() -localizationConfig.enableLocalization = true -localizationConfig.supportedLanguages = ["en", "es", "fr", "de"] -localizationConfig.defaultLanguage = "en" -localizationConfig.enableAutoDetection = true - -// Apply localization configuration -let localizationManager = WidgetLocalizationManager.shared -localizationManager.configure(localizationConfig) -``` - -### Regional Settings - -```swift -// Configure regional settings -let regionalConfig = RegionalConfiguration() -regionalConfig.enableRegionalSettings = true -regionalConfig.dateFormat = "MM/dd/yyyy" -regionalConfig.timeFormat = "HH:mm" -regionalConfig.currencyCode = "USD" -regionalConfig.timeZone = "America/New_York" - -// Apply regional configuration -localizationManager.configureRegional(regionalConfig) -``` - -## Accessibility Configuration - -### Accessibility Settings - -```swift -// Configure accessibility -let accessibilityConfig = AccessibilityConfiguration() -accessibilityConfig.enableVoiceOver = true -accessibilityConfig.enableDynamicType = true -accessibilityConfig.enableHighContrast = true -accessibilityConfig.enableReduceMotion = true - -// Apply accessibility configuration -let accessibilityManager = WidgetAccessibilityManager.shared -accessibilityManager.configure(accessibilityConfig) -``` - -### Custom Accessibility - -```swift -// Configure custom accessibility -let customAccessibilityConfig = CustomAccessibilityConfiguration() -customAccessibilityConfig.enableCustomLabels = true -customAccessibilityConfig.enableCustomHints = true -customAccessibilityConfig.enableCustomActions = true - -// Apply custom accessibility configuration -accessibilityManager.configureCustom(customAccessibilityConfig) -``` - -## Configuration Management - -### Configuration Persistence - -```swift -// Configure persistence -let persistenceConfig = PersistenceConfiguration() -persistenceConfig.enableConfigurationPersistence = true -persistenceConfig.storageType = .userDefaults -persistenceConfig.enableEncryption = true -persistenceConfig.enableBackup = true - -// Apply persistence configuration -let configManager = WidgetConfigurationManager.shared -configManager.configurePersistence(persistenceConfig) -``` - -### Configuration Validation - -```swift -// Configure validation -let validationConfig = ValidationConfiguration() -validationConfig.enableConfigurationValidation = true -validationConfig.enableSchemaValidation = true -validationConfig.enableTypeChecking = true -validationConfig.enableConstraintValidation = true - -// Apply validation configuration -configManager.configureValidation(validationConfig) -``` - -## Error Handling - -### Configuration Errors - -```swift -enum ConfigurationError: Error { - case invalidConfiguration(String) - case missingRequiredParameter(String) - case incompatibleSettings(String) - case validationFailed(String) - case persistenceError(String) -} -``` - -### Error Handling Example - -```swift -// Handle configuration errors -func handleConfigurationError(_ error: ConfigurationError) { - switch error { - case .invalidConfiguration(let message): - print("Invalid configuration: \(message)") - // Reset to default configuration - - case .missingRequiredParameter(let parameter): - print("Missing required parameter: \(parameter)") - // Add missing parameter - - case .incompatibleSettings(let settings): - print("Incompatible settings: \(settings)") - // Resolve conflicts - - case .validationFailed(let reason): - print("Validation failed: \(reason)") - // Fix validation issues - - case .persistenceError(let message): - print("Persistence error: \(message)") - // Handle persistence issues - } -} -``` - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `configure` | Configure widget settings | `WidgetConfiguration` | `Result` | -| `configureData` | Configure data integration | `DataIntegrationConfiguration` | `Result` | -| `configurePerformance` | Configure performance settings | `PerformanceConfiguration` | `Result` | -| `configureSecurity` | Configure security settings | `SecurityConfiguration` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `enableHomeScreenWidgets` | `Bool` | Enable home screen widgets | -| `enableLockScreenWidgets` | `Bool` | Enable lock screen widgets | -| `enableLiveActivities` | `Bool` | Enable Live Activities | -| `enableDynamicIsland` | `Bool` | Enable Dynamic Island | -| `defaultRefreshInterval` | `TimeInterval` | Default refresh interval | -| `enableBackgroundUpdates` | `Bool` | Enable background updates | -| `enableInteractions` | `Bool` | Enable widget interactions | -| `enableDeepLinking` | `Bool` | Enable deep linking | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldConfig = WidgetConfiguration() -oldConfig.enableWidgets = true - -// iOS 15+ approach -let newConfig = WidgetConfiguration() -newConfig.enableHomeScreenWidgets = true -newConfig.enableLockScreenWidgets = true -newConfig.enableLiveActivities = true -newConfig.enableDynamicIsland = true -``` - -## Troubleshooting - -### Common Issues - -1. **Configuration Not Applied** - - Check configuration validity - - Verify parameter types - - Test on physical device - -2. **Performance Issues** - - Optimize configuration settings - - Monitor memory usage - - Check background processing - -3. **Security Issues** - - Verify certificate pinning - - Check encryption settings - - Validate authentication - -### Debug Tools - -```swift -// Enable configuration debugging -let debugLogger = ConfigurationDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor configuration changes -debugLogger.onConfigurationChanged = { config in - print("Configuration changed: \(config)") -} - -debugLogger.onConfigurationError = { error in - print("Configuration error: \(error)") -} -``` - -## Best Practices - -### Configuration Management - -1. **Validate Configurations**: Always validate configurations before applying -2. **Use Default Values**: Provide sensible default values -3. **Handle Errors**: Implement proper error handling -4. **Test Configurations**: Test configurations on multiple devices - -### Performance Optimization - -1. **Minimize Overhead**: Keep configuration overhead minimal -2. **Cache Configurations**: Cache frequently used configurations -3. **Lazy Loading**: Load configurations on demand -4. **Background Processing**: Use background processing for heavy operations - -### Security Considerations - -1. **Encrypt Sensitive Data**: Encrypt sensitive configuration data -2. **Validate Inputs**: Validate all configuration inputs -3. **Use Secure Storage**: Use secure storage for sensitive configurations -4. **Regular Updates**: Regularly update security configurations diff --git a/Documentation/CustomizationAPI.md b/Documentation/CustomizationAPI.md deleted file mode 100644 index 6deb45b..0000000 --- a/Documentation/CustomizationAPI.md +++ /dev/null @@ -1,695 +0,0 @@ -# Customization API - - -## Table of Contents -- [Customization API](#customization-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [WidgetCustomizationManager](#widgetcustomizationmanager) - - [WidgetStyle](#widgetstyle) - - [WidgetTheme](#widgettheme) -- [Color Customization](#color-customization) - - [Color Schemes](#color-schemes) - - [Dynamic Colors](#dynamic-colors) - - [Brand Colors](#brand-colors) -- [Font Customization](#font-customization) - - [Custom Fonts](#custom-fonts) - - [Dynamic Type](#dynamic-type) - - [Font Scaling](#font-scaling) -- [Layout Customization](#layout-customization) - - [Custom Layouts](#custom-layouts) - - [Responsive Design](#responsive-design) - - [Grid Layouts](#grid-layouts) -- [Animation Customization](#animation-customization) - - [Basic Animations](#basic-animations) - - [Custom Animations](#custom-animations) - - [Transition Effects](#transition-effects) - - [Spring Animations](#spring-animations) -- [Theming System](#theming-system) - - [Theme Creation](#theme-creation) - - [Theme Switching](#theme-switching) - - [Theme Persistence](#theme-persistence) -- [Interactive Elements](#interactive-elements) - - [Custom Buttons](#custom-buttons) - - [Gesture Recognition](#gesture-recognition) - - [Haptic Feedback](#haptic-feedback) -- [Content Customization](#content-customization) - - [Dynamic Content](#dynamic-content) - - [Conditional Styling](#conditional-styling) - - [Content Templates](#content-templates) -- [Accessibility](#accessibility) - - [Accessibility Configuration](#accessibility-configuration) - - [Custom Accessibility Labels](#custom-accessibility-labels) - - [Accessibility Actions](#accessibility-actions) -- [Performance Optimization](#performance-optimization) - - [Rendering Optimization](#rendering-optimization) - - [Memory Management](#memory-management) -- [Error Handling](#error-handling) - - [CustomizationError Types](#customizationerror-types) - - [Error Handling Example](#error-handling-example) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Best Practices](#best-practices) - - [Design Principles](#design-principles) - - [Implementation](#implementation) - - - -## Overview - -The Customization API provides comprehensive functionality for customizing widgets in iOS applications using the iOS Widget Development Kit. This API enables developers to create unique, branded, and visually appealing widgets through various customization options including colors, fonts, layouts, and animations. - -## Core Classes - -### WidgetCustomizationManager - -The main manager class for handling customization operations. - -```swift -class WidgetCustomizationManager { - static let shared = WidgetCustomizationManager() - - func applyStyle(_ style: WidgetStyle, to widget: Widget) -> Result - func applyTheme(_ theme: WidgetTheme, to widget: Widget) -> Result - func applyAnimation(_ animation: WidgetAnimation, to widget: Widget) -> Result - func applyLayout(_ layout: WidgetLayout, to widget: Widget) -> Result -} -``` - -### WidgetStyle - -The style class for widget appearance customization. - -```swift -struct WidgetStyle { - let backgroundColor: Color - let textColor: Color - let cornerRadius: CGFloat - let shadow: WidgetShadow? - let border: WidgetBorder? - - init(backgroundColor: Color, textColor: Color, cornerRadius: CGFloat) -} -``` - -### WidgetTheme - -The theme class for comprehensive widget theming. - -```swift -struct WidgetTheme { - let name: String - let colors: WidgetColorScheme - let fonts: CustomFontConfiguration - let layout: WidgetLayout - let animations: WidgetAnimationConfiguration - - init(name: String, colors: WidgetColorScheme) -} -``` - -## Color Customization - -### Color Schemes - -```swift -// Create custom color scheme -let customColorScheme = WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .systemBackground, - text: .primary -) - -// Apply color scheme -let stylingManager = WidgetStylingManager() -stylingManager.applyColorScheme(customColorScheme, to: widget) -``` - -### Dynamic Colors - -```swift -// Create dynamic color scheme -let dynamicColorScheme = DynamicColorScheme( - lightMode: WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .white, - text: .black - ), - darkMode: WidgetColorScheme( - primary: .cyan, - secondary: .lightGray, - accent: .yellow, - background: .black, - text: .white - ) -) - -// Apply dynamic colors -stylingManager.applyDynamicColorScheme(dynamicColorScheme, to: widget) -``` - -### Brand Colors - -```swift -// Create brand color scheme -let brandColorScheme = BrandColorScheme( - primary: Color(red: 0.2, green: 0.6, blue: 1.0), - secondary: Color(red: 0.1, green: 0.3, blue: 0.8), - accent: Color(red: 1.0, green: 0.8, blue: 0.0), - background: Color(red: 0.95, green: 0.95, blue: 0.95) -) - -// Apply brand colors -stylingManager.applyBrandColorScheme(brandColorScheme, to: widget) -``` - -## Font Customization - -### Custom Fonts - -```swift -// Create custom font configuration -let fontConfig = CustomFontConfiguration() -fontConfig.titleFont = .custom("Helvetica-Bold", size: 18) -fontConfig.subtitleFont = .custom("Helvetica", size: 14) -fontConfig.bodyFont = .custom("Helvetica", size: 12) -fontConfig.captionFont = .custom("Helvetica-Light", size: 10) - -// Apply custom fonts -let fontManager = CustomFontManager() -fontManager.applyFontConfiguration(fontConfig, to: widget) -``` - -### Dynamic Type - -```swift -// Configure dynamic type -let dynamicTypeConfig = DynamicTypeConfiguration() -dynamicTypeConfig.enableDynamicType = true -dynamicTypeConfig.scaleFactor = 1.0 -dynamicTypeConfig.maximumScaleFactor = 2.0 - -// Apply dynamic type -fontManager.applyDynamicTypeConfiguration(dynamicTypeConfig, to: widget) -``` - -### Font Scaling - -```swift -// Configure font scaling -let fontScalingConfig = FontScalingConfiguration() -fontScalingConfig.enableAutoScaling = true -fontScalingConfig.minimumScaleFactor = 0.5 -fontScalingConfig.maximumScaleFactor = 2.0 - -// Apply font scaling -fontManager.applyFontScalingConfiguration(fontScalingConfig, to: widget) -``` - -## Layout Customization - -### Custom Layouts - -```swift -// Create custom layout -let customLayout = WidgetLayout( - type: .grid, - columns: 2, - spacing: 8, - padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) -) - -// Apply custom layout -let layoutManager = WidgetLayoutManager() -layoutManager.applyLayout(customLayout, to: widget) -``` - -### Responsive Design - -```swift -// Create responsive layout -let responsiveLayout = ResponsiveLayout() -responsiveLayout.configure { config in - config.adaptToScreenSize = true - config.minimumSpacing = 4 - config.maximumSpacing = 16 - config.enableAutoScaling = true -} - -// Apply responsive layout -layoutManager.applyResponsiveLayout(responsiveLayout, to: widget) -``` - -### Grid Layouts - -```swift -// Create grid layout -let gridLayout = GridLayout() -gridLayout.configure { config in - config.columns = 3 - config.spacing = 8 - config.enableEqualSpacing = true - config.enableAutoSizing = true -} - -// Apply grid layout -layoutManager.applyGridLayout(gridLayout, to: widget) -``` - -## Animation Customization - -### Basic Animations - -```swift -// Create animation configuration -let animationConfig = WidgetAnimationConfiguration() -animationConfig.enableAnimations = true -animationConfig.animationDuration = 0.3 -animationConfig.animationCurve = .easeInOut - -// Apply animations -let animationManager = WidgetAnimationManager() -animationManager.applyAnimationConfiguration(animationConfig, to: widget) -``` - -### Custom Animations - -```swift -// Create custom animation -let customAnimation = WidgetAnimation( - type: .fade, - duration: 0.5, - delay: 0.1, - curve: .easeInOut -) - -// Apply custom animation -animationManager.applyCustomAnimation(customAnimation, to: widget) -``` - -### Transition Effects - -```swift -// Create transition effect -let transitionEffect = WidgetTransition( - type: .slide, - direction: .fromRight, - duration: 0.4, - curve: .easeInOut -) - -// Apply transition effect -animationManager.applyTransition(transitionEffect, to: widget) -``` - -### Spring Animations - -```swift -// Create spring animation -let springAnimation = SpringAnimation( - damping: 0.8, - response: 0.6, - blendDuration: 0.3 -) - -// Apply spring animation -animationManager.applySpringAnimation(springAnimation, to: widget) -``` - -## Theming System - -### Theme Creation - -```swift -// Create custom theme -let customTheme = WidgetTheme( - name: "Corporate Theme", - colors: customColorScheme, - fonts: fontConfig, - layout: customLayout, - animations: animationConfig -) - -// Apply custom theme -let themeManager = WidgetThemeManager() -themeManager.applyTheme(customTheme, to: widget) -``` - -### Theme Switching - -```swift -// Create multiple themes -let lightTheme = WidgetTheme(name: "Light", colors: lightColorScheme) -let darkTheme = WidgetTheme(name: "Dark", colors: darkColorScheme) -let customTheme = WidgetTheme(name: "Custom", colors: customColorScheme) - -// Switch themes -themeManager.switchTheme(to: lightTheme, for: widget) { result in - switch result { - case .success: - print("✅ Theme switched successfully") - case .failure(let error): - print("❌ Theme switch failed: \(error)") - } -} -``` - -### Theme Persistence - -```swift -// Configure theme persistence -let themePersistenceConfig = ThemePersistenceConfiguration() -themePersistenceConfig.enablePersistence = true -themePersistenceConfig.storageType = .userDefaults -themePersistenceConfig.enableAutoSave = true - -// Apply theme persistence -themeManager.configurePersistence(themePersistenceConfig) -``` - -## Interactive Elements - -### Custom Buttons - -```swift -// Create custom button style -let customButtonStyle = WidgetButtonStyle( - backgroundColor: .blue, - textColor: .white, - cornerRadius: 8, - padding: EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) -) - -// Apply custom button style -let buttonManager = WidgetButtonManager() -buttonManager.applyButtonStyle(customButtonStyle, to: widget) -``` - -### Gesture Recognition - -```swift -// Add custom gestures -let gestureManager = WidgetGestureManager() - -// Add tap gesture -gestureManager.addTapGesture(to: widget) { context in - print("Widget tapped!") - // Handle tap action -} - -// Add long press gesture -gestureManager.addLongPressGesture(to: widget) { context in - print("Widget long pressed!") - // Handle long press action -} - -// Add swipe gesture -gestureManager.addSwipeGesture(to: widget, direction: .left) { context in - print("Widget swiped left!") - // Handle swipe action -} -``` - -### Haptic Feedback - -```swift -// Configure haptic feedback -let hapticConfig = HapticFeedbackConfiguration() -hapticConfig.enableHapticFeedback = true -hapticConfig.feedbackType = .impact -hapticConfig.intensity = .medium - -// Apply haptic feedback -let hapticManager = WidgetHapticManager() -hapticManager.applyHapticConfiguration(hapticConfig, to: widget) -``` - -## Content Customization - -### Dynamic Content - -```swift -// Create dynamic content configuration -let dynamicContentConfig = DynamicContentConfiguration() -dynamicContentConfig.enableRealTimeUpdates = true -dynamicContentConfig.updateInterval = 60 // 1 minute -dynamicContentConfig.enableConditionalDisplay = true - -// Apply dynamic content -let contentManager = WidgetContentManager() -contentManager.applyDynamicContentConfiguration(dynamicContentConfig, to: widget) -``` - -### Conditional Styling - -```swift -// Create conditional styling -let conditionalStyling = ConditionalStyling() -conditionalStyling.addCondition { data in - return data.value > 100 -} style: { data in - return WidgetStyle(backgroundColor: .red) -} - -conditionalStyling.addCondition { data in - return data.value < 50 -} style: { data in - return WidgetStyle(backgroundColor: .green) -} - -// Apply conditional styling -stylingManager.applyConditionalStyling(conditionalStyling, to: widget) -``` - -### Content Templates - -```swift -// Create content template -let contentTemplate = WidgetContentTemplate( - name: "Weather Template", - layout: weatherLayout, - styling: weatherStyling, - dataMapping: weatherDataMapping -) - -// Apply content template -contentManager.applyContentTemplate(contentTemplate, to: widget) -``` - -## Accessibility - -### Accessibility Configuration - -```swift -// Configure accessibility -let accessibilityConfig = AccessibilityConfiguration() -accessibilityConfig.enableVoiceOver = true -accessibilityConfig.enableDynamicType = true -accessibilityConfig.enableHighContrast = true -accessibilityConfig.enableReduceMotion = true - -// Apply accessibility configuration -let accessibilityManager = WidgetAccessibilityManager() -accessibilityManager.applyAccessibilityConfiguration(accessibilityConfig, to: widget) -``` - -### Custom Accessibility Labels - -```swift -// Add custom accessibility labels -accessibilityManager.addAccessibilityLabel("Weather Widget", to: widget) -accessibilityManager.addAccessibilityHint("Shows current weather information", to: widget) -accessibilityManager.addAccessibilityValue("72°F, Sunny", to: widget) -``` - -### Accessibility Actions - -```swift -// Add accessibility actions -accessibilityManager.addAccessibilityAction("Refresh Weather", to: widget) { context in - // Handle refresh action - refreshWeatherData() -} - -accessibilityManager.addAccessibilityAction("Open App", to: widget) { context in - // Handle open app action - openWeatherApp() -} -``` - -## Performance Optimization - -### Rendering Optimization - -```swift -// Configure rendering optimization -let renderingConfig = RenderingConfiguration() -renderingConfig.enableHardwareAcceleration = true -renderingConfig.enableLayerBacking = true -renderingConfig.enableOpaqueLayers = true - -// Apply rendering optimization -let renderingManager = WidgetRenderingManager() -renderingManager.applyRenderingConfiguration(renderingConfig, to: widget) -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.enableImageCaching = true -memoryConfig.maxCacheSize = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableAutomaticCleanup = true - -// Apply memory management -let memoryManager = WidgetMemoryManager() -memoryManager.applyMemoryConfiguration(memoryConfig, to: widget) -``` - -## Error Handling - -### CustomizationError Types - -```swift -enum CustomizationError: Error { - case styleApplicationFailed(String) - case themeApplicationFailed(String) - case animationApplicationFailed(String) - case layoutApplicationFailed(String) - case fontApplicationFailed(String) - case accessibilityApplicationFailed(String) -} -``` - -### Error Handling Example - -```swift -func handleCustomizationError(_ error: CustomizationError) { - switch error { - case .styleApplicationFailed(let message): - print("Style application failed: \(message)") - // Handle style error - - case .themeApplicationFailed(let message): - print("Theme application failed: \(message)") - // Handle theme error - - case .animationApplicationFailed(let message): - print("Animation application failed: \(message)") - // Handle animation error - - case .layoutApplicationFailed(let message): - print("Layout application failed: \(message)") - // Handle layout error - - case .fontApplicationFailed(let message): - print("Font application failed: \(message)") - // Handle font error - - case .accessibilityApplicationFailed(let message): - print("Accessibility application failed: \(message)") - // Handle accessibility error - } -} -``` - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `applyStyle` | Apply style to widget | `WidgetStyle`, `Widget` | `Result` | -| `applyTheme` | Apply theme to widget | `WidgetTheme`, `Widget` | `Result` | -| `applyAnimation` | Apply animation to widget | `WidgetAnimation`, `Widget` | `Result` | -| `applyLayout` | Apply layout to widget | `WidgetLayout`, `Widget` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `backgroundColor` | `Color` | Widget background color | -| `textColor` | `Color` | Widget text color | -| `cornerRadius` | `CGFloat` | Widget corner radius | -| `shadow` | `WidgetShadow?` | Widget shadow | -| `border` | `WidgetBorder?` | Widget border | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldStyle = WidgetStyle(backgroundColor: .blue) - -// iOS 15+ approach -let newStyle = WidgetStyle( - backgroundColor: .blue, - textColor: .white, - cornerRadius: 12 -) -``` - -## Troubleshooting - -### Common Issues - -1. **Customization Not Applied** - - Check configuration validity - - Verify widget registration - - Test on physical device - -2. **Performance Issues** - - Optimize custom animations - - Reduce complexity - - Monitor memory usage - -3. **Visual Inconsistencies** - - Verify color schemes - - Check font availability - - Test on different devices - -### Debug Tools - -```swift -// Enable customization debugging -let debugLogger = CustomizationDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor customization operations -debugLogger.onCustomizationApplied = { customization in - print("Customization applied: \(customization)") -} - -debugLogger.onCustomizationError = { error in - print("Customization error: \(error)") -} -``` - -## Best Practices - -### Design Principles - -1. **Consistency**: Maintain consistent design language -2. **Accessibility**: Ensure accessibility compliance -3. **Performance**: Optimize for performance -4. **User Experience**: Focus on user experience - -### Implementation - -1. **Modular Design**: Use modular customization components -2. **Reusability**: Create reusable customization patterns -3. **Testing**: Test customizations on multiple devices -4. **Documentation**: Document customization options diff --git a/Documentation/CustomizationGuide.md b/Documentation/CustomizationGuide.md deleted file mode 100644 index 317333c..0000000 --- a/Documentation/CustomizationGuide.md +++ /dev/null @@ -1,649 +0,0 @@ -# Customization Guide - - -## Table of Contents -- [Customization Guide](#customization-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Import the Framework](#1-import-the-framework) - - [2. Initialize Customization Manager](#2-initialize-customization-manager) -- [Visual Customization](#visual-customization) - - [Color Schemes](#color-schemes) - - [Dynamic Colors](#dynamic-colors) - - [Custom Fonts](#custom-fonts) -- [Layout Customization](#layout-customization) - - [Custom Layouts](#custom-layouts) - - [Responsive Design](#responsive-design) -- [Animation and Transitions](#animation-and-transitions) - - [Basic Animations](#basic-animations) - - [Custom Animations](#custom-animations) - - [Transition Effects](#transition-effects) -- [Theming System](#theming-system) - - [Theme Creation](#theme-creation) - - [Theme Switching](#theme-switching) -- [Interactive Elements](#interactive-elements) - - [Custom Buttons](#custom-buttons) - - [Gesture Recognition](#gesture-recognition) -- [Content Customization](#content-customization) - - [Dynamic Content](#dynamic-content) - - [Conditional Styling](#conditional-styling) -- [Accessibility](#accessibility) - - [Accessibility Configuration](#accessibility-configuration) - - [Custom Accessibility Labels](#custom-accessibility-labels) -- [Performance Optimization](#performance-optimization) - - [Rendering Optimization](#rendering-optimization) - - [Memory Management](#memory-management) -- [Branding and Identity](#branding-and-identity) - - [Brand Colors](#brand-colors) - - [Logo Integration](#logo-integration) -- [Advanced Customization](#advanced-customization) - - [Custom Widget Types](#custom-widget-types) - - [Plugin System](#plugin-system) -- [Testing Customizations](#testing-customizations) - - [Unit Testing](#unit-testing) - - [Visual Testing](#visual-testing) -- [Deployment](#deployment) - - [Production Configuration](#production-configuration) - - [Quality Assurance](#quality-assurance) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for customizing widgets in iOS applications using the iOS Widget Development Kit. Customization enables developers to create unique, branded, and visually appealing widgets that match their app's design language and user preferences. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and design principles - -## Getting Started - -### 1. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 2. Initialize Customization Manager - -```swift -let customizationManager = WidgetCustomizationManager.shared -customizationManager.configure { config in - config.enableDynamicColors = true - config.enableCustomFonts = true - config.enableAnimations = true - config.enableTheming = true -} -``` - -## Visual Customization - -### Color Schemes - -```swift -// Create custom color scheme -let customColorScheme = WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .systemBackground, - text: .primary -) - -// Apply color scheme -let stylingManager = WidgetStylingManager() -stylingManager.applyColorScheme(customColorScheme, to: widget) -``` - -### Dynamic Colors - -```swift -// Create dynamic color scheme -let dynamicColorScheme = DynamicColorScheme( - lightMode: WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .white, - text: .black - ), - darkMode: WidgetColorScheme( - primary: .cyan, - secondary: .lightGray, - accent: .yellow, - background: .black, - text: .white - ) -) - -// Apply dynamic colors -stylingManager.applyDynamicColorScheme(dynamicColorScheme, to: widget) -``` - -### Custom Fonts - -```swift -// Create custom font configuration -let fontConfig = CustomFontConfiguration() -fontConfig.titleFont = .custom("Helvetica-Bold", size: 18) -fontConfig.subtitleFont = .custom("Helvetica", size: 14) -fontConfig.bodyFont = .custom("Helvetica", size: 12) -fontConfig.captionFont = .custom("Helvetica-Light", size: 10) - -// Apply custom fonts -let fontManager = CustomFontManager() -fontManager.applyFontConfiguration(fontConfig, to: widget) -``` - -## Layout Customization - -### Custom Layouts - -```swift -// Create custom layout -let customLayout = WidgetLayout( - type: .grid, - columns: 2, - spacing: 8, - padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) -) - -// Apply custom layout -let layoutManager = WidgetLayoutManager() -layoutManager.applyLayout(customLayout, to: widget) -``` - -### Responsive Design - -```swift -// Create responsive layout -let responsiveLayout = ResponsiveLayout() -responsiveLayout.configure { config in - config.adaptToScreenSize = true - config.minimumSpacing = 4 - config.maximumSpacing = 16 - config.enableAutoScaling = true -} - -// Apply responsive layout -layoutManager.applyResponsiveLayout(responsiveLayout, to: widget) -``` - -## Animation and Transitions - -### Basic Animations - -```swift -// Create animation configuration -let animationConfig = WidgetAnimationConfiguration() -animationConfig.enableAnimations = true -animationConfig.animationDuration = 0.3 -animationConfig.animationCurve = .easeInOut - -// Apply animations -let animationManager = WidgetAnimationManager() -animationManager.applyAnimationConfiguration(animationConfig, to: widget) -``` - -### Custom Animations - -```swift -// Create custom animation -let customAnimation = WidgetAnimation( - type: .fade, - duration: 0.5, - delay: 0.1, - curve: .easeInOut -) - -// Apply custom animation -animationManager.applyCustomAnimation(customAnimation, to: widget) -``` - -### Transition Effects - -```swift -// Create transition effect -let transitionEffect = WidgetTransition( - type: .slide, - direction: .fromRight, - duration: 0.4, - curve: .easeInOut -) - -// Apply transition effect -animationManager.applyTransition(transitionEffect, to: widget) -``` - -## Theming System - -### Theme Creation - -```swift -// Create custom theme -let customTheme = WidgetTheme( - name: "Corporate Theme", - colors: customColorScheme, - fonts: fontConfig, - layout: customLayout, - animations: animationConfig -) - -// Apply custom theme -let themeManager = WidgetThemeManager() -themeManager.applyTheme(customTheme, to: widget) -``` - -### Theme Switching - -```swift -// Create multiple themes -let lightTheme = WidgetTheme(name: "Light", colors: lightColorScheme) -let darkTheme = WidgetTheme(name: "Dark", colors: darkColorScheme) -let customTheme = WidgetTheme(name: "Custom", colors: customColorScheme) - -// Switch themes -themeManager.switchTheme(to: lightTheme, for: widget) { result in - switch result { - case .success: - print("✅ Theme switched successfully") - case .failure(let error): - print("❌ Theme switch failed: \(error)") - } -} -``` - -## Interactive Elements - -### Custom Buttons - -```swift -// Create custom button style -let customButtonStyle = WidgetButtonStyle( - backgroundColor: .blue, - textColor: .white, - cornerRadius: 8, - padding: EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) -) - -// Apply custom button style -let buttonManager = WidgetButtonManager() -buttonManager.applyButtonStyle(customButtonStyle, to: widget) -``` - -### Gesture Recognition - -```swift -// Add custom gestures -let gestureManager = WidgetGestureManager() - -// Add tap gesture -gestureManager.addTapGesture(to: widget) { context in - print("Widget tapped!") - // Handle tap action -} - -// Add long press gesture -gestureManager.addLongPressGesture(to: widget) { context in - print("Widget long pressed!") - // Handle long press action -} - -// Add swipe gesture -gestureManager.addSwipeGesture(to: widget, direction: .left) { context in - print("Widget swiped left!") - // Handle swipe action -} -``` - -## Content Customization - -### Dynamic Content - -```swift -// Create dynamic content configuration -let dynamicContentConfig = DynamicContentConfiguration() -dynamicContentConfig.enableRealTimeUpdates = true -dynamicContentConfig.updateInterval = 60 // 1 minute -dynamicContentConfig.enableConditionalDisplay = true - -// Apply dynamic content -let contentManager = WidgetContentManager() -contentManager.applyDynamicContentConfiguration(dynamicContentConfig, to: widget) -``` - -### Conditional Styling - -```swift -// Create conditional styling -let conditionalStyling = ConditionalStyling() -conditionalStyling.addCondition { data in - return data.value > 100 -} style: { data in - return WidgetStyle(backgroundColor: .red) -} - -conditionalStyling.addCondition { data in - return data.value < 50 -} style: { data in - return WidgetStyle(backgroundColor: .green) -} - -// Apply conditional styling -stylingManager.applyConditionalStyling(conditionalStyling, to: widget) -``` - -## Accessibility - -### Accessibility Configuration - -```swift -// Configure accessibility -let accessibilityConfig = AccessibilityConfiguration() -accessibilityConfig.enableVoiceOver = true -accessibilityConfig.enableDynamicType = true -accessibilityConfig.enableHighContrast = true -accessibilityConfig.enableReduceMotion = true - -// Apply accessibility configuration -let accessibilityManager = WidgetAccessibilityManager() -accessibilityManager.applyAccessibilityConfiguration(accessibilityConfig, to: widget) -``` - -### Custom Accessibility Labels - -```swift -// Add custom accessibility labels -accessibilityManager.addAccessibilityLabel("Weather Widget", to: widget) -accessibilityManager.addAccessibilityHint("Shows current weather information", to: widget) -accessibilityManager.addAccessibilityValue("72°F, Sunny", to: widget) -``` - -## Performance Optimization - -### Rendering Optimization - -```swift -// Configure rendering optimization -let renderingConfig = RenderingConfiguration() -renderingConfig.enableHardwareAcceleration = true -renderingConfig.enableLayerBacking = true -renderingConfig.enableOpaqueLayers = true - -// Apply rendering optimization -let renderingManager = WidgetRenderingManager() -renderingManager.applyRenderingConfiguration(renderingConfig, to: widget) -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.enableImageCaching = true -memoryConfig.maxCacheSize = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableAutomaticCleanup = true - -// Apply memory management -let memoryManager = WidgetMemoryManager() -memoryManager.applyMemoryConfiguration(memoryConfig, to: widget) -``` - -## Branding and Identity - -### Brand Colors - -```swift -// Create brand color scheme -let brandColorScheme = BrandColorScheme( - primary: Color(red: 0.2, green: 0.6, blue: 1.0), - secondary: Color(red: 0.1, green: 0.3, blue: 0.8), - accent: Color(red: 1.0, green: 0.8, blue: 0.0), - background: Color(red: 0.95, green: 0.95, blue: 0.95) -) - -// Apply brand colors -stylingManager.applyBrandColorScheme(brandColorScheme, to: widget) -``` - -### Logo Integration - -```swift -// Add logo to widget -let logoConfig = LogoConfiguration() -logoConfig.logoImage = UIImage(named: "app-logo") -logoConfig.logoSize = CGSize(width: 32, height: 32) -logoConfig.logoPosition = .topLeft - -// Apply logo configuration -let logoManager = WidgetLogoManager() -logoManager.applyLogoConfiguration(logoConfig, to: widget) -``` - -## Advanced Customization - -### Custom Widget Types - -```swift -// Create custom widget type -let customWidgetType = CustomWidgetType( - name: "Weather Widget", - layout: customLayout, - styling: customTheme, - interactions: customInteractions -) - -// Register custom widget type -let widgetTypeManager = WidgetTypeManager() -widgetTypeManager.registerCustomWidgetType(customWidgetType) -``` - -### Plugin System - -```swift -// Create custom plugin -let customPlugin = WidgetPlugin( - name: "Analytics Plugin", - version: "1.0.0", - configuration: pluginConfig -) - -// Register plugin -let pluginManager = WidgetPluginManager() -pluginManager.registerPlugin(customPlugin) { result in - switch result { - case .success: - print("✅ Plugin registered successfully") - case .failure(let error): - print("❌ Plugin registration failed: \(error)") - } -} -``` - -## Testing Customizations - -### Unit Testing - -```swift -import XCTest - -class CustomizationTests: XCTestCase { - var customizationManager: WidgetCustomizationManager! - - override func setUp() { - super.setUp() - customizationManager = WidgetCustomizationManager.shared - } - - func testColorSchemeApplication() { - let expectation = XCTestExpectation(description: "Color scheme application") - - let colorScheme = WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange - ) - - customizationManager.applyColorScheme(colorScheme, to: widget) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Color scheme application failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testThemeSwitching() { - let expectation = XCTestExpectation(description: "Theme switching") - - let lightTheme = WidgetTheme(name: "Light") - let darkTheme = WidgetTheme(name: "Dark") - - customizationManager.switchTheme(from: lightTheme, to: darkTheme) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Theme switching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### Visual Testing - -```swift -import XCTest - -class VisualCustomizationTests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testCustomThemeAppearance() { - // Test custom theme appears correctly - let widget = app.otherElements["customWidget"] - XCTAssertTrue(widget.exists) - - // Verify custom colors - let customColor = widget.otherElements["customColor"] - XCTAssertTrue(customColor.exists) - } - - func testAnimationSmoothness() { - // Test animation smoothness - let animatedWidget = app.otherElements["animatedWidget"] - animatedWidget.tap() - - // Verify animation completes - let animationComplete = app.otherElements["animationComplete"] - XCTAssertTrue(animationComplete.waitForExistence(timeout: 2.0)) - } -} -``` - -## Deployment - -### Production Configuration - -```swift -// Production customization configuration -let productionConfig = CustomizationConfiguration() -productionConfig.enableOptimization = true -productionConfig.enableCompression = true -productionConfig.enableCaching = true -productionConfig.enablePerformanceMonitoring = true - -// Apply production configuration -customizationManager.configure(productionConfig) { result in - switch result { - case .success: - print("✅ Production customization configured") - case .failure(let error): - print("❌ Production configuration failed: \(error)") - } -} -``` - -### Quality Assurance - -```swift -// Quality assurance checklist -let qualityChecklist = CustomizationQualityChecklist() -qualityChecklist.verifyColorContrast = true -qualityChecklist.verifyAccessibility = true -qualityChecklist.verifyPerformance = true -qualityChecklist.verifyCompatibility = true - -// Run quality checks -customizationManager.runQualityChecks(qualityChecklist) { result in - switch result { - case .success(let report): - print("✅ Quality checks passed") - print("Report: \(report)") - case .failure(let errors): - print("❌ Quality checks failed: \(errors)") - } -} -``` - -## Troubleshooting - -### Common Issues - -1. **Customization Not Applied** - - Check configuration validity - - Verify widget registration - - Test on physical device - -2. **Performance Issues** - - Optimize custom animations - - Reduce complexity - - Monitor memory usage - -3. **Visual Inconsistencies** - - Verify color schemes - - Check font availability - - Test on different devices - -### Debug Tools - -```swift -// Enable customization debugging -let debugLogger = CustomizationDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor customization operations -debugLogger.onCustomizationApplied = { customization in - print("Customization applied: \(customization)") -} - -debugLogger.onCustomizationError = { error in - print("Customization error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of customizing widgets using the iOS Widget Development Kit. By following these guidelines, you can create unique, branded, and visually appealing widgets that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Customization API](CustomizationAPI.md) documentation. diff --git a/Documentation/DataIntegrationAPI.md b/Documentation/DataIntegrationAPI.md deleted file mode 100644 index edb25f1..0000000 --- a/Documentation/DataIntegrationAPI.md +++ /dev/null @@ -1,672 +0,0 @@ -# Data Integration API - - -## Table of Contents -- [Data Integration API](#data-integration-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [WidgetDataManager](#widgetdatamanager) - - [WidgetDataSource](#widgetdatasource) - - [WidgetData](#widgetdata) -- [Data Source Types](#data-source-types) - - [API Data Sources](#api-data-sources) - - [Local Data Sources](#local-data-sources) - - [Database Data Sources](#database-data-sources) -- [Data Fetching](#data-fetching) - - [Basic Data Fetching](#basic-data-fetching) - - [Cached Data Fetching](#cached-data-fetching) - - [Background Data Fetching](#background-data-fetching) -- [Real-Time Updates](#real-time-updates) - - [WebSocket Integration](#websocket-integration) - - [Push Notifications](#push-notifications) -- [Data Processing](#data-processing) - - [Data Transformation](#data-transformation) - - [Data Validation](#data-validation) -- [Caching](#caching) - - [Cache Configuration](#cache-configuration) - - [Cache Operations](#cache-operations) -- [Error Handling](#error-handling) - - [DataError Types](#dataerror-types) - - [Error Handling Example](#error-handling-example) -- [Performance Optimization](#performance-optimization) - - [Efficient Data Fetching](#efficient-data-fetching) - - [Memory Management](#memory-management) -- [Security](#security) - - [Data Encryption](#data-encryption) - - [Certificate Pinning](#certificate-pinning) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Best Practices](#best-practices) - - [Data Management](#data-management) - - [Security](#security) - - - -## Overview - -The Data Integration API provides comprehensive functionality for integrating data sources with widgets in iOS applications using the iOS Widget Development Kit. This API enables developers to fetch, process, cache, and update data from various sources including APIs, databases, and local storage. - -## Core Classes - -### WidgetDataManager - -The main manager class for handling data operations. - -```swift -class WidgetDataManager { - static let shared = WidgetDataManager() - - func fetchData(from source: WidgetDataSource) -> Result - func updateWidget(_ widget: Widget, with data: WidgetData) -> Result - func cacheData(_ data: WidgetData, for key: String) -> Result - func getCachedData(for key: String) -> Result -} -``` - -### WidgetDataSource - -The data source class for different data types. - -```swift -class WidgetDataSource { - let type: DataSourceType - let endpoint: String - let cachePolicy: CachePolicy - - init(type: DataSourceType, endpoint: String, cachePolicy: CachePolicy) - func configure(_ config: DataSourceConfiguration) -} -``` - -### WidgetData - -The data model for widget content. - -```swift -struct WidgetData { - let content: String - let timestamp: Date - let metadata: [String: Any] - let isCached: Bool - - init(content: String, timestamp: Date, metadata: [String: Any]) -} -``` - -## Data Source Types - -### API Data Sources - -```swift -// Create API data source -let apiDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.company.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure API settings -apiDataSource.configure { config in - config.timeout = 30 // 30 seconds - config.retryCount = 3 - config.headers = [ - "Authorization": "Bearer your-token", - "Content-Type": "application/json" - ] -} -``` - -### Local Data Sources - -```swift -// Create local data source -let localDataSource = WidgetDataSource( - type: .local, - endpoint: "widget-data", - cachePolicy: .localFirst -) - -// Configure local storage -localDataSource.configure { config in - config.storageType = .userDefaults - config.encryptionEnabled = true - config.compressionEnabled = true -} -``` - -### Database Data Sources - -```swift -// Create database data source -let databaseDataSource = WidgetDataSource( - type: .database, - endpoint: "widget_data_table", - cachePolicy: .databaseFirst -) - -// Configure database settings -databaseDataSource.configure { config in - config.databaseType = .sqlite - config.connectionString = "path/to/database.sqlite" - config.queryTimeout = 10 -} -``` - -## Data Fetching - -### Basic Data Fetching - -```swift -// Fetch data from API -let dataManager = WidgetDataManager.shared -dataManager.fetchData(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched successfully") - print("Data: \(data)") - - // Update widget with new data - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Cached Data Fetching - -```swift -// Fetch data with caching -dataManager.fetchDataWithCache(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched (cached: \(data.isCached))") - - if data.isCached { - print("Using cached data") - } else { - print("Using fresh data") - } - - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Background Data Fetching - -```swift -// Configure background fetching -let backgroundConfig = BackgroundFetchConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.fetchInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background fetching -dataManager.configureBackgroundFetching(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background fetching configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} - -// Start background fetching -dataManager.startBackgroundFetching(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Background data fetched") - updateWidget(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Real-Time Updates - -### WebSocket Integration - -```swift -// Create WebSocket data source -let webSocketDataSource = WidgetDataSource( - type: .websocket, - endpoint: "wss://api.company.com/ws", - cachePolicy: .realtime -) - -// Configure WebSocket settings -webSocketDataSource.configure { config in - config.autoReconnect = true - config.reconnectInterval = 5 // 5 seconds - config.maxReconnectAttempts = 10 - config.heartbeatInterval = 30 // 30 seconds -} - -// Connect to WebSocket -dataManager.connectWebSocket(webSocketDataSource) { result in - switch result { - case .success: - print("✅ WebSocket connected") - - // Listen for real-time updates - dataManager.onWebSocketMessage { message in - print("📱 Real-time message received: \(message)") - - // Parse and update widget - if let data = parseWebSocketMessage(message) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ WebSocket connection failed: \(error)") - } -} -``` - -### Push Notifications - -```swift -// Configure push notifications -let pushConfig = PushNotificationConfiguration() -pushConfig.enablePushNotifications = true -pushConfig.topic = "widget-updates" -pushConfig.enableSilentNotifications = true - -// Setup push notifications -dataManager.configurePushNotifications(pushConfig) { result in - switch result { - case .success: - print("✅ Push notifications configured") - - // Handle push notification - dataManager.onPushNotification { notification in - print("📱 Push notification received: \(notification)") - - // Extract data from notification - if let data = extractDataFromNotification(notification) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ Push notification setup failed: \(error)") - } -} -``` - -## Data Processing - -### Data Transformation - -```swift -// Create data transformer -let dataTransformer = WidgetDataTransformer() - -// Transform API data to widget format -dataTransformer.transform(apiData) { result in - switch result { - case .success(let widgetData): - print("✅ Data transformed successfully") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data transformation failed: \(error)") - } -} - -// Custom transformation -dataTransformer.addTransformation { data in - // Custom transformation logic - return WidgetData( - content: data.content, - timestamp: Date(), - metadata: data.metadata - ) -} -``` - -### Data Validation - -```swift -// Create data validator -let dataValidator = WidgetDataValidator() - -// Validate data before updating widget -dataValidator.validate(widgetData) { result in - switch result { - case .success: - print("✅ Data validation passed") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data validation failed: \(error)") - handleValidationError(error) - } -} - -// Custom validation rules -dataValidator.addValidationRule { data in - // Check if data is not empty - guard !data.content.isEmpty else { - return .failure(.emptyData) - } - - // Check if timestamp is recent - guard data.timestamp.timeIntervalSinceNow > -3600 else { - return .failure(.staleData) - } - - return .success -} -``` - -## Caching - -### Cache Configuration - -```swift -// Configure caching -let cacheConfig = CacheConfiguration() -cacheConfig.enableCaching = true -cacheConfig.maxCacheSize = 100 * 1024 * 1024 // 100 MB -cacheConfig.cacheExpiration = 3600 // 1 hour -cacheConfig.enableCompression = true - -// Setup caching -dataManager.configureCaching(cacheConfig) { result in - switch result { - case .success: - print("✅ Caching configured") - case .failure(let error): - print("❌ Cache configuration failed: \(error)") - } -} -``` - -### Cache Operations - -```swift -// Store data in cache -dataManager.cacheData(widgetData, for: "widget-key") { result in - switch result { - case .success: - print("✅ Data cached successfully") - case .failure(let error): - print("❌ Caching failed: \(error)") - } -} - -// Retrieve data from cache -dataManager.getCachedData(for: "widget-key") { result in - switch result { - case .success(let data): - print("✅ Cached data retrieved") - updateWidget(with: data) - - case .failure(let error): - print("❌ Cache retrieval failed: \(error)") - } -} - -// Clear cache -dataManager.clearCache { result in - switch result { - case .success: - print("✅ Cache cleared") - case .failure(let error): - print("❌ Cache clearing failed: \(error)") - } -} -``` - -## Error Handling - -### DataError Types - -```swift -enum DataError: Error { - case networkError(String) - case parseError(String) - case validationError(String) - case cacheError(String) - case transformationError(String) - case timeoutError(String) - case unauthorizedError(String) - case serverError(String) -} -``` - -### Error Handling Example - -```swift -func handleDataError(_ error: DataError) { - switch error { - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - - case .parseError(let message): - print("Parse error: \(message)") - // Handle parse error - - case .validationError(let message): - print("Validation error: \(message)") - // Handle validation error - - case .cacheError(let message): - print("Cache error: \(message)") - // Handle cache error - - case .transformationError(let message): - print("Transformation error: \(message)") - // Handle transformation error - - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout error - - case .unauthorizedError(let message): - print("Unauthorized error: \(message)") - // Handle unauthorized error - - case .serverError(let message): - print("Server error: \(message)") - // Handle server error - } -} -``` - -## Performance Optimization - -### Efficient Data Fetching - -```swift -// Optimize data fetching -let optimizedConfig = DataFetchConfiguration() -optimizedConfig.enableCompression = true -optimizedConfig.enablePagination = true -optimizedConfig.pageSize = 20 -optimizedConfig.enableIncrementalUpdates = true - -// Configure optimized fetching -dataManager.configureOptimizedFetching(optimizedConfig) { result in - switch result { - case .success: - print("✅ Optimized fetching configured") - case .failure(let error): - print("❌ Optimization failed: \(error)") - } -} -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.maxMemoryUsage = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableMemoryMonitoring = true -memoryConfig.enableAutomaticCleanup = true - -// Setup memory management -dataManager.configureMemoryManagement(memoryConfig) { result in - switch result { - case .success: - print("✅ Memory management configured") - case .failure(let error): - print("❌ Memory configuration failed: \(error)") - } -} -``` - -## Security - -### Data Encryption - -```swift -// Configure data encryption -let encryptionConfig = EncryptionConfiguration() -encryptionConfig.enableEncryption = true -encryptionConfig.algorithm = .aes256 -encryptionConfig.keySize = 256 - -// Setup encryption -dataManager.configureEncryption(encryptionConfig) { result in - switch result { - case .success: - print("✅ Encryption configured") - case .failure(let error): - print("❌ Encryption setup failed: \(error)") - } -} -``` - -### Certificate Pinning - -```swift -// Configure certificate pinning -let securityConfig = SecurityConfiguration() -securityConfig.enableCertificatePinning = true -securityConfig.pinnedCertificates = ["cert1", "cert2"] -securityConfig.enableSSLVerification = true - -// Setup security -dataManager.configureSecurity(securityConfig) { result in - switch result { - case .success: - print("✅ Security configured") - case .failure(let error): - print("❌ Security setup failed: \(error)") - } -} -``` - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `fetchData` | Fetch data from source | `WidgetDataSource` | `Result` | -| `updateWidget` | Update widget with data | `Widget`, `WidgetData` | `Result` | -| `cacheData` | Cache data | `WidgetData`, `String` | `Result` | -| `getCachedData` | Get cached data | `String` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `type` | `DataSourceType` | Data source type | -| `endpoint` | `String` | Data source endpoint | -| `cachePolicy` | `CachePolicy` | Cache policy | -| `content` | `String` | Widget data content | -| `timestamp` | `Date` | Data timestamp | -| `metadata` | `[String: Any]` | Data metadata | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldDataSource = DataSource(type: "api", url: "https://api.com/data") - -// iOS 15+ approach -let newDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.com/data", - cachePolicy: .cacheFirst -) -``` - -## Troubleshooting - -### Common Issues - -1. **Network Connectivity** - - Check internet connection - - Verify API endpoints - - Test with different networks - -2. **Data Format Issues** - - Validate API responses - - Check data transformation - - Verify JSON parsing - -3. **Performance Problems** - - Monitor memory usage - - Optimize data fetching - - Implement caching - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = DataDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor data operations -debugLogger.onDataFetched = { data in - print("Data fetched: \(data)") -} - -debugLogger.onDataCached = { key in - print("Data cached: \(key)") -} - -debugLogger.onDataError = { error in - print("Data error: \(error)") -} -``` - -## Best Practices - -### Data Management - -1. **Validate Data**: Always validate data before using -2. **Handle Errors**: Implement proper error handling -3. **Cache Strategically**: Use caching for performance -4. **Monitor Performance**: Monitor data operations - -### Security - -1. **Encrypt Sensitive Data**: Encrypt sensitive data -2. **Validate Inputs**: Validate all data inputs -3. **Use Secure Connections**: Use secure network connections -4. **Regular Updates**: Regularly update security measures diff --git a/Documentation/DataIntegrationGuide.md b/Documentation/DataIntegrationGuide.md deleted file mode 100644 index ce958f7..0000000 --- a/Documentation/DataIntegrationGuide.md +++ /dev/null @@ -1,785 +0,0 @@ -# Data Integration Guide - - -## Table of Contents -- [Data Integration Guide](#data-integration-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Import the Framework](#1-import-the-framework) - - [2. Initialize Data Manager](#2-initialize-data-manager) -- [Data Sources](#data-sources) - - [API Data Sources](#api-data-sources) - - [Local Data Sources](#local-data-sources) - - [Database Data Sources](#database-data-sources) -- [Data Fetching](#data-fetching) - - [Basic Data Fetching](#basic-data-fetching) - - [Cached Data Fetching](#cached-data-fetching) - - [Background Data Fetching](#background-data-fetching) -- [Real-Time Updates](#real-time-updates) - - [WebSocket Integration](#websocket-integration) - - [Push Notifications](#push-notifications) -- [Data Processing](#data-processing) - - [Data Transformation](#data-transformation) - - [Data Validation](#data-validation) -- [Caching](#caching) - - [Cache Configuration](#cache-configuration) - - [Cache Operations](#cache-operations) -- [Error Handling](#error-handling) - - [Network Errors](#network-errors) - - [Data Errors](#data-errors) - - [Recovery Strategies](#recovery-strategies) -- [Performance Optimization](#performance-optimization) - - [Efficient Data Fetching](#efficient-data-fetching) - - [Memory Management](#memory-management) -- [Security](#security) - - [Data Encryption](#data-encryption) - - [Certificate Pinning](#certificate-pinning) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [Integration Testing](#integration-testing) -- [Deployment](#deployment) - - [Production Configuration](#production-configuration) - - [Monitoring](#monitoring) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for integrating data sources with widgets in iOS applications using the iOS Widget Development Kit. Data integration enables widgets to display real-time, relevant information from various sources including APIs, databases, and local storage. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of networking and data handling - -## Getting Started - -### 1. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 2. Initialize Data Manager - -```swift -let dataManager = WidgetDataManager.shared -dataManager.configure { config in - config.enableRealTimeUpdates = true - config.enableBackgroundSync = true - config.enableCaching = true - config.refreshInterval = 300 // 5 minutes -} -``` - -## Data Sources - -### API Data Sources - -```swift -// Create API data source -let apiDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure API settings -apiDataSource.configure { config in - config.timeout = 30 // 30 seconds - config.retryCount = 3 - config.headers = [ - "Authorization": "Bearer your-token", - "Content-Type": "application/json" - ] -} -``` - -### Local Data Sources - -```swift -// Create local data source -let localDataSource = WidgetDataSource( - type: .local, - endpoint: "widget-data", - cachePolicy: .localFirst -) - -// Configure local storage -localDataSource.configure { config in - config.storageType = .userDefaults - config.encryptionEnabled = true - config.compressionEnabled = true -} -``` - -### Database Data Sources - -```swift -// Create database data source -let databaseDataSource = WidgetDataSource( - type: .database, - endpoint: "widget_data_table", - cachePolicy: .databaseFirst -) - -// Configure database settings -databaseDataSource.configure { config in - config.databaseType = .sqlite - config.connectionString = "path/to/database.sqlite" - config.queryTimeout = 10 -} -``` - -## Data Fetching - -### Basic Data Fetching - -```swift -// Fetch data from API -dataManager.fetchData(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched successfully") - print("Data: \(data)") - - // Update widget with new data - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Cached Data Fetching - -```swift -// Fetch data with caching -dataManager.fetchDataWithCache(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched (cached: \(data.isCached))") - - if data.isCached { - print("Using cached data") - } else { - print("Using fresh data") - } - - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Background Data Fetching - -```swift -// Configure background fetching -let backgroundConfig = BackgroundFetchConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.fetchInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background fetching -dataManager.configureBackgroundFetching(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background fetching configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} - -// Start background fetching -dataManager.startBackgroundFetching(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Background data fetched") - updateWidget(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Real-Time Updates - -### WebSocket Integration - -```swift -// Create WebSocket data source -let webSocketDataSource = WidgetDataSource( - type: .websocket, - endpoint: "wss://api.yourcompany.com/ws", - cachePolicy: .realtime -) - -// Configure WebSocket settings -webSocketDataSource.configure { config in - config.autoReconnect = true - config.reconnectInterval = 5 // 5 seconds - config.maxReconnectAttempts = 10 - config.heartbeatInterval = 30 // 30 seconds -} - -// Connect to WebSocket -dataManager.connectWebSocket(webSocketDataSource) { result in - switch result { - case .success: - print("✅ WebSocket connected") - - // Listen for real-time updates - dataManager.onWebSocketMessage { message in - print("📱 Real-time message received: \(message)") - - // Parse and update widget - if let data = parseWebSocketMessage(message) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ WebSocket connection failed: \(error)") - } -} -``` - -### Push Notifications - -```swift -// Configure push notifications -let pushConfig = PushNotificationConfiguration() -pushConfig.enablePushNotifications = true -pushConfig.topic = "widget-updates" -pushConfig.enableSilentNotifications = true - -// Setup push notifications -dataManager.configurePushNotifications(pushConfig) { result in - switch result { - case .success: - print("✅ Push notifications configured") - - // Handle push notification - dataManager.onPushNotification { notification in - print("📱 Push notification received: \(notification)") - - // Extract data from notification - if let data = extractDataFromNotification(notification) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ Push notification setup failed: \(error)") - } -} -``` - -## Data Processing - -### Data Transformation - -```swift -// Create data transformer -let dataTransformer = WidgetDataTransformer() - -// Transform API data to widget format -dataTransformer.transform(apiData) { result in - switch result { - case .success(let widgetData): - print("✅ Data transformed successfully") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data transformation failed: \(error)") - } -} - -// Custom transformation -dataTransformer.addTransformation { data in - // Custom transformation logic - return WidgetData( - title: data.title, - subtitle: data.subtitle, - value: data.value, - timestamp: Date() - ) -} -``` - -### Data Validation - -```swift -// Create data validator -let dataValidator = WidgetDataValidator() - -// Validate data before updating widget -dataValidator.validate(widgetData) { result in - switch result { - case .success: - print("✅ Data validation passed") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data validation failed: \(error)") - handleValidationError(error) - } -} - -// Custom validation rules -dataValidator.addValidationRule { data in - // Check if data is not empty - guard !data.title.isEmpty else { - return .failure(.emptyData) - } - - // Check if timestamp is recent - guard data.timestamp.timeIntervalSinceNow > -3600 else { - return .failure(.staleData) - } - - return .success -} -``` - -## Caching - -### Cache Configuration - -```swift -// Configure caching -let cacheConfig = CacheConfiguration() -cacheConfig.enableCaching = true -cacheConfig.maxCacheSize = 100 * 1024 * 1024 // 100 MB -cacheConfig.cacheExpiration = 3600 // 1 hour -cacheConfig.enableCompression = true - -// Setup caching -dataManager.configureCaching(cacheConfig) { result in - switch result { - case .success: - print("✅ Caching configured") - case .failure(let error): - print("❌ Cache configuration failed: \(error)") - } -} -``` - -### Cache Operations - -```swift -// Store data in cache -dataManager.cacheData(widgetData, for: "widget-key") { result in - switch result { - case .success: - print("✅ Data cached successfully") - case .failure(let error): - print("❌ Caching failed: \(error)") - } -} - -// Retrieve data from cache -dataManager.getCachedData(for: "widget-key") { result in - switch result { - case .success(let data): - print("✅ Cached data retrieved") - updateWidget(with: data) - - case .failure(let error): - print("❌ Cache retrieval failed: \(error)") - } -} - -// Clear cache -dataManager.clearCache { result in - switch result { - case .success: - print("✅ Cache cleared") - case .failure(let error): - print("❌ Cache clearing failed: \(error)") - } -} -``` - -## Error Handling - -### Network Errors - -```swift -// Handle network errors -func handleNetworkError(_ error: NetworkError) { - switch error { - case .noConnection: - print("No internet connection") - useCachedData() - - case .timeout: - print("Request timeout") - retryWithBackoff() - - case .serverError(let code): - print("Server error: \(code)") - handleServerError(code) - - case .unauthorized: - print("Unauthorized access") - refreshAuthentication() - - case .forbidden: - print("Access forbidden") - handleAccessDenied() - } -} -``` - -### Data Errors - -```swift -// Handle data errors -func handleDataError(_ error: DataError) { - switch error { - case .invalidFormat: - print("Invalid data format") - useFallbackData() - - case .missingFields: - print("Missing required fields") - useDefaultValues() - - case .parseError: - print("Data parsing error") - handleParseError() - - case .validationError: - print("Data validation error") - handleValidationError() - } -} -``` - -### Recovery Strategies - -```swift -// Implement recovery strategies -func implementRecoveryStrategy(for error: Error) { - switch error { - case is NetworkError: - // Network error recovery - retryWithExponentialBackoff() - - case is DataError: - // Data error recovery - useCachedData() - - case is CacheError: - // Cache error recovery - fetchFreshData() - - default: - // Generic error recovery - logError(error) - notifyUser() - } -} -``` - -## Performance Optimization - -### Efficient Data Fetching - -```swift -// Optimize data fetching -let optimizedConfig = DataFetchConfiguration() -optimizedConfig.enableCompression = true -optimizedConfig.enablePagination = true -optimizedConfig.pageSize = 20 -optimizedConfig.enableIncrementalUpdates = true - -// Configure optimized fetching -dataManager.configureOptimizedFetching(optimizedConfig) { result in - switch result { - case .success: - print("✅ Optimized fetching configured") - case .failure(let error): - print("❌ Optimization failed: \(error)") - } -} -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.maxMemoryUsage = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableMemoryMonitoring = true -memoryConfig.enableAutomaticCleanup = true - -// Setup memory management -dataManager.configureMemoryManagement(memoryConfig) { result in - switch result { - case .success: - print("✅ Memory management configured") - case .failure(let error): - print("❌ Memory configuration failed: \(error)") - } -} -``` - -## Security - -### Data Encryption - -```swift -// Configure data encryption -let encryptionConfig = EncryptionConfiguration() -encryptionConfig.enableEncryption = true -encryptionConfig.algorithm = .aes256 -encryptionConfig.keySize = 256 - -// Setup encryption -dataManager.configureEncryption(encryptionConfig) { result in - switch result { - case .success: - print("✅ Encryption configured") - case .failure(let error): - print("❌ Encryption setup failed: \(error)") - } -} -``` - -### Certificate Pinning - -```swift -// Configure certificate pinning -let securityConfig = SecurityConfiguration() -securityConfig.enableCertificatePinning = true -securityConfig.pinnedCertificates = ["cert1", "cert2"] -securityConfig.enableSSLVerification = true - -// Setup security -dataManager.configureSecurity(securityConfig) { result in - switch result { - case .success: - print("✅ Security configured") - case .failure(let error): - print("❌ Security setup failed: \(error)") - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class DataIntegrationTests: XCTestCase { - var dataManager: WidgetDataManager! - - override func setUp() { - super.setUp() - dataManager = WidgetDataManager.shared - } - - func testDataFetching() { - let expectation = XCTestExpectation(description: "Data fetching") - - let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.test.com/data", - cachePolicy: .cacheFirst - ) - - dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - expectation.fulfill() - case .failure(let error): - XCTFail("Data fetching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 10.0) - } - - func testCaching() { - let expectation = XCTestExpectation(description: "Caching") - - let testData = WidgetData(content: "Test data") - dataManager.cacheData(testData, for: "test-key") { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Caching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### Integration Testing - -```swift -import XCTest - -class DataIntegrationIntegrationTests: XCTestCase { - var dataManager: WidgetDataManager! - - override func setUp() { - super.setUp() - dataManager = WidgetDataManager.shared - } - - func testEndToEndDataFlow() { - let expectation = XCTestExpectation(description: "End-to-end data flow") - - // Setup data source - let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.test.com/widget-data", - cachePolicy: .cacheFirst - ) - - // Fetch and process data - dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - // Transform data - let transformer = WidgetDataTransformer() - transformer.transform(data) { transformResult in - switch transformResult { - case .success(let widgetData): - // Validate data - let validator = WidgetDataValidator() - validator.validate(widgetData) { validationResult in - switch validationResult { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Validation failed: \(error)") - } - } - case .failure(let error): - XCTFail("Transformation failed: \(error)") - } - } - case .failure(let error): - XCTFail("Data fetching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 15.0) - } -} -``` - -## Deployment - -### Production Configuration - -```swift -// Production data configuration -let productionConfig = DataConfiguration() -productionConfig.enableEncryption = true -productionConfig.enableCertificatePinning = true -productionConfig.enableCompression = true -productionConfig.enableCaching = true -productionConfig.cacheExpiration = 1800 // 30 minutes -productionConfig.retryCount = 3 -productionConfig.timeout = 30 - -// Apply production configuration -dataManager.configure(productionConfig) { result in - switch result { - case .success: - print("✅ Production configuration applied") - case .failure(let error): - print("❌ Production configuration failed: \(error)") - } -} -``` - -### Monitoring - -```swift -// Configure monitoring -let monitoringConfig = MonitoringConfiguration() -monitoringConfig.enablePerformanceMonitoring = true -monitoringConfig.enableErrorTracking = true -monitoringConfig.enableUsageAnalytics = true - -// Setup monitoring -dataManager.configureMonitoring(monitoringConfig) { result in - switch result { - case .success: - print("✅ Monitoring configured") - case .failure(let error): - print("❌ Monitoring setup failed: \(error)") - } -} -``` - -## Troubleshooting - -### Common Issues - -1. **Network Connectivity** - - Check internet connection - - Verify API endpoints - - Test with different networks - -2. **Data Format Issues** - - Validate API responses - - Check data transformation - - Verify JSON parsing - -3. **Performance Problems** - - Monitor memory usage - - Optimize data fetching - - Implement caching - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = DataDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor data operations -debugLogger.onDataFetched = { data in - print("Data fetched: \(data)") -} - -debugLogger.onDataCached = { key in - print("Data cached: \(key)") -} - -debugLogger.onDataError = { error in - print("Data error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of data integration with widgets using the iOS Widget Development Kit. By following these guidelines, you can create robust, efficient, and secure data integration solutions that enhance the functionality of your widgets. - -For more advanced features and detailed API documentation, refer to the [Data Integration API](DataIntegrationAPI.md) documentation. diff --git a/Documentation/DynamicIslandAPI.md b/Documentation/DynamicIslandAPI.md deleted file mode 100644 index ef7485f..0000000 --- a/Documentation/DynamicIslandAPI.md +++ /dev/null @@ -1,458 +0,0 @@ -# Dynamic Island API - - -## Table of Contents -- [Dynamic Island API](#dynamic-island-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [DynamicIslandManager](#dynamicislandmanager) - - [DynamicIslandActivity](#dynamicislandactivity) - - [DynamicIslandConfiguration](#dynamicislandconfiguration) -- [View Types](#view-types) - - [Supported View Types](#supported-view-types) -- [Content Types](#content-types) - - [Basic Island Content](#basic-island-content) - - [Interactive Island Content](#interactive-island-content) -- [Usage Examples](#usage-examples) - - [Creating a Simple Dynamic Island Activity](#creating-a-simple-dynamic-island-activity) - - [Creating an Interactive Dynamic Island Activity](#creating-an-interactive-dynamic-island-activity) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Error Handling](#error-handling) - - [IslandError Types](#islanderror-types) - - [Error Handling Example](#error-handling-example) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [Design Guidelines](#design-guidelines) - - [Security Considerations](#security-considerations) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 15 to iOS 16+](#from-ios-15-to-ios-16) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tips](#debug-tips) - - - -## Overview - -The Dynamic Island API provides comprehensive functionality for creating and managing Dynamic Island experiences in iOS applications. This API enables developers to create engaging, interactive experiences that integrate seamlessly with the iPhone's Dynamic Island. - -## Core Classes - -### DynamicIslandManager - -The main manager class for handling Dynamic Island operations. - -```swift -class DynamicIslandManager { - static let shared = DynamicIslandManager() - - func start(_ activity: DynamicIslandActivity) -> Result - func stop(_ activity: DynamicIslandActivity) -> Result - func update(_ activity: DynamicIslandActivity, with data: IslandData) -> Result - func refresh(_ activity: DynamicIslandActivity) -> Result -} -``` - -### DynamicIslandActivity - -The core activity class for Dynamic Island implementations. - -```swift -class DynamicIslandActivity { - let activityType: String - let configuration: DynamicIslandConfiguration - var content: IslandContent? - - init(activityType: String, configuration: DynamicIslandConfiguration) - func setCompactView(_ content: @escaping (IslandContext) -> IslandContent) - func setExpandedView(_ content: @escaping (IslandContext) -> IslandContent) - func setMinimalView(_ content: @escaping (IslandContext) -> IslandContent) - func update(with data: IslandData) -> Result -} -``` - -### DynamicIslandConfiguration - -Configuration options for Dynamic Island activities. - -```swift -struct DynamicIslandConfiguration { - var enableCompactView: Bool - var enableExpandedView: Bool - var enableMinimalView: Bool - var enableLeadingView: Bool - var enableTrailingView: Bool - var updateInterval: TimeInterval - var enableInteractions: Bool - var enableBackgroundUpdates: Bool -} -``` - -## View Types - -### Supported View Types - -```swift -enum IslandViewType { - case compact // Compact view in Dynamic Island - case expanded // Expanded view in Dynamic Island - case minimal // Minimal view in Dynamic Island - case leading // Leading view in Dynamic Island - case trailing // Trailing view in Dynamic Island -} -``` - -## Content Types - -### Basic Island Content - -```swift -struct BasicIslandContent: IslandContent { - let title: String - let subtitle: String? - let icon: String? - let backgroundColor: Color - let textColor: Color -} -``` - -### Interactive Island Content - -```swift -struct InteractiveIslandContent: IslandContent { - let title: String - let subtitle: String? - let actions: [IslandAction] - let backgroundColor: Color - let textColor: Color -} -``` - -## Usage Examples - -### Creating a Simple Dynamic Island Activity - -```swift -// Create Dynamic Island manager -let islandManager = DynamicIslandManager.shared - -// Configure activity -let config = DynamicIslandConfiguration() -config.enableCompactView = true -config.enableExpandedView = true -config.updateInterval = 30 // 30 seconds - -// Create activity -let islandActivity = DynamicIslandActivity( - activityType: "com.company.app.music", - configuration: config -) - -// Set compact view -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} - -// Set expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} - -// Start activity -let result = islandManager.start(islandActivity) -switch result { -case .success(let activity): - print("✅ Dynamic Island activity started successfully") - print("Activity ID: \(activity.id)") -case .failure(let error): - print("❌ Activity start failed: \(error)") -} -``` - -### Creating an Interactive Dynamic Island Activity - -```swift -// Create interactive activity -let interactiveConfig = DynamicIslandConfiguration() -interactiveConfig.enableCompactView = true -interactiveConfig.enableExpandedView = true -interactiveConfig.enableInteractions = true - -let interactiveActivity = DynamicIslandActivity( - activityType: "com.company.app.delivery", - configuration: interactiveConfig -) - -// Set interactive compact view -interactiveActivity.setCompactView { context in - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - Text("Delivery") - .font(.caption) - } -} - -// Set interactive expanded view -interactiveActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Track") { - // Track package action - } - .buttonStyle(.bordered) - - Button("Contact") { - // Contact driver action - } - .buttonStyle(.bordered) - } - } - .padding() -} - -// Start interactive activity -islandManager.start(interactiveActivity) -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = IslandDataSource( - type: .api, - endpoint: "https://api.company.com/island-data", - cachePolicy: .cacheFirst -) - -// Configure real-time updates -let realTimeManager = RealTimeUpdateManager() -realTimeManager.configure { config in - config.enableWebSocket = true - config.enablePushNotifications = true - config.enableBackgroundUpdates = true -} - -// Setup real-time connection -realTimeManager.connect { result in - switch result { - case .success: - print("✅ Real-time connection established") - - // Listen for updates - realTimeManager.onDataUpdate { data in - print("📱 Real-time data received: \(data)") - - // Update activity with real-time data - islandActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Dynamic Island updated with real-time data") - case .failure(let error): - print("❌ Real-time update failed: \(error)") - } - } - } - case .failure(let error): - print("❌ Real-time connection failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -islandManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Error Handling - -### IslandError Types - -```swift -enum IslandError: Error { - case startFailed(String) - case stopFailed(String) - case updateFailed(String) - case configurationError(String) - case dataError(String) - case networkError(String) - case permissionError(String) - case timeoutError(String) -} -``` - -### Error Handling Example - -```swift -func handleIslandError(_ error: IslandError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Handle start error - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Handle update error - case .configurationError(let message): - print("Configuration error: \(message)") - // Handle configuration error - case .dataError(let message): - print("Data error: \(message)") - // Handle data error - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - case .permissionError(let message): - print("Permission error: \(message)") - // Handle permission error - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout error - } -} -``` - -## Best Practices - -### Performance Optimization - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Design Guidelines - -1. **Clear Information**: Display essential information clearly -2. **Consistent Styling**: Maintain consistent visual design -3. **Accessibility**: Ensure activities are accessible -4. **User Privacy**: Respect user privacy settings - -### Security Considerations - -1. **Data Protection**: Secure sensitive data -2. **Permission Handling**: Request only necessary permissions -3. **Network Security**: Use secure network connections -4. **Error Handling**: Handle errors gracefully - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `start` | Start a Dynamic Island activity | `DynamicIslandActivity` | `Result` | -| `stop` | Stop a Dynamic Island activity | `DynamicIslandActivity` | `Result` | -| `update` | Update activity with new data | `DynamicIslandActivity`, `IslandData` | `Result` | -| `refresh` | Refresh activity content | `DynamicIslandActivity` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `activityType` | `String` | Unique activity type identifier | -| `configuration` | `DynamicIslandConfiguration` | Activity configuration | -| `content` | `IslandContent?` | Activity content | -| `isActive` | `Bool` | Activity status | - -## Migration Guide - -### From iOS 15 to iOS 16+ - -```swift -// iOS 15 approach (deprecated) -let oldActivity = Activity(kind: "com.app.activity") - -// iOS 16+ approach -let newActivity = DynamicIslandActivity( - activityType: "com.app.activity", - configuration: DynamicIslandConfiguration() -) -``` - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting**: Check activity configuration and permissions -2. **Updates Not Working**: Verify data source and network connectivity -3. **Performance Issues**: Optimize data fetching and background processing -4. **Crash Issues**: Implement proper error handling - -### Debug Tips - -1. **Enable Logging**: Use debug logging for troubleshooting -2. **Test on Device**: Always test on physical devices with Dynamic Island -3. **Monitor Performance**: Use performance monitoring tools -4. **Check Permissions**: Verify required permissions are granted diff --git a/Documentation/DynamicIslandGuide.md b/Documentation/DynamicIslandGuide.md deleted file mode 100644 index ffd4271..0000000 --- a/Documentation/DynamicIslandGuide.md +++ /dev/null @@ -1,774 +0,0 @@ -# Dynamic Island Guide - - -## Table of Contents -- [Dynamic Island Guide](#dynamic-island-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Island Manager](#3-initialize-island-manager) -- [Creating Your First Dynamic Island Activity](#creating-your-first-dynamic-island-activity) - - [Step 1: Define Island Configuration](#step-1-define-island-configuration) - - [Step 2: Create Activity Instance](#step-2-create-activity-instance) - - [Step 3: Define Island Views](#step-3-define-island-views) - - [Step 4: Start Activity](#step-4-start-activity) -- [Activity Types and Use Cases](#activity-types-and-use-cases) - - [Music Playback](#music-playback) - - [Delivery Tracking](#delivery-tracking) - - [Ride Sharing](#ride-sharing) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Interactive Elements](#interactive-elements) - - [Action Buttons](#action-buttons) - - [Gesture Recognition](#gesture-recognition) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing Dynamic Island experiences in iOS applications using the iOS Widget Development Kit. The Dynamic Island provides users with engaging, interactive experiences that seamlessly integrate with the iPhone's hardware design. - -## Prerequisites - -- iOS 16.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- iPhone 14 Pro or later (for testing) -- Basic knowledge of SwiftUI and ActivityKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for Dynamic Island: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -NSSupportsLiveActivitiesBackgroundProcessing - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Island Manager - -```swift -let islandManager = DynamicIslandManager.shared -islandManager.configure { config in - config.enableCompactView = true - config.enableExpandedView = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Dynamic Island Activity - -### Step 1: Define Island Configuration - -```swift -let islandConfig = DynamicIslandConfiguration() -islandConfig.enableCompactView = true -islandConfig.enableExpandedView = true -islandConfig.enableMinimalView = true -islandConfig.updateInterval = 30 // 30 seconds -``` - -### Step 2: Create Activity Instance - -```swift -let islandActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.music", - configuration: islandConfig -) -``` - -### Step 3: Define Island Views - -```swift -// Set compact view -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} - -// Set expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} - -// Set minimal view -islandActivity.setMinimalView { context in - Image(systemName: "music.note") - .foregroundColor(.purple) -} -``` - -### Step 4: Start Activity - -```swift -islandManager.start(islandActivity) { result in - switch result { - case .success(let activity): - print("✅ Dynamic Island activity started successfully") - print("Activity ID: \(activity.id)") - case .failure(let error): - print("❌ Activity start failed: \(error)") - } -} -``` - -## Activity Types and Use Cases - -### Music Playback - -```swift -// Music activity configuration -let musicConfig = DynamicIslandConfiguration() -musicConfig.enableCompactView = true -musicConfig.enableExpandedView = true -musicConfig.enableInteractions = true - -let musicActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.music", - configuration: musicConfig -) - -// Set music views -musicActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} - -musicActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Delivery Tracking - -```swift -// Delivery activity -let deliveryConfig = DynamicIslandConfiguration() -deliveryConfig.enableCompactView = true -deliveryConfig.enableExpandedView = true -deliveryConfig.enableInteractions = true - -let deliveryActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.delivery", - configuration: deliveryConfig -) - -// Set delivery views -deliveryActivity.setCompactView { context in - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - Text("Delivery") - .font(.caption) - } -} - -deliveryActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Track Package") { - // Track package action - } - .buttonStyle(.bordered) - - Button("Contact Driver") { - // Contact driver action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Ride Sharing - -```swift -// Ride sharing activity -let rideConfig = DynamicIslandConfiguration() -rideConfig.enableCompactView = true -rideConfig.enableExpandedView = true -rideConfig.enableInteractions = true - -let rideActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.ride", - configuration: rideConfig -) - -// Set ride views -rideActivity.setCompactView { context in - HStack { - Image(systemName: "car.fill") - .foregroundColor(.green) - Text("Ride") - .font(.caption) - } -} - -rideActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "car.fill") - .foregroundColor(.green) - - VStack(alignment: .leading) { - Text("Your ride is arriving") - .font(.headline) - Text("Driver: John Doe") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("2 min") - .font(.caption2) - .foregroundColor(.green) - } - - HStack(spacing: 16) { - Button("Call Driver") { - // Call driver action - } - .buttonStyle(.bordered) - - Button("Cancel Ride") { - // Cancel ride action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = IslandDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/island-data", - cachePolicy: .cacheFirst -) - -// Configure real-time updates -let realTimeManager = RealTimeUpdateManager() -realTimeManager.configure { config in - config.enableWebSocket = true - config.enablePushNotifications = true - config.enableBackgroundUpdates = true -} - -// Setup real-time connection -realTimeManager.connect { result in - switch result { - case .success: - print("✅ Real-time connection established") - - // Listen for updates - realTimeManager.onDataUpdate { data in - print("📱 Real-time data received: \(data)") - - // Update activity with real-time data - islandActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Dynamic Island updated with real-time data") - case .failure(let error): - print("❌ Real-time update failed: \(error)") - } - } - } - case .failure(let error): - print("❌ Real-time connection failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -islandManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Interactive Elements - -### Action Buttons - -```swift -// Add action buttons to expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Gesture Recognition - -```swift -// Add gesture recognition -islandActivity.onTap { context in - print("Dynamic Island tapped!") - // Handle tap gesture -} - -islandActivity.onLongPress { context in - print("Dynamic Island long pressed!") - // Handle long press gesture -} - -islandActivity.onSwipe { direction in - print("Dynamic Island swiped: \(direction)") - // Handle swipe gesture -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom island style -let customStyle = IslandStyle( - backgroundColor: .systemPurple, - cornerRadius: 12, - shadow: IslandShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = IslandStylingManager() -stylingManager.applyStyle(customStyle, to: islandActivity) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicIslandTheme( - lightMode: IslandTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .purple - ), - darkMode: IslandTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicIslandThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Performance Optimization - -### Best Practices - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = IslandPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor island performance -performanceMonitor.startMonitoring(islandActivity) { metrics in - print("Dynamic Island performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Activity Start Failed** - - Check activity configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleIslandError(_ error: IslandError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Retry start or show user message - - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout issues - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class DynamicIslandTests: XCTestCase { - var islandActivity: DynamicIslandActivity! - - override func setUp() { - super.setUp() - let config = DynamicIslandConfiguration() - islandActivity = DynamicIslandActivity(activityType: "test.activity", configuration: config) - } - - func testActivityStart() { - let expectation = XCTestExpectation(description: "Activity start") - - let manager = DynamicIslandManager.shared - manager.start(islandActivity) { result in - switch result { - case .success(let activity): - expectation.fulfill() - case .failure(let error): - XCTFail("Activity start failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testActivityUpdate() { - let expectation = XCTestExpectation(description: "Activity update") - - let testData = IslandData(content: "Test content") - islandActivity.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class DynamicIslandUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testIslandAppearance() { - // Test Dynamic Island appears - let island = app.otherElements["dynamicIsland"] - XCTAssertTrue(island.exists) - } - - func testIslandInteraction() { - // Test Dynamic Island interactions - let island = app.otherElements["dynamicIsland"] - island.tap() - - // Verify interaction handled - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Activity tested on iPhone 14 Pro or later -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting** - - Check activity configuration - - Verify permissions - - Test on iPhone 14 Pro or later - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = IslandDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor activity lifecycle -debugLogger.onActivityStarted = { activity in - print("Dynamic Island activity started: \(activity.activityType)") -} - -debugLogger.onActivityUpdated = { activity in - print("Dynamic Island activity updated: \(activity.activityType)") -} - -debugLogger.onActivityStopped = { activity in - print("Dynamic Island activity stopped: \(activity.activityType)") -} - -debugLogger.onActivityError = { error in - print("Dynamic Island activity error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating Dynamic Island experiences using the iOS Widget Development Kit. By following these guidelines, you can create engaging, interactive experiences that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Dynamic Island API](DynamicIslandAPI.md) documentation. diff --git a/Documentation/GettingStarted.md b/Documentation/GettingStarted.md deleted file mode 100644 index 31016b6..0000000 --- a/Documentation/GettingStarted.md +++ /dev/null @@ -1,35 +0,0 @@ -# 🚀 Getting Started - -Welcome to our world-class iOS development framework! This guide will help you get started quickly and efficiently. - -## Prerequisites - -- iOS 15.0+ -- Xcode 14.0+ -- Swift 5.9+ -- macOS 12.0+ - -## Installation - -### Swift Package Manager - -Add the following dependency to your `Package.swift`: - -```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/REPO_NAME.git", from: "1.0.0") -] -``` - -## Basic Setup - -1. Import the framework -2. Initialize the main component -3. Configure your settings -4. Start using the features - -## Next Steps - -- Check out our [Examples](Examples/) folder -- Read the [API Documentation](API.md) -- Explore [Advanced Usage](AdvancedUsage.md) diff --git a/Documentation/HomeScreenWidgetAPI.md b/Documentation/HomeScreenWidgetAPI.md deleted file mode 100644 index 8064783..0000000 --- a/Documentation/HomeScreenWidgetAPI.md +++ /dev/null @@ -1,439 +0,0 @@ -# Home Screen Widget API - - -## Table of Contents -- [Home Screen Widget API](#home-screen-widget-api) -- [Overview](#overview) -- [Core Features](#core-features) - - [Widget Types](#widget-types) - - [Interactive Features](#interactive-features) - - [Data Integration](#data-integration) -- [API Reference](#api-reference) - - [HomeScreenWidget](#homescreenwidget) - - [HomeWidgetConfiguration](#homewidgetconfiguration) - - [HomeWidgetContentProvider](#homewidgetcontentprovider) - - [HomeWidgetInteractionHandler](#homewidgetinteractionhandler) -- [Usage Examples](#usage-examples) - - [Basic Home Screen Widget](#basic-home-screen-widget) - - [Interactive Home Screen Widget](#interactive-home-screen-widget) - - [Data-Driven Home Screen Widget](#data-driven-home-screen-widget) -- [Widget Sizes](#widget-sizes) - - [Small Widget (155x155)](#small-widget-155x155) - - [Medium Widget (329x155)](#medium-widget-329x155) - - [Large Widget (329x345)](#large-widget-329x345) - - [Extra Large Widget (329x345)](#extra-large-widget-329x345) -- [Best Practices](#best-practices) - - [Performance](#performance) - - [User Experience](#user-experience) - - [Data Management](#data-management) - - [Interactions](#interactions) -- [Related Documentation](#related-documentation) - - - -## Overview - -The `HomeScreenWidget` API provides comprehensive functionality for creating and managing home screen widgets in iOS applications. This API enables developers to build beautiful, interactive, and data-driven widgets that enhance the user experience on the iOS home screen. - -## Core Features - -### Widget Types -- **Small Widgets**: Compact information display -- **Medium Widgets**: Detailed information with interactions -- **Large Widgets**: Rich content with multiple sections -- **Extra Large Widgets**: Comprehensive data visualization - -### Interactive Features -- **Tap Gestures**: Handle user taps and interactions -- **Deep Linking**: Navigate to specific app sections -- **Dynamic Content**: Real-time content updates -- **Custom Animations**: Smooth transitions and effects - -### Data Integration -- **Real-Time Updates**: Live data synchronization -- **Background Refresh**: Automatic content updates -- **Data Caching**: Efficient data management -- **Offline Support**: Graceful offline handling - -## API Reference - -### HomeScreenWidget - -The main class for home screen widget implementation. - -```swift -class HomeScreenWidget: Widget { - // MARK: - Initialization - - /// Initialize with widget kind and configuration - init(kind: String, configuration: HomeWidgetConfiguration) - - /// Initialize with custom content provider - init(kind: String, contentProvider: HomeWidgetContentProvider) - - // MARK: - Configuration - - /// Widget configuration - var configuration: HomeWidgetConfiguration { get set } - - /// Widget content provider - var contentProvider: HomeWidgetContentProvider { get set } - - /// Widget interaction handler - var interactionHandler: HomeWidgetInteractionHandler? { get set } - - // MARK: - Content Management - - /// Set widget content - func setContent(_ content: @escaping (WidgetContext) -> WidgetView) - - /// Update widget content - func updateContent(with data: WidgetData) async throws - - /// Refresh widget content - func refreshContent() async throws - - // MARK: - Interaction Handling - - /// Handle widget tap - func onTap(_ handler: @escaping (WidgetContext) -> Void) - - /// Handle widget long press - func onLongPress(_ handler: @escaping (WidgetContext) -> Void) - - /// Handle widget drag - func onDrag(_ handler: @escaping (WidgetContext, DragGesture) -> Void) -} -``` - -### HomeWidgetConfiguration - -Configuration class for home screen widget settings. - -```swift -struct HomeWidgetConfiguration { - // MARK: - Widget Settings - - /// Widget size - var widgetSize: WidgetSize = .medium - - /// Enable interactions - var enableInteractions: Bool = true - - /// Enable deep linking - var enableDeepLinking: Bool = true - - /// Refresh interval in seconds - var refreshInterval: TimeInterval = 300 - - /// Enable background updates - var enableBackgroundUpdates: Bool = true - - // MARK: - Styling Settings - - /// Widget background color - var backgroundColor: Color = .systemBackground - - /// Widget corner radius - var cornerRadius: CGFloat = 12 - - /// Widget shadow - var shadow: WidgetShadow? - - /// Widget border - var border: WidgetBorder? - - // MARK: - Content Settings - - /// Enable dynamic content - var enableDynamicContent: Bool = true - - /// Enable animations - var enableAnimations: Bool = true - - /// Enable accessibility - var enableAccessibility: Bool = true -} -``` - -### HomeWidgetContentProvider - -Protocol for providing widget content. - -```swift -protocol HomeWidgetContentProvider { - /// Create widget content - func createContent(for context: WidgetContext) -> WidgetView - - /// Update widget content - func updateContent(_ view: WidgetView, with context: WidgetContext) async throws - - /// Handle content refresh - func refreshContent() async throws -> WidgetData -} -``` - -### HomeWidgetInteractionHandler - -Protocol for handling widget interactions. - -```swift -protocol HomeWidgetInteractionHandler { - /// Handle tap interaction - func handleTap(_ context: WidgetContext) async throws - - /// Handle long press interaction - func handleLongPress(_ context: WidgetContext) async throws - - /// Handle drag interaction - func handleDrag(_ context: WidgetContext, gesture: DragGesture) async throws -} -``` - -## Usage Examples - -### Basic Home Screen Widget - -```swift -import WidgetDevelopmentKit - -// Create home screen widget -let homeWidget = HomeScreenWidget( - kind: "com.company.app.homewidget", - configuration: HomeWidgetConfiguration() -) - -// Set widget content -homeWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "house.fill") - .foregroundColor(.blue) - .font(.title2) - - Spacer() - - Text("Welcome") - .font(.headline) - .foregroundColor(.primary) - } - - Text("You have 5 new messages") - .font(.caption) - .foregroundColor(.secondary) - - Button("Open App") { - // Deep link to app - } - .buttonStyle(.bordered) - } - .padding() - .background(Color(.systemBackground)) -} - -// Handle widget tap -homeWidget.onTap { context in - print("Widget tapped!") - // Navigate to app -} - -// Register widget -let widgetManager = WidgetDevelopmentManager() -try await widgetManager.register(homeWidget) -``` - -### Interactive Home Screen Widget - -```swift -// Create interactive widget -let interactiveWidget = HomeScreenWidget( - kind: "com.company.app.interactive", - configuration: HomeWidgetConfiguration() -) - -// Set interactive content -interactiveWidget.setContent { context in - VStack(spacing: 16) { - HStack { - Image(systemName: "message.fill") - .foregroundColor(.green) - .font(.title) - - VStack(alignment: .leading) { - Text("Messages") - .font(.headline) - - Text("3 new messages") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("View") { - // Open messages - } - .buttonStyle(.bordered) - } - - HStack(spacing: 12) { - Button("Reply") { - // Quick reply - } - .buttonStyle(.bordered) - - Button("Mark Read") { - // Mark as read - } - .buttonStyle(.bordered) - } - } - .padding() -} - -// Handle interactions -interactiveWidget.onTap { context in - // Handle tap -} - -interactiveWidget.onLongPress { context in - // Show options menu -} -``` - -### Data-Driven Home Screen Widget - -```swift -// Create data-driven widget -let dataWidget = HomeScreenWidget( - kind: "com.company.app.datawidget", - configuration: HomeWidgetConfiguration() -) - -// Set data-driven content -dataWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "chart.line.uptrend.xyaxis") - .foregroundColor(.blue) - .font(.title2) - - Spacer() - - Text("Analytics") - .font(.headline) - } - - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Today's Sales") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("$1,234") - .font(.caption) - .fontWeight(.semibold) - } - - HStack { - Text("Orders") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("12") - .font(.caption) - .fontWeight(.semibold) - } - - HStack { - Text("Customers") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("8") - .font(.caption) - .fontWeight(.semibold) - } - } - - Button("View Details") { - // Open analytics - } - .buttonStyle(.bordered) - } - .padding() -} - -// Update widget with real-time data -Task { - let data = try await fetchAnalyticsData() - try await dataWidget.updateContent(with: data) -} -``` - -## Widget Sizes - -### Small Widget (155x155) -- Compact information display -- Single data point or status -- Minimal interactions -- Quick glance information - -### Medium Widget (329x155) -- Detailed information display -- Multiple data points -- Interactive elements -- Rich content presentation - -### Large Widget (329x345) -- Comprehensive information -- Multiple sections -- Rich interactions -- Data visualization - -### Extra Large Widget (329x345) -- Maximum content display -- Complex layouts -- Advanced interactions -- Full feature set - -## Best Practices - -### Performance -- Optimize widget rendering -- Minimize memory usage -- Use efficient data fetching -- Implement proper caching - -### User Experience -- Provide meaningful content -- Handle loading states -- Support accessibility -- Implement smooth animations - -### Data Management -- Cache frequently used data -- Handle offline scenarios -- Validate data integrity -- Provide fallback content - -### Interactions -- Make interactions intuitive -- Provide visual feedback -- Handle edge cases -- Support accessibility - -## Related Documentation - -- [Widget Development Manager API](WidgetDevelopmentManagerAPI.md) -- [Lock Screen Widget API](LockScreenWidgetAPI.md) -- [Live Activity API](LiveActivityAPI.md) -- [Dynamic Island API](DynamicIslandAPI.md) -- [Data Integration API](DataIntegrationAPI.md) -- [Configuration API](ConfigurationAPI.md) diff --git a/Documentation/HomeScreenWidgetGuide.md b/Documentation/HomeScreenWidgetGuide.md deleted file mode 100644 index 68698c7..0000000 --- a/Documentation/HomeScreenWidgetGuide.md +++ /dev/null @@ -1,592 +0,0 @@ -# Home Screen Widget Guide - - -## Table of Contents -- [Home Screen Widget Guide](#home-screen-widget-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Widget Manager](#3-initialize-widget-manager) -- [Creating Your First Home Screen Widget](#creating-your-first-home-screen-widget) - - [Step 1: Define Widget Configuration](#step-1-define-widget-configuration) - - [Step 2: Create Widget Instance](#step-2-create-widget-instance) - - [Step 3: Define Widget Content](#step-3-define-widget-content) - - [Step 4: Register Widget](#step-4-register-widget) -- [Widget Sizes and Layouts](#widget-sizes-and-layouts) - - [Supported Sizes](#supported-sizes) - - [Layout Guidelines](#layout-guidelines) -- [Data Integration](#data-integration) - - [Setting Up Data Sources](#setting-up-data-sources) - - [Updating Widget with Data](#updating-widget-with-data) -- [Interactive Widgets](#interactive-widgets) - - [Adding Interactions](#adding-interactions) - - [Action Buttons](#action-buttons) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Background Updates](#background-updates) - - [Configuring Background Updates](#configuring-background-updates) - - [Background Data Fetching](#background-data-fetching) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing home screen widgets in iOS applications using the iOS Widget Development Kit. Home screen widgets provide users with quick access to important information and app functionality directly from their home screen. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and WidgetKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for home screen widgets: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Widget Manager - -```swift -let widgetManager = WidgetDevelopmentManager.shared -widgetManager.configure { config in - config.enableHomeScreenWidgets = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Home Screen Widget - -### Step 1: Define Widget Configuration - -```swift -let homeWidgetConfig = HomeWidgetConfiguration() -homeWidgetConfig.widgetSize = .medium -homeWidgetConfig.enableInteractions = true -homeWidgetConfig.enableDeepLinking = true -homeWidgetConfig.refreshInterval = 300 // 5 minutes -``` - -### Step 2: Create Widget Instance - -```swift -let homeWidget = HomeScreenWidget( - kind: "com.yourcompany.app.homewidget", - configuration: homeWidgetConfig -) -``` - -### Step 3: Define Widget Content - -```swift -homeWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "message.fill") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text("Messages") - .font(.headline) - Text("5 new messages") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - Button("Open App") { - // Deep link to app - } - .buttonStyle(.bordered) - } - .padding() - .background(Color(.systemBackground)) -} -``` - -### Step 4: Register Widget - -```swift -let homeWidgetManager = HomeScreenWidgetManager.shared -homeWidgetManager.register(homeWidget) { result in - switch result { - case .success: - print("✅ Home screen widget registered successfully") - case .failure(let error): - print("❌ Registration failed: \(error)") - } -} -``` - -## Widget Sizes and Layouts - -### Supported Sizes - -| Size | Dimensions | Use Case | -|------|------------|----------| -| Small | 158x158 | Quick info, status | -| Medium | 338x158 | Detailed info, actions | -| Large | 338x354 | Rich content, lists | -| Extra Large | 338x548 | Complex layouts | - -### Layout Guidelines - -1. **Small Widgets**: Focus on essential information -2. **Medium Widgets**: Balance information and actions -3. **Large Widgets**: Provide detailed content -4. **Extra Large Widgets**: Complex interactions - -## Data Integration - -### Setting Up Data Sources - -```swift -// Create data source -let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure data manager -let dataManager = WidgetDataManager() -dataManager.configure { config in - config.enableRealTimeUpdates = true - config.enableBackgroundSync = true - config.refreshInterval = 300 -} -``` - -### Updating Widget with Data - -```swift -// Fetch data -dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - // Update widget - homeWidget.update(with: data) { result in - switch result { - case .success: - print("✅ Widget updated with new data") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -## Interactive Widgets - -### Adding Interactions - -```swift -// Create interactive widget -let interactiveConfig = HomeWidgetConfiguration() -interactiveConfig.enableInteractions = true -interactiveConfig.enableDeepLinking = true - -let interactiveWidget = HomeScreenWidget( - kind: "com.yourcompany.app.interactive", - configuration: interactiveConfig -) - -// Add interaction handlers -interactiveWidget.onTap { context in - // Handle tap interaction - print("Widget tapped!") - - // Deep link to app - if let url = URL(string: "yourapp://open") { - context.openURL(url) - } -} - -interactiveWidget.onLongPress { context in - // Handle long press - print("Widget long pressed!") -} -``` - -### Action Buttons - -```swift -interactiveWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Now Playing") - .font(.headline) - Text("Song Title") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom style -let customStyle = WidgetStyle( - backgroundColor: .systemBlue, - cornerRadius: 12, - shadow: WidgetShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = WidgetStylingManager() -stylingManager.applyStyle(customStyle, to: homeWidget) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicTheme( - lightMode: WidgetTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .blue - ), - darkMode: WidgetTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Background Updates - -### Configuring Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -widgetManager.configureBackgroundUpdates(backgroundConfig) -``` - -### Background Data Fetching - -```swift -// Background data source -let backgroundDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/background-data", - cachePolicy: .backgroundFirst -) - -// Configure background fetching -dataManager.configureBackgroundFetching(backgroundDataSource) { result in - switch result { - case .success(let data): - // Update widget in background - homeWidget.update(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Performance Optimization - -### Best Practices - -1. **Minimize Updates**: Only update when necessary -2. **Efficient Data Fetching**: Use caching and background updates -3. **Memory Management**: Properly manage widget resources -4. **Battery Optimization**: Minimize background processing - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = WidgetPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor widget performance -performanceMonitor.startMonitoring(homeWidget) { metrics in - print("Widget performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Registration Failed** - - Check widget configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleWidgetError(_ error: WidgetError) { - switch error { - case .registrationFailed(let message): - print("Registration failed: \(message)") - // Retry registration or show user message - - case .updateFailed(let message): - print("Update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class HomeScreenWidgetTests: XCTestCase { - var homeWidget: HomeScreenWidget! - - override func setUp() { - super.setUp() - let config = HomeWidgetConfiguration() - homeWidget = HomeScreenWidget(kind: "test.widget", configuration: config) - } - - func testWidgetRegistration() { - let expectation = XCTestExpectation(description: "Widget registration") - - let manager = HomeScreenWidgetManager.shared - manager.register(homeWidget) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Registration failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testWidgetUpdate() { - let expectation = XCTestExpectation(description: "Widget update") - - let testData = WidgetData(content: "Test content") - homeWidget.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class HomeScreenWidgetUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testWidgetAppearance() { - // Test widget appears on home screen - let widget = app.otherElements["homeScreenWidget"] - XCTAssertTrue(widget.exists) - } - - func testWidgetInteraction() { - // Test widget interactions - let widget = app.otherElements["homeScreenWidget"] - widget.tap() - - // Verify app opens - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Widget tested on multiple devices -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Widget Not Appearing** - - Check registration status - - Verify permissions - - Test on physical device - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = WidgetDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor widget lifecycle -debugLogger.onWidgetRegistered = { widget in - print("Widget registered: \(widget.kind)") -} - -debugLogger.onWidgetUpdated = { widget in - print("Widget updated: \(widget.kind)") -} - -debugLogger.onWidgetError = { error in - print("Widget error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating home screen widgets using the iOS Widget Development Kit. By following these guidelines, you can create engaging, performant, and user-friendly home screen widgets that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Home Screen Widget API](HomeScreenWidgetAPI.md) documentation. diff --git a/Documentation/InstallationGuide.md b/Documentation/InstallationGuide.md deleted file mode 100644 index fb25f22..0000000 --- a/Documentation/InstallationGuide.md +++ /dev/null @@ -1,122 +0,0 @@ -# Installation Guide - - -## Table of Contents -- [Installation Guide](#installation-guide) -- [Prerequisites](#prerequisites) -- [Installation Methods](#installation-methods) - - [Swift Package Manager (Recommended)](#swift-package-manager-recommended) - - [Manual Installation](#manual-installation) -- [Configuration](#configuration) - - [Basic Setup](#basic-setup) - - [Advanced Configuration](#advanced-configuration) -- [Dependencies](#dependencies) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Support](#support) - - - -## Prerequisites - -- iOS 16.0+ -- Xcode 14.0+ -- Swift 5.9+ -- macOS 13.0+ (for development) - -## Installation Methods - -### Swift Package Manager (Recommended) - -1. Open your Xcode project -2. Go to File → Add Package Dependencies -3. Enter the repository URL: - ``` - https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git - ``` -4. Select version 3.2.0 or later -5. Choose the targets you want to include: - - WidgetKit (Core functionality) - - WidgetTemplates (Pre-built templates) - - LiveDataIntegration (Real-time data) - - WidgetAnalytics (Performance tracking) - -### Manual Installation - -1. Clone the repository: - ```bash - git clone https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git - ``` - -2. Add the package to your project: - ```swift - dependencies: [ - .package(path: "path/to/iOS-Widget-Development-Kit") - ] - ``` - -## Configuration - -### Basic Setup - -```swift -import WidgetKit -import WidgetTemplates -import LiveDataIntegration -import WidgetAnalytics - -// Initialize the widget engine -let widgetEngine = WidgetEngine.shared - -// Register default templates -let weatherTemplate = WeatherWidgetTemplate() -widgetEngine.registerTemplate(weatherTemplate) -``` - -### Advanced Configuration - -```swift -// Configure performance settings -let performanceSettings = WidgetPerformanceSettings( - maxMemoryUsage: 100 * 1024 * 1024, // 100MB - refreshInterval: 30.0, // 30 seconds - enableBatteryOptimization: true -) - -widgetEngine.configurePerformance(performanceSettings) - -// Configure analytics -let analyticsConfig = AnalyticsConfiguration( - enableExternalAnalytics: false, - maxEventLogSize: 1000, - enablePerformanceTracking: true, - enableErrorTracking: true -) - -WidgetAnalyticsService.shared.configure(analyticsConfig) -``` - -## Dependencies - -The framework includes the following dependencies: - -- **Alamofire**: Network requests and API integration -- **SwiftLint**: Code quality enforcement -- **Quick/Nimble**: Testing framework - -These are automatically resolved when using Swift Package Manager. - -## Troubleshooting - -### Common Issues - -1. **Build Errors**: Ensure you're using iOS 16.0+ and Swift 5.9+ -2. **Import Errors**: Check that all targets are properly linked -3. **Runtime Errors**: Verify that templates are registered before use - -### Support - -For additional help, see: -- [Documentation](Documentation/) -- [GitHub Issues](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/issues) -- [Examples](Examples/) \ No newline at end of file diff --git a/Documentation/LiveActivityAPI.md b/Documentation/LiveActivityAPI.md deleted file mode 100644 index c217a86..0000000 --- a/Documentation/LiveActivityAPI.md +++ /dev/null @@ -1,446 +0,0 @@ -# Live Activity API - - -## Table of Contents -- [Live Activity API](#live-activity-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [LiveActivityManager](#liveactivitymanager) - - [LiveActivity](#liveactivity) - - [LiveActivityConfiguration](#liveactivityconfiguration) -- [Activity Types](#activity-types) - - [Supported Activity Types](#supported-activity-types) -- [Content Types](#content-types) - - [Basic Activity Content](#basic-activity-content) - - [Interactive Activity Content](#interactive-activity-content) -- [Usage Examples](#usage-examples) - - [Creating a Simple Live Activity](#creating-a-simple-live-activity) - - [Creating an Interactive Live Activity](#creating-an-interactive-live-activity) -- [Dynamic Island Integration](#dynamic-island-integration) - - [Dynamic Island Views](#dynamic-island-views) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Error Handling](#error-handling) - - [ActivityError Types](#activityerror-types) - - [Error Handling Example](#error-handling-example) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [Design Guidelines](#design-guidelines) - - [Security Considerations](#security-considerations) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 15 to iOS 16+](#from-ios-15-to-ios-16) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tips](#debug-tips) - - - -## Overview - -The Live Activity API provides comprehensive functionality for creating and managing Live Activities in iOS applications. This API enables developers to create engaging, real-time experiences that integrate with the Dynamic Island and Lock Screen. - -## Core Classes - -### LiveActivityManager - -The main manager class for handling Live Activity operations. - -```swift -class LiveActivityManager { - static let shared = LiveActivityManager() - - func start(_ activity: LiveActivity) -> Result - func stop(_ activity: LiveActivity) -> Result - func update(_ activity: LiveActivity, with data: ActivityData) -> Result - func refresh(_ activity: LiveActivity) -> Result -} -``` - -### LiveActivity - -The core activity class for Live Activity implementations. - -```swift -class LiveActivity { - let activityType: String - let configuration: LiveActivityConfiguration - var content: ActivityContent? - - init(activityType: String, configuration: LiveActivityConfiguration) - func setContent(_ content: @escaping (ActivityContext) -> ActivityContent) - func update(with data: ActivityData) -> Result -} -``` - -### LiveActivityConfiguration - -Configuration options for Live Activities. - -```swift -struct LiveActivityConfiguration { - var enableDynamicIsland: Bool - var enableLockScreen: Bool - var enableNotifications: Bool - var updateInterval: TimeInterval - var enableInteractions: Bool - var enableBackgroundUpdates: Bool - var maxDuration: TimeInterval -} -``` - -## Activity Types - -### Supported Activity Types - -```swift -enum ActivityType { - case delivery // Package delivery tracking - case ride // Ride sharing - case food // Food delivery - case workout // Fitness tracking - case music // Music playback - case timer // Timer/countdown - case game // Game status - case custom // Custom activity -} -``` - -## Content Types - -### Basic Activity Content - -```swift -struct BasicActivityContent: ActivityContent { - let title: String - let subtitle: String? - let icon: String? - let progress: Double? - let backgroundColor: Color - let textColor: Color -} -``` - -### Interactive Activity Content - -```swift -struct InteractiveActivityContent: ActivityContent { - let title: String - let subtitle: String? - let actions: [ActivityAction] - let progress: Double? - let backgroundColor: Color - let textColor: Color -} -``` - -## Usage Examples - -### Creating a Simple Live Activity - -```swift -// Create Live Activity manager -let activityManager = LiveActivityManager.shared - -// Configure activity -let config = LiveActivityConfiguration() -config.enableDynamicIsland = true -config.enableLockScreen = true -config.updateInterval = 30 // 30 seconds -config.maxDuration = 3600 // 1 hour - -// Create activity -let liveActivity = LiveActivity( - activityType: "com.company.app.delivery", - configuration: config -) - -// Set activity content -liveActivity.setContent { context in - BasicActivityContent( - title: "Package Delivery", - subtitle: "Out for delivery", - icon: "shippingbox.fill", - progress: 0.8, - backgroundColor: .systemBlue, - textColor: .white - ) -} - -// Start activity -let result = activityManager.start(liveActivity) -switch result { -case .success(let activity): - print("✅ Live Activity started successfully") - print("Activity ID: \(activity.id)") -case .failure(let error): - print("❌ Activity start failed: \(error)") -} -``` - -### Creating an Interactive Live Activity - -```swift -// Create interactive activity -let interactiveConfig = LiveActivityConfiguration() -interactiveConfig.enableDynamicIsland = true -interactiveConfig.enableInteractions = true -interactiveConfig.enableBackgroundUpdates = true - -let interactiveActivity = LiveActivity( - activityType: "com.company.app.music", - configuration: interactiveConfig -) - -// Set interactive content -interactiveActivity.setContent { context in - InteractiveActivityContent( - title: "Now Playing", - subtitle: "Artist Name", - actions: [ - ActivityAction(title: "Play", icon: "play.fill", action: .play), - ActivityAction(title: "Pause", icon: "pause.fill", action: .pause), - ActivityAction(title: "Next", icon: "forward.fill", action: .next) - ], - progress: 0.6, - backgroundColor: .systemBackground, - textColor: .primary - ) -} - -// Start interactive activity -activityManager.start(interactiveActivity) -``` - -## Dynamic Island Integration - -### Dynamic Island Views - -```swift -// Configure Dynamic Island -let islandConfig = DynamicIslandConfiguration() -islandConfig.enableCompactView = true -islandConfig.enableExpandedView = true -islandConfig.enableMinimalView = true - -// Create Dynamic Island activity -let islandActivity = DynamicIslandActivity( - activityType: "com.company.app.music", - configuration: islandConfig -) - -// Set Dynamic Island views -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - Text("Now Playing") - .font(.caption) - } -} - -islandActivity.setExpandedView { context in - VStack { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} - -// Start Dynamic Island activity -activityManager.start(islandActivity) -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = ActivityDataSource( - type: .api, - endpoint: "https://api.company.com/activity-data", - cachePolicy: .cacheFirst -) - -// Fetch and update data -dataSource.fetchData { result in - switch result { - case .success(let data): - // Update activity with new data - liveActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Live Activity updated") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 300 // 5 minutes - -// Setup background updates -activityManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Error Handling - -### ActivityError Types - -```swift -enum ActivityError: Error { - case startFailed(String) - case stopFailed(String) - case updateFailed(String) - case configurationError(String) - case dataError(String) - case networkError(String) - case permissionError(String) - case timeoutError(String) -} -``` - -### Error Handling Example - -```swift -func handleActivityError(_ error: ActivityError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Handle start error - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Handle update error - case .configurationError(let message): - print("Configuration error: \(message)") - // Handle configuration error - case .dataError(let message): - print("Data error: \(message)") - // Handle data error - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - case .permissionError(let message): - print("Permission error: \(message)") - // Handle permission error - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout error - } -} -``` - -## Best Practices - -### Performance Optimization - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Design Guidelines - -1. **Clear Information**: Display essential information clearly -2. **Consistent Styling**: Maintain consistent visual design -3. **Accessibility**: Ensure activities are accessible -4. **User Privacy**: Respect user privacy settings - -### Security Considerations - -1. **Data Protection**: Secure sensitive data -2. **Permission Handling**: Request only necessary permissions -3. **Network Security**: Use secure network connections -4. **Error Handling**: Handle errors gracefully - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `start` | Start a Live Activity | `LiveActivity` | `Result` | -| `stop` | Stop a Live Activity | `LiveActivity` | `Result` | -| `update` | Update activity with new data | `LiveActivity`, `ActivityData` | `Result` | -| `refresh` | Refresh activity content | `LiveActivity` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `activityType` | `String` | Unique activity type identifier | -| `configuration` | `LiveActivityConfiguration` | Activity configuration | -| `content` | `ActivityContent?` | Activity content | -| `isActive` | `Bool` | Activity status | - -## Migration Guide - -### From iOS 15 to iOS 16+ - -```swift -// iOS 15 approach (deprecated) -let oldActivity = Activity(kind: "com.app.activity") - -// iOS 16+ approach -let newActivity = LiveActivity( - activityType: "com.app.activity", - configuration: LiveActivityConfiguration() -) -``` - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting**: Check permissions and configuration -2. **Updates Not Working**: Verify data source and network connectivity -3. **Performance Issues**: Optimize data fetching and background processing -4. **Crash Issues**: Implement proper error handling - -### Debug Tips - -1. **Enable Logging**: Use debug logging for troubleshooting -2. **Test on Device**: Always test on physical devices -3. **Monitor Performance**: Use performance monitoring tools -4. **Check Permissions**: Verify required permissions are granted diff --git a/Documentation/LiveActivityGuide.md b/Documentation/LiveActivityGuide.md deleted file mode 100644 index 12bbee4..0000000 --- a/Documentation/LiveActivityGuide.md +++ /dev/null @@ -1,772 +0,0 @@ -# Live Activity Guide - - -## Table of Contents -- [Live Activity Guide](#live-activity-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Activity Manager](#3-initialize-activity-manager) -- [Creating Your First Live Activity](#creating-your-first-live-activity) - - [Step 1: Define Activity Configuration](#step-1-define-activity-configuration) - - [Step 2: Create Activity Instance](#step-2-create-activity-instance) - - [Step 3: Define Activity Content](#step-3-define-activity-content) - - [Step 4: Start Activity](#step-4-start-activity) -- [Activity Types and Use Cases](#activity-types-and-use-cases) - - [Delivery Tracking](#delivery-tracking) - - [Ride Sharing](#ride-sharing) - - [Music Playback](#music-playback) -- [Dynamic Island Integration](#dynamic-island-integration) - - [Compact View](#compact-view) - - [Expanded View](#expanded-view) - - [Minimal View](#minimal-view) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Interactive Elements](#interactive-elements) - - [Action Buttons](#action-buttons) - - [Gesture Recognition](#gesture-recognition) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing Live Activities in iOS applications using the iOS Widget Development Kit. Live Activities provide users with real-time, engaging experiences that integrate with the Dynamic Island and Lock Screen. - -## Prerequisites - -- iOS 16.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and ActivityKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for Live Activities: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -NSSupportsLiveActivitiesBackgroundProcessing - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Activity Manager - -```swift -let activityManager = LiveActivityManager.shared -activityManager.configure { config in - config.enableDynamicIsland = true - config.enableLockScreen = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Live Activity - -### Step 1: Define Activity Configuration - -```swift -let activityConfig = LiveActivityConfiguration() -activityConfig.enableDynamicIsland = true -activityConfig.enableLockScreen = true -activityConfig.enableNotifications = true -activityConfig.updateInterval = 30 // 30 seconds -activityConfig.maxDuration = 3600 // 1 hour -``` - -### Step 2: Create Activity Instance - -```swift -let liveActivity = LiveActivity( - activityType: "com.yourcompany.app.delivery", - configuration: activityConfig -) -``` - -### Step 3: Define Activity Content - -```swift -liveActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Order #12345") - .font(.headline) - - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("2:30 PM") - .font(.caption2) - .foregroundColor(.secondary) - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - .tint(.blue) - } - .padding() - .background(Color(.systemBackground)) -} -``` - -### Step 4: Start Activity - -```swift -activityManager.start(liveActivity) { result in - switch result { - case .success(let activity): - print("✅ Live Activity started successfully") - print("Activity ID: \(activity.id)") - case .failure(let error): - print("❌ Activity start failed: \(error)") - } -} -``` - -## Activity Types and Use Cases - -### Delivery Tracking - -```swift -// Delivery activity configuration -let deliveryConfig = LiveActivityConfiguration() -deliveryConfig.enableDynamicIsland = true -deliveryConfig.enableLockScreen = true -deliveryConfig.updateInterval = 60 // 1 minute - -let deliveryActivity = LiveActivity( - activityType: "com.yourcompany.app.delivery", - configuration: deliveryConfig -) - -deliveryActivity.setContent { context in - VStack(spacing: 8) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Estimated arrival: 3:00 PM") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.7) - .progressViewStyle(.linear) - } - .padding() -} -``` - -### Ride Sharing - -```swift -// Ride sharing activity -let rideConfig = LiveActivityConfiguration() -rideConfig.enableDynamicIsland = true -rideConfig.enableInteractions = true - -let rideActivity = LiveActivity( - activityType: "com.yourcompany.app.ride", - configuration: rideConfig -) - -rideActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "car.fill") - .foregroundColor(.green) - - VStack(alignment: .leading) { - Text("Your ride is arriving") - .font(.headline) - Text("Driver: John Doe") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("2 min") - .font(.caption2) - .foregroundColor(.green) - } - - HStack(spacing: 16) { - Button("Call Driver") { - // Call driver action - } - .buttonStyle(.bordered) - - Button("Cancel Ride") { - // Cancel ride action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Music Playback - -```swift -// Music activity -let musicConfig = LiveActivityConfiguration() -musicConfig.enableDynamicIsland = true -musicConfig.enableInteractions = true - -let musicActivity = LiveActivity( - activityType: "com.yourcompany.app.music", - configuration: musicConfig -) - -musicActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Now Playing") - .font(.headline) - Text("Song Title - Artist") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} -``` - -## Dynamic Island Integration - -### Compact View - -```swift -// Configure Dynamic Island -let islandConfig = DynamicIslandConfiguration() -islandConfig.enableCompactView = true -islandConfig.enableExpandedView = true -islandConfig.enableMinimalView = true - -let islandActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.music", - configuration: islandConfig -) - -// Set compact view -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} -``` - -### Expanded View - -```swift -// Set expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} -``` - -### Minimal View - -```swift -// Set minimal view -islandActivity.setMinimalView { context in - Image(systemName: "music.note") - .foregroundColor(.purple) -} -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = ActivityDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/activity-data", - cachePolicy: .cacheFirst -) - -// Configure real-time updates -let realTimeManager = RealTimeUpdateManager() -realTimeManager.configure { config in - config.enableWebSocket = true - config.enablePushNotifications = true - config.enableBackgroundUpdates = true -} - -// Setup real-time connection -realTimeManager.connect { result in - switch result { - case .success: - print("✅ Real-time connection established") - - // Listen for updates - realTimeManager.onDataUpdate { data in - print("📱 Real-time data received: \(data)") - - // Update activity with real-time data - liveActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Activity updated with real-time data") - case .failure(let error): - print("❌ Real-time update failed: \(error)") - } - } - } - case .failure(let error): - print("❌ Real-time connection failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -activityManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Interactive Elements - -### Action Buttons - -```swift -// Add action buttons to activity -liveActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Track Package") { - // Track package action - } - .buttonStyle(.bordered) - - Button("Contact Driver") { - // Contact driver action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Gesture Recognition - -```swift -// Add gesture recognition -liveActivity.onTap { context in - print("Activity tapped!") - // Handle tap gesture -} - -liveActivity.onLongPress { context in - print("Activity long pressed!") - // Handle long press gesture -} - -liveActivity.onSwipe { direction in - print("Activity swiped: \(direction)") - // Handle swipe gesture -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom activity style -let customStyle = ActivityStyle( - backgroundColor: .systemBlue, - cornerRadius: 12, - shadow: ActivityShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = ActivityStylingManager() -stylingManager.applyStyle(customStyle, to: liveActivity) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicActivityTheme( - lightMode: ActivityTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .blue - ), - darkMode: ActivityTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicActivityThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Performance Optimization - -### Best Practices - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = ActivityPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor activity performance -performanceMonitor.startMonitoring(liveActivity) { metrics in - print("Activity performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Activity Start Failed** - - Check activity configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleActivityError(_ error: ActivityError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Retry start or show user message - - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout issues - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class LiveActivityTests: XCTestCase { - var liveActivity: LiveActivity! - - override func setUp() { - super.setUp() - let config = LiveActivityConfiguration() - liveActivity = LiveActivity(activityType: "test.activity", configuration: config) - } - - func testActivityStart() { - let expectation = XCTestExpectation(description: "Activity start") - - let manager = LiveActivityManager.shared - manager.start(liveActivity) { result in - switch result { - case .success(let activity): - expectation.fulfill() - case .failure(let error): - XCTFail("Activity start failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testActivityUpdate() { - let expectation = XCTestExpectation(description: "Activity update") - - let testData = ActivityData(content: "Test content") - liveActivity.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class LiveActivityUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testActivityAppearance() { - // Test activity appears on lock screen - let activity = app.otherElements["liveActivity"] - XCTAssertTrue(activity.exists) - } - - func testActivityInteraction() { - // Test activity interactions - let activity = app.otherElements["liveActivity"] - activity.tap() - - // Verify interaction handled - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Activity tested on multiple devices -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting** - - Check activity configuration - - Verify permissions - - Test on physical device - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = ActivityDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor activity lifecycle -debugLogger.onActivityStarted = { activity in - print("Activity started: \(activity.activityType)") -} - -debugLogger.onActivityUpdated = { activity in - print("Activity updated: \(activity.activityType)") -} - -debugLogger.onActivityStopped = { activity in - print("Activity stopped: \(activity.activityType)") -} - -debugLogger.onActivityError = { error in - print("Activity error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating Live Activities using the iOS Widget Development Kit. By following these guidelines, you can create engaging, real-time experiences that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Live Activity API](LiveActivityAPI.md) documentation. diff --git a/Documentation/LockScreenWidgetAPI.md b/Documentation/LockScreenWidgetAPI.md deleted file mode 100644 index 649fe61..0000000 --- a/Documentation/LockScreenWidgetAPI.md +++ /dev/null @@ -1,349 +0,0 @@ -# Lock Screen Widget API - - -## Table of Contents -- [Lock Screen Widget API](#lock-screen-widget-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [LockScreenWidgetManager](#lockscreenwidgetmanager) - - [LockScreenWidget](#lockscreenwidget) - - [LockWidgetConfiguration](#lockwidgetconfiguration) -- [Widget Sizes](#widget-sizes) - - [Supported Sizes](#supported-sizes) -- [Content Types](#content-types) - - [Basic Content](#basic-content) - - [Interactive Content](#interactive-content) -- [Usage Examples](#usage-examples) - - [Creating a Simple Lock Screen Widget](#creating-a-simple-lock-screen-widget) - - [Creating an Interactive Lock Screen Widget](#creating-an-interactive-lock-screen-widget) -- [Data Integration](#data-integration) - - [Updating Widget with Data](#updating-widget-with-data) -- [Error Handling](#error-handling) - - [WidgetError Types](#widgeterror-types) - - [Error Handling Example](#error-handling-example) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [Design Guidelines](#design-guidelines) - - [Security Considerations](#security-considerations) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tips](#debug-tips) - - - -## Overview - -The Lock Screen Widget API provides comprehensive functionality for creating and managing lock screen widgets in iOS applications. This API enables developers to create engaging, informative, and interactive widgets that appear on the device's lock screen. - -## Core Classes - -### LockScreenWidgetManager - -The main manager class for handling lock screen widget operations. - -```swift -class LockScreenWidgetManager { - static let shared = LockScreenWidgetManager() - - func register(_ widget: LockScreenWidget) -> Result - func unregister(_ widget: LockScreenWidget) -> Result - func update(_ widget: LockScreenWidget, with data: WidgetData) -> Result - func refresh(_ widget: LockScreenWidget) -> Result -} -``` - -### LockScreenWidget - -The core widget class for lock screen implementations. - -```swift -class LockScreenWidget { - let kind: String - let configuration: LockWidgetConfiguration - var content: WidgetContent? - - init(kind: String, configuration: LockWidgetConfiguration) - func setContent(_ content: @escaping (WidgetContext) -> WidgetContent) - func update(with data: WidgetData) -> Result -} -``` - -### LockWidgetConfiguration - -Configuration options for lock screen widgets. - -```swift -struct LockWidgetConfiguration { - var widgetSize: WidgetSize - var enableGlance: Bool - var enableComplications: Bool - var refreshInterval: TimeInterval - var enableInteractions: Bool - var enableDeepLinking: Bool - var enableBackgroundUpdates: Bool -} -``` - -## Widget Sizes - -### Supported Sizes - -```swift -enum WidgetSize { - case small // 158x158 points - case medium // 338x158 points - case large // 338x354 points - case extraLarge // 338x548 points -} -``` - -## Content Types - -### Basic Content - -```swift -struct BasicWidgetContent: WidgetContent { - let title: String - let subtitle: String? - let icon: String? - let backgroundColor: Color - let textColor: Color -} -``` - -### Interactive Content - -```swift -struct InteractiveWidgetContent: WidgetContent { - let title: String - let subtitle: String? - let actions: [WidgetAction] - let backgroundColor: Color - let textColor: Color -} -``` - -## Usage Examples - -### Creating a Simple Lock Screen Widget - -```swift -// Create lock screen widget manager -let lockWidgetManager = LockScreenWidgetManager.shared - -// Configure widget -let config = LockWidgetConfiguration() -config.widgetSize = .small -config.enableGlance = true -config.refreshInterval = 300 // 5 minutes - -// Create widget -let lockWidget = LockScreenWidget( - kind: "com.company.app.lockwidget", - configuration: config -) - -// Set widget content -lockWidget.setContent { context in - BasicWidgetContent( - title: "Messages", - subtitle: "3 new", - icon: "message.fill", - backgroundColor: .systemBlue, - textColor: .white - ) -} - -// Register widget -let result = lockWidgetManager.register(lockWidget) -switch result { -case .success: - print("✅ Lock screen widget registered successfully") -case .failure(let error): - print("❌ Registration failed: \(error)") -} -``` - -### Creating an Interactive Lock Screen Widget - -```swift -// Create interactive widget -let interactiveConfig = LockWidgetConfiguration() -interactiveConfig.widgetSize = .medium -interactiveConfig.enableInteractions = true -interactiveConfig.enableDeepLinking = true - -let interactiveWidget = LockScreenWidget( - kind: "com.company.app.interactive.lock", - configuration: interactiveConfig -) - -// Set interactive content -interactiveWidget.setContent { context in - InteractiveWidgetContent( - title: "Quick Actions", - subtitle: "Tap to open", - actions: [ - WidgetAction(title: "Messages", icon: "message.fill", action: .openApp), - WidgetAction(title: "Camera", icon: "camera.fill", action: .openCamera) - ], - backgroundColor: .systemBackground, - textColor: .primary - ) -} - -// Register interactive widget -lockWidgetManager.register(interactiveWidget) -``` - -## Data Integration - -### Updating Widget with Data - -```swift -// Create data source -let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.company.com/lock-widget-data", - cachePolicy: .cacheFirst -) - -// Fetch and update data -dataSource.fetchData { result in - switch result { - case .success(let data): - // Update widget with new data - lockWidget.update(with: data) { result in - switch result { - case .success: - print("✅ Lock screen widget updated") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -## Error Handling - -### WidgetError Types - -```swift -enum WidgetError: Error { - case registrationFailed(String) - case updateFailed(String) - case configurationError(String) - case dataError(String) - case networkError(String) - case permissionError(String) -} -``` - -### Error Handling Example - -```swift -func handleWidgetError(_ error: WidgetError) { - switch error { - case .registrationFailed(let message): - print("Registration failed: \(message)") - // Handle registration error - case .updateFailed(let message): - print("Update failed: \(message)") - // Handle update error - case .configurationError(let message): - print("Configuration error: \(message)") - // Handle configuration error - case .dataError(let message): - print("Data error: \(message)") - // Handle data error - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - case .permissionError(let message): - print("Permission error: \(message)") - // Handle permission error - } -} -``` - -## Best Practices - -### Performance Optimization - -1. **Minimize Updates**: Only update widgets when necessary -2. **Efficient Data Fetching**: Use caching and background updates -3. **Memory Management**: Properly manage widget resources -4. **Battery Optimization**: Minimize background processing - -### Design Guidelines - -1. **Clear Information**: Display essential information clearly -2. **Consistent Styling**: Maintain consistent visual design -3. **Accessibility**: Ensure widgets are accessible -4. **User Privacy**: Respect user privacy settings - -### Security Considerations - -1. **Data Protection**: Secure sensitive data -2. **Permission Handling**: Request only necessary permissions -3. **Network Security**: Use secure network connections -4. **Error Handling**: Handle errors gracefully - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `register` | Register a lock screen widget | `LockScreenWidget` | `Result` | -| `unregister` | Unregister a lock screen widget | `LockScreenWidget` | `Result` | -| `update` | Update widget with new data | `LockScreenWidget`, `WidgetData` | `Result` | -| `refresh` | Refresh widget content | `LockScreenWidget` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `kind` | `String` | Unique widget identifier | -| `configuration` | `LockWidgetConfiguration` | Widget configuration | -| `content` | `WidgetContent?` | Widget content | -| `isRegistered` | `Bool` | Registration status | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldWidget = Widget(kind: "com.app.widget") - -// iOS 15+ approach -let newWidget = LockScreenWidget( - kind: "com.app.widget", - configuration: LockWidgetConfiguration() -) -``` - -## Troubleshooting - -### Common Issues - -1. **Widget Not Appearing**: Check registration and permissions -2. **Updates Not Working**: Verify data source and network connectivity -3. **Performance Issues**: Optimize data fetching and caching -4. **Crash Issues**: Implement proper error handling - -### Debug Tips - -1. **Enable Logging**: Use debug logging for troubleshooting -2. **Test on Device**: Always test on physical devices -3. **Monitor Performance**: Use performance monitoring tools -4. **Check Permissions**: Verify required permissions are granted diff --git a/Documentation/LockScreenWidgetGuide.md b/Documentation/LockScreenWidgetGuide.md deleted file mode 100644 index d105200..0000000 --- a/Documentation/LockScreenWidgetGuide.md +++ /dev/null @@ -1,589 +0,0 @@ -# Lock Screen Widget Guide - - -## Table of Contents -- [Lock Screen Widget Guide](#lock-screen-widget-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Widget Manager](#3-initialize-widget-manager) -- [Creating Your First Lock Screen Widget](#creating-your-first-lock-screen-widget) - - [Step 1: Define Widget Configuration](#step-1-define-widget-configuration) - - [Step 2: Create Widget Instance](#step-2-create-widget-instance) - - [Step 3: Define Widget Content](#step-3-define-widget-content) - - [Step 4: Register Widget](#step-4-register-widget) -- [Widget Sizes and Layouts](#widget-sizes-and-layouts) - - [Supported Sizes](#supported-sizes) - - [Layout Guidelines](#layout-guidelines) -- [Data Integration](#data-integration) - - [Setting Up Data Sources](#setting-up-data-sources) - - [Updating Widget with Data](#updating-widget-with-data) -- [Interactive Widgets](#interactive-widgets) - - [Adding Interactions](#adding-interactions) - - [Action Buttons](#action-buttons) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Background Updates](#background-updates) - - [Configuring Background Updates](#configuring-background-updates) - - [Background Data Fetching](#background-data-fetching) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing lock screen widgets in iOS applications using the iOS Widget Development Kit. Lock screen widgets provide users with quick access to important information without unlocking their device. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and WidgetKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for lock screen widgets: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Widget Manager - -```swift -let widgetManager = WidgetDevelopmentManager.shared -widgetManager.configure { config in - config.enableLockScreenWidgets = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Lock Screen Widget - -### Step 1: Define Widget Configuration - -```swift -let lockWidgetConfig = LockWidgetConfiguration() -lockWidgetConfig.widgetSize = .small -lockWidgetConfig.enableGlance = true -lockWidgetConfig.enableComplications = true -lockWidgetConfig.refreshInterval = 300 // 5 minutes -``` - -### Step 2: Create Widget Instance - -```swift -let lockWidget = LockScreenWidget( - kind: "com.yourcompany.app.lockscreen", - configuration: lockWidgetConfig -) -``` - -### Step 3: Define Widget Content - -```swift -lockWidget.setContent { context in - VStack(spacing: 8) { - HStack { - Image(systemName: "message.fill") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text("Messages") - .font(.caption) - .foregroundColor(.secondary) - - Text("3 new") - .font(.caption2) - .foregroundColor(.primary) - } - - Spacer() - } - .padding(.horizontal, 8) - } - .background(Color(.systemBackground)) -} -``` - -### Step 4: Register Widget - -```swift -let lockWidgetManager = LockScreenWidgetManager.shared -lockWidgetManager.register(lockWidget) { result in - switch result { - case .success: - print("✅ Lock screen widget registered successfully") - case .failure(let error): - print("❌ Registration failed: \(error)") - } -} -``` - -## Widget Sizes and Layouts - -### Supported Sizes - -| Size | Dimensions | Use Case | -|------|------------|----------| -| Small | 158x158 | Quick info, status | -| Medium | 338x158 | Detailed info, actions | -| Large | 338x354 | Rich content, lists | -| Extra Large | 338x548 | Complex layouts | - -### Layout Guidelines - -1. **Small Widgets**: Focus on essential information -2. **Medium Widgets**: Balance information and actions -3. **Large Widgets**: Provide detailed content -4. **Extra Large Widgets**: Complex interactions - -## Data Integration - -### Setting Up Data Sources - -```swift -// Create data source -let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure data manager -let dataManager = WidgetDataManager() -dataManager.configure { config in - config.enableRealTimeUpdates = true - config.enableBackgroundSync = true - config.refreshInterval = 300 -} -``` - -### Updating Widget with Data - -```swift -// Fetch data -dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - // Update widget - lockWidget.update(with: data) { result in - switch result { - case .success: - print("✅ Widget updated with new data") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -## Interactive Widgets - -### Adding Interactions - -```swift -// Create interactive widget -let interactiveConfig = LockWidgetConfiguration() -interactiveConfig.enableInteractions = true -interactiveConfig.enableDeepLinking = true - -let interactiveWidget = LockScreenWidget( - kind: "com.yourcompany.app.interactive", - configuration: interactiveConfig -) - -// Add interaction handlers -interactiveWidget.onTap { context in - // Handle tap interaction - print("Widget tapped!") - - // Deep link to app - if let url = URL(string: "yourapp://open") { - context.openURL(url) - } -} - -interactiveWidget.onLongPress { context in - // Handle long press - print("Widget long pressed!") -} -``` - -### Action Buttons - -```swift -interactiveWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Now Playing") - .font(.headline) - Text("Song Title") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom style -let customStyle = WidgetStyle( - backgroundColor: .systemBlue, - cornerRadius: 12, - shadow: WidgetShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = WidgetStylingManager() -stylingManager.applyStyle(customStyle, to: lockWidget) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicTheme( - lightMode: WidgetTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .blue - ), - darkMode: WidgetTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Background Updates - -### Configuring Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -widgetManager.configureBackgroundUpdates(backgroundConfig) -``` - -### Background Data Fetching - -```swift -// Background data source -let backgroundDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/background-data", - cachePolicy: .backgroundFirst -) - -// Configure background fetching -dataManager.configureBackgroundFetching(backgroundDataSource) { result in - switch result { - case .success(let data): - // Update widget in background - lockWidget.update(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Performance Optimization - -### Best Practices - -1. **Minimize Updates**: Only update when necessary -2. **Efficient Data Fetching**: Use caching and background updates -3. **Memory Management**: Properly manage widget resources -4. **Battery Optimization**: Minimize background processing - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = WidgetPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor widget performance -performanceMonitor.startMonitoring(lockWidget) { metrics in - print("Widget performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Registration Failed** - - Check widget configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleWidgetError(_ error: WidgetError) { - switch error { - case .registrationFailed(let message): - print("Registration failed: \(message)") - // Retry registration or show user message - - case .updateFailed(let message): - print("Update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class LockScreenWidgetTests: XCTestCase { - var lockWidget: LockScreenWidget! - - override func setUp() { - super.setUp() - let config = LockWidgetConfiguration() - lockWidget = LockScreenWidget(kind: "test.widget", configuration: config) - } - - func testWidgetRegistration() { - let expectation = XCTestExpectation(description: "Widget registration") - - let manager = LockScreenWidgetManager.shared - manager.register(lockWidget) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Registration failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testWidgetUpdate() { - let expectation = XCTestExpectation(description: "Widget update") - - let testData = WidgetData(content: "Test content") - lockWidget.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class LockScreenWidgetUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testWidgetAppearance() { - // Test widget appears on lock screen - let widget = app.otherElements["lockScreenWidget"] - XCTAssertTrue(widget.exists) - } - - func testWidgetInteraction() { - // Test widget interactions - let widget = app.otherElements["lockScreenWidget"] - widget.tap() - - // Verify app opens - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Widget tested on multiple devices -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Widget Not Appearing** - - Check registration status - - Verify permissions - - Test on physical device - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = WidgetDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor widget lifecycle -debugLogger.onWidgetRegistered = { widget in - print("Widget registered: \(widget.kind)") -} - -debugLogger.onWidgetUpdated = { widget in - print("Widget updated: \(widget.kind)") -} - -debugLogger.onWidgetError = { error in - print("Widget error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating lock screen widgets using the iOS Widget Development Kit. By following these guidelines, you can create engaging, performant, and user-friendly lock screen widgets that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Lock Screen Widget API](LockScreenWidgetAPI.md) documentation. diff --git a/Documentation/Performance.md b/Documentation/Performance.md deleted file mode 100644 index 9cc23c9..0000000 --- a/Documentation/Performance.md +++ /dev/null @@ -1,536 +0,0 @@ -# ⚡ Performance Guide - iOS Widget Development Kit - -## 📋 Table of Contents - -- [Performance Overview](#performance-overview) -- [Performance Targets](#performance-targets) -- [Optimization Strategies](#optimization-strategies) -- [Performance Monitoring](#performance-monitoring) -- [Benchmarking](#benchmarking) -- [Memory Management](#memory-management) -- [Battery Optimization](#battery-optimization) -- [Network Performance](#network-performance) -- [Rendering Optimization](#rendering-optimization) -- [Troubleshooting](#troubleshooting) - ---- - -## 🎯 Performance Overview - -The iOS Widget Development Kit is engineered for **exceptional performance** with enterprise-grade optimization strategies. Our framework delivers **40% faster widget rendering** compared to standard WidgetKit implementations while maintaining **60% less memory consumption** and **50% reduced battery drain**. - -### Core Performance Philosophy -- **Performance is a Feature**: Treat performance as a user-facing feature, not an afterthought -- **Measure First**: Base optimization decisions on actual measurements, not assumptions -- **Continuous Monitoring**: Real-time performance tracking and automated optimization -- **Resource Awareness**: Intelligent resource management based on device capabilities - ---- - -## 🎯 Performance Targets - -### 📱 Application Performance -| Metric | Target | Excellent | Good | Needs Improvement | -|--------|--------|-----------|------|-------------------| -| **Cold Launch** | < 0.8s | < 0.5s | 0.5s - 0.8s | > 0.8s | -| **Warm Launch** | < 0.3s | < 0.2s | 0.2s - 0.3s | > 0.3s | -| **Widget Rendering** | < 90ms | < 50ms | 50ms - 90ms | > 90ms | -| **Frame Rate** | 60fps | 60fps | 55-60fps | < 55fps | -| **Memory Usage** | < 100MB | < 50MB | 50MB - 100MB | > 100MB | -| **Battery Impact** | < 2.5%/hour | < 1%/hour | 1% - 2.5%/hour | > 2.5%/hour | - -### 🔄 Widget-Specific Performance -| Widget Operation | Target | Notes | -|------------------|--------|-------| -| **Widget Load Time** | < 100ms | From tap to display | -| **Data Refresh** | < 200ms | Background data updates | -| **Animation Smoothness** | 60fps | All widget animations | -| **Memory per Widget** | < 5MB | Per active widget instance | -| **CPU Usage** | < 10% | During active operations | - -### 🌐 Network Performance -| Operation | Target | Timeout | Retry Strategy | -|-----------|--------|---------|----------------| -| **API Calls** | < 500ms | 10s | Exponential backoff | -| **Image Loading** | < 1s | 15s | Progressive enhancement | -| **Background Sync** | < 2s | 30s | Smart scheduling | -| **Real-time Updates** | < 100ms | 5s | WebSocket fallback | - ---- - -## 🚀 Optimization Strategies - -### 1. **Widget Rendering Optimization** - -#### Lazy Loading -```swift -struct OptimizedWidgetView: View { - @State private var isLoaded = false - - var body: some View { - Group { - if isLoaded { - ComplexWidgetContent() - } else { - WidgetPlaceholder() - .onAppear { - Task { - await loadContentAsync() - isLoaded = true - } - } - } - } - } -} -``` - -#### View Optimization -```swift -// ✅ Optimized approach -struct PerformantWidgetView: View { - let viewModel: WidgetViewModel - - var body: some View { - LazyVStack(spacing: 8) { - ForEach(viewModel.items.indices, id: \.self) { index in - WidgetItemView(item: viewModel.items[index]) - .equatable() // Prevent unnecessary redraws - } - } - .drawingGroup() // Composite into single layer - } -} -``` - -### 2. **Memory Management** - -#### Smart Caching -```swift -class PerformantCacheManager { - private let memoryCache = NSCache() - private let diskCache: DiskCache - - init() { - // Configure memory limits based on device capabilities - memoryCache.countLimit = 100 - memoryCache.totalCostLimit = 50 * 1024 * 1024 // 50MB - - // Memory pressure handling - NotificationCenter.default.addObserver( - self, - selector: #selector(didReceiveMemoryWarning), - name: UIApplication.didReceiveMemoryWarningNotification, - object: nil - ) - } - - @objc private func didReceiveMemoryWarning() { - memoryCache.removeAllObjects() - performGarbageCollection() - } -} -``` - -#### Memory Pool Pattern -```swift -class WidgetMemoryPool { - private var availableWidgets: [ReusableWidget] = [] - private let maxPoolSize = 20 - - func dequeueWidget() -> ReusableWidget { - if let widget = availableWidgets.popLast() { - return widget - } - return ReusableWidget() - } - - func enqueueWidget(_ widget: ReusableWidget) { - guard availableWidgets.count < maxPoolSize else { return } - widget.reset() - availableWidgets.append(widget) - } -} -``` - -### 3. **Background Processing** - -#### Intelligent Task Scheduling -```swift -class BackgroundTaskScheduler { - private let queue = DispatchQueue(label: "widget.background", qos: .utility) - private var pendingTasks: [BackgroundTask] = [] - - func scheduleTask(_ task: BackgroundTask) { - queue.async { [weak self] in - self?.processTasks() - } - } - - private func processTasks() { - // Battery level consideration - guard UIDevice.current.batteryLevel > 0.2 else { return } - - // Network availability check - guard NetworkMonitor.shared.isConnected else { return } - - // Execute tasks with priority - pendingTasks.sort { $0.priority > $1.priority } - processPendingTasks() - } -} -``` - ---- - -## 📊 Performance Monitoring - -### Real-time Metrics Collection -```swift -class PerformanceMonitor { - static let shared = PerformanceMonitor() - - private var metrics: [PerformanceMetric] = [] - private let metricsQueue = DispatchQueue(label: "metrics") - - func recordMetric(_ metric: PerformanceMetric) { - metricsQueue.async { - self.metrics.append(metric) - self.analyzePerformance() - } - } - - private func analyzePerformance() { - let recentMetrics = metrics.suffix(100) - - // Detect performance regressions - if let regression = detectRegression(in: recentMetrics) { - notifyPerformanceIssue(regression) - } - - // Auto-optimization triggers - if shouldOptimize(based: recentMetrics) { - triggerOptimization() - } - } -} -``` - -### Performance Dashboards -```swift -struct PerformanceDashboard: View { - @StateObject private var monitor = PerformanceMonitor.shared - - var body: some View { - VStack(spacing: 16) { - PerformanceChart( - title: "Widget Render Time", - data: monitor.renderTimeMetrics, - target: 90, - unit: "ms" - ) - - PerformanceChart( - title: "Memory Usage", - data: monitor.memoryMetrics, - target: 100, - unit: "MB" - ) - - PerformanceChart( - title: "Battery Impact", - data: monitor.batteryMetrics, - target: 2.5, - unit: "%/hour" - ) - } - } -} -``` - ---- - -## 🧪 Benchmarking - -### Automated Performance Testing -```swift -class PerformanceBenchmark { - func benchmarkWidgetRendering() async { - let iterations = 1000 - var renderTimes: [TimeInterval] = [] - - for _ in 0..() - - init() { - cache.totalCostLimit = 100 * 1024 * 1024 // 100MB - cache.delegate = self - } -} - -extension ImageCache: NSCacheDelegate { - func cache(_ cache: NSCache, willEvictObject obj: Any) { - // Log eviction for monitoring - PerformanceMonitor.shared.recordCacheEviction() - } -} -``` - -#### 3. Automatic Memory Management -```swift -class AutoMemoryManager { - private var memoryPressureSource: DispatchSourceMemoryPressure? - - func startMonitoring() { - memoryPressureSource = DispatchSource.makeMemoryPressureSource( - eventMask: [.warning, .critical], - queue: .main - ) - - memoryPressureSource?.setEventHandler { [weak self] in - self?.handleMemoryPressure() - } - - memoryPressureSource?.resume() - } - - private func handleMemoryPressure() { - // Clear caches - ImageCache.shared.clear() - WidgetCache.shared.clear() - - // Reduce background operations - BackgroundTaskManager.shared.pauseNonCriticalTasks() - } -} -``` - ---- - -## 🔋 Battery Optimization - -### Power-Efficient Strategies - -#### 1. Adaptive Refresh Rates -```swift -class BatteryOptimizedRefreshManager { - private var refreshInterval: TimeInterval = 30.0 - - func updateRefreshInterval() { - let batteryLevel = UIDevice.current.batteryLevel - let batteryState = UIDevice.current.batteryState - - switch (batteryLevel, batteryState) { - case (0.0...0.2, .unplugged): - refreshInterval = 300.0 // 5 minutes - case (0.2...0.5, .unplugged): - refreshInterval = 120.0 // 2 minutes - case (0.5...1.0, .unplugged): - refreshInterval = 60.0 // 1 minute - default: - refreshInterval = 30.0 // 30 seconds - } - } -} -``` - -#### 2. Smart Background Processing -```swift -class PowerEfficientProcessor { - func processInBackground(_ operation: @escaping () async -> T) async -> T? { - // Check thermal state - guard ProcessInfo.processInfo.thermalState != .critical else { - return nil - } - - // Check battery level - guard UIDevice.current.batteryLevel > 0.15 else { - return nil - } - - return await operation() - } -} -``` - ---- - -## 🌐 Network Performance - -### Intelligent Network Management -```swift -class NetworkOptimizer { - private let reachability = NetworkReachability() - - func optimizeForNetwork() { - switch reachability.connectionType { - case .cellular: - enableDataCompression() - reduceBatchSize() - increaseTimeouts() - case .wifi: - disableDataCompression() - increaseBatchSize() - decreaseTimeouts() - case .none: - enableOfflineMode() - } - } -} -``` - ---- - -## 🎨 Rendering Optimization - -### View Hierarchy Optimization -```swift -struct OptimizedWidgetView: View { - var body: some View { - // Use GeometryReader sparingly - content - .drawingGroup() // Composite complex views - .clipped() // Optimize drawing bounds - } - - @ViewBuilder - private var content: some View { - // Minimize view nesting - LazyVStack { - header - body - footer - } - } -} -``` - ---- - -## 🔧 Troubleshooting - -### Common Performance Issues - -#### 1. Memory Leaks -```bash -# Use Instruments to detect memory leaks -instruments -t "Leaks" -D /tmp/MyApp.trace MyApp.app -``` - -#### 2. Retain Cycles -```swift -// ❌ Problematic -class WidgetManager { - var delegate: WidgetDelegate? - - func setup() { - delegate?.manager = self // Creates retain cycle - } -} - -// ✅ Fixed -class WidgetManager { - weak var delegate: WidgetDelegate? - - func setup() { - delegate?.manager = self // Uses weak reference - } -} -``` - -#### 3. Main Thread Blocking -```swift -// ❌ Blocks main thread -func loadData() { - let data = heavyComputation() // Blocking operation - updateUI(with: data) -} - -// ✅ Optimized -func loadData() async { - let data = await Task.detached { - heavyComputation() // Background thread - }.value - - await MainActor.run { - updateUI(with: data) // UI update on main thread - } -} -``` - -### Performance Debugging Tools -- **Instruments**: Profiling and performance analysis -- **Xcode Debugger**: Memory graph debugging -- **Console**: Performance logging and analysis -- **Network Link Conditioner**: Network performance testing - ---- - -This comprehensive performance guide ensures your iOS Widget Development Kit implementation achieves optimal performance while maintaining excellent user experience across all device conditions. diff --git a/Documentation/PerformanceGuide.md b/Documentation/PerformanceGuide.md deleted file mode 100644 index 52d3da5..0000000 --- a/Documentation/PerformanceGuide.md +++ /dev/null @@ -1,358 +0,0 @@ -# Performance Guide - - -## Table of Contents -- [Performance Guide](#performance-guide) -- [Overview](#overview) -- [Performance Metrics](#performance-metrics) - - [Target Performance Goals](#target-performance-goals) -- [Memory Management](#memory-management) - - [Widget Cache Optimization](#widget-cache-optimization) - - [Memory Monitoring](#memory-monitoring) - - [Memory Cleanup Strategies](#memory-cleanup-strategies) -- [Battery Optimization](#battery-optimization) - - [Refresh Rate Management](#refresh-rate-management) - - [Background Processing](#background-processing) -- [Network Optimization](#network-optimization) - - [Request Batching](#request-batching) - - [Caching Strategies](#caching-strategies) -- [Animation Performance](#animation-performance) - - [60fps Animations](#60fps-animations) - - [Animation Optimization](#animation-optimization) -- [Data Processing](#data-processing) - - [Efficient Data Handling](#efficient-data-handling) - - [Background Processing](#background-processing) -- [Error Handling and Recovery](#error-handling-and-recovery) - - [Graceful Degradation](#graceful-degradation) - - [Automatic Recovery](#automatic-recovery) -- [Monitoring and Analytics](#monitoring-and-analytics) - - [Performance Monitoring](#performance-monitoring) - - [Real-time Monitoring](#real-time-monitoring) -- [Best Practices](#best-practices) - - [Widget Design](#widget-design) - - [Code Optimization](#code-optimization) - - [Testing Performance](#testing-performance) -- [Troubleshooting](#troubleshooting) - - [Common Performance Issues](#common-performance-issues) - - [Performance Debugging](#performance-debugging) -- [Conclusion](#conclusion) - - - -## Overview - -This guide provides comprehensive information about optimizing widget performance in the iOS Widget Development Kit. - -## Performance Metrics - -### Target Performance Goals - -- **Widget Loading Time**: <500ms -- **Live Data Updates**: <200ms -- **Memory Usage**: <50MB per widget -- **Battery Consumption**: Minimal impact -- **Animation Frame Rate**: 60fps -- **Background Refresh**: Efficient and intelligent - -## Memory Management - -### Widget Cache Optimization - -```swift -// Configure cache settings -let cacheSettings = WidgetCacheSettings( - maxCacheSize: 50 * 1024 * 1024, // 50MB - cacheExpirationTime: 300, // 5 minutes - enableCompression: true -) - -WidgetEngine.shared.configureCache(cacheSettings) -``` - -### Memory Monitoring - -```swift -// Monitor memory usage -let metrics = WidgetEngine.shared.performanceMetrics -print("Current memory usage: \(metrics.memoryUsage / 1024 / 1024) MB") - -// Set up memory alerts -if metrics.memoryUsage > 100 * 1024 * 1024 { // 100MB - performMemoryCleanup() -} -``` - -### Memory Cleanup Strategies - -1. **Cache Eviction**: Remove old cache entries -2. **Image Optimization**: Compress and resize images -3. **Data Compression**: Compress large data sets -4. **Background Cleanup**: Regular cleanup in background - -## Battery Optimization - -### Refresh Rate Management - -```swift -// Adaptive refresh rates based on battery level -let batteryLevel = UIDevice.current.batteryLevel - -switch batteryLevel { -case 0.0..<0.2: // Low battery - refreshInterval = 300.0 // 5 minutes -case 0.2..<0.5: // Medium battery - refreshInterval = 120.0 // 2 minutes -default: // High battery - refreshInterval = 30.0 // 30 seconds -} -``` - -### Background Processing - -```swift -// Optimize background processing -let backgroundSettings = BackgroundProcessingSettings( - enableBackgroundRefresh: true, - backgroundRefreshInterval: 600.0, // 10 minutes - enableBatteryOptimization: true, - enableNetworkOptimization: true -) -``` - -## Network Optimization - -### Request Batching - -```swift -// Batch multiple requests -let batchRequest = WidgetBatchRequest() -batchRequest.addRequest(WeatherRequest()) -batchRequest.addRequest(CalendarRequest()) -batchRequest.addRequest(FitnessRequest()) - -batchRequest.execute { results in - // Process all results together -} -``` - -### Caching Strategies - -```swift -// Implement intelligent caching -let cacheStrategy = CacheStrategy( - enableMemoryCache: true, - enableDiskCache: true, - cacheExpiration: 300, // 5 minutes - enableCompression: true -) -``` - -## Animation Performance - -### 60fps Animations - -```swift -// Ensure smooth animations -let animationSettings = AnimationSettings( - targetFrameRate: 60, - enableHardwareAcceleration: true, - enableSmoothTransitions: true -) -``` - -### Animation Optimization - -1. **Use Core Animation**: Leverage hardware acceleration -2. **Minimize Layer Count**: Reduce view hierarchy complexity -3. **Optimize Drawing**: Use efficient drawing methods -4. **Avoid Off-screen Rendering**: Minimize expensive operations - -## Data Processing - -### Efficient Data Handling - -```swift -// Process data efficiently -class DataProcessor { - func processWidgetData(_ data: WidgetData) -> ProcessedData { - // Use background queues for processing - return DispatchQueue.global(qos: .userInitiated).sync { - // Process data - return processedData - } - } -} -``` - -### Background Processing - -```swift -// Handle data processing in background -DispatchQueue.global(qos: .background).async { - // Process large data sets - let processedData = processLargeDataSet() - - DispatchQueue.main.async { - // Update UI with processed data - updateWidget(with: processedData) - } -} -``` - -## Error Handling and Recovery - -### Graceful Degradation - -```swift -// Handle errors gracefully -func handleWidgetError(_ error: WidgetError) { - switch error { - case .networkUnavailable: - showCachedData() - case .dataSourceNotFound: - showDefaultWidget() - case .templateNotFound: - showFallbackTemplate() - default: - showErrorWidget() - } -} -``` - -### Automatic Recovery - -```swift -// Implement automatic recovery -class WidgetRecoveryManager { - func attemptRecovery(for error: WidgetError) { - switch error { - case .connectionFailed: - retryConnection() - case .dataDecodeFailed: - reloadData() - case .memoryCleanup: - performMemoryCleanup() - default: - logError(error) - } - } -} -``` - -## Monitoring and Analytics - -### Performance Monitoring - -```swift -// Monitor performance metrics -class PerformanceMonitor { - func trackMetric(_ metric: PerformanceMetric) { - analyticsService.logMetric(metric) - - if metric.value > metric.threshold { - triggerAlert(for: metric) - } - } -} -``` - -### Real-time Monitoring - -```swift -// Real-time performance monitoring -Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in - let metrics = WidgetEngine.shared.performanceMetrics - - // Monitor key metrics - monitorMemoryUsage(metrics.memoryUsage) - monitorBatteryLevel(metrics.batteryLevel) - monitorRefreshRate(metrics.refreshCount) -} -``` - -## Best Practices - -### Widget Design - -1. **Keep Widgets Lightweight**: Minimize complexity -2. **Use Efficient Data Structures**: Choose appropriate data types -3. **Implement Lazy Loading**: Load data on demand -4. **Optimize Images**: Use appropriate image formats and sizes - -### Code Optimization - -1. **Avoid Memory Leaks**: Use weak references appropriately -2. **Minimize Network Calls**: Batch requests when possible -3. **Use Efficient Algorithms**: Choose optimal algorithms -4. **Profile Regularly**: Monitor performance continuously - -### Testing Performance - -```swift -// Performance testing -class PerformanceTests: XCTestCase { - func testWidgetLoadingPerformance() { - measure { - let widget = createWidget() - XCTAssertNotNil(widget) - } - } - - func testMemoryUsage() { - let initialMemory = getCurrentMemoryUsage() - - for _ in 0..<100 { - createWidget() - } - - let finalMemory = getCurrentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - XCTAssertLessThan(memoryIncrease, 50 * 1024 * 1024) // 50MB - } -} -``` - -## Troubleshooting - -### Common Performance Issues - -1. **High Memory Usage** - - Check for memory leaks - - Implement proper cleanup - - Use memory-efficient data structures - -2. **Slow Widget Loading** - - Optimize data processing - - Implement caching - - Use background processing - -3. **Battery Drain** - - Reduce refresh frequency - - Optimize network requests - - Implement battery-aware scheduling - -4. **Poor Animation Performance** - - Use Core Animation - - Minimize view hierarchy - - Avoid expensive operations - -### Performance Debugging - -```swift -// Debug performance issues -class PerformanceDebugger { - func debugWidgetPerformance() { - let metrics = WidgetEngine.shared.performanceMetrics - - print("Memory Usage: \(metrics.memoryUsage / 1024 / 1024) MB") - print("Battery Level: \(metrics.batteryLevel * 100)%") - print("Refresh Count: \(metrics.refreshCount)") - print("Error Count: \(metrics.errorCount)") - } -} -``` - -## Conclusion - -Following these performance guidelines will ensure your widgets are fast, efficient, and provide an excellent user experience. Regular monitoring and optimization are key to maintaining high performance standards. \ No newline at end of file diff --git a/Documentation/README.md b/Documentation/README.md deleted file mode 100644 index 397f628..0000000 --- a/Documentation/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Documentation Index - -- [API-Reference.md -- [API.md -- [APIReference.md -- [ArchitectureGuide.md -- [Best-Practices.md -- [ConfigurationAPI.md -- [CustomizationAPI.md -- [CustomizationGuide.md -- [DataIntegrationAPI.md -- [DataIntegrationGuide.md -- [DynamicIslandAPI.md -- [DynamicIslandGuide.md -- [Getting-Started.md -- [GettingStarted.md -- [HomeScreenWidgetAPI.md -- [HomeScreenWidgetGuide.md -- [Installation.md -- [InstallationGuide.md -- [LiveActivityAPI.md -- [LiveActivityGuide.md -- [LockScreenWidgetAPI.md -- [LockScreenWidgetGuide.md -- [PerformanceGuide.md -- [Troubleshooting.md -- [UsageGuide.md -- [WidgetDevelopmentManagerAPI.md diff --git a/Documentation/Security.md b/Documentation/Security.md deleted file mode 100644 index e0ecc6c..0000000 --- a/Documentation/Security.md +++ /dev/null @@ -1,729 +0,0 @@ -# 🛡️ Security & Privacy Guide - iOS Widget Development Kit - -## 📋 Table of Contents - -- [Security Overview](#security-overview) -- [Security Architecture](#security-architecture) -- [Authentication & Authorization](#authentication--authorization) -- [Data Protection](#data-protection) -- [Network Security](#network-security) -- [Privacy Framework](#privacy-framework) -- [Compliance & Standards](#compliance--standards) -- [Security Monitoring](#security-monitoring) -- [Threat Mitigation](#threat-mitigation) -- [Security Best Practices](#security-best-practices) - ---- - -## 🌟 Security Overview - -The iOS Widget Development Kit implements **enterprise-grade security** with a **zero-trust architecture** and **defense-in-depth** strategy. Our security framework ensures comprehensive protection for widget data, user privacy, and system integrity while maintaining optimal performance. - -### Core Security Principles -- **🔒 Zero Trust Architecture**: Never trust, always verify -- **🛡️ Defense in Depth**: Multiple layers of security controls -- **🔐 Privacy by Design**: Built-in privacy protection from the ground up -- **📋 Compliance First**: GDPR, HIPAA, SOX, and CCPA compliant -- **🚨 Proactive Monitoring**: Real-time threat detection and response -- **🔑 Minimal Access**: Principle of least privilege throughout - ---- - -## 🏗️ Security Architecture - -### Multi-Layered Security Model -``` -┌─────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │Input Validation│ │Authorization│ │ Audit Logging│ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Security Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Encryption │ │Authentication│ │ Privacy │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Transport Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ TLS 1.3 │ │Cert Pinning │ │ HSTS │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Platform Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Keychain │ │ Secure │ │ App │ │ -│ │ Services │ │ Enclave │ │ Sandboxing │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - -### Security Components -- **EncryptionManager**: End-to-end encryption services -- **AuthenticationManager**: Multi-factor authentication -- **PrivacyManager**: Data privacy and consent management -- **SecurityAuditor**: Real-time security monitoring -- **ComplianceManager**: Regulatory compliance enforcement - ---- - -## 🔐 Authentication & Authorization - -### Multi-Factor Authentication -```swift -class AuthenticationManager { - private let biometricAuth = BiometricAuthenticationService() - private let tokenManager = SecureTokenManager() - private let keychain = KeychainManager() - - func authenticateUser() async throws -> AuthenticationResult { - // Primary authentication: Biometric - let biometricResult = try await biometricAuth.authenticate() - guard biometricResult.isSuccessful else { - throw AuthenticationError.biometricFailed - } - - // Secondary authentication: Device binding - let deviceBinding = try await validateDeviceBinding() - guard deviceBinding.isValid else { - throw AuthenticationError.deviceBindingFailed - } - - // Generate secure session token - let sessionToken = try generateSecureSessionToken() - - return AuthenticationResult( - token: sessionToken, - expiresAt: Date().addingTimeInterval(3600), - permissions: determineUserPermissions() - ) - } -} -``` - -### Biometric Integration -```swift -class BiometricAuthenticationService { - private let context = LAContext() - - func authenticate() async throws -> BiometricResult { - var error: NSError? - - guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { - throw BiometricError.notAvailable - } - - do { - let success = try await context.evaluatePolicy( - .deviceOwnerAuthenticationWithBiometrics, - localizedReason: "Authenticate to access secure widget data" - ) - - return BiometricResult( - success: success, - biometryType: context.biometryType, - timestamp: Date() - ) - } catch { - throw BiometricError.authenticationFailed(error) - } - } -} -``` - -### Token Management -```swift -class SecureTokenManager { - private let keychain = KeychainManager() - private let encryption = EncryptionManager() - - func generateSecureToken() throws -> SecureToken { - // Generate cryptographically secure random token - var bytes = Data(count: 32) - let result = bytes.withUnsafeMutableBytes { - SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) - } - - guard result == errSecSuccess else { - throw TokenError.generationFailed - } - - // Encrypt token before storage - let encryptedToken = try encryption.encrypt(bytes) - - // Store in Keychain with access control - let accessControl = SecAccessControlCreateWithFlags( - nil, - kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - .biometryCurrentSet, - nil - ) - - try keychain.store( - encryptedToken, - account: "widget.auth.token", - accessControl: accessControl - ) - - return SecureToken( - value: encryptedToken, - expiresAt: Date().addingTimeInterval(3600), - scopes: [.widgetAccess, .dataAccess] - ) - } -} -``` - ---- - -## 🔒 Data Protection - -### Encryption at Rest -```swift -class EncryptionManager { - private let keyManager = CryptographicKeyManager() - - func encryptData(_ data: Data) throws -> EncryptedData { - // Generate unique encryption key - let encryptionKey = try keyManager.generateDataEncryptionKey() - - // Use AES-256-GCM for authenticated encryption - let cipher = try AES.GCM.seal(data, using: encryptionKey) - - // Store key securely in Secure Enclave - try keyManager.storeKeyInSecureEnclave(encryptionKey) - - return EncryptedData( - ciphertext: cipher.ciphertext, - nonce: cipher.nonce, - tag: cipher.tag, - keyIdentifier: encryptionKey.identifier - ) - } - - func decryptData(_ encryptedData: EncryptedData) throws -> Data { - // Retrieve key from Secure Enclave - let encryptionKey = try keyManager.retrieveKeyFromSecureEnclave( - encryptedData.keyIdentifier - ) - - // Reconstruct sealed box - let sealedBox = try AES.GCM.SealedBox( - nonce: encryptedData.nonce, - ciphertext: encryptedData.ciphertext, - tag: encryptedData.tag - ) - - // Decrypt and verify - return try AES.GCM.open(sealedBox, using: encryptionKey) - } -} -``` - -### Secure Key Management -```swift -class CryptographicKeyManager { - func generateDataEncryptionKey() throws -> SymmetricKey { - return SymmetricKey(size: .bits256) - } - - func storeKeyInSecureEnclave(_ key: SymmetricKey) throws { - let keyData = key.withUnsafeBytes { Data($0) } - - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key.identifier, - kSecValueData as String: keyData, - kSecAttrAccessControl as String: SecAccessControlCreateWithFlags( - nil, - kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - .privateKeyUsage, - nil - )! - ] - - let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecSuccess else { - throw KeyStorageError.failed(status) - } - } -} -``` - -### Data Sanitization -```swift -class DataSanitizer { - static func sanitizeInput(_ input: String) -> String { - // Remove potentially harmful characters - let sanitized = input - .replacingOccurrences(of: "<", with: "<") - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "\"", with: """) - .replacingOccurrences(of: "'", with: "'") - .replacingOccurrences(of: "&", with: "&") - - // Validate length - let maxLength = 1000 - return String(sanitized.prefix(maxLength)) - } - - static func validateAndSanitizeJSON(_ json: [String: Any]) throws -> [String: Any] { - var sanitized: [String: Any] = [:] - - for (key, value) in json { - let sanitizedKey = sanitizeInput(key) - - switch value { - case let string as String: - sanitized[sanitizedKey] = sanitizeInput(string) - case let number as NSNumber: - sanitized[sanitizedKey] = number - case let array as [Any]: - sanitized[sanitizedKey] = try sanitizeArray(array) - case let dict as [String: Any]: - sanitized[sanitizedKey] = try validateAndSanitizeJSON(dict) - default: - throw ValidationError.unsupportedDataType - } - } - - return sanitized - } -} -``` - ---- - -## 🌐 Network Security - -### TLS Configuration -```swift -class NetworkSecurityManager { - private let session: URLSession - - init() { - let configuration = URLSessionConfiguration.default - configuration.tlsMinimumSupportedProtocolVersion = .TLSv13 - configuration.tlsMaximumSupportedProtocolVersion = .TLSv13 - - self.session = URLSession( - configuration: configuration, - delegate: self, - delegateQueue: nil - ) - } -} - -extension NetworkSecurityManager: URLSessionDelegate { - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - // Certificate pinning implementation - guard let serverTrust = challenge.protectionSpace.serverTrust else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } - - // Validate certificate chain - let policy = SecPolicyCreateSSL(true, "api.widgetkit.com" as CFString) - SecTrustSetPolicies(serverTrust, policy) - - var result: SecTrustResultType = .invalid - let status = SecTrustEvaluate(serverTrust, &result) - - guard status == errSecSuccess && (result == .unspecified || result == .proceed) else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } - - // Pin specific certificates - if validateCertificatePinning(serverTrust) { - completionHandler(.useCredential, URLCredential(trust: serverTrust)) - } else { - completionHandler(.cancelAuthenticationChallenge, nil) - } - } - - private func validateCertificatePinning(_ serverTrust: SecTrust) -> Bool { - // Implementation of certificate pinning validation - // Compare against known good certificates - return true // Simplified for example - } -} -``` - -### API Security -```swift -class SecureAPIClient { - private let baseURL = URL(string: "https://api.widgetkit.com")! - private let authManager = AuthenticationManager() - private let encryption = EncryptionManager() - - func secureRequest( - endpoint: String, - method: HTTPMethod, - body: T? = nil - ) async throws -> APIResponse { - - // Get authenticated session - let auth = try await authManager.getCurrentSession() - - // Prepare request - var request = URLRequest(url: baseURL.appendingPathComponent(endpoint)) - request.httpMethod = method.rawValue - request.setValue("Bearer \(auth.token)", forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - // Encrypt request body if present - if let body = body { - let bodyData = try JSONEncoder().encode(body) - let encryptedBody = try encryption.encryptData(bodyData) - request.httpBody = try JSONEncoder().encode(encryptedBody) - } - - // Add request signing - try signRequest(&request, with: auth.token) - - // Execute request - let (data, response) = try await URLSession.shared.data(for: request) - - // Validate response - guard let httpResponse = response as? HTTPURLResponse, - 200...299 ~= httpResponse.statusCode else { - throw APIError.invalidResponse - } - - // Decrypt and parse response - let encryptedResponse = try JSONDecoder().decode(EncryptedData.self, from: data) - let decryptedData = try encryption.decryptData(encryptedResponse) - let result = try JSONDecoder().decode(APIResponse.self, from: decryptedData) - - return result - } -} -``` - ---- - -## 🔒 Privacy Framework - -### Privacy by Design -```swift -class PrivacyManager { - private let consentManager = ConsentManager() - private let dataMinimizer = DataMinimizer() - private let anonymizer = DataAnonymizer() - - func collectData(_ data: T, for purpose: DataPurpose) async throws -> Bool { - // Check user consent - guard try await consentManager.hasConsent(for: purpose) else { - throw PrivacyError.noConsent - } - - // Minimize data collection - let minimizedData = try dataMinimizer.minimize(data, for: purpose) - - // Anonymize if required - let processedData = try anonymizer.anonymize(minimizedData, level: purpose.anonymizationLevel) - - // Store with privacy metadata - try await storeDataWithPrivacyMetadata(processedData, purpose: purpose) - - return true - } -} -``` - -### Consent Management -```swift -class ConsentManager { - private let storage = SecureStorage() - - func requestConsent(for purposes: [DataPurpose]) async throws -> ConsentResult { - // Present consent UI - let consentUI = ConsentViewController(purposes: purposes) - let result = try await consentUI.present() - - // Store consent with cryptographic proof - for purpose in purposes { - if result.approvedPurposes.contains(purpose) { - try await storeConsent(for: purpose, granted: true) - } - } - - return result - } - - private func storeConsent(for purpose: DataPurpose, granted: Bool) async throws { - let consent = ConsentRecord( - purpose: purpose, - granted: granted, - timestamp: Date(), - version: "1.0", - signature: try generateConsentSignature(purpose, granted) - ) - - try await storage.store(consent, key: "consent.\(purpose.id)") - } -} -``` - -### Data Anonymization -```swift -class DataAnonymizer { - func anonymize(_ data: T, level: AnonymizationLevel) throws -> AnonymizedData { - switch level { - case .none: - return AnonymizedData(data: data, level: .none) - - case .pseudonymization: - return try pseudonymize(data) - - case .kAnonymity(let k): - return try applyKAnonymity(data, k: k) - - case .differential: - return try applyDifferentialPrivacy(data) - } - } - - private func pseudonymize(_ data: T) throws -> AnonymizedData { - // Replace identifiers with pseudonyms - // Implementation depends on data type - return AnonymizedData(data: data, level: .pseudonymization) - } -} -``` - ---- - -## 📋 Compliance & Standards - -### GDPR Compliance -```swift -class GDPRComplianceManager { - func handleDataSubjectRequest(_ request: DataSubjectRequest) async throws -> ComplianceResponse { - switch request.type { - case .access: - return try await handleAccessRequest(request) - case .rectification: - return try await handleRectificationRequest(request) - case .erasure: - return try await handleErasureRequest(request) - case .portability: - return try await handlePortabilityRequest(request) - case .restriction: - return try await handleRestrictionRequest(request) - } - } - - private func handleErasureRequest(_ request: DataSubjectRequest) async throws -> ComplianceResponse { - // Validate request - guard try await validateDataSubject(request.subjectId) else { - throw ComplianceError.invalidSubject - } - - // Find all data for subject - let dataLocations = try await findAllDataForSubject(request.subjectId) - - // Secure deletion - for location in dataLocations { - try await securelyDeleteData(at: location) - } - - // Generate compliance proof - let proof = try generateDeletionProof(for: request.subjectId) - - return ComplianceResponse( - requestId: request.id, - status: .completed, - proof: proof, - completedAt: Date() - ) - } -} -``` - -### Security Audit Trail -```swift -class SecurityAuditLogger { - private let encryptedStorage = EncryptedAuditStorage() - - func logSecurityEvent(_ event: SecurityEvent) async { - let auditEntry = AuditEntry( - timestamp: Date(), - eventType: event.type, - severity: event.severity, - userId: event.userId, - details: event.details, - ipAddress: event.ipAddress, - userAgent: event.userAgent, - signature: try? generateAuditSignature(event) - ) - - await encryptedStorage.store(auditEntry) - - // Real-time monitoring - if event.severity >= .high { - await notifySecurityTeam(event) - } - } - - private func generateAuditSignature(_ event: SecurityEvent) throws -> String { - let data = "\(event.type)|\(event.timestamp)|\(event.userId ?? "")|\(event.details)" - return try HMAC.authenticationCode( - for: Data(data.utf8), - using: getAuditSigningKey() - ).description - } -} -``` - ---- - -## 🚨 Security Monitoring - -### Threat Detection -```swift -class ThreatDetectionEngine { - private let anomalyDetector = AnomalyDetector() - private let threatIntelligence = ThreatIntelligenceService() - - func analyzeSecurityEvent(_ event: SecurityEvent) async -> ThreatAssessment { - // Check against known threat patterns - let knownThreats = await threatIntelligence.checkEvent(event) - - // Analyze for anomalies - let anomalyScore = await anomalyDetector.calculateAnomalyScore(event) - - // Calculate overall threat level - let threatLevel = calculateThreatLevel( - knownThreats: knownThreats, - anomalyScore: anomalyScore - ) - - // Generate response recommendations - let recommendations = generateSecurityRecommendations( - threatLevel: threatLevel, - event: event - ) - - return ThreatAssessment( - event: event, - threatLevel: threatLevel, - confidence: calculateConfidence(knownThreats, anomalyScore), - recommendations: recommendations, - timestamp: Date() - ) - } -} -``` - -### Incident Response -```swift -class IncidentResponseManager { - func handleSecurityIncident(_ incident: SecurityIncident) async { - // Immediate containment - await containThreat(incident) - - // Forensic analysis - let analysis = await performForensicAnalysis(incident) - - // Notify stakeholders - await notifyStakeholders(incident, analysis: analysis) - - // Recovery actions - await initiateRecovery(incident) - - // Lessons learned - await updateSecurityControls(based: analysis) - } -} -``` - ---- - -## 🛡️ Security Best Practices - -### Secure Development Guidelines - -#### 1. Input Validation -```swift -// ✅ Always validate and sanitize input -func processUserInput(_ input: String) throws -> ProcessedInput { - // Validate length - guard input.count <= 1000 else { - throw ValidationError.inputTooLong - } - - // Sanitize content - let sanitized = DataSanitizer.sanitizeInput(input) - - // Validate format - guard isValidFormat(sanitized) else { - throw ValidationError.invalidFormat - } - - return ProcessedInput(value: sanitized) -} -``` - -#### 2. Secure Error Handling -```swift -// ✅ Don't expose sensitive information in errors -func authenticateUser(_ credentials: UserCredentials) throws -> AuthResult { - do { - return try performAuthentication(credentials) - } catch AuthenticationError.invalidCredentials { - // Log actual error securely - SecurityLogger.logAuthFailure(credentials.username) - - // Return generic error to user - throw AuthenticationError.authenticationFailed - } -} -``` - -#### 3. Secure Logging -```swift -// ✅ Never log sensitive data -func logUserAction(_ action: UserAction) { - let logEntry = LogEntry( - userId: action.userId.hashed(), // Hash instead of plain text - action: action.type.rawValue, - timestamp: Date(), - sessionId: action.sessionId.truncated() // Truncate session ID - ) - - logger.log(logEntry) -} -``` - -### Security Checklist - -#### Development Phase -- [ ] All inputs validated and sanitized -- [ ] Sensitive data encrypted at rest and in transit -- [ ] Authentication and authorization implemented -- [ ] Error handling doesn't expose sensitive information -- [ ] Logging follows security guidelines -- [ ] Dependencies regularly updated and scanned - -#### Testing Phase -- [ ] Security tests included in test suite -- [ ] Penetration testing performed -- [ ] Vulnerability scanning completed -- [ ] Code review with security focus -- [ ] Compliance requirements verified - -#### Deployment Phase -- [ ] Security configurations reviewed -- [ ] Monitoring and alerting configured -- [ ] Incident response plan in place -- [ ] Backup and recovery tested -- [ ] Documentation updated - ---- - -This comprehensive security guide ensures that your iOS Widget Development Kit implementation maintains the highest security standards while protecting user privacy and complying with relevant regulations. diff --git a/Documentation/Testing.md b/Documentation/Testing.md deleted file mode 100644 index e5ddc55..0000000 --- a/Documentation/Testing.md +++ /dev/null @@ -1,644 +0,0 @@ -# Comprehensive Testing Guide — iOS Widget Development Kit - -## Overview - -This document outlines the comprehensive testing strategy for the iOS Widget Development Kit, covering unit tests, integration tests, performance testing, security testing, and end-to-end validation across all enterprise components. - -## Testing Architecture - -### Test Pyramid Structure - -``` - ╭─────────────╮ - ╱ E2E Tests ╲ ← 10% (Critical user journeys) - ╱_______________╲ - ╱ ╲ -╱ Integration Tests ╲ ← 20% (Component interactions) -╲___________________╱ - ╲ ╱ - ╲ Unit Tests ╱ ← 70% (Individual components) - ╲_____________╱ -``` - -### Coverage Requirements - -- **Unit Tests**: 95%+ coverage for all business logic -- **Integration Tests**: 90%+ coverage for critical data paths -- **E2E Tests**: 100% coverage for core user workflows -- **Performance Tests**: All critical paths under 100ms -- **Security Tests**: All authentication and encryption flows - -## Testing Framework Setup - -### Dependencies - -```swift -// Package.swift -dependencies: [ - .package(url: "https://github.com/Quick/Quick.git", from: "6.0.0"), - .package(url: "https://github.com/Quick/Nimble.git", from: "11.0.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.12.0") -] -``` - -### Test Configuration - -```swift -// TestConfiguration.swift -import XCTest -import Quick -import Nimble -@testable import WidgetDevelopmentKit - -class TestConfiguration { - static let shared = TestConfiguration() - - var testEnvironment: TestEnvironment = .testing - var mockDataEnabled: Bool = true - var performanceTestingEnabled: Bool = true - var securityTestingEnabled: Bool = true -} -``` - -## Unit Testing - -### Core Components - -#### Widget Manager Tests - -```swift -class WidgetManagerTests: QuickSpec { - override func spec() { - describe("WidgetManager") { - var sut: WidgetManager! - var mockDataSource: MockDataSource! - - beforeEach { - mockDataSource = MockDataSource() - sut = WidgetManager(dataSource: mockDataSource) - } - - context("when creating widgets") { - it("should create widget with valid configuration") { - let config = WidgetConfiguration.mock() - let result = sut.createWidget(with: config) - expect(result).to(beAKindOf(Widget.self)) - } - - it("should throw error for invalid configuration") { - let invalidConfig = WidgetConfiguration.invalid() - expect { - try sut.createWidget(with: invalidConfig) - }.to(throwError(WidgetError.invalidConfiguration)) - } - } - } - } -} -``` - -#### Security Component Tests - -```swift -class EncryptionManagerTests: XCTestCase { - var sut: EncryptionManager! - - override func setUp() { - super.setUp() - sut = EncryptionManager() - } - - func testDataEncryption() async throws { - // Given - let testData = "Test encryption data".data(using: .utf8)! - - // When - let encryptedData = try await sut.encryptData(testData, keyIdentifier: "test-key") - let decryptedData = try await sut.decryptData(encryptedData) - - // Then - XCTAssertEqual(testData, decryptedData) - XCTAssertNotEqual(testData, encryptedData.data) - } - - func testSecureEnclaveIntegration() async throws { - // Performance test for Secure Enclave operations - let testData = Data(repeating: 0x42, count: 1024) - - let startTime = CFAbsoluteTimeGetCurrent() - _ = try await sut.encryptData(testData, requireBiometric: true) - let duration = CFAbsoluteTimeGetCurrent() - startTime - - XCTAssertLessThan(duration, 0.1, "Secure Enclave encryption should complete within 100ms") - } -} -``` - -#### Performance Component Tests - -```swift -class PerformanceMonitorTests: XCTestCase { - var sut: PerformanceMonitor! - - override func setUp() { - super.setUp() - sut = PerformanceMonitor.shared - } - - func testMemoryMonitoring() { - // Given - let initialMemory = sut.getCurrentMemoryUsage() - - // When - let largeArray = Array(0..<1_000_000) - let newMemory = sut.getCurrentMemoryUsage() - - // Then - XCTAssertGreaterThan(newMemory, initialMemory) - _ = largeArray // Keep reference to avoid optimization - } - - func testPerformanceThresholds() { - let metric = PerformanceMetric( - type: .renderTime, - value: 0.05, // 50ms - threshold: 0.1, // 100ms - timestamp: Date() - ) - - XCTAssertFalse(sut.isThresholdExceeded(metric)) - - let slowMetric = PerformanceMetric( - type: .renderTime, - value: 0.15, // 150ms - threshold: 0.1, // 100ms - timestamp: Date() - ) - - XCTAssertTrue(sut.isThresholdExceeded(slowMetric)) - } -} -``` - -## Integration Testing - -### Data Flow Tests - -```swift -class DataIntegrationTests: XCTestCase { - var dataSourceManager: DataSourceManager! - var apiManager: APIManager! - var cacheManager: CacheManager! - - override func setUp() { - super.setUp() - dataSourceManager = DataSourceManager.shared - apiManager = APIManager.shared - cacheManager = CacheManager.shared - } - - func testEndToEndDataFlow() async throws { - // Given - let dataSource = DataSourceConfiguration( - name: "test-api", - type: .rest, - endpoint: "https://api.test.com" - ) - - try await dataSourceManager.registerDataSource(dataSource) - - // When - let query = DataQuery( - dataSourceName: "test-api", - query: "users/1", - enableCache: true - ) - - let response: DataResponse = try await dataSourceManager.executeQuery( - query, - responseType: UserModel.self - ) - - // Then - XCTAssertTrue(response.isSuccess) - XCTAssertNotNil(response.data) - - // Verify caching - let cachedResponse: DataResponse = try await dataSourceManager.executeQuery( - query, - responseType: UserModel.self - ) - - XCTAssertTrue(cachedResponse.cached) - } -} -``` - -### Cloud Integration Tests - -```swift -class CloudIntegrationTests: XCTestCase { - var cloudIntegration: CloudIntegration! - - override func setUp() { - super.setUp() - cloudIntegration = CloudIntegration.shared - } - - func testMultiCloudSync() async throws { - // Given - let configuration = CloudIntegration.CloudConfiguration() - configuration.primaryProvider = .icloud - configuration.secondaryProviders = [.firebase] - configuration.syncStrategy = .multiCloud - - try await cloudIntegration.initialize(configuration: configuration) - - // When - let testData = "Test sync data".data(using: .utf8)! - let cloudFile = try await cloudIntegration.upload( - data: testData, - to: "test/sync-file.json" - ) - - // Wait for sync - try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second - - // Then - let downloadedData = try await cloudIntegration.download(path: cloudFile.path) - XCTAssertEqual(testData, downloadedData) - - // Verify sync status - let syncStatus = await cloudIntegration.getSyncStatus() - XCTAssertEqual(syncStatus.count, 2) // Primary + Secondary - } -} -``` - -## Performance Testing - -### Load Testing - -```swift -class PerformanceTests: XCTestCase { - func testWidgetRenderingPerformance() { - measure { - let widget = ComplexWidget() - widget.render() - } - } - - func testConcurrentDataLoading() async throws { - let expectation = expectation(description: "Concurrent loading") - expectation.expectedFulfillmentCount = 100 - - await withTaskGroup(of: Void.self) { group in - for i in 0..<100 { - group.addTask { - do { - let query = DataQuery( - dataSourceName: "test-api", - query: "data/\(i)" - ) - _ = try await DataSourceManager.shared.executeQuery( - query, - responseType: GenericModel.self - ) - expectation.fulfill() - } catch { - XCTFail("Concurrent request failed: \(error)") - } - } - } - } - - await fulfillment(of: [expectation], timeout: 5.0) - } - - func testMemoryLeaks() { - weak var weakWidget: Widget? - - autoreleasepool { - let widget = Widget(configuration: .default) - weakWidget = widget - widget.performHeavyOperation() - } - - XCTAssertNil(weakWidget, "Widget should be deallocated") - } -} -``` - -### Battery Performance Tests - -```swift -class BatteryPerformanceTests: XCTestCase { - func testBatteryOptimization() { - let optimizationEngine = OptimizationEngine.shared - - measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) { - Task { - try? await optimizationEngine.optimizeBattery() - } - } - } -} -``` - -## Security Testing - -### Authentication Tests - -```swift -class SecurityTests: XCTestCase { - var authManager: AuthenticationManager! - - override func setUp() { - super.setUp() - authManager = AuthenticationManager.shared - } - - func testBiometricAuthentication() async throws { - // Given - let mockBiometric = MockBiometricService() - authManager.biometricService = mockBiometric - - // When - mockBiometric.isAvailable = true - mockBiometric.shouldSucceed = true - - let result = try await authManager.authenticate(reason: "Test authentication") - - // Then - XCTAssertTrue(result.success) - XCTAssertEqual(result.method, .biometric) - } - - func testEncryptionCompliance() async throws { - let privacyManager = PrivacyManager.shared - - // Test GDPR compliance - let gdprResult = try await privacyManager.validateGDPRCompliance() - XCTAssertTrue(gdprResult.isCompliant) - - // Test HIPAA compliance - let hipaaResult = try await privacyManager.validateHIPAACompliance() - XCTAssertTrue(hipaaResult.isCompliant) - } -} -``` - -### Penetration Testing - -```swift -class PenetrationTests: XCTestCase { - func testSQLInjectionPrevention() async throws { - let maliciousQuery = "'; DROP TABLE users; --" - - do { - let query = DataQuery( - dataSourceName: "database", - query: maliciousQuery - ) - _ = try await DataSourceManager.shared.executeQuery( - query, - responseType: GenericModel.self - ) - XCTFail("Should have prevented SQL injection") - } catch DataSourceError.validationFailed { - // Expected behavior - } - } - - func testDataEncryptionAtRest() async throws { - let sensitiveData = "SSN: 123-45-6789" - - let encryptedData = try await EncryptionManager.shared.encryptData( - sensitiveData.data(using: .utf8)!, - keyIdentifier: "sensitive-data" - ) - - // Verify data is actually encrypted - let encryptedString = String(data: encryptedData.data, encoding: .utf8) ?? "" - XCTAssertFalse(encryptedString.contains("123-45-6789")) - } -} -``` - -## End-to-End Testing - -### User Journey Tests - -```swift -class E2ETests: XCTestCase { - func testCompleteWidgetLifecycle() async throws { - // 1. Initialize system - let widgetManager = AdvancedWidgetManager.shared - try await widgetManager.initialize() - - // 2. Create widget - let configuration = WidgetConfiguration.weather() - let widget = try await widgetManager.createWidget(with: configuration) - - // 3. Load data - let weatherData = try await widget.loadData() - XCTAssertNotNil(weatherData) - - // 4. Render widget - let renderStart = CFAbsoluteTimeGetCurrent() - try await widget.render() - let renderTime = CFAbsoluteTimeGetCurrent() - renderStart - - XCTAssertLessThan(renderTime, 0.1, "Widget rendering should be under 100ms") - - // 5. Update data - try await widget.refreshData() - - // 6. Verify performance metrics - let metrics = await PerformanceMonitor.shared.getMetrics(for: widget.id) - XCTAssertLessThan(metrics.averageRenderTime, 0.1) - XCTAssertLessThan(metrics.memoryUsage, 50_000_000) // 50MB limit - } -} -``` - -## Test Utilities and Mocks - -### Mock Data Sources - -```swift -class MockDataSource: DataSourceProtocol { - var shouldSucceed: Bool = true - var responseDelay: TimeInterval = 0.01 - var mockData: [String: Any] = [:] - - func fetch(_ type: T.Type, from endpoint: String) async throws -> T { - if !shouldSucceed { - throw DataSourceError.connectionFailed("Mock failure") - } - - try await Task.sleep(nanoseconds: UInt64(responseDelay * 1_000_000_000)) - - guard let data = mockData[endpoint] else { - throw DataSourceError.dataNotFound - } - - let jsonData = try JSONSerialization.data(withJSONObject: data) - return try JSONDecoder().decode(type, from: jsonData) - } -} -``` - -### Test Data Builders - -```swift -struct TestDataBuilder { - static func buildWidget( - type: WidgetType = .weather, - size: WidgetSize = .medium, - configuration: WidgetConfiguration? = nil - ) -> Widget { - let config = configuration ?? WidgetConfiguration.default(for: type) - return Widget(type: type, size: size, configuration: config) - } - - static func buildUserData( - name: String = "Test User", - email: String = "test@example.com", - permissions: [Permission] = [.read, .write] - ) -> UserData { - return UserData(name: name, email: email, permissions: permissions) - } -} -``` - -## Continuous Integration - -### GitHub Actions Configuration - -```yaml -name: iOS Widget Development Kit Tests - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Run Unit Tests - run: | - xcodebuild test \ - -scheme WidgetDevelopmentKit \ - -destination 'platform=iOS Simulator,name=iPhone 14' \ - -resultBundlePath TestResults \ - -enableCodeCoverage YES - - - name: Generate Code Coverage - run: | - xcrun xccov view --report --json TestResults.xcresult > coverage.json - - - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: coverage.json -``` - -## Test Commands - -### Xcode Testing -```bash -# Run all tests -⌘U (Product > Test) - -# Run specific test class -⌘⇧U (Product > Test > Test Class) - -# Run with coverage -Product > Test > Test with Code Coverage -``` - -### Swift Package Manager Testing -```bash -# Run all tests -swift test - -# Run specific test -swift test --filter WidgetManagerTests - -# Run with coverage -swift test --enable-code-coverage - -# Generate coverage report -swift test --enable-code-coverage --build-path .build -``` - -### Performance Testing -```bash -# Run performance tests only -swift test --filter PerformanceTests - -# Run with instruments -xcodebuild test -scheme WidgetDevelopmentKit \ - -destination 'platform=iOS Simulator,name=iPhone 14' \ - -enableAddressSanitizer YES \ - -enableThreadSanitizer YES -``` - -## Best Practices - -### Test Organization -- Group tests by feature/component -- Use descriptive test names -- Follow AAA pattern (Arrange, Act, Assert) -- Keep tests independent and isolated -- Use meaningful assertions with custom messages - -### Performance Testing Guidelines -- Set realistic performance thresholds -- Test on various device configurations -- Monitor memory usage and leaks -- Test with realistic data volumes -- Validate battery impact - -### Security Testing Standards -- Test all authentication flows -- Validate encryption implementations -- Test access control mechanisms -- Verify data sanitization -- Test for common vulnerabilities (OWASP Top 10) - -### Continuous Testing -- Run tests on every commit -- Maintain high code coverage (95%+) -- Automate performance regression detection -- Include security scanning in CI/CD -- Generate comprehensive test reports - -## Troubleshooting - -### Common Issues -1. **Flaky Tests**: Use proper async/await patterns, avoid hardcoded delays -2. **Slow Tests**: Optimize setup/teardown, use mocks effectively -3. **Memory Leaks**: Use weak references, proper autoreleasepool usage -4. **Device Differences**: Test on multiple simulators and devices - -### Debug Tools -- Xcode Test Navigator -- Instruments for performance analysis -- Console for detailed logging -- Accessibility Inspector for UI tests -- Network Link Conditioner for network testing - -This comprehensive testing strategy ensures the highest quality and reliability for the iOS Widget Development Kit across all enterprise scenarios. diff --git a/Documentation/Troubleshooting.md b/Documentation/Troubleshooting.md deleted file mode 100644 index 3a45155..0000000 --- a/Documentation/Troubleshooting.md +++ /dev/null @@ -1,955 +0,0 @@ -# Comprehensive Troubleshooting Guide — iOS Widget Development Kit - -## Overview - -This comprehensive troubleshooting guide provides solutions for common issues, debugging strategies, and performance optimization techniques for the iOS Widget Development Kit. Whether you're facing build errors, runtime issues, or performance problems, this guide offers step-by-step solutions. - -## Table of Contents - -1. [Build & Compilation Issues](#build--compilation-issues) -2. [Runtime Errors](#runtime-errors) -3. [Performance Issues](#performance-issues) -4. [Widget Display Problems](#widget-display-problems) -5. [Data & Network Issues](#data--network-issues) -6. [Security & Privacy Issues](#security--privacy-issues) -7. [Memory & Resource Issues](#memory--resource-issues) -8. [Debugging Techniques](#debugging-techniques) -9. [Common Error Codes](#common-error-codes) -10. [Platform-Specific Issues](#platform-specific-issues) - -## Build & Compilation Issues - -### Issue: Module 'WidgetDevelopmentKit' Not Found - -**Symptoms:** -``` -error: no such module 'WidgetDevelopmentKit' -import WidgetDevelopmentKit - ^ -``` - -**Solutions:** - -1. **Verify SPM Installation:** -```bash -# Clean and reset SPM -rm -rf .build -rm Package.resolved -swift package reset -swift package resolve -``` - -2. **Check Package.swift:** -```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "3.2.0") -] -``` - -3. **Xcode Integration:** -- File → Add Package Dependencies -- Enter repository URL -- Select version rule: "Up to Next Major Version" -- Add to target - -### Issue: Swift Version Incompatibility - -**Symptoms:** -``` -error: compiling for iOS 15.0, but module was compiled for iOS 16.0 -``` - -**Solutions:** - -1. **Update Deployment Target:** -```swift -// In your target settings -platforms: [ - .iOS(.v16) -] -``` - -2. **Check Swift Version:** -```bash -swift --version -# Should be Swift 5.7 or later -``` - -3. **Update Build Settings:** -- Project → Build Settings → Swift Language Version → Swift 5 -- Deployment Target → iOS 16.0 - -### Issue: Ambiguous Type References - -**Symptoms:** -``` -error: 'Widget' is ambiguous for type lookup in this context -``` - -**Solution:** -```swift -// Use fully qualified names -typealias AppWidget = WidgetDevelopmentKit.Widget -typealias SystemWidget = WidgetKit.Widget - -// Or use explicit module prefix -let widget = WidgetDevelopmentKit.Widget(configuration: config) -``` - -## Runtime Errors - -### Issue: Widget Not Updating - -**Symptoms:** -- Widget shows stale data -- Update timeline not triggering -- Background refresh not working - -**Debugging Steps:** - -1. **Check Update Configuration:** -```swift -// Verify update interval -let configuration = WidgetConfiguration( - type: .weather, - size: .medium, - updateInterval: 300 // 5 minutes minimum -) - -// Enable debug logging -configuration.behavior.intelligentScheduling = false -``` - -2. **Verify Timeline Provider:** -```swift -func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - let currentDate = Date() - let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)! - - // Create timeline with proper refresh policy - let timeline = Timeline(entries: entries, policy: .after(refreshDate)) - - // Log for debugging - logger.debug("Timeline refresh scheduled for: \(refreshDate)") - - completion(timeline) -} -``` - -3. **Check Background App Refresh:** -```swift -// In Info.plist -UIBackgroundModes - - fetch - processing - - -// In AppDelegate -func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - Task { - do { - try await WidgetCenter.shared.reloadAllTimelines() - completionHandler(.newData) - } catch { - completionHandler(.failed) - } - } -} -``` - -### Issue: Widget Crashes on Launch - -**Symptoms:** -``` -Thread 1: EXC_BAD_ACCESS (code=1, address=0x0) -``` - -**Solutions:** - -1. **Enable Zombie Objects:** -- Edit Scheme → Run → Diagnostics → Enable Zombie Objects - -2. **Add Exception Breakpoint:** -```swift -// Identify crash location -class WidgetDebugger { - static func captureException() { - NSSetUncaughtExceptionHandler { exception in - print("Exception: \(exception)") - print("Stack trace: \(exception.callStackSymbols)") - } - } -} -``` - -3. **Check Memory Management:** -```swift -// Avoid retain cycles -class WidgetViewModel { - weak var delegate: WidgetDelegate? - - func loadData() { - dataLoader.fetch { [weak self] data in - self?.processData(data) - } - } -} -``` - -### Issue: Authentication Failures - -**Symptoms:** -``` -AuthenticationError: Biometric authentication failed -``` - -**Solutions:** - -1. **Check Biometric Availability:** -```swift -func authenticateUser() async throws { - let authManager = AuthenticationManager.shared - - // Check availability first - guard await authManager.isBiometricAvailable() else { - // Fallback to passcode - return try await authManager.authenticateWithPasscode() - } - - do { - try await authManager.authenticate( - reason: "Authenticate to access widget data", - allowFallback: true - ) - } catch AuthenticationError.biometricLockout { - // Handle lockout - try await authManager.resetBiometric() - } -} -``` - -2. **Add Info.plist Keys:** -```xml -NSFaceIDUsageDescription -Authenticate to access secure widget data -``` - -## Performance Issues - -### Issue: Slow Widget Rendering - -**Symptoms:** -- Widget takes >100ms to render -- UI freezes during updates -- High CPU usage - -**Performance Analysis:** - -1. **Profile with Instruments:** -```swift -// Add signposts for profiling -import os.signpost - -let log = OSLog(subsystem: "com.app.widget", category: .pointsOfInterest) - -func renderWidget() { - let signpostID = OSSignpostID(log: log) - - os_signpost(.begin, log: log, name: "Widget Render", signpostID: signpostID) - defer { - os_signpost(.end, log: log, name: "Widget Render", signpostID: signpostID) - } - - // Rendering code -} -``` - -2. **Optimize View Hierarchy:** -```swift -// ❌ Bad: Complex nested views -struct ComplexWidget: View { - var body: some View { - VStack { - ForEach(0..<100) { _ in - HStack { - ForEach(0..<10) { _ in - Text("Item") - } - } - } - } - } -} - -// ✅ Good: Optimized flat structure -struct OptimizedWidget: View { - var body: some View { - LazyVGrid(columns: columns) { - ForEach(items) { item in - ItemView(item: item) - } - } - } -} -``` - -3. **Implement Caching:** -```swift -class WidgetDataCache { - private let cache = NSCache() - - func getCachedData(for key: String) -> Data? { - // Set memory limits - cache.totalCostLimit = 10 * 1024 * 1024 // 10MB - cache.countLimit = 50 - - return cache.object(forKey: NSString(string: key))?.data - } -} -``` - -### Issue: High Memory Usage - -**Symptoms:** -``` -Received memory pressure notification -Widget terminated due to memory pressure -``` - -**Solutions:** - -1. **Monitor Memory Usage:** -```swift -class MemoryMonitor { - func getCurrentMemoryUsage() -> Int64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size) / 4 - - let result = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, - task_flavor_t(MACH_TASK_BASIC_INFO), - $0, - &count) - } - } - - return result == KERN_SUCCESS ? Int64(info.resident_size) : 0 - } - - func handleMemoryWarning() { - // Clear caches - URLCache.shared.removeAllCachedResponses() - - // Release non-essential resources - ImageCache.shared.clearMemoryCache() - - // Force garbage collection - autoreleasepool { } - } -} -``` - -2. **Optimize Image Loading:** -```swift -// ❌ Bad: Loading full resolution -let image = UIImage(named: "large_image") - -// ✅ Good: Downsampling for widgets -func downsampledImage(at url: URL, maxSize: CGSize) -> UIImage? { - let options: [CFString: Any] = [ - kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height), - kCGImageSourceCreateThumbnailFromImageAlways: true, - kCGImageSourceCreateThumbnailWithTransform: true - ] - - guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil), - let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { - return nil - } - - return UIImage(cgImage: cgImage) -} -``` - -## Widget Display Problems - -### Issue: Widget Not Appearing - -**Symptoms:** -- Widget not in widget gallery -- "Unable to Load" message - -**Solutions:** - -1. **Verify Widget Extension Configuration:** -```swift -@main -struct WidgetBundle: WidgetBundle { - var body: some Widget { - WeatherWidget() - CalendarWidget() - } -} - -// Ensure proper configuration -struct WeatherWidget: Widget { - let kind: String = "WeatherWidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider()) { entry in - WeatherWidgetView(entry: entry) - } - .configurationDisplayName("Weather") - .description("Shows current weather") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} -``` - -2. **Check Entitlements:** -```xml - -com.apple.security.application-groups - - group.com.yourapp.widgets - -``` - -### Issue: Widget Layout Issues - -**Symptoms:** -- Content clipped or overflowing -- Incorrect sizing -- Layout breaking on different devices - -**Solutions:** - -```swift -struct AdaptiveWidgetView: View { - @Environment(\.widgetFamily) var family - - var body: some View { - GeometryReader { geometry in - content(for: geometry.size) - } - } - - @ViewBuilder - func content(for size: CGSize) -> some View { - switch family { - case .systemSmall: - SmallWidgetLayout() - .frame(width: size.width, height: size.height) - case .systemMedium: - MediumWidgetLayout() - .frame(width: size.width, height: size.height) - case .systemLarge: - LargeWidgetLayout() - .frame(width: size.width, height: size.height) - default: - EmptyView() - } - } -} -``` - -## Data & Network Issues - -### Issue: API Rate Limiting - -**Symptoms:** -``` -Error: Rate limit exceeded (429) -``` - -**Solutions:** - -1. **Implement Exponential Backoff:** -```swift -class RetryManager { - func performRequestWithRetry( - operation: () async throws -> T, - maxAttempts: Int = 3 - ) async throws -> T { - var lastError: Error? - - for attempt in 0.. Bool { - let now = Date() - let oneMinuteAgo = now.addingTimeInterval(-60) - - // Remove old timestamps - requestTimestamps.removeAll { $0 < oneMinuteAgo } - - return requestTimestamps.count < maxRequestsPerMinute - } - - func recordRequest() { - requestTimestamps.append(Date()) - } -} -``` - -### Issue: Data Synchronization Conflicts - -**Symptoms:** -- Conflicting data between devices -- Lost updates -- Sync failures - -**Solution:** -```swift -class ConflictResolver { - func resolveConflict(local: WidgetData, remote: WidgetData) -> WidgetData { - // Last-write-wins strategy - if local.lastModified > remote.lastModified { - return local - } else if remote.lastModified > local.lastModified { - return remote - } else { - // Same timestamp - merge changes - return mergeData(local: local, remote: remote) - } - } - - private func mergeData(local: WidgetData, remote: WidgetData) -> WidgetData { - // Custom merge logic based on your data structure - var merged = local - - // Example: Combine arrays, prefer remote for conflicts - merged.items = Array(Set(local.items + remote.items)) - merged.settings = remote.settings // Prefer remote settings - - return merged - } -} -``` - -## Security & Privacy Issues - -### Issue: Keychain Access Errors - -**Symptoms:** -``` -Error: errSecItemNotFound (-25300) -``` - -**Solutions:** - -1. **Proper Keychain Configuration:** -```swift -class KeychainManager { - func saveToKeychain(data: Data, for key: String) throws { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecAttrAccessGroup as String: "group.com.yourapp.widgets", - kSecValueData as String: data, - kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock - ] - - // Delete existing item first - SecItemDelete(query as CFDictionary) - - // Add new item - let status = SecItemAdd(query as CFDictionary, nil) - - guard status == errSecSuccess else { - throw KeychainError.saveFailed(status) - } - } -} -``` - -2. **Handle Keychain Sharing:** -```xml - -keychain-access-groups - - $(AppIdentifierPrefix)com.yourapp.sharedkeychain - -``` - -### Issue: Privacy Compliance Violations - -**Symptoms:** -- App Store rejection for privacy issues -- GDPR compliance warnings - -**Solutions:** - -```swift -class PrivacyComplianceChecker { - func validateDataCollection() throws { - // Check for PII in logs - Logger.shared.setPIIFilter { message in - // Remove email addresses - let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - var filtered = message.replacingOccurrences( - of: emailRegex, - with: "[EMAIL]", - options: .regularExpression - ) - - // Remove phone numbers - let phoneRegex = "\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b" - filtered = filtered.replacingOccurrences( - of: phoneRegex, - with: "[PHONE]", - options: .regularExpression - ) - - return filtered - } - - // Ensure consent before tracking - guard PrivacyManager.shared.hasUserConsent() else { - throw PrivacyError.noConsent - } - } -} -``` - -## Memory & Resource Issues - -### Issue: Widget Extension Terminated - -**Symptoms:** -``` -Message from debugger: Terminated due to memory issue -``` - -**Solutions:** - -1. **Implement Memory Budget:** -```swift -class MemoryBudgetManager { - private let maxMemoryMB: Int = 30 // Widget extension limit - - func canAllocateMemory(bytes: Int) -> Bool { - let currentUsage = getCurrentMemoryUsageMB() - let requestedMB = bytes / (1024 * 1024) - - return (currentUsage + requestedMB) < maxMemoryMB - } - - func optimizeMemoryUsage() { - // Clear image cache - SDImageCache.shared.clearMemory() - - // Reduce data cache size - URLCache.shared.memoryCapacity = 5 * 1024 * 1024 // 5MB - - // Force cleanup - autoreleasepool { - // Temporary objects will be released - } - } -} -``` - -2. **Use Lazy Loading:** -```swift -struct LazyWidgetView: View { - @StateObject private var viewModel = WidgetViewModel() - - var body: some View { - Group { - if viewModel.isLoaded { - ContentView(data: viewModel.data) - } else { - PlaceholderView() - .onAppear { - Task { - await viewModel.loadDataIfNeeded() - } - } - } - } - } -} -``` - -## Debugging Techniques - -### Advanced Debugging Setup - -1. **Enable Verbose Logging:** -```swift -class DebugLogger { - static let shared = DebugLogger() - - private let subsystem = "com.app.widget" - - func configureLogging() { - #if DEBUG - // Enable all log levels in debug - Logger.logLevel = .debug - - // Log to file for analysis - let documentsPath = FileManager.default.urls( - for: .documentDirectory, - in: .userDomainMask - ).first! - - let logFile = documentsPath.appendingPathComponent("widget_debug.log") - freopen(logFile.path, "a+", stderr) - #endif - } - - func log(_ message: String, type: OSLogType = .debug) { - let logger = Logger(subsystem: subsystem, category: "Debug") - logger.log(level: type, "\(message)") - - #if DEBUG - print("🔍 [\(Date())] \(message)") - #endif - } -} -``` - -2. **Network Debugging:** -```swift -class NetworkDebugger: URLProtocol { - override class func canInit(with request: URLRequest) -> Bool { - #if DEBUG - print("📡 Request: \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") - if let headers = request.allHTTPHeaderFields { - print(" Headers: \(headers)") - } - if let body = request.httpBody { - print(" Body: \(String(data: body, encoding: .utf8) ?? "Binary data")") - } - #endif - return false - } -} - -// Register in app -URLProtocol.registerClass(NetworkDebugger.self) -``` - -3. **Widget Preview Debugging:** -```swift -struct WidgetPreviews: PreviewProvider { - static var previews: some View { - Group { - WeatherWidgetView(entry: .mock()) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .previewDisplayName("Small") - - WeatherWidgetView(entry: .mockError()) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .previewDisplayName("Error State") - - WeatherWidgetView(entry: .mockLoading()) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - .previewDisplayName("Loading State") - } - .environment(\.colorScheme, .dark) - } -} -``` - -## Common Error Codes - -### Framework-Specific Error Codes - -| Error Code | Description | Solution | -|------------|-------------|----------| -| WDK-001 | Invalid configuration | Verify WidgetConfiguration parameters | -| WDK-002 | Template not found | Check template ID and registration | -| WDK-003 | Quota exceeded | Reduce number of active widgets | -| WDK-004 | Orchestration failed | Check widget dependencies | -| WDK-005 | Lifecycle error | Review widget state transitions | -| WDK-100 | Encryption failed | Verify encryption keys and Secure Enclave | -| WDK-101 | Authentication failed | Check biometric availability and permissions | -| WDK-102 | Privacy violation | Ensure GDPR/CCPA compliance | -| WDK-200 | Cache overflow | Implement cache eviction policy | -| WDK-201 | Performance threshold exceeded | Optimize rendering and data loading | -| WDK-300 | API timeout | Increase timeout or optimize request | -| WDK-301 | Cloud sync failed | Check network and cloud credentials | -| WDK-302 | Data source unavailable | Verify data source configuration | - -### System Error Codes - -| Error Code | Description | Solution | -|------------|-------------|----------| -| -25300 | Keychain item not found | Create keychain item before accessing | -| -25299 | Keychain duplicate item | Delete existing item before adding | -| -9806 | SSL connection failed | Check certificate and TLS configuration | -| -1009 | No internet connection | Implement offline mode | -| -1001 | Request timeout | Increase timeout or retry | - -## Platform-Specific Issues - -### iOS 16 Specific - -**Lock Screen Widgets:** -```swift -// Ensure minimal UI for lock screen -struct LockScreenWidget: View { - @Environment(\.isLuminanceReduced) var isLuminanceReduced - - var body: some View { - if isLuminanceReduced { - // Simplified UI for always-on display - MinimalWidgetView() - } else { - // Regular lock screen widget - StandardWidgetView() - } - } -} -``` - -### iOS 17 Specific - -**Interactive Widgets:** -```swift -struct InteractiveWidget: View { - var body: some View { - Button(intent: ToggleIntent()) { - Text("Toggle") - } - .buttonStyle(.plain) - } -} - -// App Intent -struct ToggleIntent: AppIntent { - static var title: LocalizedStringResource = "Toggle Widget" - - func perform() async throws -> some IntentResult { - // Handle interaction - WidgetCenter.shared.reloadAllTimelines() - return .result() - } -} -``` - -### iPad Specific - -**Size Class Handling:** -```swift -struct iPadWidget: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass - - var body: some View { - if horizontalSizeClass == .regular && verticalSizeClass == .regular { - // iPad layout - iPadOptimizedLayout() - } else { - // Compact layout - CompactLayout() - } - } -} -``` - -## Getting Help - -### Debug Information Collection - -When reporting issues, include: - -1. **System Information:** -```swift -func collectDebugInfo() -> String { - return """ - Device: \(UIDevice.current.model) - OS Version: \(UIDevice.current.systemVersion) - App Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown") - Framework Version: 3.2.0 - Memory Usage: \(getCurrentMemoryUsageMB())MB - Active Widgets: \(AdvancedWidgetManager.shared.widgets.count) - """ -} -``` - -2. **Crash Logs:** -- Xcode → Window → Devices and Simulators → View Device Logs - -3. **Sample Code:** -- Minimal reproducible example -- Configuration used -- Expected vs actual behavior - -### Support Resources - -- **GitHub Issues**: [Report bugs](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/issues) -- **Discussions**: [Ask questions](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/discussions) -- **Stack Overflow**: Tag with `ios-widget-development-kit` -- **Documentation**: [Full documentation](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/tree/main/Documentation) - -### Filing Bug Reports - -Use this template: - -```markdown -## Environment -- Framework Version: 3.2.0 -- iOS Version: 17.0 -- Xcode Version: 15.0 -- Device: iPhone 15 Pro - -## Description -Brief description of the issue - -## Steps to Reproduce -1. Step one -2. Step two -3. Step three - -## Expected Behavior -What should happen - -## Actual Behavior -What actually happens - -## Code Sample -```swift -// Minimal code to reproduce -``` - -## Logs -``` -// Relevant error messages or logs -``` -``` - -## Conclusion - -This troubleshooting guide covers the most common issues and their solutions. For issues not covered here, please: - -1. Check the latest documentation -2. Search existing GitHub issues -3. Post in discussions for community help -4. File a detailed bug report if needed - -Remember to always test on real devices, as simulator behavior may differ, especially for features like widgets, background refresh, and biometric authentication. \ No newline at end of file diff --git a/Documentation/UsageGuide.md b/Documentation/UsageGuide.md deleted file mode 100644 index 6f3b799..0000000 --- a/Documentation/UsageGuide.md +++ /dev/null @@ -1,258 +0,0 @@ -# Usage Guide - - -## Table of Contents -- [Usage Guide](#usage-guide) -- [Quick Start](#quick-start) - - [Creating Your First Widget](#creating-your-first-widget) - - [Customizing Widgets](#customizing-widgets) -- [Live Data Integration](#live-data-integration) - - [Setting Up Data Sources](#setting-up-data-sources) - - [Custom Data Sources](#custom-data-sources) -- [Analytics and Monitoring](#analytics-and-monitoring) - - [Performance Tracking](#performance-tracking) - - [Error Handling](#error-handling) -- [Advanced Features](#advanced-features) - - [Performance Optimization](#performance-optimization) - - [Widget Lifecycle Management](#widget-lifecycle-management) -- [Best Practices](#best-practices) - - [Memory Management](#memory-management) - - [Battery Optimization](#battery-optimization) - - [Error Handling](#error-handling) - - [Performance](#performance) -- [Examples](#examples) - - - -## Quick Start - -### Creating Your First Widget - -```swift -import WidgetKit -import WidgetTemplates - -// 1. Get a template -let weatherTemplate = WeatherWidgetTemplate() - -// 2. Get default configuration -let configuration = weatherTemplate.getDefaultConfiguration() - -// 3. Create the widget -let widget = WidgetEngine.shared.createWidget(with: configuration) -``` - -### Customizing Widgets - -```swift -// Create custom configuration -let customization = WidgetCustomization( - backgroundColor: .blue.opacity(0.1), - textColor: .primary, - accentColor: .blue, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16), - font: .title2 -) - -let widgetDefinition = WidgetDefinition( - id: "my_weather_widget", - type: .weather, - dataSourceIdentifier: "weather_api", - customization: customization -) - -let configuration = WidgetConfiguration( - id: "my_config", - templateIdentifier: "weather_widget", - widgets: [widgetDefinition], - settings: WidgetSettings( - refreshInterval: 300.0, - enableAnimations: true, - enableHaptics: true, - enableSound: false - ) -) -``` - -## Live Data Integration - -### Setting Up Data Sources - -```swift -import LiveDataIntegration - -// Register a data source -let weatherDataSource = WeatherDataSource() -LiveDataIntegration.shared.registerDataSource("weather_api", dataSource: weatherDataSource) - -// Connect to WebSocket for real-time updates -let url = URL(string: "ws://your-api.com/weather")! -LiveDataIntegration.shared.connectWebSocket(identifier: "weather_ws", url: url) - -// Get data publisher -let publisher = LiveDataIntegration.shared.getDataPublisher(for: "weather_api") -publisher?.sink { data in - print("Received weather data: \(data)") -} -.store(in: &cancellables) -``` - -### Custom Data Sources - -```swift -class CustomDataSource: WidgetDataSource { - let identifier = "custom_data" - private let dataSubject = PassthroughSubject() - - var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - func refresh() { - // Fetch data from your API - let data = WidgetData( - id: "custom_data", - type: .weather, - content: ["temperature": 22, "condition": "sunny"] - ) - dataSubject.send(data) - } - - func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} -``` - -## Analytics and Monitoring - -### Performance Tracking - -```swift -import WidgetAnalytics - -// Get analytics data -let analytics = WidgetEngine.shared.getAnalytics() -print("Total widget updates: \(analytics.totalWidgetUpdates)") -print("Memory usage: \(analytics.memoryUsage)") -print("Battery level: \(analytics.batteryLevel)") - -// Configure analytics -let config = AnalyticsConfiguration( - enableExternalAnalytics: true, - maxEventLogSize: 1000, - enablePerformanceTracking: true, - enableErrorTracking: true -) -WidgetAnalyticsService.shared.configure(config) -``` - -### Error Handling - -```swift -// Listen for errors -NotificationCenter.default.addObserver( - forName: .widgetErrorOccurred, - object: nil, - queue: .main -) { notification in - if let error = notification.userInfo?["error"] as? WidgetError { - print("Widget error: \(error.localizedDescription)") - } -} - -// Handle specific error types -switch error { -case .templateNotFound(let templateId): - print("Template not found: \(templateId)") -case .dataSourceNotFound(let dataSourceId): - print("Data source not found: \(dataSourceId)") -case .connectionFailed(let reason): - print("Connection failed: \(reason)") -default: - print("Unknown error") -} -``` - -## Advanced Features - -### Performance Optimization - -```swift -// Configure performance settings -let settings = WidgetPerformanceSettings( - maxMemoryUsage: 50 * 1024 * 1024, // 50MB - refreshInterval: 60.0, // 60 seconds - enableBatteryOptimization: true -) - -WidgetEngine.shared.configurePerformance(settings) - -// Monitor performance -let metrics = WidgetEngine.shared.performanceMetrics -print("Memory usage: \(metrics.memoryUsage)") -print("Battery level: \(metrics.batteryLevel)") -print("Refresh count: \(metrics.refreshCount)") -``` - -### Widget Lifecycle Management - -```swift -// Register templates -let templates = [ - WeatherWidgetTemplate(), - CalendarWidgetTemplate(), - FitnessWidgetTemplate() -] - -templates.forEach { template in - WidgetEngine.shared.registerTemplate(template) -} - -// Update widgets -let data = WidgetData( - id: "widget_1", - type: .weather, - content: ["temperature": 25] -) - -WidgetEngine.shared.updateWidget("widget_1", with: data) -``` - -## Best Practices - -### Memory Management - -- Use weak references in closures -- Properly dispose of Combine cancellables -- Monitor memory usage with analytics - -### Battery Optimization - -- Enable battery optimization in settings -- Use appropriate refresh intervals -- Minimize network requests - -### Error Handling - -- Always handle potential errors -- Provide fallback data -- Log errors for debugging - -### Performance - -- Use lazy loading for large datasets -- Implement proper caching -- Monitor performance metrics - -## Examples - -See the [Examples](Examples/) directory for complete working examples of: - -- Basic widget creation -- Custom data sources -- Live data integration -- Analytics implementation -- Error handling -- Performance optimization \ No newline at end of file diff --git a/Documentation/WidgetDevelopmentManagerAPI.md b/Documentation/WidgetDevelopmentManagerAPI.md deleted file mode 100644 index 2b87767..0000000 --- a/Documentation/WidgetDevelopmentManagerAPI.md +++ /dev/null @@ -1,332 +0,0 @@ -# Widget Development Manager API - - -## Table of Contents -- [Widget Development Manager API](#widget-development-manager-api) -- [Overview](#overview) -- [Core Features](#core-features) - - [Widget Management](#widget-management) - - [Multi-Widget Support](#multi-widget-support) - - [Advanced Features](#advanced-features) -- [API Reference](#api-reference) - - [WidgetDevelopmentManager](#widgetdevelopmentmanager) - - [WidgetConfiguration](#widgetconfiguration) - - [Widget](#widget) - - [WidgetContent](#widgetcontent) - - [WidgetLifecycle](#widgetlifecycle) -- [Usage Examples](#usage-examples) - - [Basic Widget Manager Setup](#basic-widget-manager-setup) - - [Widget Registration](#widget-registration) - - [Widget Data Updates](#widget-data-updates) - - [Widget Analytics](#widget-analytics) -- [Error Handling](#error-handling) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [User Experience](#user-experience) - - [Data Management](#data-management) -- [Related Documentation](#related-documentation) - - - -## Overview - -The `WidgetDevelopmentManager` is the core component of the iOS Widget Development Kit that provides comprehensive widget management capabilities. This API enables developers to create, configure, and manage various types of widgets with advanced features and customization options. - -## Core Features - -### Widget Management -- **Widget Creation**: Create and register new widgets -- **Widget Configuration**: Configure widget settings and properties -- **Widget Lifecycle**: Manage widget lifecycle events -- **Widget Updates**: Handle widget data updates and refreshes - -### Multi-Widget Support -- **Home Screen Widgets**: Full-featured home screen widget support -- **Lock Screen Widgets**: Lock screen widget integration -- **StandBy Widgets**: StandBy mode widget support -- **Live Activities**: Real-time Live Activity management -- **Dynamic Island**: Dynamic Island integration - -### Advanced Features -- **Data Integration**: Seamless data source integration -- **Real-Time Updates**: Real-time widget updates -- **Background Sync**: Background data synchronization -- **Analytics**: Widget usage analytics and monitoring -- **Performance**: Optimized performance and battery efficiency - -## API Reference - -### WidgetDevelopmentManager - -The main manager class for widget development operations. - -```swift -class WidgetDevelopmentManager { - // MARK: - Initialization - - /// Initialize the widget development manager - init() - - /// Initialize with custom configuration - init(configuration: WidgetConfiguration) - - // MARK: - Widget Management - - /// Start the widget manager - func start(with configuration: WidgetConfiguration) async throws - - /// Stop the widget manager - func stop() async throws - - /// Register a new widget - func register(_ widget: Widget) async throws -> WidgetID - - /// Unregister a widget - func unregister(_ widgetID: WidgetID) async throws - - /// Update widget configuration - func updateConfiguration(_ configuration: WidgetConfiguration, for widgetID: WidgetID) async throws - - // MARK: - Data Integration - - /// Configure data integration - func configureDataIntegration(_ configuration: DataIntegrationConfiguration) async throws - - /// Fetch widget data - func fetchData(for widgetID: WidgetID) async throws -> WidgetData - - /// Update widget with new data - func updateWidget(_ widgetID: WidgetID, with data: WidgetData) async throws - - // MARK: - Analytics - - /// Get widget analytics - func getAnalytics(for widgetID: WidgetID) async throws -> WidgetAnalytics - - /// Get performance metrics - func getPerformanceMetrics(for widgetID: WidgetID) async throws -> PerformanceMetrics -} -``` - -### WidgetConfiguration - -Configuration class for widget settings and properties. - -```swift -struct WidgetConfiguration { - // MARK: - Widget Types - - /// Enable home screen widgets - var enableHomeScreenWidgets: Bool = true - - /// Enable lock screen widgets - var enableLockScreenWidgets: Bool = true - - /// Enable Live Activities - var enableLiveActivities: Bool = true - - /// Enable Dynamic Island - var enableDynamicIsland: Bool = true - - // MARK: - Widget Settings - - /// Default refresh interval in seconds - var defaultRefreshInterval: TimeInterval = 300 - - /// Enable background updates - var enableBackgroundUpdates: Bool = true - - /// Enable widget interactions - var enableInteractions: Bool = true - - /// Enable deep linking - var enableDeepLinking: Bool = true - - // MARK: - Styling Settings - - /// Enable dynamic colors - var enableDynamicColors: Bool = true - - /// Enable dark mode support - var enableDarkMode: Bool = true - - /// Enable custom fonts - var enableCustomFonts: Bool = true - - /// Enable animations - var enableAnimations: Bool = true -} -``` - -### Widget - -Base protocol for all widget types. - -```swift -protocol Widget { - /// Unique widget identifier - var id: WidgetID { get } - - /// Widget kind identifier - var kind: String { get } - - /// Widget configuration - var configuration: WidgetConfiguration { get } - - /// Widget content view - var content: WidgetContent { get } - - /// Widget lifecycle events - var lifecycle: WidgetLifecycle { get } -} -``` - -### WidgetContent - -Protocol for widget content views. - -```swift -protocol WidgetContent { - /// Create widget view - func createView(for context: WidgetContext) -> WidgetView - - /// Update widget view - func updateView(_ view: WidgetView, with context: WidgetContext) async throws - - /// Handle widget interactions - func handleInteraction(_ interaction: WidgetInteraction) async throws -} -``` - -### WidgetLifecycle - -Protocol for widget lifecycle management. - -```swift -protocol WidgetLifecycle { - /// Widget will appear - func willAppear() async throws - - /// Widget did appear - func didAppear() async throws - - /// Widget will disappear - func willDisappear() async throws - - /// Widget did disappear - func didDisappear() async throws - - /// Widget will update - func willUpdate() async throws - - /// Widget did update - func didUpdate() async throws -} -``` - -## Usage Examples - -### Basic Widget Manager Setup - -```swift -import WidgetDevelopmentKit - -// Initialize widget manager -let widgetManager = WidgetDevelopmentManager() - -// Configure widget settings -let widgetConfig = WidgetConfiguration() -widgetConfig.enableHomeScreenWidgets = true -widgetConfig.enableLockScreenWidgets = true -widgetConfig.enableLiveActivities = true -widgetConfig.enableDynamicIsland = true - -// Start widget manager -try await widgetManager.start(with: widgetConfig) - -// Configure data integration -try await widgetManager.configureDataIntegration(DataIntegrationConfiguration()) -``` - -### Widget Registration - -```swift -// Create widget -let widget = HomeScreenWidget( - kind: "com.company.app.widget", - configuration: widgetConfig -) - -// Register widget -let widgetID = try await widgetManager.register(widget) -print("✅ Widget registered with ID: \(widgetID)") -``` - -### Widget Data Updates - -```swift -// Fetch widget data -let data = try await widgetManager.fetchData(for: widgetID) - -// Update widget with new data -try await widgetManager.updateWidget(widgetID, with: data) -``` - -### Widget Analytics - -```swift -// Get widget analytics -let analytics = try await widgetManager.getAnalytics(for: widgetID) -print("Widget views: \(analytics.viewCount)") -print("Widget interactions: \(analytics.interactionCount)") - -// Get performance metrics -let metrics = try await widgetManager.getPerformanceMetrics(for: widgetID) -print("Average load time: \(metrics.averageLoadTime)s") -print("Memory usage: \(metrics.memoryUsage)MB") -``` - -## Error Handling - -The Widget Development Manager uses comprehensive error handling for all operations: - -```swift -enum WidgetDevelopmentError: Error { - case initializationFailed(String) - case widgetRegistrationFailed(String) - case widgetUpdateFailed(String) - case dataFetchFailed(String) - case configurationInvalid(String) - case widgetNotFound(WidgetID) - case operationNotSupported(String) -} -``` - -## Best Practices - -### Performance Optimization -- Use appropriate refresh intervals -- Implement efficient data fetching -- Minimize widget memory usage -- Optimize widget rendering - -### User Experience -- Provide meaningful widget content -- Handle loading and error states -- Support accessibility features -- Implement smooth animations - -### Data Management -- Cache frequently used data -- Implement offline support -- Handle data validation -- Provide fallback content - -## Related Documentation - -- [Home Screen Widget API](HomeScreenWidgetAPI.md) -- [Lock Screen Widget API](LockScreenWidgetAPI.md) -- [Live Activity API](LiveActivityAPI.md) -- [Dynamic Island API](DynamicIslandAPI.md) -- [Data Integration API](DataIntegrationAPI.md) -- [Configuration API](ConfigurationAPI.md) diff --git a/Examples/AdvancedExample.swift b/Examples/AdvancedExample.swift deleted file mode 100644 index 348c7aa..0000000 --- a/Examples/AdvancedExample.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import iOS-Widget-Development-Kit - -/// Advanced example showing complex usage patterns -struct AdvancedExample { - let framework: iOS-Widget-Development-Kit - - init() { - self.framework = iOS-Widget-Development-Kit() - } - - func run() { - print("🚀 iOS-Widget-Development-Kit Advanced Example") - - // Advanced configuration - configureAdvanced() - - // Run advanced features - runAdvancedFeatures() - - // Performance testing - runPerformanceTests() - } - - private func configureAdvanced() { - print("\n⚙️ Configuring advanced settings...") - - // Add advanced configuration here - print("✅ Advanced configuration complete") - } - - private func runAdvancedFeatures() { - print("\n🎯 Running advanced features...") - - // Add advanced feature demonstrations here - print("✅ Advanced features completed") - } - - private func runPerformanceTests() { - print("\n📊 Running performance tests...") - - // Add performance testing code here - print("✅ Performance tests completed") - } -} diff --git a/Examples/AdvancedExamples/AdvancedDemo.swift b/Examples/AdvancedExamples/AdvancedDemo.swift deleted file mode 100644 index ac53b13..0000000 --- a/Examples/AdvancedExamples/AdvancedDemo.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -@available(iOS 15.0, *) -struct AdvancedDemo: View { - @State private var isActive: Bool = false - - var body: some View { - VStack(spacing: 16) { - Text("Advanced Example") - .font(.title) - Toggle("Enable Feature", isOn: $isActive) - RoundedRectangle(cornerRadius: 16) - .fill(isActive ? .green : .gray) - .frame(width: 240, height: 140) - .overlay(Text(isActive ? "ENABLED" : "DISABLED").foregroundColor(.white)) - .animation(.easeInOut, value: isActive) - } - .padding(24) - } -} diff --git a/Examples/AdvancedExamples/AdvancedWidgetExample.swift b/Examples/AdvancedExamples/AdvancedWidgetExample.swift deleted file mode 100644 index 053305c..0000000 --- a/Examples/AdvancedExamples/AdvancedWidgetExample.swift +++ /dev/null @@ -1,365 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates -import LiveDataIntegration -import WidgetAnalytics - -@available(iOS 16.0, *) -struct AdvancedWidgetExample: View { - @StateObject private var widgetEngine = WidgetEngine.shared - @State private var selectedWidgets: [WidgetConfiguration] = [] - @State private var isLiveDataEnabled = false - @State private var analyticsData: WidgetAnalytics? - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 20) { - // Live Data Integration Section - LiveDataSection(isEnabled: $isLiveDataEnabled) - - // Widget Gallery - WidgetGallerySection(selectedWidgets: $selectedWidgets) - - // Analytics Dashboard - AnalyticsSection(analyticsData: $analyticsData) - - // Performance Monitoring - PerformanceSection() - } - .padding() - } - .navigationTitle("Advanced Widget Builder") - .onAppear { - setupLiveDataIntegration() - loadAnalytics() - } - } - } - - private func setupLiveDataIntegration() { - // Register data sources - let weatherDataSource = WeatherDataSource() - let calendarDataSource = CalendarDataSource() - let fitnessDataSource = FitnessDataSource() - - LiveDataIntegration.shared.registerDataSource("weather_api", dataSource: weatherDataSource) - LiveDataIntegration.shared.registerDataSource("calendar_events", dataSource: calendarDataSource) - LiveDataIntegration.shared.registerDataSource("health_kit", dataSource: fitnessDataSource) - - // Configure data source settings - let settings = DataSourceSettings( - refreshInterval: 30.0, - enableCaching: true, - maxRetries: 3, - timeout: 10.0 - ) - - LiveDataIntegration.shared.configureDataSource("weather_api", settings: settings) - LiveDataIntegration.shared.configureDataSource("calendar_events", settings: settings) - LiveDataIntegration.shared.configureDataSource("health_kit", settings: settings) - } - - private func loadAnalytics() { - analyticsData = widgetEngine.getAnalytics() - } -} - -@available(iOS 16.0, *) -struct LiveDataSection: View { - @Binding var isEnabled: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Live Data Integration") - .font(.headline) - - Toggle("Enable Real-time Updates", isOn: $isEnabled) - .onChange(of: isEnabled) { newValue in - if newValue { - enableLiveData() - } else { - disableLiveData() - } - } - - if isEnabled { - LiveDataStatusView() - } - } - .padding() - .background(Color.blue.opacity(0.1)) - .cornerRadius(12) - } - - private func enableLiveData() { - // Connect to WebSocket for real-time updates - let weatherURL = URL(string: "ws://api.weatherapi.com/v1/current.json")! - LiveDataIntegration.shared.connectWebSocket(identifier: "weather_ws", url: weatherURL) - - let calendarURL = URL(string: "ws://api.calendar.com/events")! - LiveDataIntegration.shared.connectWebSocket(identifier: "calendar_ws", url: calendarURL) - } - - private func disableLiveData() { - LiveDataIntegration.shared.disconnectWebSocket(identifier: "weather_ws") - LiveDataIntegration.shared.disconnectWebSocket(identifier: "calendar_ws") - } -} - -@available(iOS 16.0, *) -struct LiveDataStatusView: View { - @State private var weatherStatus: ConnectionStatus = .connecting - @State private var calendarStatus: ConnectionStatus = .connecting - - var body: some View { - VStack(spacing: 10) { - HStack { - Text("Weather API") - Spacer() - ConnectionStatusIndicator(status: weatherStatus) - } - - HStack { - Text("Calendar API") - Spacer() - ConnectionStatusIndicator(status: calendarStatus) - } - } - .onAppear { - updateConnectionStatus() - } - } - - private func updateConnectionStatus() { - weatherStatus = LiveDataIntegration.shared.getConnectionStatus(for: "weather_ws") - calendarStatus = LiveDataIntegration.shared.getConnectionStatus(for: "calendar_ws") - } -} - -@available(iOS 16.0, *) -struct ConnectionStatusIndicator: View { - let status: ConnectionStatus - - var body: some View { - HStack(spacing: 4) { - Circle() - .fill(statusColor) - .frame(width: 8, height: 8) - - Text(statusText) - .font(.caption) - .foregroundColor(.secondary) - } - } - - private var statusColor: Color { - switch status { - case .connected: return .green - case .connecting: return .orange - case .disconnected: return .red - case .unknown: return .gray - } - } - - private var statusText: String { - switch status { - case .connected: return "Connected" - case .connecting: return "Connecting" - case .disconnected: return "Disconnected" - case .unknown: return "Unknown" - } - } -} - -@available(iOS 16.0, *) -struct WidgetGallerySection: View { - @Binding var selectedWidgets: [WidgetConfiguration] - - let availableTemplates: [WidgetTemplate] = [ - WeatherWidgetTemplate(), - CalendarWidgetTemplate(), - FitnessWidgetTemplate(), - NewsWidgetTemplate(), - SocialMediaWidgetTemplate(), - ProductivityWidgetTemplate(), - EntertainmentWidgetTemplate(), - FinanceWidgetTemplate(), - HealthWidgetTemplate(), - TravelWidgetTemplate() - ] - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Widget Gallery") - .font(.headline) - - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 15) { - ForEach(availableTemplates, id: \.identifier) { template in - WidgetTemplateCard(template: template) { - addWidget(template: template) - } - } - } - } - .padding() - .background(Color.green.opacity(0.1)) - .cornerRadius(12) - } - - private func addWidget(template: WidgetTemplate) { - let configuration = template.getDefaultConfiguration() - selectedWidgets.append(configuration) - } -} - -@available(iOS 16.0, *) -struct WidgetTemplateCard: View { - let template: WidgetTemplate - let onTap: () -> Void - - var body: some View { - VStack(spacing: 8) { - Image(systemName: iconName) - .font(.title2) - .foregroundColor(.blue) - - Text(template.displayName) - .font(.caption) - .multilineTextAlignment(.center) - .lineLimit(2) - - Text(template.description) - .font(.caption2) - .foregroundColor(.secondary) - .lineLimit(3) - } - .frame(height: 100) - .padding() - .background(Color.white) - .cornerRadius(8) - .shadow(radius: 2) - .onTapGesture { - onTap() - } - } - - private var iconName: String { - switch template.identifier { - case "weather_widget": return "cloud.sun.fill" - case "calendar_widget": return "calendar" - case "fitness_widget": return "figure.walk" - case "news_widget": return "newspaper" - case "social_media_widget": return "bubble.left.and.bubble.right" - case "productivity_widget": return "chart.line.uptrend.xyaxis" - case "entertainment_widget": return "play.circle" - case "finance_widget": return "dollarsign.circle" - case "health_widget": return "heart.fill" - case "travel_widget": return "airplane" - default: return "square.fill" - } - } -} - -@available(iOS 16.0, *) -struct AnalyticsSection: View { - @Binding var analyticsData: WidgetAnalytics? - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Analytics Dashboard") - .font(.headline) - - if let analytics = analyticsData { - AnalyticsGridView(analytics: analytics) - } else { - ProgressView("Loading analytics...") - } - } - .padding() - .background(Color.purple.opacity(0.1)) - .cornerRadius(12) - } -} - -@available(iOS 16.0, *) -struct AnalyticsGridView: View { - let analytics: WidgetAnalytics - - var body: some View { - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 10) { - AnalyticsCard(title: "Total Updates", value: "\(analytics.totalWidgetUpdates)") - AnalyticsCard(title: "Memory Usage", value: "\(analytics.memoryUsage / 1024 / 1024) MB") - AnalyticsCard(title: "Connections", value: "\(analytics.connectionsEstablished)") - AnalyticsCard(title: "Errors", value: "\(analytics.errors)") - } - } -} - -@available(iOS 16.0, *) -struct AnalyticsCard: View { - let title: String - let value: String - - var body: some View { - VStack(spacing: 4) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - - Text(value) - .font(.caption) - .fontWeight(.bold) - } - .frame(maxWidth: .infinity) - .padding(8) - .background(Color.white) - .cornerRadius(6) - } -} - -@available(iOS 16.0, *) -struct PerformanceSection: View { - @StateObject private var performanceMetrics = WidgetEngine.shared.performanceMetrics - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Performance Monitoring") - .font(.headline) - - VStack(spacing: 10) { - PerformanceMetricRow(title: "Memory Usage", value: "\(performanceMetrics.memoryUsage / 1024 / 1024) MB") - PerformanceMetricRow(title: "Battery Level", value: "\(Int(performanceMetrics.batteryLevel * 100))%") - PerformanceMetricRow(title: "Refresh Count", value: "\(performanceMetrics.refreshCount)") - PerformanceMetricRow(title: "Error Count", value: "\(performanceMetrics.errorCount)") - } - } - .padding() - .background(Color.orange.opacity(0.1)) - .cornerRadius(12) - } -} - -@available(iOS 16.0, *) -struct PerformanceMetricRow: View { - let title: String - let value: String - - var body: some View { - HStack { - Text(title) - .font(.caption) - Spacer() - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -struct AdvancedWidgetExample_Previews: PreviewProvider { - static var previews: some View { - AdvancedWidgetExample() - } -} \ No newline at end of file diff --git a/Examples/AdvancedExamples/README.md b/Examples/AdvancedExamples/README.md deleted file mode 100644 index fe6ea18..0000000 --- a/Examples/AdvancedExamples/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Advanced Examples — iOS-Widget-Development-Kit - -- Feature toggling demo with SwiftUI animations -- Replace with repository-specific examples when integrating with package modules - -Run in a host app target or SwiftUI preview. diff --git a/Examples/BasicExample.swift b/Examples/BasicExample.swift deleted file mode 100644 index bcb4924..0000000 --- a/Examples/BasicExample.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import UIKit - -/** - * Basic Example - Getting Started - * - * This example demonstrates the basic usage of our framework. - * Perfect for beginners and quick prototyping. - */ - -class BasicExampleViewController: UIViewController { - - // MARK: - Properties - private let titleLabel = UILabel() - private let descriptionLabel = UILabel() - private let actionButton = UIButton(type: .system) - - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - configureLayout() - setupActions() - } - - // MARK: - UI Setup - private func setupUI() { - view.backgroundColor = .systemBackground - - // Title Label - titleLabel.text = "Basic Example" - titleLabel.font = .systemFont(ofSize: 28, weight: .bold) - titleLabel.textAlignment = .center - titleLabel.textColor = .label - - // Description Label - descriptionLabel.text = "This is a basic example showing fundamental usage patterns." - descriptionLabel.font = .systemFont(ofSize: 16) - descriptionLabel.textAlignment = .center - descriptionLabel.textColor = .secondaryLabel - descriptionLabel.numberOfLines = 0 - - // Action Button - actionButton.setTitle("Start Example", for: .normal) - actionButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) - actionButton.backgroundColor = .systemBlue - actionButton.setTitleColor(.white, for: .normal) - actionButton.layer.cornerRadius = 12 - } - - // MARK: - Layout - private func configureLayout() { - [titleLabel, descriptionLabel, actionButton].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - view.addSubview($0) - } - - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - - descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20), - descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - - actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - actionButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 40), - actionButton.widthAnchor.constraint(equalToConstant: 200), - actionButton.heightAnchor.constraint(equalToConstant: 50) - ]) - } - - // MARK: - Actions - private func setupActions() { - actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) - } - - @objc private func actionButtonTapped() { - // Example action implementation - let alert = UIAlertController(title: "Example Started", - message: "Basic example is now running!", - preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - present(alert, animated: true) - } -} diff --git a/Examples/BasicExamples/BasicWidgetExample.swift b/Examples/BasicExamples/BasicWidgetExample.swift deleted file mode 100644 index 11b4092..0000000 --- a/Examples/BasicExamples/BasicWidgetExample.swift +++ /dev/null @@ -1,211 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates - -@available(iOS 16.0, *) -struct BasicWidgetExample: View { - @StateObject private var widgetEngine = WidgetEngine.shared - @State private var selectedTemplate: WidgetTemplate? - @State private var widgetConfiguration: WidgetConfiguration? - - var body: some View { - NavigationView { - VStack(spacing: 20) { - // Template Selection - VStack(alignment: .leading, spacing: 10) { - Text("Select Widget Template") - .font(.headline) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 15) { - TemplateCard( - template: WeatherWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "weather_widget" - ) { - selectedTemplate = WeatherWidgetTemplate() - } - - TemplateCard( - template: CalendarWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "calendar_widget" - ) { - selectedTemplate = CalendarWidgetTemplate() - } - - TemplateCard( - template: FitnessWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "fitness_widget" - ) { - selectedTemplate = FitnessWidgetTemplate() - } - - TemplateCard( - template: NewsWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "news_widget" - ) { - selectedTemplate = NewsWidgetTemplate() - } - } - .padding(.horizontal) - } - } - - // Widget Preview - if let configuration = widgetConfiguration { - VStack(alignment: .leading, spacing: 10) { - Text("Widget Preview") - .font(.headline) - - widgetEngine.createWidget(with: configuration) - .frame(height: 200) - .background(Color.gray.opacity(0.1)) - .cornerRadius(12) - } - } - - // Customization Options - if let template = selectedTemplate { - CustomizationPanel(template: template) { configuration in - self.widgetConfiguration = configuration - } - } - - Spacer() - } - .padding() - .navigationTitle("Widget Builder") - } - } -} - -@available(iOS 16.0, *) -struct TemplateCard: View { - let template: WidgetTemplate - let isSelected: Bool - let onTap: () -> Void - - var body: some View { - VStack(spacing: 8) { - Image(systemName: iconName) - .font(.title) - .foregroundColor(isSelected ? .white : .blue) - - Text(template.displayName) - .font(.caption) - .multilineTextAlignment(.center) - } - .frame(width: 80, height: 80) - .background(isSelected ? Color.blue : Color.gray.opacity(0.1)) - .cornerRadius(12) - .onTapGesture { - onTap() - } - } - - private var iconName: String { - switch template.identifier { - case "weather_widget": return "cloud.sun.fill" - case "calendar_widget": return "calendar" - case "fitness_widget": return "figure.walk" - case "news_widget": return "newspaper" - default: return "square.fill" - } - } -} - -@available(iOS 16.0, *) -struct CustomizationPanel: View { - let template: WidgetTemplate - let onConfigurationChange: (WidgetConfiguration) -> Void - - @State private var backgroundColor = Color.blue.opacity(0.1) - @State private var textColor = Color.primary - @State private var accentColor = Color.blue - @State private var cornerRadius: CGFloat = 12 - @State private var refreshInterval: Double = 30.0 - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Customization") - .font(.headline) - - VStack(spacing: 10) { - ColorPicker("Background Color", selection: $backgroundColor) - ColorPicker("Text Color", selection: $textColor) - ColorPicker("Accent Color", selection: $accentColor) - - HStack { - Text("Corner Radius") - Slider(value: $cornerRadius, in: 0...20) - Text("\(Int(cornerRadius))") - } - - HStack { - Text("Refresh Interval") - Slider(value: $refreshInterval, in: 10...300) - Text("\(Int(refreshInterval))s") - } - } - - Button("Create Widget") { - createWidget() - } - .buttonStyle(.borderedProminent) - } - .padding() - .background(Color.gray.opacity(0.05)) - .cornerRadius(12) - } - - private func createWidget() { - let customization = WidgetCustomization( - backgroundColor: backgroundColor, - textColor: textColor, - accentColor: accentColor, - cornerRadius: cornerRadius - ) - - let widgetDefinition = WidgetDefinition( - id: "custom_widget", - type: widgetType, - dataSourceIdentifier: dataSourceIdentifier, - customization: customization - ) - - let configuration = WidgetConfiguration( - id: "custom_config", - templateIdentifier: template.identifier, - widgets: [widgetDefinition], - settings: WidgetSettings(refreshInterval: refreshInterval) - ) - - onConfigurationChange(configuration) - } - - private var widgetType: WidgetType { - switch template.identifier { - case "weather_widget": return .weather - case "calendar_widget": return .calendar - case "fitness_widget": return .fitness - case "news_widget": return .news - default: return .weather - } - } - - private var dataSourceIdentifier: String { - switch template.identifier { - case "weather_widget": return "weather_api" - case "calendar_widget": return "calendar_events" - case "fitness_widget": return "health_kit" - case "news_widget": return "news_api" - default: return "default_api" - } - } -} - -@available(iOS 16.0, *) -struct BasicWidgetExample_Previews: PreviewProvider { - static var previews: some View { - BasicWidgetExample() - } -} \ No newline at end of file diff --git a/Examples/DynamicIslandExamples/DynamicIslandExample.swift b/Examples/DynamicIslandExamples/DynamicIslandExample.swift deleted file mode 100644 index cae9dc3..0000000 --- a/Examples/DynamicIslandExamples/DynamicIslandExample.swift +++ /dev/null @@ -1,195 +0,0 @@ -import SwiftUI -import ActivityKit -import WidgetKit - -// MARK: - Dynamic Island Example -struct DynamicIslandExample: Widget { - let kind: String = "com.company.app.dynamicisland" - - var body: some WidgetConfiguration { - ActivityConfiguration(for: DynamicIslandAttributes.self) { context in - // This is the Live Activity view - DynamicIslandLiveActivityView(context: context) - } dynamicIsland: { context in - DynamicIsland { - // Expanded UI - DynamicIslandExpandedRegion(.leading) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - Text("Now Playing") - .font(.caption) - } - } - - DynamicIslandExpandedRegion(.trailing) { - Text("2:30") - .font(.caption) - .foregroundColor(.secondary) - } - - DynamicIslandExpandedRegion(.center) { - VStack(spacing: 8) { - Text("Song Title") - .font(.headline) - - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - } - - DynamicIslandExpandedRegion(.bottom) { - HStack(spacing: 20) { - Button("Previous") { - // Previous track - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/Pause - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track - } - .buttonStyle(.bordered) - } - } - } compactLeading: { - // Compact leading - Image(systemName: "music.note") - .foregroundColor(.blue) - } compactTrailing: { - // Compact trailing - Text("2:30") - .font(.caption2) - .foregroundColor(.secondary) - } minimal: { - // Minimal - Image(systemName: "music.note") - .foregroundColor(.blue) - } - } - } -} - -// MARK: - Dynamic Island Attributes -struct DynamicIslandAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var songTitle: String - var artistName: String - var isPlaying: Bool - var currentTime: String - var duration: String - var progress: Double - } - - var playlistName: String -} - -// MARK: - Live Activity View -struct DynamicIslandLiveActivityView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 16) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text(context.state.songTitle) - .font(.headline) - - Text(context.state.artistName) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("\(context.state.currentTime) / \(context.state.duration)") - .font(.caption2) - .foregroundColor(.secondary) - } - - ProgressView(value: context.state.progress) - .progressViewStyle(.linear) - .tint(.blue) - - HStack(spacing: 20) { - Button("Previous") { - // Previous track - } - .buttonStyle(.bordered) - - Button(context.state.isPlaying ? "Pause" : "Play") { - // Play/Pause - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track - } - .buttonStyle(.bordered) - } - } - .padding() - .background(Color(.systemBackground)) - } -} - -// MARK: - Dynamic Island Manager -class DynamicIslandManager { - static let shared = DynamicIslandManager() - - private var activity: Activity? - - func startDynamicIsland(playlistName: String, songTitle: String, artistName: String) { - let attributes = DynamicIslandAttributes(playlistName: playlistName) - - let contentState = DynamicIslandAttributes.ContentState( - songTitle: songTitle, - artistName: artistName, - isPlaying: true, - currentTime: "1:30", - duration: "3:45", - progress: 0.4 - ) - - do { - activity = try Activity.request( - attributes: attributes, - contentState: contentState, - pushType: nil - ) - print("✅ Dynamic Island activity started") - } catch { - print("❌ Dynamic Island activity failed: \(error)") - } - } - - func updateDynamicIsland(songTitle: String, artistName: String, isPlaying: Bool, currentTime: String, duration: String, progress: Double) { - Task { - let contentState = DynamicIslandAttributes.ContentState( - songTitle: songTitle, - artistName: artistName, - isPlaying: isPlaying, - currentTime: currentTime, - duration: duration, - progress: progress - ) - - await activity?.update(using: contentState) - } - } - - func endDynamicIsland() { - Task { - await activity?.end(dismissalPolicy: .immediate) - } - } -} \ No newline at end of file diff --git a/Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift b/Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift deleted file mode 100644 index 6fb0f2c..0000000 --- a/Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift +++ /dev/null @@ -1,89 +0,0 @@ -import SwiftUI -import WidgetKit - -// MARK: - Home Screen Widget Example -struct HomeScreenWidgetExample: Widget { - let kind: String = "com.company.app.homescreenwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: HomeScreenWidgetProvider()) { entry in - HomeScreenWidgetEntryView(entry: entry) - } - .configurationDisplayName("Home Screen Widget") - .description("A beautiful home screen widget example") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} - -// MARK: - Widget Provider -struct HomeScreenWidgetProvider: TimelineProvider { - func placeholder(in context: Context) -> HomeScreenWidgetEntry { - HomeScreenWidgetEntry(date: Date(), title: "Welcome", message: "Tap to open app") - } - - func getSnapshot(in context: Context, completion: @escaping (HomeScreenWidgetEntry) -> ()) { - let entry = HomeScreenWidgetEntry(date: Date(), title: "Welcome", message: "Tap to open app") - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - let currentDate = Date() - let entry = HomeScreenWidgetEntry(date: currentDate, title: "Welcome", message: "Tap to open app") - let timeline = Timeline(entries: [entry], policy: .atEnd) - completion(timeline) - } -} - -// MARK: - Widget Entry -struct HomeScreenWidgetEntry: TimelineEntry { - let date: Date - let title: String - let message: String -} - -// MARK: - Widget Entry View -struct HomeScreenWidgetEntryView: View { - let entry: HomeScreenWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - VStack(spacing: 8) { - HStack { - Image(systemName: "house.fill") - .foregroundColor(.blue) - .font(.title2) - - Spacer() - - Text(entry.title) - .font(.headline) - .foregroundColor(.primary) - } - - Spacer() - - Text(entry.message) - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - - Spacer() - - Button("Open App") { - // Deep link to app - } - .buttonStyle(.bordered) - .font(.caption) - } - .padding() - .background(Color(.systemBackground)) - } -} - -// MARK: - Preview -struct HomeScreenWidgetExample_Previews: PreviewProvider { - static var previews: some View { - HomeScreenWidgetEntryView(entry: HomeScreenWidgetEntry(date: Date(), title: "Welcome", message: "Tap to open app")) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - } -} \ No newline at end of file diff --git a/Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift b/Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift deleted file mode 100644 index 0d13bc6..0000000 --- a/Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift +++ /dev/null @@ -1,357 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates - -// MARK: - Weather Widget Example -// This example demonstrates how to create a beautiful weather widget -// with real-time data integration and dynamic theming - -struct WeatherWidgetExample: Widget { - static let kind: String = "com.company.app.weatherwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: WeatherTimelineProvider()) { entry in - WeatherWidgetView(entry: entry) - } - .configurationDisplayName("Weather Widget") - .description("Displays current weather conditions with beautiful animations") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} - -// MARK: - Weather Timeline Provider -struct WeatherTimelineProvider: TimelineProvider { - typealias Entry = WeatherWidgetEntry - - func placeholder(in context: Context) -> WeatherWidgetEntry { - WeatherWidgetEntry( - date: Date(), - temperature: "72°F", - condition: "Sunny", - location: "San Francisco", - humidity: "65%", - windSpeed: "8 mph", - icon: "sun.max.fill" - ) - } - - func getSnapshot(in context: Context, completion: @escaping (WeatherWidgetEntry) -> Void) { - let entry = WeatherWidgetEntry( - date: Date(), - temperature: "72°F", - condition: "Sunny", - location: "San Francisco", - humidity: "65%", - windSpeed: "8 mph", - icon: "sun.max.fill" - ) - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - // Simulate weather data updates every 30 minutes - let currentDate = Date() - let entries = (0..<48).map { index in - let entryDate = Calendar.current.date(byAdding: .minute, value: index * 30, to: currentDate)! - return WeatherWidgetEntry( - date: entryDate, - temperature: "\(70 + Int.random(in: -5...5))°F", - condition: ["Sunny", "Cloudy", "Rainy", "Partly Cloudy"].randomElement()!, - location: "San Francisco", - humidity: "\(60 + Int.random(in: -10...10))%", - windSpeed: "\(5 + Int.random(in: 0...10)) mph", - icon: getWeatherIcon(for: "Sunny") - ) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - private func getWeatherIcon(for condition: String) -> String { - switch condition { - case "Sunny": return "sun.max.fill" - case "Cloudy": return "cloud.fill" - case "Rainy": return "cloud.rain.fill" - case "Partly Cloudy": return "cloud.sun.fill" - default: return "sun.max.fill" - } - } -} - -// MARK: - Weather Widget Entry -struct WeatherWidgetEntry: TimelineEntry { - let date: Date - let temperature: String - let condition: String - let location: String - let humidity: String - let windSpeed: String - let icon: String -} - -// MARK: - Weather Widget View -struct WeatherWidgetView: View { - let entry: WeatherWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - switch family { - case .systemSmall: - SmallWeatherWidgetView(entry: entry) - case .systemMedium: - MediumWeatherWidgetView(entry: entry) - case .systemLarge: - LargeWeatherWidgetView(entry: entry) - default: - SmallWeatherWidgetView(entry: entry) - } - } -} - -// MARK: - Small Weather Widget -struct SmallWeatherWidgetView: View { - let entry: WeatherWidgetEntry - - var body: some View { - ZStack { - // Background gradient - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.cyan.opacity(0.6)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - - VStack(spacing: 8) { - // Weather icon - Image(systemName: entry.icon) - .font(.system(size: 32)) - .foregroundColor(.white) - .shadow(radius: 2) - - // Temperature - Text(entry.temperature) - .font(.system(size: 24, weight: .bold, design: .rounded)) - .foregroundColor(.white) - - // Condition - Text(entry.condition) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white.opacity(0.9)) - .multilineTextAlignment(.center) - } - .padding() - } - .widgetURL(URL(string: "weatherwidget://open")) - } -} - -// MARK: - Medium Weather Widget -struct MediumWeatherWidgetView: View { - let entry: WeatherWidgetEntry - - var body: some View { - HStack(spacing: 16) { - // Left side - Main weather info - VStack(alignment: .leading, spacing: 8) { - // Location - Text(entry.location) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - - // Temperature - Text(entry.temperature) - .font(.system(size: 36, weight: .bold, design: .rounded)) - .foregroundColor(.white) - - // Condition - Text(entry.condition) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.white.opacity(0.9)) - } - - Spacer() - - // Right side - Weather icon and details - VStack(alignment: .trailing, spacing: 8) { - // Weather icon - Image(systemName: entry.icon) - .font(.system(size: 40)) - .foregroundColor(.white) - .shadow(radius: 2) - - // Additional details - VStack(alignment: .trailing, spacing: 4) { - HStack { - Image(systemName: "humidity") - .font(.system(size: 10)) - Text(entry.humidity) - .font(.system(size: 10)) - } - .foregroundColor(.white.opacity(0.8)) - - HStack { - Image(systemName: "wind") - .font(.system(size: 10)) - Text(entry.windSpeed) - .font(.system(size: 10)) - } - .foregroundColor(.white.opacity(0.8)) - } - } - } - .padding() - .background( - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.cyan.opacity(0.6)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .widgetURL(URL(string: "weatherwidget://open")) - } -} - -// MARK: - Large Weather Widget -struct LargeWeatherWidgetView: View { - let entry: WeatherWidgetEntry - - var body: some View { - VStack(spacing: 16) { - // Header - HStack { - VStack(alignment: .leading) { - Text(entry.location) - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(.white) - - Text(entry.condition) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white.opacity(0.9)) - } - - Spacer() - - // Weather icon - Image(systemName: entry.icon) - .font(.system(size: 50)) - .foregroundColor(.white) - .shadow(radius: 3) - } - - // Main temperature - Text(entry.temperature) - .font(.system(size: 60, weight: .bold, design: .rounded)) - .foregroundColor(.white) - - // Weather details - HStack(spacing: 24) { - WeatherDetailView( - icon: "humidity", - title: "Humidity", - value: entry.humidity - ) - - WeatherDetailView( - icon: "wind", - title: "Wind", - value: entry.windSpeed - ) - - WeatherDetailView( - icon: "thermometer", - title: "Feels Like", - value: "\(Int(entry.temperature.dropLast(2)) ?? 0 + 2)°F" - ) - } - - // Hourly forecast (simplified) - HStack(spacing: 16) { - ForEach(0..<4) { hour in - VStack(spacing: 4) { - Text("\(hour * 3):00") - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.8)) - - Image(systemName: entry.icon) - .font(.system(size: 16)) - .foregroundColor(.white) - - Text("\(70 + hour * 2)°") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white) - } - } - } - } - .padding() - .background( - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.cyan.opacity(0.6)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .widgetURL(URL(string: "weatherwidget://open")) - } -} - -// MARK: - Weather Detail View -struct WeatherDetailView: View { - let icon: String - let title: String - let value: String - - var body: some View { - VStack(spacing: 4) { - Image(systemName: icon) - .font(.system(size: 16)) - .foregroundColor(.white.opacity(0.8)) - - Text(title) - .font(.system(size: 10)) - .foregroundColor(.white.opacity(0.7)) - - Text(value) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white) - } - } -} - -// MARK: - Widget Bundle -@main -struct WeatherWidgetBundle: WidgetBundle { - var body: some Widget { - WeatherWidgetExample() - } -} - -// MARK: - Preview -struct WeatherWidgetView_Previews: PreviewProvider { - static var previews: some View { - let entry = WeatherWidgetEntry( - date: Date(), - temperature: "72°F", - condition: "Sunny", - location: "San Francisco", - humidity: "65%", - windSpeed: "8 mph", - icon: "sun.max.fill" - ) - - Group { - WeatherWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .previewDisplayName("Small Weather Widget") - - WeatherWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .previewDisplayName("Medium Weather Widget") - - WeatherWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - .previewDisplayName("Large Weather Widget") - } - } -} \ No newline at end of file diff --git a/Examples/LiveActivityExamples/FitnessTrackerExample.swift b/Examples/LiveActivityExamples/FitnessTrackerExample.swift deleted file mode 100644 index d096a2f..0000000 --- a/Examples/LiveActivityExamples/FitnessTrackerExample.swift +++ /dev/null @@ -1,528 +0,0 @@ -import SwiftUI -import ActivityKit -import WidgetKit -import LiveDataIntegration - -// MARK: - Fitness Tracker Live Activity Example -// This example demonstrates how to create a comprehensive fitness tracking -// Live Activity with real-time updates and Dynamic Island integration - -struct FitnessTrackerAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var currentDistance: Double - var currentPace: String - var currentHeartRate: Int - var elapsedTime: TimeInterval - var targetDistance: Double - var isActive: Bool - } - - var activityName: String - var activityType: String - var startTime: Date -} - -// MARK: - Fitness Tracker Live Activity -struct FitnessTrackerLiveActivity: Widget { - var body: some WidgetConfiguration { - ActivityConfiguration(for: FitnessTrackerAttributes.self) { context in - // Lock screen/banner UI - LockScreenLiveActivityView(context: context) - } dynamicIsland: { context in - // Dynamic Island UI - DynamicIsland { - // Expanded UI - DynamicIslandExpandedRegion(.leading) { - FitnessExpandedLeadingView(context: context) - } - - DynamicIslandExpandedRegion(.trailing) { - FitnessExpandedTrailingView(context: context) - } - - DynamicIslandExpandedRegion(.center) { - FitnessExpandedCenterView(context: context) - } - - DynamicIslandExpandedRegion(.bottom) { - FitnessExpandedBottomView(context: context) - } - } compactLeading: { - // Compact leading - FitnessCompactLeadingView(context: context) - } compactTrailing: { - // Compact trailing - FitnessCompactTrailingView(context: context) - } minimal: { - // Minimal - FitnessMinimalView(context: context) - } - } - } -} - -// MARK: - Lock Screen Live Activity View -struct LockScreenLiveActivityView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 12) { - // Header - HStack { - VStack(alignment: .leading) { - Text(context.attributes.activityName) - .font(.headline) - .foregroundColor(.primary) - - Text(context.attributes.activityType) - .font(.subheadline) - .foregroundColor(.secondary) - } - - Spacer() - - // Activity status - Circle() - .fill(context.state.isActive ? Color.green : Color.red) - .frame(width: 12, height: 12) - .overlay( - Circle() - .stroke(Color.white, lineWidth: 2) - ) - } - - // Progress section - VStack(spacing: 8) { - // Distance progress - HStack { - Text("Distance") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("\(String(format: "%.2f", context.state.currentDistance)) km") - .font(.caption) - .foregroundColor(.primary) - } - - ProgressView(value: context.state.currentDistance, total: context.state.targetDistance) - .progressViewStyle(LinearProgressViewStyle(tint: .blue)) - - // Time and pace - HStack { - VStack(alignment: .leading) { - Text("Time") - .font(.caption) - .foregroundColor(.secondary) - Text(formatTime(context.state.elapsedTime)) - .font(.caption) - .foregroundColor(.primary) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("Pace") - .font(.caption) - .foregroundColor(.secondary) - Text(context.state.currentPace) - .font(.caption) - .foregroundColor(.primary) - } - } - } - - // Heart rate - HStack { - Image(systemName: "heart.fill") - .foregroundColor(.red) - .font(.caption) - - Text("\(context.state.currentHeartRate) BPM") - .font(.caption) - .foregroundColor(.primary) - - Spacer() - } - } - .padding() - .background(Color(.systemBackground)) - } - - private func formatTime(_ timeInterval: TimeInterval) -> String { - let hours = Int(timeInterval) / 3600 - let minutes = Int(timeInterval) / 60 % 60 - let seconds = Int(timeInterval) % 60 - - if hours > 0 { - return String(format: "%02d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%02d:%02d", minutes, seconds) - } - } -} - -// MARK: - Dynamic Island Views - -// Expanded Leading View -struct FitnessExpandedLeadingView: View { - let context: ActivityViewContext - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text("Distance") - .font(.caption) - .foregroundColor(.secondary) - - Text("\(String(format: "%.2f", context.state.currentDistance)) km") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text("of \(String(format: "%.2f", context.state.targetDistance)) km") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// Expanded Trailing View -struct FitnessExpandedTrailingView: View { - let context: ActivityViewContext - - var body: some View { - VStack(alignment: .trailing, spacing: 8) { - Text("Pace") - .font(.caption) - .foregroundColor(.secondary) - - Text(context.state.currentPace) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text("per km") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// Expanded Center View -struct FitnessExpandedCenterView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 8) { - Text(context.attributes.activityName) - .font(.headline) - .foregroundColor(.primary) - - Text(formatTime(context.state.elapsedTime)) - .font(.title) - .fontWeight(.bold) - .foregroundColor(.blue) - } - } - - private func formatTime(_ timeInterval: TimeInterval) -> String { - let hours = Int(timeInterval) / 3600 - let minutes = Int(timeInterval) / 60 % 60 - let seconds = Int(timeInterval) % 60 - - if hours > 0 { - return String(format: "%02d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%02d:%02d", minutes, seconds) - } - } -} - -// Expanded Bottom View -struct FitnessExpandedBottomView: View { - let context: ActivityViewContext - - var body: some View { - HStack(spacing: 16) { - // Heart rate - VStack(spacing: 4) { - Image(systemName: "heart.fill") - .foregroundColor(.red) - .font(.caption) - - Text("\(context.state.currentHeartRate)") - .font(.caption) - .fontWeight(.semibold) - - Text("BPM") - .font(.caption2) - .foregroundColor(.secondary) - } - - // Progress bar - VStack(spacing: 4) { - ProgressView(value: context.state.currentDistance, total: context.state.targetDistance) - .progressViewStyle(LinearProgressViewStyle(tint: .blue)) - - Text("\(Int((context.state.currentDistance / context.state.targetDistance) * 100))%") - .font(.caption2) - .foregroundColor(.secondary) - } - - // Status indicator - VStack(spacing: 4) { - Circle() - .fill(context.state.isActive ? Color.green : Color.red) - .frame(width: 8, height: 8) - - Text(context.state.isActive ? "Active" : "Paused") - .font(.caption2) - .foregroundColor(.secondary) - } - } - } -} - -// Compact Leading View -struct FitnessCompactLeadingView: View { - let context: ActivityViewContext - - var body: some View { - Image(systemName: "figure.run") - .foregroundColor(.blue) - .font(.title2) - } -} - -// Compact Trailing View -struct FitnessCompactTrailingView: View { - let context: ActivityViewContext - - var body: some View { - Text(formatTime(context.state.elapsedTime)) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - } - - private func formatTime(_ timeInterval: TimeInterval) -> String { - let minutes = Int(timeInterval) / 60 - let seconds = Int(timeInterval) % 60 - return String(format: "%02d:%02d", minutes, seconds) - } -} - -// Minimal View -struct FitnessMinimalView: View { - let context: ActivityViewContext - - var body: some View { - Image(systemName: "figure.run") - .foregroundColor(.blue) - .font(.title3) - } -} - -// MARK: - Fitness Activity Manager -class FitnessActivityManager: ObservableObject { - static let shared = FitnessActivityManager() - - @Published var currentActivity: Activity? - @Published var isActivityActive = false - - private var activityTimer: Timer? - private var startTime: Date? - - private init() {} - - func startActivity(name: String, type: String, targetDistance: Double) { - let attributes = FitnessTrackerAttributes( - activityName: name, - activityType: type, - startTime: Date() - ) - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: 0.0, - currentPace: "0:00", - currentHeartRate: 0, - elapsedTime: 0, - targetDistance: targetDistance, - isActive: true - ) - - do { - let activity = try Activity.request( - attributes: attributes, - contentState: contentState, - pushType: nil - ) - - currentActivity = activity - isActivityActive = true - startTime = Date() - - startTimer() - - print("✅ Fitness activity started: \(activity.id)") - } catch { - print("❌ Failed to start fitness activity: \(error)") - } - } - - func updateActivity(distance: Double, pace: String, heartRate: Int) { - guard let activity = currentActivity, isActivityActive else { return } - - let elapsedTime = startTime?.timeIntervalSinceNow ?? 0 - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: distance, - currentPace: pace, - currentHeartRate: heartRate, - elapsedTime: abs(elapsedTime), - targetDistance: activity.attributes.targetDistance, - isActive: true - ) - - Task { - await activity.update(using: contentState) - } - } - - func pauseActivity() { - guard let activity = currentActivity else { return } - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: activity.content.state.elapsedTime, - targetDistance: activity.attributes.targetDistance, - isActive: false - ) - - Task { - await activity.update(using: contentState) - } - - isActivityActive = false - stopTimer() - } - - func resumeActivity() { - guard let activity = currentActivity else { return } - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: activity.content.state.elapsedTime, - targetDistance: activity.attributes.targetDistance, - isActive: true - ) - - Task { - await activity.update(using: contentState) - } - - isActivityActive = true - startTimer() - } - - func endActivity() { - guard let activity = currentActivity else { return } - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: activity.content.state.elapsedTime, - targetDistance: activity.attributes.targetDistance, - isActive: false - ) - - Task { - await activity.end(using: contentState, dismissalPolicy: .immediate) - } - - currentActivity = nil - isActivityActive = false - stopTimer() - - print("✅ Fitness activity ended") - } - - private func startTimer() { - activityTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in - self.updateElapsedTime() - } - } - - private func stopTimer() { - activityTimer?.invalidate() - activityTimer = nil - } - - private func updateElapsedTime() { - guard let activity = currentActivity, isActivityActive else { return } - - let elapsedTime = startTime?.timeIntervalSinceNow ?? 0 - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: abs(elapsedTime), - targetDistance: activity.attributes.targetDistance, - isActive: true - ) - - Task { - await activity.update(using: contentState) - } - } -} - -// MARK: - Widget Bundle -@main -struct FitnessTrackerWidgetBundle: WidgetBundle { - var body: some Widget { - FitnessTrackerLiveActivity() - } -} - -// MARK: - Preview -struct FitnessTrackerLiveActivity_Previews: PreviewProvider { - static let attributes = FitnessTrackerAttributes( - activityName: "Morning Run", - activityType: "Running", - startTime: Date() - ) - - static let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: 2.5, - currentPace: "5:30", - currentHeartRate: 145, - elapsedTime: 1800, - targetDistance: 5.0, - isActive: true - ) - - static var previews: some View { - attributes - .previewContext(contentState, viewKind: .content) - .previewDisplayName("Lock Screen") - - attributes - .previewContext(contentState, viewKind: .dynamicIsland(.compact)) - .previewDisplayName("Dynamic Island Compact") - - attributes - .previewContext(contentState, viewKind: .dynamicIsland(.expanded)) - .previewDisplayName("Dynamic Island Expanded") - - attributes - .previewContext(contentState, viewKind: .dynamicIsland(.minimal)) - .previewDisplayName("Dynamic Island Minimal") - } -} \ No newline at end of file diff --git a/Examples/LiveActivityExamples/LiveActivityExample.swift b/Examples/LiveActivityExamples/LiveActivityExample.swift deleted file mode 100644 index 00af688..0000000 --- a/Examples/LiveActivityExamples/LiveActivityExample.swift +++ /dev/null @@ -1,159 +0,0 @@ -import SwiftUI -import ActivityKit -import WidgetKit - -// MARK: - Live Activity Example -struct LiveActivityExample: Widget { - let kind: String = "com.company.app.liveactivity" - - var body: some WidgetConfiguration { - ActivityConfiguration(for: LiveActivityAttributes.self) { context in - LiveActivityView(context: context) - } dynamicIsland: { context in - DynamicIsland { - DynamicIslandExpandedRegion(.leading) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - Text("Order #\(context.state.orderNumber)") - .font(.caption) - } - } - - DynamicIslandExpandedRegion(.trailing) { - Text(context.state.status) - .font(.caption) - .foregroundColor(.secondary) - } - - DynamicIslandExpandedRegion(.bottom) { - ProgressView(value: context.state.progress) - .progressViewStyle(.linear) - } - } compactLeading: { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - } compactTrailing: { - Text(context.state.status) - .font(.caption2) - } minimal: { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - } - } - } -} - -// MARK: - Live Activity Attributes -struct LiveActivityAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var orderNumber: String - var status: String - var progress: Double - var estimatedTime: String - } - - var orderNumber: String - var customerName: String -} - -// MARK: - Live Activity View -struct LiveActivityView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 16) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text("Order #\(context.state.orderNumber)") - .font(.headline) - - Text(context.state.status) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text(context.state.estimatedTime) - .font(.caption2) - .foregroundColor(.secondary) - } - - ProgressView(value: context.state.progress) - .progressViewStyle(.linear) - .tint(.blue) - - HStack { - Button("Track Order") { - // Open app to track order - } - .buttonStyle(.bordered) - - Spacer() - - Button("Contact Support") { - // Contact support - } - .buttonStyle(.bordered) - } - } - .padding() - .background(Color(.systemBackground)) - } -} - -// MARK: - Live Activity Manager -class LiveActivityManager { - static let shared = LiveActivityManager() - - private var activity: Activity? - - func startLiveActivity(orderNumber: String, customerName: String) { - let attributes = LiveActivityAttributes( - orderNumber: orderNumber, - customerName: customerName - ) - - let contentState = LiveActivityAttributes.ContentState( - orderNumber: orderNumber, - status: "Order confirmed", - progress: 0.1, - estimatedTime: "2:30 PM" - ) - - do { - activity = try Activity.request( - attributes: attributes, - contentState: contentState, - pushType: nil - ) - print("✅ Live activity started") - } catch { - print("❌ Live activity failed: \(error)") - } - } - - func updateLiveActivity(status: String, progress: Double, estimatedTime: String) { - Task { - let contentState = LiveActivityAttributes.ContentState( - orderNumber: activity?.attributes.orderNumber ?? "", - status: status, - progress: progress, - estimatedTime: estimatedTime - ) - - await activity?.update(using: contentState) - } - } - - func endLiveActivity() { - Task { - await activity?.end(dismissalPolicy: .immediate) - } - } -} \ No newline at end of file diff --git a/Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift b/Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift deleted file mode 100644 index 9640f8c..0000000 --- a/Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift +++ /dev/null @@ -1,418 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates - -// MARK: - Calendar Widget Example -// This example demonstrates how to create a beautiful calendar widget -// with event management and date navigation - -struct CalendarWidgetExample: Widget { - static let kind: String = "com.company.app.calendarwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: CalendarTimelineProvider()) { entry in - CalendarWidgetView(entry: entry) - } - .configurationDisplayName("Calendar Widget") - .description("Displays calendar events and date information") - .supportedFamilies([.systemSmall, .systemMedium]) - } -} - -// MARK: - Calendar Timeline Provider -struct CalendarTimelineProvider: TimelineProvider { - typealias Entry = CalendarWidgetEntry - - func placeholder(in context: Context) -> CalendarWidgetEntry { - CalendarWidgetEntry( - date: Date(), - currentDate: Date(), - events: [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch", time: "12:00 PM", isAllDay: false), - CalendarEvent(title: "Project Deadline", time: "5:00 PM", isAllDay: false) - ], - eventCount: 3, - nextEvent: CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false) - ) - } - - func getSnapshot(in context: Context, completion: @escaping (CalendarWidgetEntry) -> Void) { - let entry = CalendarWidgetEntry( - date: Date(), - currentDate: Date(), - events: [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch", time: "12:00 PM", isAllDay: false), - CalendarEvent(title: "Project Deadline", time: "5:00 PM", isAllDay: false) - ], - eventCount: 3, - nextEvent: CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false) - ) - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - let currentDate = Date() - let calendar = Calendar.current - - // Generate entries for the next 7 days - let entries = (0..<7).map { dayOffset in - let entryDate = calendar.date(byAdding: .day, value: dayOffset, to: currentDate)! - let events = generateEvents(for: entryDate) - - return CalendarWidgetEntry( - date: entryDate, - currentDate: entryDate, - events: events, - eventCount: events.count, - nextEvent: events.first - ) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - private func generateEvents(for date: Date) -> [CalendarEvent] { - let calendar = Calendar.current - let weekday = calendar.component(.weekday, from: date) - - var events: [CalendarEvent] = [] - - // Add events based on the day of the week - switch weekday { - case 1: // Sunday - events = [ - CalendarEvent(title: "Family Brunch", time: "11:00 AM", isAllDay: false), - CalendarEvent(title: "Weekly Planning", time: "2:00 PM", isAllDay: false) - ] - case 2: // Monday - events = [ - CalendarEvent(title: "Team Meeting", time: "9:00 AM", isAllDay: false), - CalendarEvent(title: "Client Call", time: "2:00 PM", isAllDay: false), - CalendarEvent(title: "Gym Session", time: "6:00 PM", isAllDay: false) - ] - case 3: // Tuesday - events = [ - CalendarEvent(title: "Project Review", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch with Colleague", time: "12:30 PM", isAllDay: false) - ] - case 4: // Wednesday - events = [ - CalendarEvent(title: "Board Meeting", time: "11:00 AM", isAllDay: false), - CalendarEvent(title: "Team Building", time: "4:00 PM", isAllDay: false) - ] - case 5: // Thursday - events = [ - CalendarEvent(title: "Product Demo", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Weekly Report", time: "3:00 PM", isAllDay: false) - ] - case 6: // Friday - events = [ - CalendarEvent(title: "Weekend Planning", time: "2:00 PM", isAllDay: false), - CalendarEvent(title: "Happy Hour", time: "5:00 PM", isAllDay: false) - ] - case 7: // Saturday - events = [ - CalendarEvent(title: "Shopping", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Movie Night", time: "7:00 PM", isAllDay: false) - ] - default: - events = [] - } - - return events - } -} - -// MARK: - Calendar Widget Entry -struct CalendarWidgetEntry: TimelineEntry { - let date: Date - let currentDate: Date - let events: [CalendarEvent] - let eventCount: Int - let nextEvent: CalendarEvent? -} - -// MARK: - Calendar Event -struct CalendarEvent { - let title: String - let time: String - let isAllDay: Bool -} - -// MARK: - Calendar Widget View -struct CalendarWidgetView: View { - let entry: CalendarWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - switch family { - case .systemSmall: - SmallCalendarWidgetView(entry: entry) - case .systemMedium: - MediumCalendarWidgetView(entry: entry) - default: - SmallCalendarWidgetView(entry: entry) - } - } -} - -// MARK: - Small Calendar Widget -struct SmallCalendarWidgetView: View { - let entry: CalendarWidgetEntry - - var body: some View { - VStack(spacing: 8) { - // Date header - HStack { - Text(formatDate(entry.currentDate)) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.primary) - - Spacer() - - Text("\(entry.eventCount)") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue) - .clipShape(Circle()) - } - - // Next event - if let nextEvent = entry.nextEvent { - VStack(alignment: .leading, spacing: 4) { - Text("Next") - .font(.system(size: 10)) - .foregroundColor(.secondary) - - Text(nextEvent.title) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.primary) - .lineLimit(1) - - Text(nextEvent.time) - .font(.system(size: 10)) - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - } else { - Text("No events today") - .font(.system(size: 12)) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .center) - } - - Spacer() - - // Calendar icon - Image(systemName: "calendar") - .font(.system(size: 20)) - .foregroundColor(.blue) - } - .padding() - .background(Color(.systemBackground)) - .widgetURL(URL(string: "calendarwidget://open")) - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } -} - -// MARK: - Medium Calendar Widget -struct MediumCalendarWidgetView: View { - let entry: CalendarWidgetEntry - - var body: some View { - HStack(spacing: 16) { - // Left side - Date and event count - VStack(alignment: .leading, spacing: 8) { - // Date - Text(formatDate(entry.currentDate)) - .font(.system(size: 20, weight: .bold)) - .foregroundColor(.primary) - - // Day of week - Text(formatDayOfWeek(entry.currentDate)) - .font(.system(size: 14)) - .foregroundColor(.secondary) - - // Event count - HStack { - Image(systemName: "calendar") - .font(.system(size: 12)) - .foregroundColor(.blue) - - Text("\(entry.eventCount) events") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - } - - Spacer() - - // Right side - Events list - VStack(alignment: .leading, spacing: 6) { - Text("Today") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(.secondary) - - if entry.events.isEmpty { - Text("No events") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } else { - ForEach(Array(entry.events.prefix(3).enumerated()), id: \.offset) { index, event in - HStack(spacing: 8) { - Circle() - .fill(Color.blue) - .frame(width: 6, height: 6) - - VStack(alignment: .leading, spacing: 2) { - Text(event.title) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.primary) - .lineLimit(1) - - Text(event.time) - .font(.system(size: 9)) - .foregroundColor(.secondary) - } - } - } - - if entry.events.count > 3 { - Text("+\(entry.events.count - 3) more") - .font(.system(size: 10)) - .foregroundColor(.blue) - } - } - } - } - .padding() - .background(Color(.systemBackground)) - .widgetURL(URL(string: "calendarwidget://open")) - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } - - private func formatDayOfWeek(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "EEEE" - return formatter.string(from: date) - } -} - -// MARK: - Calendar Widget Manager -class CalendarWidgetManager: ObservableObject { - static let shared = CalendarWidgetManager() - - @Published var currentEvents: [CalendarEvent] = [] - @Published var selectedDate: Date = Date() - - private let calendar = Calendar.current - private let eventStore = EventStore() - - private init() { - loadEvents() - } - - func loadEvents() { - eventStore.fetchEvents(for: selectedDate) { events in - DispatchQueue.main.async { - self.currentEvents = events - } - } - } - - func selectDate(_ date: Date) { - selectedDate = date - loadEvents() - } - - func addEvent(_ event: CalendarEvent) { - eventStore.addEvent(event, for: selectedDate) { success in - if success { - self.loadEvents() - } - } - } - - func removeEvent(_ event: CalendarEvent) { - eventStore.removeEvent(event, for: selectedDate) { success in - if success { - self.loadEvents() - } - } - } -} - -// MARK: - Event Store -class EventStore { - private var events: [Date: [CalendarEvent]] = [:] - - func fetchEvents(for date: Date, completion: @escaping ([CalendarEvent]) -> Void) { - let key = Calendar.current.startOfDay(for: date) - completion(events[key] ?? []) - } - - func addEvent(_ event: CalendarEvent, for date: Date, completion: @escaping (Bool) -> Void) { - let key = Calendar.current.startOfDay(for: date) - if events[key] == nil { - events[key] = [] - } - events[key]?.append(event) - completion(true) - } - - func removeEvent(_ event: CalendarEvent, for date: Date, completion: @escaping (Bool) -> Void) { - let key = Calendar.current.startOfDay(for: date) - events[key]?.removeAll { $0.title == event.title && $0.time == event.time } - completion(true) - } -} - -// MARK: - Widget Bundle -@main -struct CalendarWidgetBundle: WidgetBundle { - var body: some Widget { - CalendarWidgetExample() - } -} - -// MARK: - Preview -struct CalendarWidgetView_Previews: PreviewProvider { - static var previews: some View { - let entry = CalendarWidgetEntry( - date: Date(), - currentDate: Date(), - events: [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch", time: "12:00 PM", isAllDay: false), - CalendarEvent(title: "Project Deadline", time: "5:00 PM", isAllDay: false) - ], - eventCount: 3, - nextEvent: CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false) - ) - - Group { - CalendarWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .previewDisplayName("Small Calendar Widget") - - CalendarWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .previewDisplayName("Medium Calendar Widget") - } - } -} \ No newline at end of file diff --git a/Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift b/Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift deleted file mode 100644 index 410dfd8..0000000 --- a/Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift +++ /dev/null @@ -1,81 +0,0 @@ -import SwiftUI -import WidgetKit - -// MARK: - Lock Screen Widget Example -struct LockScreenWidgetExample: Widget { - let kind: String = "com.company.app.lockscreenwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: LockScreenWidgetProvider()) { entry in - LockScreenWidgetEntryView(entry: entry) - } - .configurationDisplayName("Lock Screen Widget") - .description("A beautiful lock screen widget example") - .supportedFamilies([.systemSmall, .systemMedium]) - } -} - -// MARK: - Widget Provider -struct LockScreenWidgetProvider: TimelineProvider { - func placeholder(in context: Context) -> LockScreenWidgetEntry { - LockScreenWidgetEntry(date: Date(), icon: "message.fill", title: "Messages", count: "3 new") - } - - func getSnapshot(in context: Context, completion: @escaping (LockScreenWidgetEntry) -> ()) { - let entry = LockScreenWidgetEntry(date: Date(), icon: "message.fill", title: "Messages", count: "3 new") - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - let currentDate = Date() - let entry = LockScreenWidgetEntry(date: currentDate, icon: "message.fill", title: "Messages", count: "3 new") - let timeline = Timeline(entries: [entry], policy: .atEnd) - completion(timeline) - } -} - -// MARK: - Widget Entry -struct LockScreenWidgetEntry: TimelineEntry { - let date: Date - let icon: String - let title: String - let count: String -} - -// MARK: - Widget Entry View -struct LockScreenWidgetEntryView: View { - let entry: LockScreenWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - HStack(spacing: 12) { - Image(systemName: entry.icon) - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading, spacing: 2) { - Text(entry.title) - .font(.caption) - .foregroundColor(.secondary) - - Text(entry.count) - .font(.caption2) - .foregroundColor(.primary) - .fontWeight(.medium) - } - - Spacer() - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color(.systemBackground)) - } -} - -// MARK: - Preview -struct LockScreenWidgetExample_Previews: PreviewProvider { - static var previews: some View { - LockScreenWidgetEntryView(entry: LockScreenWidgetEntry(date: Date(), icon: "message.fill", title: "Messages", count: "3 new")) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - } -} \ No newline at end of file diff --git a/Examples/README.md b/Examples/README.md deleted file mode 100644 index b82d805..0000000 --- a/Examples/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Examples Index - -- ``Examples/AdvancedExample.swift -- ``Examples/AdvancedExamples/AdvancedWidgetExample.swift -- ``Examples/BasicExample.swift -- ``Examples/BasicExamples/BasicWidgetExample.swift -- ``Examples/DynamicIslandExamples/DynamicIslandExample.swift -- ``Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift -- ``Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift -- ``Examples/LiveActivityExamples/FitnessTrackerExample.swift -- ``Examples/LiveActivityExamples/LiveActivityExample.swift -- ``Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift -- ``Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift diff --git a/Package.swift b/Package.swift index d82a0bd..367bd69 100644 --- a/Package.swift +++ b/Package.swift @@ -26,11 +26,6 @@ let package = Package( swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") ] - ), - .testTarget( - name: "WidgetDevelopmentKitTests", - dependencies: ["WidgetDevelopmentKit"], - path: "Tests/Unit" ) ] ) diff --git a/README_TR.md b/README_TR.md deleted file mode 100644 index 61d0a67..0000000 --- a/README_TR.md +++ /dev/null @@ -1,36 +0,0 @@ -# iOS-Widget-Development-Kit - -> 🇹🇷 Türkçe | [English](README.md) - -📲 WidgetKit development kit with Live Activities and dynamic widgets - -## 📦 Kurulum - -### Swift Package Manager - -```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "1.0.0") -] -``` - -## 🚀 Hızlı Başlangıç - -Detaylı kullanım ve API referansı için [İngilizce README](README.md) dosyasına bakın. - -## 📖 Dokümantasyon - -- [README (English)](README.md) -- [Contributing Guide](CONTRIBUTING.md) -- [Changelog](CHANGELOG.md) -- [Security Policy](SECURITY.md) - -## 📄 Lisans - -MIT License - detaylar için [LICENSE](LICENSE) dosyasına bakın. - -## 👨‍💻 Geliştirici - -**Muhittin Camdali** - -[![GitHub](https://img.shields.io/badge/GitHub-muhittincamdali-181717?style=flat&logo=github)](https://github.com/muhittincamdali) diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index d918ade..0000000 --- a/SUPPORT.md +++ /dev/null @@ -1,25 +0,0 @@ -# Support - -## Getting Help - -- 📖 **Documentation**: Check the [README](README.md) and [Documentation](Documentation/) folder -- 💬 **Discussions**: Ask questions in [GitHub Discussions](../../discussions) -- 🐛 **Bug Reports**: Open an [issue](../../issues/new?template=bug_report.yml) -- 💡 **Feature Requests**: Submit a [feature request](../../issues/new?template=feature_request.yml) - -## Response Time - -| Channel | Expected Response | -|---------|-------------------| -| Bug (critical) | < 24 hours | -| Bug (normal) | < 48 hours | -| Feature Request | < 72 hours | -| Question | < 48 hours | - -## Commercial Support - -For commercial support or consulting, contact: muhittincamdali@gmail.com - -## Code of Conduct - -Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. diff --git a/Sources/Analytics/WidgetAnalytics.swift b/Sources/Analytics/WidgetAnalytics.swift deleted file mode 100644 index b62dba4..0000000 --- a/Sources/Analytics/WidgetAnalytics.swift +++ /dev/null @@ -1,515 +0,0 @@ -import Foundation -import Combine - -/// Widget Analytics Service -/// Provides comprehensive analytics and performance tracking for widgets -@available(iOS 16.0, *) -public class WidgetAnalyticsService: ObservableObject { - - // MARK: - Properties - - /// Analytics data storage - private var analyticsData = WidgetAnalytics() - - /// Performance metrics - private let performanceSubject = PassthroughSubject() - public var performancePublisher: AnyPublisher { - return performanceSubject.eraseToAnyPublisher() - } - - /// Event tracking - private var eventLog: [AnalyticsEvent] = [] - - /// Performance monitoring - private var performanceMetrics = WidgetPerformanceMetrics() - - /// Combine cancellables - private var cancellables = Set() - - /// Analytics configuration - private var configuration = AnalyticsConfiguration() - - // MARK: - Initialization - - public init() { - setupAnalytics() - } - - // MARK: - Public Methods - - /// Get current analytics data - /// - Returns: Analytics data - public func getAnalytics() -> WidgetAnalytics { - return analyticsData - } - - /// Log widget update - /// - Parameter widgetId: Widget identifier - public func logWidgetUpdate(widgetId: String) { - let event = AnalyticsEvent( - type: .widgetUpdate, - widgetId: widgetId, - timestamp: Date(), - metadata: ["widgetId": widgetId] - ) - - logEvent(event) - updateWidgetMetrics(widgetId: widgetId) - } - - /// Log template registration - /// - Parameter templateId: Template identifier - public func logTemplateRegistration(_ templateId: String) { - let event = AnalyticsEvent( - type: .templateRegistration, - widgetId: nil, - timestamp: Date(), - metadata: ["templateId": templateId] - ) - - logEvent(event) - analyticsData.templateRegistrations += 1 - } - - /// Log data source registration - /// - Parameter identifier: Data source identifier - public func logDataSourceRegistration(_ identifier: String) { - let event = AnalyticsEvent( - type: .dataSourceRegistration, - widgetId: nil, - timestamp: Date(), - metadata: ["dataSourceId": identifier] - ) - - logEvent(event) - analyticsData.dataSourceRegistrations += 1 - } - - /// Log WebSocket connection - /// - Parameters: - /// - identifier: Connection identifier - /// - url: Connection URL - public func logWebSocketConnection(identifier: String, url: String) { - let event = AnalyticsEvent( - type: .webSocketConnection, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "url": url] - ) - - logEvent(event) - analyticsData.webSocketConnections += 1 - } - - /// Log WebSocket disconnection - /// - Parameter identifier: Connection identifier - public func logWebSocketDisconnection(identifier: String) { - let event = AnalyticsEvent( - type: .webSocketDisconnection, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.webSocketDisconnections += 1 - } - - /// Log data sent - /// - Parameters: - /// - widgetId: Widget identifier - /// - dataType: Data type - public func logDataSent(widgetId: String, dataType: String) { - let event = AnalyticsEvent( - type: .dataSent, - widgetId: widgetId, - timestamp: Date(), - metadata: ["widgetId": widgetId, "dataType": dataType] - ) - - logEvent(event) - analyticsData.dataSentCount += 1 - } - - /// Log data source update - /// - Parameters: - /// - identifier: Data source identifier - /// - dataType: Data type - public func logDataSourceUpdate(identifier: String, dataType: String) { - let event = AnalyticsEvent( - type: .dataSourceUpdate, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "dataType": dataType] - ) - - logEvent(event) - analyticsData.dataSourceUpdates += 1 - } - - /// Log data source configuration - /// - Parameter identifier: Data source identifier - public func logDataSourceConfiguration(identifier: String) { - let event = AnalyticsEvent( - type: .dataSourceConfiguration, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.dataSourceConfigurations += 1 - } - - /// Log connection established - /// - Parameter identifier: Connection identifier - public func logConnectionEstablished(identifier: String) { - let event = AnalyticsEvent( - type: .connectionEstablished, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.connectionsEstablished += 1 - } - - /// Log connection failed - /// - Parameters: - /// - identifier: Connection identifier - /// - error: Error details - public func logConnectionFailed(identifier: String, error: Error) { - let event = AnalyticsEvent( - type: .connectionFailed, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.connectionFailures += 1 - } - - /// Log connection cancelled - /// - Parameter identifier: Connection identifier - public func logConnectionCancelled(identifier: String) { - let event = AnalyticsEvent( - type: .connectionCancelled, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.connectionsCancelled += 1 - } - - /// Log connection retry - /// - Parameter identifier: Connection identifier - public func logConnectionRetry(identifier: String) { - let event = AnalyticsEvent( - type: .connectionRetry, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.connectionRetries += 1 - } - - /// Log reconnection attempt - /// - Parameter identifier: Connection identifier - public func logReconnectionAttempt(identifier: String) { - let event = AnalyticsEvent( - type: .reconnectionAttempt, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.reconnectionAttempts += 1 - } - - /// Log data receive error - /// - Parameters: - /// - identifier: Connection identifier - /// - error: Error details - public func logDataReceiveError(identifier: String, error: Error) { - let event = AnalyticsEvent( - type: .dataReceiveError, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.dataReceiveErrors += 1 - } - - /// Log data decode error - /// - Parameters: - /// - identifier: Connection identifier - /// - error: Error details - public func logDataDecodeError(identifier: String, error: Error) { - let event = AnalyticsEvent( - type: .dataDecodeError, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.dataDecodeErrors += 1 - } - - /// Log network unavailable - public func logNetworkUnavailable() { - let event = AnalyticsEvent( - type: .networkUnavailable, - widgetId: nil, - timestamp: Date(), - metadata: [:] - ) - - logEvent(event) - analyticsData.networkUnavailableEvents += 1 - } - - /// Log memory cleanup - /// - Parameter removedEntries: Number of removed cache entries - public func logMemoryCleanup(removedEntries: Int) { - let event = AnalyticsEvent( - type: .memoryCleanup, - widgetId: nil, - timestamp: Date(), - metadata: ["removedEntries": removedEntries] - ) - - logEvent(event) - analyticsData.memoryCleanups += 1 - } - - /// Log battery optimization - /// - Parameter newInterval: New refresh interval - public func logBatteryOptimization(newInterval: TimeInterval) { - let event = AnalyticsEvent( - type: .batteryOptimization, - widgetId: nil, - timestamp: Date(), - metadata: ["newInterval": newInterval] - ) - - logEvent(event) - analyticsData.batteryOptimizations += 1 - } - - /// Log error - /// - Parameter error: Widget error - public func logError(_ error: WidgetError) { - let event = AnalyticsEvent( - type: .error, - widgetId: nil, - timestamp: Date(), - metadata: ["error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.errors += 1 - } - - /// Log engine initialization - public func logEngineInitialization() { - let event = AnalyticsEvent( - type: .engineInitialization, - widgetId: nil, - timestamp: Date(), - metadata: [:] - ) - - logEvent(event) - analyticsData.engineInitializations += 1 - } - - /// Configure analytics - /// - Parameter config: Analytics configuration - public func configure(_ config: AnalyticsConfiguration) { - configuration = config - } - - /// Export analytics data - /// - Returns: Exported analytics data - public func exportAnalytics() -> Data? { - do { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - return try encoder.encode(analyticsData) - } catch { - return nil - } - } - - /// Clear analytics data - public func clearAnalytics() { - analyticsData = WidgetAnalytics() - eventLog.removeAll() - } - - // MARK: - Private Methods - - private func setupAnalytics() { - // Setup performance monitoring - Timer.publish(every: 60.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.updatePerformanceMetrics() - } - .store(in: &cancellables) - } - - private func logEvent(_ event: AnalyticsEvent) { - eventLog.append(event) - - // Limit event log size - if eventLog.count > configuration.maxEventLogSize { - eventLog.removeFirst(eventLog.count - configuration.maxEventLogSize) - } - - // Send to external analytics if configured - if configuration.enableExternalAnalytics { - sendToExternalAnalytics(event) - } - } - - private func updateWidgetMetrics(widgetId: String) { - analyticsData.widgetUpdates[widgetId, default: 0] += 1 - analyticsData.totalWidgetUpdates += 1 - } - - private func updatePerformanceMetrics() { - performanceSubject.send(performanceMetrics) - } - - private func sendToExternalAnalytics(_ event: AnalyticsEvent) { - // Implementation for external analytics service - // This could send data to Firebase, Mixpanel, etc. - } -} - -// MARK: - Supporting Types - -/// Analytics data structure -public struct WidgetAnalytics: Codable { - public var totalWidgetUpdates: Int = 0 - public var widgetUpdates: [String: Int] = [:] - public var templateRegistrations: Int = 0 - public var dataSourceRegistrations: Int = 0 - public var webSocketConnections: Int = 0 - public var webSocketDisconnections: Int = 0 - public var dataSentCount: Int = 0 - public var dataSourceUpdates: Int = 0 - public var dataSourceConfigurations: Int = 0 - public var connectionsEstablished: Int = 0 - public var connectionFailures: Int = 0 - public var connectionsCancelled: Int = 0 - public var connectionRetries: Int = 0 - public var reconnectionAttempts: Int = 0 - public var dataReceiveErrors: Int = 0 - public var dataDecodeErrors: Int = 0 - public var networkUnavailableEvents: Int = 0 - public var memoryCleanups: Int = 0 - public var batteryOptimizations: Int = 0 - public var errors: Int = 0 - public var engineInitializations: Int = 0 - public var sessionStartTime: Date = Date() - public var lastUpdateTime: Date = Date() - - public init() {} -} - -/// Analytics event -public struct AnalyticsEvent { - public let type: AnalyticsEventType - public let widgetId: String? - public let timestamp: Date - public let metadata: [String: Any] - - public init(type: AnalyticsEventType, widgetId: String?, timestamp: Date, metadata: [String: Any]) { - self.type = type - self.widgetId = widgetId - self.timestamp = timestamp - self.metadata = metadata - } -} - -/// Analytics event types -public enum AnalyticsEventType: String, CaseIterable { - case widgetUpdate = "widget_update" - case templateRegistration = "template_registration" - case dataSourceRegistration = "data_source_registration" - case webSocketConnection = "websocket_connection" - case webSocketDisconnection = "websocket_disconnection" - case dataSent = "data_sent" - case dataSourceUpdate = "data_source_update" - case dataSourceConfiguration = "data_source_configuration" - case connectionEstablished = "connection_established" - case connectionFailed = "connection_failed" - case connectionCancelled = "connection_cancelled" - case connectionRetry = "connection_retry" - case reconnectionAttempt = "reconnection_attempt" - case dataReceiveError = "data_receive_error" - case dataDecodeError = "data_decode_error" - case networkUnavailable = "network_unavailable" - case memoryCleanup = "memory_cleanup" - case batteryOptimization = "battery_optimization" - case error = "error" - case engineInitialization = "engine_initialization" -} - -/// Analytics configuration -public struct AnalyticsConfiguration { - public let enableExternalAnalytics: Bool - public let maxEventLogSize: Int - public let enablePerformanceTracking: Bool - public let enableErrorTracking: Bool - - public init(enableExternalAnalytics: Bool = false, maxEventLogSize: Int = 1000, enablePerformanceTracking: Bool = true, enableErrorTracking: Bool = true) { - self.enableExternalAnalytics = enableExternalAnalytics - self.maxEventLogSize = maxEventLogSize - self.enablePerformanceTracking = enablePerformanceTracking - self.enableErrorTracking = enableErrorTracking - } -} - -/// Widget error types -public enum WidgetError: Error, LocalizedError { - case templateNotFound(String) - case dataSourceNotFound(String) - case connectionFailed(String) - case dataDecodeFailed(String) - case networkUnavailable - case unknown - - public var errorDescription: String? { - switch self { - case .templateNotFound(let templateId): - return "Template not found: \(templateId)" - case .dataSourceNotFound(let dataSourceId): - return "Data source not found: \(dataSourceId)" - case .connectionFailed(let reason): - return "Connection failed: \(reason)" - case .dataDecodeFailed(let reason): - return "Data decode failed: \(reason)" - case .networkUnavailable: - return "Network unavailable" - case .unknown: - return "Unknown error occurred" - } - } - - public var localizedDescription: String { - return errorDescription ?? "Unknown error" - } -} \ No newline at end of file diff --git a/Sources/Analytics/WidgetErrorHandler.swift b/Sources/Analytics/WidgetErrorHandler.swift deleted file mode 100644 index 6c53c54..0000000 --- a/Sources/Analytics/WidgetErrorHandler.swift +++ /dev/null @@ -1,208 +0,0 @@ -import Foundation -import Combine - -/// Widget Error Handler -/// Provides comprehensive error handling and reporting for widgets -@available(iOS 16.0, *) -public class WidgetErrorHandler: ObservableObject { - - // MARK: - Properties - - /// Error publisher - private let errorSubject = PassthroughSubject() - public var errorPublisher: AnyPublisher { - return errorSubject.eraseToAnyPublisher() - } - - /// Error log - private var errorLog: [ErrorLogEntry] = [] - - /// Error configuration - private var configuration = ErrorHandlerConfiguration() - - /// Combine cancellables - private var cancellables = Set() - - // MARK: - Initialization - - public init() { - setupErrorHandling() - } - - // MARK: - Public Methods - - /// Handle widget error - /// - Parameter error: Widget error - public func handleError(_ error: WidgetError) { - let logEntry = ErrorLogEntry( - error: error, - timestamp: Date(), - context: getCurrentContext() - ) - - errorLog.append(logEntry) - - // Limit error log size - if errorLog.count > configuration.maxErrorLogSize { - errorLog.removeFirst(errorLog.count - configuration.maxErrorLogSize) - } - - // Send error to publisher - errorSubject.send(error) - - // Log error if enabled - if configuration.enableErrorLogging { - logError(logEntry) - } - - // Send to external error reporting if enabled - if configuration.enableExternalErrorReporting { - sendToExternalErrorReporting(logEntry) - } - } - - /// Get error log - /// - Returns: Array of error log entries - public func getErrorLog() -> [ErrorLogEntry] { - return errorLog - } - - /// Clear error log - public func clearErrorLog() { - errorLog.removeAll() - } - - /// Configure error handler - /// - Parameter config: Error handler configuration - public func configure(_ config: ErrorHandlerConfiguration) { - configuration = config - } - - /// Get error statistics - /// - Returns: Error statistics - public func getErrorStatistics() -> ErrorStatistics { - var statistics = ErrorStatistics() - - for entry in errorLog { - statistics.totalErrors += 1 - - switch entry.error { - case .templateNotFound: - statistics.templateNotFoundErrors += 1 - case .dataSourceNotFound: - statistics.dataSourceNotFoundErrors += 1 - case .connectionFailed: - statistics.connectionFailedErrors += 1 - case .dataDecodeFailed: - statistics.dataDecodeFailedErrors += 1 - case .networkUnavailable: - statistics.networkUnavailableErrors += 1 - case .unknown: - statistics.unknownErrors += 1 - } - } - - return statistics - } - - // MARK: - Private Methods - - private func setupErrorHandling() { - // Setup error monitoring - Timer.publish(every: 300.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.cleanupOldErrors() - } - .store(in: &cancellables) - } - - private func getCurrentContext() -> [String: Any] { - return [ - "timestamp": Date(), - "memoryUsage": getCurrentMemoryUsage(), - "batteryLevel": getCurrentBatteryLevel() - ] - } - - private func getCurrentMemoryUsage() -> UInt64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size)/4 - - let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, - task_flavor_t(MACH_TASK_BASIC_INFO), - $0, - &count) - } - } - - return kerr == KERN_SUCCESS ? info.resident_size : 0 - } - - private func getCurrentBatteryLevel() -> Float { - UIDevice.current.isBatteryMonitoringEnabled = true - return UIDevice.current.batteryLevel - } - - private func logError(_ entry: ErrorLogEntry) { - // Implementation for local error logging - print("Widget Error: \(entry.error.localizedDescription) at \(entry.timestamp)") - } - - private func sendToExternalErrorReporting(_ entry: ErrorLogEntry) { - // Implementation for external error reporting - // This could send data to Crashlytics, Sentry, etc. - } - - private func cleanupOldErrors() { - let cutoffDate = Date().addingTimeInterval(-configuration.errorRetentionPeriod) - errorLog.removeAll { entry in - entry.timestamp < cutoffDate - } - } -} - -// MARK: - Supporting Types - -/// Error log entry -public struct ErrorLogEntry { - public let error: WidgetError - public let timestamp: Date - public let context: [String: Any] - - public init(error: WidgetError, timestamp: Date, context: [String: Any]) { - self.error = error - self.timestamp = timestamp - self.context = context - } -} - -/// Error statistics -public struct ErrorStatistics { - public var totalErrors: Int = 0 - public var templateNotFoundErrors: Int = 0 - public var dataSourceNotFoundErrors: Int = 0 - public var connectionFailedErrors: Int = 0 - public var dataDecodeFailedErrors: Int = 0 - public var networkUnavailableErrors: Int = 0 - public var unknownErrors: Int = 0 - - public init() {} -} - -/// Error handler configuration -public struct ErrorHandlerConfiguration { - public let enableErrorLogging: Bool - public let enableExternalErrorReporting: Bool - public let maxErrorLogSize: Int - public let errorRetentionPeriod: TimeInterval - - public init(enableErrorLogging: Bool = true, enableExternalErrorReporting: Bool = false, maxErrorLogSize: Int = 100, errorRetentionPeriod: TimeInterval = 86400) { - self.enableErrorLogging = enableErrorLogging - self.enableExternalErrorReporting = enableExternalErrorReporting - self.maxErrorLogSize = maxErrorLogSize - self.errorRetentionPeriod = errorRetentionPeriod - } -} \ No newline at end of file diff --git a/Sources/Enterprise/AIManager.swift b/Sources/Enterprise/AIManager.swift deleted file mode 100644 index 203ddb2..0000000 --- a/Sources/Enterprise/AIManager.swift +++ /dev/null @@ -1,1013 +0,0 @@ -import Foundation -import CoreML -import Combine -import OSLog -import CreateML - -/// Enterprise-grade AI manager providing machine learning capabilities, -/// intelligent widget optimization, and predictive analytics for iOS widgets -@available(iOS 16.0, *) -public class AIManager: ObservableObject { - - // MARK: - Types - - public enum AIError: LocalizedError { - case modelNotLoaded - case predictionFailed(String) - case invalidInput - case modelTrainingFailed - case coreMLNotAvailable - case insufficientData - case learningDisabled - case configurationInvalid - - public var errorDescription: String? { - switch self { - case .modelNotLoaded: - return "AI model is not loaded" - case .predictionFailed(let reason): - return "Prediction failed: \(reason)" - case .invalidInput: - return "Invalid input data for AI processing" - case .modelTrainingFailed: - return "Model training failed" - case .coreMLNotAvailable: - return "Core ML is not available on this device" - case .insufficientData: - return "Insufficient data for AI operations" - case .learningDisabled: - return "Machine learning is disabled" - case .configurationInvalid: - return "AI configuration is invalid" - } - } - } - - public enum ModelType: String, CaseIterable, Codable { - case widgetUsagePrediction = "widget_usage_prediction" - case contentRecommendation = "content_recommendation" - case performanceOptimization = "performance_optimization" - case userBehaviorAnalysis = "user_behavior_analysis" - case anomalyDetection = "anomaly_detection" - case sentimentAnalysis = "sentiment_analysis" - case timeSeriesForecasting = "time_series_forecasting" - case clusteringAnalysis = "clustering_analysis" - - public var description: String { - switch self { - case .widgetUsagePrediction: - return "Predicts widget usage patterns and trends" - case .contentRecommendation: - return "Recommends personalized content for widgets" - case .performanceOptimization: - return "Optimizes widget performance based on usage data" - case .userBehaviorAnalysis: - return "Analyzes user behavior patterns and preferences" - case .anomalyDetection: - return "Detects unusual patterns and anomalies" - case .sentimentAnalysis: - return "Analyzes sentiment in user interactions" - case .timeSeriesForecasting: - return "Forecasts future trends and patterns" - case .clusteringAnalysis: - return "Groups similar users and behaviors" - } - } - } - - public struct AIConfiguration { - public var enableAI: Bool = true - public var enableLearning: Bool = true - public var enablePersonalization: Bool = true - public var enablePredictions: Bool = true - public var maxTrainingDataPoints: Int = 10000 - public var modelUpdateInterval: TimeInterval = 86400 // 24 hours - public var predictionConfidenceThreshold: Double = 0.7 - public var privacyLevel: PrivacyLevel = .balanced - public var enableFederatedLearning: Bool = false - public var enableEdgeComputing: Bool = true - - public enum PrivacyLevel: String, CaseIterable { - case strict = "strict" - case balanced = "balanced" - case minimal = "minimal" - - public var dataRetentionDays: Int { - switch self { - case .strict: return 7 - case .balanced: return 30 - case .minimal: return 90 - } - } - } - - public init() {} - } - - public struct PredictionResult: Codable { - public let modelType: ModelType - public let prediction: Double - public let confidence: Double - public let features: [String: Double] - public let metadata: [String: String] - public let timestamp: Date - public let explanation: String? - - public var isReliable: Bool { - return confidence >= 0.7 - } - - public init( - modelType: ModelType, - prediction: Double, - confidence: Double, - features: [String: Double], - metadata: [String: String] = [:], - explanation: String? = nil - ) { - self.modelType = modelType - self.prediction = prediction - self.confidence = confidence - self.features = features - self.metadata = metadata - self.timestamp = Date() - self.explanation = explanation - } - } - - public struct UserBehaviorProfile: Codable { - public let userId: String - public let preferredWidgetTypes: [String] - public let usagePatterns: [String: Double] - public let timePreferences: [String: Double] - public let interactionFrequency: Double - public let sessionDuration: Double - public let deviceUsageContext: [String: Double] - public let personalizedFeatures: [String: Double] - public let lastUpdated: Date - - public var engagementScore: Double { - let frequency = interactionFrequency - let duration = sessionDuration / 3600 // Convert to hours - let consistency = usagePatterns.values.reduce(0, +) / Double(usagePatterns.count) - - return (frequency * 0.4 + duration * 0.3 + consistency * 0.3).clamped(to: 0...1) - } - } - - public struct ContentRecommendation: Codable, Identifiable { - public let id: String - public let contentType: String - public let title: String - public let description: String - public let relevanceScore: Double - public let confidenceScore: Double - public let reasoning: [String] - public let metadata: [String: String] - public let expiresAt: Date - - public var isExpired: Bool { - return Date() > expiresAt - } - - public init( - contentType: String, - title: String, - description: String, - relevanceScore: Double, - confidenceScore: Double, - reasoning: [String] = [], - metadata: [String: String] = [:], - validityDuration: TimeInterval = 3600 - ) { - self.id = UUID().uuidString - self.contentType = contentType - self.title = title - self.description = description - self.relevanceScore = relevanceScore - self.confidenceScore = confidenceScore - self.reasoning = reasoning - self.metadata = metadata - self.expiresAt = Date().addingTimeInterval(validityDuration) - } - } - - public struct ModelMetrics: Codable { - public let modelType: ModelType - public let accuracy: Double - public let precision: Double - public let recall: Double - public let f1Score: Double - public let trainingDataSize: Int - public let lastTrainingDate: Date - public let predictionCount: Int - public let averageConfidence: Double - - public var qualityScore: Double { - return (accuracy * 0.3 + precision * 0.25 + recall * 0.25 + f1Score * 0.2).clamped(to: 0...1) - } - } - - // MARK: - Properties - - public static let shared = AIManager() - - private let modelRegistry: MLModelRegistry - private let featureExtractor: FeatureExtractor - private let predictionEngine: PredictionEngine - private let learningEngine: LearningEngine - private let recommendationEngine: RecommendationEngine - private let behaviorAnalyzer: BehaviorAnalyzer - private let modelTrainer: ModelTrainer - private let privacyManager: AIPrivacyManager - - @Published public private(set) var configuration: AIConfiguration = AIConfiguration() - @Published public private(set) var isAIEnabled: Bool = false - @Published public private(set) var loadedModels: Set = [] - @Published public private(set) var modelMetrics: [ModelType: ModelMetrics] = [:] - @Published public private(set) var userProfiles: [String: UserBehaviorProfile] = [:] - @Published public private(set) var recentPredictions: [PredictionResult] = [] - @Published public private(set) var recommendationCache: [ContentRecommendation] = [] - - private var trainingTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "ai.manager", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "AI") - - // MARK: - Initialization - - private init() { - self.modelRegistry = MLModelRegistry() - self.featureExtractor = FeatureExtractor() - self.predictionEngine = PredictionEngine() - self.learningEngine = LearningEngine() - self.recommendationEngine = RecommendationEngine() - self.behaviorAnalyzer = BehaviorAnalyzer() - self.modelTrainer = ModelTrainer() - self.privacyManager = AIPrivacyManager() - - setupAIManager() - } - - // MARK: - Public Methods - - /// Initializes and starts the AI system - public func startAI() async throws { - guard configuration.enableAI else { - throw AIError.learningDisabled - } - - guard await checkCoreMLAvailability() else { - throw AIError.coreMLNotAvailable - } - - await MainActor.run { - self.isAIEnabled = true - } - - // Load pre-trained models - await loadInitialModels() - - // Setup automatic training - if configuration.enableLearning { - setupAutomaticTraining() - } - - // Initialize privacy compliance - await privacyManager.initialize(configuration: configuration) - - logger.info("AI Manager started successfully") - } - - /// Stops the AI system - public func stopAI() async { - await MainActor.run { - self.isAIEnabled = false - } - - trainingTimer?.invalidate() - trainingTimer = nil - - await unloadAllModels() - - logger.info("AI Manager stopped") - } - - /// Makes a prediction using the specified model - public func predict( - modelType: ModelType, - features: [String: Double], - metadata: [String: String] = [:] - ) async throws -> PredictionResult { - guard isAIEnabled else { - throw AIError.learningDisabled - } - - guard loadedModels.contains(modelType) else { - // Try to load the model - try await loadModel(modelType) - } - - let result = try await predictionEngine.predict( - modelType: modelType, - features: features, - metadata: metadata - ) - - // Store prediction for learning - await storePredictionResult(result) - - // Update metrics - await updateModelMetrics(for: modelType, prediction: result) - - return result - } - - /// Trains a model with new data - public func trainModel( - modelType: ModelType, - trainingData: [[String: Double]], - labels: [Double] - ) async throws { - guard configuration.enableLearning else { - throw AIError.learningDisabled - } - - guard trainingData.count == labels.count else { - throw AIError.invalidInput - } - - // Privacy compliance check - let sanitizedData = await privacyManager.sanitizeTrainingData(trainingData) - - try await modelTrainer.trainModel( - type: modelType, - features: sanitizedData, - labels: labels, - configuration: configuration - ) - - // Update model metrics - await updateModelMetricsAfterTraining(modelType) - - logger.info("Model \(modelType.rawValue) training completed") - } - - /// Analyzes user behavior and updates profile - public func analyzeUserBehavior( - userId: String, - interactions: [UserInteraction], - context: [String: String] = [:] - ) async throws -> UserBehaviorProfile { - guard isAIEnabled else { - throw AIError.learningDisabled - } - - let profile = try await behaviorAnalyzer.analyzeUser( - userId: userId, - interactions: interactions, - context: context, - privacyLevel: configuration.privacyLevel - ) - - await MainActor.run { - self.userProfiles[userId] = profile - } - - // Generate personalized recommendations - if configuration.enablePersonalization { - await generatePersonalizedRecommendations(for: userId, profile: profile) - } - - return profile - } - - /// Generates content recommendations for a user - public func getContentRecommendations( - for userId: String, - contentTypes: [String] = [], - limit: Int = 10 - ) async throws -> [ContentRecommendation] { - guard isAIEnabled && configuration.enablePersonalization else { - throw AIError.learningDisabled - } - - let userProfile = userProfiles[userId] - let recommendations = try await recommendationEngine.generateRecommendations( - userId: userId, - userProfile: userProfile, - contentTypes: contentTypes, - limit: limit - ) - - // Cache recommendations - await MainActor.run { - self.recommendationCache.append(contentsOf: recommendations) - - // Keep cache size manageable - if self.recommendationCache.count > 1000 { - self.recommendationCache.removeFirst(self.recommendationCache.count - 1000) - } - } - - return recommendations - } - - /// Predicts widget usage patterns - public func predictWidgetUsage( - widgetId: String, - timeframe: TimeInterval, - context: [String: Double] = [:] - ) async throws -> PredictionResult { - let features = await featureExtractor.extractWidgetFeatures( - widgetId: widgetId, - timeframe: timeframe, - context: context - ) - - return try await predict( - modelType: .widgetUsagePrediction, - features: features, - metadata: ["widget_id": widgetId, "timeframe": String(timeframe)] - ) - } - - /// Detects anomalies in widget behavior - public func detectAnomalies( - widgetId: String, - metrics: [String: Double], - threshold: Double = 0.95 - ) async throws -> [AnomalyDetectionResult] { - let features = await featureExtractor.extractAnomalyFeatures( - widgetId: widgetId, - metrics: metrics - ) - - let prediction = try await predict( - modelType: .anomalyDetection, - features: features, - metadata: ["widget_id": widgetId, "threshold": String(threshold)] - ) - - return await interpretAnomalyPrediction(prediction, threshold: threshold) - } - - /// Optimizes widget performance using AI - public func optimizeWidgetPerformance( - widgetId: String, - currentMetrics: [String: Double], - constraints: [String: Double] = [:] - ) async throws -> PerformanceOptimizationSuggestion { - let features = await featureExtractor.extractPerformanceFeatures( - widgetId: widgetId, - metrics: currentMetrics, - constraints: constraints - ) - - let prediction = try await predict( - modelType: .performanceOptimization, - features: features, - metadata: ["widget_id": widgetId] - ) - - return await interpretPerformanceOptimization(prediction, currentMetrics: currentMetrics) - } - - /// Updates AI configuration - public func updateConfiguration(_ newConfiguration: AIConfiguration) async throws { - guard isValidConfiguration(newConfiguration) else { - throw AIError.configurationInvalid - } - - let wasEnabled = isAIEnabled - - if wasEnabled { - await stopAI() - } - - await MainActor.run { - self.configuration = newConfiguration - } - - if wasEnabled && newConfiguration.enableAI { - try await startAI() - } - - logger.info("AI configuration updated") - } - - /// Gets AI insights and analytics - public func getAIInsights() async -> AIInsights { - let totalPredictions = recentPredictions.count - let averageConfidence = recentPredictions.isEmpty ? 0 : - recentPredictions.map { $0.confidence }.reduce(0, +) / Double(totalPredictions) - - let modelQuality = modelMetrics.values.map { $0.qualityScore }.reduce(0, +) / Double(max(1, modelMetrics.count)) - - return AIInsights( - totalUsers: userProfiles.count, - totalPredictions: totalPredictions, - averageConfidence: averageConfidence, - loadedModels: Array(loadedModels), - modelQuality: modelQuality, - recommendationHitRate: await calculateRecommendationHitRate(), - learningEfficiency: await calculateLearningEfficiency(), - privacyCompliance: await privacyManager.getComplianceScore() - ) - } - - /// Exports AI model data for analysis - public func exportModelData(modelType: ModelType) async throws -> AIModelExport { - guard loadedModels.contains(modelType) else { - throw AIError.modelNotLoaded - } - - let metrics = modelMetrics[modelType] - let recentPredictionsForModel = recentPredictions.filter { $0.modelType == modelType } - - return AIModelExport( - modelType: modelType, - metrics: metrics, - recentPredictions: recentPredictionsForModel, - exportedAt: Date() - ) - } - - /// Clears all AI data and resets learning - public func resetAI() async throws { - await MainActor.run { - self.userProfiles.removeAll() - self.recentPredictions.removeAll() - self.recommendationCache.removeAll() - self.modelMetrics.removeAll() - } - - await unloadAllModels() - await learningEngine.resetLearning() - await privacyManager.clearData() - - logger.info("AI system reset completed") - } - - // MARK: - Private Methods - - private func setupAIManager() { - // Setup notification observers - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppWillEnterForeground() } - } - .store(in: &cancellables) - - logger.info("AI Manager initialized") - } - - private func checkCoreMLAvailability() async -> Bool { - // Check if Core ML is available on the device - return true // Simplified implementation - } - - private func loadInitialModels() async { - let priorityModels: [ModelType] = [ - .widgetUsagePrediction, - .contentRecommendation, - .performanceOptimization - ] - - for modelType in priorityModels { - do { - try await loadModel(modelType) - } catch { - logger.error("Failed to load model \(modelType.rawValue): \(error)") - } - } - } - - private func loadModel(_ modelType: ModelType) async throws { - try await modelRegistry.loadModel(modelType) - - await MainActor.run { - self.loadedModels.insert(modelType) - } - - logger.info("Loaded AI model: \(modelType.rawValue)") - } - - private func unloadAllModels() async { - await modelRegistry.unloadAllModels() - - await MainActor.run { - self.loadedModels.removeAll() - } - } - - private func setupAutomaticTraining() { - trainingTimer = Timer.scheduledTimer(withTimeInterval: configuration.modelUpdateInterval, repeats: true) { [weak self] _ in - Task { - await self?.performAutomaticTraining() - } - } - } - - private func performAutomaticTraining() async { - guard configuration.enableLearning else { return } - - for modelType in loadedModels { - do { - await learningEngine.performIncrementalLearning(for: modelType) - } catch { - logger.error("Automatic training failed for \(modelType.rawValue): \(error)") - } - } - } - - private func storePredictionResult(_ result: PredictionResult) async { - await MainActor.run { - self.recentPredictions.append(result) - - // Keep only recent predictions - if self.recentPredictions.count > 1000 { - self.recentPredictions.removeFirst(self.recentPredictions.count - 1000) - } - } - } - - private func updateModelMetrics(for modelType: ModelType, prediction: PredictionResult) async { - // Update prediction count and average confidence - await MainActor.run { - if var metrics = self.modelMetrics[modelType] { - metrics = ModelMetrics( - modelType: modelType, - accuracy: metrics.accuracy, - precision: metrics.precision, - recall: metrics.recall, - f1Score: metrics.f1Score, - trainingDataSize: metrics.trainingDataSize, - lastTrainingDate: metrics.lastTrainingDate, - predictionCount: metrics.predictionCount + 1, - averageConfidence: (metrics.averageConfidence * Double(metrics.predictionCount) + prediction.confidence) / Double(metrics.predictionCount + 1) - ) - self.modelMetrics[modelType] = metrics - } - } - } - - private func updateModelMetricsAfterTraining(_ modelType: ModelType) async { - // This would update metrics based on training results - let metrics = ModelMetrics( - modelType: modelType, - accuracy: 0.85, - precision: 0.82, - recall: 0.88, - f1Score: 0.85, - trainingDataSize: 1000, - lastTrainingDate: Date(), - predictionCount: 0, - averageConfidence: 0.0 - ) - - await MainActor.run { - self.modelMetrics[modelType] = metrics - } - } - - private func generatePersonalizedRecommendations(for userId: String, profile: UserBehaviorProfile) async { - do { - let recommendations = try await getContentRecommendations(for: userId) - // Recommendations are automatically cached in getContentRecommendations - } catch { - logger.error("Failed to generate personalized recommendations for user \(userId): \(error)") - } - } - - private func interpretAnomalyPrediction(_ prediction: PredictionResult, threshold: Double) async -> [AnomalyDetectionResult] { - guard prediction.confidence >= threshold else { - return [] - } - - return [ - AnomalyDetectionResult( - type: .performanceAnomaly, - severity: prediction.prediction > 0.8 ? .high : .medium, - description: "Unusual widget behavior detected", - confidence: prediction.confidence, - affectedMetrics: Array(prediction.features.keys), - timestamp: Date() - ) - ] - } - - private func interpretPerformanceOptimization(_ prediction: PredictionResult, currentMetrics: [String: Double]) async -> PerformanceOptimizationSuggestion { - return PerformanceOptimizationSuggestion( - widgetId: prediction.metadata["widget_id"] ?? "", - optimizationType: .memoryOptimization, - expectedImprovement: prediction.prediction, - confidence: prediction.confidence, - actionItems: [ - "Reduce memory allocation frequency", - "Optimize image caching strategy", - "Implement lazy loading for heavy components" - ], - estimatedImpact: prediction.prediction > 0.7 ? .high : .medium - ) - } - - private func isValidConfiguration(_ config: AIConfiguration) -> Bool { - return config.maxTrainingDataPoints > 0 && - config.modelUpdateInterval > 0 && - config.predictionConfidenceThreshold >= 0 && - config.predictionConfidenceThreshold <= 1 - } - - private func calculateRecommendationHitRate() async -> Double { - // Simplified implementation - return 0.75 - } - - private func calculateLearningEfficiency() async -> Double { - // Simplified implementation - return 0.82 - } - - private func handleAppDidEnterBackground() async { - // Save current state and optimize for background operation - if configuration.enableLearning { - await performAutomaticTraining() - } - } - - private func handleAppWillEnterForeground() async { - // Resume normal operation - await updateModelMetricsForAllModels() - } - - private func updateModelMetricsForAllModels() async { - for modelType in loadedModels { - await updateModelMetricsAfterTraining(modelType) - } - } -} - -// MARK: - Supporting Types - -public struct UserInteraction: Codable { - public let type: String - public let widgetId: String - public let timestamp: Date - public let duration: TimeInterval - public let context: [String: String] - - public init(type: String, widgetId: String, duration: TimeInterval, context: [String: String] = [:]) { - self.type = type - self.widgetId = widgetId - self.timestamp = Date() - self.duration = duration - self.context = context - } -} - -public struct AnomalyDetectionResult: Codable { - public let type: AnomalyType - public let severity: Severity - public let description: String - public let confidence: Double - public let affectedMetrics: [String] - public let timestamp: Date - - public enum AnomalyType: String, Codable { - case performanceAnomaly = "performance" - case usageAnomaly = "usage" - case dataAnomaly = "data" - } - - public enum Severity: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - } -} - -public struct PerformanceOptimizationSuggestion: Codable { - public let widgetId: String - public let optimizationType: OptimizationType - public let expectedImprovement: Double - public let confidence: Double - public let actionItems: [String] - public let estimatedImpact: Impact - - public enum OptimizationType: String, Codable { - case memoryOptimization = "memory" - case cpuOptimization = "cpu" - case batteryOptimization = "battery" - case networkOptimization = "network" - } - - public enum Impact: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - } -} - -public struct AIInsights { - public let totalUsers: Int - public let totalPredictions: Int - public let averageConfidence: Double - public let loadedModels: [AIManager.ModelType] - public let modelQuality: Double - public let recommendationHitRate: Double - public let learningEfficiency: Double - public let privacyCompliance: Double -} - -public struct AIModelExport: Codable { - public let modelType: AIManager.ModelType - public let metrics: AIManager.ModelMetrics? - public let recentPredictions: [AIManager.PredictionResult] - public let exportedAt: Date -} - -// MARK: - Private Supporting Classes - -private class MLModelRegistry { - private var models: [AIManager.ModelType: MLModel] = [:] - - func loadModel(_ type: AIManager.ModelType) async throws { - // Implementation would load actual Core ML models - // For now, we'll simulate model loading - models[type] = try await createMockModel(for: type) - } - - func unloadAllModels() async { - models.removeAll() - } - - private func createMockModel(for type: AIManager.ModelType) async throws -> MLModel { - // This would load actual Core ML models in a real implementation - // For now, return a mock model - throw AIManager.AIError.modelNotLoaded - } -} - -private class FeatureExtractor { - func extractWidgetFeatures(widgetId: String, timeframe: TimeInterval, context: [String: Double]) async -> [String: Double] { - var features: [String: Double] = [:] - features["timeframe"] = timeframe - features["hour_of_day"] = Double(Calendar.current.component(.hour, from: Date())) - features["day_of_week"] = Double(Calendar.current.component(.weekday, from: Date())) - features.merge(context) { $1 } - return features - } - - func extractAnomalyFeatures(widgetId: String, metrics: [String: Double]) async -> [String: Double] { - var features = metrics - features["timestamp"] = Date().timeIntervalSince1970 - features["widget_hash"] = Double(widgetId.hashValue) - return features - } - - func extractPerformanceFeatures(widgetId: String, metrics: [String: Double], constraints: [String: Double]) async -> [String: Double] { - var features = metrics - features.merge(constraints) { $1 } - features["optimization_timestamp"] = Date().timeIntervalSince1970 - return features - } -} - -private class PredictionEngine { - func predict(modelType: AIManager.ModelType, features: [String: Double], metadata: [String: String]) async throws -> AIManager.PredictionResult { - // Simulate AI prediction - let prediction = Double.random(in: 0...1) - let confidence = Double.random(in: 0.6...0.95) - - return AIManager.PredictionResult( - modelType: modelType, - prediction: prediction, - confidence: confidence, - features: features, - metadata: metadata, - explanation: "Simulated AI prediction based on \(features.count) features" - ) - } -} - -private class LearningEngine { - func performIncrementalLearning(for modelType: AIManager.ModelType) async { - // Implementation would perform incremental learning - } - - func resetLearning() async { - // Implementation would reset learning data - } -} - -private class RecommendationEngine { - func generateRecommendations( - userId: String, - userProfile: AIManager.UserBehaviorProfile?, - contentTypes: [String], - limit: Int - ) async throws -> [AIManager.ContentRecommendation] { - // Generate mock recommendations - var recommendations: [AIManager.ContentRecommendation] = [] - - for i in 0.. AIManager.UserBehaviorProfile { - // Analyze user interactions and generate profile - let widgetTypes = Set(interactions.map { $0.widgetId }).map { String($0) } - let totalDuration = interactions.reduce(0) { $0 + $1.duration } - let averageDuration = totalDuration / Double(max(1, interactions.count)) - - var usagePatterns: [String: Double] = [:] - var timePreferences: [String: Double] = [:] - - // Analyze usage patterns - for interaction in interactions { - let hour = Calendar.current.component(.hour, from: interaction.timestamp) - let hourKey = "hour_\(hour)" - timePreferences[hourKey, default: 0] += 1 - - usagePatterns[interaction.type, default: 0] += interaction.duration - } - - return AIManager.UserBehaviorProfile( - userId: userId, - preferredWidgetTypes: widgetTypes, - usagePatterns: usagePatterns, - timePreferences: timePreferences, - interactionFrequency: Double(interactions.count) / 7.0, // per week - sessionDuration: averageDuration, - deviceUsageContext: [:], - personalizedFeatures: [:], - lastUpdated: Date() - ) - } -} - -private class ModelTrainer { - func trainModel( - type: AIManager.ModelType, - features: [[String: Double]], - labels: [Double], - configuration: AIManager.AIConfiguration - ) async throws { - // Implementation would train actual models - // For now, simulate training - await Task.sleep(nanoseconds: UInt64(2 * 1_000_000_000)) // 2 seconds - } -} - -private class AIPrivacyManager { - func initialize(configuration: AIManager.AIConfiguration) async { - // Initialize privacy settings - } - - func sanitizeTrainingData(_ data: [[String: Double]]) async -> [[String: Double]] { - // Remove or anonymize sensitive data - return data - } - - func getComplianceScore() async -> Double { - return 0.95 // 95% compliance - } - - func clearData() async { - // Clear privacy-sensitive data - } -} - -// MARK: - Extensions - -extension Double { - func clamped(to range: ClosedRange) -> Double { - return Swift.min(Swift.max(self, range.lowerBound), range.upperBound) - } -} \ No newline at end of file diff --git a/Sources/Enterprise/AdvancedAnalytics.swift b/Sources/Enterprise/AdvancedAnalytics.swift deleted file mode 100644 index 99d6450..0000000 --- a/Sources/Enterprise/AdvancedAnalytics.swift +++ /dev/null @@ -1,1314 +0,0 @@ -import Foundation -import SwiftUI -import Combine -import OSLog -import Charts - -/// Enterprise-grade advanced analytics and business intelligence platform -/// providing comprehensive dashboards, KPI tracking, and strategic insights for iOS widgets -@available(iOS 16.0, *) -public class AdvancedAnalytics: ObservableObject { - - // MARK: - Types - - public enum AnalyticsError: LocalizedError { - case dashboardNotConfigured - case kpiCalculationFailed - case reportGenerationFailed - case dataVisualizationFailed - case alertConfigurationInvalid - case businessRuleViolation - case integrationFailed - case permissionDenied - - public var errorDescription: String? { - switch self { - case .dashboardNotConfigured: - return "Analytics dashboard is not properly configured" - case .kpiCalculationFailed: - return "Failed to calculate KPI metrics" - case .reportGenerationFailed: - return "Failed to generate analytics report" - case .dataVisualizationFailed: - return "Failed to create data visualization" - case .alertConfigurationInvalid: - return "Alert configuration is invalid" - case .businessRuleViolation: - return "Business rule validation failed" - case .integrationFailed: - return "External system integration failed" - case .permissionDenied: - return "Insufficient permissions for analytics operation" - } - } - } - - public enum DashboardType: String, CaseIterable, Codable { - case executive = "executive" - case operational = "operational" - case tactical = "tactical" - case technical = "technical" - case financial = "financial" - case marketing = "marketing" - case product = "product" - case userExperience = "user_experience" - - public var displayName: String { - switch self { - case .executive: return "Executive Dashboard" - case .operational: return "Operational Dashboard" - case .tactical: return "Tactical Dashboard" - case .technical: return "Technical Dashboard" - case .financial: return "Financial Dashboard" - case .marketing: return "Marketing Dashboard" - case .product: return "Product Dashboard" - case .userExperience: return "User Experience Dashboard" - } - } - - public var description: String { - switch self { - case .executive: - return "High-level strategic KPIs and business performance indicators" - case .operational: - return "Day-to-day operational metrics and system health" - case .tactical: - return "Mid-term tactical insights and performance trends" - case .technical: - return "Technical performance, system metrics, and engineering KPIs" - case .financial: - return "Revenue, cost, and financial performance analytics" - case .marketing: - return "User acquisition, engagement, and marketing effectiveness" - case .product: - return "Product usage, feature adoption, and user satisfaction" - case .userExperience: - return "User experience metrics, satisfaction, and usability insights" - } - } - } - - public enum KPICategory: String, CaseIterable, Codable { - case business = "business" - case technical = "technical" - case user = "user" - case financial = "financial" - case operational = "operational" - case quality = "quality" - - public var color: Color { - switch self { - case .business: return .blue - case .technical: return .green - case .user: return .purple - case .financial: return .orange - case .operational: return .red - case .quality: return .cyan - } - } - } - - public struct KPI: Identifiable, Codable { - public let id: String - public let name: String - public let description: String - public let category: KPICategory - public let currentValue: Double - public let targetValue: Double - public let previousValue: Double - public let unit: String - public let trend: Trend - public let status: Status - public let lastUpdated: Date - public let metadata: [String: String] - - public enum Trend: String, Codable { - case up = "up" - case down = "down" - case stable = "stable" - case volatile = "volatile" - - public var icon: String { - switch self { - case .up: return "arrow.up.circle.fill" - case .down: return "arrow.down.circle.fill" - case .stable: return "minus.circle.fill" - case .volatile: return "waveform.circle.fill" - } - } - - public var color: Color { - switch self { - case .up: return .green - case .down: return .red - case .stable: return .blue - case .volatile: return .orange - } - } - } - - public enum Status: String, Codable { - case excellent = "excellent" - case good = "good" - case warning = "warning" - case critical = "critical" - - public var color: Color { - switch self { - case .excellent: return .green - case .good: return .blue - case .warning: return .orange - case .critical: return .red - } - } - } - - public var achievementPercentage: Double { - guard targetValue != 0 else { return 0 } - return (currentValue / targetValue) * 100 - } - - public var changeFromPrevious: Double { - guard previousValue != 0 else { return 0 } - return ((currentValue - previousValue) / previousValue) * 100 - } - - public init( - name: String, - description: String, - category: KPICategory, - currentValue: Double, - targetValue: Double, - previousValue: Double = 0, - unit: String = "", - metadata: [String: String] = [:] - ) { - self.id = UUID().uuidString - self.name = name - self.description = description - self.category = category - self.currentValue = currentValue - self.targetValue = targetValue - self.previousValue = previousValue - self.unit = unit - self.lastUpdated = Date() - self.metadata = metadata - - // Calculate trend - if previousValue == 0 { - self.trend = .stable - } else { - let change = ((currentValue - previousValue) / previousValue) - if abs(change) < 0.05 { - self.trend = .stable - } else if change > 0.2 || change < -0.2 { - self.trend = .volatile - } else if change > 0 { - self.trend = .up - } else { - self.trend = .down - } - } - - // Calculate status - let achievement = (currentValue / targetValue) - if achievement >= 1.0 { - self.status = .excellent - } else if achievement >= 0.8 { - self.status = .good - } else if achievement >= 0.6 { - self.status = .warning - } else { - self.status = .critical - } - } - } - - public struct Dashboard: Identifiable, Codable { - public let id: String - public let name: String - public let type: DashboardType - public let description: String - public let widgets: [DashboardWidget] - public let layout: DashboardLayout - public let permissions: DashboardPermissions - public let refreshInterval: TimeInterval - public let createdAt: Date - public let lastUpdated: Date - - public struct DashboardWidget: Identifiable, Codable { - public let id: String - public let type: WidgetType - public let title: String - public let position: CGPoint - public let size: CGSize - public let configuration: WidgetConfiguration - public let dataSource: String - public let refreshInterval: TimeInterval - - public enum WidgetType: String, Codable { - case kpiCard = "kpi_card" - case chart = "chart" - case table = "table" - case gauge = "gauge" - case sparkline = "sparkline" - case heatmap = "heatmap" - case funnel = "funnel" - case scorecard = "scorecard" - } - - public struct WidgetConfiguration: Codable { - public let chartType: ChartType? - public let colorScheme: String - public let showTrend: Bool - public let showTarget: Bool - public let aggregationPeriod: String - public let filters: [String: String] - - public enum ChartType: String, Codable { - case line = "line" - case bar = "bar" - case pie = "pie" - case area = "area" - case scatter = "scatter" - case donut = "donut" - } - - public init( - chartType: ChartType? = nil, - colorScheme: String = "default", - showTrend: Bool = true, - showTarget: Bool = true, - aggregationPeriod: String = "daily", - filters: [String: String] = [:] - ) { - self.chartType = chartType - self.colorScheme = colorScheme - self.showTrend = showTrend - self.showTarget = showTarget - self.aggregationPeriod = aggregationPeriod - self.filters = filters - } - } - } - - public struct DashboardLayout: Codable { - public let columns: Int - public let rows: Int - public let gridSize: CGSize - public let spacing: CGFloat - } - - public struct DashboardPermissions: Codable { - public let viewUsers: [String] - public let editUsers: [String] - public let adminUsers: [String] - public let isPublic: Bool - } - - public init( - name: String, - type: DashboardType, - description: String, - widgets: [DashboardWidget] = [], - refreshInterval: TimeInterval = 300 - ) { - self.id = UUID().uuidString - self.name = name - self.type = type - self.description = description - self.widgets = widgets - self.layout = DashboardLayout( - columns: 12, - rows: 8, - gridSize: CGSize(width: 100, height: 100), - spacing: 16 - ) - self.permissions = DashboardPermissions( - viewUsers: [], - editUsers: [], - adminUsers: [], - isPublic: false - ) - self.refreshInterval = refreshInterval - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - public struct AnalyticsReport: Codable { - public let id: String - public let title: String - public let type: ReportType - public let period: DateInterval - public let executiveSummary: String - public let keyFindings: [String] - public let recommendations: [Recommendation] - public let kpiSummary: [KPI] - public let charts: [ChartData] - public let appendices: [ReportSection] - public let generatedAt: Date - public let generatedBy: String - - public enum ReportType: String, Codable { - case daily = "daily" - case weekly = "weekly" - case monthly = "monthly" - case quarterly = "quarterly" - case annual = "annual" - case adhoc = "adhoc" - } - - public struct Recommendation: Codable { - public let priority: Priority - public let category: String - public let title: String - public let description: String - public let expectedImpact: String - public let timeframe: String - public let effort: String - public let stakeholders: [String] - - public enum Priority: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - case critical = "critical" - } - } - - public struct ChartData: Codable { - public let title: String - public let type: Dashboard.DashboardWidget.WidgetConfiguration.ChartType - public let data: [DataPoint] - public let insights: [String] - - public struct DataPoint: Codable { - public let x: Double - public let y: Double - public let label: String? - public let metadata: [String: String] - } - } - - public struct ReportSection: Codable { - public let title: String - public let content: String - public let charts: [ChartData] - public let tables: [TableData] - - public struct TableData: Codable { - public let title: String - public let headers: [String] - public let rows: [[String]] - public let summary: String? - } - } - } - - public struct Alert: Identifiable, Codable { - public let id: String - public let name: String - public let description: String - public let condition: AlertCondition - public let severity: Severity - public let isActive: Bool - public let triggeredAt: Date? - public let lastTriggered: Date? - public let triggerCount: Int - public let recipients: [String] - public let actions: [AlertAction] - - public struct AlertCondition: Codable { - public let kpiId: String - public let operator: ComparisonOperator - public let threshold: Double - public let duration: TimeInterval - public let aggregation: AggregationType - - public enum ComparisonOperator: String, Codable { - case greaterThan = "gt" - case lessThan = "lt" - case equals = "eq" - case greaterThanOrEquals = "gte" - case lessThanOrEquals = "lte" - case notEquals = "ne" - } - - public enum AggregationType: String, Codable { - case average = "avg" - case sum = "sum" - case count = "count" - case minimum = "min" - case maximum = "max" - } - } - - public enum Severity: String, Codable { - case info = "info" - case warning = "warning" - case critical = "critical" - case emergency = "emergency" - - public var color: Color { - switch self { - case .info: return .blue - case .warning: return .orange - case .critical: return .red - case .emergency: return .purple - } - } - } - - public struct AlertAction: Codable { - public let type: ActionType - public let configuration: [String: String] - - public enum ActionType: String, Codable { - case email = "email" - case push = "push" - case webhook = "webhook" - case sms = "sms" - case slack = "slack" - } - } - } - - public struct BusinessInsight: Identifiable, Codable { - public let id: String - public let title: String - public let description: String - public let category: InsightCategory - public let impact: Impact - public let confidence: Double - public let dataPoints: [String] - public let recommendations: [String] - public let generatedAt: Date - public let expiresAt: Date? - - public enum InsightCategory: String, Codable { - case performance = "performance" - case user = "user" - case revenue = "revenue" - case cost = "cost" - case quality = "quality" - case opportunity = "opportunity" - case risk = "risk" - } - - public enum Impact: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - case critical = "critical" - - public var color: Color { - switch self { - case .low: return .blue - case .medium: return .orange - case .high: return .red - case .critical: return .purple - } - } - } - } - - // MARK: - Properties - - public static let shared = AdvancedAnalytics() - - private let kpiEngine: KPIEngine - private let dashboardEngine: DashboardEngine - private let reportGenerator: ReportGenerator - private let alertManager: AlertManager - private let insightEngine: BusinessInsightEngine - private let visualizationEngine: DataVisualizationEngine - private let exportManager: AnalyticsExportManager - private let integrationManager: ExternalIntegrationManager - - @Published public private(set) var dashboards: [Dashboard] = [] - @Published public private(set) var kpis: [KPI] = [] - @Published public private(set) var alerts: [Alert] = [] - @Published public private(set) var insights: [BusinessInsight] = [] - @Published public private(set) var recentReports: [AnalyticsReport] = [] - @Published public private(set) var isProcessing: Bool = false - @Published public private(set) var lastRefreshTime: Date? - - private var refreshTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "advanced.analytics", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "AdvancedAnalytics") - - // MARK: - Initialization - - private init() { - self.kpiEngine = KPIEngine() - self.dashboardEngine = DashboardEngine() - self.reportGenerator = ReportGenerator() - self.alertManager = AlertManager() - self.insightEngine = BusinessInsightEngine() - self.visualizationEngine = DataVisualizationEngine() - self.exportManager = AnalyticsExportManager() - self.integrationManager = ExternalIntegrationManager() - - setupAdvancedAnalytics() - } - - // MARK: - Public Methods - - /// Initializes advanced analytics system - public func initialize() async throws { - await MainActor.run { - self.isProcessing = true - } - - // Load existing dashboards and KPIs - await loadDashboards() - await loadKPIs() - await loadAlerts() - await loadInsights() - - // Setup automatic refresh - setupAutoRefresh() - - await MainActor.run { - self.isProcessing = false - self.lastRefreshTime = Date() - } - - logger.info("Advanced Analytics initialized") - } - - /// Creates a new dashboard - public func createDashboard( - name: String, - type: DashboardType, - description: String, - widgets: [Dashboard.DashboardWidget] = [] - ) async throws -> Dashboard { - let dashboard = Dashboard( - name: name, - type: type, - description: description, - widgets: widgets - ) - - await MainActor.run { - self.dashboards.append(dashboard) - } - - await dashboardEngine.saveDashboard(dashboard) - - logger.info("Created dashboard: \(name)") - - return dashboard - } - - /// Updates an existing dashboard - public func updateDashboard(_ dashboard: Dashboard) async throws { - await MainActor.run { - if let index = self.dashboards.firstIndex(where: { $0.id == dashboard.id }) { - self.dashboards[index] = dashboard - } - } - - await dashboardEngine.saveDashboard(dashboard) - - logger.info("Updated dashboard: \(dashboard.name)") - } - - /// Deletes a dashboard - public func deleteDashboard(_ dashboardId: String) async throws { - await MainActor.run { - self.dashboards.removeAll { $0.id == dashboardId } - } - - await dashboardEngine.deleteDashboard(dashboardId) - - logger.info("Deleted dashboard: \(dashboardId)") - } - - /// Calculates and updates KPIs - public func refreshKPIs() async throws { - await MainActor.run { - self.isProcessing = true - } - - let updatedKPIs = try await kpiEngine.calculateKPIs() - - await MainActor.run { - self.kpis = updatedKPIs - self.lastRefreshTime = Date() - self.isProcessing = false - } - - // Check for alert conditions - await checkAlertConditions() - - logger.info("Refreshed \(updatedKPIs.count) KPIs") - } - - /// Generates comprehensive analytics report - public func generateReport( - type: AnalyticsReport.ReportType, - period: DateInterval, - includeCharts: Bool = true - ) async throws -> AnalyticsReport { - await MainActor.run { - self.isProcessing = true - } - - defer { - Task { - await MainActor.run { - self.isProcessing = false - } - } - } - - let report = try await reportGenerator.generateReport( - type: type, - period: period, - kpis: kpis, - includeCharts: includeCharts - ) - - await MainActor.run { - self.recentReports.append(report) - - // Keep only recent reports - if self.recentReports.count > 50 { - self.recentReports.removeFirst(self.recentReports.count - 50) - } - } - - logger.info("Generated \(type.rawValue) report for period \(period)") - - return report - } - - /// Creates or updates an alert - public func configureAlert( - name: String, - description: String, - condition: Alert.AlertCondition, - severity: Alert.Severity, - recipients: [String], - actions: [Alert.AlertAction] - ) async throws -> Alert { - let alert = Alert( - id: UUID().uuidString, - name: name, - description: description, - condition: condition, - severity: severity, - isActive: true, - triggeredAt: nil, - lastTriggered: nil, - triggerCount: 0, - recipients: recipients, - actions: actions - ) - - await MainActor.run { - self.alerts.append(alert) - } - - await alertManager.saveAlert(alert) - - logger.info("Configured alert: \(name)") - - return alert - } - - /// Gets dashboard data for visualization - public func getDashboardData(_ dashboardId: String) async throws -> DashboardData { - guard let dashboard = dashboards.first(where: { $0.id == dashboardId }) else { - throw AnalyticsError.dashboardNotConfigured - } - - var widgetData: [String: WidgetData] = [:] - - for widget in dashboard.widgets { - let data = try await dashboardEngine.getWidgetData( - widget: widget, - kpis: kpis - ) - widgetData[widget.id] = data - } - - return DashboardData( - dashboard: dashboard, - widgetData: widgetData, - lastUpdated: Date() - ) - } - - /// Generates business insights using AI - public func generateBusinessInsights() async throws -> [BusinessInsight] { - let newInsights = try await insightEngine.generateInsights( - kpis: kpis, - historicalData: await getHistoricalKPIData(), - externalFactors: await getExternalFactors() - ) - - await MainActor.run { - self.insights.append(contentsOf: newInsights) - - // Remove expired insights - self.insights.removeAll { insight in - if let expiresAt = insight.expiresAt { - return Date() > expiresAt - } - return false - } - - // Keep only recent insights - if self.insights.count > 100 { - self.insights = Array(self.insights.suffix(100)) - } - } - - logger.info("Generated \(newInsights.count) business insights") - - return newInsights - } - - /// Exports analytics data in various formats - public func exportData( - format: ExportFormat, - dashboardIds: [String] = [], - includeRawData: Bool = false - ) async throws -> ExportResult { - return try await exportManager.exportData( - format: format, - dashboards: dashboards.filter { dashboardIds.isEmpty || dashboardIds.contains($0.id) }, - kpis: kpis, - reports: recentReports, - includeRawData: includeRawData - ) - } - - /// Integrates with external analytics platforms - public func integrateWithPlatform( - platform: ExternalPlatform, - configuration: [String: String] - ) async throws { - try await integrationManager.setupIntegration( - platform: platform, - configuration: configuration, - dashboards: dashboards, - kpis: kpis - ) - - logger.info("Integrated with platform: \(platform.rawValue)") - } - - /// Gets real-time analytics summary - public func getAnalyticsSummary() async -> AnalyticsSummary { - let kpiSummary = await generateKPISummary() - let alertSummary = await generateAlertSummary() - let insightSummary = await generateInsightSummary() - - return AnalyticsSummary( - totalDashboards: dashboards.count, - totalKPIs: kpis.count, - activeAlerts: alerts.filter { $0.isActive }.count, - criticalAlerts: alerts.filter { $0.severity == .critical || $0.severity == .emergency }.count, - recentInsights: insights.filter { Date().timeIntervalSince($0.generatedAt) < 24 * 3600 }.count, - kpiSummary: kpiSummary, - alertSummary: alertSummary, - insightSummary: insightSummary, - lastRefresh: lastRefreshTime ?? Date() - ) - } - - // MARK: - Private Methods - - private func setupAdvancedAnalytics() { - // Setup notification observers - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - - logger.info("Advanced Analytics setup completed") - } - - private func loadDashboards() async { - do { - let loadedDashboards = try await dashboardEngine.loadDashboards() - await MainActor.run { - self.dashboards = loadedDashboards - } - } catch { - logger.error("Failed to load dashboards: \(error)") - } - } - - private func loadKPIs() async { - do { - let loadedKPIs = try await kpiEngine.loadKPIs() - await MainActor.run { - self.kpis = loadedKPIs - } - } catch { - logger.error("Failed to load KPIs: \(error)") - } - } - - private func loadAlerts() async { - do { - let loadedAlerts = try await alertManager.loadAlerts() - await MainActor.run { - self.alerts = loadedAlerts - } - } catch { - logger.error("Failed to load alerts: \(error)") - } - } - - private func loadInsights() async { - do { - let loadedInsights = try await insightEngine.loadInsights() - await MainActor.run { - self.insights = loadedInsights - } - } catch { - logger.error("Failed to load insights: \(error)") - } - } - - private func setupAutoRefresh() { - refreshTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in - Task { - try? await self?.refreshKPIs() - } - } - } - - private func checkAlertConditions() async { - for alert in alerts.filter({ $0.isActive }) { - let shouldTrigger = await alertManager.evaluateCondition( - condition: alert.condition, - kpis: kpis - ) - - if shouldTrigger { - await alertManager.triggerAlert(alert) - } - } - } - - private func getHistoricalKPIData() async -> [String: [(Date, Double)]] { - // Implementation would fetch historical KPI data - return [:] - } - - private func getExternalFactors() async -> [String: Double] { - // Implementation would fetch external factors (market data, seasonality, etc.) - return [:] - } - - private func generateKPISummary() async -> KPISummary { - let total = kpis.count - let excellent = kpis.filter { $0.status == .excellent }.count - let good = kpis.filter { $0.status == .good }.count - let warning = kpis.filter { $0.status == .warning }.count - let critical = kpis.filter { $0.status == .critical }.count - - return KPISummary( - total: total, - excellent: excellent, - good: good, - warning: warning, - critical: critical - ) - } - - private func generateAlertSummary() async -> AlertSummary { - let total = alerts.count - let active = alerts.filter { $0.isActive }.count - let triggered = alerts.filter { $0.triggeredAt != nil }.count - - return AlertSummary( - total: total, - active: active, - triggered: triggered - ) - } - - private func generateInsightSummary() async -> InsightSummary { - let total = insights.count - let highImpact = insights.filter { $0.impact == .high || $0.impact == .critical }.count - let recent = insights.filter { Date().timeIntervalSince($0.generatedAt) < 24 * 3600 }.count - - return InsightSummary( - total: total, - highImpact: highImpact, - recent: recent - ) - } - - private func handleAppDidEnterBackground() async { - // Save current state - refreshTimer?.invalidate() - - // Perform final data sync - try? await refreshKPIs() - } -} - -// MARK: - Supporting Types - -public struct DashboardData { - public let dashboard: AdvancedAnalytics.Dashboard - public let widgetData: [String: WidgetData] - public let lastUpdated: Date -} - -public struct WidgetData { - public let values: [Double] - public let labels: [String] - public let metadata: [String: String] - public let chartData: ChartDataPoints? - - public struct ChartDataPoints { - public let x: [Double] - public let y: [Double] - public let series: [Series] - - public struct Series { - public let name: String - public let data: [(Double, Double)] - public let color: Color - } - } -} - -public enum ExportFormat: String, CaseIterable { - case json = "json" - case csv = "csv" - case excel = "excel" - case pdf = "pdf" -} - -public enum ExternalPlatform: String, CaseIterable { - case googleAnalytics = "google_analytics" - case mixpanel = "mixpanel" - case amplitude = "amplitude" - case tableau = "tableau" - case powerBI = "power_bi" - case looker = "looker" -} - -public struct ExportResult { - public let format: ExportFormat - public let data: Data - public let filename: String - public let exportedAt: Date -} - -public struct AnalyticsSummary { - public let totalDashboards: Int - public let totalKPIs: Int - public let activeAlerts: Int - public let criticalAlerts: Int - public let recentInsights: Int - public let kpiSummary: KPISummary - public let alertSummary: AlertSummary - public let insightSummary: InsightSummary - public let lastRefresh: Date -} - -public struct KPISummary { - public let total: Int - public let excellent: Int - public let good: Int - public let warning: Int - public let critical: Int - - public var healthScore: Double { - guard total > 0 else { return 0 } - let score = (excellent * 4 + good * 3 + warning * 2 + critical * 1) - return Double(score) / Double(total * 4) - } -} - -public struct AlertSummary { - public let total: Int - public let active: Int - public let triggered: Int -} - -public struct InsightSummary { - public let total: Int - public let highImpact: Int - public let recent: Int -} - -// MARK: - Private Supporting Classes - -private class KPIEngine { - func calculateKPIs() async throws -> [AdvancedAnalytics.KPI] { - // Mock KPI calculation - let kpis = [ - AdvancedAnalytics.KPI( - name: "Daily Active Users", - description: "Number of unique users who engaged with widgets daily", - category: .user, - currentValue: 12500, - targetValue: 15000, - previousValue: 11800, - unit: "users" - ), - AdvancedAnalytics.KPI( - name: "Widget Load Time", - description: "Average time to load widget content", - category: .technical, - currentValue: 0.85, - targetValue: 1.0, - previousValue: 0.92, - unit: "seconds" - ), - AdvancedAnalytics.KPI( - name: "User Retention Rate", - description: "Percentage of users who return after 7 days", - category: .business, - currentValue: 78.5, - targetValue: 80.0, - previousValue: 76.2, - unit: "%" - ) - ] - - return kpis - } - - func loadKPIs() async throws -> [AdvancedAnalytics.KPI] { - // Load KPIs from storage - return [] - } -} - -private class DashboardEngine { - func saveDashboard(_ dashboard: AdvancedAnalytics.Dashboard) async { - // Save dashboard to storage - } - - func deleteDashboard(_ dashboardId: String) async { - // Delete dashboard from storage - } - - func loadDashboards() async throws -> [AdvancedAnalytics.Dashboard] { - // Load dashboards from storage - return [] - } - - func getWidgetData(widget: AdvancedAnalytics.Dashboard.DashboardWidget, kpis: [AdvancedAnalytics.KPI]) async throws -> WidgetData { - // Generate widget data based on KPIs - return WidgetData( - values: [100, 150, 200, 180, 220], - labels: ["Mon", "Tue", "Wed", "Thu", "Fri"], - metadata: [:], - chartData: nil - ) - } -} - -private class ReportGenerator { - func generateReport( - type: AdvancedAnalytics.AnalyticsReport.ReportType, - period: DateInterval, - kpis: [AdvancedAnalytics.KPI], - includeCharts: Bool - ) async throws -> AdvancedAnalytics.AnalyticsReport { - // Generate comprehensive report - return AdvancedAnalytics.AnalyticsReport( - id: UUID().uuidString, - title: "\(type.rawValue.capitalized) Analytics Report", - type: type, - period: period, - executiveSummary: "Overall performance shows positive trends with key metrics exceeding targets.", - keyFindings: [ - "User engagement increased by 15% compared to previous period", - "Widget load times improved by 8%", - "Revenue per user grew by 12%" - ], - recommendations: [ - AdvancedAnalytics.AnalyticsReport.Recommendation( - priority: .high, - category: "Performance", - title: "Optimize Widget Caching", - description: "Implement intelligent caching to further reduce load times", - expectedImpact: "15% improvement in load times", - timeframe: "2 weeks", - effort: "Medium", - stakeholders: ["Engineering", "Product"] - ) - ], - kpiSummary: kpis, - charts: [], - appendices: [], - generatedAt: Date(), - generatedBy: "Advanced Analytics Engine" - ) - } -} - -private class AlertManager { - func saveAlert(_ alert: AdvancedAnalytics.Alert) async { - // Save alert configuration - } - - func loadAlerts() async throws -> [AdvancedAnalytics.Alert] { - // Load alerts from storage - return [] - } - - func evaluateCondition(condition: AdvancedAnalytics.Alert.AlertCondition, kpis: [AdvancedAnalytics.KPI]) async -> Bool { - // Evaluate alert condition against current KPIs - guard let kpi = kpis.first(where: { $0.id == condition.kpiId }) else { - return false - } - - switch condition.operator { - case .greaterThan: - return kpi.currentValue > condition.threshold - case .lessThan: - return kpi.currentValue < condition.threshold - case .equals: - return abs(kpi.currentValue - condition.threshold) < 0.01 - case .greaterThanOrEquals: - return kpi.currentValue >= condition.threshold - case .lessThanOrEquals: - return kpi.currentValue <= condition.threshold - case .notEquals: - return abs(kpi.currentValue - condition.threshold) >= 0.01 - } - } - - func triggerAlert(_ alert: AdvancedAnalytics.Alert) async { - // Trigger alert actions (email, push, webhook, etc.) - for action in alert.actions { - await executeAlertAction(action, alert: alert) - } - } - - private func executeAlertAction(_ action: AdvancedAnalytics.Alert.AlertAction, alert: AdvancedAnalytics.Alert) async { - // Execute specific alert action - switch action.type { - case .email: - // Send email notification - break - case .push: - // Send push notification - break - case .webhook: - // Call webhook - break - case .sms: - // Send SMS - break - case .slack: - // Send Slack message - break - } - } -} - -private class BusinessInsightEngine { - func generateInsights( - kpis: [AdvancedAnalytics.KPI], - historicalData: [String: [(Date, Double)]], - externalFactors: [String: Double] - ) async throws -> [AdvancedAnalytics.BusinessInsight] { - // Generate AI-powered business insights - var insights: [AdvancedAnalytics.BusinessInsight] = [] - - // Example insight generation - let performanceKPIs = kpis.filter { $0.category == .technical } - if performanceKPIs.allSatisfy({ $0.trend == .up }) { - insights.append( - AdvancedAnalytics.BusinessInsight( - id: UUID().uuidString, - title: "Performance Optimization Success", - description: "All technical KPIs show positive trends indicating successful optimization efforts", - category: .performance, - impact: .high, - confidence: 0.85, - dataPoints: performanceKPIs.map { $0.id }, - recommendations: [ - "Continue current optimization strategies", - "Consider expanding optimization to other areas" - ], - generatedAt: Date(), - expiresAt: Date().addingTimeInterval(7 * 24 * 3600) - ) - ) - } - - return insights - } - - func loadInsights() async throws -> [AdvancedAnalytics.BusinessInsight] { - // Load insights from storage - return [] - } -} - -private class DataVisualizationEngine { - // Data visualization implementation -} - -private class AnalyticsExportManager { - func exportData( - format: ExportFormat, - dashboards: [AdvancedAnalytics.Dashboard], - kpis: [AdvancedAnalytics.KPI], - reports: [AdvancedAnalytics.AnalyticsReport], - includeRawData: Bool - ) async throws -> ExportResult { - // Export data in specified format - let exportData = Data() - - return ExportResult( - format: format, - data: exportData, - filename: "analytics_export_\(Date().timeIntervalSince1970).\(format.rawValue)", - exportedAt: Date() - ) - } -} - -private class ExternalIntegrationManager { - func setupIntegration( - platform: ExternalPlatform, - configuration: [String: String], - dashboards: [AdvancedAnalytics.Dashboard], - kpis: [AdvancedAnalytics.KPI] - ) async throws { - // Setup integration with external platform - switch platform { - case .googleAnalytics: - // Setup Google Analytics integration - break - case .mixpanel: - // Setup Mixpanel integration - break - case .amplitude: - // Setup Amplitude integration - break - case .tableau: - // Setup Tableau integration - break - case .powerBI: - // Setup Power BI integration - break - case .looker: - // Setup Looker integration - break - } - } -} \ No newline at end of file diff --git a/Sources/Enterprise/MLAnalyticsEngine.swift b/Sources/Enterprise/MLAnalyticsEngine.swift deleted file mode 100644 index cf006cc..0000000 --- a/Sources/Enterprise/MLAnalyticsEngine.swift +++ /dev/null @@ -1,1275 +0,0 @@ -import Foundation -import CoreML -import Combine -import OSLog -import Charts - -/// Enterprise-grade machine learning analytics engine providing advanced data analysis, -/// pattern recognition, and predictive insights for iOS widget ecosystems -@available(iOS 16.0, *) -public class MLAnalyticsEngine: ObservableObject { - - // MARK: - Types - - public enum AnalyticsError: LocalizedError { - case insufficientData - case analysisNotAvailable - case modelNotTrained - case invalidParameters - case dataProcessingFailed - case segmentationFailed - case forecastingFailed - case clusteringFailed - - public var errorDescription: String? { - switch self { - case .insufficientData: - return "Insufficient data for analytics processing" - case .analysisNotAvailable: - return "Analytics feature is not available" - case .modelNotTrained: - return "Analytics model is not trained" - case .invalidParameters: - return "Invalid parameters provided for analysis" - case .dataProcessingFailed: - return "Data processing failed" - case .segmentationFailed: - return "User segmentation analysis failed" - case .forecastingFailed: - return "Time series forecasting failed" - case .clusteringFailed: - return "Clustering analysis failed" - } - } - } - - public enum AnalyticsType: String, CaseIterable, Codable { - case userSegmentation = "user_segmentation" - case behaviorAnalysis = "behavior_analysis" - case timeSeriesForecasting = "time_series_forecasting" - case cohortAnalysis = "cohort_analysis" - case funnelAnalysis = "funnel_analysis" - case retentionAnalysis = "retention_analysis" - case sentimentAnalysis = "sentiment_analysis" - case anomalyDetection = "anomaly_detection" - case patternRecognition = "pattern_recognition" - case predictiveScoring = "predictive_scoring" - - public var description: String { - switch self { - case .userSegmentation: - return "Groups users based on behavior patterns and characteristics" - case .behaviorAnalysis: - return "Analyzes user behavior patterns and trends" - case .timeSeriesForecasting: - return "Predicts future trends based on historical data" - case .cohortAnalysis: - return "Analyzes user groups over time periods" - case .funnelAnalysis: - return "Analyzes user journey and conversion funnels" - case .retentionAnalysis: - return "Measures user retention and churn patterns" - case .sentimentAnalysis: - return "Analyzes sentiment in user interactions and feedback" - case .anomalyDetection: - return "Detects unusual patterns and outliers in data" - case .patternRecognition: - return "Identifies recurring patterns and trends" - case .predictiveScoring: - return "Calculates predictive scores for user actions" - } - } - } - - public struct AnalyticsConfiguration { - public var isEnabled: Bool = true - public var enableRealTimeAnalytics: Bool = true - public var enablePredictiveAnalytics: Bool = true - public var dataRetentionDays: Int = 90 - public var minDataPointsForAnalysis: Int = 100 - public var analysisUpdateInterval: TimeInterval = 3600 // 1 hour - public var confidenceThreshold: Double = 0.75 - public var enableAdvancedSegmentation: Bool = true - public var enableDeepLearning: Bool = false - public var maxConcurrentAnalyses: Int = 5 - - public init() {} - } - - public struct AnalyticsDataPoint: Codable { - public let id: String - public let userId: String? - public let widgetId: String? - public let eventType: String - public let timestamp: Date - public let properties: [String: AnyCodable] - public let metadata: [String: String] - public let sessionId: String? - - public init( - userId: String? = nil, - widgetId: String? = nil, - eventType: String, - properties: [String: AnyCodable] = [:], - metadata: [String: String] = [:], - sessionId: String? = nil - ) { - self.id = UUID().uuidString - self.userId = userId - self.widgetId = widgetId - self.eventType = eventType - self.timestamp = Date() - self.properties = properties - self.metadata = metadata - self.sessionId = sessionId - } - } - - public struct UserSegment: Codable, Identifiable { - public let id: String - public let name: String - public let description: String - public let criteria: SegmentCriteria - public let userCount: Int - public let characteristics: [String: Double] - public let createdAt: Date - public let lastUpdated: Date - public let confidence: Double - - public struct SegmentCriteria: Codable { - public let behaviorPatterns: [String: Double] - public let demographicFilters: [String: String] - public let usageThresholds: [String: Double] - public let timeBasedFilters: [String: String] - } - - public init( - name: String, - description: String, - criteria: SegmentCriteria, - userCount: Int, - characteristics: [String: Double], - confidence: Double - ) { - self.id = UUID().uuidString - self.name = name - self.description = description - self.criteria = criteria - self.userCount = userCount - self.characteristics = characteristics - self.createdAt = Date() - self.lastUpdated = Date() - self.confidence = confidence - } - } - - public struct TimeSeriesForecast: Codable { - public let metric: String - public let forecastPeriod: DateInterval - public let predictions: [ForecastPoint] - public let confidence: Double - public let methodology: String - public let seasonality: SeasonalityInfo? - public let trend: TrendInfo - - public struct ForecastPoint: Codable { - public let timestamp: Date - public let predictedValue: Double - public let confidenceInterval: ConfidenceInterval - public let trend: Double - public let seasonalComponent: Double? - } - - public struct ConfidenceInterval: Codable { - public let lower: Double - public let upper: Double - } - - public struct SeasonalityInfo: Codable { - public let period: TimeInterval - public let strength: Double - public let pattern: [Double] - } - - public struct TrendInfo: Codable { - public let direction: TrendDirection - public let strength: Double - public let changePoints: [Date] - - public enum TrendDirection: String, Codable { - case increasing = "increasing" - case decreasing = "decreasing" - case stable = "stable" - } - } - } - - public struct CohortAnalysisResult: Codable { - public let cohortDefinition: CohortDefinition - public let cohorts: [Cohort] - public let metrics: [String: [Double]] - public let insights: [String] - public let generatedAt: Date - - public struct CohortDefinition: Codable { - public let timeUnit: TimeUnit - public let metric: String - public let startDate: Date - public let endDate: Date - - public enum TimeUnit: String, Codable { - case day = "day" - case week = "week" - case month = "month" - } - } - - public struct Cohort: Codable { - public let name: String - public let startDate: Date - public let initialSize: Int - public let retentionRates: [Double] - public let averageValue: Double - public let churnRate: Double - } - } - - public struct BehaviorPattern: Codable, Identifiable { - public let id: String - public let name: String - public let description: String - public let frequency: Double - public let users: [String] - public let events: [String] - public let timePattern: TimePattern - public let strength: Double - public let significance: Double - - public struct TimePattern: Codable { - public let hourlyDistribution: [Double] - public let dailyDistribution: [Double] - public let weeklyDistribution: [Double] - public let seasonalityDetected: Bool - } - - public init( - name: String, - description: String, - frequency: Double, - users: [String], - events: [String], - timePattern: TimePattern, - strength: Double, - significance: Double - ) { - self.id = UUID().uuidString - self.name = name - self.description = description - self.frequency = frequency - self.users = users - self.events = events - self.timePattern = timePattern - self.strength = strength - self.significance = significance - } - } - - public struct FunnelAnalysisResult: Codable { - public let funnelName: String - public let steps: [FunnelStep] - public let conversionRate: Double - public let dropOffPoints: [DropOffAnalysis] - public let insights: [String] - public let timeframe: DateInterval - - public struct FunnelStep: Codable { - public let stepName: String - public let stepOrder: Int - public let userCount: Int - public let conversionFromPrevious: Double - public let averageTimeToComplete: TimeInterval - public let dropOffRate: Double - } - - public struct DropOffAnalysis: Codable { - public let stepName: String - public let dropOffRate: Double - public let commonReasons: [String] - public let recommendations: [String] - } - } - - // MARK: - Properties - - public static let shared = MLAnalyticsEngine() - - private let dataProcessor: AnalyticsDataProcessor - private let segmentationEngine: UserSegmentationEngine - private let forecastingEngine: TimeSeriesForecastingEngine - private let behaviorAnalyzer: BehaviorAnalysisEngine - private let cohortAnalyzer: CohortAnalysisEngine - private let patternRecognizer: PatternRecognitionEngine - private let anomalyDetector: AnomalyDetectionEngine - private let sentimentAnalyzer: SentimentAnalysisEngine - private let storageManager: AnalyticsStorageManager - - @Published public private(set) var configuration: AnalyticsConfiguration = AnalyticsConfiguration() - @Published public private(set) var isAnalyticsEnabled: Bool = false - @Published public private(set) var currentSegments: [UserSegment] = [] - @Published public private(set) var behaviorPatterns: [BehaviorPattern] = [] - @Published public private(set) var recentForecasts: [TimeSeriesForecast] = [] - @Published public private(set) var analyticsMetrics: AnalyticsMetrics = AnalyticsMetrics() - @Published public private(set) var processingQueue: [AnalyticsJob] = [] - - private var analysisTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "analytics.engine", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "Analytics") - - // MARK: - Initialization - - private init() { - self.dataProcessor = AnalyticsDataProcessor() - self.segmentationEngine = UserSegmentationEngine() - self.forecastingEngine = TimeSeriesForecastingEngine() - self.behaviorAnalyzer = BehaviorAnalysisEngine() - self.cohortAnalyzer = CohortAnalysisEngine() - self.patternRecognizer = PatternRecognitionEngine() - self.anomalyDetector = AnomalyDetectionEngine() - self.sentimentAnalyzer = SentimentAnalysisEngine() - self.storageManager = AnalyticsStorageManager() - - setupAnalyticsEngine() - } - - // MARK: - Public Methods - - /// Starts the analytics engine - public func startAnalytics() async throws { - guard configuration.isEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - await MainActor.run { - self.isAnalyticsEnabled = true - } - - // Load historical data - await loadHistoricalAnalytics() - - // Setup real-time analytics - if configuration.enableRealTimeAnalytics { - setupRealTimeAnalytics() - } - - // Setup periodic analysis - setupPeriodicAnalysis() - - logger.info("ML Analytics Engine started") - } - - /// Stops the analytics engine - public func stopAnalytics() async { - await MainActor.run { - self.isAnalyticsEnabled = false - } - - analysisTimer?.invalidate() - analysisTimer = nil - - // Save current state - await saveAnalyticsState() - - logger.info("ML Analytics Engine stopped") - } - - /// Records a new analytics data point - public func recordEvent( - userId: String? = nil, - widgetId: String? = nil, - eventType: String, - properties: [String: AnyCodable] = [:], - metadata: [String: String] = [:], - sessionId: String? = nil - ) async { - guard isAnalyticsEnabled else { return } - - let dataPoint = AnalyticsDataPoint( - userId: userId, - widgetId: widgetId, - eventType: eventType, - properties: properties, - metadata: metadata, - sessionId: sessionId - ) - - await dataProcessor.processDataPoint(dataPoint) - - // Trigger real-time analysis if enabled - if configuration.enableRealTimeAnalytics { - await triggerRealTimeAnalysis(for: dataPoint) - } - - await updateAnalyticsMetrics() - } - - /// Performs user segmentation analysis - public func performUserSegmentation( - criteria: [String: Any] = [:], - minSegmentSize: Int = 50 - ) async throws -> [UserSegment] { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let userData = try await dataProcessor.getUserData() - - guard userData.count >= configuration.minDataPointsForAnalysis else { - throw AnalyticsError.insufficientData - } - - let segments = try await segmentationEngine.segmentUsers( - userData: userData, - criteria: criteria, - minSegmentSize: minSegmentSize, - configuration: configuration - ) - - await MainActor.run { - self.currentSegments = segments - } - - await storageManager.saveSegments(segments) - - logger.info("User segmentation completed: \(segments.count) segments identified") - - return segments - } - - /// Performs time series forecasting - public func forecastMetric( - metric: String, - forecastPeriod: TimeInterval, - historicalPeriod: TimeInterval? = nil - ) async throws -> TimeSeriesForecast { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let timePeriod = historicalPeriod ?? (forecastPeriod * 4) // Default to 4x historical data - let historicalData = try await dataProcessor.getTimeSeriesData( - metric: metric, - period: timePeriod - ) - - guard historicalData.count >= configuration.minDataPointsForAnalysis else { - throw AnalyticsError.insufficientData - } - - let forecast = try await forecastingEngine.generateForecast( - metric: metric, - historicalData: historicalData, - forecastPeriod: forecastPeriod, - configuration: configuration - ) - - await MainActor.run { - self.recentForecasts.append(forecast) - - // Keep only recent forecasts - if self.recentForecasts.count > 100 { - self.recentForecasts.removeFirst(self.recentForecasts.count - 100) - } - } - - await storageManager.saveForecast(forecast) - - return forecast - } - - /// Analyzes user behavior patterns - public func analyzeBehaviorPatterns( - timeframe: DateInterval? = nil, - minPatternStrength: Double = 0.6 - ) async throws -> [BehaviorPattern] { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let period = timeframe ?? DateInterval( - start: Date().addingTimeInterval(-30 * 24 * 3600), // Last 30 days - end: Date() - ) - - let behaviorData = try await dataProcessor.getBehaviorData(for: period) - - let patterns = try await behaviorAnalyzer.identifyPatterns( - behaviorData: behaviorData, - minStrength: minPatternStrength, - configuration: configuration - ) - - await MainActor.run { - self.behaviorPatterns = patterns - } - - await storageManager.savePatterns(patterns) - - logger.info("Behavior analysis completed: \(patterns.count) patterns identified") - - return patterns - } - - /// Performs cohort analysis - public func performCohortAnalysis( - metric: String, - timeUnit: CohortAnalysisResult.CohortDefinition.TimeUnit, - startDate: Date, - endDate: Date - ) async throws -> CohortAnalysisResult { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let cohortDefinition = CohortAnalysisResult.CohortDefinition( - timeUnit: timeUnit, - metric: metric, - startDate: startDate, - endDate: endDate - ) - - let userData = try await dataProcessor.getCohortData(for: cohortDefinition) - - let result = try await cohortAnalyzer.analyzeCohorts( - definition: cohortDefinition, - userData: userData, - configuration: configuration - ) - - await storageManager.saveCohortAnalysis(result) - - return result - } - - /// Performs funnel analysis - public func performFunnelAnalysis( - funnelName: String, - steps: [String], - timeframe: DateInterval - ) async throws -> FunnelAnalysisResult { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let funnelData = try await dataProcessor.getFunnelData( - steps: steps, - timeframe: timeframe - ) - - let result = try await behaviorAnalyzer.analyzeFunnel( - name: funnelName, - steps: steps, - data: funnelData, - timeframe: timeframe, - configuration: configuration - ) - - await storageManager.saveFunnelAnalysis(result) - - return result - } - - /// Detects anomalies in metrics - public func detectAnomalies( - metrics: [String], - timeframe: DateInterval, - sensitivity: Double = 0.95 - ) async throws -> [AnomalyDetectionResult] { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let metricsData = try await dataProcessor.getMetricsData( - metrics: metrics, - timeframe: timeframe - ) - - let anomalies = try await anomalyDetector.detectAnomalies( - data: metricsData, - sensitivity: sensitivity, - configuration: configuration - ) - - return anomalies - } - - /// Gets predictive scores for users - public func getPredictiveScores( - userIds: [String], - scoreType: String, - timeHorizon: TimeInterval = 7 * 24 * 3600 // 7 days - ) async throws -> [String: Double] { - guard isAnalyticsEnabled && configuration.enablePredictiveAnalytics else { - throw AnalyticsError.analysisNotAvailable - } - - let userData = try await dataProcessor.getUserData(for: userIds) - - let scores = try await behaviorAnalyzer.calculatePredictiveScores( - userData: userData, - scoreType: scoreType, - timeHorizon: timeHorizon, - configuration: configuration - ) - - return scores - } - - /// Gets analytics insights and recommendations - public func getAnalyticsInsights() async -> AnalyticsInsights { - let totalDataPoints = await dataProcessor.getTotalDataPoints() - let activeUsers = await dataProcessor.getActiveUserCount() - let processingSpeed = analyticsMetrics.averageProcessingTime - - return AnalyticsInsights( - totalDataPoints: totalDataPoints, - activeUsers: activeUsers, - segments: currentSegments.count, - patterns: behaviorPatterns.count, - forecasts: recentForecasts.count, - processingSpeed: processingSpeed, - dataQuality: await calculateDataQuality(), - insights: await generateInsights(), - recommendations: await generateRecommendations() - ) - } - - /// Exports analytics data for external analysis - public func exportAnalyticsData( - types: [AnalyticsType], - dateRange: DateInterval - ) async throws -> AnalyticsExport { - var exportData: [String: Any] = [:] - - for type in types { - switch type { - case .userSegmentation: - exportData["segments"] = currentSegments - case .behaviorAnalysis: - exportData["patterns"] = behaviorPatterns - case .timeSeriesForecasting: - exportData["forecasts"] = recentForecasts - case .cohortAnalysis: - let cohorts = try await storageManager.getCohortAnalyses(for: dateRange) - exportData["cohorts"] = cohorts - default: - // Add other analytics types as needed - break - } - } - - return AnalyticsExport( - exportedAt: Date(), - dateRange: dateRange, - types: types, - data: exportData, - metadata: [ - "total_users": String(await dataProcessor.getActiveUserCount()), - "data_points": String(await dataProcessor.getTotalDataPoints()), - "quality_score": String(await calculateDataQuality()) - ] - ) - } - - /// Updates analytics configuration - public func updateConfiguration(_ newConfiguration: AnalyticsConfiguration) async throws { - guard isValidConfiguration(newConfiguration) else { - throw AnalyticsError.invalidParameters - } - - let wasEnabled = isAnalyticsEnabled - - if wasEnabled { - await stopAnalytics() - } - - await MainActor.run { - self.configuration = newConfiguration - } - - if wasEnabled && newConfiguration.isEnabled { - try await startAnalytics() - } - - logger.info("Analytics configuration updated") - } - - /// Clears all analytics data - public func clearAnalyticsData() async throws { - await MainActor.run { - self.currentSegments.removeAll() - self.behaviorPatterns.removeAll() - self.recentForecasts.removeAll() - self.processingQueue.removeAll() - self.analyticsMetrics = AnalyticsMetrics() - } - - await storageManager.clearAllData() - - logger.info("Analytics data cleared") - } - - // MARK: - Private Methods - - private func setupAnalyticsEngine() { - // Setup notification observers - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - - logger.info("ML Analytics Engine initialized") - } - - private func loadHistoricalAnalytics() async { - do { - let segments = try await storageManager.loadSegments() - let patterns = try await storageManager.loadPatterns() - let forecasts = try await storageManager.loadForecasts() - - await MainActor.run { - self.currentSegments = segments - self.behaviorPatterns = patterns - self.recentForecasts = forecasts - } - } catch { - logger.error("Failed to load historical analytics: \(error)") - } - } - - private func setupRealTimeAnalytics() { - // Setup real-time analytics processing - Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in - Task { - await self?.processRealTimeAnalytics() - } - } - } - - private func setupPeriodicAnalysis() { - analysisTimer = Timer.scheduledTimer(withTimeInterval: configuration.analysisUpdateInterval, repeats: true) { [weak self] _ in - Task { - await self?.performPeriodicAnalysis() - } - } - } - - private func triggerRealTimeAnalysis(for dataPoint: AnalyticsDataPoint) async { - // Perform real-time analysis on new data point - await patternRecognizer.analyzeDataPoint(dataPoint) - - // Update any real-time metrics - await updateRealTimeMetrics(with: dataPoint) - } - - private func processRealTimeAnalytics() async { - // Process pending real-time analytics - await processAnalyticsQueue() - } - - private func performPeriodicAnalysis() async { - guard isAnalyticsEnabled else { return } - - do { - // Perform automated analytics - _ = try await performUserSegmentation() - _ = try await analyzeBehaviorPatterns() - - // Update forecasts - let commonMetrics = ["user_engagement", "widget_usage", "retention_rate"] - for metric in commonMetrics { - _ = try? await forecastMetric(metric: metric, forecastPeriod: 7 * 24 * 3600) // 7 days - } - - } catch { - logger.error("Periodic analysis failed: \(error)") - } - } - - private func processAnalyticsQueue() async { - // Process pending analytics jobs - let jobs = processingQueue - - await MainActor.run { - self.processingQueue.removeAll() - } - - for job in jobs { - await processAnalyticsJob(job) - } - } - - private func processAnalyticsJob(_ job: AnalyticsJob) async { - // Process individual analytics job - let startTime = CFAbsoluteTimeGetCurrent() - - defer { - let processingTime = CFAbsoluteTimeGetCurrent() - startTime - Task { - await self.updateProcessingMetrics(processingTime: processingTime) - } - } - - // Process job based on type - // Implementation would handle different job types - } - - private func updateAnalyticsMetrics() async { - await MainActor.run { - self.analyticsMetrics.totalEvents += 1 - self.analyticsMetrics.lastProcessedAt = Date() - } - } - - private func updateRealTimeMetrics(with dataPoint: AnalyticsDataPoint) async { - await MainActor.run { - self.analyticsMetrics.realTimeEvents += 1 - } - } - - private func updateProcessingMetrics(processingTime: TimeInterval) async { - await MainActor.run { - self.analyticsMetrics.totalProcessingTime += processingTime - self.analyticsMetrics.processedJobs += 1 - self.analyticsMetrics.averageProcessingTime = - self.analyticsMetrics.totalProcessingTime / Double(self.analyticsMetrics.processedJobs) - } - } - - private func saveAnalyticsState() async { - do { - try await storageManager.saveSegments(currentSegments) - try await storageManager.savePatterns(behaviorPatterns) - try await storageManager.saveForecasts(recentForecasts) - } catch { - logger.error("Failed to save analytics state: \(error)") - } - } - - private func calculateDataQuality() async -> Double { - // Calculate data quality score based on completeness, accuracy, consistency - let completeness = await dataProcessor.getDataCompleteness() - let accuracy = await dataProcessor.getDataAccuracy() - let consistency = await dataProcessor.getDataConsistency() - - return (completeness * 0.4 + accuracy * 0.4 + consistency * 0.2).clamped(to: 0...1) - } - - private func generateInsights() async -> [String] { - var insights: [String] = [] - - // Generate insights based on current analytics - if currentSegments.count > 0 { - insights.append("Identified \(currentSegments.count) distinct user segments") - } - - if behaviorPatterns.count > 0 { - let strongPatterns = behaviorPatterns.filter { $0.strength > 0.8 }.count - insights.append("Found \(strongPatterns) strong behavior patterns") - } - - if !recentForecasts.isEmpty { - let averageConfidence = recentForecasts.map { $0.confidence }.reduce(0, +) / Double(recentForecasts.count) - insights.append("Forecasting models showing \(Int(averageConfidence * 100))% average confidence") - } - - return insights - } - - private func generateRecommendations() async -> [String] { - var recommendations: [String] = [] - - // Generate recommendations based on analytics - let dataQuality = await calculateDataQuality() - if dataQuality < 0.8 { - recommendations.append("Improve data collection to enhance analytics accuracy") - } - - if currentSegments.isEmpty { - recommendations.append("Perform user segmentation to better understand your audience") - } - - if behaviorPatterns.count < 5 { - recommendations.append("Collect more behavioral data to identify usage patterns") - } - - return recommendations - } - - private func isValidConfiguration(_ config: AnalyticsConfiguration) -> Bool { - return config.dataRetentionDays > 0 && - config.minDataPointsForAnalysis > 0 && - config.analysisUpdateInterval > 0 && - config.confidenceThreshold >= 0 && - config.confidenceThreshold <= 1 && - config.maxConcurrentAnalyses > 0 - } - - private func handleAppDidEnterBackground() async { - // Save state and optimize for background operation - await saveAnalyticsState() - } -} - -// MARK: - Supporting Types - -public struct AnalyticsMetrics { - public var totalEvents: Int = 0 - public var realTimeEvents: Int = 0 - public var processedJobs: Int = 0 - public var totalProcessingTime: TimeInterval = 0 - public var averageProcessingTime: TimeInterval = 0 - public var lastProcessedAt: Date? -} - -public struct AnalyticsJob { - public let id: String - public let type: MLAnalyticsEngine.AnalyticsType - public let parameters: [String: Any] - public let priority: Int - public let createdAt: Date - - public init(type: MLAnalyticsEngine.AnalyticsType, parameters: [String: Any], priority: Int = 1) { - self.id = UUID().uuidString - self.type = type - self.parameters = parameters - self.priority = priority - self.createdAt = Date() - } -} - -public struct AnalyticsInsights { - public let totalDataPoints: Int - public let activeUsers: Int - public let segments: Int - public let patterns: Int - public let forecasts: Int - public let processingSpeed: TimeInterval - public let dataQuality: Double - public let insights: [String] - public let recommendations: [String] -} - -public struct AnalyticsExport: Codable { - public let exportedAt: Date - public let dateRange: DateInterval - public let types: [MLAnalyticsEngine.AnalyticsType] - public let data: [String: Any] - public let metadata: [String: String] - - enum CodingKeys: String, CodingKey { - case exportedAt, dateRange, types, metadata - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(exportedAt, forKey: .exportedAt) - try container.encode(dateRange, forKey: .dateRange) - try container.encode(types, forKey: .types) - try container.encode(metadata, forKey: .metadata) - // Note: data field would need custom encoding in real implementation - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - exportedAt = try container.decode(Date.self, forKey: .exportedAt) - dateRange = try container.decode(DateInterval.self, forKey: .dateRange) - types = try container.decode([MLAnalyticsEngine.AnalyticsType].self, forKey: .types) - metadata = try container.decode([String: String].self, forKey: .metadata) - data = [:] // Would be decoded from custom format - } - - init(exportedAt: Date, dateRange: DateInterval, types: [MLAnalyticsEngine.AnalyticsType], data: [String: Any], metadata: [String: String]) { - self.exportedAt = exportedAt - self.dateRange = dateRange - self.types = types - self.data = data - self.metadata = metadata - } -} - -public struct AnyCodable: Codable { - public let value: Any - - public init(_ value: T?) { - self.value = value ?? () - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch value { - case let value as String: - try container.encode(value) - case let value as Int: - try container.encode(value) - case let value as Double: - try container.encode(value) - case let value as Bool: - try container.encode(value) - default: - try container.encodeNil() - } - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if let value = try? container.decode(String.self) { - self.value = value - } else if let value = try? container.decode(Int.self) { - self.value = value - } else if let value = try? container.decode(Double.self) { - self.value = value - } else if let value = try? container.decode(Bool.self) { - self.value = value - } else { - self.value = () - } - } -} - -// MARK: - Private Supporting Classes - -private class AnalyticsDataProcessor { - func processDataPoint(_ dataPoint: MLAnalyticsEngine.AnalyticsDataPoint) async { - // Process incoming data point - } - - func getUserData() async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return user data grouped by user ID - return [:] - } - - func getUserData(for userIds: [String]) async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return user data for specific users - return [:] - } - - func getTimeSeriesData(metric: String, period: TimeInterval) async throws -> [(Date, Double)] { - // Return time series data for metric - return [] - } - - func getBehaviorData(for period: DateInterval) async throws -> [MLAnalyticsEngine.AnalyticsDataPoint] { - // Return behavior data for period - return [] - } - - func getCohortData(for definition: MLAnalyticsEngine.CohortAnalysisResult.CohortDefinition) async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return cohort data - return [:] - } - - func getFunnelData(steps: [String], timeframe: DateInterval) async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return funnel data - return [:] - } - - func getMetricsData(metrics: [String], timeframe: DateInterval) async throws -> [String: [(Date, Double)]] { - // Return metrics data - return [:] - } - - func getTotalDataPoints() async -> Int { - return 10000 // Mock value - } - - func getActiveUserCount() async -> Int { - return 500 // Mock value - } - - func getDataCompleteness() async -> Double { - return 0.85 // Mock value - } - - func getDataAccuracy() async -> Double { - return 0.92 // Mock value - } - - func getDataConsistency() async -> Double { - return 0.88 // Mock value - } -} - -private class UserSegmentationEngine { - func segmentUsers( - userData: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - criteria: [String: Any], - minSegmentSize: Int, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [MLAnalyticsEngine.UserSegment] { - // Perform user segmentation - return [] - } -} - -private class TimeSeriesForecastingEngine { - func generateForecast( - metric: String, - historicalData: [(Date, Double)], - forecastPeriod: TimeInterval, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> MLAnalyticsEngine.TimeSeriesForecast { - // Generate time series forecast - let predictions: [MLAnalyticsEngine.TimeSeriesForecast.ForecastPoint] = [] - - return MLAnalyticsEngine.TimeSeriesForecast( - metric: metric, - forecastPeriod: DateInterval(start: Date(), duration: forecastPeriod), - predictions: predictions, - confidence: 0.85, - methodology: "ARIMA", - seasonality: nil, - trend: MLAnalyticsEngine.TimeSeriesForecast.TrendInfo( - direction: .stable, - strength: 0.3, - changePoints: [] - ) - ) - } -} - -private class BehaviorAnalysisEngine { - func identifyPatterns( - behaviorData: [MLAnalyticsEngine.AnalyticsDataPoint], - minStrength: Double, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [MLAnalyticsEngine.BehaviorPattern] { - // Identify behavior patterns - return [] - } - - func analyzeFunnel( - name: String, - steps: [String], - data: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - timeframe: DateInterval, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> MLAnalyticsEngine.FunnelAnalysisResult { - // Analyze funnel - return MLAnalyticsEngine.FunnelAnalysisResult( - funnelName: name, - steps: [], - conversionRate: 0.0, - dropOffPoints: [], - insights: [], - timeframe: timeframe - ) - } - - func calculatePredictiveScores( - userData: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - scoreType: String, - timeHorizon: TimeInterval, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [String: Double] { - // Calculate predictive scores - return [:] - } -} - -private class CohortAnalysisEngine { - func analyzeCohorts( - definition: MLAnalyticsEngine.CohortAnalysisResult.CohortDefinition, - userData: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> MLAnalyticsEngine.CohortAnalysisResult { - // Perform cohort analysis - return MLAnalyticsEngine.CohortAnalysisResult( - cohortDefinition: definition, - cohorts: [], - metrics: [:], - insights: [], - generatedAt: Date() - ) - } -} - -private class PatternRecognitionEngine { - func analyzeDataPoint(_ dataPoint: MLAnalyticsEngine.AnalyticsDataPoint) async { - // Analyze data point for patterns - } -} - -private class AnomalyDetectionEngine { - func detectAnomalies( - data: [String: [(Date, Double)]], - sensitivity: Double, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [AnomalyDetectionResult] { - // Detect anomalies in data - return [] - } -} - -private class SentimentAnalysisEngine { - // Sentiment analysis implementation -} - -private class AnalyticsStorageManager { - func saveSegments(_ segments: [MLAnalyticsEngine.UserSegment]) async throws { - // Save segments to storage - } - - func loadSegments() async throws -> [MLAnalyticsEngine.UserSegment] { - // Load segments from storage - return [] - } - - func savePatterns(_ patterns: [MLAnalyticsEngine.BehaviorPattern]) async throws { - // Save patterns to storage - } - - func loadPatterns() async throws -> [MLAnalyticsEngine.BehaviorPattern] { - // Load patterns from storage - return [] - } - - func saveForecasts(_ forecasts: [MLAnalyticsEngine.TimeSeriesForecast]) async throws { - // Save forecasts to storage - } - - func loadForecasts() async throws -> [MLAnalyticsEngine.TimeSeriesForecast] { - // Load forecasts from storage - return [] - } - - func saveForecast(_ forecast: MLAnalyticsEngine.TimeSeriesForecast) async throws { - // Save single forecast - } - - func saveCohortAnalysis(_ analysis: MLAnalyticsEngine.CohortAnalysisResult) async throws { - // Save cohort analysis - } - - func getCohortAnalyses(for dateRange: DateInterval) async throws -> [MLAnalyticsEngine.CohortAnalysisResult] { - // Get cohort analyses for date range - return [] - } - - func saveFunnelAnalysis(_ analysis: MLAnalyticsEngine.FunnelAnalysisResult) async throws { - // Save funnel analysis - } - - func clearAllData() async throws { - // Clear all stored data - } -} - -extension Double { - func clamped(to range: ClosedRange) -> Double { - return Swift.min(Swift.max(self, range.lowerBound), range.upperBound) - } -} \ No newline at end of file diff --git a/Sources/Integration/APIManager.swift b/Sources/Integration/APIManager.swift deleted file mode 100644 index 5753017..0000000 --- a/Sources/Integration/APIManager.swift +++ /dev/null @@ -1,1162 +0,0 @@ -import Foundation -import Network -import Combine -import OSLog - -/// Enterprise-grade API management system providing comprehensive RESTful, GraphQL, -/// and WebSocket integrations with advanced security, monitoring, and resilience features -@available(iOS 16.0, *) -public class APIManager: ObservableObject { - - // MARK: - Types - - public enum APIError: LocalizedError { - case invalidURL - case networkUnavailable - case authenticationFailed - case rateLimitExceeded - case serverError(Int, String) - case timeoutError - case serializationError - case validationError(String) - case circuitBreakerOpen - case retryLimitExceeded - case certificatePinningFailed - case requestTooLarge - case responseProcessingFailed - - public var errorDescription: String? { - switch self { - case .invalidURL: - return "Invalid URL provided" - case .networkUnavailable: - return "Network connection unavailable" - case .authenticationFailed: - return "Authentication failed" - case .rateLimitExceeded: - return "Rate limit exceeded" - case .serverError(let code, let message): - return "Server error \(code): \(message)" - case .timeoutError: - return "Request timeout" - case .serializationError: - return "Data serialization failed" - case .validationError(let details): - return "Validation error: \(details)" - case .circuitBreakerOpen: - return "Circuit breaker is open" - case .retryLimitExceeded: - return "Maximum retry attempts exceeded" - case .certificatePinningFailed: - return "Certificate pinning validation failed" - case .requestTooLarge: - return "Request payload too large" - case .responseProcessingFailed: - return "Response processing failed" - } - } - } - - public enum HTTPMethod: String, CaseIterable { - case GET = "GET" - case POST = "POST" - case PUT = "PUT" - case DELETE = "DELETE" - case PATCH = "PATCH" - case HEAD = "HEAD" - case OPTIONS = "OPTIONS" - } - - public enum ContentType: String { - case json = "application/json" - case formURLEncoded = "application/x-www-form-urlencoded" - case multipartFormData = "multipart/form-data" - case plainText = "text/plain" - case xml = "application/xml" - case protobuf = "application/x-protobuf" - } - - public struct APIConfiguration { - public var baseURL: String - public var timeout: TimeInterval = 30.0 - public var retryPolicy: RetryPolicy = RetryPolicy() - public var rateLimiting: RateLimitConfig = RateLimitConfig() - public var security: SecurityConfig = SecurityConfig() - public var caching: CacheConfig = CacheConfig() - public var monitoring: MonitoringConfig = MonitoringConfig() - public var circuitBreaker: CircuitBreakerConfig = CircuitBreakerConfig() - public var headers: [String: String] = [:] - public var enableCompression: Bool = true - public var enableLogging: Bool = true - - public struct RetryPolicy { - public var maxRetries: Int = 3 - public var backoffStrategy: BackoffStrategy = .exponential - public var retryableStatusCodes: Set = [408, 429, 500, 502, 503, 504] - public var retryableErrors: Set = [.timedOut, .networkConnectionLost, .notConnectedToInternet] - - public enum BackoffStrategy { - case linear(TimeInterval) - case exponential(base: TimeInterval = 1.0, multiplier: Double = 2.0) - case fixed(TimeInterval) - case custom((Int) -> TimeInterval) - } - } - - public struct RateLimitConfig { - public var requestsPerSecond: Int = 10 - public var burstLimit: Int = 50 - public var enableAdaptiveRateLimit: Bool = true - public var trackByEndpoint: Bool = true - } - - public struct SecurityConfig { - public var enableCertificatePinning: Bool = true - public var pinnedCertificates: [String] = [] - public var enableTLSValidation: Bool = true - public var allowInsecureConnections: Bool = false - public var customTrustEvaluator: ((SecTrust) -> Bool)? - public var enableRequestSigning: Bool = false - public var signingAlgorithm: String = "HMAC-SHA256" - } - - public struct CacheConfig { - public var enableCaching: Bool = true - public var cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad - public var maxCacheAge: TimeInterval = 3600 - public var cacheSizeLimit: Int = 100 * 1024 * 1024 // 100MB - public var enableETagSupport: Bool = true - } - - public struct MonitoringConfig { - public var enableMetrics: Bool = true - public var enableTracing: Bool = true - public var enableHealthChecks: Bool = true - public var metricsReportingInterval: TimeInterval = 60 - public var traceHeaders: [String] = ["X-Trace-ID", "X-Span-ID"] - } - - public struct CircuitBreakerConfig { - public var isEnabled: Bool = true - public var failureThreshold: Int = 5 - public var recoveryTimeout: TimeInterval = 30 - public var halfOpenMaxCalls: Int = 3 - public var monitoringPeriod: TimeInterval = 60 - } - - public init(baseURL: String) { - self.baseURL = baseURL - } - } - - public struct APIRequest { - public let method: HTTPMethod - public let endpoint: String - public let parameters: [String: Any]? - public let headers: [String: String] - public let body: Data? - public let contentType: ContentType - public let timeout: TimeInterval? - public let priority: Priority - public let tags: [String] - public let cachePolicy: URLRequest.CachePolicy? - public let retryPolicy: APIConfiguration.RetryPolicy? - - public enum Priority: Int, CaseIterable { - case low = 0 - case normal = 1 - case high = 2 - case critical = 3 - } - - public init( - method: HTTPMethod = .GET, - endpoint: String, - parameters: [String: Any]? = nil, - headers: [String: String] = [:], - body: Data? = nil, - contentType: ContentType = .json, - timeout: TimeInterval? = nil, - priority: Priority = .normal, - tags: [String] = [], - cachePolicy: URLRequest.CachePolicy? = nil, - retryPolicy: APIConfiguration.RetryPolicy? = nil - ) { - self.method = method - self.endpoint = endpoint - self.parameters = parameters - self.headers = headers - self.body = body - self.contentType = contentType - self.timeout = timeout - self.priority = priority - self.tags = tags - self.cachePolicy = cachePolicy - self.retryPolicy = retryPolicy - } - } - - public struct APIResponse { - public let data: T? - public let statusCode: Int - public let headers: [String: String] - public let responseTime: TimeInterval - public let fromCache: Bool - public let retryCount: Int - public let traceId: String? - public let rawData: Data? - - public var isSuccessful: Bool { - return 200...299 ~= statusCode - } - - public init( - data: T?, - statusCode: Int, - headers: [String: String] = [:], - responseTime: TimeInterval, - fromCache: Bool = false, - retryCount: Int = 0, - traceId: String? = nil, - rawData: Data? = nil - ) { - self.data = data - self.statusCode = statusCode - self.headers = headers - self.responseTime = responseTime - self.fromCache = fromCache - self.retryCount = retryCount - self.traceId = traceId - self.rawData = rawData - } - } - - public struct GraphQLRequest { - public let query: String - public let variables: [String: Any]? - public let operationName: String? - public let timeout: TimeInterval? - public let priority: APIRequest.Priority - - public init( - query: String, - variables: [String: Any]? = nil, - operationName: String? = nil, - timeout: TimeInterval? = nil, - priority: APIRequest.Priority = .normal - ) { - self.query = query - self.variables = variables - self.operationName = operationName - self.timeout = timeout - self.priority = priority - } - } - - public struct WebSocketConfig { - public let url: URL - public let protocols: [String] - public let headers: [String: String] - public let pingInterval: TimeInterval - public let reconnectPolicy: ReconnectPolicy - public let compression: Bool - - public struct ReconnectPolicy { - public let maxRetries: Int - public let backoffStrategy: APIConfiguration.RetryPolicy.BackoffStrategy - public let enableAutoReconnect: Bool - } - - public init( - url: URL, - protocols: [String] = [], - headers: [String: String] = [:], - pingInterval: TimeInterval = 30, - maxRetries: Int = 5, - enableAutoReconnect: Bool = true, - compression: Bool = true - ) { - self.url = url - self.protocols = protocols - self.headers = headers - self.pingInterval = pingInterval - self.reconnectPolicy = ReconnectPolicy( - maxRetries: maxRetries, - backoffStrategy: .exponential(), - enableAutoReconnect: enableAutoReconnect - ) - self.compression = compression - } - } - - public struct APIMetrics { - public var totalRequests: Int = 0 - public var successfulRequests: Int = 0 - public var failedRequests: Int = 0 - public var averageResponseTime: TimeInterval = 0 - public var cacheHitRate: Double = 0 - public var rateLimitHits: Int = 0 - public var circuitBreakerTrips: Int = 0 - public var bytesTransferred: Int = 0 - public var lastRequestTime: Date? - - public var successRate: Double { - guard totalRequests > 0 else { return 0 } - return Double(successfulRequests) / Double(totalRequests) - } - - public var errorRate: Double { - return 1.0 - successRate - } - } - - // MARK: - Properties - - public static let shared = APIManager() - - private let session: URLSession - private let configuration: APIConfiguration - private let rateLimiter: RateLimiter - private let circuitBreaker: CircuitBreaker - private let retryHandler: RetryHandler - private let securityHandler: SecurityHandler - private let metricsCollector: MetricsCollector - private let cacheManager: APICacheManager - private let requestQueue: RequestQueue - private let websocketManager: WebSocketManager - - @Published public private(set) var metrics: APIMetrics = APIMetrics() - @Published public private(set) var isNetworkAvailable: Bool = true - @Published public private(set) var activeConnections: Int = 0 - @Published public private(set) var circuitBreakerState: CircuitBreakerState = .closed - - private let networkMonitor = NWPathMonitor() - private var cancellables = Set() - private let queue = DispatchQueue(label: "api.manager", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "API") - - public enum CircuitBreakerState { - case closed - case open - case halfOpen - } - - // MARK: - Initialization - - public init(configuration: APIConfiguration? = nil) { - self.configuration = configuration ?? APIConfiguration(baseURL: "https://api.example.com") - - // Configure URLSession - let sessionConfig = URLSessionConfiguration.default - sessionConfig.timeoutIntervalForRequest = self.configuration.timeout - sessionConfig.timeoutIntervalForResource = self.configuration.timeout * 2 - sessionConfig.requestCachePolicy = self.configuration.caching.cachePolicy - sessionConfig.urlCache = URLCache( - memoryCapacity: self.configuration.caching.cacheSizeLimit / 2, - diskCapacity: self.configuration.caching.cacheSizeLimit, - diskPath: "APICache" - ) - - if self.configuration.enableCompression { - sessionConfig.httpAdditionalHeaders = ["Accept-Encoding": "gzip, deflate, br"] - } - - self.session = URLSession(configuration: sessionConfig) - - // Initialize components - self.rateLimiter = RateLimiter(config: self.configuration.rateLimiting) - self.circuitBreaker = CircuitBreaker(config: self.configuration.circuitBreaker) - self.retryHandler = RetryHandler(config: self.configuration.retryPolicy) - self.securityHandler = SecurityHandler(config: self.configuration.security) - self.metricsCollector = MetricsCollector() - self.cacheManager = APICacheManager(config: self.configuration.caching) - self.requestQueue = RequestQueue() - self.websocketManager = WebSocketManager() - - setupAPIManager() - } - - // MARK: - Public Methods - - /// Executes a REST API request with comprehensive error handling and monitoring - public func request( - _ request: APIRequest, - responseType: T.Type - ) async throws -> APIResponse { - let startTime = CFAbsoluteTimeGetCurrent() - let traceId = UUID().uuidString - - // Pre-flight checks - try await performPreflightChecks(request) - - // Rate limiting - try await rateLimiter.checkLimit(for: request.endpoint) - - // Circuit breaker check - try circuitBreaker.checkState() - - // Build URL request - let urlRequest = try buildURLRequest(from: request, traceId: traceId) - - // Check cache first - if let cachedResponse: APIResponse = await cacheManager.getCachedResponse(for: urlRequest) { - await updateMetrics(success: true, responseTime: 0, fromCache: true) - return cachedResponse - } - - var lastError: Error? - let maxRetries = request.retryPolicy?.maxRetries ?? configuration.retryPolicy.maxRetries - - for attempt in 0...maxRetries { - do { - // Execute request - let (data, response) = try await session.data(for: urlRequest) - let httpResponse = response as! HTTPURLResponse - - // Measure response time - let responseTime = CFAbsoluteTimeGetCurrent() - startTime - - // Validate response - try validateResponse(httpResponse, data: data) - - // Parse response - let parsedData = try parseResponse(data: data, type: responseType) - - let apiResponse = APIResponse( - data: parsedData, - statusCode: httpResponse.statusCode, - headers: extractHeaders(from: httpResponse), - responseTime: responseTime, - fromCache: false, - retryCount: attempt, - traceId: traceId, - rawData: data - ) - - // Cache successful response - if apiResponse.isSuccessful { - await cacheManager.cacheResponse(apiResponse, for: urlRequest) - } - - // Update metrics and circuit breaker - await updateMetrics(success: apiResponse.isSuccessful, responseTime: responseTime) - circuitBreaker.recordSuccess() - - logger.info("API request successful: \(request.endpoint) (\(responseTime)ms)") - - return apiResponse - - } catch { - lastError = error - - // Record failure - circuitBreaker.recordFailure() - - // Check if retry is appropriate - if attempt < maxRetries && shouldRetry(error: error, request: request) { - let delay = calculateRetryDelay(attempt: attempt, strategy: request.retryPolicy?.backoffStrategy ?? configuration.retryPolicy.backoffStrategy) - await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - continue - } - - break - } - } - - // All retries exhausted - await updateMetrics(success: false, responseTime: CFAbsoluteTimeGetCurrent() - startTime) - - if let error = lastError { - logger.error("API request failed: \(request.endpoint) - \(error)") - throw error - } else { - throw APIError.retryLimitExceeded - } - } - - /// Executes a GraphQL query with optimized batching and caching - public func graphQL( - _ request: GraphQLRequest, - responseType: T.Type - ) async throws -> APIResponse { - let graphQLBody: [String: Any] = [ - "query": request.query, - "variables": request.variables ?? [:], - "operationName": request.operationName as Any - ] - - let bodyData = try JSONSerialization.data(withJSONObject: graphQLBody) - - let apiRequest = APIRequest( - method: .POST, - endpoint: "/graphql", - body: bodyData, - contentType: .json, - timeout: request.timeout, - priority: request.priority - ) - - return try await self.request(apiRequest, responseType: responseType) - } - - /// Establishes and manages WebSocket connections - public func connectWebSocket( - config: WebSocketConfig, - onMessage: @escaping (Data) -> Void, - onConnect: @escaping () -> Void, - onDisconnect: @escaping (Error?) -> Void - ) async throws -> WebSocketConnection { - return try await websocketManager.connect( - config: config, - onMessage: onMessage, - onConnect: onConnect, - onDisconnect: onDisconnect - ) - } - - /// Uploads files with progress tracking and resumable uploads - public func uploadFile( - endpoint: String, - fileURL: URL, - parameters: [String: String] = [:], - progressHandler: @escaping (Double) -> Void - ) async throws -> APIResponse { - let boundary = UUID().uuidString - let contentType = "multipart/form-data; boundary=\(boundary)" - - // Create multipart body - let body = try createMultipartBody( - fileURL: fileURL, - parameters: parameters, - boundary: boundary - ) - - let request = APIRequest( - method: .POST, - endpoint: endpoint, - body: body, - contentType: ContentType(rawValue: contentType) ?? .multipartFormData, - priority: .high - ) - - // TODO: Implement progress tracking with URLSessionUploadTask - return try await self.request(request, responseType: Data.self) - } - - /// Downloads files with progress tracking and resumable downloads - public func downloadFile( - from url: URL, - to destinationURL: URL, - progressHandler: @escaping (Double) -> Void - ) async throws -> URL { - // TODO: Implement download with URLSessionDownloadTask - return destinationURL - } - - /// Executes batch requests with optimal parallelization - public func batchRequests( - _ requests: [APIRequest], - responseType: T.Type, - maxConcurrency: Int = 5 - ) async throws -> [Result, Error>] { - return await withTaskGroup(of: (Int, Result, Error>).self, returning: [Result, Error>].self) { group in - let semaphore = AsyncSemaphore(value: maxConcurrency) - - // Add tasks to group - for (index, request) in requests.enumerated() { - group.addTask { - await semaphore.wait() - defer { semaphore.signal() } - - do { - let response = try await self.request(request, responseType: responseType) - return (index, .success(response)) - } catch { - return (index, .failure(error)) - } - } - } - - // Collect results - var results: [(Int, Result, Error>)] = [] - for await result in group { - results.append(result) - } - - // Sort by original order - results.sort { $0.0 < $1.0 } - return results.map { $0.1 } - } - } - - /// Gets comprehensive API health status - public func getHealthStatus() async -> APIHealthStatus { - let latency = await measureLatency() - let networkStatus = await checkNetworkConnectivity() - - return APIHealthStatus( - isHealthy: circuitBreakerState == .closed && isNetworkAvailable, - latency: latency, - circuitBreakerState: circuitBreakerState, - networkAvailable: isNetworkAvailable, - activeConnections: activeConnections, - rateLimitStatus: await rateLimiter.getStatus(), - cacheStatus: await cacheManager.getStatus(), - metrics: metrics, - lastHealthCheck: Date() - ) - } - - /// Invalidates cache for specific endpoints or all cached data - public func invalidateCache(endpoints: [String] = []) async { - if endpoints.isEmpty { - await cacheManager.clearAll() - } else { - for endpoint in endpoints { - await cacheManager.invalidate(endpoint: endpoint) - } - } - - logger.info("Cache invalidated for \(endpoints.isEmpty ? "all endpoints" : endpoints.joined(separator: ", "))") - } - - /// Updates API configuration dynamically - public func updateConfiguration(_ newConfiguration: APIConfiguration) async { - // Update internal configuration - // Note: In a real implementation, this would update all components - logger.info("API configuration updated") - } - - /// Gets detailed API analytics and performance insights - public func getAnalytics(timeframe: TimeInterval = 3600) async -> APIAnalytics { - return await metricsCollector.generateAnalytics(for: timeframe) - } - - // MARK: - Private Methods - - private func setupAPIManager() { - // Setup network monitoring - networkMonitor.pathUpdateHandler = { [weak self] path in - DispatchQueue.main.async { - self?.isNetworkAvailable = path.status == .satisfied - } - } - networkMonitor.start(queue: queue) - - // Setup circuit breaker monitoring - circuitBreaker.onStateChange = { [weak self] state in - DispatchQueue.main.async { - self?.circuitBreakerState = state - } - } - - logger.info("API Manager initialized with base URL: \(configuration.baseURL)") - } - - private func performPreflightChecks(_ request: APIRequest) async throws { - // Network availability check - guard isNetworkAvailable else { - throw APIError.networkUnavailable - } - - // Request size check - if let body = request.body, body.count > 10 * 1024 * 1024 { // 10MB limit - throw APIError.requestTooLarge - } - - // Security validation - try securityHandler.validateRequest(request) - } - - private func buildURLRequest(from request: APIRequest, traceId: String) throws -> URLRequest { - // Build URL - guard let baseURL = URL(string: configuration.baseURL) else { - throw APIError.invalidURL - } - - let fullURL = baseURL.appendingPathComponent(request.endpoint) - - var urlRequest = URLRequest(url: fullURL) - urlRequest.httpMethod = request.method.rawValue - urlRequest.timeoutInterval = request.timeout ?? configuration.timeout - - // Add headers - var headers = configuration.headers - headers.merge(request.headers) { _, new in new } - headers["Content-Type"] = request.contentType.rawValue - headers["Accept"] = "application/json" - headers["User-Agent"] = "iOS-Widget-Development-Kit/1.0" - headers["X-Trace-ID"] = traceId - - for (key, value) in headers { - urlRequest.setValue(value, forHTTPHeaderField: key) - } - - // Add body - if let body = request.body { - urlRequest.httpBody = body - } else if let parameters = request.parameters, request.method != .GET { - urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters) - } - - // Add query parameters for GET requests - if request.method == .GET, let parameters = request.parameters { - let queryItems = parameters.map { URLQueryItem(name: $0.key, value: "\($0.value)") } - var components = URLComponents(url: fullURL, resolvingAgainstBaseURL: false) - components?.queryItems = queryItems - if let newURL = components?.url { - urlRequest.url = newURL - } - } - - // Apply cache policy - if let cachePolicy = request.cachePolicy { - urlRequest.cachePolicy = cachePolicy - } - - return urlRequest - } - - private func validateResponse(_ response: HTTPURLResponse, data: Data) throws { - let statusCode = response.statusCode - - switch statusCode { - case 200...299: - break // Success - case 401: - throw APIError.authenticationFailed - case 429: - throw APIError.rateLimitExceeded - case 500...599: - let message = String(data: data, encoding: .utf8) ?? "Unknown server error" - throw APIError.serverError(statusCode, message) - default: - let message = String(data: data, encoding: .utf8) ?? "Unknown error" - throw APIError.serverError(statusCode, message) - } - } - - private func parseResponse(data: Data, type: T.Type) throws -> T { - do { - if type == Data.self { - return data as! T - } else if type == String.self { - guard let string = String(data: data, encoding: .utf8) else { - throw APIError.serializationError - } - return string as! T - } else { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - return try decoder.decode(type, from: data) - } - } catch { - throw APIError.serializationError - } - } - - private func extractHeaders(from response: HTTPURLResponse) -> [String: String] { - var headers: [String: String] = [:] - for (key, value) in response.allHeaderFields { - if let stringKey = key as? String, let stringValue = value as? String { - headers[stringKey] = stringValue - } - } - return headers - } - - private func shouldRetry(error: Error, request: APIRequest) -> Bool { - let retryPolicy = request.retryPolicy ?? configuration.retryPolicy - - if let apiError = error as? APIError { - switch apiError { - case .timeoutError, .networkUnavailable, .serverError(let code, _): - return retryPolicy.retryableStatusCodes.contains(code) - default: - return false - } - } - - if let urlError = error as? URLError { - return retryPolicy.retryableErrors.contains(urlError.code) - } - - return false - } - - private func calculateRetryDelay(attempt: Int, strategy: APIConfiguration.RetryPolicy.BackoffStrategy) -> TimeInterval { - switch strategy { - case .linear(let interval): - return interval * TimeInterval(attempt + 1) - case .exponential(let base, let multiplier): - return base * pow(multiplier, Double(attempt)) - case .fixed(let interval): - return interval - case .custom(let calculator): - return calculator(attempt) - } - } - - private func createMultipartBody(fileURL: URL, parameters: [String: String], boundary: String) throws -> Data { - var body = Data() - - // Add parameters - for (key, value) in parameters { - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - body.append("\(value)\r\n".data(using: .utf8)!) - } - - // Add file - let fileData = try Data(contentsOf: fileURL) - let filename = fileURL.lastPathComponent - let mimeType = "application/octet-stream" - - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) - body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) - body.append(fileData) - body.append("\r\n".data(using: .utf8)!) - body.append("--\(boundary)--\r\n".data(using: .utf8)!) - - return body - } - - private func updateMetrics(success: Bool, responseTime: TimeInterval, fromCache: Bool = false) async { - await MainActor.run { - self.metrics.totalRequests += 1 - - if success { - self.metrics.successfulRequests += 1 - } else { - self.metrics.failedRequests += 1 - } - - // Update average response time - let totalTime = self.metrics.averageResponseTime * Double(self.metrics.totalRequests - 1) - self.metrics.averageResponseTime = (totalTime + responseTime) / Double(self.metrics.totalRequests) - - if fromCache { - let totalCacheRequests = Double(self.metrics.totalRequests) - let cacheHits = self.metrics.cacheHitRate * (totalCacheRequests - 1) + 1 - self.metrics.cacheHitRate = cacheHits / totalCacheRequests - } - - self.metrics.lastRequestTime = Date() - } - } - - private func measureLatency() async -> TimeInterval { - // Implement ping/latency measurement - return 0.05 // Mock 50ms latency - } - - private func checkNetworkConnectivity() async -> Bool { - return isNetworkAvailable - } -} - -// MARK: - Supporting Types and Classes - -public struct APIHealthStatus { - public let isHealthy: Bool - public let latency: TimeInterval - public let circuitBreakerState: APIManager.CircuitBreakerState - public let networkAvailable: Bool - public let activeConnections: Int - public let rateLimitStatus: RateLimitStatus - public let cacheStatus: CacheStatus - public let metrics: APIManager.APIMetrics - public let lastHealthCheck: Date -} - -public struct RateLimitStatus { - public let remainingRequests: Int - public let resetTime: Date - public let isLimited: Bool -} - -public struct CacheStatus { - public let hitRate: Double - public let size: Int - public let maxSize: Int - public let entryCount: Int -} - -public struct APIAnalytics { - public let timeframe: TimeInterval - public let requestVolume: [TimeSeriesPoint] - public let responseTimePercentiles: ResponseTimePercentiles - public let errorRateByEndpoint: [String: Double] - public let topEndpoints: [EndpointStats] - public let geographicDistribution: [String: Int] - - public struct TimeSeriesPoint { - public let timestamp: Date - public let value: Double - } - - public struct ResponseTimePercentiles { - public let p50: TimeInterval - public let p90: TimeInterval - public let p95: TimeInterval - public let p99: TimeInterval - } - - public struct EndpointStats { - public let endpoint: String - public let requestCount: Int - public let averageResponseTime: TimeInterval - public let errorRate: Double - } -} - -public protocol WebSocketConnection { - func send(_ data: Data) async throws - func send(_ text: String) async throws - func close() async - var isConnected: Bool { get } -} - -// MARK: - Private Supporting Classes - -private class RateLimiter { - private let config: APIManager.APIConfiguration.RateLimitConfig - private var requestCounts: [String: [(Date, Int)]] = [:] - private let queue = DispatchQueue(label: "rate.limiter") - - init(config: APIManager.APIConfiguration.RateLimitConfig) { - self.config = config - } - - func checkLimit(for endpoint: String) async throws { - // Implementation would check rate limits - await cleanOldRequests(for: endpoint) - - let key = config.trackByEndpoint ? endpoint : "global" - let now = Date() - let requests = requestCounts[key] ?? [] - - let recentRequests = requests.filter { now.timeIntervalSince($0.0) < 1.0 } - - if recentRequests.count >= config.requestsPerSecond { - throw APIManager.APIError.rateLimitExceeded - } - - // Record this request - await queue.async { - self.requestCounts[key, default: []].append((now, 1)) - } - } - - private func cleanOldRequests(for endpoint: String) async { - let key = config.trackByEndpoint ? endpoint : "global" - let cutoff = Date().addingTimeInterval(-60) // Keep last minute - - await queue.async { - self.requestCounts[key] = self.requestCounts[key]?.filter { $0.0 > cutoff } ?? [] - } - } - - func getStatus() async -> RateLimitStatus { - return RateLimitStatus( - remainingRequests: config.requestsPerSecond, - resetTime: Date().addingTimeInterval(1), - isLimited: false - ) - } -} - -private class CircuitBreaker { - private let config: APIManager.APIConfiguration.CircuitBreakerConfig - private var state: APIManager.CircuitBreakerState = .closed - private var failureCount = 0 - private var lastFailureTime: Date? - private var halfOpenAttempts = 0 - - var onStateChange: ((APIManager.CircuitBreakerState) -> Void)? - - init(config: APIManager.APIConfiguration.CircuitBreakerConfig) { - self.config = config - } - - func checkState() throws { - guard config.isEnabled else { return } - - switch state { - case .open: - if let lastFailure = lastFailureTime, - Date().timeIntervalSince(lastFailure) > config.recoveryTimeout { - setState(.halfOpen) - halfOpenAttempts = 0 - } else { - throw APIManager.APIError.circuitBreakerOpen - } - case .halfOpen: - if halfOpenAttempts >= config.halfOpenMaxCalls { - throw APIManager.APIError.circuitBreakerOpen - } - halfOpenAttempts += 1 - case .closed: - break - } - } - - func recordSuccess() { - guard config.isEnabled else { return } - - if state == .halfOpen { - setState(.closed) - failureCount = 0 - } - } - - func recordFailure() { - guard config.isEnabled else { return } - - failureCount += 1 - lastFailureTime = Date() - - if failureCount >= config.failureThreshold { - setState(.open) - } - } - - private func setState(_ newState: APIManager.CircuitBreakerState) { - state = newState - onStateChange?(newState) - } -} - -private class RetryHandler { - private let config: APIManager.APIConfiguration.RetryPolicy - - init(config: APIManager.APIConfiguration.RetryPolicy) { - self.config = config - } -} - -private class SecurityHandler { - private let config: APIManager.APIConfiguration.SecurityConfig - - init(config: APIManager.APIConfiguration.SecurityConfig) { - self.config = config - } - - func validateRequest(_ request: APIManager.APIRequest) throws { - // Implement security validation - } -} - -private class MetricsCollector { - func generateAnalytics(for timeframe: TimeInterval) async -> APIAnalytics { - // Generate analytics data - return APIAnalytics( - timeframe: timeframe, - requestVolume: [], - responseTimePercentiles: APIAnalytics.ResponseTimePercentiles( - p50: 0.1, - p90: 0.5, - p95: 1.0, - p99: 2.0 - ), - errorRateByEndpoint: [:], - topEndpoints: [], - geographicDistribution: [:] - ) - } -} - -private class APICacheManager { - private let config: APIManager.APIConfiguration.CacheConfig - - init(config: APIManager.APIConfiguration.CacheConfig) { - self.config = config - } - - func getCachedResponse(for request: URLRequest) async -> APIManager.APIResponse? { - // Check cache for response - return nil - } - - func cacheResponse(_ response: APIManager.APIResponse, for request: URLRequest) async { - // Cache the response - } - - func invalidate(endpoint: String) async { - // Invalidate cache for endpoint - } - - func clearAll() async { - // Clear all cached data - } - - func getStatus() async -> CacheStatus { - return CacheStatus( - hitRate: 0.75, - size: 10 * 1024 * 1024, - maxSize: config.cacheSizeLimit, - entryCount: 150 - ) - } -} - -private class RequestQueue { - // Request queue implementation for handling priority-based requests -} - -private class WebSocketManager { - func connect( - config: APIManager.WebSocketConfig, - onMessage: @escaping (Data) -> Void, - onConnect: @escaping () -> Void, - onDisconnect: @escaping (Error?) -> Void - ) async throws -> WebSocketConnection { - // WebSocket implementation - return MockWebSocketConnection() - } -} - -private class MockWebSocketConnection: WebSocketConnection { - var isConnected: Bool = true - - func send(_ data: Data) async throws { - // Mock implementation - } - - func send(_ text: String) async throws { - // Mock implementation - } - - func close() async { - isConnected = false - } -} - -private actor AsyncSemaphore { - private var value: Int - private var waiters: [CheckedContinuation] = [] - - init(value: Int) { - self.value = value - } - - func wait() async { - if value > 0 { - value -= 1 - } else { - await withCheckedContinuation { continuation in - waiters.append(continuation) - } - } - } - - func signal() { - if waiters.isEmpty { - value += 1 - } else { - let waiter = waiters.removeFirst() - waiter.resume() - } - } -} \ No newline at end of file diff --git a/Sources/Integration/CloudIntegration.swift b/Sources/Integration/CloudIntegration.swift deleted file mode 100644 index eab57e2..0000000 --- a/Sources/Integration/CloudIntegration.swift +++ /dev/null @@ -1,1396 +0,0 @@ -import Foundation -import CloudKit -import Combine -import OSLog -import CryptoKit - -/// Enterprise-grade cloud integration manager providing multi-cloud support, -/// hybrid deployment strategies, and comprehensive cloud services integration -@available(iOS 16.0, *) -public class CloudIntegration: ObservableObject { - - // MARK: - Types - - public enum CloudError: LocalizedError { - case configurationInvalid - case authenticationFailed(String) - case networkUnavailable - case cloudServiceUnavailable(String) - case syncFailed(String) - case quotaExceeded(String) - case permissionDenied - case dataCorrupted - case operationTimeout - case unsupportedOperation - case multiCloudConflict - case migrationFailed - - public var errorDescription: String? { - switch self {\ - case .configurationInvalid: - return "Cloud configuration is invalid" - case .authenticationFailed(let reason): - return "Cloud authentication failed: \(reason)" - case .networkUnavailable: - return "Network connection is unavailable" - case .cloudServiceUnavailable(let service): - return "Cloud service '\(service)' is unavailable" - case .syncFailed(let reason): - return "Cloud synchronization failed: \(reason)" - case .quotaExceeded(let quota): - return "Cloud quota exceeded: \(quota)" - case .permissionDenied: - return "Permission denied for cloud operation" - case .dataCorrupted: - return "Cloud data is corrupted" - case .operationTimeout: - return "Cloud operation timed out" - case .unsupportedOperation: - return "Operation not supported by cloud service" - case .multiCloudConflict: - return "Conflict detected between multiple cloud providers" - case .migrationFailed: - return "Cloud migration failed" - } - } - } - - public enum CloudProvider: String, CaseIterable, Codable { - case icloud = "icloud" - case aws = "aws" - case azure = "azure" - case googleCloud = "gcp" - case firebase = "firebase" - case custom = "custom" - - public var displayName: String { - switch self { - case .icloud: return "iCloud" - case .aws: return "Amazon Web Services" - case .azure: return "Microsoft Azure" - case .googleCloud: return "Google Cloud Platform" - case .firebase: return "Firebase" - case .custom: return "Custom Cloud Provider" - } - } - - public var capabilities: Set { - switch self { - case .icloud: - return [.storage, .sync, .authentication, .keyValue, .documents] - case .aws: - return [.storage, .sync, .authentication, .database, .analytics, .ml, .computing, .cdn, .messaging] - case .azure: - return [.storage, .sync, .authentication, .database, .analytics, .ml, .computing, .cdn] - case .googleCloud: - return [.storage, .sync, .authentication, .database, .analytics, .ml, .computing, .cdn] - case .firebase: - return [.storage, .sync, .authentication, .database, .analytics, .messaging, .hosting] - case .custom: - return Set(CloudCapability.allCases) - } - } - } - - public enum CloudCapability: String, CaseIterable, Codable { - case storage = "storage" - case sync = "sync" - case authentication = "authentication" - case database = "database" - case analytics = "analytics" - case ml = "machine_learning" - case computing = "computing" - case cdn = "content_delivery" - case messaging = "messaging" - case keyValue = "key_value" - case documents = "documents" - case hosting = "hosting" - - public var description: String { - switch self { - case .storage: return "File and blob storage" - case .sync: return "Real-time synchronization" - case .authentication: return "User authentication and authorization" - case .database: return "Database services" - case .analytics: return "Analytics and monitoring" - case .ml: return "Machine learning services" - case .computing: return "Serverless computing" - case .cdn: return "Content delivery network" - case .messaging: return "Push notifications and messaging" - case .keyValue: return "Key-value storage" - case .documents: return "Document storage and management" - case .hosting: return "Web hosting and deployment" - } - } - } - - public enum SyncStrategy: String, CaseIterable, Codable { - case primary = "primary" - case backup = "backup" - case multiCloud = "multi_cloud" - case hybrid = "hybrid" - case failover = "failover" - - public var description: String { - switch self { - case .primary: return "Single primary cloud provider" - case .backup: return "Primary with backup cloud provider" - case .multiCloud: return "Multiple active cloud providers" - case .hybrid: return "On-premise and cloud hybrid" - case .failover: return "Automatic failover between providers" - } - } - } - - public struct CloudConfiguration { - public var primaryProvider: CloudProvider = .icloud - public var secondaryProviders: [CloudProvider] = [] - public var syncStrategy: SyncStrategy = .primary - public var enableAutoSync: Bool = true - public var syncInterval: TimeInterval = 300 // 5 minutes - public var enableCompression: Bool = true - public var enableEncryption: Bool = true - public var enableConflictResolution: Bool = true - public var maxRetryAttempts: Int = 3 - public var operationTimeout: TimeInterval = 30 - public var enableOfflineMode: Bool = true - public var enableMetrics: Bool = true - public var enableCaching: Bool = true - public var compressionThreshold: Int = 1024 // 1KB - - // Provider-specific configurations - public var icloudContainerIdentifier: String = "" - public var awsRegion: String = "us-east-1" - public var awsAccessKeyId: String = "" - public var awsSecretAccessKey: String = "" - public var azureConnectionString: String = "" - public var gcpProjectId: String = "" - public var gcpCredentialsPath: String = "" - public var firebaseProjectId: String = "" - public var customEndpoint: String = "" - public var customApiKey: String = "" - - public init() {} - } - - public struct CloudOperation: Identifiable, Codable { - public let id: String - public let type: OperationType - public let provider: CloudProvider - public let path: String - public let timestamp: Date - public let status: OperationStatus - public let metadata: [String: String] - public let retryCount: Int - public let error: String? - - public enum OperationType: String, Codable { - case upload = "upload" - case download = "download" - case sync = "sync" - case delete = "delete" - case query = "query" - case authenticate = "authenticate" - case backup = "backup" - case restore = "restore" - case migrate = "migrate" - } - - public enum OperationStatus: String, Codable { - case pending = "pending" - case inProgress = "in_progress" - case completed = "completed" - case failed = "failed" - case cancelled = "cancelled" - case retrying = "retrying" - } - - public init( - type: OperationType, - provider: CloudProvider, - path: String, - metadata: [String: String] = [:] - ) { - self.id = UUID().uuidString - self.type = type - self.provider = provider - self.path = path - self.timestamp = Date() - self.status = .pending - self.metadata = metadata - self.retryCount = 0 - self.error = nil - } - } - - public struct CloudFile: Codable, Identifiable { - public let id: String - public let name: String - public let path: String - public let size: Int64 - public let contentType: String - public let checksum: String - public let createdAt: Date - public let modifiedAt: Date - public let provider: CloudProvider - public let isEncrypted: Bool - public let metadata: [String: String] - public let tags: [String] - - public var formattedSize: String { - ByteCountFormatter.string(fromByteCount: size, countStyle: .file) - } - - public init( - name: String, - path: String, - size: Int64, - contentType: String, - checksum: String, - provider: CloudProvider, - isEncrypted: Bool = false, - metadata: [String: String] = [:], - tags: [String] = [] - ) { - self.id = UUID().uuidString - self.name = name - self.path = path - self.size = size - self.contentType = contentType - self.checksum = checksum - self.createdAt = Date() - self.modifiedAt = Date() - self.provider = provider - self.isEncrypted = isEncrypted - self.metadata = metadata - self.tags = tags - } - } - - public struct SyncStatus: Codable { - public let provider: CloudProvider - public let lastSyncTime: Date - public let filesInSync: Int - public let pendingOperations: Int - public let conflicts: Int - public let quota: CloudQuota - public let isOnline: Bool - public let healthScore: Double - - public struct CloudQuota: Codable { - public let used: Int64 - public let total: Int64 - public let available: Int64 - - public var usagePercentage: Double { - guard total > 0 else { return 0 } - return Double(used) / Double(total) * 100 - } - } - } - - public struct ConflictResolution: Codable { - public let conflictId: String - public let localFile: CloudFile - public let remoteFile: CloudFile - public let resolutionStrategy: ResolutionStrategy - public let resolvedAt: Date? - - public enum ResolutionStrategy: String, Codable { - case keepLocal = "keep_local" - case keepRemote = "keep_remote" - case merge = "merge" - case createCopy = "create_copy" - case manual = "manual" - } - } - - // MARK: - Properties - - public static let shared = CloudIntegration() - - private let icloudService: iCloudService - private let awsService: AWSService - private let azureService: AzureService - private let gcpService: GCPService - private let firebaseService: FirebaseService - private let customService: CustomCloudService - private let conflictResolver: ConflictResolver - private let migrationManager: CloudMigrationManager - private let metricsCollector: CloudMetricsCollector - private let cacheManager: CacheManager - - @Published public private(set) var configuration: CloudConfiguration = CloudConfiguration() - @Published public private(set) var isConnected: Bool = false - @Published public private(set) var activeOperations: [CloudOperation] = [] - @Published public private(set) var syncStatuses: [CloudProvider: SyncStatus] = [:] - @Published public private(set) var conflicts: [ConflictResolution] = [] - @Published public private(set) var availableProviders: Set = [] - @Published public private(set) var offlineQueue: [CloudOperation] = [] - - private var syncTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "cloud.integration", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "CloudIntegration") - - // MARK: - Initialization - - private init() { - self.icloudService = iCloudService() - self.awsService = AWSService() - self.azureService = AzureService() - self.gcpService = GCPService() - self.firebaseService = FirebaseService() - self.customService = CustomCloudService() - self.conflictResolver = ConflictResolver() - self.migrationManager = CloudMigrationManager() - self.metricsCollector = CloudMetricsCollector() - self.cacheManager = CacheManager.shared - - setupCloudIntegration() - } - - // MARK: - Public Methods - - /// Initializes cloud integration with configuration - public func initialize(configuration: CloudConfiguration) async throws { - guard isValidConfiguration(configuration) else { - throw CloudError.configurationInvalid - } - - await MainActor.run { - self.configuration = configuration - } - - // Initialize cloud services - try await initializeServices() - - // Setup automatic sync if enabled - if configuration.enableAutoSync { - setupAutomaticSync() - } - - // Detect available providers - await detectAvailableProviders() - - await MainActor.run { - self.isConnected = true - } - - logger.info("Cloud integration initialized with primary provider: \(configuration.primaryProvider.rawValue)") - } - - /// Uploads data to cloud storage - public func upload( - data: Data, - to path: String, - provider: CloudProvider? = nil, - metadata: [String: String] = [:] - ) async throws -> CloudFile { - let targetProvider = provider ?? configuration.primaryProvider - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - let operation = CloudOperation( - type: .upload, - provider: targetProvider, - path: path, - metadata: metadata - ) - - await addToActiveOperations(operation) - - do { - // Encrypt data if enabled - let processedData = configuration.enableEncryption ? try encryptData(data) : data - - // Compress data if enabled and above threshold - let finalData = shouldCompress(data) ? try compressData(processedData) : processedData - - let cloudFile = try await uploadToProvider( - data: finalData, - path: path, - provider: targetProvider, - metadata: metadata - ) - - // Cache locally if enabled - if configuration.enableCaching { - await cacheFile(cloudFile, data: data) - } - - // Update sync status - await updateSyncStatus(for: targetProvider) - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Successfully uploaded file to \(targetProvider.rawValue): \(path)") - return cloudFile - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - - // Add to offline queue if network issue - if error is URLError || error is CloudError { - await addToOfflineQueue(operation) - } - - throw error - } - } - - /// Downloads data from cloud storage - public func download( - path: String, - provider: CloudProvider? = nil - ) async throws -> Data { - let targetProvider = provider ?? configuration.primaryProvider - - // Try cache first if enabled - if configuration.enableCaching, - let cachedData = await getCachedFile(path: path, provider: targetProvider) { - return cachedData - } - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - let operation = CloudOperation( - type: .download, - provider: targetProvider, - path: path - ) - - await addToActiveOperations(operation) - - do { - let data = try await downloadFromProvider(path: path, provider: targetProvider) - - // Decrypt data if encrypted - let finalData = configuration.enableEncryption ? try decryptData(data) : data - - // Cache for future use if enabled - if configuration.enableCaching { - await cacheData(finalData, path: path, provider: targetProvider) - } - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Successfully downloaded file from \(targetProvider.rawValue): \(path)") - return finalData - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Synchronizes data across all configured providers - public func synchronize(paths: [String] = []) async throws { - guard isConnected else { - throw CloudError.networkUnavailable - } - - let operation = CloudOperation( - type: .sync, - provider: configuration.primaryProvider, - path: paths.isEmpty ? "/" : paths.joined(separator: ",") - ) - - await addToActiveOperations(operation) - - do { - // Primary synchronization - try await synchronizeWithProvider(configuration.primaryProvider, paths: paths) - - // Secondary provider synchronization - for provider in configuration.secondaryProviders { - if availableProviders.contains(provider) { - try await synchronizeWithProvider(provider, paths: paths) - } - } - - // Resolve conflicts if any - if configuration.enableConflictResolution { - await resolveConflicts() - } - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Synchronization completed across \(availableProviders.count) providers") - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Deletes file from cloud storage - public func delete( - path: String, - provider: CloudProvider? = nil - ) async throws { - let targetProvider = provider ?? configuration.primaryProvider - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - let operation = CloudOperation( - type: .delete, - provider: targetProvider, - path: path - ) - - await addToActiveOperations(operation) - - do { - try await deleteFromProvider(path: path, provider: targetProvider) - - // Remove from cache - await removeCachedFile(path: path, provider: targetProvider) - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Successfully deleted file from \(targetProvider.rawValue): \(path)") - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Lists files in cloud storage - public func listFiles( - path: String = "/", - provider: CloudProvider? = nil, - recursive: Bool = false - ) async throws -> [CloudFile] { - let targetProvider = provider ?? configuration.primaryProvider - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - return try await listFilesFromProvider(path: path, provider: targetProvider, recursive: recursive) - } - - /// Migrates data between cloud providers - public func migrate( - from sourceProvider: CloudProvider, - to targetProvider: CloudProvider, - paths: [String] = [] - ) async throws { - let operation = CloudOperation( - type: .migrate, - provider: sourceProvider, - path: "migrate_to_\(targetProvider.rawValue)", - metadata: ["target_provider": targetProvider.rawValue] - ) - - await addToActiveOperations(operation) - - do { - try await migrationManager.migrate( - from: sourceProvider, - to: targetProvider, - paths: paths, - cloudIntegration: self - ) - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Migration completed from \(sourceProvider.rawValue) to \(targetProvider.rawValue)") - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Gets synchronization status for all providers - public func getSyncStatus() async -> [CloudProvider: SyncStatus] { - var statuses: [CloudProvider: SyncStatus] = [:] - - for provider in availableProviders { - if let status = await getSyncStatusForProvider(provider) { - statuses[provider] = status - } - } - - await MainActor.run { - self.syncStatuses = statuses - } - - return statuses - } - - /// Resolves a specific conflict - public func resolveConflict( - conflictId: String, - strategy: ConflictResolution.ResolutionStrategy - ) async throws { - guard let conflictIndex = conflicts.firstIndex(where: { $0.conflictId == conflictId }) else { - throw CloudError.dataCorrupted - } - - var conflict = conflicts[conflictIndex] - - try await conflictResolver.resolve( - conflict: conflict, - strategy: strategy, - cloudIntegration: self - ) - - conflict = ConflictResolution( - conflictId: conflict.conflictId, - localFile: conflict.localFile, - remoteFile: conflict.remoteFile, - resolutionStrategy: strategy, - resolvedAt: Date() - ) - - await MainActor.run { - self.conflicts[conflictIndex] = conflict - } - - logger.info("Conflict resolved: \(conflictId) using strategy: \(strategy.rawValue)") - } - - /// Updates cloud configuration - public func updateConfiguration(_ newConfiguration: CloudConfiguration) async throws { - guard isValidConfiguration(newConfiguration) else { - throw CloudError.configurationInvalid - } - - let wasConnected = isConnected - - if wasConnected { - await disconnect() - } - - await MainActor.run { - self.configuration = newConfiguration - } - - if wasConnected { - try await initialize(configuration: newConfiguration) - } - - logger.info("Cloud configuration updated") - } - - /// Disconnects from cloud services - public func disconnect() async { - await MainActor.run { - self.isConnected = false - } - - syncTimer?.invalidate() - syncTimer = nil - - // Cancel all active operations - await clearActiveOperations() - - // Disconnect from all services - await disconnectFromAllServices() - - logger.info("Disconnected from cloud services") - } - - /// Processes offline queue when connection is restored - public func processOfflineQueue() async { - guard isConnected else { return } - - let queueCopy = offlineQueue - await clearOfflineQueue() - - for operation in queueCopy { - do { - switch operation.type { - case .upload: - // Retry upload operation - logger.info("Retrying offline upload: \(operation.path)") - case .delete: - // Retry delete operation - logger.info("Retrying offline delete: \(operation.path)") - case .sync: - // Retry sync operation - try await synchronize() - default: - break - } - } catch { - logger.error("Failed to process offline operation: \(error)") - } - } - - logger.info("Processed \(queueCopy.count) offline operations") - } - - /// Gets cloud integration metrics and insights - public func getCloudInsights() async -> CloudInsights { - let totalOperations = activeOperations.count + (syncStatuses.values.map { $0.pendingOperations }.reduce(0, +)) - let totalConflicts = conflicts.count - let totalStorageUsed = syncStatuses.values.map { $0.quota.used }.reduce(0, +) - let averageHealthScore = syncStatuses.values.map { $0.healthScore }.reduce(0, +) / Double(max(1, syncStatuses.count)) - - return CloudInsights( - connectedProviders: availableProviders.count, - totalOperations: totalOperations, - pendingOperations: activeOperations.filter { $0.status == .pending }.count, - totalConflicts: totalConflicts, - storageUsed: totalStorageUsed, - averageHealthScore: averageHealthScore, - offlineOperations: offlineQueue.count, - lastSyncTime: syncStatuses.values.map { $0.lastSyncTime }.max(), - syncStrategy: configuration.syncStrategy - ) - } - - // MARK: - Private Methods - - private func setupCloudIntegration() { - // Setup network monitoring - setupNetworkMonitoring() - - // Setup notification observers - setupNotificationObservers() - - logger.info("Cloud integration initialized") - } - - private func setupNetworkMonitoring() { - // Monitor network connectivity for automatic sync resumption - NotificationCenter.default.publisher(for: .NSSystemClockDidChange) - .sink { [weak self] _ in - Task { await self?.handleNetworkChange() } - } - .store(in: &cancellables) - } - - private func setupNotificationObservers() { - NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) - .sink { [weak self] _ in - Task { await self?.processOfflineQueue() } - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - } - - private func initializeServices() async throws { - for provider in [configuration.primaryProvider] + configuration.secondaryProviders { - try await initializeProvider(provider) - } - } - - private func initializeProvider(_ provider: CloudProvider) async throws { - switch provider { - case .icloud: - try await icloudService.initialize(containerIdentifier: configuration.icloudContainerIdentifier) - case .aws: - try await awsService.initialize( - region: configuration.awsRegion, - accessKeyId: configuration.awsAccessKeyId, - secretAccessKey: configuration.awsSecretAccessKey - ) - case .azure: - try await azureService.initialize(connectionString: configuration.azureConnectionString) - case .googleCloud: - try await gcpService.initialize( - projectId: configuration.gcpProjectId, - credentialsPath: configuration.gcpCredentialsPath - ) - case .firebase: - try await firebaseService.initialize(projectId: configuration.firebaseProjectId) - case .custom: - try await customService.initialize( - endpoint: configuration.customEndpoint, - apiKey: configuration.customApiKey - ) - } - } - - private func setupAutomaticSync() { - syncTimer = Timer.scheduledTimer(withTimeInterval: configuration.syncInterval, repeats: true) { [weak self] _ in - Task { - try? await self?.synchronize() - } - } - } - - private func detectAvailableProviders() async { - var providers: Set = [] - - for provider in CloudProvider.allCases { - if await isProviderAvailable(provider) { - providers.insert(provider) - } - } - - await MainActor.run { - self.availableProviders = providers - } - } - - private func isProviderAvailable(_ provider: CloudProvider) async -> Bool { - switch provider { - case .icloud: - return await icloudService.isAvailable() - case .aws: - return await awsService.isAvailable() - case .azure: - return await azureService.isAvailable() - case .googleCloud: - return await gcpService.isAvailable() - case .firebase: - return await firebaseService.isAvailable() - case .custom: - return await customService.isAvailable() - } - } - - private func uploadToProvider( - data: Data, - path: String, - provider: CloudProvider, - metadata: [String: String] - ) async throws -> CloudFile { - switch provider { - case .icloud: - return try await icloudService.upload(data: data, path: path, metadata: metadata) - case .aws: - return try await awsService.upload(data: data, path: path, metadata: metadata) - case .azure: - return try await azureService.upload(data: data, path: path, metadata: metadata) - case .googleCloud: - return try await gcpService.upload(data: data, path: path, metadata: metadata) - case .firebase: - return try await firebaseService.upload(data: data, path: path, metadata: metadata) - case .custom: - return try await customService.upload(data: data, path: path, metadata: metadata) - } - } - - private func downloadFromProvider(path: String, provider: CloudProvider) async throws -> Data { - switch provider { - case .icloud: - return try await icloudService.download(path: path) - case .aws: - return try await awsService.download(path: path) - case .azure: - return try await azureService.download(path: path) - case .googleCloud: - return try await gcpService.download(path: path) - case .firebase: - return try await firebaseService.download(path: path) - case .custom: - return try await customService.download(path: path) - } - } - - private func deleteFromProvider(path: String, provider: CloudProvider) async throws { - switch provider { - case .icloud: - try await icloudService.delete(path: path) - case .aws: - try await awsService.delete(path: path) - case .azure: - try await azureService.delete(path: path) - case .googleCloud: - try await gcpService.delete(path: path) - case .firebase: - try await firebaseService.delete(path: path) - case .custom: - try await customService.delete(path: path) - } - } - - private func listFilesFromProvider(path: String, provider: CloudProvider, recursive: Bool) async throws -> [CloudFile] { - switch provider { - case .icloud: - return try await icloudService.listFiles(path: path, recursive: recursive) - case .aws: - return try await awsService.listFiles(path: path, recursive: recursive) - case .azure: - return try await azureService.listFiles(path: path, recursive: recursive) - case .googleCloud: - return try await gcpService.listFiles(path: path, recursive: recursive) - case .firebase: - return try await firebaseService.listFiles(path: path, recursive: recursive) - case .custom: - return try await customService.listFiles(path: path, recursive: recursive) - } - } - - private func synchronizeWithProvider(_ provider: CloudProvider, paths: [String]) async throws { - // Implementation would perform actual synchronization - logger.info("Synchronizing with \(provider.rawValue)") - } - - private func getSyncStatusForProvider(_ provider: CloudProvider) async -> SyncStatus? { - // Implementation would get actual sync status - return SyncStatus( - provider: provider, - lastSyncTime: Date(), - filesInSync: 100, - pendingOperations: 0, - conflicts: 0, - quota: SyncStatus.CloudQuota( - used: 1_000_000_000, // 1GB - total: 5_000_000_000, // 5GB - available: 4_000_000_000 // 4GB - ), - isOnline: true, - healthScore: 0.95 - ) - } - - private func resolveConflicts() async { - // Implementation would resolve conflicts automatically - } - - private func disconnectFromAllServices() async { - await icloudService.disconnect() - await awsService.disconnect() - await azureService.disconnect() - await gcpService.disconnect() - await firebaseService.disconnect() - await customService.disconnect() - } - - private func encryptData(_ data: Data) throws -> Data { - let key = SymmetricKey(size: .bits256) - let encryptedData = try AES.GCM.seal(data, using: key) - return encryptedData.combined ?? data - } - - private func decryptData(_ data: Data) throws -> Data { - // Implementation would decrypt data - return data - } - - private func compressData(_ data: Data) throws -> Data { - // Implementation would compress data - return data - } - - private func shouldCompress(_ data: Data) -> Bool { - return configuration.enableCompression && data.count > configuration.compressionThreshold - } - - private func cacheFile(_ file: CloudFile, data: Data) async { - let key = "cloud_file_\(file.provider.rawValue)_\(file.path)" - try? await cacheManager.store(data, forKey: key) - } - - private func getCachedFile(path: String, provider: CloudProvider) async -> Data? { - let key = "cloud_file_\(provider.rawValue)_\(path)" - return try? await cacheManager.retrieve(Data.self, forKey: key) - } - - private func cacheData(_ data: Data, path: String, provider: CloudProvider) async { - let key = "cloud_file_\(provider.rawValue)_\(path)" - try? await cacheManager.store(data, forKey: key) - } - - private func removeCachedFile(path: String, provider: CloudProvider) async { - let key = "cloud_file_\(provider.rawValue)_\(path)" - await cacheManager.removeObject(forKey: key) - } - - private func addToActiveOperations(_ operation: CloudOperation) async { - await MainActor.run { - self.activeOperations.append(operation) - } - } - - private func updateOperationStatus(_ id: String, status: CloudOperation.OperationStatus, error: String? = nil) async { - await MainActor.run { - if let index = self.activeOperations.firstIndex(where: { $0.id == id }) { - var operation = self.activeOperations[index] - operation = CloudOperation( - type: operation.type, - provider: operation.provider, - path: operation.path, - metadata: operation.metadata - ) - self.activeOperations[index] = operation - - if status == .completed || status == .failed { - self.activeOperations.remove(at: index) - } - } - } - } - - private func clearActiveOperations() async { - await MainActor.run { - self.activeOperations.removeAll() - } - } - - private func addToOfflineQueue(_ operation: CloudOperation) async { - await MainActor.run { - self.offlineQueue.append(operation) - } - } - - private func clearOfflineQueue() async { - await MainActor.run { - self.offlineQueue.removeAll() - } - } - - private func updateSyncStatus(for provider: CloudProvider) async { - // Implementation would update sync status - } - - private func isValidConfiguration(_ config: CloudConfiguration) -> Bool { - return config.syncInterval > 0 && - config.maxRetryAttempts > 0 && - config.operationTimeout > 0 - } - - private func handleNetworkChange() async { - if isConnected { - await processOfflineQueue() - } - } - - private func handleAppDidEnterBackground() async { - // Save current state for background operation - if configuration.enableAutoSync { - try? await synchronize() - } - } -} - -// MARK: - Supporting Types - -public struct CloudInsights { - public let connectedProviders: Int - public let totalOperations: Int - public let pendingOperations: Int - public let totalConflicts: Int - public let storageUsed: Int64 - public let averageHealthScore: Double - public let offlineOperations: Int - public let lastSyncTime: Date? - public let syncStrategy: CloudIntegration.SyncStrategy -} - -// MARK: - Cloud Service Protocols and Implementations - -private protocol CloudService { - func initialize() async throws - func isAvailable() async -> Bool - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile - func download(path: String) async throws -> Data - func delete(path: String) async throws - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] - func disconnect() async -} - -// MARK: - Cloud Service Implementations - -private class iCloudService: CloudService { - private var container: CKContainer? - - func initialize(containerIdentifier: String = "") async throws { - let identifier = containerIdentifier.isEmpty ? CKContainer.default().containerIdentifier : containerIdentifier - self.container = CKContainer(identifier: identifier!) - } - - func isAvailable() async -> Bool { - guard let container = container else { return false } - - do { - let status = try await container.accountStatus() - return status == .available - } catch { - return false - } - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to iCloud - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .icloud - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from iCloud - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from iCloud - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list iCloud files - return [] - } - - func disconnect() async { - container = nil - } -} - -private class AWSService: CloudService { - func initialize(region: String, accessKeyId: String, secretAccessKey: String) async throws { - // Implementation would initialize AWS SDK - } - - func isAvailable() async -> Bool { - // Implementation would check AWS availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to S3 - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .aws - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from S3 - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from S3 - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list S3 files - return [] - } - - func disconnect() async { - // Implementation would disconnect from AWS - } -} - -private class AzureService: CloudService { - func initialize(connectionString: String) async throws { - // Implementation would initialize Azure SDK - } - - func isAvailable() async -> Bool { - // Implementation would check Azure availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to Azure Blob Storage - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .azure - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from Azure - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from Azure - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list Azure files - return [] - } - - func disconnect() async { - // Implementation would disconnect from Azure - } -} - -private class GCPService: CloudService { - func initialize(projectId: String, credentialsPath: String) async throws { - // Implementation would initialize GCP SDK - } - - func isAvailable() async -> Bool { - // Implementation would check GCP availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to Google Cloud Storage - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .googleCloud - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from GCP - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from GCP - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list GCP files - return [] - } - - func disconnect() async { - // Implementation would disconnect from GCP - } -} - -private class FirebaseService: CloudService { - func initialize(projectId: String) async throws { - // Implementation would initialize Firebase SDK - } - - func isAvailable() async -> Bool { - // Implementation would check Firebase availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to Firebase Storage - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .firebase - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from Firebase - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from Firebase - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list Firebase files - return [] - } - - func disconnect() async { - // Implementation would disconnect from Firebase - } -} - -private class CustomCloudService: CloudService { - func initialize(endpoint: String, apiKey: String) async throws { - // Implementation would initialize custom cloud service - } - - func isAvailable() async -> Bool { - // Implementation would check custom service availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to custom service - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .custom - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from custom service - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from custom service - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list custom service files - return [] - } - - func disconnect() async { - // Implementation would disconnect from custom service - } -} - -// MARK: - Supporting Classes - -private class ConflictResolver { - func resolve( - conflict: CloudIntegration.ConflictResolution, - strategy: CloudIntegration.ConflictResolution.ResolutionStrategy, - cloudIntegration: CloudIntegration - ) async throws { - // Implementation would resolve conflicts - } -} - -private class CloudMigrationManager { - func migrate( - from sourceProvider: CloudIntegration.CloudProvider, - to targetProvider: CloudIntegration.CloudProvider, - paths: [String], - cloudIntegration: CloudIntegration - ) async throws { - // Implementation would migrate data between providers - } -} - -private class CloudMetricsCollector { - func recordMetric(_ metric: String, value: Double) async { - // Implementation would record metrics - } -} - -// MARK: - Extensions - -extension Data { - var sha256: String { - let digest = SHA256.hash(data: self) - return digest.compactMap { String(format: "%02x", $0) }.joined() - } -} \ No newline at end of file diff --git a/Sources/Integration/DataSourceManager.swift b/Sources/Integration/DataSourceManager.swift deleted file mode 100644 index 05c21e7..0000000 --- a/Sources/Integration/DataSourceManager.swift +++ /dev/null @@ -1,1281 +0,0 @@ -import Foundation -import Combine -import OSLog -import CoreData -import SQLite3 - -/// Enterprise-grade data source manager providing unified access to multiple data sources, -/// real-time streaming, intelligent caching, and comprehensive data orchestration -@available(iOS 16.0, *) -public class DataSourceManager: ObservableObject { - - // MARK: - Types - - public enum DataSourceError: LocalizedError { - case configurationInvalid - case dataSourceUnavailable(String) - case connectionFailed(String) - case queryFailed(String) - case transformationFailed(String) - case validationFailed(String) - case permissionDenied - case quotaExceeded - case dataCorrupted - case unsupportedOperation - case streamingFailed(String) - case cacheError(String) - case migrationFailed - - public var errorDescription: String? { - switch self { - case .configurationInvalid: - return "Data source configuration is invalid" - case .dataSourceUnavailable(let source): - return "Data source '\(source)' is unavailable" - case .connectionFailed(let reason): - return "Connection failed: \(reason)" - case .queryFailed(let reason): - return "Query failed: \(reason)" - case .transformationFailed(let reason): - return "Data transformation failed: \(reason)" - case .validationFailed(let reason): - return "Data validation failed: \(reason)" - case .permissionDenied: - return "Permission denied for data source operation" - case .quotaExceeded: - return "Data source quota exceeded" - case .dataCorrupted: - return "Data is corrupted or invalid" - case .unsupportedOperation: - return "Operation not supported by data source" - case .streamingFailed(let reason): - return "Data streaming failed: \(reason)" - case .cacheError(let reason): - return "Cache operation failed: \(reason)" - case .migrationFailed: - return "Data migration failed" - } - } - } - - public enum DataSourceType: String, CaseIterable, Codable { - case rest = "rest" - case graphql = "graphql" - case websocket = "websocket" - case database = "database" - case file = "file" - case cloud = "cloud" - case stream = "stream" - case mqtt = "mqtt" - case rss = "rss" - case custom = "custom" - - public var displayName: String { - switch self { - case .rest: return "REST API" - case .graphql: return "GraphQL API" - case .websocket: return "WebSocket Stream" - case .database: return "Database" - case .file: return "File System" - case .cloud: return "Cloud Storage" - case .stream: return "Data Stream" - case .mqtt: return "MQTT Broker" - case .rss: return "RSS Feed" - case .custom: return "Custom Data Source" - } - } - - public var capabilities: Set { - switch self { - case .rest: - return [.read, .write, .query, .pagination, .filtering, .caching] - case .graphql: - return [.read, .write, .query, .subscription, .filtering, .caching, .realtime] - case .websocket: - return [.read, .write, .realtime, .streaming, .bidirectional] - case .database: - return [.read, .write, .query, .transaction, .indexing, .caching, .persistence] - case .file: - return [.read, .write, .persistence, .caching, .compression] - case .cloud: - return [.read, .write, .sync, .caching, .persistence, .compression] - case .stream: - return [.read, .realtime, .streaming, .filtering, .transformation] - case .mqtt: - return [.read, .write, .realtime, .messaging, .subscription] - case .rss: - return [.read, .polling, .caching, .filtering] - case .custom: - return Set(DataCapability.allCases) - } - } - } - - public enum DataCapability: String, CaseIterable, Codable { - case read = "read" - case write = "write" - case query = "query" - case subscription = "subscription" - case realtime = "realtime" - case streaming = "streaming" - case pagination = "pagination" - case filtering = "filtering" - case caching = "caching" - case persistence = "persistence" - case transaction = "transaction" - case indexing = "indexing" - case compression = "compression" - case sync = "sync" - case bidirectional = "bidirectional" - case messaging = "messaging" - case polling = "polling" - case transformation = "transformation" - - public var description: String { - switch self { - case .read: return "Data reading operations" - case .write: return "Data writing operations" - case .query: return "Advanced query capabilities" - case .subscription: return "Real-time subscriptions" - case .realtime: return "Real-time data updates" - case .streaming: return "Continuous data streaming" - case .pagination: return "Paginated data retrieval" - case .filtering: return "Data filtering and search" - case .caching: return "Intelligent data caching" - case .persistence: return "Data persistence" - case .transaction: return "Transactional operations" - case .indexing: return "Data indexing for performance" - case .compression: return "Data compression" - case .sync: return "Data synchronization" - case .bidirectional: return "Bidirectional communication" - case .messaging: return "Message-based communication" - case .polling: return "Periodic data polling" - case .transformation: return "Data transformation" - } - } - } - - public struct DataSourceConfiguration { - public var name: String - public var type: DataSourceType - public var endpoint: String - public var authentication: AuthenticationConfig - public var enableCaching: Bool = true - public var enableRetries: Bool = true - public var maxRetryAttempts: Int = 3 - public var timeout: TimeInterval = 30 - public var enableCompression: Bool = true - public var enableEncryption: Bool = false - public var enableRateLimiting: Bool = true - public var rateLimit: RateLimit = RateLimit() - public var enableMetrics: Bool = true - public var enableValidation: Bool = true - public var transformationRules: [String: String] = [:] - public var headers: [String: String] = [:] - public var queryParameters: [String: String] = [:] - - public struct AuthenticationConfig { - public var type: AuthType = .none - public var apiKey: String = "" - public var bearerToken: String = "" - public var username: String = "" - public var password: String = "" - public var clientId: String = "" - public var clientSecret: String = "" - public var customHeaders: [String: String] = [:] - - public enum AuthType: String, CaseIterable { - case none = "none" - case apiKey = "api_key" - case bearer = "bearer" - case basic = "basic" - case oauth2 = "oauth2" - case custom = "custom" - } - } - - public struct RateLimit { - public var requestsPerSecond: Int = 10 - public var requestsPerMinute: Int = 600 - public var requestsPerHour: Int = 36000 - public var burstSize: Int = 20 - } - - public init(name: String, type: DataSourceType, endpoint: String) { - self.name = name - self.type = type - self.endpoint = endpoint - self.authentication = AuthenticationConfig() - } - } - - public struct DataQuery: Codable { - public let id: String - public let dataSourceName: String - public let query: String - public let parameters: [String: Any] - public let filters: [DataFilter] - public let pagination: PaginationOptions? - public let sortOptions: [SortOption] - public let enableCache: Bool - public let cacheTTL: TimeInterval? - public let priority: Priority - public let timeout: TimeInterval? - - public enum Priority: String, CaseIterable, Codable { - case low = "low" - case normal = "normal" - case high = "high" - case critical = "critical" - - public var value: Int { - switch self { - case .low: return 1 - case .normal: return 2 - case .high: return 3 - case .critical: return 4 - } - } - } - - public struct DataFilter: Codable { - public let field: String - public let operator: FilterOperator - public let value: String - public let caseSensitive: Bool - - public enum FilterOperator: String, CaseIterable, Codable { - case equals = "eq" - case notEquals = "ne" - case greaterThan = "gt" - case greaterThanOrEqual = "gte" - case lessThan = "lt" - case lessThanOrEqual = "lte" - case contains = "contains" - case startsWith = "starts_with" - case endsWith = "ends_with" - case regex = "regex" - case `in` = "in" - case notIn = "not_in" - } - } - - public struct PaginationOptions: Codable { - public let page: Int - public let pageSize: Int - public let offset: Int? - public let limit: Int? - public let cursor: String? - } - - public struct SortOption: Codable { - public let field: String - public let ascending: Bool - } - - public init( - dataSourceName: String, - query: String, - parameters: [String: Any] = [:], - filters: [DataFilter] = [], - pagination: PaginationOptions? = nil, - priority: Priority = .normal - ) { - self.id = UUID().uuidString - self.dataSourceName = dataSourceName - self.query = query - self.parameters = parameters - self.filters = filters - self.pagination = pagination - self.sortOptions = [] - self.enableCache = true - self.cacheTTL = nil - self.priority = priority - self.timeout = nil - } - } - - public struct DataResponse: Codable { - public let queryId: String - public let data: T? - public let metadata: ResponseMetadata - public let errors: [DataError] - public let cached: Bool - public let timestamp: Date - - public struct ResponseMetadata: Codable { - public let totalCount: Int? - public let pageCount: Int? - public let currentPage: Int? - public let hasNextPage: Bool - public let hasPreviousPage: Bool - public let executionTime: TimeInterval - public let dataSourceName: String - public let version: String - } - - public struct DataError: Codable { - public let code: String - public let message: String - public let field: String? - public let severity: Severity - - public enum Severity: String, Codable { - case info = "info" - case warning = "warning" - case error = "error" - case critical = "critical" - } - } - - public var isSuccess: Bool { - return data != nil && errors.isEmpty - } - } - - public struct StreamingData: Identifiable { - public let id: String - public let data: T - public let timestamp: Date - public let sourceId: String - public let metadata: [String: String] - - public init(data: T, sourceId: String, metadata: [String: String] = [:]) { - self.id = UUID().uuidString - self.data = data - self.timestamp = Date() - self.sourceId = sourceId - self.metadata = metadata - } - } - - // MARK: - Properties - - public static let shared = DataSourceManager() - - private let apiManager: APIManager - private let cloudIntegration: CloudIntegration - private let cacheManager: CacheManager - private let queryEngine: QueryEngine - private let transformationEngine: TransformationEngine - private let validationEngine: ValidationEngine - private let streamingManager: StreamingManager - private let metricsCollector: DataSourceMetricsCollector - private let rateLimiter: RateLimiter - - @Published public private(set) var dataSources: [String: DataSourceConfiguration] = [:] - @Published public private(set) var activeConnections: [String: DataSourceConnection] = [:] - @Published public private(set) var streamingConnections: [String: StreamingConnection] = [:] - @Published public private(set) var queryQueue: [DataQuery] = [] - @Published public private(set) var connectionStatus: [String: ConnectionStatus] = [:] - @Published public private(set) var metrics: DataSourceMetrics = DataSourceMetrics() - - private var cancellables = Set() - private let queue = DispatchQueue(label: "datasource.manager", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "DataSource") - - // MARK: - Initialization - - private init() { - self.apiManager = APIManager.shared - self.cloudIntegration = CloudIntegration.shared - self.cacheManager = CacheManager.shared - self.queryEngine = QueryEngine() - self.transformationEngine = TransformationEngine() - self.validationEngine = ValidationEngine() - self.streamingManager = StreamingManager() - self.metricsCollector = DataSourceMetricsCollector() - self.rateLimiter = RateLimiter() - - setupDataSourceManager() - } - - // MARK: - Public Methods - - /// Registers a new data source - public func registerDataSource(_ configuration: DataSourceConfiguration) async throws { - guard isValidConfiguration(configuration) else { - throw DataSourceError.configurationInvalid - } - - // Test connection - try await testConnection(configuration) - - await MainActor.run { - self.dataSources[configuration.name] = configuration - self.connectionStatus[configuration.name] = .connected - } - - // Initialize connection if needed - if configuration.type.capabilities.contains(.realtime) { - try await initializeRealtimeConnection(configuration) - } - - logger.info("Data source registered: \(configuration.name) (\(configuration.type.rawValue))") - } - - /// Removes a data source - public func removeDataSource(_ name: String) async { - await disconnectDataSource(name) - - await MainActor.run { - self.dataSources.removeValue(forKey: name) - self.connectionStatus.removeValue(forKey: name) - } - - // Clear cached data - await clearCachedData(for: name) - - logger.info("Data source removed: \(name)") - } - - /// Executes a data query - public func executeQuery( - _ query: DataQuery, - responseType: T.Type - ) async throws -> DataResponse { - guard let configuration = dataSources[query.dataSourceName] else { - throw DataSourceError.dataSourceUnavailable(query.dataSourceName) - } - - // Check rate limiting - if configuration.enableRateLimiting { - try await rateLimiter.checkRateLimit(for: query.dataSourceName, configuration: configuration) - } - - // Check cache first if enabled - if query.enableCache { - if let cachedResponse = await getCachedResponse(query: query, responseType: responseType) { - await updateMetrics(queryId: query.id, cached: true, executionTime: 0) - return cachedResponse - } - } - - let startTime = CFAbsoluteTimeGetCurrent() - - do { - // Execute query based on data source type - let response = try await executeQueryForType(query, configuration: configuration, responseType: responseType) - - let executionTime = CFAbsoluteTimeGetCurrent() - startTime - - // Cache response if enabled - if query.enableCache && response.isSuccess { - await cacheResponse(query: query, response: response) - } - - // Update metrics - await updateMetrics(queryId: query.id, cached: false, executionTime: executionTime) - - logger.info("Query executed successfully: \(query.id) in \(executionTime)s") - return response - - } catch { - let executionTime = CFAbsoluteTimeGetCurrent() - startTime - await updateMetrics(queryId: query.id, cached: false, executionTime: executionTime, error: error) - throw error - } - } - - /// Streams data from a real-time data source - public func streamData( - from dataSourceName: String, - query: String? = nil, - responseType: T.Type - ) -> AsyncThrowingStream, Error> { - return AsyncThrowingStream { continuation in - Task { - do { - let stream = try await createDataStream( - dataSourceName: dataSourceName, - query: query, - responseType: responseType - ) - - for try await data in stream { - continuation.yield(data) - } - - continuation.finish() - - } catch { - continuation.finish(throwing: error) - } - } - } - } - - /// Writes data to a data source - public func writeData( - _ data: T, - to dataSourceName: String, - path: String? = nil, - options: WriteOptions = WriteOptions() - ) async throws -> WriteResult { - guard let configuration = dataSources[dataSourceName] else { - throw DataSourceError.dataSourceUnavailable(dataSourceName) - } - - guard configuration.type.capabilities.contains(.write) else { - throw DataSourceError.unsupportedOperation - } - - // Validate data if enabled - if configuration.enableValidation { - try await validationEngine.validate(data, for: configuration) - } - - // Transform data if rules are configured - let transformedData = try await transformationEngine.transform( - data, - using: configuration.transformationRules - ) - - return try await writeDataToSource( - transformedData, - configuration: configuration, - path: path, - options: options - ) - } - - /// Batch processes multiple queries - public func batchExecute( - queries: [DataQuery], - responseType: T.Type, - maxConcurrency: Int = 5 - ) async throws -> [DataResponse] { - let semaphore = AsyncSemaphore(value: maxConcurrency) - - return try await withThrowingTaskGroup(of: (Int, DataResponse).self) { group in - for (index, query) in queries.enumerated() { - group.addTask { - await semaphore.wait() - defer { semaphore.signal() } - - let response = try await self.executeQuery(query, responseType: responseType) - return (index, response) - } - } - - var results: [DataResponse] = Array(repeating: DataResponse( - queryId: "", - data: nil, - metadata: DataResponse.ResponseMetadata( - totalCount: nil, - pageCount: nil, - currentPage: nil, - hasNextPage: false, - hasPreviousPage: false, - executionTime: 0, - dataSourceName: "", - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ), count: queries.count) - - for try await (index, response) in group { - results[index] = response - } - - return results - } - } - - /// Synchronizes data between multiple data sources - public func synchronizeDataSources( - sourceNames: [String], - syncRules: [SyncRule] = [] - ) async throws { - for rule in syncRules { - try await applySyncRule(rule, dataSources: sourceNames) - } - - logger.info("Data source synchronization completed for \(sourceNames.count) sources") - } - - /// Migrates data from one source to another - public func migrateData( - from sourceDataSource: String, - to targetDataSource: String, - mapping: DataMapping, - batchSize: Int = 1000 - ) async throws { - guard let sourceConfig = dataSources[sourceDataSource], - let targetConfig = dataSources[targetDataSource] else { - throw DataSourceError.migrationFailed - } - - try await performDataMigration( - from: sourceConfig, - to: targetConfig, - mapping: mapping, - batchSize: batchSize - ) - - logger.info("Data migration completed from \(sourceDataSource) to \(targetDataSource)") - } - - /// Gets connection status for all data sources - public func getConnectionStatus() async -> [String: ConnectionStatus] { - var statuses: [String: ConnectionStatus] = [:] - - for (name, configuration) in dataSources { - statuses[name] = await checkConnectionStatus(name, configuration: configuration) - } - - await MainActor.run { - self.connectionStatus = statuses - } - - return statuses - } - - /// Gets data source metrics and insights - public func getDataSourceInsights() async -> DataSourceInsights { - let totalQueries = metrics.totalQueries - let successRate = metrics.successfulQueries > 0 ? - Double(metrics.successfulQueries) / Double(metrics.totalQueries) : 0 - let averageResponseTime = metrics.averageResponseTime - let cacheHitRate = metrics.cacheHits > 0 ? - Double(metrics.cacheHits) / Double(metrics.totalQueries) : 0 - - return DataSourceInsights( - totalDataSources: dataSources.count, - activeConnections: activeConnections.count, - totalQueries: totalQueries, - successRate: successRate, - averageResponseTime: averageResponseTime, - cacheHitRate: cacheHitRate, - streamingConnections: streamingConnections.count, - queuedQueries: queryQueue.count - ) - } - - /// Clears all cached data - public func clearAllCaches() async { - for dataSourceName in dataSources.keys { - await clearCachedData(for: dataSourceName) - } - - logger.info("All data source caches cleared") - } - - /// Resets data source manager - public func reset() async { - // Disconnect all connections - for name in dataSources.keys { - await disconnectDataSource(name) - } - - await MainActor.run { - self.dataSources.removeAll() - self.activeConnections.removeAll() - self.streamingConnections.removeAll() - self.queryQueue.removeAll() - self.connectionStatus.removeAll() - self.metrics = DataSourceMetrics() - } - - // Clear all caches - await clearAllCaches() - - logger.info("Data source manager reset completed") - } - - // MARK: - Private Methods - - private func setupDataSourceManager() { - // Setup background processing - setupBackgroundProcessing() - - // Setup notification observers - setupNotificationObservers() - - logger.info("Data source manager initialized") - } - - private func setupBackgroundProcessing() { - // Process query queue periodically - Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - Task { await self?.processQueryQueue() } - } - .store(in: &cancellables) - } - - private func setupNotificationObservers() { - NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) - .sink { [weak self] _ in - Task { await self?.reconnectDisconnectedSources() } - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - } - - private func isValidConfiguration(_ configuration: DataSourceConfiguration) -> Bool { - return !configuration.name.isEmpty && - !configuration.endpoint.isEmpty && - configuration.timeout > 0 && - configuration.maxRetryAttempts > 0 - } - - private func testConnection(_ configuration: DataSourceConfiguration) async throws { - switch configuration.type { - case .rest, .graphql: - try await apiManager.testConnection(endpoint: configuration.endpoint) - case .websocket: - try await streamingManager.testWebSocketConnection(endpoint: configuration.endpoint) - case .database: - try await testDatabaseConnection(configuration) - case .file: - try await testFileSystemAccess(configuration) - case .cloud: - try await cloudIntegration.testConnection(configuration) - default: - // Custom testing implementation - break - } - } - - private func executeQueryForType( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - switch configuration.type { - case .rest: - return try await executeRESTQuery(query, configuration: configuration, responseType: responseType) - case .graphql: - return try await executeGraphQLQuery(query, configuration: configuration, responseType: responseType) - case .database: - return try await executeDatabaseQuery(query, configuration: configuration, responseType: responseType) - case .file: - return try await executeFileQuery(query, configuration: configuration, responseType: responseType) - case .cloud: - return try await executeCloudQuery(query, configuration: configuration, responseType: responseType) - case .rss: - return try await executeRSSQuery(query, configuration: configuration, responseType: responseType) - default: - throw DataSourceError.unsupportedOperation - } - } - - private func executeRESTQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Build REST request - var endpoint = configuration.endpoint - if !query.query.isEmpty { - endpoint = "\(endpoint)/\(query.query)" - } - - // Add query parameters - var urlComponents = URLComponents(string: endpoint) - var queryItems: [URLQueryItem] = [] - - for (key, value) in query.parameters { - queryItems.append(URLQueryItem(name: key, value: String(describing: value))) - } - - // Add filters as query parameters - for filter in query.filters { - let filterValue = "\(filter.operator.rawValue):\(filter.value)" - queryItems.append(URLQueryItem(name: filter.field, value: filterValue)) - } - - urlComponents?.queryItems = queryItems - - guard let finalURL = urlComponents?.url else { - throw DataSourceError.queryFailed("Invalid URL construction") - } - - // Execute via APIManager - let apiRequest = APIManager.APIRequest( - url: finalURL, - method: .GET, - headers: configuration.headers, - timeout: query.timeout ?? configuration.timeout - ) - - let apiResponse = try await apiManager.request(apiRequest, responseType: responseType) - - // Convert to DataResponse - return DataResponse( - queryId: query.id, - data: apiResponse.data, - metadata: DataResponse.ResponseMetadata( - totalCount: nil, - pageCount: nil, - currentPage: query.pagination?.page, - hasNextPage: false, - hasPreviousPage: false, - executionTime: apiResponse.responseTime, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeGraphQLQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Build GraphQL request - let graphQLRequest = APIManager.GraphQLRequest( - query: query.query, - variables: query.parameters, - operationName: nil - ) - - let apiResponse = try await apiManager.graphQL(graphQLRequest, responseType: responseType) - - return DataResponse( - queryId: query.id, - data: apiResponse.data, - metadata: DataResponse.ResponseMetadata( - totalCount: nil, - pageCount: nil, - currentPage: nil, - hasNextPage: false, - hasPreviousPage: false, - executionTime: apiResponse.responseTime, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeDatabaseQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute database query - // Implementation would use Core Data, SQLite, or other database framework - throw DataSourceError.unsupportedOperation - } - - private func executeFileQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute file system query - let filePath = "\(configuration.endpoint)/\(query.query)" - let fileURL = URL(fileURLWithPath: filePath) - - let data = try Data(contentsOf: fileURL) - let decodedData = try JSONDecoder().decode(responseType, from: data) - - return DataResponse( - queryId: query.id, - data: decodedData, - metadata: DataResponse.ResponseMetadata( - totalCount: 1, - pageCount: 1, - currentPage: 1, - hasNextPage: false, - hasPreviousPage: false, - executionTime: 0.001, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeCloudQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute cloud query via CloudIntegration - let data = try await cloudIntegration.download(path: query.query) - let decodedData = try JSONDecoder().decode(responseType, from: data) - - return DataResponse( - queryId: query.id, - data: decodedData, - metadata: DataResponse.ResponseMetadata( - totalCount: 1, - pageCount: 1, - currentPage: 1, - hasNextPage: false, - hasPreviousPage: false, - executionTime: 0.1, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeRSSQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute RSS feed query - // Implementation would parse RSS/XML data - throw DataSourceError.unsupportedOperation - } - - private func createDataStream( - dataSourceName: String, - query: String?, - responseType: T.Type - ) async throws -> AsyncThrowingStream, Error> { - guard let configuration = dataSources[dataSourceName] else { - throw DataSourceError.dataSourceUnavailable(dataSourceName) - } - - return try await streamingManager.createStream( - configuration: configuration, - query: query, - responseType: responseType - ) - } - - private func initializeRealtimeConnection(_ configuration: DataSourceConfiguration) async throws { - // Initialize real-time connection for WebSocket, etc. - let connection = try await streamingManager.connect(configuration: configuration) - - await MainActor.run { - self.streamingConnections[configuration.name] = connection - } - } - - private func writeDataToSource( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - switch configuration.type { - case .rest, .graphql: - return try await writeToAPI(data, configuration: configuration, path: path, options: options) - case .database: - return try await writeToDatabase(data, configuration: configuration, options: options) - case .file: - return try await writeToFile(data, configuration: configuration, path: path, options: options) - case .cloud: - return try await writeToCloud(data, configuration: configuration, path: path, options: options) - default: - throw DataSourceError.unsupportedOperation - } - } - - private func writeToAPI( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - // Implementation would write to API - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func writeToDatabase( - _ data: T, - configuration: DataSourceConfiguration, - options: WriteOptions - ) async throws -> WriteResult { - // Implementation would write to database - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func writeToFile( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - let encoder = JSONEncoder() - let jsonData = try encoder.encode(data) - - let filePath = path ?? "\(configuration.endpoint)/data.json" - let fileURL = URL(fileURLWithPath: filePath) - - try jsonData.write(to: fileURL) - - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func writeToCloud( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - let encoder = JSONEncoder() - let jsonData = try encoder.encode(data) - - let cloudPath = path ?? "data.json" - _ = try await cloudIntegration.upload(data: jsonData, to: cloudPath) - - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func getCachedResponse( - query: DataQuery, - responseType: T.Type - ) async -> DataResponse? { - let cacheKey = "query_\(query.dataSourceName)_\(query.query.hashValue)" - return try? await cacheManager.retrieve(DataResponse.self, forKey: cacheKey) - } - - private func cacheResponse( - query: DataQuery, - response: DataResponse - ) async { - let cacheKey = "query_\(query.dataSourceName)_\(query.query.hashValue)" - let ttl = query.cacheTTL ?? 300 // 5 minutes default - try? await cacheManager.store(response, forKey: cacheKey, ttl: ttl) - } - - private func clearCachedData(for dataSourceName: String) async { - // Implementation would clear cache for specific data source - } - - private func disconnectDataSource(_ name: String) async { - // Disconnect from data source - await MainActor.run { - self.activeConnections.removeValue(forKey: name) - self.streamingConnections.removeValue(forKey: name) - } - } - - private func checkConnectionStatus(_ name: String, configuration: DataSourceConfiguration) async -> ConnectionStatus { - // Check actual connection status - return .connected - } - - private func updateMetrics(queryId: String, cached: Bool, executionTime: TimeInterval, error: Error? = nil) async { - await MainActor.run { - self.metrics.totalQueries += 1 - - if error == nil { - self.metrics.successfulQueries += 1 - } else { - self.metrics.failedQueries += 1 - } - - if cached { - self.metrics.cacheHits += 1 - } else { - self.metrics.cacheMisses += 1 - } - - self.metrics.totalResponseTime += executionTime - self.metrics.averageResponseTime = self.metrics.totalResponseTime / Double(self.metrics.totalQueries) - } - } - - private func processQueryQueue() async { - // Process queued queries - } - - private func reconnectDisconnectedSources() async { - // Reconnect to disconnected sources - } - - private func handleAppDidEnterBackground() async { - // Handle background operation - } - - private func testDatabaseConnection(_ configuration: DataSourceConfiguration) async throws { - // Test database connection - } - - private func testFileSystemAccess(_ configuration: DataSourceConfiguration) async throws { - // Test file system access - } - - private func applySyncRule(_ rule: SyncRule, dataSources: [String]) async throws { - // Apply synchronization rule - } - - private func performDataMigration( - from sourceConfig: DataSourceConfiguration, - to targetConfig: DataSourceConfiguration, - mapping: DataMapping, - batchSize: Int - ) async throws { - // Perform data migration - } -} - -// MARK: - Supporting Types - -public enum ConnectionStatus: String, CaseIterable { - case connected = "connected" - case disconnected = "disconnected" - case connecting = "connecting" - case error = "error" -} - -public struct DataSourceConnection { - public let name: String - public let type: DataSourceManager.DataSourceType - public let connectedAt: Date - public let lastActivity: Date -} - -public struct StreamingConnection { - public let name: String - public let endpoint: String - public let connectedAt: Date - public let messageCount: Int -} - -public struct DataSourceMetrics { - public var totalQueries: Int = 0 - public var successfulQueries: Int = 0 - public var failedQueries: Int = 0 - public var cacheHits: Int = 0 - public var cacheMisses: Int = 0 - public var totalResponseTime: TimeInterval = 0 - public var averageResponseTime: TimeInterval = 0 -} - -public struct DataSourceInsights { - public let totalDataSources: Int - public let activeConnections: Int - public let totalQueries: Int - public let successRate: Double - public let averageResponseTime: TimeInterval - public let cacheHitRate: Double - public let streamingConnections: Int - public let queuedQueries: Int -} - -public struct WriteOptions { - public var upsert: Bool = false - public var batch: Bool = false - public var transaction: Bool = false - public var validateBeforeWrite: Bool = true - - public init() {} -} - -public struct WriteResult { - public let success: Bool - public let recordsWritten: Int - public let errors: [String] -} - -public struct SyncRule { - public let sourceField: String - public let targetField: String - public let transformation: String? - public let bidirectional: Bool -} - -public struct DataMapping { - public let fieldMappings: [String: String] - public let transformations: [String: String] - public let filters: [DataSourceManager.DataQuery.DataFilter] -} - -// MARK: - Supporting Classes - -private class QueryEngine { - func optimizeQuery(_ query: DataSourceManager.DataQuery) -> DataSourceManager.DataQuery { - // Implementation would optimize queries - return query - } -} - -private class TransformationEngine { - func transform(_ data: T, using rules: [String: String]) async throws -> T { - // Implementation would transform data - return data - } -} - -private class ValidationEngine { - func validate(_ data: T, for configuration: DataSourceManager.DataSourceConfiguration) async throws { - // Implementation would validate data - } -} - -private class StreamingManager { - func connect(configuration: DataSourceManager.DataSourceConfiguration) async throws -> StreamingConnection { - return StreamingConnection( - name: configuration.name, - endpoint: configuration.endpoint, - connectedAt: Date(), - messageCount: 0 - ) - } - - func createStream( - configuration: DataSourceManager.DataSourceConfiguration, - query: String?, - responseType: T.Type - ) async throws -> AsyncThrowingStream, Error> { - return AsyncThrowingStream { continuation in - // Implementation would create actual stream - continuation.finish() - } - } - - func testWebSocketConnection(endpoint: String) async throws { - // Implementation would test WebSocket connection - } -} - -private class DataSourceMetricsCollector { - func recordMetric(_ metric: String, value: Double) async { - // Implementation would record metrics - } -} - -private class RateLimiter { - func checkRateLimit(for dataSourceName: String, configuration: DataSourceManager.DataSourceConfiguration) async throws { - // Implementation would check rate limits - } -} - -private class AsyncSemaphore { - private let semaphore: DispatchSemaphore - - init(value: Int) { - self.semaphore = DispatchSemaphore(value: value) - } - - func wait() async { - await withCheckedContinuation { continuation in - DispatchQueue.global().async { - self.semaphore.wait() - continuation.resume() - } - } - } - - func signal() { - semaphore.signal() - } -} \ No newline at end of file diff --git a/Sources/LiveData/LiveDataIntegration.swift b/Sources/LiveData/LiveDataIntegration.swift deleted file mode 100644 index 1433873..0000000 --- a/Sources/LiveData/LiveDataIntegration.swift +++ /dev/null @@ -1,590 +0,0 @@ -import Foundation -import Combine -import Network - -/// Live Data Integration Framework -/// Provides real-time data updates for widgets with WebSocket support -@available(iOS 16.0, *) -public class LiveDataIntegration: ObservableObject { - - // MARK: - Properties - - /// Shared instance for singleton access - public static let shared = LiveDataIntegration() - - /// Active data sources - private var dataSources: [String: WidgetDataSource] = [:] - - /// WebSocket connections - private var webSocketConnections: [String: NWConnection] = [:] - - /// Data publishers - private var dataPublishers: [String: PassthroughSubject] = [:] - - /// Combine cancellables - private var cancellables = Set() - - /// Network monitor - private let networkMonitor = NWPathMonitor() - - /// Connection queue - private let connectionQueue = DispatchQueue(label: "com.widgetkit.connection") - - /// Analytics service - private let analyticsService = WidgetAnalyticsService() - - // MARK: - Initialization - - private init() { - setupNetworkMonitoring() - setupDataSources() - } - - // MARK: - Public Methods - - /// Register a data source for live updates - /// - Parameters: - /// - identifier: Data source identifier - /// - dataSource: Data source implementation - public func registerDataSource(_ identifier: String, dataSource: WidgetDataSource) { - dataSources[identifier] = dataSource - dataPublishers[identifier] = PassthroughSubject() - - setupDataSourceUpdates(for: identifier, dataSource: dataSource) - analyticsService.logDataSourceRegistration(identifier) - } - - /// Connect to WebSocket for real-time data - /// - Parameters: - /// - identifier: Connection identifier - /// - url: WebSocket URL - /// - headers: Optional headers - public func connectWebSocket(identifier: String, url: URL, headers: [String: String] = [:]) { - let endpoint = NWEndpoint.url(url) - let connection = NWConnection(to: endpoint, using: .ws) - - connection.stateUpdateHandler = { [weak self] state in - self?.handleConnectionStateChange(identifier: identifier, state: state) - } - - webSocketConnections[identifier] = connection - connection.start(queue: connectionQueue) - - analyticsService.logWebSocketConnection(identifier: identifier, url: url.absoluteString) - } - - /// Disconnect WebSocket - /// - Parameter identifier: Connection identifier - public func disconnectWebSocket(identifier: String) { - webSocketConnections[identifier]?.cancel() - webSocketConnections.removeValue(forKey: identifier) - - analyticsService.logWebSocketDisconnection(identifier: identifier) - } - - /// Get data publisher for specific source - /// - Parameter identifier: Data source identifier - /// - Returns: Publisher for data updates - public func getDataPublisher(for identifier: String) -> AnyPublisher? { - return dataPublishers[identifier]?.eraseToAnyPublisher() - } - - /// Send data to specific widget - /// - Parameters: - /// - widgetId: Widget identifier - /// - data: Data to send - public func sendData(to widgetId: String, data: WidgetData) { - NotificationCenter.default.post( - name: .widgetDataReceived, - object: nil, - userInfo: ["widgetId": widgetId, "data": data] - ) - - analyticsService.logDataSent(widgetId: widgetId, dataType: data.type.rawValue) - } - - /// Configure data source settings - /// - Parameters: - /// - identifier: Data source identifier - /// - settings: Data source settings - public func configureDataSource(_ identifier: String, settings: DataSourceSettings) { - dataSources[identifier]?.configure(settings) - analyticsService.logDataSourceConfiguration(identifier: identifier) - } - - /// Get connection status - /// - Parameter identifier: Connection identifier - /// - Returns: Connection status - public func getConnectionStatus(for identifier: String) -> ConnectionStatus { - guard let connection = webSocketConnections[identifier] else { - return .disconnected - } - - switch connection.state { - case .ready: - return .connected - case .failed, .cancelled: - return .disconnected - case .preparing, .setup: - return .connecting - default: - return .unknown - } - } - - // MARK: - Private Methods - - private func setupNetworkMonitoring() { - networkMonitor.pathUpdateHandler = { [weak self] path in - self?.handleNetworkPathUpdate(path) - } - networkMonitor.start(queue: DispatchQueue.global()) - } - - private func setupDataSources() { - // Register default data sources - registerDefaultDataSources() - } - - private func registerDefaultDataSources() { - let defaultSources: [WidgetDataSource] = [ - WeatherDataSource(), - CalendarDataSource(), - FitnessDataSource(), - NewsDataSource(), - SocialMediaDataSource(), - ProductivityDataSource(), - EntertainmentDataSource(), - FinanceDataSource(), - HealthDataSource(), - TravelDataSource() - ] - - defaultSources.forEach { dataSource in - registerDataSource(dataSource.identifier, dataSource: dataSource) - } - } - - private func setupDataSourceUpdates(for identifier: String, dataSource: WidgetDataSource) { - dataSource.dataPublisher - .sink { [weak self] data in - self?.handleDataSourceUpdate(identifier: identifier, data: data) - } - .store(in: &cancellables) - } - - private func handleDataSourceUpdate(identifier: String, data: WidgetData) { - dataPublishers[identifier]?.send(data) - - // Notify widgets using this data source - NotificationCenter.default.post( - name: .dataSourceUpdated, - object: nil, - userInfo: ["identifier": identifier, "data": data] - ) - - analyticsService.logDataSourceUpdate(identifier: identifier, dataType: data.type.rawValue) - } - - private func handleConnectionStateChange(identifier: String, state: NWConnection.State) { - DispatchQueue.main.async { [weak self] in - switch state { - case .ready: - self?.analyticsService.logConnectionEstablished(identifier: identifier) - self?.startReceivingData(for: identifier) - case .failed(let error): - self?.analyticsService.logConnectionFailed(identifier: identifier, error: error) - self?.handleConnectionFailure(identifier: identifier, error: error) - case .cancelled: - self?.analyticsService.logConnectionCancelled(identifier: identifier) - default: - break - } - } - } - - private func handleNetworkPathUpdate(_ path: NWPath) { - if path.status == .satisfied { - // Network is available, reconnect if needed - reconnectIfNeeded() - } else { - // Network is unavailable - analyticsService.logNetworkUnavailable() - } - } - - private func startReceivingData(for identifier: String) { - guard let connection = webSocketConnections[identifier] else { return } - - connection.receiveMessage { [weak self] content, context, isComplete, error in - if let error = error { - self?.analyticsService.logDataReceiveError(identifier: identifier, error: error) - return - } - - if let content = content { - self?.handleReceivedData(identifier: identifier, data: content) - } - - // Continue receiving - if !isComplete { - self?.startReceivingData(for: identifier) - } - } - } - - private func handleReceivedData(identifier: String, data: Data) { - do { - let widgetData = try JSONDecoder().decode(WidgetData.self, from: data) - handleDataSourceUpdate(identifier: identifier, data: widgetData) - } catch { - analyticsService.logDataDecodeError(identifier: identifier, error: error) - } - } - - private func handleConnectionFailure(identifier: String, error: NWError) { - // Implement retry logic - DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in - self?.retryConnection(identifier: identifier) - } - } - - private func retryConnection(identifier: String) { - // Implement retry logic for failed connections - analyticsService.logConnectionRetry(identifier: identifier) - } - - private func reconnectIfNeeded() { - // Reconnect any disconnected WebSocket connections - for (identifier, connection) in webSocketConnections { - if connection.state == .failed || connection.state == .cancelled { - // Reconnect logic - analyticsService.logReconnectionAttempt(identifier: identifier) - } - } - } -} - -// MARK: - Supporting Types - -/// Data source protocol -@available(iOS 16.0, *) -public protocol WidgetDataSource { - var identifier: String { get } - var dataPublisher: AnyPublisher { get } - - func refresh() - func configure(_ settings: DataSourceSettings) -} - -/// Data source settings -public struct DataSourceSettings { - public let refreshInterval: TimeInterval - public let enableCaching: Bool - public let maxRetries: Int - public let timeout: TimeInterval - - public init(refreshInterval: TimeInterval = 30.0, enableCaching: Bool = true, maxRetries: Int = 3, timeout: TimeInterval = 10.0) { - self.refreshInterval = refreshInterval - self.enableCaching = enableCaching - self.maxRetries = maxRetries - self.timeout = timeout - } -} - -/// Connection status -public enum ConnectionStatus { - case connected - case connecting - case disconnected - case unknown -} - -// MARK: - Default Data Sources - -@available(iOS 16.0, *) -public class WeatherDataSource: WidgetDataSource { - public let identifier = "weather_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate weather API call - let weatherData = WidgetData( - id: "weather_current", - type: .weather, - content: [ - "temperature": 22, - "condition": "sunny", - "humidity": 65, - "windSpeed": 12 - ] - ) - dataSubject.send(weatherData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class CalendarDataSource: WidgetDataSource { - public let identifier = "calendar_events" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate calendar API call - let calendarData = WidgetData( - id: "calendar_events", - type: .calendar, - content: [ - "events": [ - ["title": "Team Meeting", "time": "10:00 AM", "location": "Conference Room"], - ["title": "Lunch", "time": "12:30 PM", "location": "Cafeteria"] - ] - ] - ) - dataSubject.send(calendarData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class FitnessDataSource: WidgetDataSource { - public let identifier = "health_kit" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate HealthKit data - let fitnessData = WidgetData( - id: "fitness_current", - type: .fitness, - content: [ - "steps": 8420, - "calories": 320, - "activeMinutes": 45, - "workouts": 2 - ] - ) - dataSubject.send(fitnessData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class NewsDataSource: WidgetDataSource { - public let identifier = "news_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate news API call - let newsData = WidgetData( - id: "news_latest", - type: .news, - content: [ - "articles": [ - ["title": "Tech Innovation", "summary": "Latest developments in AI technology", "category": "Technology"], - ["title": "Market Update", "summary": "Stock market trends and analysis", "category": "Finance"] - ] - ] - ) - dataSubject.send(newsData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class SocialMediaDataSource: WidgetDataSource { - public let identifier = "social_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate social media API call - let socialData = WidgetData( - id: "social_updates", - type: .social, - content: [ - "updates": [ - ["platform": "Twitter", "content": "New product launch!", "likes": 150], - ["platform": "Instagram", "content": "Beautiful sunset", "likes": 89] - ] - ] - ) - dataSubject.send(socialData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class ProductivityDataSource: WidgetDataSource { - public let identifier = "productivity_data" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate productivity data - let productivityData = WidgetData( - id: "productivity_current", - type: .productivity, - content: [ - "tasksCompleted": 8, - "tasksRemaining": 3, - "focusTime": 240, - "productivityScore": 85 - ] - ) - dataSubject.send(productivityData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class EntertainmentDataSource: WidgetDataSource { - public let identifier = "entertainment_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate entertainment API call - let entertainmentData = WidgetData( - id: "entertainment_latest", - type: .entertainment, - content: [ - "items": [ - ["title": "New Movie", "type": "Movie", "rating": 4.5], - ["title": "Popular Series", "type": "TV Show", "rating": 4.8] - ] - ] - ) - dataSubject.send(entertainmentData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class FinanceDataSource: WidgetDataSource { - public let identifier = "finance_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate finance API call - let financeData = WidgetData( - id: "finance_current", - type: .finance, - content: [ - "portfolioValue": 125000, - "dailyChange": 1250, - "topStock": "AAPL", - "marketTrend": "Bullish" - ] - ) - dataSubject.send(financeData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class HealthDataSource: WidgetDataSource { - public let identifier = "health_kit" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate health data - let healthData = WidgetData( - id: "health_current", - type: .health, - content: [ - "heartRate": 72, - "sleepHours": 7.5, - "waterIntake": 8, - "mood": "Good" - ] - ) - dataSubject.send(healthData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class TravelDataSource: WidgetDataSource { - public let identifier = "travel_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate travel API call - let travelData = WidgetData( - id: "travel_current", - type: .travel, - content: [ - "destination": "Paris", - "departureTime": "10:30 AM", - "flightNumber": "AF123", - "weather": "Sunny" - ] - ) - dataSubject.send(travelData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -// MARK: - Notification Names - -extension Notification.Name { - static let widgetDataReceived = Notification.Name("widgetDataReceived") - static let dataSourceUpdated = Notification.Name("dataSourceUpdated") -} \ No newline at end of file diff --git a/Sources/Widgets/WidgetContentView.swift b/Sources/Widgets/WidgetContentView.swift deleted file mode 100644 index 99eb6ca..0000000 --- a/Sources/Widgets/WidgetContentView.swift +++ /dev/null @@ -1,881 +0,0 @@ -import SwiftUI - -// MARK: - Weather Widget Views - -@available(iOS 16.0, *) -public struct WeatherLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading weather...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct WeatherContentView: View { - let weatherData: WeatherData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(weatherData.temperature)°") - .font(.title) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - - Text(weatherData.condition.capitalized) - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - Image(systemName: weatherIcon) - .font(.title2) - .foregroundColor(customization.accentColor) - } - - HStack { - WeatherDetailView(title: "Humidity", value: "\(weatherData.humidity)%") - Spacer() - WeatherDetailView(title: "Wind", value: "\(weatherData.windSpeed) km/h") - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } - - private var weatherIcon: String { - switch weatherData.condition.lowercased() { - case "sunny": return "sun.max.fill" - case "cloudy": return "cloud.fill" - case "rainy": return "cloud.rain.fill" - case "snowy": return "cloud.snow.fill" - default: return "cloud.fill" - } - } -} - -@available(iOS 16.0, *) -public struct WeatherDetailView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct WeatherErrorView: View { - public var body: some View { - VStack { - Image(systemName: "exclamationmark.triangle") - .font(.title2) - .foregroundColor(.orange) - Text("Weather unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Calendar Widget Views - -@available(iOS 16.0, *) -public struct CalendarLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading calendar...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct CalendarContentView: View { - let events: [CalendarEvent] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "calendar") - .foregroundColor(customization.accentColor) - Text("Today's Events") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(events.prefix(3), id: \.title) { event in - CalendarEventRow(event: event, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct CalendarEventRow: View { - let event: CalendarEvent - let customization: WidgetCustomization - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(event.title) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(customization.textColor) - .lineLimit(1) - - Text(event.time) - .font(.caption2) - .foregroundColor(customization.textColor.opacity(0.7)) - } - - Spacer() - - Text(event.location) - .font(.caption2) - .foregroundColor(customization.accentColor) - .lineLimit(1) - } - } -} - -@available(iOS 16.0, *) -public struct CalendarEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "calendar.badge.plus") - .font(.title2) - .foregroundColor(.green) - Text("No events today") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Fitness Widget Views - -@available(iOS 16.0, *) -public struct FitnessLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading fitness data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct FitnessContentView: View { - let fitnessData: FitnessData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(fitnessData.steps)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Steps") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("\(fitnessData.calories)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Calories") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - FitnessMetricView(title: "Active", value: "\(fitnessData.activeMinutes)m") - Spacer() - FitnessMetricView(title: "Workouts", value: "\(fitnessData.workouts)") - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct FitnessMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct FitnessErrorView: View { - public var body: some View { - VStack { - Image(systemName: "heart.slash") - .font(.title2) - .foregroundColor(.red) - Text("Fitness data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - News Widget Views - -@available(iOS 16.0, *) -public struct NewsLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading news...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct NewsContentView: View { - let newsItems: [NewsItem] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "newspaper") - .foregroundColor(customization.accentColor) - Text("Latest News") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(newsItems.prefix(2), id: \.title) { item in - NewsItemRow(item: item, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct NewsItemRow: View { - let item: NewsItem - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - Text(item.title) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(customization.textColor) - .lineLimit(2) - - HStack { - Text(item.category) - .font(.caption2) - .foregroundColor(customization.accentColor) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(customization.accentColor.opacity(0.1)) - .cornerRadius(4) - - Spacer() - } - } - } -} - -@available(iOS 16.0, *) -public struct NewsEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "newspaper") - .font(.title2) - .foregroundColor(.blue) - Text("No news available") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Social Media Widget Views - -@available(iOS 16.0, *) -public struct SocialLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading social updates...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct SocialContentView: View { - let socialUpdates: [SocialUpdate] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "bubble.left.and.bubble.right") - .foregroundColor(customization.accentColor) - Text("Social Updates") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(socialUpdates.prefix(2), id: \.content) { update in - SocialUpdateRow(update: update, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct SocialUpdateRow: View { - let update: SocialUpdate - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(update.platform) - .font(.caption2) - .foregroundColor(customization.accentColor) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(customization.accentColor.opacity(0.1)) - .cornerRadius(4) - - Spacer() - - Text("\(update.likes) likes") - .font(.caption2) - .foregroundColor(.secondary) - } - - Text(update.content) - .font(.caption) - .foregroundColor(customization.textColor) - .lineLimit(2) - } - } -} - -@available(iOS 16.0, *) -public struct SocialEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "bubble.left.and.bubble.right") - .font(.title2) - .foregroundColor(.pink) - Text("No social updates") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Productivity Widget Views - -@available(iOS 16.0, *) -public struct ProductivityLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading productivity data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct ProductivityContentView: View { - let productivityData: ProductivityData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(productivityData.tasksCompleted)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Completed") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("\(productivityData.productivityScore)%") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Score") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - ProductivityMetricView(title: "Remaining", value: "\(productivityData.tasksRemaining)") - Spacer() - ProductivityMetricView(title: "Focus Time", value: "\(productivityData.focusTime)m") - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct ProductivityMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct ProductivityErrorView: View { - public var body: some View { - VStack { - Image(systemName: "chart.line.downtrend.xyaxis") - .font(.title2) - .foregroundColor(.orange) - Text("Productivity data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Entertainment Widget Views - -@available(iOS 16.0, *) -public struct EntertainmentLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading entertainment...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct EntertainmentContentView: View { - let entertainmentItems: [EntertainmentItem] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "play.circle") - .foregroundColor(customization.accentColor) - Text("Entertainment") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(entertainmentItems.prefix(2), id: \.title) { item in - EntertainmentItemRow(item: item, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct EntertainmentItemRow: View { - let item: EntertainmentItem - let customization: WidgetCustomization - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(item.title) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(customization.textColor) - .lineLimit(1) - - Text(item.type) - .font(.caption2) - .foregroundColor(customization.textColor.opacity(0.7)) - } - - Spacer() - - HStack(spacing: 2) { - Image(systemName: "star.fill") - .font(.caption2) - .foregroundColor(.yellow) - Text(String(format: "%.1f", item.rating)) - .font(.caption2) - .foregroundColor(customization.accentColor) - } - } - } -} - -@available(iOS 16.0, *) -public struct EntertainmentEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "play.circle") - .font(.title2) - .foregroundColor(.red) - Text("No entertainment available") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Finance Widget Views - -@available(iOS 16.0, *) -public struct FinanceLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading finance data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct FinanceContentView: View { - let financeData: FinanceData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("$\(Int(financeData.portfolioValue))") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Portfolio") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("$\(Int(financeData.dailyChange))") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(financeData.dailyChange >= 0 ? .green : .red) - Text("Today") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - } - - HStack { - FinanceMetricView(title: "Top Stock", value: financeData.topStock) - Spacer() - FinanceMetricView(title: "Trend", value: financeData.marketTrend) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct FinanceMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct FinanceErrorView: View { - public var body: some View { - VStack { - Image(systemName: "chart.line.downtrend.xyaxis") - .font(.title2) - .foregroundColor(.red) - Text("Finance data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Health Widget Views - -@available(iOS 16.0, *) -public struct HealthLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading health data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct HealthContentView: View { - let healthData: HealthData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(healthData.heartRate)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("BPM") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("\(healthData.sleepHours, specifier: "%.1f")h") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Sleep") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - HealthMetricView(title: "Water", value: "\(healthData.waterIntake) cups") - Spacer() - HealthMetricView(title: "Mood", value: healthData.mood) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct HealthMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct HealthErrorView: View { - public var body: some View { - VStack { - Image(systemName: "heart.slash") - .font(.title2) - .foregroundColor(.red) - Text("Health data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Travel Widget Views - -@available(iOS 16.0, *) -public struct TravelLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading travel info...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct TravelContentView: View { - let travelData: TravelData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text(travelData.destination) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Destination") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text(travelData.departureTime) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Departure") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - TravelMetricView(title: "Flight", value: travelData.flightNumber) - Spacer() - TravelMetricView(title: "Weather", value: travelData.weather) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct TravelMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct TravelErrorView: View { - public var body: some View { - VStack { - Image(systemName: "airplane") - .font(.title2) - .foregroundColor(.blue) - Text("Travel data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} \ No newline at end of file diff --git a/Sources/Widgets/WidgetTemplate.swift b/Sources/Widgets/WidgetTemplate.swift deleted file mode 100644 index a39aa57..0000000 --- a/Sources/Widgets/WidgetTemplate.swift +++ /dev/null @@ -1,417 +0,0 @@ -import SwiftUI -import WidgetKit - -/// Base protocol for all widget templates -@available(iOS 16.0, *) -public protocol WidgetTemplate { - /// Unique identifier for the template - var identifier: String { get } - - /// Display name for the template - var displayName: String { get } - - /// Template description - var description: String { get } - - /// Supported widget sizes - var supportedSizes: [WidgetFamily] { get } - - /// Create widget view with configuration - func createWidget(configuration: WidgetConfiguration) -> AnyView - - /// Validate configuration - func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool - - /// Get default configuration - func getDefaultConfiguration() -> WidgetConfiguration -} - -/// Weather widget template -@available(iOS 16.0, *) -public class WeatherWidgetTemplate: WidgetTemplate { - public let identifier = "weather_widget" - public let displayName = "Weather Widget" - public let description = "Display current weather conditions with beautiful animations" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(WeatherWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .weather } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let weatherWidget = WidgetDefinition( - id: "weather_main", - type: .weather, - dataSourceIdentifier: "weather_api", - customization: WidgetCustomization( - backgroundColor: Color.blue.opacity(0.1), - textColor: .primary, - accentColor: .blue, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "weather_config", - templateIdentifier: identifier, - widgets: [weatherWidget], - settings: WidgetSettings(refreshInterval: 300.0, enableAnimations: true) - ) - } -} - -/// Calendar widget template -@available(iOS 16.0, *) -public class CalendarWidgetTemplate: WidgetTemplate { - public let identifier = "calendar_widget" - public let displayName = "Calendar Widget" - public let description = "Show upcoming events and calendar information" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(CalendarWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .calendar } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let calendarWidget = WidgetDefinition( - id: "calendar_main", - type: .calendar, - dataSourceIdentifier: "calendar_events", - customization: WidgetCustomization( - backgroundColor: Color.green.opacity(0.1), - textColor: .primary, - accentColor: .green, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "calendar_config", - templateIdentifier: identifier, - widgets: [calendarWidget], - settings: WidgetSettings(refreshInterval: 600.0, enableAnimations: true) - ) - } -} - -/// Fitness widget template -@available(iOS 16.0, *) -public class FitnessWidgetTemplate: WidgetTemplate { - public let identifier = "fitness_widget" - public let displayName = "Fitness Widget" - public let description = "Track fitness metrics and workout progress" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(FitnessWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .fitness } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let fitnessWidget = WidgetDefinition( - id: "fitness_main", - type: .fitness, - dataSourceIdentifier: "health_kit", - customization: WidgetCustomization( - backgroundColor: Color.orange.opacity(0.1), - textColor: .primary, - accentColor: .orange, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "fitness_config", - templateIdentifier: identifier, - widgets: [fitnessWidget], - settings: WidgetSettings(refreshInterval: 900.0, enableAnimations: true) - ) - } -} - -/// News widget template -@available(iOS 16.0, *) -public class NewsWidgetTemplate: WidgetTemplate { - public let identifier = "news_widget" - public let displayName = "News Widget" - public let description = "Display latest news and headlines" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(NewsWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .news } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let newsWidget = WidgetDefinition( - id: "news_main", - type: .news, - dataSourceIdentifier: "news_api", - customization: WidgetCustomization( - backgroundColor: Color.purple.opacity(0.1), - textColor: .primary, - accentColor: .purple, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "news_config", - templateIdentifier: identifier, - widgets: [newsWidget], - settings: WidgetSettings(refreshInterval: 1800.0, enableAnimations: true) - ) - } -} - -/// Social media widget template -@available(iOS 16.0, *) -public class SocialMediaWidgetTemplate: WidgetTemplate { - public let identifier = "social_media_widget" - public let displayName = "Social Media Widget" - public let description = "Show social media updates and notifications" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(SocialMediaWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .social } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let socialWidget = WidgetDefinition( - id: "social_main", - type: .social, - dataSourceIdentifier: "social_api", - customization: WidgetCustomization( - backgroundColor: Color.pink.opacity(0.1), - textColor: .primary, - accentColor: .pink, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "social_config", - templateIdentifier: identifier, - widgets: [socialWidget], - settings: WidgetSettings(refreshInterval: 1200.0, enableAnimations: true) - ) - } -} - -/// Productivity widget template -@available(iOS 16.0, *) -public class ProductivityWidgetTemplate: WidgetTemplate { - public let identifier = "productivity_widget" - public let displayName = "Productivity Widget" - public let description = "Track tasks, goals, and productivity metrics" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(ProductivityWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .productivity } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let productivityWidget = WidgetDefinition( - id: "productivity_main", - type: .productivity, - dataSourceIdentifier: "productivity_data", - customization: WidgetCustomization( - backgroundColor: Color.indigo.opacity(0.1), - textColor: .primary, - accentColor: .indigo, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "productivity_config", - templateIdentifier: identifier, - widgets: [productivityWidget], - settings: WidgetSettings(refreshInterval: 600.0, enableAnimations: true) - ) - } -} - -/// Entertainment widget template -@available(iOS 16.0, *) -public class EntertainmentWidgetTemplate: WidgetTemplate { - public let identifier = "entertainment_widget" - public let displayName = "Entertainment Widget" - public let description = "Show entertainment content and recommendations" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(EntertainmentWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .entertainment } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let entertainmentWidget = WidgetDefinition( - id: "entertainment_main", - type: .entertainment, - dataSourceIdentifier: "entertainment_api", - customization: WidgetCustomization( - backgroundColor: Color.red.opacity(0.1), - textColor: .primary, - accentColor: .red, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "entertainment_config", - templateIdentifier: identifier, - widgets: [entertainmentWidget], - settings: WidgetSettings(refreshInterval: 3600.0, enableAnimations: true) - ) - } -} - -/// Finance widget template -@available(iOS 16.0, *) -public class FinanceWidgetTemplate: WidgetTemplate { - public let identifier = "finance_widget" - public let displayName = "Finance Widget" - public let description = "Display financial data and market information" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(FinanceWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .finance } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let financeWidget = WidgetDefinition( - id: "finance_main", - type: .finance, - dataSourceIdentifier: "finance_api", - customization: WidgetCustomization( - backgroundColor: Color.green.opacity(0.1), - textColor: .primary, - accentColor: .green, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "finance_config", - templateIdentifier: identifier, - widgets: [financeWidget], - settings: WidgetSettings(refreshInterval: 300.0, enableAnimations: true) - ) - } -} - -/// Health widget template -@available(iOS 16.0, *) -public class HealthWidgetTemplate: WidgetTemplate { - public let identifier = "health_widget" - public let displayName = "Health Widget" - public let description = "Monitor health metrics and wellness data" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(HealthWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .health } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let healthWidget = WidgetDefinition( - id: "health_main", - type: .health, - dataSourceIdentifier: "health_kit", - customization: WidgetCustomization( - backgroundColor: Color.teal.opacity(0.1), - textColor: .primary, - accentColor: .teal, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "health_config", - templateIdentifier: identifier, - widgets: [healthWidget], - settings: WidgetSettings(refreshInterval: 1800.0, enableAnimations: true) - ) - } -} - -/// Travel widget template -@available(iOS 16.0, *) -public class TravelWidgetTemplate: WidgetTemplate { - public let identifier = "travel_widget" - public let displayName = "Travel Widget" - public let description = "Show travel information and trip details" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(TravelWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .travel } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let travelWidget = WidgetDefinition( - id: "travel_main", - type: .travel, - dataSourceIdentifier: "travel_api", - customization: WidgetCustomization( - backgroundColor: Color.cyan.opacity(0.1), - textColor: .primary, - accentColor: .cyan, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "travel_config", - templateIdentifier: identifier, - widgets: [travelWidget], - settings: WidgetSettings(refreshInterval: 900.0, enableAnimations: true) - ) - } -} \ No newline at end of file diff --git a/Sources/Widgets/WidgetViews.swift b/Sources/Widgets/WidgetViews.swift deleted file mode 100644 index e757870..0000000 --- a/Sources/Widgets/WidgetViews.swift +++ /dev/null @@ -1,506 +0,0 @@ -import SwiftUI -import WidgetKit - -/// Weather widget view -@available(iOS 16.0, *) -public struct WeatherWidgetView: View { - let configuration: WidgetConfiguration - @State private var weatherData: WeatherData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - WeatherLoadingView() - } else if let weatherData = weatherData { - WeatherContentView(weatherData: weatherData, customization: getCustomization()) - } else { - WeatherErrorView() - } - } - .onAppear { - loadWeatherData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadWeatherData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.weatherData = WeatherData( - temperature: 22, - condition: "sunny", - humidity: 65, - windSpeed: 12 - ) - self.isLoading = false - } - } -} - -/// Calendar widget view -@available(iOS 16.0, *) -public struct CalendarWidgetView: View { - let configuration: WidgetConfiguration - @State private var events: [CalendarEvent] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - CalendarLoadingView() - } else if !events.isEmpty { - CalendarContentView(events: events, customization: getCustomization()) - } else { - CalendarEmptyView() - } - } - .onAppear { - loadCalendarEvents() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadCalendarEvents() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.events = [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", location: "Conference Room"), - CalendarEvent(title: "Lunch", time: "12:30 PM", location: "Cafeteria"), - CalendarEvent(title: "Project Review", time: "3:00 PM", location: "Office") - ] - self.isLoading = false - } - } -} - -/// Fitness widget view -@available(iOS 16.0, *) -public struct FitnessWidgetView: View { - let configuration: WidgetConfiguration - @State private var fitnessData: FitnessData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - FitnessLoadingView() - } else if let fitnessData = fitnessData { - FitnessContentView(fitnessData: fitnessData, customization: getCustomization()) - } else { - FitnessErrorView() - } - } - .onAppear { - loadFitnessData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadFitnessData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.fitnessData = FitnessData( - steps: 8420, - calories: 320, - activeMinutes: 45, - workouts: 2 - ) - self.isLoading = false - } - } -} - -/// News widget view -@available(iOS 16.0, *) -public struct NewsWidgetView: View { - let configuration: WidgetConfiguration - @State private var newsItems: [NewsItem] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - NewsLoadingView() - } else if !newsItems.isEmpty { - NewsContentView(newsItems: newsItems, customization: getCustomization()) - } else { - NewsEmptyView() - } - } - .onAppear { - loadNewsData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadNewsData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.newsItems = [ - NewsItem(title: "Tech Innovation", summary: "Latest developments in AI technology", category: "Technology"), - NewsItem(title: "Market Update", summary: "Stock market trends and analysis", category: "Finance"), - NewsItem(title: "Health News", summary: "New medical breakthroughs", category: "Health") - ] - self.isLoading = false - } - } -} - -/// Social media widget view -@available(iOS 16.0, *) -public struct SocialMediaWidgetView: View { - let configuration: WidgetConfiguration - @State private var socialUpdates: [SocialUpdate] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - SocialLoadingView() - } else if !socialUpdates.isEmpty { - SocialContentView(socialUpdates: socialUpdates, customization: getCustomization()) - } else { - SocialEmptyView() - } - } - .onAppear { - loadSocialData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadSocialData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.socialUpdates = [ - SocialUpdate(platform: "Twitter", content: "New product launch!", likes: 150), - SocialUpdate(platform: "Instagram", content: "Beautiful sunset", likes: 89), - SocialUpdate(platform: "LinkedIn", content: "Professional achievement", likes: 45) - ] - self.isLoading = false - } - } -} - -/// Productivity widget view -@available(iOS 16.0, *) -public struct ProductivityWidgetView: View { - let configuration: WidgetConfiguration - @State private var productivityData: ProductivityData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - ProductivityLoadingView() - } else if let productivityData = productivityData { - ProductivityContentView(productivityData: productivityData, customization: getCustomization()) - } else { - ProductivityErrorView() - } - } - .onAppear { - loadProductivityData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadProductivityData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.productivityData = ProductivityData( - tasksCompleted: 8, - tasksRemaining: 3, - focusTime: 240, - productivityScore: 85 - ) - self.isLoading = false - } - } -} - -/// Entertainment widget view -@available(iOS 16.0, *) -public struct EntertainmentWidgetView: View { - let configuration: WidgetConfiguration - @State private var entertainmentItems: [EntertainmentItem] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - EntertainmentLoadingView() - } else if !entertainmentItems.isEmpty { - EntertainmentContentView(entertainmentItems: entertainmentItems, customization: getCustomization()) - } else { - EntertainmentEmptyView() - } - } - .onAppear { - loadEntertainmentData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadEntertainmentData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.entertainmentItems = [ - EntertainmentItem(title: "New Movie", type: "Movie", rating: 4.5), - EntertainmentItem(title: "Popular Series", type: "TV Show", rating: 4.8), - EntertainmentItem(title: "Trending Game", type: "Game", rating: 4.2) - ] - self.isLoading = false - } - } -} - -/// Finance widget view -@available(iOS 16.0, *) -public struct FinanceWidgetView: View { - let configuration: WidgetConfiguration - @State private var financeData: FinanceData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - FinanceLoadingView() - } else if let financeData = financeData { - FinanceContentView(financeData: financeData, customization: getCustomization()) - } else { - FinanceErrorView() - } - } - .onAppear { - loadFinanceData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadFinanceData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.financeData = FinanceData( - portfolioValue: 125000, - dailyChange: 1250, - topStock: "AAPL", - marketTrend: "Bullish" - ) - self.isLoading = false - } - } -} - -/// Health widget view -@available(iOS 16.0, *) -public struct HealthWidgetView: View { - let configuration: WidgetConfiguration - @State private var healthData: HealthData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - HealthLoadingView() - } else if let healthData = healthData { - HealthContentView(healthData: healthData, customization: getCustomization()) - } else { - HealthErrorView() - } - } - .onAppear { - loadHealthData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadHealthData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.healthData = HealthData( - heartRate: 72, - sleepHours: 7.5, - waterIntake: 8, - mood: "Good" - ) - self.isLoading = false - } - } -} - -/// Travel widget view -@available(iOS 16.0, *) -public struct TravelWidgetView: View { - let configuration: WidgetConfiguration - @State private var travelData: TravelData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - TravelLoadingView() - } else if let travelData = travelData { - TravelContentView(travelData: travelData, customization: getCustomization()) - } else { - TravelErrorView() - } - } - .onAppear { - loadTravelData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadTravelData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.travelData = TravelData( - destination: "Paris", - departureTime: "10:30 AM", - flightNumber: "AF123", - weather: "Sunny" - ) - self.isLoading = false - } - } -} - -// MARK: - Data Models - -public struct WeatherData { - let temperature: Int - let condition: String - let humidity: Int - let windSpeed: Int -} - -public struct CalendarEvent { - let title: String - let time: String - let location: String -} - -public struct FitnessData { - let steps: Int - let calories: Int - let activeMinutes: Int - let workouts: Int -} - -public struct NewsItem { - let title: String - let summary: String - let category: String -} - -public struct SocialUpdate { - let platform: String - let content: String - let likes: Int -} - -public struct ProductivityData { - let tasksCompleted: Int - let tasksRemaining: Int - let focusTime: Int - let productivityScore: Int -} - -public struct EntertainmentItem { - let title: String - let type: String - let rating: Double -} - -public struct FinanceData { - let portfolioValue: Double - let dailyChange: Double - let topStock: String - let marketTrend: String -} - -public struct HealthData { - let heartRate: Int - let sleepHours: Double - let waterIntake: Int - let mood: String -} - -public struct TravelData { - let destination: String - let departureTime: String - let flightNumber: String - let weather: String -} \ No newline at end of file diff --git a/Templates/ServiceTemplate.swift b/Templates/ServiceTemplate.swift deleted file mode 100644 index 4055145..0000000 --- a/Templates/ServiceTemplate.swift +++ /dev/null @@ -1,126 +0,0 @@ -// MARK: - Widget Service Template -// Use this template for creating Widget data services - -import Foundation -import WidgetKit - -// MARK: - Protocol -protocol __NAME__WidgetServiceProtocol: Sendable { - func fetchData() async throws -> __TYPE__ - func refreshWidget() -} - -// MARK: - Service Implementation -final class __NAME__WidgetService: __NAME__WidgetServiceProtocol, @unchecked Sendable { - // MARK: - Singleton - static let shared = __NAME__WidgetService() - - // MARK: - Properties - private let networkClient: NetworkClientProtocol - private let cache: WidgetCacheProtocol - private let userDefaults: UserDefaults - - // MARK: - Constants - private enum Constants { - static let cacheKey = "__NAME__WidgetCache" - static let suiteName = "group.com.yourcompany.widget" - static let cacheExpiration: TimeInterval = 3600 // 1 hour - } - - // MARK: - Initialization - init( - networkClient: NetworkClientProtocol = NetworkClient.shared, - cache: WidgetCacheProtocol = WidgetCache.shared, - userDefaults: UserDefaults = UserDefaults(suiteName: Constants.suiteName)! - ) { - self.networkClient = networkClient - self.cache = cache - self.userDefaults = userDefaults - } - - // MARK: - Fetch Data - func fetchData() async throws -> __TYPE__ { - // Try cache first - if let cached = loadFromCache(), !isCacheExpired() { - return cached - } - - // Fetch from network - let data = try await fetchFromNetwork() - saveToCache(data) - return data - } - - // MARK: - Refresh Widget - func refreshWidget() { - WidgetCenter.shared.reloadTimelines(ofKind: "__NAME__Widget") - } - - func refreshAllWidgets() { - WidgetCenter.shared.reloadAllTimelines() - } - - // MARK: - Cache Management - private func loadFromCache() -> __TYPE__? { - guard let data = userDefaults.data(forKey: Constants.cacheKey) else { - return nil - } - return try? JSONDecoder().decode(__TYPE__.self, from: data) - } - - private func saveToCache(_ value: __TYPE__) { - guard let data = try? JSONEncoder().encode(value) else { return } - userDefaults.set(data, forKey: Constants.cacheKey) - userDefaults.set(Date(), forKey: "\(Constants.cacheKey)_timestamp") - } - - private func isCacheExpired() -> Bool { - guard let timestamp = userDefaults.object(forKey: "\(Constants.cacheKey)_timestamp") as? Date else { - return true - } - return Date().timeIntervalSince(timestamp) > Constants.cacheExpiration - } - - private func clearCache() { - userDefaults.removeObject(forKey: Constants.cacheKey) - userDefaults.removeObject(forKey: "\(Constants.cacheKey)_timestamp") - } - - // MARK: - Network - private func fetchFromNetwork() async throws -> __TYPE__ { - let url = URL(string: "https://api.example.com/__ENDPOINT__")! - return try await networkClient.get(url) - } -} - -// MARK: - Widget Cache Protocol -protocol WidgetCacheProtocol { - func get(for key: String) -> T? - func set(_ value: T, for key: String) - func remove(for key: String) -} - -// MARK: - Widget Cache Implementation -final class WidgetCache: WidgetCacheProtocol { - static let shared = WidgetCache() - - private let userDefaults: UserDefaults - - init(userDefaults: UserDefaults = UserDefaults(suiteName: "group.com.yourcompany.widget")!) { - self.userDefaults = userDefaults - } - - func get(for key: String) -> T? { - guard let data = userDefaults.data(forKey: key) else { return nil } - return try? JSONDecoder().decode(T.self, from: data) - } - - func set(_ value: T, for key: String) { - guard let data = try? JSONEncoder().encode(value) else { return } - userDefaults.set(data, forKey: key) - } - - func remove(for key: String) { - userDefaults.removeObject(forKey: key) - } -} diff --git a/Templates/TestTemplate.swift b/Templates/TestTemplate.swift deleted file mode 100644 index d42755d..0000000 --- a/Templates/TestTemplate.swift +++ /dev/null @@ -1,152 +0,0 @@ -// MARK: - Widget Test Template -// Use this template for testing Widget components - -import XCTest -import WidgetKit -@testable import __MODULE__ - -final class __NAME__WidgetTests: XCTestCase { - // MARK: - Properties - private var sut: __NAME__Provider! - private var mockService: Mock__NAME__Service! - - // MARK: - Setup & Teardown - override func setUp() { - super.setUp() - mockService = Mock__NAME__Service() - sut = __NAME__Provider(service: mockService) - } - - override func tearDown() { - sut = nil - mockService = nil - super.tearDown() - } - - // MARK: - Placeholder Tests - func test_placeholder_returnsValidEntry() { - // Given - let context = MockTimelineProviderContext() - - // When - let entry = sut.placeholder(in: context) - - // Then - XCTAssertNotNil(entry) - XCTAssertNotNil(entry.date) - } - - // MARK: - Snapshot Tests - func test_getSnapshot_returnsEntryImmediately() { - // Given - let context = MockTimelineProviderContext() - let expectation = expectation(description: "Snapshot returned") - - // When - sut.getSnapshot(in: context) { entry in - // Then - XCTAssertNotNil(entry) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - } - - // MARK: - Timeline Tests - func test_getTimeline_whenSuccessful_returnsEntriesWithPolicy() { - // Given - let context = MockTimelineProviderContext() - let expectation = expectation(description: "Timeline returned") - mockService.fetchResult = .success(.mock) - - // When - sut.getTimeline(in: context) { timeline in - // Then - XCTAssertFalse(timeline.entries.isEmpty) - XCTAssertEqual(timeline.policy, .atEnd) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - func test_getTimeline_whenFails_returnsPlaceholderEntry() { - // Given - let context = MockTimelineProviderContext() - let expectation = expectation(description: "Timeline returned") - mockService.fetchResult = .failure(NSError(domain: "test", code: 1)) - - // When - sut.getTimeline(in: context) { timeline in - // Then - XCTAssertEqual(timeline.entries.count, 1) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - // MARK: - Entry Tests - func test_entry_formatsDateCorrectly() { - // Given - let date = Date() - let data = __NAME__Data.mock - - // When - let entry = __NAME__Entry(date: date, data: data) - - // Then - XCTAssertEqual(entry.date, date) - XCTAssertEqual(entry.data.title, data.title) - } - - // MARK: - View Tests - func test_widgetView_rendersWithoutCrash() { - // Given - let entry = __NAME__Entry(date: Date(), data: .mock) - - // When/Then - should not crash - let view = __NAME__WidgetEntryView(entry: entry) - XCTAssertNotNil(view) - } -} - -// MARK: - Mock Objects -final class Mock__NAME__Service: __NAME__ServiceProtocol { - var fetchCalled = false - var fetchResult: Result<__TYPE__, Error> = .success(.mock) - - func fetchData() async throws -> __TYPE__ { - fetchCalled = true - return try fetchResult.get() - } - - func refreshWidget() { - // No-op for testing - } -} - -final class MockTimelineProviderContext: TimelineProviderContext { - var family: WidgetFamily { .systemSmall } - var isPreview: Bool { false } - var displaySize: CGSize { CGSize(width: 169, height: 169) } - var environmentVariants: EnvironmentVariants { [:] } -} - -// MARK: - Test Data -extension __NAME__Data { - static var mock: __NAME__Data { - __NAME__Data( - title: "Test Title", - subtitle: "Test Subtitle", - value: 42.0 - ) - } -} - -extension __TYPE__ { - static var mock: __TYPE__ { - // Return mock instance - fatalError("Implement mock data") - } -} diff --git a/Templates/ViewModelTemplate.swift b/Templates/ViewModelTemplate.swift deleted file mode 100644 index 14a2b6a..0000000 --- a/Templates/ViewModelTemplate.swift +++ /dev/null @@ -1,138 +0,0 @@ -// MARK: - Widget ViewModel Template -// Use this template for creating new Widget ViewModels - -import Foundation -import WidgetKit -import SwiftUI - -// MARK: - Timeline Provider -struct __NAME__Provider: TimelineProvider { - typealias Entry = __NAME__Entry - - // MARK: - Placeholder - func placeholder(in context: Context) -> __NAME__Entry { - __NAME__Entry(date: Date(), data: .placeholder) - } - - // MARK: - Snapshot - func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) { - let entry = __NAME__Entry(date: Date(), data: .placeholder) - completion(entry) - } - - // MARK: - Timeline - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - Task { - do { - let data = try await fetchData() - let entry = __NAME__Entry(date: Date(), data: data) - let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())! - let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) - completion(timeline) - } catch { - let entry = __NAME__Entry(date: Date(), data: .placeholder) - let timeline = Timeline(entries: [entry], policy: .after(Date().addingTimeInterval(300))) - completion(timeline) - } - } - } - - // MARK: - Data Fetching - private func fetchData() async throws -> __NAME__Data { - // Implement data fetching logic - return .placeholder - } -} - -// MARK: - Entry -struct __NAME__Entry: TimelineEntry { - let date: Date - let data: __NAME__Data -} - -// MARK: - Data Model -struct __NAME__Data { - let title: String - let subtitle: String - let value: Double - - static var placeholder: __NAME__Data { - __NAME__Data(title: "Loading...", subtitle: "", value: 0) - } -} - -// MARK: - Widget View -struct __NAME__WidgetEntryView: View { - var entry: __NAME__Provider.Entry - @Environment(\.widgetFamily) var family - - var body: some View { - switch family { - case .systemSmall: - SmallWidgetView(entry: entry) - case .systemMedium: - MediumWidgetView(entry: entry) - case .systemLarge: - LargeWidgetView(entry: entry) - default: - SmallWidgetView(entry: entry) - } - } -} - -// MARK: - Size-Specific Views -private struct SmallWidgetView: View { - let entry: __NAME__Entry - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(entry.data.title) - .font(.headline) - Text(entry.data.subtitle) - .font(.caption) - .foregroundColor(.secondary) - } - .padding() - } -} - -private struct MediumWidgetView: View { - let entry: __NAME__Entry - - var body: some View { - HStack { - SmallWidgetView(entry: entry) - Spacer() - Text("\(entry.data.value, specifier: "%.1f")") - .font(.largeTitle) - .bold() - } - .padding() - } -} - -private struct LargeWidgetView: View { - let entry: __NAME__Entry - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - MediumWidgetView(entry: entry) - // Add more content for large widget - } - .padding() - } -} - -// MARK: - Widget Definition -struct __NAME__Widget: Widget { - let kind: String = "__NAME__Widget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: __NAME__Provider()) { entry in - __NAME__WidgetEntryView(entry: entry) - } - .configurationDisplayName("__NAME__ Widget") - .description("Widget description goes here.") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} diff --git a/Tests/Integration/WidgetIntegrationTests.swift b/Tests/Integration/WidgetIntegrationTests.swift deleted file mode 100644 index d55e952..0000000 --- a/Tests/Integration/WidgetIntegrationTests.swift +++ /dev/null @@ -1,231 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit -@testable import WidgetTemplates -@testable import LiveDataIntegration - -final class WidgetIntegrationTests: QuickSpec { - override func spec() { - describe("Widget Integration") { - var widgetEngine: WidgetEngine! - var dataManager: WidgetDataManager! - var templateManager: WidgetTemplateManager! - - beforeEach { - widgetEngine = WidgetEngine() - dataManager = WidgetDataManager() - templateManager = WidgetTemplateManager() - } - - afterEach { - widgetEngine = nil - dataManager = nil - templateManager = nil - } - - context("Widget Template Integration") { - it("should create widget from template") { - let template = WidgetTemplate(name: "WeatherWidget", size: .medium) - template.setContent { context in - VStack { - Text("Weather") - .font(.headline) - Text("72°F") - .font(.title) - } - } - - let widget = templateManager.createWidget(from: template) { result in - expect(result).to(beSuccess()) - } - - expect(widget).toNot(beNil()) - expect(widget.kind).to(equal("WeatherWidget")) - } - - it("should apply template styling") { - let template = WidgetTemplate(name: "StyledWidget", size: .large) - template.applyStyle(.modern) - - let widget = templateManager.createWidget(from: template) { result in - expect(result).to(beSuccess()) - } - - expect(widget.style).to(equal(.modern)) - } - } - - context("Data Integration") { - it("should update widget with real-time data") { - let widget = HomeScreenWidget(kind: "DataWidget") - let dataSource = MockDataSource() - - dataManager.connect(dataSource) { result in - expect(result).to(beSuccess()) - - dataManager.onDataUpdate { data in - widget.update(with: data) { result in - expect(result).to(beSuccess()) - } - } - } - } - - it("should handle data caching") { - let cacheManager = WidgetCacheManager() - let data = WidgetData(id: "test", content: ["temperature": "75°F"]) - - cacheManager.cache(data) { result in - expect(result).to(beSuccess()) - - cacheManager.retrieve(id: "test") { result in - expect(result).to(beSuccess()) - expect(result.value?.content["temperature"]).to(equal("75°F")) - } - } - } - } - - context("Live Activity Integration") { - it("should create live activity from widget") { - let widget = LiveActivityWidget(kind: "ActivityWidget") - let activityManager = LiveActivityManager() - - activityManager.createActivity(from: widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should update live activity in real-time") { - let activity = LiveActivity(id: "test-activity") - let updateManager = LiveActivityUpdateManager() - - updateManager.update(activity, with: ["status": "active"]) { result in - expect(result).to(beSuccess()) - } - } - } - - context("Dynamic Island Integration") { - it("should create Dynamic Island activity") { - let islandManager = DynamicIslandManager() - let activity = DynamicIslandActivity(id: "island-activity") - - islandManager.start(activity) { result in - expect(result).to(beSuccess()) - } - } - - it("should handle Dynamic Island interactions") { - let activity = DynamicIslandActivity(id: "interactive-activity") - let interactionHandler = DynamicIslandInteractionHandler() - - interactionHandler.handleTap(for: activity) { result in - expect(result).to(beSuccess()) - } - } - } - - context("Widget Analytics") { - it("should track widget usage") { - let analytics = WidgetAnalytics() - let widget = HomeScreenWidget(kind: "AnalyticsWidget") - - analytics.trackWidgetView(widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should track widget interactions") { - let analytics = WidgetAnalytics() - let widget = InteractiveWidget(kind: "InteractiveWidget") - - analytics.trackInteraction(widget, action: "tap") { result in - expect(result).to(beSuccess()) - } - } - } - - context("Performance Integration") { - it("should handle multiple widget updates efficiently") { - let widgets = (0..<5).map { HomeScreenWidget(kind: "perf.\($0)") } - let updateManager = WidgetUpdateManager() - - measure { - for widget in widgets { - updateManager.update(widget, with: ["data": "test"]) { _ in } - } - } - } - - it("should manage memory efficiently") { - let memoryManager = WidgetMemoryManager() - let widgets = (0..<10).map { HomeScreenWidget(kind: "memory.\($0)") } - - for widget in widgets { - memoryManager.register(widget) { _ in } - } - - expect(memoryManager.activeWidgetCount).to(equal(10)) - - memoryManager.cleanup() { result in - expect(result).to(beSuccess()) - } - } - } - - context("Error Recovery") { - it("should recover from data source failure") { - let recoveryManager = WidgetRecoveryManager() - let failedDataSource = MockFailingDataSource() - - recoveryManager.handleDataSourceFailure(failedDataSource) { result in - expect(result).to(beSuccess()) - } - } - - it("should retry failed operations") { - let retryManager = WidgetRetryManager() - let operation = MockFailingOperation() - - retryManager.retry(operation, maxAttempts: 3) { result in - expect(result).to(beSuccess()) - } - } - } - } - } -} - -// MARK: - Mock Classes - -class MockDataSource: WidgetDataSource { - override func fetch() async throws -> WidgetData { - return WidgetData(id: "mock", content: ["test": "data"]) - } -} - -class MockFailingDataSource: WidgetDataSource { - override func fetch() async throws -> WidgetData { - throw WidgetError.dataFetchFailed - } -} - -class MockFailingOperation: WidgetOperation { - private var attemptCount = 0 - - override func execute() async throws { - attemptCount += 1 - if attemptCount < 3 { - throw WidgetError.operationFailed - } - } -} - -// MARK: - Widget Error - -enum WidgetError: Error { - case dataFetchFailed - case operationFailed -} \ No newline at end of file diff --git a/Tests/Integration/WidgetTemplatesTests.swift b/Tests/Integration/WidgetTemplatesTests.swift deleted file mode 100644 index 021f95d..0000000 --- a/Tests/Integration/WidgetTemplatesTests.swift +++ /dev/null @@ -1,492 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetTemplates - -@available(iOS 16.0, *) -class WidgetTemplatesTests: QuickSpec { - - override func spec() { - describe("Widget Templates") { - context("Weather Widget Template") { - var template: WeatherWidgetTemplate! - - beforeEach { - template = WeatherWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("weather_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Weather Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("weather conditions")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("weather_config")) - expect(configuration.templateIdentifier).to(equal("weather_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Calendar Widget Template") { - var template: CalendarWidgetTemplate! - - beforeEach { - template = CalendarWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("calendar_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Calendar Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("events")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("calendar_config")) - expect(configuration.templateIdentifier).to(equal("calendar_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Fitness Widget Template") { - var template: FitnessWidgetTemplate! - - beforeEach { - template = FitnessWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("fitness_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Fitness Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("fitness metrics")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("fitness_config")) - expect(configuration.templateIdentifier).to(equal("fitness_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("News Widget Template") { - var template: NewsWidgetTemplate! - - beforeEach { - template = NewsWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("news_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("News Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("news")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("news_config")) - expect(configuration.templateIdentifier).to(equal("news_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Social Media Widget Template") { - var template: SocialMediaWidgetTemplate! - - beforeEach { - template = SocialMediaWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("social_media_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Social Media Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("social media")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("social_config")) - expect(configuration.templateIdentifier).to(equal("social_media_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Productivity Widget Template") { - var template: ProductivityWidgetTemplate! - - beforeEach { - template = ProductivityWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("productivity_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Productivity Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("productivity")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("productivity_config")) - expect(configuration.templateIdentifier).to(equal("productivity_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Entertainment Widget Template") { - var template: EntertainmentWidgetTemplate! - - beforeEach { - template = EntertainmentWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("entertainment_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Entertainment Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("entertainment")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("entertainment_config")) - expect(configuration.templateIdentifier).to(equal("entertainment_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Finance Widget Template") { - var template: FinanceWidgetTemplate! - - beforeEach { - template = FinanceWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("finance_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Finance Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("financial")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("finance_config")) - expect(configuration.templateIdentifier).to(equal("finance_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Health Widget Template") { - var template: HealthWidgetTemplate! - - beforeEach { - template = HealthWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("health_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Health Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("health")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("health_config")) - expect(configuration.templateIdentifier).to(equal("health_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Travel Widget Template") { - var template: TravelWidgetTemplate! - - beforeEach { - template = TravelWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("travel_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Travel Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("travel")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("travel_config")) - expect(configuration.templateIdentifier).to(equal("travel_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - } - } -} \ No newline at end of file diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index b6d608a..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import iOS-Widget-Development-KitTests - -var tests = [XCTestCaseEntry]() -tests += iOS-Widget-Development-KitTests.allTests() -XCTMain(tests) diff --git a/Tests/Performance/LiveDataIntegrationTests.swift b/Tests/Performance/LiveDataIntegrationTests.swift deleted file mode 100644 index 3f3c648..0000000 --- a/Tests/Performance/LiveDataIntegrationTests.swift +++ /dev/null @@ -1,333 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import LiveDataIntegration - -@available(iOS 16.0, *) -class LiveDataIntegrationTests: QuickSpec { - - override func spec() { - describe("Live Data Integration Performance") { - var liveDataIntegration: LiveDataIntegration! - - beforeEach { - liveDataIntegration = LiveDataIntegration.shared - } - - context("Data Source Registration Performance") { - it("should register data sources quickly") { - let startTime = CFAbsoluteTimeGetCurrent() - - let weatherDataSource = WeatherDataSource() - liveDataIntegration.registerDataSource("weather_test", dataSource: weatherDataSource) - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.1)) // Should complete in less than 100ms - } - - it("should register multiple data sources efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - let dataSources = [ - WeatherDataSource(), - CalendarDataSource(), - FitnessDataSource(), - NewsDataSource(), - SocialMediaDataSource() - ] - - for (index, dataSource) in dataSources.enumerated() { - liveDataIntegration.registerDataSource("test_\(index)", dataSource: dataSource) - } - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.5)) // Should complete in less than 500ms - } - } - - context("WebSocket Connection Performance") { - it("should establish connection within reasonable time") { - let expectation = XCTestExpectation(description: "WebSocket connection") - - let startTime = CFAbsoluteTimeGetCurrent() - - // Note: This is a mock test since we can't actually connect to a real WebSocket - // In a real scenario, you would test with a mock WebSocket server - let mockURL = URL(string: "ws://localhost:8080")! - - liveDataIntegration.connectWebSocket(identifier: "test_connection", url: mockURL) - - // Simulate connection time - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.2)) // Should complete in less than 200ms - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - } - - it("should handle multiple connections efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - let urls = [ - URL(string: "ws://localhost:8081")!, - URL(string: "ws://localhost:8082")!, - URL(string: "ws://localhost:8083")! - ] - - for (index, url) in urls.enumerated() { - liveDataIntegration.connectWebSocket(identifier: "test_\(index)", url: url) - } - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.3)) // Should complete in less than 300ms - } - } - - context("Data Publishing Performance") { - it("should publish data quickly") { - let expectation = XCTestExpectation(description: "Data publishing") - - let startTime = CFAbsoluteTimeGetCurrent() - - let publisher = liveDataIntegration.getDataPublisher(for: "test_publisher") - - publisher?.sink { data in - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.05)) // Should complete in less than 50ms - expectation.fulfill() - } - .store(in: &Set()) - - // Simulate data publishing - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - let testData = WidgetData( - id: "test_data", - type: .weather, - content: ["temperature": 22] - ) - - // This would normally be called by the data source - // For testing, we're simulating the data flow - } - - wait(for: [expectation], timeout: 1.0) - } - - it("should handle high-frequency updates efficiently") { - let expectation = XCTestExpectation(description: "High-frequency updates") - expectation.expectedFulfillmentCount = 100 - - let startTime = CFAbsoluteTimeGetCurrent() - var updateCount = 0 - - let publisher = liveDataIntegration.getDataPublisher(for: "test_high_freq") - - publisher?.sink { data in - updateCount += 1 - - if updateCount == 100 { - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(1.0)) // Should complete in less than 1 second - expectation.fulfill() - } - } - .store(in: &Set()) - - // Simulate high-frequency updates - for i in 0..<100 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.01) { - let testData = WidgetData( - id: "test_data_\(i)", - type: .weather, - content: ["temperature": 20 + i] - ) - - // This would normally be called by the data source - } - } - - wait(for: [expectation], timeout: 5.0) - } - } - - context("Memory Usage Performance") { - it("should maintain reasonable memory usage") { - let initialMemory = getCurrentMemoryUsage() - - // Register multiple data sources - for i in 0..<50 { - let dataSource = WeatherDataSource() - liveDataIntegration.registerDataSource("memory_test_\(i)", dataSource: dataSource) - } - - let finalMemory = getCurrentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - // Memory increase should be reasonable (less than 10MB) - expect(memoryIncrease).to(beLessThan(10 * 1024 * 1024)) - } - - it("should clean up resources properly") { - let initialMemory = getCurrentMemoryUsage() - - // Register and then disconnect multiple connections - for i in 0..<20 { - let mockURL = URL(string: "ws://localhost:\(8080 + i)")! - liveDataIntegration.connectWebSocket(identifier: "cleanup_test_\(i)", url: mockURL) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - liveDataIntegration.disconnectWebSocket(identifier: "cleanup_test_\(i)") - } - } - - // Wait for cleanup - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - let finalMemory = getCurrentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - // Memory should not increase significantly after cleanup - expect(memoryIncrease).to(beLessThan(5 * 1024 * 1024)) - } - } - } - - context("Battery Optimization Performance") { - it("should reduce refresh frequency when battery is low") { - // Simulate low battery condition - UIDevice.current.isBatteryMonitoringEnabled = true - - let initialSettings = DataSourceSettings(refreshInterval: 30.0) - let lowBatterySettings = DataSourceSettings(refreshInterval: 60.0) - - let weatherDataSource = WeatherDataSource() - weatherDataSource.configure(initialSettings) - - // Simulate battery level drop - // Note: In a real test, you would need to mock the battery level - // This is a conceptual test - - expect(weatherDataSource).toNot(beNil()) - } - } - - context("Network Performance") { - it("should handle network interruptions gracefully") { - let expectation = XCTestExpectation(description: "Network handling") - - // Simulate network interruption - // In a real test, you would mock network conditions - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // Simulate network recovery - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - } - - it("should retry connections efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - // Simulate connection retries - for i in 0..<5 { - let mockURL = URL(string: "ws://localhost:\(9000 + i)")! - liveDataIntegration.connectWebSocket(identifier: "retry_test_\(i)", url: mockURL) - } - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.5)) // Should complete in less than 500ms - } - } - - context("Data Processing Performance") { - it("should process large data sets efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - // Simulate processing large data set - let largeDataSet = WidgetData( - id: "large_data", - type: .weather, - content: [ - "temperature": 22, - "humidity": 65, - "pressure": 1013, - "windSpeed": 12, - "windDirection": "NW", - "visibility": 10, - "uvIndex": 5, - "airQuality": "Good", - "forecast": Array(0..<24).map { hour in - ["hour": hour, "temp": 20 + hour % 5, "condition": "sunny"] - } - ] - ) - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.01)) // Should complete in less than 10ms - } - - it("should handle concurrent data processing") { - let expectation = XCTestExpectation(description: "Concurrent processing") - expectation.expectedFulfillmentCount = 10 - - let startTime = CFAbsoluteTimeGetCurrent() - - // Simulate concurrent data processing - DispatchQueue.concurrentPerform(iterations: 10) { index in - let testData = WidgetData( - id: "concurrent_\(index)", - type: .weather, - content: ["temperature": 20 + index] - ) - - DispatchQueue.main.async { - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 2.0) - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(1.0)) // Should complete in less than 1 second - } - } - } - } - - // Helper function to get current memory usage - private func getCurrentMemoryUsage() -> UInt64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size)/4 - - let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, - task_flavor_t(MACH_TASK_BASIC_INFO), - $0, - &count) - } - } - - return kerr == KERN_SUCCESS ? info.resident_size : 0 - } -} \ No newline at end of file diff --git a/Tests/Performance/WidgetPerformanceTests.swift b/Tests/Performance/WidgetPerformanceTests.swift deleted file mode 100644 index 99b6c7b..0000000 --- a/Tests/Performance/WidgetPerformanceTests.swift +++ /dev/null @@ -1,270 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit - -final class WidgetPerformanceTests: QuickSpec { - override func spec() { - describe("Widget Performance") { - var performanceMonitor: WidgetPerformanceMonitor! - - beforeEach { - performanceMonitor = WidgetPerformanceMonitor() - } - - afterEach { - performanceMonitor = nil - } - - context("Widget Rendering Performance") { - it("should render widget within acceptable time") { - let widget = HomeScreenWidget(kind: "perf.widget") - - measure { - widget.render { _ in } - } - } - - it("should handle complex widget layouts efficiently") { - let complexWidget = ComplexWidget(kind: "complex.widget") - - measure { - complexWidget.render { _ in } - } - } - } - - context("Data Processing Performance") { - it("should process large datasets efficiently") { - let largeDataset = generateLargeDataset(size: 1000) - let dataProcessor = WidgetDataProcessor() - - measure { - dataProcessor.process(largeDataset) { _ in } - } - } - - it("should cache data efficiently") { - let cacheManager = WidgetCacheManager() - let data = generateTestData(count: 100) - - measure { - for item in data { - cacheManager.cache(item) { _ in } - } - } - } - } - - context("Memory Usage") { - it("should maintain stable memory usage") { - let memoryTracker = WidgetMemoryTracker() - let widgets = (0..<50).map { HomeScreenWidget(kind: "memory.\($0)") } - - let initialMemory = memoryTracker.currentMemoryUsage() - - for widget in widgets { - widget.initialize { _ in } - } - - let finalMemory = memoryTracker.currentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - expect(memoryIncrease).to(beLessThan(50 * 1024 * 1024)) // 50MB limit - } - - it("should release memory when widgets are destroyed") { - let memoryTracker = WidgetMemoryTracker() - var widgets: [HomeScreenWidget] = [] - - let initialMemory = memoryTracker.currentMemoryUsage() - - for i in 0..<20 { - let widget = HomeScreenWidget(kind: "temp.\(i)") - widgets.append(widget) - widget.initialize { _ in } - } - - widgets.removeAll() - - let finalMemory = memoryTracker.currentMemoryUsage() - let memoryDifference = abs(finalMemory - initialMemory) - - expect(memoryDifference).to(beLessThan(10 * 1024 * 1024)) // 10MB tolerance - } - } - - context("Network Performance") { - it("should handle concurrent network requests efficiently") { - let networkManager = WidgetNetworkManager() - let requests = (0..<10).map { MockNetworkRequest(id: "req.\($0)") } - - measure { - for request in requests { - networkManager.execute(request) { _ in } - } - } - } - - it("should handle network timeouts gracefully") { - let networkManager = WidgetNetworkManager() - let slowRequest = MockSlowNetworkRequest() - - let startTime = Date() - - networkManager.execute(slowRequest) { result in - let endTime = Date() - let duration = endTime.timeIntervalSince(startTime) - - expect(duration).to(beLessThan(5.0)) // 5 second timeout - } - } - } - - context("Background Processing") { - it("should process background updates efficiently") { - let backgroundProcessor = WidgetBackgroundProcessor() - let updates = generateBackgroundUpdates(count: 100) - - measure { - backgroundProcessor.process(updates) { _ in } - } - } - - it("should handle background task scheduling") { - let taskScheduler = WidgetTaskScheduler() - let tasks = (0..<20).map { MockBackgroundTask(id: "task.\($0)") } - - measure { - for task in tasks { - taskScheduler.schedule(task) { _ in } - } - } - } - } - - context("UI Responsiveness") { - it("should maintain UI responsiveness during heavy operations") { - let uiMonitor = WidgetUIMonitor() - let heavyOperation = MockHeavyOperation() - - uiMonitor.startMonitoring() - - heavyOperation.execute { _ in - let responsiveness = uiMonitor.getResponsiveness() - expect(responsiveness).to(beGreaterThan(0.8)) // 80% responsiveness - } - } - - it("should handle rapid widget updates without lag") { - let updateManager = WidgetUpdateManager() - let widget = HomeScreenWidget(kind: "rapid.widget") - - let startTime = Date() - var updateCount = 0 - - for _ in 0..<100 { - updateManager.update(widget, with: ["data": "update"]) { _ in - updateCount += 1 - } - } - - let endTime = Date() - let duration = endTime.timeIntervalSince(startTime) - - expect(duration).to(beLessThan(1.0)) // 1 second for 100 updates - expect(updateCount).to(equal(100)) - } - } - } - } -} - -// MARK: - Helper Methods - -extension WidgetPerformanceTests { - func generateLargeDataset(size: Int) -> [WidgetData] { - return (0.. [WidgetData] { - return (0.. [WidgetUpdate] { - return (0..) -> Void) { - // Simulate complex rendering - DispatchQueue.global().async { - Thread.sleep(forTimeInterval: 0.1) - completion(.success(())) - } - } -} - -class MockNetworkRequest: WidgetNetworkRequest { - let id: String - - init(id: String) { - self.id = id - } - - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - completion(.success(Data())) - } - } -} - -class MockSlowNetworkRequest: WidgetNetworkRequest { - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - Thread.sleep(forTimeInterval: 3.0) - completion(.failure(WidgetError.networkTimeout)) - } - } -} - -class MockBackgroundTask: WidgetBackgroundTask { - let id: String - - init(id: String) { - self.id = id - } - - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - completion(.success(())) - } - } -} - -class MockHeavyOperation: WidgetOperation { - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - // Simulate heavy processing - for _ in 0..<1000000 { - _ = sqrt(Double.random(in: 0...1000)) - } - completion(.success(())) - } - } -} - -// MARK: - Additional Errors - -extension WidgetError { - static let networkTimeout = WidgetError.operationFailed -} \ No newline at end of file diff --git a/Tests/Unit/WidgetAnalyticsTests.swift b/Tests/Unit/WidgetAnalyticsTests.swift deleted file mode 100644 index 4243fd7..0000000 --- a/Tests/Unit/WidgetAnalyticsTests.swift +++ /dev/null @@ -1,314 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetAnalytics - -@available(iOS 16.0, *) -class WidgetAnalyticsTests: QuickSpec { - - override func spec() { - describe("WidgetAnalyticsService") { - var analyticsService: WidgetAnalyticsService! - - beforeEach { - analyticsService = WidgetAnalyticsService() - } - - context("when logging events") { - it("should log widget updates") { - analyticsService.logWidgetUpdate(widgetId: "test_widget") - let analytics = analyticsService.getAnalytics() - - expect(analytics.totalWidgetUpdates).to(equal(1)) - expect(analytics.widgetUpdates["test_widget"]).to(equal(1)) - } - - it("should log template registrations") { - analyticsService.logTemplateRegistration("weather_template") - let analytics = analyticsService.getAnalytics() - - expect(analytics.templateRegistrations).to(equal(1)) - } - - it("should log data source registrations") { - analyticsService.logDataSourceRegistration("weather_api") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSourceRegistrations).to(equal(1)) - } - - it("should log WebSocket connections") { - let url = URL(string: "ws://test.com")! - analyticsService.logWebSocketConnection(identifier: "test_ws", url: url.absoluteString) - let analytics = analyticsService.getAnalytics() - - expect(analytics.webSocketConnections).to(equal(1)) - } - - it("should log WebSocket disconnections") { - analyticsService.logWebSocketDisconnection(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.webSocketDisconnections).to(equal(1)) - } - - it("should log data sent") { - analyticsService.logDataSent(widgetId: "test_widget", dataType: "weather") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSentCount).to(equal(1)) - } - - it("should log data source updates") { - analyticsService.logDataSourceUpdate(identifier: "weather_api", dataType: "weather") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSourceUpdates).to(equal(1)) - } - - it("should log data source configurations") { - analyticsService.logDataSourceConfiguration(identifier: "weather_api") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSourceConfigurations).to(equal(1)) - } - - it("should log connection established") { - analyticsService.logConnectionEstablished(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionsEstablished).to(equal(1)) - } - - it("should log connection failures") { - let error = NSError(domain: "test", code: 1, userInfo: nil) - analyticsService.logConnectionFailed(identifier: "test_ws", error: error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionFailures).to(equal(1)) - } - - it("should log connection cancellations") { - analyticsService.logConnectionCancelled(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionsCancelled).to(equal(1)) - } - - it("should log connection retries") { - analyticsService.logConnectionRetry(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionRetries).to(equal(1)) - } - - it("should log reconnection attempts") { - analyticsService.logReconnectionAttempt(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.reconnectionAttempts).to(equal(1)) - } - - it("should log data receive errors") { - let error = NSError(domain: "test", code: 1, userInfo: nil) - analyticsService.logDataReceiveError(identifier: "test_ws", error: error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataReceiveErrors).to(equal(1)) - } - - it("should log data decode errors") { - let error = NSError(domain: "test", code: 1, userInfo: nil) - analyticsService.logDataDecodeError(identifier: "test_ws", error: error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataDecodeErrors).to(equal(1)) - } - - it("should log network unavailable events") { - analyticsService.logNetworkUnavailable() - let analytics = analyticsService.getAnalytics() - - expect(analytics.networkUnavailableEvents).to(equal(1)) - } - - it("should log memory cleanups") { - analyticsService.logMemoryCleanup(removedEntries: 5) - let analytics = analyticsService.getAnalytics() - - expect(analytics.memoryCleanups).to(equal(1)) - } - - it("should log battery optimizations") { - analyticsService.logBatteryOptimization(newInterval: 60.0) - let analytics = analyticsService.getAnalytics() - - expect(analytics.batteryOptimizations).to(equal(1)) - } - - it("should log errors") { - let error = WidgetError.templateNotFound("test_template") - analyticsService.logError(error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.errors).to(equal(1)) - } - - it("should log engine initializations") { - analyticsService.logEngineInitialization() - let analytics = analyticsService.getAnalytics() - - expect(analytics.engineInitializations).to(equal(1)) - } - } - - context("when configuring analytics") { - it("should configure analytics settings") { - let config = AnalyticsConfiguration( - enableExternalAnalytics: true, - maxEventLogSize: 500, - enablePerformanceTracking: true, - enableErrorTracking: true - ) - - analyticsService.configure(config) - - // Verify configuration was applied - expect(analyticsService).toNot(beNil()) - } - } - - context("when exporting analytics") { - it("should export analytics data") { - analyticsService.logWidgetUpdate(widgetId: "test_widget") - - let exportedData = analyticsService.exportAnalytics() - - expect(exportedData).toNot(beNil()) - } - } - - context("when clearing analytics") { - it("should clear analytics data") { - analyticsService.logWidgetUpdate(widgetId: "test_widget") - analyticsService.clearAnalytics() - - let analytics = analyticsService.getAnalytics() - - expect(analytics.totalWidgetUpdates).to(equal(0)) - } - } - } - - describe("WidgetAnalytics") { - context("when creating analytics data") { - it("should have default values") { - let analytics = WidgetAnalytics() - - expect(analytics.totalWidgetUpdates).to(equal(0)) - expect(analytics.templateRegistrations).to(equal(0)) - expect(analytics.dataSourceRegistrations).to(equal(0)) - expect(analytics.webSocketConnections).to(equal(0)) - expect(analytics.webSocketDisconnections).to(equal(0)) - expect(analytics.dataSentCount).to(equal(0)) - expect(analytics.dataSourceUpdates).to(equal(0)) - expect(analytics.dataSourceConfigurations).to(equal(0)) - expect(analytics.connectionsEstablished).to(equal(0)) - expect(analytics.connectionFailures).to(equal(0)) - expect(analytics.connectionsCancelled).to(equal(0)) - expect(analytics.connectionRetries).to(equal(0)) - expect(analytics.reconnectionAttempts).to(equal(0)) - expect(analytics.dataReceiveErrors).to(equal(0)) - expect(analytics.dataDecodeErrors).to(equal(0)) - expect(analytics.networkUnavailableEvents).to(equal(0)) - expect(analytics.memoryCleanups).to(equal(0)) - expect(analytics.batteryOptimizations).to(equal(0)) - expect(analytics.errors).to(equal(0)) - expect(analytics.engineInitializations).to(equal(0)) - } - } - } - - describe("AnalyticsConfiguration") { - context("when creating configuration") { - it("should have default values") { - let config = AnalyticsConfiguration() - - expect(config.enableExternalAnalytics).to(beFalse()) - expect(config.maxEventLogSize).to(equal(1000)) - expect(config.enablePerformanceTracking).to(beTrue()) - expect(config.enableErrorTracking).to(beTrue()) - } - - it("should have custom values") { - let config = AnalyticsConfiguration( - enableExternalAnalytics: true, - maxEventLogSize: 500, - enablePerformanceTracking: false, - enableErrorTracking: false - ) - - expect(config.enableExternalAnalytics).to(beTrue()) - expect(config.maxEventLogSize).to(equal(500)) - expect(config.enablePerformanceTracking).to(beFalse()) - expect(config.enableErrorTracking).to(beFalse()) - } - } - } - - describe("AnalyticsEventType") { - context("when using event types") { - it("should have all required event types") { - let eventTypes = AnalyticsEventType.allCases - - expect(eventTypes).to(contain(.widgetUpdate)) - expect(eventTypes).to(contain(.templateRegistration)) - expect(eventTypes).to(contain(.dataSourceRegistration)) - expect(eventTypes).to(contain(.webSocketConnection)) - expect(eventTypes).to(contain(.webSocketDisconnection)) - expect(eventTypes).to(contain(.dataSent)) - expect(eventTypes).to(contain(.dataSourceUpdate)) - expect(eventTypes).to(contain(.dataSourceConfiguration)) - expect(eventTypes).to(contain(.connectionEstablished)) - expect(eventTypes).to(contain(.connectionFailed)) - expect(eventTypes).to(contain(.connectionCancelled)) - expect(eventTypes).to(contain(.connectionRetry)) - expect(eventTypes).to(contain(.reconnectionAttempt)) - expect(eventTypes).to(contain(.dataReceiveError)) - expect(eventTypes).to(contain(.dataDecodeError)) - expect(eventTypes).to(contain(.networkUnavailable)) - expect(eventTypes).to(contain(.memoryCleanup)) - expect(eventTypes).to(contain(.batteryOptimization)) - expect(eventTypes).to(contain(.error)) - expect(eventTypes).to(contain(.engineInitialization)) - } - - it("should have correct raw values") { - expect(AnalyticsEventType.widgetUpdate.rawValue).to(equal("widget_update")) - expect(AnalyticsEventType.templateRegistration.rawValue).to(equal("template_registration")) - expect(AnalyticsEventType.dataSourceRegistration.rawValue).to(equal("data_source_registration")) - } - } - } - - describe("WidgetError") { - context("when creating errors") { - it("should have correct error descriptions") { - let templateError = WidgetError.templateNotFound("test_template") - let dataSourceError = WidgetError.dataSourceNotFound("test_api") - let connectionError = WidgetError.connectionFailed("Network timeout") - let decodeError = WidgetError.dataDecodeFailed("Invalid JSON") - let networkError = WidgetError.networkUnavailable - let unknownError = WidgetError.unknown - - expect(templateError.localizedDescription).to(contain("Template not found")) - expect(dataSourceError.localizedDescription).to(contain("Data source not found")) - expect(connectionError.localizedDescription).to(contain("Connection failed")) - expect(decodeError.localizedDescription).to(contain("Data decode failed")) - expect(networkError.localizedDescription).to(equal("Network unavailable")) - expect(unknownError.localizedDescription).to(equal("Unknown error")) - } - } - } - } -} \ No newline at end of file diff --git a/Tests/Unit/WidgetDataManagerTests.swift b/Tests/Unit/WidgetDataManagerTests.swift deleted file mode 100644 index e5a828c..0000000 --- a/Tests/Unit/WidgetDataManagerTests.swift +++ /dev/null @@ -1,349 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit -@testable import LiveDataIntegration - -final class WidgetDataManagerTests: QuickSpec { - override func spec() { - describe("WidgetDataManager") { - var dataManager: WidgetDataManager! - var mockDataSource: MockWidgetDataSource! - - beforeEach { - dataManager = WidgetDataManager() - mockDataSource = MockWidgetDataSource() - } - - afterEach { - dataManager = nil - mockDataSource = nil - } - - context("Data Source Connection") { - it("should connect to data source successfully") { - dataManager.connect(mockDataSource) { result in - expect(result).to(beSuccess()) - } - } - - it("should handle connection failure") { - let failingDataSource = MockFailingDataSource() - - dataManager.connect(failingDataSource) { result in - expect(result).to(beFailure()) - } - } - - it("should disconnect from data source") { - dataManager.connect(mockDataSource) { _ in - dataManager.disconnect { result in - expect(result).to(beSuccess()) - } - } - } - } - - context("Data Fetching") { - it("should fetch data successfully") { - dataManager.connect(mockDataSource) { _ in - dataManager.fetchData { result in - expect(result).to(beSuccess()) - expect(result.value?.id).to(equal("test-data")) - } - } - } - - it("should handle fetch failure") { - let failingDataSource = MockFailingDataSource() - - dataManager.connect(failingDataSource) { _ in - dataManager.fetchData { result in - expect(result).to(beFailure()) - } - } - } - - it("should fetch data with caching") { - dataManager.enableCaching = true - dataManager.connect(mockDataSource) { _ in - dataManager.fetchData { result in - expect(result).to(beSuccess()) - - // Second fetch should use cache - dataManager.fetchData { cachedResult in - expect(cachedResult).to(beSuccess()) - expect(cachedResult.value?.id).to(equal("test-data")) - } - } - } - } - } - - context("Real-time Updates") { - it("should receive real-time updates") { - let expectation = XCTestExpectation(description: "Real-time update") - - dataManager.connect(mockDataSource) { _ in - dataManager.onDataUpdate { data in - expect(data.id).to(equal("real-time-data")) - expectation.fulfill() - } - - mockDataSource.simulateUpdate() - } - - wait(for: [expectation], timeout: 2.0) - } - - it("should handle multiple subscribers") { - let expectation1 = XCTestExpectation(description: "Subscriber 1") - let expectation2 = XCTestExpectation(description: "Subscriber 2") - - dataManager.connect(mockDataSource) { _ in - dataManager.onDataUpdate { _ in - expectation1.fulfill() - } - - dataManager.onDataUpdate { _ in - expectation2.fulfill() - } - - mockDataSource.simulateUpdate() - } - - wait(for: [expectation1, expectation2], timeout: 2.0) - } - } - - context("Data Caching") { - it("should cache data successfully") { - let testData = WidgetData(id: "cache-test", content: ["key": "value"]) - - dataManager.cache(testData) { result in - expect(result).to(beSuccess()) - } - } - - it("should retrieve cached data") { - let testData = WidgetData(id: "retrieve-test", content: ["key": "value"]) - - dataManager.cache(testData) { _ in - dataManager.retrieve(id: "retrieve-test") { result in - expect(result).to(beSuccess()) - expect(result.value?.content["key"]).to(equal("value")) - } - } - } - - it("should handle cache miss") { - dataManager.retrieve(id: "non-existent") { result in - expect(result).to(beFailure()) - } - } - - it("should clear cache") { - let testData = WidgetData(id: "clear-test", content: ["key": "value"]) - - dataManager.cache(testData) { _ in - dataManager.clearCache { result in - expect(result).to(beSuccess()) - - dataManager.retrieve(id: "clear-test") { result in - expect(result).to(beFailure()) - } - } - } - } - } - - context("Data Validation") { - it("should validate data format") { - let validData = WidgetData(id: "valid", content: ["key": "value"]) - let invalidData = WidgetData(id: "", content: [:]) - - dataManager.validateData(validData) { result in - expect(result).to(beSuccess()) - } - - dataManager.validateData(invalidData) { result in - expect(result).to(beFailure()) - } - } - - it("should handle data transformation") { - let rawData = ["temperature": "72", "humidity": "65"] - - dataManager.transformData(rawData) { result in - expect(result).to(beSuccess()) - expect(result.value?.content["temperature"]).to(equal("72°F")) - } - } - } - - context("Error Handling") { - it("should handle network errors") { - let networkErrorDataSource = MockNetworkErrorDataSource() - - dataManager.connect(networkErrorDataSource) { result in - expect(result).to(beFailure()) - } - } - - it("should handle timeout errors") { - let timeoutDataSource = MockTimeoutDataSource() - - dataManager.connect(timeoutDataSource) { result in - expect(result).to(beFailure()) - } - } - - it("should retry failed operations") { - let retryDataSource = MockRetryDataSource() - - dataManager.connect(retryDataSource) { result in - expect(result).to(beSuccess()) - } - } - } - - context("Performance") { - it("should handle large datasets efficiently") { - let largeDataSource = MockLargeDataSource() - - measure { - dataManager.connect(largeDataSource) { _ in - dataManager.fetchData { _ in } - } - } - } - - it("should handle concurrent operations") { - let expectation = XCTestExpectation(description: "Concurrent operations") - expectation.expectedFulfillmentCount = 5 - - for i in 0..<5 { - DispatchQueue.global().async { - dataManager.fetchData { _ in - expectation.fulfill() - } - } - } - - wait(for: [expectation], timeout: 5.0) - } - } - } - } -} - -// MARK: - Mock Classes - -class MockWidgetDataSource: WidgetDataSource { - private var updateCallbacks: [() -> Void] = [] - - func fetch() async throws -> WidgetData { - return WidgetData(id: "test-data", content: ["key": "value"]) - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - updateCallbacks.append { - updates(WidgetData(id: "real-time-data", content: ["updated": "true"])) - } - } - - func unsubscribe() { - updateCallbacks.removeAll() - } - - func simulateUpdate() { - updateCallbacks.forEach { $0() } - } -} - -class MockFailingDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - throw WidgetError.dataFetchFailed - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockNetworkErrorDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - throw WidgetError.networkError - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockTimeoutDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - try await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds - throw WidgetError.networkTimeout - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockRetryDataSource: WidgetDataSource { - private var attemptCount = 0 - - func fetch() async throws -> WidgetData { - attemptCount += 1 - if attemptCount < 3 { - throw WidgetError.dataFetchFailed - } - return WidgetData(id: "retry-success", content: ["attempts": "\(attemptCount)"]) - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockLargeDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - var largeContent: [String: String] = [:] - for i in 0..<1000 { - largeContent["key\(i)"] = "value\(i)" - } - return WidgetData(id: "large-data", content: largeContent) - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -// MARK: - Additional Errors - -extension WidgetError { - static let networkTimeout = WidgetError.networkError -} \ No newline at end of file diff --git a/Tests/Unit/WidgetEngineTests.swift b/Tests/Unit/WidgetEngineTests.swift deleted file mode 100644 index 0d0bcef..0000000 --- a/Tests/Unit/WidgetEngineTests.swift +++ /dev/null @@ -1,190 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit - -final class WidgetEngineTests: QuickSpec { - override func spec() { - describe("WidgetEngine") { - var widgetEngine: WidgetEngine! - - beforeEach { - widgetEngine = WidgetEngine() - } - - afterEach { - widgetEngine = nil - } - - context("Initialization") { - it("should initialize with default configuration") { - expect(widgetEngine).toNot(beNil()) - expect(widgetEngine.isRunning).to(beFalse()) - } - - it("should have correct default settings") { - expect(widgetEngine.configuration.enableHomeScreenWidgets).to(beTrue()) - expect(widgetEngine.configuration.enableLockScreenWidgets).to(beTrue()) - expect(widgetEngine.configuration.enableLiveActivities).to(beTrue()) - } - } - - context("Widget Registration") { - it("should register home screen widget successfully") { - let widget = HomeScreenWidget(kind: "test.widget") - - widgetEngine.register(widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should register lock screen widget successfully") { - let widget = LockScreenWidget(kind: "test.lockwidget") - - widgetEngine.register(widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should fail to register widget with invalid kind") { - let widget = HomeScreenWidget(kind: "") - - widgetEngine.register(widget) { result in - expect(result).to(beFailure()) - } - } - } - - context("Widget Management") { - it("should start widget engine successfully") { - widgetEngine.start { result in - expect(result).to(beSuccess()) - expect(widgetEngine.isRunning).to(beTrue()) - } - } - - it("should stop widget engine successfully") { - widgetEngine.start { _ in - widgetEngine.stop { result in - expect(result).to(beSuccess()) - expect(widgetEngine.isRunning).to(beFalse()) - } - } - } - - it("should update widget configuration") { - let newConfig = WidgetConfiguration() - newConfig.enableHomeScreenWidgets = false - - widgetEngine.updateConfiguration(newConfig) { result in - expect(result).to(beSuccess()) - expect(widgetEngine.configuration.enableHomeScreenWidgets).to(beFalse()) - } - } - } - - context("Data Integration") { - it("should fetch widget data successfully") { - let dataSource = WidgetDataSource(type: .api, endpoint: "https://api.test.com") - - widgetEngine.fetchData(from: dataSource) { result in - expect(result).to(beSuccess()) - } - } - - it("should handle data fetch failure") { - let dataSource = WidgetDataSource(type: .api, endpoint: "https://invalid-url.com") - - widgetEngine.fetchData(from: dataSource) { result in - expect(result).to(beFailure()) - } - } - } - - context("Performance") { - it("should register multiple widgets efficiently") { - let widgets = (0..<10).map { HomeScreenWidget(kind: "widget.\($0)") } - - measure { - for widget in widgets { - widgetEngine.register(widget) { _ in } - } - } - } - - it("should handle concurrent widget updates") { - let expectation = XCTestExpectation(description: "Concurrent updates") - expectation.expectedFulfillmentCount = 5 - - for i in 0..<5 { - DispatchQueue.global().async { - let widget = HomeScreenWidget(kind: "concurrent.\(i)") - widgetEngine.register(widget) { _ in - expectation.fulfill() - } - } - } - - wait(for: [expectation], timeout: 5.0) - } - } - - context("Error Handling") { - it("should handle invalid widget configuration") { - let invalidConfig = WidgetConfiguration() - invalidConfig.refreshInterval = -1 - - widgetEngine.updateConfiguration(invalidConfig) { result in - expect(result).to(beFailure()) - } - } - - it("should handle network errors gracefully") { - let dataSource = WidgetDataSource(type: .api, endpoint: "https://unreachable-url.com") - - widgetEngine.fetchData(from: dataSource) { result in - expect(result).to(beFailure()) - } - } - } - } - } -} - -// MARK: - Test Helpers - -extension Result { - func beSuccess() -> Predicate { - return Predicate { actual in - let message = ExpectationMessage.expectedActualValueTo("be success") - - guard let actual = try actual.evaluate() else { - return PredicateResult(status: .fail, message: message.appendedBeNilHint()) - } - - switch actual { - case .success: - return PredicateResult(status: .matches, message: message) - case .failure: - return PredicateResult(status: .fail, message: message) - } - } - } - - func beFailure() -> Predicate { - return Predicate { actual in - let message = ExpectationMessage.expectedActualValueTo("be failure") - - guard let actual = try actual.evaluate() else { - return PredicateResult(status: .fail, message: message.appendedBeNilHint()) - } - - switch actual { - case .success: - return PredicateResult(status: .fail, message: message) - case .failure: - return PredicateResult(status: .matches, message: message) - } - } - } -} \ No newline at end of file diff --git a/Tests/UnitTests.swift b/Tests/UnitTests.swift deleted file mode 100644 index 7a129a2..0000000 --- a/Tests/UnitTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -import XCTest -@testable import REPO_NAME - -/** - * Unit Tests for REPO_NAME Framework - * - * Comprehensive test coverage for all core functionality. - * Ensures reliability and stability of the framework. - */ - -final class REPO_NAMEUnitTests: XCTestCase { - - // MARK: - Test Properties - var sut: MainFramework! - - // MARK: - Setup and Teardown - override func setUp() { - super.setUp() - sut = MainFramework() - } - - override func tearDown() { - sut = nil - super.tearDown() - } - - // MARK: - Initialization Tests - func testInitialization() { - // Given - let framework = MainFramework() - - // Then - XCTAssertNotNil(framework, "Framework should be initialized successfully") - } - - func testDefaultConfiguration() { - // Given - let framework = MainFramework() - - // Then - XCTAssertFalse(framework.isRunning, "Framework should not be running by default") - XCTAssertNotNil(framework.configuration, "Configuration should be set by default") - } -} diff --git a/Tests/iOS-Widget-Development-KitTests.swift b/Tests/iOS-Widget-Development-KitTests.swift deleted file mode 100644 index 06c27a2..0000000 --- a/Tests/iOS-Widget-Development-KitTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -import XCTest -@testable import iOS-Widget-Development-Kit - -final class iOS-Widget-Development-KitTests: XCTestCase { - var framework: iOS-Widget-Development-Kit! - - override func setUpWithError() throws { - framework = iOS-Widget-Development-Kit() - } - - override func tearDownWithError() throws { - framework = nil - } - - func testInitialization() throws { - // Test basic initialization - XCTAssertNotNil(framework) - XCTAssertTrue(framework is iOS-Widget-Development-Kit) - } - - func testConfiguration() throws { - // Test configuration - XCTAssertNoThrow(framework.configure()) - } - - func testPerformance() throws { - // Performance test - measure { - framework.configure() - } - } - - func testErrorHandling() throws { - // Test error scenarios - // Add your error handling tests here - XCTAssertTrue(true) // Placeholder - } - - static var allTests = [ - ("testInitialization", testInitialization), - ("testConfiguration", testConfiguration), - ("testPerformance", testPerformance), - ("testErrorHandling", testErrorHandling) - ] -} diff --git a/WidgetDevelopmentKit.podspec b/WidgetDevelopmentKit.podspec deleted file mode 100644 index 9d9478e..0000000 --- a/WidgetDevelopmentKit.podspec +++ /dev/null @@ -1,55 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'WidgetDevelopmentKit' - s.version = '1.0.0' - s.summary = 'The complete toolkit for building beautiful iOS widgets with WidgetKit, Live Activities, and Dynamic Island.' - s.description = <<-DESC - WidgetDevelopmentKit provides everything you need to build stunning iOS widgets. - Features include Home Screen Widgets, Lock Screen Widgets, Live Activities, - StandBy Mode support, Interactive Widgets with App Intents, and 20+ pre-built - templates. Build beautiful widgets in minutes, not days. - DESC - - s.homepage = 'https://github.com/muhittincamdali/iOS-Widget-Development-Kit' - s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'Muhittin Camdali' => 'contact@muhittincamdali.com' } - s.source = { :git => 'https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git', :tag => s.version.to_s } - s.social_media_url = 'https://twitter.com/muhittincamdali' - - s.ios.deployment_target = '16.0' - s.osx.deployment_target = '13.0' - s.tvos.deployment_target = '16.0' - s.watchos.deployment_target = '9.0' - - s.swift_versions = ['5.9', '5.10', '6.0'] - - s.source_files = 'Sources/**/*.swift' - - s.frameworks = 'Foundation', 'SwiftUI', 'WidgetKit' - - s.default_subspecs = 'Core' - - s.subspec 'Core' do |core| - core.source_files = 'Sources/Core/**/*.swift' - end - - s.subspec 'Security' do |security| - security.source_files = 'Sources/Security/**/*.swift' - security.dependency 'WidgetDevelopmentKit/Core' - end - - s.subspec 'Performance' do |performance| - performance.source_files = 'Sources/Performance/**/*.swift' - performance.dependency 'WidgetDevelopmentKit/Core' - end - - s.subspec 'Enterprise' do |enterprise| - enterprise.source_files = 'Sources/Enterprise/**/*.swift' - enterprise.dependency 'WidgetDevelopmentKit/Core' - enterprise.dependency 'WidgetDevelopmentKit/Security' - end - - s.subspec 'Integration' do |integration| - integration.source_files = 'Sources/Integration/**/*.swift' - integration.dependency 'WidgetDevelopmentKit/Core' - end -end From c515e4463e4a8cdbed3fb98673e9a0ca1ffef43e Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:45:38 +0300 Subject: [PATCH 02/59] build: move manifest to Swift 6 tools with iOS 17 and macOS 14 Drops the swift-async-algorithms dependency, which no source file used. --- Package.resolved | 23 ----------------------- Package.swift | 22 +++++----------------- 2 files changed, 5 insertions(+), 40 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 0d70cbb..0000000 --- a/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms", - "state" : { - "revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", - "version" : "1.6.0" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index 367bd69..c06be32 100644 --- a/Package.swift +++ b/Package.swift @@ -1,31 +1,19 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "WidgetDevelopmentKit", platforms: [ - .iOS(.v16), - .macOS(.v13), - .watchOS(.v9), - .tvOS(.v16), - .visionOS(.v1) + .iOS(.v17), + .macOS(.v14) ], products: [ - .library(name: "WidgetDevelopmentKit", targets: ["WidgetDevelopmentKit"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), + .library(name: "WidgetDevelopmentKit", targets: ["WidgetDevelopmentKit"]) ], targets: [ .target( name: "WidgetDevelopmentKit", - dependencies: [ - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - ], - path: "Sources/WidgetDevelopmentKit", - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") - ] + path: "Sources/WidgetDevelopmentKit" ) ] ) From b4bb13d319297d9bb6a1b4c7f1e0df107e21c29c Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:00:41 +0300 Subject: [PATCH 03/59] refactor: remove placeholder APIs - Remove the no-op WidgetDevelopmentKit singleton, the hard-coded version constant and the WidgetConfiguration struct, which shadowed SwiftUI's WidgetConfiguration protocol for any file importing both modules. - Remove TimelinePrewarmer: it only called reloadTimelines and did not do the usage-based scheduling its documentation described. - Remove WidgetGallery; the restored template views cover the same cases. --- .../Core/WidgetCore.swift | 24 ----- .../Templates/WidgetGallery.swift | 94 ------------------- .../Timeline/TimelinePrewarmer.swift | 19 ---- .../WidgetDevelopmentKit.swift | 14 --- 4 files changed, 151 deletions(-) delete mode 100644 Sources/WidgetDevelopmentKit/Core/WidgetCore.swift delete mode 100644 Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift delete mode 100644 Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift delete mode 100644 Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift diff --git a/Sources/WidgetDevelopmentKit/Core/WidgetCore.swift b/Sources/WidgetDevelopmentKit/Core/WidgetCore.swift deleted file mode 100644 index fdc324e..0000000 --- a/Sources/WidgetDevelopmentKit/Core/WidgetCore.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import WidgetKit - -/// Main Widget Development Kit namespace -public enum WidgetDevKit { - public static let version = "2.0.0" -} - -/// A protocol for data sources that provide data to widgets -public protocol WidgetDataSource: Sendable { - associatedtype DataType: Codable & Sendable - func fetchData() async throws -> DataType -} - -/// A standard configuration for widgets -public struct WidgetConfiguration: Codable, Equatable, Sendable { - public let identifier: String - public let refreshInterval: TimeInterval - - public init(identifier: String, refreshInterval: TimeInterval = 3600) { - self.identifier = identifier - self.refreshInterval = refreshInterval - } -} diff --git a/Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift b/Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift deleted file mode 100644 index c83789b..0000000 --- a/Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift +++ /dev/null @@ -1,94 +0,0 @@ -import SwiftUI -import WidgetKit - -/// A world-class gallery of pre-built, copy-paste ready Widget Templates. -/// -/// These templates are designed to dominate the iOS 17/18 Widget space with -/// Interactive Widgets and StandBy mode support. -@available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) -public enum WidgetGallery { - - /// A high-conversion Crypto Tracker Widget. - public struct CryptoWidget: View { - public let coinName: String - public let price: String - public let trend: String - public let isPositive: Bool - - public init(coinName: String, price: String, trend: String, isPositive: Bool) { - self.coinName = coinName - self.price = price - self.trend = trend - self.isPositive = isPositive - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "bitcoinsign.circle.fill") - .foregroundColor(.orange) - Text(coinName) - .font(.headline) - .foregroundColor(.primary) - } - - Spacer() - - Text(price) - .font(.system(.title, design: .rounded).bold()) - .foregroundColor(.primary) - - Text(trend) - .font(.subheadline.bold()) - .foregroundColor(isPositive ? .green : .red) - } - .padding() - .containerBackground(for: .widget) { - #if canImport(UIKit) - Color(uiColor: .systemBackground) - #else - Color.white - #endif - } - } - } - - /// An interactive Health & Fitness Goal Widget. - public struct FitnessWidget: View { - public let steps: Int - public let goal: Int - - public init(steps: Int, goal: Int) { - self.steps = steps - self.goal = goal - } - - public var body: some View { - VStack { - HStack { - Image(systemName: "figure.walk") - .foregroundColor(.blue) - Text("Daily Steps") - .font(.subheadline.bold()) - Spacer() - } - - Spacer() - - Gauge(value: Double(steps), in: 0...Double(goal)) { - Text("\(steps)") - } - .gaugeStyle(.accessoryCircular) - .tint(Gradient(colors: [.blue, .purple])) - } - .padding() - .containerBackground(for: .widget) { - #if canImport(UIKit) - Color(uiColor: .secondarySystemBackground) - #else - Color.gray.opacity(0.1) - #endif - } - } - } -} diff --git a/Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift b/Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift deleted file mode 100644 index 6a873cc..0000000 --- a/Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import WidgetKit - -/// iOS-Widget-Development-Kit: Smart Timeline Pre-warmer. -/// -/// Intelligently schedules background updates to ensure data is fresh -/// exactly when the user is most likely to check their home screen. -public actor TimelinePrewarmer { - public static let shared = TimelinePrewarmer() - - private init() {} - - /// Schedules a pre-emptive refresh based on usage patterns. - public func scheduleOptimalRefresh(for kind: String) { - print("⏱️ [WidgetKit] Pre-warming timeline for: \\(kind)") - // Logic to calculate usage peak and trigger WidgetCenter.reloadTimelines - WidgetCenter.shared.reloadTimelines(ofKind: kind) - } -} diff --git a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift deleted file mode 100644 index 6cb1cd0..0000000 --- a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import SwiftUI - -/// WidgetDevelopmentKit provides tools for building modern, high-performance iOS widgets. -@MainActor -public final class WidgetDevelopmentKit { - public static let shared = WidgetDevelopmentKit() - - private init() {} - - public func configure() { - // Setup logic here - } -} From 5cbca9857cf4d12a76ddbc154f43e3891a60c12c Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:00:41 +0300 Subject: [PATCH 04/59] fix(live-activities): report rejected transitions in LiveActivityStateMachine - Drop the print call, which logged a literal "\(newState)" because the interpolation was escaped. - Throw StateMachineError.invalidTransition(from:to:) with typed throws so callers can see which transition was rejected. - Store allowed transitions as sets, expose currentState as a read-only property instead of getCurrentState(), and add canTransition(to:) and allowedNextStates(). --- .../LiveActivityStateMachine.swift | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift b/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift index 2c9c403..d036e11 100644 --- a/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift +++ b/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift @@ -1,34 +1,48 @@ import Foundation -/// iOS-Widget-Development-Kit: Live Activity State-Machine. -/// -/// Manages complex state transitions for Live Activities using a -/// strictly-typed, high-integrity state machine. +/// A small state machine for driving Live Activity content through allowed phases. +/// +/// Live Activity updates often arrive out of order (push, background refresh, app +/// launch). Declaring the allowed transitions up front lets the app reject an update +/// that would move an activity backwards, for example from `.delivered` to `.preparing`. +/// +/// ```swift +/// enum Phase: Hashable, Sendable { case preparing, onTheWay, delivered } +/// +/// let machine = LiveActivityStateMachine( +/// initialState: Phase.preparing, +/// transitions: [.preparing: [.onTheWay], .onTheWay: [.delivered]] +/// ) +/// try await machine.transition(to: .onTheWay) +/// ``` public actor LiveActivityStateMachine { - private var currentState: State - private let transitions: [State: [State]] - - public init(initialState: State, transitions: [State: [State]]) { + public private(set) var currentState: State + private let transitions: [State: Set] + + public init(initialState: State, transitions: [State: Set]) { self.currentState = initialState self.transitions = transitions } - - /// Attempts to transition to a new state. - /// Validates the transition against the defined graph. - public func transition(to newState: State) throws { - guard let allowed = transitions[currentState], allowed.contains(newState) else { - throw StateMachineError.invalidTransition + + /// Whether moving from the current state to `newState` is allowed. + public func canTransition(to newState: State) -> Bool { + transitions[currentState]?.contains(newState) ?? false + } + + /// Moves to `newState`, or throws without changing state if the transition is not allowed. + public func transition(to newState: State) throws(StateMachineError) { + guard canTransition(to: newState) else { + throw .invalidTransition(from: currentState, to: newState) } - currentState = newState - print("⚡ [WidgetKit] Live Activity Transitioned to: \\(newState)") } - - public func getCurrentState() -> State { - return currentState + + /// States reachable in one step from the current state. + public func allowedNextStates() -> Set { + transitions[currentState] ?? [] } } -public enum StateMachineError: Error { - case invalidTransition +public enum StateMachineError: Error, Equatable { + case invalidTransition(from: State, to: State) } From 5940be878e0fbf302b83fae69aa3e1d37dce2dbf Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:00:41 +0300 Subject: [PATCH 05/59] fix(ui): clamp the WidgetTimelineScrubber selection - Clamp the selected index to the entry range, so the view cannot index out of bounds when the entries array shrinks. - Hide the slider when there is only one entry instead of building a 0...0 slider. - Allow a custom preview size instead of a fixed 158 x 158 frame. - Show the entry date with a short date format, and replace the deprecated foregroundColor and cornerRadius modifiers. --- .../UI/WidgetTimelineScrubber.swift | 84 ++++++++++++------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift b/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift index df29cb9..4688331 100644 --- a/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift +++ b/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift @@ -1,52 +1,76 @@ import SwiftUI import WidgetKit -/// A SwiftUI view for scrubbing through a widget's timeline. -/// -/// This tool allows developers to visualize how their widget changes over time. -public struct WidgetTimelineScrubber: View { - public let entries: [T] - public let content: (T) -> Content - - @State private var currentIndex: Int = 0 - - public init(entries: [T], @ViewBuilder content: @escaping (T) -> Content) { +/// A debugging view that steps through a widget's timeline entries. +/// +/// Embed it in an app target or a SwiftUI preview to check how a widget looks at +/// each entry without waiting for WidgetKit to advance the timeline. +/// +/// ```swift +/// WidgetTimelineScrubber(entries: timeline.entries) { entry in +/// MyWidgetView(entry: entry) +/// } +/// ``` +public struct WidgetTimelineScrubber: View { + public let entries: [Entry] + public let widgetSize: CGSize + public let content: (Entry) -> Content + + @State private var selectedIndex = 0 + + /// - Parameter widgetSize: Frame used for the preview; defaults to a small widget on a 6.1" iPhone. + public init( + entries: [Entry], + widgetSize: CGSize = CGSize(width: 158, height: 158), + @ViewBuilder content: @escaping (Entry) -> Content + ) { self.entries = entries + self.widgetSize = widgetSize self.content = content } - + public var body: some View { VStack(spacing: 20) { if entries.isEmpty { Text("No timeline entries provided") - .foregroundColor(.secondary) + .foregroundStyle(.secondary) } else { - let currentEntry = entries[currentIndex] - + let index = Self.clampedIndex(selectedIndex, count: entries.count) + let entry = entries[index] + VStack(alignment: .leading, spacing: 4) { - Text("Timeline Entry \(currentIndex + 1) of \(entries.count)") + Text("Entry \(index + 1) of \(entries.count)") .font(.caption.bold()) - .foregroundColor(.secondary) - - Text("Date: \(currentEntry.date.description)") + Text(entry.date, format: .dateTime.month().day().hour().minute()) .font(.caption2) - .foregroundColor(.secondary) } - - content(currentEntry) - .frame(width: 158, height: 158) // Standard small widget size + .foregroundStyle(.secondary) + + content(entry) + .frame(width: widgetSize.width, height: widgetSize.height) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) .shadow(radius: 10) - - Slider(value: Binding( - get: { Double(currentIndex) }, - set: { currentIndex = Int($0) } - ), in: 0...Double(max(0, entries.count - 1)), step: 1) - .padding(.horizontal) + + if entries.count > 1 { + Slider( + value: Binding( + get: { Double(index) }, + set: { selectedIndex = Int($0.rounded()) } + ), + in: 0...Double(entries.count - 1), + step: 1 + ) + .padding(.horizontal) + } } } .padding() - .background(.ultraThinMaterial) - .cornerRadius(20) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20)) + } + + /// Keeps a stored index valid when the entries array shrinks. + static func clampedIndex(_ index: Int, count: Int) -> Int { + guard count > 0 else { return 0 } + return min(max(index, 0), count - 1) } } From 6f47f8d9c1413254fe746bc72361955e1edd5d3f Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:45:43 +0300 Subject: [PATCH 06/59] feat(core): add clamping, formatting and calendar helpers WidgetMath, WidgetFormat, MonthGrid and CountdownComponents hold the arithmetic the views need (progress fractions, zero totals, reversed timer ranges, locale-aware month grids) so it can be unit tested. Tests use a fixed Gregorian calendar and cover NaN, negative and zero inputs, Monday-first weeks, a leap month and a DST transition. Adds the WidgetDevelopmentKitTests target for these tests. --- Package.swift | 5 + .../Core/CalendarMath.swift | 53 ++++++++++ .../Core/WidgetFormat.swift | 31 ++++++ .../Core/WidgetMath.swift | 69 ++++++++++++ .../CalendarMathTests.swift | 62 +++++++++++ .../TestSupport.swift | 24 +++++ .../WidgetMathTests.swift | 100 ++++++++++++++++++ 7 files changed, 344 insertions(+) create mode 100644 Sources/WidgetDevelopmentKit/Core/CalendarMath.swift create mode 100644 Sources/WidgetDevelopmentKit/Core/WidgetFormat.swift create mode 100644 Sources/WidgetDevelopmentKit/Core/WidgetMath.swift create mode 100644 Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift create mode 100644 Tests/WidgetDevelopmentKitTests/TestSupport.swift create mode 100644 Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift diff --git a/Package.swift b/Package.swift index c06be32..4142c59 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,11 @@ let package = Package( .target( name: "WidgetDevelopmentKit", path: "Sources/WidgetDevelopmentKit" + ), + .testTarget( + name: "WidgetDevelopmentKitTests", + dependencies: ["WidgetDevelopmentKit"], + path: "Tests/WidgetDevelopmentKitTests" ) ] ) diff --git a/Sources/WidgetDevelopmentKit/Core/CalendarMath.swift b/Sources/WidgetDevelopmentKit/Core/CalendarMath.swift new file mode 100644 index 0000000..4fb75e7 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Core/CalendarMath.swift @@ -0,0 +1,53 @@ +import Foundation + +/// A month laid out as rows of seven cells, respecting the calendar's first weekday. +public struct MonthGrid: Equatable, Sendable { + /// Each row has exactly seven entries; `nil` marks a cell outside the month. + public let weeks: [[Int?]] + /// Very short weekday symbols ordered to match the grid columns. + public let weekdaySymbols: [String] + + public init(month date: Date, calendar: Calendar = .current) { + let daysPerWeek = 7 + let symbols = calendar.veryShortWeekdaySymbols + let firstIndex = calendar.firstWeekday - 1 + weekdaySymbols = Array(symbols[firstIndex...] + symbols[.. String { + let magnitude = number.magnitude + let sign = number < 0 ? "-" : "" + switch magnitude { + case 1_000_000...: + return sign + String(format: "%.1fM", Double(magnitude) / 1_000_000) + case 1_000...: + return sign + String(format: "%.1fK", Double(magnitude) / 1_000) + default: + return "\(number)" + } + } + + /// Playback position such as `"3:07"` or `"1:02:05"`. Invalid or negative input reads `"0:00"`. + public static func playbackTime(_ interval: TimeInterval) -> String { + guard interval.isFinite, interval > 0 else { return "0:00" } + let totalSeconds = Int(interval) + let hours = totalSeconds / 3_600 + let minutes = totalSeconds % 3_600 / 60 + let seconds = totalSeconds % 60 + if hours > 0 { + return String(format: "%d:%02d:%02d", hours, minutes, seconds) + } + return String(format: "%d:%02d", minutes, seconds) + } +} diff --git a/Sources/WidgetDevelopmentKit/Core/WidgetMath.swift b/Sources/WidgetDevelopmentKit/Core/WidgetMath.swift new file mode 100644 index 0000000..4fbea3a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Core/WidgetMath.swift @@ -0,0 +1,69 @@ +import Foundation + +/// Pure helpers shared by the widget views. +/// +/// Widget views receive values straight from app data, so progress values can be +/// negative, larger than one, `NaN`, or come with a zero total. Keeping the +/// arithmetic here makes those cases testable on every platform. +public enum WidgetMath { + /// Clamps a progress value into `0...1`. `NaN` and infinities map to `0`. + public static func clampedFraction(_ value: Double) -> Double { + guard value.isFinite else { return 0 } + return min(max(value, 0), 1) + } + + /// Returns `value / total` clamped into `0...1`, or `0` when `total` is not positive. + public static func fraction(_ value: Double, of total: Double) -> Double { + guard total > 0, total.isFinite else { return 0 } + return clampedFraction(value / total) + } + + /// Whole-number percentage (`0...100`) of a fraction, rounded down. + public static func percent(_ fraction: Double) -> Int { + Int(clampedFraction(fraction) * 100) + } + + /// Percentage string such as `"42%"`. + public static func percentString(_ fraction: Double) -> String { + "\(percent(fraction))%" + } + + /// SF Symbol name for a battery level in `0...1`. + public static func batterySymbolName(level: Double, isCharging: Bool) -> String { + if isCharging { return "battery.100.bolt" } + switch clampedFraction(level) { + case ..<0.1: return "battery.0" + case ..<0.25: return "battery.25" + case ..<0.5: return "battery.50" + case ..<0.75: return "battery.75" + default: return "battery.100" + } + } + + /// Builds a date range for timer views. `ClosedRange` traps when the bounds are + /// reversed, so an `end` earlier than `start` collapses to an empty interval. + public static func timerInterval(start: Date, end: Date) -> ClosedRange { + start...max(start, end) + } + + /// Number of calendar days from `date` until `target`; `0` once the target has passed. + public static func daysRemaining( + until target: Date, + from date: Date, + calendar: Calendar = .current + ) -> Int { + let start = calendar.startOfDay(for: date) + let end = calendar.startOfDay(for: target) + let days = calendar.dateComponents([.day], from: start, to: end).day ?? 0 + return max(0, days) + } + + /// Discrete intensity level (`0.. Int { + guard levels > 1, value > 0, maximum > 0, value.isFinite else { return 0 } + let fraction = min(value / maximum, 1) + let level = Int((fraction * Double(levels - 1)).rounded(.up)) + return min(max(level, 1), levels - 1) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift b/Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift new file mode 100644 index 0000000..6d7081a --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift @@ -0,0 +1,62 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("MonthGrid") +struct MonthGridTests { + @Test func sundayFirstCalendar() { + // 1 October 2026 is a Thursday. + let calendar = TestCalendar.gregorian(firstWeekday: 1) + let grid = MonthGrid(month: TestCalendar.date(2026, 10, 14), calendar: calendar) + + #expect(grid.weekdaySymbols == ["S", "M", "T", "W", "T", "F", "S"]) + #expect(grid.weeks.count == 5) + #expect(grid.weeks[0] == [nil, nil, nil, nil, 1, 2, 3]) + #expect(grid.weeks[4] == [25, 26, 27, 28, 29, 30, 31]) + } + + @Test func mondayFirstCalendarShiftsColumns() { + let calendar = TestCalendar.gregorian(firstWeekday: 2) + let grid = MonthGrid(month: TestCalendar.date(2026, 10, 1), calendar: calendar) + + #expect(grid.weekdaySymbols == ["M", "T", "W", "T", "F", "S", "S"]) + #expect(grid.weeks[0] == [nil, nil, nil, 1, 2, 3, 4]) + #expect(grid.weeks.last == [26, 27, 28, 29, 30, 31, nil]) + } + + @Test func everyRowHasSevenCellsAndEveryDayAppearsOnce() { + let calendar = TestCalendar.gregorian(firstWeekday: 2) + // February 2028 is a leap month. + let grid = MonthGrid(month: TestCalendar.date(2028, 2, 10), calendar: calendar) + + #expect(grid.weeks.allSatisfy { $0.count == 7 }) + #expect(grid.weeks.flatMap { $0 }.compactMap { $0 } == Array(1...29)) + } + + @Test func monthStartingOnFirstWeekdayHasNoLeadingBlanks() { + // 1 February 2026 is a Sunday. + let grid = MonthGrid(month: TestCalendar.date(2026, 2, 1), calendar: TestCalendar.gregorian()) + #expect(grid.weeks.count == 4) + #expect(grid.weeks[0].first == 1) + } +} + +@Suite("CountdownComponents") +struct CountdownComponentsTests { + @Test func splitsRemainingTime() { + let now = TestCalendar.date(2026, 1, 1) + let target = now.addingTimeInterval(2 * 86_400 + 3 * 3_600 + 4 * 60 + 59) + let countdown = CountdownComponents(from: now, to: target) + + #expect(countdown == CountdownComponents(from: now, to: target)) + #expect(countdown.days == 2) + #expect(countdown.hours == 3) + #expect(countdown.minutes == 4) + } + + @Test func pastTargetIsAllZero() { + let now = TestCalendar.date(2026, 1, 1) + let countdown = CountdownComponents(from: now, to: now.addingTimeInterval(-500)) + #expect(countdown.days == 0 && countdown.hours == 0 && countdown.minutes == 0) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/TestSupport.swift b/Tests/WidgetDevelopmentKitTests/TestSupport.swift new file mode 100644 index 0000000..1f38dd3 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TestSupport.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Deterministic calendar and dates so tests do not depend on the machine's locale or time zone. +enum TestCalendar { + static func gregorian(firstWeekday: Int = 1, timeZone: String = "UTC") -> Calendar { + var calendar = Calendar(identifier: .gregorian) + calendar.locale = Locale(identifier: "en_US_POSIX") + calendar.timeZone = TimeZone(identifier: timeZone)! + calendar.firstWeekday = firstWeekday + return calendar + } + + static func date( + _ year: Int, _ month: Int, _ day: Int, + _ hour: Int = 0, _ minute: Int = 0, _ second: Int = 0, + calendar: Calendar = gregorian() + ) -> Date { + let components = DateComponents( + year: year, month: month, day: day, + hour: hour, minute: minute, second: second + ) + return calendar.date(from: components)! + } +} diff --git a/Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift b/Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift new file mode 100644 index 0000000..0f68a9e --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift @@ -0,0 +1,100 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("WidgetMath") +struct WidgetMathTests { + @Test(arguments: [ + (-0.5, 0.0), (0.0, 0.0), (0.42, 0.42), (1.0, 1.0), (7.0, 1.0), + (Double.nan, 0.0), (Double.infinity, 0.0), (-Double.infinity, 0.0) + ]) + func clampedFraction(input: Double, expected: Double) { + #expect(WidgetMath.clampedFraction(input) == expected) + } + + @Test func fractionGuardsAgainstZeroAndNegativeTotals() { + #expect(WidgetMath.fraction(250, of: 500) == 0.5) + #expect(WidgetMath.fraction(900, of: 500) == 1) + #expect(WidgetMath.fraction(10, of: 0) == 0) + #expect(WidgetMath.fraction(10, of: -5) == 0) + #expect(WidgetMath.fraction(10, of: .infinity) == 0) + } + + @Test func percentRoundsDownAndClamps() { + #expect(WidgetMath.percent(0.999) == 99) + #expect(WidgetMath.percent(1.5) == 100) + #expect(WidgetMath.percentString(0.42) == "42%") + #expect(WidgetMath.percentString(-1) == "0%") + } + + @Test(arguments: [ + (0.05, "battery.0"), (0.1, "battery.25"), (0.3, "battery.50"), + (0.6, "battery.75"), (0.75, "battery.100"), (2.0, "battery.100"), (-1.0, "battery.0") + ]) + func batterySymbol(level: Double, expected: String) { + #expect(WidgetMath.batterySymbolName(level: level, isCharging: false) == expected) + } + + @Test func chargingAlwaysUsesBoltSymbol() { + #expect(WidgetMath.batterySymbolName(level: 0.05, isCharging: true) == "battery.100.bolt") + } + + @Test func timerIntervalNeverReversesBounds() { + let start = TestCalendar.date(2026, 3, 1, 12) + let earlier = start.addingTimeInterval(-60) + let later = start.addingTimeInterval(60) + + #expect(WidgetMath.timerInterval(start: start, end: later) == start...later) + #expect(WidgetMath.timerInterval(start: start, end: earlier) == start...start) + } + + @Test func daysRemainingCountsCalendarDays() { + let calendar = TestCalendar.gregorian() + let lateEvening = TestCalendar.date(2026, 3, 1, 23, 30) + let nextMorning = TestCalendar.date(2026, 3, 2, 8) + let nextWeek = TestCalendar.date(2026, 3, 8, 0, 1) + + // Less than 24 hours away, but on the next calendar day. + #expect(WidgetMath.daysRemaining(until: nextMorning, from: lateEvening, calendar: calendar) == 1) + #expect(WidgetMath.daysRemaining(until: nextWeek, from: lateEvening, calendar: calendar) == 7) + #expect(WidgetMath.daysRemaining(until: lateEvening, from: nextMorning, calendar: calendar) == 0) + } + + @Test func daysRemainingIsStableAcrossDaylightSavingChange() { + let calendar = TestCalendar.gregorian(timeZone: "America/New_York") + // US clocks spring forward on 8 March 2026, so that day has 23 hours. + let before = TestCalendar.date(2026, 3, 7, 12, calendar: calendar) + let after = TestCalendar.date(2026, 3, 9, 12, calendar: calendar) + #expect(WidgetMath.daysRemaining(until: after, from: before, calendar: calendar) == 2) + } + + @Test func intensityLevels() { + #expect(WidgetMath.intensityLevel(0, maximum: 10, levels: 5) == 0) + #expect(WidgetMath.intensityLevel(-3, maximum: 10, levels: 5) == 0) + #expect(WidgetMath.intensityLevel(0.1, maximum: 10, levels: 5) == 1) + #expect(WidgetMath.intensityLevel(5, maximum: 10, levels: 5) == 2) + #expect(WidgetMath.intensityLevel(10, maximum: 10, levels: 5) == 4) + #expect(WidgetMath.intensityLevel(25, maximum: 10, levels: 5) == 4) + #expect(WidgetMath.intensityLevel(5, maximum: 0, levels: 5) == 0) + #expect(WidgetMath.intensityLevel(5, maximum: 10, levels: 1) == 0) + } +} + +@Suite("WidgetFormat") +struct WidgetFormatTests { + @Test(arguments: [ + (0, "0"), (999, "999"), (1_000, "1.0K"), (12_345, "12.3K"), + (1_500_000, "1.5M"), (-2_500, "-2.5K"), (-42, "-42") + ]) + func compactNumber(input: Int, expected: String) { + #expect(WidgetFormat.compactNumber(input) == expected) + } + + @Test(arguments: [ + (0.0, "0:00"), (7.9, "0:07"), (187, "3:07"), (3_725, "1:02:05"), + (-10, "0:00"), (Double.nan, "0:00"), (Double.infinity, "0:00") + ]) + func playbackTime(input: Double, expected: String) { + #expect(WidgetFormat.playbackTime(input) == expected) + } +} From d4113c0fbf8a8aabb187cbd4f641f1cdf00dc806 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:02:11 +0300 Subject: [PATCH 07/59] test: cover LiveActivityStateMachine transitions --- .../LiveActivityStateMachineTests.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift diff --git a/Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift b/Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift new file mode 100644 index 0000000..8b258f5 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift @@ -0,0 +1,47 @@ +import Testing +@testable import WidgetDevelopmentKit + +@Suite("LiveActivityStateMachine") +struct LiveActivityStateMachineTests { + enum Phase: Hashable, Sendable { + case preparing, onTheWay, delivered, cancelled + } + + private func makeMachine() -> LiveActivityStateMachine { + LiveActivityStateMachine( + initialState: .preparing, + transitions: [ + .preparing: [.onTheWay, .cancelled], + .onTheWay: [.delivered, .cancelled] + ] + ) + } + + @Test func followsAllowedTransitions() async throws { + let machine = makeMachine() + try await machine.transition(to: .onTheWay) + try await machine.transition(to: .delivered) + #expect(await machine.currentState == .delivered) + } + + @Test func rejectsTransitionAndKeepsState() async { + let machine = makeMachine() + await #expect(throws: StateMachineError.invalidTransition(from: Phase.preparing, to: .delivered)) { + try await machine.transition(to: .delivered) + } + #expect(await machine.currentState == .preparing) + } + + @Test func terminalStateHasNoNextStates() async throws { + let machine = makeMachine() + try await machine.transition(to: .cancelled) + #expect(await machine.allowedNextStates().isEmpty) + #expect(await machine.canTransition(to: .preparing) == false) + } + + @Test func reportsAllowedNextStates() async { + let machine = makeMachine() + #expect(await machine.allowedNextStates() == [.onTheWay, .cancelled]) + #expect(await machine.canTransition(to: .onTheWay)) + } +} From dfd4c5ec834873a7d494d2aa803e30c31a1b4cd1 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:02:39 +0300 Subject: [PATCH 08/59] feat(charts): restore Swift Charts widget views Restores the chart views that were outside every build target and fixes: sparkline x values no longer depend on Date(), progress ring no longer force-unwraps an empty colour list or divides by a zero total, multi-line series use a stable order, horizontal bars clamp their width, and the activity heat map computes its maximum once and uses discrete levels. --- Sources/Charts/WidgetCharts.swift | 506 ------------------ .../Charts/ChartCategorical.swift | 255 +++++++++ .../Charts/ChartTimeSeries.swift | 233 ++++++++ 3 files changed, 488 insertions(+), 506 deletions(-) delete mode 100644 Sources/Charts/WidgetCharts.swift create mode 100644 Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift create mode 100644 Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift diff --git a/Sources/Charts/WidgetCharts.swift b/Sources/Charts/WidgetCharts.swift deleted file mode 100644 index 0c4ce13..0000000 --- a/Sources/Charts/WidgetCharts.swift +++ /dev/null @@ -1,506 +0,0 @@ -// WidgetCharts.swift -// iOS-Widget-Development-Kit -// -// SwiftUI Charts Support for Widgets -// Created by Muhittin Camdali - -import SwiftUI -import Charts - -// MARK: - Chart Data Types - -/// Generic data point for charts -public struct ChartDataPoint: Identifiable { - public let id = UUID() - public let label: String - public let value: Double - public let date: Date? - public let category: String? - - public init(label: String, value: Double, date: Date? = nil, category: String? = nil) { - self.label = label - self.value = value - self.date = date - self.category = category - } -} - -/// Time series data point -public struct TimeSeriesDataPoint: Identifiable { - public let id = UUID() - public let date: Date - public let value: Double - public let series: String? - - public init(date: Date, value: Double, series: String? = nil) { - self.date = date - self.value = value - self.series = series - } -} - -// MARK: - Widget Bar Chart - -/// Compact bar chart for widgets -@available(iOS 16.0, *) -public struct WidgetBarChart: View { - let data: [ChartDataPoint] - let accentColor: Color - let showLabels: Bool - let showValues: Bool - - public init( - data: [ChartDataPoint], - accentColor: Color = .blue, - showLabels: Bool = true, - showValues: Bool = false - ) { - self.data = data - self.accentColor = accentColor - self.showLabels = showLabels - self.showValues = showValues - } - - public var body: some View { - Chart(data) { point in - BarMark( - x: .value("Category", point.label), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor.gradient) - .cornerRadius(4) - - if showValues { - RuleMark(y: .value("Value", point.value)) - .lineStyle(StrokeStyle(lineWidth: 0)) - .annotation(position: .top) { - Text("\(Int(point.value))") - .font(.system(size: 8)) - .foregroundStyle(.secondary) - } - } - } - .chartXAxis(showLabels ? .automatic : .hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Widget Line Chart - -/// Compact line chart for widgets -@available(iOS 16.0, *) -public struct WidgetLineChart: View { - let data: [TimeSeriesDataPoint] - let accentColor: Color - let showArea: Bool - let showPoints: Bool - let showXAxis: Bool - - public init( - data: [TimeSeriesDataPoint], - accentColor: Color = .blue, - showArea: Bool = true, - showPoints: Bool = false, - showXAxis: Bool = false - ) { - self.data = data - self.accentColor = accentColor - self.showArea = showArea - self.showPoints = showPoints - self.showXAxis = showXAxis - } - - public var body: some View { - Chart(data) { point in - LineMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor) - .interpolationMethod(.catmullRom) - - if showArea { - AreaMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor.opacity(0.2).gradient) - .interpolationMethod(.catmullRom) - } - - if showPoints { - PointMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor) - } - } - .chartXAxis(showXAxis ? .automatic : .hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Widget Sparkline - -/// Minimal sparkline chart for widgets -@available(iOS 16.0, *) -public struct WidgetSparkline: View { - let values: [Double] - let color: Color - let lineWidth: CGFloat - let showGradient: Bool - - public init( - values: [Double], - color: Color = .blue, - lineWidth: CGFloat = 2, - showGradient: Bool = true - ) { - self.values = values - self.color = color - self.lineWidth = lineWidth - self.showGradient = showGradient - } - - private var dataPoints: [TimeSeriesDataPoint] { - values.enumerated().map { index, value in - TimeSeriesDataPoint( - date: Date().addingTimeInterval(Double(index) * 3600), - value: value - ) - } - } - - public var body: some View { - Chart(dataPoints) { point in - LineMark( - x: .value("Index", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(color) - .lineStyle(StrokeStyle(lineWidth: lineWidth, lineCap: .round)) - .interpolationMethod(.catmullRom) - - if showGradient { - AreaMark( - x: .value("Index", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle( - LinearGradient( - colors: [color.opacity(0.3), color.opacity(0)], - startPoint: .top, - endPoint: .bottom - ) - ) - .interpolationMethod(.catmullRom) - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Widget Pie Chart - -/// Compact pie chart for widgets -@available(iOS 16.0, *) -public struct WidgetPieChart: View { - let data: [ChartDataPoint] - let colors: [Color] - let showLabels: Bool - let innerRadiusRatio: Double - - public init( - data: [ChartDataPoint], - colors: [Color]? = nil, - showLabels: Bool = false, - innerRadiusRatio: Double = 0 - ) { - self.data = data - self.colors = colors ?? [.blue, .green, .orange, .red, .purple, .pink, .cyan, .yellow] - self.showLabels = showLabels - self.innerRadiusRatio = innerRadiusRatio - } - - public var body: some View { - Chart(data) { point in - SectorMark( - angle: .value("Value", point.value), - innerRadius: .ratio(innerRadiusRatio), - angularInset: 1 - ) - .foregroundStyle(by: .value("Category", point.label)) - .cornerRadius(4) - } - .chartLegend(showLabels ? .visible : .hidden) - .chartForegroundStyleScale(domain: data.map { $0.label }, range: colors) - } -} - -// MARK: - Widget Progress Ring Chart - -/// Circular progress chart -@available(iOS 16.0, *) -public struct WidgetProgressRing: View { - let progress: Double - let total: Double - let colors: [Color] - let lineWidth: CGFloat - let showValue: Bool - let valueFormat: String - - public init( - progress: Double, - total: Double = 1.0, - colors: [Color] = [.blue], - lineWidth: CGFloat = 12, - showValue: Bool = true, - valueFormat: String = "%.0f%%" - ) { - self.progress = progress - self.total = total - self.colors = colors - self.lineWidth = lineWidth - self.showValue = showValue - self.valueFormat = valueFormat - } - - private var normalizedProgress: Double { - min(max(progress / total, 0), 1) - } - - private var gradient: AngularGradient { - AngularGradient( - colors: colors + [colors.first!], - center: .center, - startAngle: .degrees(-90), - endAngle: .degrees(270) - ) - } - - public var body: some View { - ZStack { - Circle() - .stroke(colors.first!.opacity(0.2), lineWidth: lineWidth) - - Circle() - .trim(from: 0, to: normalizedProgress) - .stroke( - colors.count == 1 ? AnyShapeStyle(colors.first!) : AnyShapeStyle(gradient), - style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) - ) - .rotationEffect(.degrees(-90)) - - if showValue { - Text(String(format: valueFormat, normalizedProgress * 100)) - .font(.system(size: lineWidth * 1.5, weight: .bold, design: .rounded)) - } - } - } -} - -// MARK: - Multi-Line Chart - -/// Line chart with multiple series -@available(iOS 16.0, *) -public struct WidgetMultiLineChart: View { - let series: [String: [TimeSeriesDataPoint]] - let colors: [String: Color] - let showLegend: Bool - - public init( - series: [String: [TimeSeriesDataPoint]], - colors: [String: Color], - showLegend: Bool = true - ) { - self.series = series - self.colors = colors - self.showLegend = showLegend - } - - private var allDataPoints: [TimeSeriesDataPoint] { - series.flatMap { seriesName, points in - points.map { point in - TimeSeriesDataPoint(date: point.date, value: point.value, series: seriesName) - } - } - } - - public var body: some View { - Chart(allDataPoints) { point in - LineMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(by: .value("Series", point.series ?? "")) - .interpolationMethod(.catmullRom) - } - .chartForegroundStyleScale(domain: Array(colors.keys), range: Array(colors.values)) - .chartLegend(showLegend ? .visible : .hidden) - .chartXAxis(.hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Stats Chart Widget - -/// Combined stats with chart -@available(iOS 16.0, *) -public struct WidgetStatsChart: View { - let title: String - let value: String - let change: Double - let sparklineData: [Double] - let accentColor: Color - - public init( - title: String, - value: String, - change: Double, - sparklineData: [Double], - accentColor: Color = .blue - ) { - self.title = title - self.value = value - self.change = change - self.sparklineData = sparklineData - self.accentColor = accentColor - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - - HStack(alignment: .firstTextBaseline) { - Text(value) - .font(.title2) - .fontWeight(.bold) - - HStack(spacing: 2) { - Image(systemName: change >= 0 ? "arrow.up.right" : "arrow.down.right") - Text(String(format: "%.1f%%", abs(change))) - } - .font(.caption) - .foregroundStyle(change >= 0 ? .green : .red) - } - - WidgetSparkline(values: sparklineData, color: accentColor) - .frame(height: 40) - } - } -} - -// MARK: - Horizontal Bar Chart - -/// Horizontal bar chart for comparisons -@available(iOS 16.0, *) -public struct WidgetHorizontalBarChart: View { - let data: [ChartDataPoint] - let accentColor: Color - let showValues: Bool - let maxValue: Double? - - public init( - data: [ChartDataPoint], - accentColor: Color = .blue, - showValues: Bool = true, - maxValue: Double? = nil - ) { - self.data = data - self.accentColor = accentColor - self.showValues = showValues - self.maxValue = maxValue - } - - private var normalizedMax: Double { - maxValue ?? (data.map { $0.value }.max() ?? 1) - } - - public var body: some View { - VStack(spacing: 8) { - ForEach(data) { point in - HStack(spacing: 8) { - Text(point.label) - .font(.caption) - .frame(width: 60, alignment: .leading) - .lineLimit(1) - - GeometryReader { geo in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 4) - .fill(accentColor.opacity(0.2)) - - RoundedRectangle(cornerRadius: 4) - .fill(accentColor.gradient) - .frame(width: geo.size.width * (point.value / normalizedMax)) - } - } - .frame(height: 16) - - if showValues { - Text("\(Int(point.value))") - .font(.caption) - .foregroundStyle(.secondary) - .frame(width: 30, alignment: .trailing) - } - } - } - } - } -} - -// MARK: - Activity Chart - -/// Activity/contribution chart (like GitHub) -@available(iOS 16.0, *) -public struct WidgetActivityChart: View { - let activities: [[Double]] // Weeks x Days - let colorRange: [Color] - let cellSize: CGFloat - let spacing: CGFloat - - public init( - activities: [[Double]], - colorRange: [Color] = [Color.gray.opacity(0.2), .green], - cellSize: CGFloat = 10, - spacing: CGFloat = 2 - ) { - self.activities = activities - self.colorRange = colorRange - self.cellSize = cellSize - self.spacing = spacing - } - - private func colorForValue(_ value: Double) -> Color { - if value <= 0 { return colorRange.first! } - let maxValue = activities.flatMap { $0 }.max() ?? 1 - let normalized = min(value / maxValue, 1) - - // Interpolate between colors - let index = normalized * Double(colorRange.count - 1) - let lowerIndex = Int(index) - let upperIndex = min(lowerIndex + 1, colorRange.count - 1) - let fraction = index - Double(lowerIndex) - - // Simple color interpolation - return colorRange[upperIndex].opacity(0.3 + normalized * 0.7) - } - - public var body: some View { - HStack(spacing: spacing) { - ForEach(activities.indices, id: \.self) { weekIndex in - VStack(spacing: spacing) { - ForEach(activities[weekIndex].indices, id: \.self) { dayIndex in - RoundedRectangle(cornerRadius: 2) - .fill(colorForValue(activities[weekIndex][dayIndex])) - .frame(width: cellSize, height: cellSize) - } - } - } - } - } -} diff --git a/Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift b/Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift new file mode 100644 index 0000000..807c8f8 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift @@ -0,0 +1,255 @@ +import SwiftUI +import Charts + +// MARK: - Widget Bar Chart + +/// Compact bar chart for widgets +public struct WidgetBarChart: View { + let data: [ChartDataPoint] + let accentColor: Color + let showLabels: Bool + let showValues: Bool + + public init( + data: [ChartDataPoint], + accentColor: Color = .blue, + showLabels: Bool = true, + showValues: Bool = false + ) { + self.data = data + self.accentColor = accentColor + self.showLabels = showLabels + self.showValues = showValues + } + + public var body: some View { + Chart(data) { point in + BarMark( + x: .value("Category", point.label), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor.gradient) + .cornerRadius(4) + + if showValues { + RuleMark(y: .value("Value", point.value)) + .lineStyle(StrokeStyle(lineWidth: 0)) + .annotation(position: .top) { + Text("\(Int(point.value))") + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + } + } + .chartXAxis(showLabels ? .automatic : .hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Widget Pie Chart + +/// Compact pie chart for widgets +public struct WidgetPieChart: View { + let data: [ChartDataPoint] + let colors: [Color] + let showLabels: Bool + let innerRadiusRatio: Double + + public init( + data: [ChartDataPoint], + colors: [Color]? = nil, + showLabels: Bool = false, + innerRadiusRatio: Double = 0 + ) { + self.data = data + let palette: [Color] = [.blue, .green, .orange, .red, .purple, .pink, .cyan, .yellow] + self.colors = (colors?.isEmpty == false ? colors : nil) ?? palette + self.showLabels = showLabels + self.innerRadiusRatio = innerRadiusRatio + } + + public var body: some View { + Chart(data) { point in + SectorMark( + angle: .value("Value", point.value), + innerRadius: .ratio(innerRadiusRatio), + angularInset: 1 + ) + .foregroundStyle(by: .value("Category", point.label)) + .cornerRadius(4) + } + .chartLegend(showLabels ? .visible : .hidden) + .chartForegroundStyleScale(domain: data.map(\.label), range: colors) + } +} + +// MARK: - Widget Progress Ring Chart + +/// Circular progress chart +public struct WidgetProgressRing: View { + let progress: Double + let total: Double + let colors: [Color] + let lineWidth: CGFloat + let showValue: Bool + let valueFormat: String + + public init( + progress: Double, + total: Double = 1.0, + colors: [Color] = [.blue], + lineWidth: CGFloat = 12, + showValue: Bool = true, + valueFormat: String = "%.0f%%" + ) { + self.progress = progress + self.total = total + self.colors = colors.isEmpty ? [.blue] : colors + self.lineWidth = lineWidth + self.showValue = showValue + self.valueFormat = valueFormat + } + + private var normalizedProgress: Double { + WidgetMath.fraction(progress, of: total) + } + + private var baseColor: Color { colors[0] } + + private var gradient: AngularGradient { + AngularGradient( + colors: colors + [baseColor], + center: .center, + startAngle: .degrees(-90), + endAngle: .degrees(270) + ) + } + + public var body: some View { + ZStack { + Circle() + .stroke(baseColor.opacity(0.2), lineWidth: lineWidth) + + Circle() + .trim(from: 0, to: normalizedProgress) + .stroke( + colors.count == 1 ? AnyShapeStyle(baseColor) : AnyShapeStyle(gradient), + style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + + if showValue { + Text(String(format: valueFormat, normalizedProgress * 100)) + .font(.system(size: lineWidth * 1.5, weight: .bold, design: .rounded)) + } + } + } +} + +// MARK: - Horizontal Bar Chart + +/// Horizontal bar chart for comparisons +public struct WidgetHorizontalBarChart: View { + let data: [ChartDataPoint] + let accentColor: Color + let showValues: Bool + let maxValue: Double? + + public init( + data: [ChartDataPoint], + accentColor: Color = .blue, + showValues: Bool = true, + maxValue: Double? = nil + ) { + self.data = data + self.accentColor = accentColor + self.showValues = showValues + self.maxValue = maxValue + } + + private var normalizedMax: Double { + maxValue ?? data.map(\.value).max() ?? 1 + } + + public var body: some View { + VStack(spacing: 8) { + ForEach(data) { point in + HStack(spacing: 8) { + Text(point.label) + .font(.caption) + .frame(width: 60, alignment: .leading) + .lineLimit(1) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4) + .fill(accentColor.opacity(0.2)) + + RoundedRectangle(cornerRadius: 4) + .fill(accentColor.gradient) + .frame(width: geo.size.width * WidgetMath.fraction(point.value, of: normalizedMax)) + } + } + .frame(height: 16) + + if showValues { + Text("\(Int(point.value))") + .font(.caption) + .foregroundStyle(.secondary) + .frame(width: 30, alignment: .trailing) + } + } + } + } + } +} + +// MARK: - Activity Chart + +/// Contribution-style heat map (weeks as columns, days as rows). +public struct WidgetActivityChart: View { + let activities: [[Double]] + let color: Color + let levels: Int + let cellSize: CGFloat + let spacing: CGFloat + + /// - Parameters: + /// - activities: One array per week, each holding the values for its days. + /// - color: Color of the most active cells; lower levels use lower opacity. + /// - levels: Number of discrete intensity steps, including the empty level. + public init( + activities: [[Double]], + color: Color = .green, + levels: Int = 5, + cellSize: CGFloat = 10, + spacing: CGFloat = 2 + ) { + self.activities = activities + self.color = color + self.levels = max(levels, 2) + self.cellSize = cellSize + self.spacing = spacing + } + + public var body: some View { + let maximum = activities.flatMap { $0 }.max() ?? 0 + HStack(spacing: spacing) { + ForEach(activities.indices, id: \.self) { weekIndex in + VStack(spacing: spacing) { + ForEach(activities[weekIndex].indices, id: \.self) { dayIndex in + RoundedRectangle(cornerRadius: 2) + .fill(cellColor(activities[weekIndex][dayIndex], maximum: maximum)) + .frame(width: cellSize, height: cellSize) + } + } + } + } + } + + private func cellColor(_ value: Double, maximum: Double) -> Color { + let level = WidgetMath.intensityLevel(value, maximum: maximum, levels: levels) + guard level > 0 else { return Color.gray.opacity(0.2) } + return color.opacity(Double(level) / Double(levels - 1)) + } +} diff --git a/Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift b/Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift new file mode 100644 index 0000000..5c949ce --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift @@ -0,0 +1,233 @@ +import SwiftUI +import Charts + +// MARK: - Chart Data Types + +/// Generic data point for charts +public struct ChartDataPoint: Identifiable, Hashable, Sendable { + public let id = UUID() + public let label: String + public let value: Double + public let date: Date? + public let category: String? + + public init(label: String, value: Double, date: Date? = nil, category: String? = nil) { + self.label = label + self.value = value + self.date = date + self.category = category + } +} + +/// Time series data point +public struct TimeSeriesDataPoint: Identifiable, Hashable, Sendable { + public let id = UUID() + public let date: Date + public let value: Double + public let series: String? + + public init(date: Date, value: Double, series: String? = nil) { + self.date = date + self.value = value + self.series = series + } +} + +// MARK: - Widget Line Chart + +/// Compact line chart for widgets +public struct WidgetLineChart: View { + let data: [TimeSeriesDataPoint] + let accentColor: Color + let showArea: Bool + let showPoints: Bool + let showXAxis: Bool + + public init( + data: [TimeSeriesDataPoint], + accentColor: Color = .blue, + showArea: Bool = true, + showPoints: Bool = false, + showXAxis: Bool = false + ) { + self.data = data + self.accentColor = accentColor + self.showArea = showArea + self.showPoints = showPoints + self.showXAxis = showXAxis + } + + public var body: some View { + Chart(data) { point in + LineMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor) + .interpolationMethod(.catmullRom) + + if showArea { + AreaMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor.opacity(0.2).gradient) + .interpolationMethod(.catmullRom) + } + + if showPoints { + PointMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor) + } + } + .chartXAxis(showXAxis ? .automatic : .hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Widget Sparkline + +/// Minimal sparkline chart for widgets +public struct WidgetSparkline: View { + let values: [Double] + let color: Color + let lineWidth: CGFloat + let showGradient: Bool + + public init( + values: [Double], + color: Color = .blue, + lineWidth: CGFloat = 2, + showGradient: Bool = true + ) { + self.values = values + self.color = color + self.lineWidth = lineWidth + self.showGradient = showGradient + } + + public var body: some View { + Chart(Array(values.enumerated()), id: \.offset) { index, value in + LineMark( + x: .value("Index", index), + y: .value("Value", value) + ) + .foregroundStyle(color) + .lineStyle(StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .interpolationMethod(.catmullRom) + + if showGradient { + AreaMark( + x: .value("Index", index), + y: .value("Value", value) + ) + .foregroundStyle( + LinearGradient( + colors: [color.opacity(0.3), color.opacity(0)], + startPoint: .top, + endPoint: .bottom + ) + ) + .interpolationMethod(.catmullRom) + } + } + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Multi-Line Chart + +/// Line chart with multiple series +public struct WidgetMultiLineChart: View { + let series: [String: [TimeSeriesDataPoint]] + let colors: [String: Color] + let showLegend: Bool + + public init( + series: [String: [TimeSeriesDataPoint]], + colors: [String: Color], + showLegend: Bool = true + ) { + self.series = series + self.colors = colors + self.showLegend = showLegend + } + + private var seriesNames: [String] { series.keys.sorted() } + + private var allDataPoints: [TimeSeriesDataPoint] { + seriesNames.flatMap { seriesName in + (series[seriesName] ?? []).map { point in + TimeSeriesDataPoint(date: point.date, value: point.value, series: seriesName) + } + } + } + + public var body: some View { + Chart(allDataPoints) { point in + LineMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(by: .value("Series", point.series ?? "")) + .interpolationMethod(.catmullRom) + } + .chartForegroundStyleScale(domain: seriesNames, range: seriesNames.map { colors[$0] ?? .gray }) + .chartLegend(showLegend ? .visible : .hidden) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Stats Chart Widget + +/// Combined stats with chart +public struct WidgetStatsChart: View { + let title: String + let value: String + let change: Double + let sparklineData: [Double] + let accentColor: Color + + public init( + title: String, + value: String, + change: Double, + sparklineData: [Double], + accentColor: Color = .blue + ) { + self.title = title + self.value = value + self.change = change + self.sparklineData = sparklineData + self.accentColor = accentColor + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + + HStack(alignment: .firstTextBaseline) { + Text(value) + .font(.title2) + .fontWeight(.bold) + + HStack(spacing: 2) { + Image(systemName: change >= 0 ? "arrow.up.right" : "arrow.down.right") + Text(String(format: "%.1f%%", abs(change))) + } + .font(.caption) + .foregroundStyle(change >= 0 ? .green : .red) + } + + WidgetSparkline(values: sparklineData, color: accentColor) + .frame(height: 40) + } + } +} From 9f666b87da82354bee1cd6d6e28b3680fbf307e6 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:03:18 +0300 Subject: [PATCH 09/59] feat(lock-screen): restore accessory family widget views Accessory families are unavailable on macOS, so the views compile for iOS and watchOS only. Timer views no longer build a reversed date range (which traps), countdowns count calendar days from the timeline entry date, progress values are clamped and duplicate labels no longer collide as ForEach identifiers. --- Sources/LockScreen/LockScreenWidgets.swift | 796 ------------------ .../LockScreenMultiFamilyWidgets.swift | 366 ++++++++ .../LockScreenSingleFamilyWidgets.swift | 406 +++++++++ 3 files changed, 772 insertions(+), 796 deletions(-) delete mode 100644 Sources/LockScreen/LockScreenWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift diff --git a/Sources/LockScreen/LockScreenWidgets.swift b/Sources/LockScreen/LockScreenWidgets.swift deleted file mode 100644 index 1abcbbf..0000000 --- a/Sources/LockScreen/LockScreenWidgets.swift +++ /dev/null @@ -1,796 +0,0 @@ -// LockScreenWidgets.swift -// iOS-Widget-Development-Kit -// -// iOS 16+ Lock Screen Widget Templates -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit - -// MARK: - Lock Screen Widget Families - -/// Supported Lock Screen widget families -@available(iOS 16.0, *) -public enum LockScreenWidgetFamily: CaseIterable { - case accessoryCircular - case accessoryRectangular - case accessoryInline - - public var widgetFamily: WidgetFamily { - switch self { - case .accessoryCircular: - return .accessoryCircular - case .accessoryRectangular: - return .accessoryRectangular - case .accessoryInline: - return .accessoryInline - } - } -} - -// MARK: - Lock Screen Widget Protocol - -/// Protocol for Lock Screen widgets -@available(iOS 16.0, *) -public protocol LockScreenWidgetProtocol { - associatedtype CircularView: View - associatedtype RectangularView: View - associatedtype InlineView: View - - @ViewBuilder func makeCircular() -> CircularView - @ViewBuilder func makeRectangular() -> RectangularView - @ViewBuilder func makeInline() -> InlineView -} - -// MARK: - 1. Circular Progress Widget - -/// Circular progress indicator widget -@available(iOS 16.0, *) -public struct CircularProgressWidget: View { - let progress: Double - let icon: String - let title: String - let accentColor: Color - - public init( - progress: Double, - icon: String = "star.fill", - title: String = "", - accentColor: Color = .blue - ) { - self.progress = progress - self.icon = icon - self.title = title - self.accentColor = accentColor - } - - public var body: some View { - Gauge(value: progress, in: 0...1) { - Image(systemName: icon) - } currentValueLabel: { - Text("\(Int(progress * 100))%") - .font(.system(size: 12, weight: .semibold)) - } - .gaugeStyle(.accessoryCircularCapacity) - .tint(accentColor) - } -} - -// MARK: - 2. Circular Icon Widget - -/// Simple icon display widget -@available(iOS 16.0, *) -public struct CircularIconWidget: View { - let icon: String - let value: String? - let backgroundColor: Color - - public init( - icon: String, - value: String? = nil, - backgroundColor: Color = .blue - ) { - self.icon = icon - self.value = value - self.backgroundColor = backgroundColor - } - - public var body: some View { - ZStack { - AccessoryWidgetBackground() - - VStack(spacing: 2) { - Image(systemName: icon) - .font(.title3) - - if let value = value { - Text(value) - .font(.system(size: 10, weight: .semibold)) - } - } - } - } -} - -// MARK: - 3. Circular Ring Widget - -/// Activity ring style widget -@available(iOS 16.0, *) -public struct CircularRingWidget: View { - let innerProgress: Double - let outerProgress: Double - let innerColor: Color - let outerColor: Color - let centerIcon: String - - public init( - innerProgress: Double, - outerProgress: Double, - innerColor: Color = .cyan, - outerColor: Color = .green, - centerIcon: String = "figure.walk" - ) { - self.innerProgress = innerProgress - self.outerProgress = outerProgress - self.innerColor = innerColor - self.outerColor = outerColor - self.centerIcon = centerIcon - } - - public var body: some View { - ZStack { - // Outer ring - Circle() - .stroke(outerColor.opacity(0.3), lineWidth: 4) - Circle() - .trim(from: 0, to: outerProgress) - .stroke(outerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - - // Inner ring - Circle() - .stroke(innerColor.opacity(0.3), lineWidth: 4) - .padding(6) - Circle() - .trim(from: 0, to: innerProgress) - .stroke(innerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .padding(6) - - // Center icon - Image(systemName: centerIcon) - .font(.body) - } - } -} - -// MARK: - 4. Rectangular Text Widget - -/// Multi-line text widget -@available(iOS 16.0, *) -public struct RectangularTextWidget: View { - let title: String - let subtitle: String - let detail: String? - let icon: String - - public init( - title: String, - subtitle: String, - detail: String? = nil, - icon: String = "star" - ) { - self.title = title - self.subtitle = subtitle - self.detail = detail - self.icon = icon - } - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.headline) - .widgetAccentable() - - Text(subtitle) - .font(.caption) - - if let detail = detail { - Text(detail) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: icon) - .font(.title2) - .widgetAccentable() - } - } -} - -// MARK: - 5. Rectangular Stats Widget - -/// Statistics display widget -@available(iOS 16.0, *) -public struct RectangularStatsWidget: View { - let stats: [(String, String)] - let title: String? - - public init(stats: [(String, String)], title: String? = nil) { - self.stats = stats - self.title = title - } - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - if let title = title { - Text(title) - .font(.caption2) - .foregroundStyle(.secondary) - } - - HStack { - ForEach(stats.prefix(3), id: \.0) { stat in - VStack(spacing: 2) { - Text(stat.1) - .font(.headline) - .widgetAccentable() - Text(stat.0) - .font(.system(size: 8)) - .foregroundStyle(.secondary) - } - .frame(maxWidth: .infinity) - } - } - } - } -} - -// MARK: - 6. Rectangular Progress Widget - -/// Progress bar widget -@available(iOS 16.0, *) -public struct RectangularProgressWidget: View { - let title: String - let progress: Double - let currentValue: String - let maxValue: String - let icon: String - - public init( - title: String, - progress: Double, - currentValue: String, - maxValue: String, - icon: String = "chart.bar.fill" - ) { - self.title = title - self.progress = progress - self.currentValue = currentValue - self.maxValue = maxValue - self.icon = icon - } - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Image(systemName: icon) - .widgetAccentable() - - Text(title) - .font(.caption) - - Spacer() - - Text("\(currentValue)/\(maxValue)") - .font(.caption2) - .foregroundStyle(.secondary) - } - - ProgressView(value: progress, total: 1.0) - .tint(.primary) - } - } -} - -// MARK: - 7. Rectangular Calendar Widget - -/// Upcoming events widget -@available(iOS 16.0, *) -public struct RectangularCalendarWidget: View { - let events: [(String, Date)] - - public init(events: [(String, Date)]) { - self.events = events - } - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(events.prefix(2), id: \.0) { event in - HStack { - Circle() - .fill(Color.primary) - .frame(width: 6, height: 6) - - Text(event.0) - .font(.caption) - .lineLimit(1) - - Spacer() - - Text(event.1, style: .time) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - } - } -} - -// MARK: - 8. Rectangular Weather Widget - -/// Weather conditions widget -@available(iOS 16.0, *) -public struct RectangularWeatherWidget: View { - let temperature: Int - let high: Int - let low: Int - let condition: String - let location: String - - public init( - temperature: Int, - high: Int, - low: Int, - condition: String = "sun.max.fill", - location: String - ) { - self.temperature = temperature - self.high = high - self.low = low - self.condition = condition - self.location = location - } - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(location) - .font(.caption2) - .foregroundStyle(.secondary) - - Text("\(temperature)°") - .font(.title) - .fontWeight(.medium) - .widgetAccentable() - - HStack(spacing: 4) { - Text("H:\(high)°") - Text("L:\(low)°") - } - .font(.caption2) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: condition) - .font(.largeTitle) - .widgetAccentable() - } - } -} - -// MARK: - 9. Inline Text Widget - -/// Simple inline text widget -@available(iOS 16.0, *) -public struct InlineTextWidget: View { - let icon: String? - let text: String - - public init(icon: String? = nil, text: String) { - self.icon = icon - self.text = text - } - - public var body: some View { - if let icon = icon { - Label(text, systemImage: icon) - } else { - Text(text) - } - } -} - -// MARK: - 10. Inline Stats Widget - -/// Inline statistics widget -@available(iOS 16.0, *) -public struct InlineStatsWidget: View { - let label: String - let value: String - let icon: String - - public init(label: String, value: String, icon: String) { - self.label = label - self.value = value - self.icon = icon - } - - public var body: some View { - Label { - Text("\(label): \(value)") - } icon: { - Image(systemName: icon) - } - } -} - -// MARK: - 11. Battery Widget (All Families) - -/// Battery level widget supporting all Lock Screen families -@available(iOS 16.0, *) -public struct LockScreenBatteryWidget: View { - let level: Double - let isCharging: Bool - let deviceName: String? - let family: WidgetFamily - - public init( - level: Double, - isCharging: Bool = false, - deviceName: String? = nil, - family: WidgetFamily = .accessoryCircular - ) { - self.level = level - self.isCharging = isCharging - self.deviceName = deviceName - self.family = family - } - - private var batteryIcon: String { - if isCharging { return "battery.100.bolt" } - if level < 0.1 { return "battery.0" } - if level < 0.25 { return "battery.25" } - if level < 0.5 { return "battery.50" } - if level < 0.75 { return "battery.75" } - return "battery.100" - } - - public var body: some View { - switch family { - case .accessoryCircular: - Gauge(value: level, in: 0...1) { - Image(systemName: batteryIcon) - } currentValueLabel: { - Text("\(Int(level * 100))") - .font(.system(size: 14, weight: .semibold)) - } - .gaugeStyle(.accessoryCircularCapacity) - .tint(level < 0.2 ? .red : (level < 0.5 ? .orange : .green)) - - case .accessoryRectangular: - HStack { - VStack(alignment: .leading, spacing: 2) { - if let name = deviceName { - Text(name) - .font(.caption2) - .foregroundStyle(.secondary) - } - - Text("\(Int(level * 100))%") - .font(.title2) - .fontWeight(.semibold) - .widgetAccentable() - - if isCharging { - Label("Charging", systemImage: "bolt.fill") - .font(.caption2) - .foregroundStyle(.green) - } - } - - Spacer() - - Image(systemName: batteryIcon) - .font(.title) - .widgetAccentable() - } - - case .accessoryInline: - Label("\(Int(level * 100))%\(isCharging ? " ⚡" : "")", systemImage: batteryIcon) - - default: - EmptyView() - } - } -} - -// MARK: - 12. Timer Widget (All Families) - -/// Countdown timer widget supporting all families -@available(iOS 16.0, *) -public struct LockScreenTimerWidget: View { - let endTime: Date - let title: String? - let family: WidgetFamily - - public init( - endTime: Date, - title: String? = nil, - family: WidgetFamily = .accessoryCircular - ) { - self.endTime = endTime - self.title = title - self.family = family - } - - private var progress: Double { - // Calculate progress (assuming 1 hour max for visualization) - let remaining = endTime.timeIntervalSince(Date()) - return max(0, min(1, remaining / 3600)) - } - - public var body: some View { - switch family { - case .accessoryCircular: - ZStack { - ProgressView(timerInterval: Date()...endTime) { - Image(systemName: "timer") - } - .progressViewStyle(.circular) - } - - case .accessoryRectangular: - VStack(alignment: .leading, spacing: 2) { - if let title = title { - Text(title) - .font(.caption2) - .foregroundStyle(.secondary) - } - - Text(endTime, style: .timer) - .font(.title) - .fontWeight(.medium) - .monospacedDigit() - .widgetAccentable() - - ProgressView(value: progress) - .tint(.primary) - } - - case .accessoryInline: - Label { - Text(endTime, style: .timer) - } icon: { - Image(systemName: "timer") - } - - default: - EmptyView() - } - } -} - -// MARK: - 13. Fitness Widget (All Families) - -/// Fitness activity widget -@available(iOS 16.0, *) -public struct LockScreenFitnessWidget: View { - let moveProgress: Double - let exerciseProgress: Double - let standProgress: Double - let family: WidgetFamily - - public init( - moveProgress: Double, - exerciseProgress: Double, - standProgress: Double, - family: WidgetFamily = .accessoryCircular - ) { - self.moveProgress = moveProgress - self.exerciseProgress = exerciseProgress - self.standProgress = standProgress - self.family = family - } - - public var body: some View { - switch family { - case .accessoryCircular: - ZStack { - // Move ring (outer) - Circle() - .stroke(Color.red.opacity(0.3), lineWidth: 4) - Circle() - .trim(from: 0, to: min(moveProgress, 1)) - .stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - - // Exercise ring (middle) - Circle() - .stroke(Color.green.opacity(0.3), lineWidth: 4) - .padding(5) - Circle() - .trim(from: 0, to: min(exerciseProgress, 1)) - .stroke(Color.green, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .padding(5) - - // Stand ring (inner) - Circle() - .stroke(Color.cyan.opacity(0.3), lineWidth: 4) - .padding(10) - Circle() - .trim(from: 0, to: min(standProgress, 1)) - .stroke(Color.cyan, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .padding(10) - } - - case .accessoryRectangular: - HStack { - VStack(alignment: .leading, spacing: 4) { - FitnessRow(label: "Move", progress: moveProgress, color: .red) - FitnessRow(label: "Exercise", progress: exerciseProgress, color: .green) - FitnessRow(label: "Stand", progress: standProgress, color: .cyan) - } - } - - case .accessoryInline: - Label("\(Int(moveProgress * 100))% · \(Int(exerciseProgress * 100))% · \(Int(standProgress * 100))%", systemImage: "figure.walk") - - default: - EmptyView() - } - } - - struct FitnessRow: View { - let label: String - let progress: Double - let color: Color - - var body: some View { - HStack(spacing: 4) { - Circle() - .fill(color) - .frame(width: 6, height: 6) - - Text(label) - .font(.system(size: 9)) - - Gauge(value: progress, in: 0...1) { - EmptyView() - } - .gaugeStyle(.linearCapacity) - .tint(color) - - Text("\(Int(progress * 100))%") - .font(.system(size: 9)) - .foregroundStyle(.secondary) - } - } - } -} - -// MARK: - 14. Countdown Widget (All Families) - -/// Event countdown widget -@available(iOS 16.0, *) -public struct LockScreenCountdownWidget: View { - let targetDate: Date - let eventName: String - let family: WidgetFamily - - public init( - targetDate: Date, - eventName: String, - family: WidgetFamily = .accessoryCircular - ) { - self.targetDate = targetDate - self.eventName = eventName - self.family = family - } - - private var daysRemaining: Int { - let interval = targetDate.timeIntervalSince(Date()) - return max(0, Int(interval / 86400)) - } - - public var body: some View { - switch family { - case .accessoryCircular: - VStack(spacing: 2) { - Text("\(daysRemaining)") - .font(.system(size: 24, weight: .bold)) - Text("DAYS") - .font(.system(size: 8, weight: .medium)) - } - .background(AccessoryWidgetBackground()) - - case .accessoryRectangular: - VStack(alignment: .leading, spacing: 2) { - Text(eventName) - .font(.caption) - .foregroundStyle(.secondary) - - HStack(alignment: .firstTextBaseline, spacing: 4) { - Text("\(daysRemaining)") - .font(.title) - .fontWeight(.bold) - .widgetAccentable() - Text("days remaining") - .font(.caption) - } - - Text(targetDate, format: .dateTime.month().day().year()) - .font(.caption2) - .foregroundStyle(.secondary) - } - - case .accessoryInline: - Text("\(daysRemaining) days until \(eventName)") - - default: - EmptyView() - } - } -} - -// MARK: - 15. Quick Action Widget - -/// Quick action shortcut widget -@available(iOS 16.0, *) -public struct LockScreenQuickActionWidget: View { - let icon: String - let label: String - let family: WidgetFamily - - public init( - icon: String, - label: String, - family: WidgetFamily = .accessoryCircular - ) { - self.icon = icon - self.label = label - self.family = family - } - - public var body: some View { - switch family { - case .accessoryCircular: - ZStack { - AccessoryWidgetBackground() - - Image(systemName: icon) - .font(.title2) - } - - case .accessoryRectangular: - HStack { - Image(systemName: icon) - .font(.title) - .widgetAccentable() - - Text(label) - .font(.headline) - - Spacer() - - Image(systemName: "chevron.right") - .font(.caption) - .foregroundStyle(.secondary) - } - - case .accessoryInline: - Label(label, systemImage: icon) - - default: - EmptyView() - } - } -} diff --git a/Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift new file mode 100644 index 0000000..914219d --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift @@ -0,0 +1,366 @@ +// Lock Screen (accessory family) widget views. +// Accessory widget families do not exist on macOS, so this file is iOS/watchOS only. + +#if os(iOS) || os(watchOS) +import SwiftUI +import WidgetKit + +// MARK: - Battery Widget (All Families) + +/// Battery level widget supporting all Lock Screen families +public struct LockScreenBatteryWidget: View { + let level: Double + let isCharging: Bool + let deviceName: String? + let family: WidgetFamily + + public init( + level: Double, + isCharging: Bool = false, + deviceName: String? = nil, + family: WidgetFamily = .accessoryCircular + ) { + self.level = level + self.isCharging = isCharging + self.deviceName = deviceName + self.family = family + } + + private var batteryIcon: String { + WidgetMath.batterySymbolName(level: level, isCharging: isCharging) + } + + public var body: some View { + switch family { + case .accessoryCircular: + Gauge(value: WidgetMath.clampedFraction(level), in: 0...1) { + Image(systemName: batteryIcon) + } currentValueLabel: { + Text("\(WidgetMath.percent(level))") + .font(.system(size: 14, weight: .semibold)) + } + .gaugeStyle(.accessoryCircularCapacity) + .tint(level < 0.2 ? .red : (level < 0.5 ? .orange : .green)) + + case .accessoryRectangular: + HStack { + VStack(alignment: .leading, spacing: 2) { + if let name = deviceName { + Text(name) + .font(.caption2) + .foregroundStyle(.secondary) + } + + Text(WidgetMath.percentString(level)) + .font(.title2) + .fontWeight(.semibold) + .widgetAccentable() + + if isCharging { + Label("Charging", systemImage: "bolt.fill") + .font(.caption2) + .foregroundStyle(.green) + } + } + + Spacer() + + Image(systemName: batteryIcon) + .font(.title) + .widgetAccentable() + } + + case .accessoryInline: + Label(WidgetMath.percentString(level), systemImage: batteryIcon) + + default: + EmptyView() + } + } +} + +// MARK: - Timer Widget (All Families) + +/// Countdown timer widget supporting all accessory families. +/// +/// The system animates the timer text and progress between `startTime` and +/// `endTime`, so the widget does not need extra timeline entries to tick. +public struct LockScreenTimerWidget: View { + let interval: ClosedRange + let title: String? + let family: WidgetFamily + + public init( + startTime: Date, + endTime: Date, + title: String? = nil, + family: WidgetFamily = .accessoryCircular + ) { + self.interval = WidgetMath.timerInterval(start: startTime, end: endTime) + self.title = title + self.family = family + } + + private var endTime: Date { interval.upperBound } + + public var body: some View { + switch family { + case .accessoryCircular: + ProgressView(timerInterval: interval) { + Image(systemName: "timer") + } + .progressViewStyle(.circular) + + case .accessoryRectangular: + VStack(alignment: .leading, spacing: 2) { + if let title = title { + Text(title) + .font(.caption2) + .foregroundStyle(.secondary) + } + + Text(endTime, style: .timer) + .font(.title) + .fontWeight(.medium) + .monospacedDigit() + .widgetAccentable() + + ProgressView(timerInterval: interval, countsDown: true) { + EmptyView() + } currentValueLabel: { + EmptyView() + } + .tint(.primary) + } + + case .accessoryInline: + Label { + Text(endTime, style: .timer) + } icon: { + Image(systemName: "timer") + } + + default: + EmptyView() + } + } +} + +// MARK: - Fitness Widget (All Families) + +/// Fitness activity widget +public struct LockScreenFitnessWidget: View { + let moveProgress: Double + let exerciseProgress: Double + let standProgress: Double + let family: WidgetFamily + + public init( + moveProgress: Double, + exerciseProgress: Double, + standProgress: Double, + family: WidgetFamily = .accessoryCircular + ) { + self.moveProgress = moveProgress + self.exerciseProgress = exerciseProgress + self.standProgress = standProgress + self.family = family + } + + public var body: some View { + switch family { + case .accessoryCircular: + ZStack { + // Move ring (outer) + Circle() + .stroke(Color.red.opacity(0.3), lineWidth: 4) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(moveProgress)) + .stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + + // Exercise ring (middle) + Circle() + .stroke(Color.green.opacity(0.3), lineWidth: 4) + .padding(5) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(exerciseProgress)) + .stroke(Color.green, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(5) + + // Stand ring (inner) + Circle() + .stroke(Color.cyan.opacity(0.3), lineWidth: 4) + .padding(10) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(standProgress)) + .stroke(Color.cyan, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(10) + } + + case .accessoryRectangular: + HStack { + VStack(alignment: .leading, spacing: 4) { + FitnessRow(label: "Move", progress: moveProgress, color: .red) + FitnessRow(label: "Exercise", progress: exerciseProgress, color: .green) + FitnessRow(label: "Stand", progress: standProgress, color: .cyan) + } + } + + case .accessoryInline: + Label("\(WidgetMath.percentString(moveProgress)) · \(WidgetMath.percentString(exerciseProgress)) · \(WidgetMath.percentString(standProgress))", systemImage: "figure.walk") + + default: + EmptyView() + } + } + + struct FitnessRow: View { + let label: String + let progress: Double + let color: Color + + var body: some View { + HStack(spacing: 4) { + Circle() + .fill(color) + .frame(width: 6, height: 6) + + Text(label) + .font(.system(size: 9)) + + Gauge(value: WidgetMath.clampedFraction(progress), in: 0...1) { + EmptyView() + } + .gaugeStyle(.linearCapacity) + .tint(color) + + Text(WidgetMath.percentString(progress)) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Countdown Widget (All Families) + +/// Event countdown widget +public struct LockScreenCountdownWidget: View { + let targetDate: Date + let eventName: String + let referenceDate: Date + let family: WidgetFamily + + /// - Parameter referenceDate: The timeline entry date; pass `entry.date` from your provider. + public init( + targetDate: Date, + eventName: String, + referenceDate: Date = Date(), + family: WidgetFamily = .accessoryCircular + ) { + self.targetDate = targetDate + self.eventName = eventName + self.referenceDate = referenceDate + self.family = family + } + + private var daysRemaining: Int { + WidgetMath.daysRemaining(until: targetDate, from: referenceDate) + } + + public var body: some View { + switch family { + case .accessoryCircular: + VStack(spacing: 2) { + Text("\(daysRemaining)") + .font(.system(size: 24, weight: .bold)) + Text("DAYS") + .font(.system(size: 8, weight: .medium)) + } + .background(AccessoryWidgetBackground()) + + case .accessoryRectangular: + VStack(alignment: .leading, spacing: 2) { + Text(eventName) + .font(.caption) + .foregroundStyle(.secondary) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text("\(daysRemaining)") + .font(.title) + .fontWeight(.bold) + .widgetAccentable() + Text(daysRemaining == 1 ? "day remaining" : "days remaining") + .font(.caption) + } + + Text(targetDate, format: .dateTime.month().day().year()) + .font(.caption2) + .foregroundStyle(.secondary) + } + + case .accessoryInline: + Text("\(daysRemaining) \(daysRemaining == 1 ? "day" : "days") until \(eventName)") + + default: + EmptyView() + } + } +} + +// MARK: - Quick Action Widget + +/// Quick action shortcut widget +public struct LockScreenQuickActionWidget: View { + let icon: String + let label: String + let family: WidgetFamily + + public init( + icon: String, + label: String, + family: WidgetFamily = .accessoryCircular + ) { + self.icon = icon + self.label = label + self.family = family + } + + public var body: some View { + switch family { + case .accessoryCircular: + ZStack { + AccessoryWidgetBackground() + + Image(systemName: icon) + .font(.title2) + } + + case .accessoryRectangular: + HStack { + Image(systemName: icon) + .font(.title) + .widgetAccentable() + + Text(label) + .font(.headline) + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.secondary) + } + + case .accessoryInline: + Label(label, systemImage: icon) + + default: + EmptyView() + } + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift new file mode 100644 index 0000000..0949d2a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift @@ -0,0 +1,406 @@ +// Lock Screen (accessory family) widget views. +// Accessory widget families do not exist on macOS, so this file is iOS/watchOS only. + +#if os(iOS) || os(watchOS) +import SwiftUI +import WidgetKit + +// MARK: - Lock Screen Widget Families + +/// Supported Lock Screen widget families +public enum LockScreenWidgetFamily: CaseIterable, Sendable { + case accessoryCircular + case accessoryRectangular + case accessoryInline + + public var widgetFamily: WidgetFamily { + switch self { + case .accessoryCircular: + return .accessoryCircular + case .accessoryRectangular: + return .accessoryRectangular + case .accessoryInline: + return .accessoryInline + } + } +} + +// MARK: - Circular Progress Widget + +/// Circular progress indicator widget +public struct CircularProgressWidget: View { + let progress: Double + let icon: String + let title: String + let accentColor: Color + + public init( + progress: Double, + icon: String = "star.fill", + title: String = "", + accentColor: Color = .blue + ) { + self.progress = progress + self.icon = icon + self.title = title + self.accentColor = accentColor + } + + public var body: some View { + Gauge(value: WidgetMath.clampedFraction(progress), in: 0...1) { + Image(systemName: icon) + } currentValueLabel: { + Text(WidgetMath.percentString(progress)) + .font(.system(size: 12, weight: .semibold)) + } + .gaugeStyle(.accessoryCircularCapacity) + .tint(accentColor) + } +} + +// MARK: - Circular Icon Widget + +/// Simple icon display widget +public struct CircularIconWidget: View { + let icon: String + let value: String? + let backgroundColor: Color + + public init( + icon: String, + value: String? = nil, + backgroundColor: Color = .blue + ) { + self.icon = icon + self.value = value + self.backgroundColor = backgroundColor + } + + public var body: some View { + ZStack { + AccessoryWidgetBackground() + + VStack(spacing: 2) { + Image(systemName: icon) + .font(.title3) + + if let value = value { + Text(value) + .font(.system(size: 10, weight: .semibold)) + } + } + } + } +} + +// MARK: - Circular Ring Widget + +/// Activity ring style widget +public struct CircularRingWidget: View { + let innerProgress: Double + let outerProgress: Double + let innerColor: Color + let outerColor: Color + let centerIcon: String + + public init( + innerProgress: Double, + outerProgress: Double, + innerColor: Color = .cyan, + outerColor: Color = .green, + centerIcon: String = "figure.walk" + ) { + self.innerProgress = innerProgress + self.outerProgress = outerProgress + self.innerColor = innerColor + self.outerColor = outerColor + self.centerIcon = centerIcon + } + + public var body: some View { + ZStack { + // Outer ring + Circle() + .stroke(outerColor.opacity(0.3), lineWidth: 4) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(outerProgress)) + .stroke(outerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + + // Inner ring + Circle() + .stroke(innerColor.opacity(0.3), lineWidth: 4) + .padding(6) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(innerProgress)) + .stroke(innerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(6) + + // Center icon + Image(systemName: centerIcon) + .font(.body) + } + } +} + +// MARK: - Rectangular Text Widget + +/// Multi-line text widget +public struct RectangularTextWidget: View { + let title: String + let subtitle: String + let detail: String? + let icon: String + + public init( + title: String, + subtitle: String, + detail: String? = nil, + icon: String = "star" + ) { + self.title = title + self.subtitle = subtitle + self.detail = detail + self.icon = icon + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.headline) + .widgetAccentable() + + Text(subtitle) + .font(.caption) + + if let detail = detail { + Text(detail) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: icon) + .font(.title2) + .widgetAccentable() + } + } +} + +// MARK: - Rectangular Stats Widget + +/// Statistics display widget +public struct RectangularStatsWidget: View { + let stats: [(String, String)] + let title: String? + + public init(stats: [(String, String)], title: String? = nil) { + self.stats = stats + self.title = title + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + if let title = title { + Text(title) + .font(.caption2) + .foregroundStyle(.secondary) + } + + HStack { + ForEach(Array(stats.prefix(3).enumerated()), id: \.offset) { _, stat in + VStack(spacing: 2) { + Text(stat.1) + .font(.headline) + .widgetAccentable() + Text(stat.0) + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } + } + } + } +} + +// MARK: - Rectangular Progress Widget + +/// Progress bar widget +public struct RectangularProgressWidget: View { + let title: String + let progress: Double + let currentValue: String + let maxValue: String + let icon: String + + public init( + title: String, + progress: Double, + currentValue: String, + maxValue: String, + icon: String = "chart.bar.fill" + ) { + self.title = title + self.progress = progress + self.currentValue = currentValue + self.maxValue = maxValue + self.icon = icon + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: icon) + .widgetAccentable() + + Text(title) + .font(.caption) + + Spacer() + + Text("\(currentValue)/\(maxValue)") + .font(.caption2) + .foregroundStyle(.secondary) + } + + ProgressView(value: WidgetMath.clampedFraction(progress), total: 1.0) + .tint(.primary) + } + } +} + +// MARK: - Rectangular Calendar Widget + +/// Upcoming events widget +public struct RectangularCalendarWidget: View { + let events: [(String, Date)] + + public init(events: [(String, Date)]) { + self.events = events + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + ForEach(Array(events.prefix(2).enumerated()), id: \.offset) { _, event in + HStack { + Circle() + .fill(Color.primary) + .frame(width: 6, height: 6) + + Text(event.0) + .font(.caption) + .lineLimit(1) + + Spacer() + + Text(event.1, style: .time) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + } +} + +// MARK: - Rectangular Weather Widget + +/// Weather conditions widget +public struct RectangularWeatherWidget: View { + let temperature: Int + let high: Int + let low: Int + let condition: String + let location: String + + public init( + temperature: Int, + high: Int, + low: Int, + condition: String = "sun.max.fill", + location: String + ) { + self.temperature = temperature + self.high = high + self.low = low + self.condition = condition + self.location = location + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(location) + .font(.caption2) + .foregroundStyle(.secondary) + + Text("\(temperature)°") + .font(.title) + .fontWeight(.medium) + .widgetAccentable() + + HStack(spacing: 4) { + Text("H:\(high)°") + Text("L:\(low)°") + } + .font(.caption2) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: condition) + .font(.largeTitle) + .widgetAccentable() + } + } +} + +// MARK: - Inline Text Widget + +/// Simple inline text widget +public struct InlineTextWidget: View { + let icon: String? + let text: String + + public init(icon: String? = nil, text: String) { + self.icon = icon + self.text = text + } + + public var body: some View { + if let icon = icon { + Label(text, systemImage: icon) + } else { + Text(text) + } + } +} + +// MARK: - Inline Stats Widget + +/// Inline statistics widget +public struct InlineStatsWidget: View { + let label: String + let value: String + let icon: String + + public init(label: String, value: String, icon: String) { + self.label = label + self.value = value + self.icon = icon + } + + public var body: some View { + Label { + Text("\(label): \(value)") + } icon: { + Image(systemName: icon) + } + } +} +#endif From 912c5532757c41956f66f55ffc4b609634f56cfd Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:03:39 +0300 Subject: [PATCH 10/59] feat(standby): restore StandBy widget views The month grid now follows the calendar's first weekday and uses unique column identifiers, fitness rings guard against zero goals, and the countdown is computed from the timeline entry date instead of Date(), dropping the seconds column a static widget snapshot cannot update. --- Sources/StandBy/StandByWidgets.swift | 851 ------------------ .../StandBy/StandByActivityWidgets.swift | 268 ++++++ .../StandBy/StandByClock.swift | 218 +++++ .../StandBy/StandByInfoWidgets.swift | 295 ++++++ 4 files changed, 781 insertions(+), 851 deletions(-) delete mode 100644 Sources/StandBy/StandByWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift create mode 100644 Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift diff --git a/Sources/StandBy/StandByWidgets.swift b/Sources/StandBy/StandByWidgets.swift deleted file mode 100644 index 496c80b..0000000 --- a/Sources/StandBy/StandByWidgets.swift +++ /dev/null @@ -1,851 +0,0 @@ -// StandByWidgets.swift -// iOS-Widget-Development-Kit -// -// iOS 17+ StandBy Mode Widget Support -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit - -// MARK: - StandBy Mode Configuration - -/// Configuration for StandBy mode widgets -@available(iOS 17.0, *) -public struct StandByConfiguration { - public let supportsStandBy: Bool - public let fullColorMode: Bool - public let nightModeColor: Color - public let alwaysOnDisplay: Bool - - public init( - supportsStandBy: Bool = true, - fullColorMode: Bool = true, - nightModeColor: Color = .red, - alwaysOnDisplay: Bool = true - ) { - self.supportsStandBy = supportsStandBy - self.fullColorMode = fullColorMode - self.nightModeColor = nightModeColor - self.alwaysOnDisplay = alwaysOnDisplay - } -} - -// MARK: - StandBy View Modifier - -/// View modifier for StandBy mode optimizations -@available(iOS 17.0, *) -public struct StandByOptimizedModifier: ViewModifier { - @Environment(\.showsWidgetContainerBackground) var showsBackground - @Environment(\.widgetRenderingMode) var renderingMode - - let nightModeColor: Color - - public init(nightModeColor: Color = .red) { - self.nightModeColor = nightModeColor - } - - public func body(content: Content) -> some View { - content - .foregroundStyle(renderingMode == .fullColor ? .primary : nightModeColor) - .containerBackground(for: .widget) { - if showsBackground { - Color.clear - } - } - } -} - -@available(iOS 17.0, *) -extension View { - public func standByOptimized(nightModeColor: Color = .red) -> some View { - modifier(StandByOptimizedModifier(nightModeColor: nightModeColor)) - } -} - -// MARK: - StandBy Clock Widget - -/// Large clock widget optimized for StandBy mode -@available(iOS 17.0, *) -public struct StandByClockWidget: View { - let date: Date - let style: ClockStyle - let showDate: Bool - let accentColor: Color - - public enum ClockStyle { - case digital - case analog - case flipClock - case minimal - } - - public init( - date: Date = Date(), - style: ClockStyle = .digital, - showDate: Bool = true, - accentColor: Color = .white - ) { - self.date = date - self.style = style - self.showDate = showDate - self.accentColor = accentColor - } - - public var body: some View { - VStack(spacing: 16) { - switch style { - case .digital: - DigitalClockView(date: date, accentColor: accentColor) - case .analog: - AnalogClockView(date: date, accentColor: accentColor) - case .flipClock: - FlipClockView(date: date, accentColor: accentColor) - case .minimal: - MinimalClockView(date: date, accentColor: accentColor) - } - - if showDate { - Text(date, format: .dateTime.weekday(.wide).month().day()) - .font(.title3) - .foregroundStyle(accentColor.opacity(0.8)) - } - } - .containerBackground(.black.gradient, for: .widget) - } - - struct DigitalClockView: View { - let date: Date - let accentColor: Color - - var body: some View { - Text(date, format: .dateTime.hour().minute()) - .font(.system(size: 96, weight: .thin, design: .rounded)) - .monospacedDigit() - .foregroundStyle(accentColor) - } - } - - struct AnalogClockView: View { - let date: Date - let accentColor: Color - - var hourAngle: Double { - let hour = Calendar.current.component(.hour, from: date) % 12 - let minute = Calendar.current.component(.minute, from: date) - return Double(hour) * 30 + Double(minute) * 0.5 - } - - var minuteAngle: Double { - let minute = Calendar.current.component(.minute, from: date) - return Double(minute) * 6 - } - - var body: some View { - GeometryReader { geo in - let size = min(geo.size.width, geo.size.height) - - ZStack { - // Clock face - Circle() - .stroke(accentColor.opacity(0.3), lineWidth: 3) - - // Hour markers - ForEach(0..<12) { hour in - Rectangle() - .fill(accentColor) - .frame(width: hour % 3 == 0 ? 3 : 1, height: hour % 3 == 0 ? 15 : 8) - .offset(y: -size/2 + 20) - .rotationEffect(.degrees(Double(hour) * 30)) - } - - // Hour hand - RoundedRectangle(cornerRadius: 3) - .fill(accentColor) - .frame(width: 6, height: size * 0.25) - .offset(y: -size * 0.125) - .rotationEffect(.degrees(hourAngle)) - - // Minute hand - RoundedRectangle(cornerRadius: 2) - .fill(accentColor) - .frame(width: 4, height: size * 0.35) - .offset(y: -size * 0.175) - .rotationEffect(.degrees(minuteAngle)) - - // Center dot - Circle() - .fill(accentColor) - .frame(width: 12, height: 12) - } - .frame(width: size, height: size) - .position(x: geo.size.width / 2, y: geo.size.height / 2) - } - .aspectRatio(1, contentMode: .fit) - } - } - - struct FlipClockView: View { - let date: Date - let accentColor: Color - - var hour: String { - let formatter = DateFormatter() - formatter.dateFormat = "HH" - return formatter.string(from: date) - } - - var minute: String { - let formatter = DateFormatter() - formatter.dateFormat = "mm" - return formatter.string(from: date) - } - - var body: some View { - HStack(spacing: 8) { - FlipDigitView(digit: hour, accentColor: accentColor) - - VStack(spacing: 12) { - Circle() - .fill(accentColor) - .frame(width: 10, height: 10) - Circle() - .fill(accentColor) - .frame(width: 10, height: 10) - } - - FlipDigitView(digit: minute, accentColor: accentColor) - } - } - } - - struct FlipDigitView: View { - let digit: String - let accentColor: Color - - var body: some View { - Text(digit) - .font(.system(size: 80, weight: .bold, design: .rounded)) - .monospacedDigit() - .foregroundStyle(accentColor) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(accentColor.opacity(0.15)) - ) - } - } - - struct MinimalClockView: View { - let date: Date - let accentColor: Color - - var body: some View { - Text(date, format: .dateTime.hour().minute()) - .font(.system(size: 120, weight: .ultraLight, design: .rounded)) - .monospacedDigit() - .foregroundStyle(accentColor) - } - } -} - -// MARK: - StandBy Photo Widget - -/// Photo display widget for StandBy mode -@available(iOS 17.0, *) -public struct StandByPhotoWidget: View { - let caption: String? - let date: Date? - let style: PhotoStyle - - public enum PhotoStyle { - case full - case polaroid - case rounded - case minimal - } - - public init( - caption: String? = nil, - date: Date? = nil, - style: PhotoStyle = .full - ) { - self.caption = caption - self.date = date - self.style = style - } - - public var body: some View { - GeometryReader { geo in - VStack(spacing: 0) { - // Photo placeholder - Rectangle() - .fill( - LinearGradient( - colors: [.gray.opacity(0.3), .gray.opacity(0.5)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .overlay { - Image(systemName: "photo.fill") - .font(.system(size: 48)) - .foregroundStyle(.white.opacity(0.5)) - } - .frame(maxHeight: style == .polaroid ? geo.size.height * 0.8 : .infinity) - - if style == .polaroid { - VStack(spacing: 4) { - if let caption = caption { - Text(caption) - .font(.caption) - .foregroundStyle(.black) - } - if let date = date { - Text(date, format: .dateTime.month().day().year()) - .font(.caption2) - .foregroundStyle(.gray) - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(.white) - } - } - .clipShape(RoundedRectangle(cornerRadius: style == .rounded ? 20 : (style == .minimal ? 0 : 8))) - .shadow(radius: style == .polaroid ? 10 : 0) - } - .containerBackground(.black, for: .widget) - } -} - -// MARK: - StandBy Weather Widget - -/// Weather widget optimized for StandBy mode -@available(iOS 17.0, *) -public struct StandByWeatherWidget: View { - let temperature: Int - let condition: WeatherCondition - let location: String - let high: Int - let low: Int - let hourlyForecast: [HourlyForecast] - - public enum WeatherCondition: String { - case sunny = "sun.max.fill" - case cloudy = "cloud.fill" - case rainy = "cloud.rain.fill" - case stormy = "cloud.bolt.rain.fill" - case snowy = "cloud.snow.fill" - case partlyCloudy = "cloud.sun.fill" - - var color: Color { - switch self { - case .sunny: return .yellow - case .cloudy: return .gray - case .rainy: return .blue - case .stormy: return .purple - case .snowy: return .cyan - case .partlyCloudy: return .orange - } - } - } - - public struct HourlyForecast: Identifiable { - public let id = UUID() - public let hour: String - public let temp: Int - public let condition: WeatherCondition - - public init(hour: String, temp: Int, condition: WeatherCondition) { - self.hour = hour - self.temp = temp - self.condition = condition - } - } - - public init( - temperature: Int, - condition: WeatherCondition, - location: String, - high: Int, - low: Int, - hourlyForecast: [HourlyForecast] = [] - ) { - self.temperature = temperature - self.condition = condition - self.location = location - self.high = high - self.low = low - self.hourlyForecast = hourlyForecast - } - - public var body: some View { - VStack(spacing: 20) { - // Location - Text(location) - .font(.title3) - .foregroundStyle(.white.opacity(0.8)) - - // Main temperature - HStack(alignment: .top, spacing: 16) { - Image(systemName: condition.rawValue) - .font(.system(size: 64)) - .foregroundStyle(condition.color) - - VStack(alignment: .leading) { - Text("\(temperature)°") - .font(.system(size: 72, weight: .thin)) - .foregroundStyle(.white) - - HStack { - Text("H:\(high)°") - Text("L:\(low)°") - } - .font(.title3) - .foregroundStyle(.white.opacity(0.7)) - } - } - - // Hourly forecast - if !hourlyForecast.isEmpty { - HStack(spacing: 16) { - ForEach(hourlyForecast.prefix(5)) { forecast in - VStack(spacing: 8) { - Text(forecast.hour) - .font(.caption) - Image(systemName: forecast.condition.rawValue) - .font(.title3) - .foregroundStyle(forecast.condition.color) - Text("\(forecast.temp)°") - .font(.caption) - .fontWeight(.medium) - } - .foregroundStyle(.white.opacity(0.8)) - } - } - } - } - .padding() - .containerBackground(.black.gradient, for: .widget) - } -} - -// MARK: - StandBy Calendar Widget - -/// Calendar widget for StandBy mode -@available(iOS 17.0, *) -public struct StandByCalendarWidget: View { - let date: Date - let events: [CalendarEvent] - let accentColor: Color - - public struct CalendarEvent: Identifiable { - public let id = UUID() - public let title: String - public let time: Date - public let color: Color - - public init(title: String, time: Date, color: Color = .blue) { - self.title = title - self.time = time - self.color = color - } - } - - public init( - date: Date = Date(), - events: [CalendarEvent] = [], - accentColor: Color = .blue - ) { - self.date = date - self.events = events - self.accentColor = accentColor - } - - private var calendar: Calendar { Calendar.current } - - private var monthDays: [[Int?]] { - let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: date))! - let range = calendar.range(of: .day, in: .month, for: startOfMonth)! - let firstWeekday = calendar.component(.weekday, from: startOfMonth) - - var days: [[Int?]] = [] - var week: [Int?] = [] - - // Leading empty days - for _ in 1.. 0 else { return (0, 0, 0, 0) } - - let days = Int(interval) / 86400 - let hours = (Int(interval) % 86400) / 3600 - let minutes = (Int(interval) % 3600) / 60 - let seconds = Int(interval) % 60 - return (days, hours, minutes, seconds) - } - - public var body: some View { - VStack(spacing: 24) { - Text(eventName) - .font(.title2) - .foregroundStyle(.white.opacity(0.8)) - - HStack(spacing: 16) { - CountdownUnit(value: timeComponents.days, label: "DAYS", color: accentColor) - CountdownUnit(value: timeComponents.hours, label: "HOURS", color: accentColor) - CountdownUnit(value: timeComponents.minutes, label: "MIN", color: accentColor) - CountdownUnit(value: timeComponents.seconds, label: "SEC", color: accentColor) - } - - Text(targetDate, format: .dateTime.month().day().year()) - .font(.caption) - .foregroundStyle(.white.opacity(0.5)) - } - .padding() - .containerBackground(.black.gradient, for: .widget) - } - - struct CountdownUnit: View { - let value: Int - let label: String - let color: Color - - var body: some View { - VStack(spacing: 8) { - Text(String(format: "%02d", value)) - .font(.system(size: 48, weight: .bold, design: .rounded)) - .monospacedDigit() - .foregroundStyle(color) - - Text(label) - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.white.opacity(0.6)) - } - } - } -} diff --git a/Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift b/Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift new file mode 100644 index 0000000..588b0ba --- /dev/null +++ b/Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift @@ -0,0 +1,268 @@ +import SwiftUI +import WidgetKit + +// MARK: - StandBy Fitness Widget + +/// Fitness activity rings for StandBy mode +public struct StandByFitnessWidget: View { + let calories: Double + let caloriesGoal: Double + let exercise: Double + let exerciseGoal: Double + let standing: Double + let standingGoal: Double + + public init( + calories: Double, + caloriesGoal: Double = 500, + exercise: Double, + exerciseGoal: Double = 30, + standing: Double, + standingGoal: Double = 12 + ) { + self.calories = calories + self.caloriesGoal = caloriesGoal + self.exercise = exercise + self.exerciseGoal = exerciseGoal + self.standing = standing + self.standingGoal = standingGoal + } + + public var body: some View { + HStack(spacing: 40) { + // Activity rings + ZStack { + ActivityRing(progress: WidgetMath.fraction(calories, of: caloriesGoal), color: .red, lineWidth: 16) + .frame(width: 140, height: 140) + + ActivityRing(progress: WidgetMath.fraction(exercise, of: exerciseGoal), color: .green, lineWidth: 14) + .frame(width: 108, height: 108) + + ActivityRing(progress: WidgetMath.fraction(standing, of: standingGoal), color: .cyan, lineWidth: 12) + .frame(width: 80, height: 80) + } + + // Stats + VStack(alignment: .leading, spacing: 20) { + StatRow(label: "Move", value: "\(Int(calories))", unit: "CAL", color: .red, progress: WidgetMath.fraction(calories, of: caloriesGoal)) + StatRow(label: "Exercise", value: "\(Int(exercise))", unit: "MIN", color: .green, progress: WidgetMath.fraction(exercise, of: exerciseGoal)) + StatRow(label: "Stand", value: "\(Int(standing))", unit: "HRS", color: .cyan, progress: WidgetMath.fraction(standing, of: standingGoal)) + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } + + struct ActivityRing: View { + let progress: Double + let color: Color + let lineWidth: CGFloat + + var body: some View { + Circle() + .stroke(color.opacity(0.3), lineWidth: lineWidth) + .overlay { + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(progress)) + .stroke( + color, + style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + } + } + } + + struct StatRow: View { + let label: String + let value: String + let unit: String + let color: Color + let progress: Double + + var body: some View { + HStack(spacing: 8) { + Circle() + .fill(color) + .frame(width: 10, height: 10) + + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(.caption) + .foregroundStyle(.white.opacity(0.7)) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(value) + .font(.title2) + .fontWeight(.bold) + Text(unit) + .font(.caption) + } + .foregroundStyle(.white) + } + } + } + } +} + +// MARK: - StandBy Music Widget + +/// Now playing widget for StandBy mode +public struct StandByMusicWidget: View { + let trackName: String + let artistName: String + let albumName: String + let isPlaying: Bool + let progress: Double + let accentColor: Color + + public init( + trackName: String, + artistName: String, + albumName: String, + isPlaying: Bool = true, + progress: Double = 0.5, + accentColor: Color = .purple + ) { + self.trackName = trackName + self.artistName = artistName + self.albumName = albumName + self.isPlaying = isPlaying + self.progress = progress + self.accentColor = accentColor + } + + public var body: some View { + HStack(spacing: 24) { + // Album art placeholder + RoundedRectangle(cornerRadius: 16) + .fill( + LinearGradient( + colors: [accentColor, accentColor.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 140, height: 140) + .overlay { + Image(systemName: "music.note") + .font(.system(size: 48)) + .foregroundStyle(.white.opacity(0.5)) + } + .shadow(color: accentColor.opacity(0.5), radius: 20) + + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(trackName) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + + Text(artistName) + .font(.title3) + .foregroundStyle(.white.opacity(0.7)) + .lineLimit(1) + + Text(albumName) + .font(.caption) + .foregroundStyle(.white.opacity(0.5)) + .lineLimit(1) + } + .foregroundStyle(.white) + + // Progress bar + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.white.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(accentColor) + .frame(width: geo.size.width * WidgetMath.clampedFraction(progress)) + } + } + .frame(height: 4) + + // Controls + HStack(spacing: 32) { + Image(systemName: "backward.fill") + .font(.title2) + + Image(systemName: isPlaying ? "pause.fill" : "play.fill") + .font(.system(size: 36)) + + Image(systemName: "forward.fill") + .font(.title2) + } + .foregroundStyle(.white) + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } +} + +// MARK: - StandBy Countdown Widget + +/// Event countdown for StandBy mode +public struct StandByCountdownWidget: View { + let eventName: String + let targetDate: Date + let referenceDate: Date + let accentColor: Color + + /// - Parameter referenceDate: The timeline entry date the countdown is computed from. + /// Widgets render a static snapshot per entry, so pass `entry.date` here. + public init( + eventName: String, + targetDate: Date, + referenceDate: Date = Date(), + accentColor: Color = .orange + ) { + self.eventName = eventName + self.targetDate = targetDate + self.referenceDate = referenceDate + self.accentColor = accentColor + } + + public var body: some View { + let remaining = CountdownComponents(from: referenceDate, to: targetDate) + VStack(spacing: 24) { + Text(eventName) + .font(.title2) + .foregroundStyle(.white.opacity(0.8)) + + HStack(spacing: 16) { + CountdownUnit(value: remaining.days, label: "DAYS", color: accentColor) + CountdownUnit(value: remaining.hours, label: "HOURS", color: accentColor) + CountdownUnit(value: remaining.minutes, label: "MIN", color: accentColor) + } + + Text(targetDate, format: .dateTime.month().day().year()) + .font(.caption) + .foregroundStyle(.white.opacity(0.5)) + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } + + struct CountdownUnit: View { + let value: Int + let label: String + let color: Color + + var body: some View { + VStack(spacing: 8) { + Text(String(format: "%02d", value)) + .font(.system(size: 48, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(color) + + Text(label) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.white.opacity(0.6)) + } + } + } +} diff --git a/Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift b/Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift new file mode 100644 index 0000000..b1e38a7 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift @@ -0,0 +1,218 @@ +import SwiftUI +import WidgetKit + +// MARK: - StandBy View Modifier + +/// View modifier for StandBy mode optimizations +public struct StandByOptimizedModifier: ViewModifier { + @Environment(\.showsWidgetContainerBackground) var showsBackground + @Environment(\.widgetRenderingMode) var renderingMode + + let nightModeColor: Color + + public init(nightModeColor: Color = .red) { + self.nightModeColor = nightModeColor + } + + public func body(content: Content) -> some View { + content + .foregroundStyle(renderingMode == .fullColor ? .primary : nightModeColor) + .containerBackground(for: .widget) { + if showsBackground { + Color.clear + } + } + } +} + +extension View { + public func standByOptimized(nightModeColor: Color = .red) -> some View { + modifier(StandByOptimizedModifier(nightModeColor: nightModeColor)) + } +} + +// MARK: - StandBy Clock Widget + +/// Large clock widget optimized for StandBy mode +public struct StandByClockWidget: View { + let date: Date + let style: ClockStyle + let showDate: Bool + let accentColor: Color + + public enum ClockStyle { + case digital + case analog + case flipClock + case minimal + } + + public init( + date: Date = Date(), + style: ClockStyle = .digital, + showDate: Bool = true, + accentColor: Color = .white + ) { + self.date = date + self.style = style + self.showDate = showDate + self.accentColor = accentColor + } + + public var body: some View { + VStack(spacing: 16) { + switch style { + case .digital: + DigitalClockView(date: date, accentColor: accentColor) + case .analog: + AnalogClockView(date: date, accentColor: accentColor) + case .flipClock: + FlipClockView(date: date, accentColor: accentColor) + case .minimal: + MinimalClockView(date: date, accentColor: accentColor) + } + + if showDate { + Text(date, format: .dateTime.weekday(.wide).month().day()) + .font(.title3) + .foregroundStyle(accentColor.opacity(0.8)) + } + } + .containerBackground(.black.gradient, for: .widget) + } + + struct DigitalClockView: View { + let date: Date + let accentColor: Color + + var body: some View { + Text(date, format: .dateTime.hour().minute()) + .font(.system(size: 96, weight: .thin, design: .rounded)) + .monospacedDigit() + .foregroundStyle(accentColor) + } + } + + struct AnalogClockView: View { + let date: Date + let accentColor: Color + + var hourAngle: Double { + let hour = Calendar.current.component(.hour, from: date) % 12 + let minute = Calendar.current.component(.minute, from: date) + return Double(hour) * 30 + Double(minute) * 0.5 + } + + var minuteAngle: Double { + let minute = Calendar.current.component(.minute, from: date) + return Double(minute) * 6 + } + + var body: some View { + GeometryReader { geo in + let size = min(geo.size.width, geo.size.height) + + ZStack { + // Clock face + Circle() + .stroke(accentColor.opacity(0.3), lineWidth: 3) + + // Hour markers + ForEach(0..<12) { hour in + Rectangle() + .fill(accentColor) + .frame(width: hour % 3 == 0 ? 3 : 1, height: hour % 3 == 0 ? 15 : 8) + .offset(y: -size/2 + 20) + .rotationEffect(.degrees(Double(hour) * 30)) + } + + // Hour hand + RoundedRectangle(cornerRadius: 3) + .fill(accentColor) + .frame(width: 6, height: size * 0.25) + .offset(y: -size * 0.125) + .rotationEffect(.degrees(hourAngle)) + + // Minute hand + RoundedRectangle(cornerRadius: 2) + .fill(accentColor) + .frame(width: 4, height: size * 0.35) + .offset(y: -size * 0.175) + .rotationEffect(.degrees(minuteAngle)) + + // Center dot + Circle() + .fill(accentColor) + .frame(width: 12, height: 12) + } + .frame(width: size, height: size) + .position(x: geo.size.width / 2, y: geo.size.height / 2) + } + .aspectRatio(1, contentMode: .fit) + } + } + + struct FlipClockView: View { + let date: Date + let accentColor: Color + + var hour: String { + let formatter = DateFormatter() + formatter.dateFormat = "HH" + return formatter.string(from: date) + } + + var minute: String { + let formatter = DateFormatter() + formatter.dateFormat = "mm" + return formatter.string(from: date) + } + + var body: some View { + HStack(spacing: 8) { + FlipDigitView(digit: hour, accentColor: accentColor) + + VStack(spacing: 12) { + Circle() + .fill(accentColor) + .frame(width: 10, height: 10) + Circle() + .fill(accentColor) + .frame(width: 10, height: 10) + } + + FlipDigitView(digit: minute, accentColor: accentColor) + } + } + } + + struct FlipDigitView: View { + let digit: String + let accentColor: Color + + var body: some View { + Text(digit) + .font(.system(size: 80, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(accentColor) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(accentColor.opacity(0.15)) + ) + } + } + + struct MinimalClockView: View { + let date: Date + let accentColor: Color + + var body: some View { + Text(date, format: .dateTime.hour().minute()) + .font(.system(size: 120, weight: .ultraLight, design: .rounded)) + .monospacedDigit() + .foregroundStyle(accentColor) + } + } +} diff --git a/Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift b/Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift new file mode 100644 index 0000000..760221a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift @@ -0,0 +1,295 @@ +import SwiftUI +import WidgetKit + +// MARK: - StandBy Photo Widget + +/// Photo display widget for StandBy mode +public struct StandByPhotoWidget: View { + let caption: String? + let date: Date? + let style: PhotoStyle + + public enum PhotoStyle { + case full + case polaroid + case rounded + case minimal + } + + public init( + caption: String? = nil, + date: Date? = nil, + style: PhotoStyle = .full + ) { + self.caption = caption + self.date = date + self.style = style + } + + public var body: some View { + GeometryReader { geo in + VStack(spacing: 0) { + // Photo placeholder + Rectangle() + .fill( + LinearGradient( + colors: [.gray.opacity(0.3), .gray.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .overlay { + Image(systemName: "photo.fill") + .font(.system(size: 48)) + .foregroundStyle(.white.opacity(0.5)) + } + .frame(maxHeight: style == .polaroid ? geo.size.height * 0.8 : .infinity) + + if style == .polaroid { + VStack(spacing: 4) { + if let caption = caption { + Text(caption) + .font(.caption) + .foregroundStyle(.black) + } + if let date = date { + Text(date, format: .dateTime.month().day().year()) + .font(.caption2) + .foregroundStyle(.gray) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(.white) + } + } + .clipShape(RoundedRectangle(cornerRadius: style == .rounded ? 20 : (style == .minimal ? 0 : 8))) + .shadow(radius: style == .polaroid ? 10 : 0) + } + .containerBackground(.black, for: .widget) + } +} + +// MARK: - StandBy Weather Widget + +/// Weather widget optimized for StandBy mode +public struct StandByWeatherWidget: View { + let temperature: Int + let condition: WeatherCondition + let location: String + let high: Int + let low: Int + let hourlyForecast: [HourlyForecast] + + public enum WeatherCondition: String { + case sunny = "sun.max.fill" + case cloudy = "cloud.fill" + case rainy = "cloud.rain.fill" + case stormy = "cloud.bolt.rain.fill" + case snowy = "cloud.snow.fill" + case partlyCloudy = "cloud.sun.fill" + + var color: Color { + switch self { + case .sunny: return .yellow + case .cloudy: return .gray + case .rainy: return .blue + case .stormy: return .purple + case .snowy: return .cyan + case .partlyCloudy: return .orange + } + } + } + + public struct HourlyForecast: Identifiable { + public let id = UUID() + public let hour: String + public let temp: Int + public let condition: WeatherCondition + + public init(hour: String, temp: Int, condition: WeatherCondition) { + self.hour = hour + self.temp = temp + self.condition = condition + } + } + + public init( + temperature: Int, + condition: WeatherCondition, + location: String, + high: Int, + low: Int, + hourlyForecast: [HourlyForecast] = [] + ) { + self.temperature = temperature + self.condition = condition + self.location = location + self.high = high + self.low = low + self.hourlyForecast = hourlyForecast + } + + public var body: some View { + VStack(spacing: 20) { + // Location + Text(location) + .font(.title3) + .foregroundStyle(.white.opacity(0.8)) + + // Main temperature + HStack(alignment: .top, spacing: 16) { + Image(systemName: condition.rawValue) + .font(.system(size: 64)) + .foregroundStyle(condition.color) + + VStack(alignment: .leading) { + Text("\(temperature)°") + .font(.system(size: 72, weight: .thin)) + .foregroundStyle(.white) + + HStack { + Text("H:\(high)°") + Text("L:\(low)°") + } + .font(.title3) + .foregroundStyle(.white.opacity(0.7)) + } + } + + // Hourly forecast + if !hourlyForecast.isEmpty { + HStack(spacing: 16) { + ForEach(hourlyForecast.prefix(5)) { forecast in + VStack(spacing: 8) { + Text(forecast.hour) + .font(.caption) + Image(systemName: forecast.condition.rawValue) + .font(.title3) + .foregroundStyle(forecast.condition.color) + Text("\(forecast.temp)°") + .font(.caption) + .fontWeight(.medium) + } + .foregroundStyle(.white.opacity(0.8)) + } + } + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } +} + +// MARK: - StandBy Calendar Widget + +/// Calendar widget for StandBy mode +public struct StandByCalendarWidget: View { + let date: Date + let events: [CalendarEvent] + let accentColor: Color + + public struct CalendarEvent: Identifiable { + public let id = UUID() + public let title: String + public let time: Date + public let color: Color + + public init(title: String, time: Date, color: Color = .blue) { + self.title = title + self.time = time + self.color = color + } + } + + public init( + date: Date = Date(), + events: [CalendarEvent] = [], + accentColor: Color = .blue + ) { + self.date = date + self.events = events + self.accentColor = accentColor + } + + private var calendar: Calendar { Calendar.current } + + private var grid: MonthGrid { MonthGrid(month: date, calendar: calendar) } + + private var currentDay: Int { + calendar.component(.day, from: date) + } + + public var body: some View { + VStack(spacing: 16) { + // Month header + Text(date, format: .dateTime.month(.wide).year()) + .font(.title2) + .fontWeight(.medium) + .foregroundStyle(.white) + + // Weekday headers + HStack { + ForEach(Array(grid.weekdaySymbols.enumerated()), id: \.offset) { _, day in + Text(day) + .font(.caption) + .foregroundStyle(.white.opacity(0.5)) + .frame(maxWidth: .infinity) + } + } + + // Calendar grid + let weeks = grid.weeks + VStack(spacing: 8) { + ForEach(weeks.indices, id: \.self) { weekIndex in + HStack { + ForEach(0..<7, id: \.self) { dayIndex in + if let day = weeks[weekIndex][dayIndex] { + Text("\(day)") + .font(.caption) + .fontWeight(day == currentDay ? .bold : .regular) + .foregroundStyle(day == currentDay ? .white : .white.opacity(0.8)) + .frame(maxWidth: .infinity) + .padding(4) + .background( + day == currentDay ? + Circle().fill(accentColor) : + Circle().fill(Color.clear) + ) + } else { + Text("") + .frame(maxWidth: .infinity) + } + } + } + } + } + + // Upcoming events + if !events.isEmpty { + VStack(alignment: .leading, spacing: 8) { + ForEach(events.prefix(2)) { event in + HStack(spacing: 8) { + Circle() + .fill(event.color) + .frame(width: 6, height: 6) + + Text(event.title) + .font(.caption) + .lineLimit(1) + + Spacer() + + Text(event.time, format: .dateTime.hour().minute()) + .font(.caption2) + .foregroundStyle(.white.opacity(0.6)) + } + .foregroundStyle(.white) + } + } + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } +} From cad10ec916f36c6ff9e752b013547970c6886657 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:04:30 +0300 Subject: [PATCH 11/59] feat(templates): restore 20 home screen widget templates Split into themed files. Fixes a string literal that did not compile, zero-goal and zero-duration divisions, a force-unwrapped fallback URL in the shortcuts widget and the water tracker's fixed eight-glass row. Adds tests for the habit streak and water tracker arithmetic. --- .../Templates/InfoTemplates.swift | 353 ++++++++++++++++++ .../Templates/MediaTemplates.swift | 326 ++++++++++++++++ .../Templates/TimeTemplates.swift | 341 +++++++++++++++++ .../Templates/TrackerTemplates.swift | 337 +++++++++++++++++ .../Templates/UtilityTemplates.swift | 146 ++++++++ .../TemplateLogicTests.swift | 39 ++ 6 files changed, 1542 insertions(+) create mode 100644 Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift create mode 100644 Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift create mode 100644 Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift create mode 100644 Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift create mode 100644 Sources/WidgetDevelopmentKit/Templates/UtilityTemplates.swift create mode 100644 Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift diff --git a/Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift new file mode 100644 index 0000000..8ab11f2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift @@ -0,0 +1,353 @@ +import SwiftUI +import WidgetKit + +// MARK: - Gradient Card Widget + +/// Beautiful gradient card widget with customizable colors +public struct GradientCardWidget: View { + let title: String + let subtitle: String + let gradient: LinearGradient + let icon: String + let content: () -> Content + + public init( + title: String, + subtitle: String, + gradient: LinearGradient = LinearGradient( + colors: [.blue, .purple], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + icon: String = "star.fill", + @ViewBuilder content: @escaping () -> Content = { EmptyView() } + ) { + self.title = title + self.subtitle = subtitle + self.gradient = gradient + self.icon = icon + self.content = content + } + + public var body: some View { + ZStack { + gradient + + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: icon) + .font(.title2) + .foregroundStyle(.white.opacity(0.9)) + Spacer() + } + + Spacer() + + Text(title) + .font(.headline) + .foregroundStyle(.white) + + Text(subtitle) + .font(.caption) + .foregroundStyle(.white.opacity(0.8)) + + content() + } + .padding() + } + } +} + +// MARK: - Stats Widget + +/// Statistics widget with progress indicators +public struct StatsWidget: View { + public struct StatItem: Identifiable { + public let id = UUID() + public let title: String + public let value: String + public let progress: Double + public let color: Color + + public init(title: String, value: String, progress: Double, color: Color = .blue) { + self.title = title + self.value = value + self.progress = progress + self.color = color + } + } + + let title: String + let stats: [StatItem] + + public init(title: String, stats: [StatItem]) { + self.title = title + self.stats = stats + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundStyle(.primary) + + ForEach(stats) { stat in + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(stat.title) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text(stat.value) + .font(.caption) + .fontWeight(.semibold) + } + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4) + .fill(stat.color.opacity(0.2)) + + RoundedRectangle(cornerRadius: 4) + .fill(stat.color) + .frame(width: geo.size.width * WidgetMath.clampedFraction(stat.progress)) + } + } + .frame(height: 6) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Weather Widget + +/// Beautiful weather widget with conditions +public struct WeatherWidget: View { + public enum Condition: String, CaseIterable { + case sunny = "sun.max.fill" + case cloudy = "cloud.fill" + case rainy = "cloud.rain.fill" + case stormy = "cloud.bolt.rain.fill" + case snowy = "cloud.snow.fill" + case partlyCloudy = "cloud.sun.fill" + case foggy = "cloud.fog.fill" + case windy = "wind" + + var color: Color { + switch self { + case .sunny: return .yellow + case .cloudy: return .gray + case .rainy: return .blue + case .stormy: return .purple + case .snowy: return .cyan + case .partlyCloudy: return .orange + case .foggy: return .gray + case .windy: return .teal + } + } + } + + let temperature: Int + let condition: Condition + let location: String + let high: Int + let low: Int + + public init( + temperature: Int, + condition: Condition, + location: String, + high: Int, + low: Int + ) { + self.temperature = temperature + self.condition = condition + self.location = location + self.high = high + self.low = low + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(location) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Image(systemName: condition.rawValue) + .font(.title) + .foregroundStyle(condition.color) + } + + Spacer() + + Text("\(temperature)°") + .font(.system(size: 48, weight: .thin)) + + HStack { + Text("H:\(high)°") + Text("L:\(low)°") + } + .font(.caption) + .foregroundStyle(.secondary) + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Finance Widget + +/// Financial overview widget +public struct FinanceWidget: View { + public struct Account: Identifiable { + public let id = UUID() + public let name: String + public let balance: Double + public let change: Double + public let icon: String + + public init(name: String, balance: Double, change: Double, icon: String = "creditcard") { + self.name = name + self.balance = balance + self.change = change + self.icon = icon + } + } + + let totalBalance: Double + let accounts: [Account] + let currency: String + + public init(totalBalance: Double, accounts: [Account], currency: String = "$") { + self.totalBalance = totalBalance + self.accounts = accounts + self.currency = currency + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text("Total Balance") + .font(.caption) + .foregroundStyle(.secondary) + + Text("\(currency)\(totalBalance, specifier: "%.2f")") + .font(.title2) + .fontWeight(.bold) + } + + Divider() + + ForEach(accounts.prefix(2)) { account in + HStack { + Image(systemName: account.icon) + .font(.caption) + .foregroundStyle(.blue) + .frame(width: 24, height: 24) + .background(Color.blue.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + VStack(alignment: .leading) { + Text(account.name) + .font(.caption) + Text("\(currency)\(account.balance, specifier: "%.2f")") + .font(.caption2) + .foregroundStyle(.secondary) + } + + Spacer() + + Text("\(account.change >= 0 ? "+" : "")\(account.change, specifier: "%.1f")%") + .font(.caption) + .foregroundStyle(account.change >= 0 ? .green : .red) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Social Stats Widget + +/// Social media statistics widget +public struct SocialStatsWidget: View { + let platform: String + let platformIcon: String + let platformColor: Color + let followers: Int + let following: Int + let posts: Int + let engagement: Double + + public init( + platform: String, + platformIcon: String, + platformColor: Color, + followers: Int, + following: Int, + posts: Int, + engagement: Double + ) { + self.platform = platform + self.platformIcon = platformIcon + self.platformColor = platformColor + self.followers = followers + self.following = following + self.posts = posts + self.engagement = engagement + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: platformIcon) + .foregroundStyle(platformColor) + Text(platform) + .font(.headline) + } + + HStack(spacing: 16) { + StatBox(value: formatNumber(followers), label: "Followers") + StatBox(value: formatNumber(following), label: "Following") + StatBox(value: formatNumber(posts), label: "Posts") + } + + HStack { + Text("Engagement") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text("\(engagement, specifier: "%.1f")%") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.green) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + private func formatNumber(_ num: Int) -> String { + WidgetFormat.compactNumber(num) + } + + struct StatBox: View { + let value: String + let label: String + + var body: some View { + VStack(spacing: 2) { + Text(value) + .font(.headline) + Text(label) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift new file mode 100644 index 0000000..dcebd9a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift @@ -0,0 +1,326 @@ +import SwiftUI +import WidgetKit + +// MARK: - Quote Widget + +/// Inspirational quote widget +public struct QuoteWidget: View { + let quote: String + let author: String + let category: String? + + public init(quote: String, author: String, category: String? = nil) { + self.quote = quote + self.author = author + self.category = category + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + if let category = category { + Text(category.uppercased()) + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.secondary) + } + + Text("\u{201C}\(quote)\u{201D}") + .font(.system(.body, design: .serif)) + .italic() + .lineLimit(4) + + Spacer() + + HStack { + Text("— \(author)") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Image(systemName: "quote.closing") + .foregroundStyle(.tertiary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Music Widget + +/// Music player widget with album art placeholder +public struct MusicWidget: View { + let trackName: String + let artistName: String + let albumName: String + let isPlaying: Bool + let progress: Double + + public init( + trackName: String, + artistName: String, + albumName: String, + isPlaying: Bool = true, + progress: Double = 0.5 + ) { + self.trackName = trackName + self.artistName = artistName + self.albumName = albumName + self.isPlaying = isPlaying + self.progress = progress + } + + public var body: some View { + HStack(spacing: 12) { + // Album art placeholder + RoundedRectangle(cornerRadius: 8) + .fill(LinearGradient( + colors: [.purple, .blue], + startPoint: .topLeading, + endPoint: .bottomTrailing + )) + .frame(width: 60, height: 60) + .overlay { + Image(systemName: "music.note") + .font(.title2) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trackName) + .font(.headline) + .lineLimit(1) + + Text(artistName) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(Color.purple) + .frame(width: geo.size.width * WidgetMath.clampedFraction(progress)) + } + } + .frame(height: 4) + + HStack { + Image(systemName: isPlaying ? "pause.fill" : "play.fill") + Spacer() + Image(systemName: "forward.fill") + } + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Photo Widget + +/// Photo frame widget with customizable styling +public struct PhotoFrameWidget: View { + let imageName: String? + let caption: String? + let date: Date? + let style: FrameStyle + + public enum FrameStyle { + case minimal + case polaroid + case modern + case vintage + } + + public init( + imageName: String? = nil, + caption: String? = nil, + date: Date? = nil, + style: FrameStyle = .modern + ) { + self.imageName = imageName + self.caption = caption + self.date = date + self.style = style + } + + public var body: some View { + VStack(spacing: 0) { + // Image placeholder + Rectangle() + .fill( + LinearGradient( + colors: [.gray.opacity(0.3), .gray.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .overlay { + Image(systemName: "photo") + .font(.largeTitle) + .foregroundStyle(.secondary) + } + + if style == .polaroid, let caption = caption { + VStack(spacing: 4) { + Text(caption) + .font(.caption) + .lineLimit(1) + + if let date = date { + Text(date, style: .date) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + .background(.white) + } + } + .clipShape(RoundedRectangle(cornerRadius: style == .minimal ? 0 : 12)) + .shadow(radius: style == .modern ? 8 : 0) + .containerBackground(.clear, for: .widget) + } +} + +// MARK: - Podcast Widget + +/// Podcast player widget +public struct PodcastWidget: View { + let podcastName: String + let episodeTitle: String + let currentTime: TimeInterval + let duration: TimeInterval + let isPlaying: Bool + + public init( + podcastName: String, + episodeTitle: String, + currentTime: TimeInterval, + duration: TimeInterval, + isPlaying: Bool = false + ) { + self.podcastName = podcastName + self.episodeTitle = episodeTitle + self.currentTime = currentTime + self.duration = duration + self.isPlaying = isPlaying + } + + public var body: some View { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 8) + .fill(LinearGradient(colors: [.orange, .pink], startPoint: .top, endPoint: .bottom)) + .frame(width: 50, height: 50) + .overlay { + Image(systemName: "mic.fill") + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(podcastName) + .font(.caption2) + .foregroundStyle(.secondary) + + Text(episodeTitle) + .font(.caption) + .fontWeight(.medium) + .lineLimit(2) + + HStack { + Text(formatTime(currentTime)) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(Color.orange) + .frame(width: geo.size.width * WidgetMath.fraction(currentTime, of: duration)) + } + } + .frame(height: 3) + + Text(formatTime(duration)) + } + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + private func formatTime(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } +} + +// MARK: - News Headlines Widget + +/// News headlines widget +public struct NewsWidget: View { + public struct Article: Identifiable { + public let id = UUID() + public let headline: String + public let source: String + public let timeAgo: String + public let category: String + + public init(headline: String, source: String, timeAgo: String, category: String) { + self.headline = headline + self.source = source + self.timeAgo = timeAgo + self.category = category + } + } + + let articles: [Article] + let category: String + + public init(articles: [Article], category: String = "Top Stories") { + self.articles = articles + self.category = category + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(category) + .font(.headline) + Spacer() + Image(systemName: "newspaper.fill") + .foregroundStyle(.red) + } + + ForEach(articles.prefix(3)) { article in + VStack(alignment: .leading, spacing: 4) { + Text(article.headline) + .font(.caption) + .fontWeight(.medium) + .lineLimit(2) + + HStack { + Text(article.source) + Text("•") + Text(article.timeAgo) + } + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + + if article.id != articles.prefix(3).last?.id { + Divider() + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift new file mode 100644 index 0000000..3ee2f52 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift @@ -0,0 +1,341 @@ +import SwiftUI +import WidgetKit + +// MARK: - Calendar Widget + +/// Calendar widget showing upcoming events +public struct CalendarWidget: View { + public struct Event: Identifiable { + public let id = UUID() + public let title: String + public let time: Date + public let color: Color + + public init(title: String, time: Date, color: Color = .blue) { + self.title = title + self.time = time + self.color = color + } + } + + let date: Date + let events: [Event] + + public init(date: Date = Date(), events: [Event]) { + self.date = date + self.events = events + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + VStack(alignment: .leading) { + Text(date.formatted(.dateTime.weekday(.wide))) + .font(.caption) + .foregroundStyle(.secondary) + Text(date.formatted(.dateTime.day().month())) + .font(.title2) + .fontWeight(.bold) + } + Spacer() + Image(systemName: "calendar") + .font(.title2) + .foregroundStyle(.blue) + } + + Divider() + + ForEach(events.prefix(3)) { event in + HStack(spacing: 8) { + Circle() + .fill(event.color) + .frame(width: 8, height: 8) + + VStack(alignment: .leading, spacing: 2) { + Text(event.title) + .font(.caption) + .lineLimit(1) + Text(event.time, style: .time) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + + if events.count > 3 { + Text("+\(events.count - 3) more events") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Countdown Widget + +/// Countdown timer widget for events +public struct CountdownWidget: View { + let title: String + let targetDate: Date + let referenceDate: Date + let icon: String + let accentColor: Color + + /// - Parameter referenceDate: The timeline entry date; pass `entry.date` from your provider. + public init( + title: String, + targetDate: Date, + referenceDate: Date = Date(), + icon: String = "clock.fill", + accentColor: Color = .orange + ) { + self.title = title + self.targetDate = targetDate + self.referenceDate = referenceDate + self.icon = icon + self.accentColor = accentColor + } + + private var timeRemaining: CountdownComponents { + CountdownComponents(from: referenceDate, to: targetDate) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: icon) + .foregroundStyle(accentColor) + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 16) { + TimeUnitView(value: timeRemaining.days, unit: "DAYS", color: accentColor) + TimeUnitView(value: timeRemaining.hours, unit: "HRS", color: accentColor) + TimeUnitView(value: timeRemaining.minutes, unit: "MIN", color: accentColor) + } + + Text(targetDate, style: .date) + .font(.caption2) + .foregroundStyle(.secondary) + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + struct TimeUnitView: View { + let value: Int + let unit: String + let color: Color + + var body: some View { + VStack(spacing: 2) { + Text("\(value)") + .font(.title2) + .fontWeight(.bold) + .foregroundStyle(color) + Text(unit) + .font(.system(size: 8, weight: .medium)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Clock Widget + +/// Analog/Digital clock widget +public struct ClockWidget: View { + let date: Date + let style: ClockStyle + let showSeconds: Bool + + public enum ClockStyle { + case analog + case digital + case minimal + } + + public init(date: Date = Date(), style: ClockStyle = .digital, showSeconds: Bool = false) { + self.date = date + self.style = style + self.showSeconds = showSeconds + } + + public var body: some View { + VStack { + switch style { + case .analog: + AnalogClockView(date: date) + case .digital: + DigitalClockView(date: date, showSeconds: showSeconds) + case .minimal: + MinimalClockView(date: date) + } + } + .containerBackground(.fill.tertiary, for: .widget) + } + + struct AnalogClockView: View { + let date: Date + + var body: some View { + GeometryReader { geo in + let size = min(geo.size.width, geo.size.height) - 20 + + ZStack { + Circle() + .stroke(Color.secondary.opacity(0.3), lineWidth: 2) + + // Hour markers + ForEach(0..<12) { hour in + Rectangle() + .fill(Color.primary) + .frame(width: 2, height: hour % 3 == 0 ? 10 : 5) + .offset(y: -size/2 + 15) + .rotationEffect(.degrees(Double(hour) * 30)) + } + + // Hour hand + ClockHand(length: size * 0.25, width: 4) + .rotationEffect(.degrees(hourAngle)) + + // Minute hand + ClockHand(length: size * 0.35, width: 3) + .rotationEffect(.degrees(minuteAngle)) + + Circle() + .fill(Color.primary) + .frame(width: 8, height: 8) + } + .frame(width: size, height: size) + .position(x: geo.size.width / 2, y: geo.size.height / 2) + } + .padding() + } + + var hourAngle: Double { + let hour = Calendar.current.component(.hour, from: date) % 12 + let minute = Calendar.current.component(.minute, from: date) + return Double(hour) * 30 + Double(minute) * 0.5 + } + + var minuteAngle: Double { + let minute = Calendar.current.component(.minute, from: date) + return Double(minute) * 6 + } + } + + struct ClockHand: View { + let length: CGFloat + let width: CGFloat + + var body: some View { + RoundedRectangle(cornerRadius: width / 2) + .fill(Color.primary) + .frame(width: width, height: length) + .offset(y: -length / 2) + } + } + + struct DigitalClockView: View { + let date: Date + let showSeconds: Bool + + var body: some View { + VStack(spacing: 4) { + Text(date, format: showSeconds ? .dateTime.hour().minute().second() : .dateTime.hour().minute()) + .font(.system(size: 36, weight: .thin, design: .rounded)) + .monospacedDigit() + + Text(date, format: .dateTime.weekday(.wide).month().day()) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding() + } + } + + struct MinimalClockView: View { + let date: Date + + var body: some View { + Text(date, format: .dateTime.hour().minute()) + .font(.system(size: 48, weight: .ultraLight, design: .rounded)) + .monospacedDigit() + .padding() + } + } +} + +// MARK: - Notes Widget + +/// Quick notes widget with rich formatting +public struct NotesWidget: View { + public struct Note: Identifiable { + public let id = UUID() + public let title: String + public let preview: String + public let date: Date + public let color: Color + + public init(title: String, preview: String, date: Date = Date(), color: Color = .yellow) { + self.title = title + self.preview = preview + self.date = date + self.color = color + } + } + + let notes: [Note] + + public init(notes: [Note]) { + self.notes = notes + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "note.text") + .foregroundStyle(.yellow) + Text("Notes") + .font(.headline) + Spacer() + Text("\(notes.count)") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(notes.prefix(3)) { note in + HStack(spacing: 8) { + RoundedRectangle(cornerRadius: 2) + .fill(note.color) + .frame(width: 3) + + VStack(alignment: .leading, spacing: 2) { + Text(note.title) + .font(.caption) + .fontWeight(.medium) + .lineLimit(1) + + Text(note.preview) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + } + + Spacer() + + Text(note.date, style: .relative) + .font(.system(size: 9)) + .foregroundStyle(.tertiary) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift new file mode 100644 index 0000000..b03e580 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift @@ -0,0 +1,337 @@ +import SwiftUI +import WidgetKit + +// MARK: - Fitness Widget + +/// Fitness tracking widget with activity rings +public struct FitnessWidget: View { + let calories: Double + let caloriesGoal: Double + let exercise: Double + let exerciseGoal: Double + let standing: Double + let standingGoal: Double + + public init( + calories: Double, + caloriesGoal: Double = 500, + exercise: Double, + exerciseGoal: Double = 30, + standing: Double, + standingGoal: Double = 12 + ) { + self.calories = calories + self.caloriesGoal = caloriesGoal + self.exercise = exercise + self.exerciseGoal = exerciseGoal + self.standing = standing + self.standingGoal = standingGoal + } + + public var body: some View { + HStack { + ZStack { + ActivityRing(progress: WidgetMath.fraction(calories, of: caloriesGoal), color: .red, lineWidth: 12) + .frame(width: 80, height: 80) + + ActivityRing(progress: WidgetMath.fraction(exercise, of: exerciseGoal), color: .green, lineWidth: 10) + .frame(width: 60, height: 60) + + ActivityRing(progress: WidgetMath.fraction(standing, of: standingGoal), color: .cyan, lineWidth: 8) + .frame(width: 44, height: 44) + } + + VStack(alignment: .leading, spacing: 8) { + ActivityStat(label: "Move", value: "\(Int(calories))/\(Int(caloriesGoal))", unit: "CAL", color: .red) + ActivityStat(label: "Exercise", value: "\(Int(exercise))/\(Int(exerciseGoal))", unit: "MIN", color: .green) + ActivityStat(label: "Stand", value: "\(Int(standing))/\(Int(standingGoal))", unit: "HRS", color: .cyan) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + struct ActivityRing: View { + let progress: Double + let color: Color + let lineWidth: CGFloat + + var body: some View { + Circle() + .stroke(color.opacity(0.3), lineWidth: lineWidth) + .overlay { + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(progress)) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .rotationEffect(.degrees(-90)) + } + } + } + + struct ActivityStat: View { + let label: String + let value: String + let unit: String + let color: Color + + var body: some View { + HStack(spacing: 4) { + Circle() + .fill(color) + .frame(width: 6, height: 6) + + Text(label) + .font(.system(size: 10)) + .foregroundStyle(.secondary) + + Spacer() + + Text(value) + .font(.system(size: 10, weight: .semibold)) + + Text(unit) + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Habit Tracker Widget + +/// Habit tracking widget with streak display +public struct HabitTrackerWidget: View { + public struct Habit: Identifiable { + public let id = UUID() + public let name: String + public let icon: String + public let color: Color + public let completedDays: [Bool] // Last 7 days + + public init(name: String, icon: String, color: Color, completedDays: [Bool]) { + self.name = name + self.icon = icon + self.color = color + self.completedDays = completedDays + } + + public var streak: Int { + var count = 0 + for completed in completedDays.reversed() { + if completed { count += 1 } else { break } + } + return count + } + } + + let habits: [Habit] + + public init(habits: [Habit]) { + self.habits = habits + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Habits") + .font(.headline) + Spacer() + Text("7 Day Streak") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(habits.prefix(3)) { habit in + HStack(spacing: 8) { + Image(systemName: habit.icon) + .font(.caption) + .foregroundStyle(habit.color) + .frame(width: 20) + + Text(habit.name) + .font(.caption) + .lineLimit(1) + + Spacer() + + HStack(spacing: 2) { + ForEach(0..<7, id: \.self) { day in + Circle() + .fill(habit.completedDays.indices.contains(day) && habit.completedDays[day] + ? habit.color + : Color.secondary.opacity(0.3)) + .frame(width: 8, height: 8) + } + } + + Text("🔥\(habit.streak)") + .font(.system(size: 10)) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - To-Do Widget + +/// Task list widget +public struct TodoWidget: View { + public struct Task: Identifiable { + public let id = UUID() + public let title: String + public let isCompleted: Bool + public let priority: Priority + public let dueDate: Date? + + public enum Priority { + case low, medium, high + + var color: Color { + switch self { + case .low: return .blue + case .medium: return .orange + case .high: return .red + } + } + } + + public init(title: String, isCompleted: Bool = false, priority: Priority = .medium, dueDate: Date? = nil) { + self.title = title + self.isCompleted = isCompleted + self.priority = priority + self.dueDate = dueDate + } + } + + let tasks: [Task] + let listName: String + + public init(tasks: [Task], listName: String = "Tasks") { + self.tasks = tasks + self.listName = listName + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(listName) + .font(.headline) + Spacer() + let completed = tasks.filter { $0.isCompleted }.count + Text("\(completed)/\(tasks.count)") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(tasks.prefix(4)) { task in + HStack(spacing: 8) { + Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundStyle(task.isCompleted ? .green : task.priority.color) + .font(.body) + + Text(task.title) + .font(.caption) + .strikethrough(task.isCompleted) + .foregroundStyle(task.isCompleted ? .secondary : .primary) + .lineLimit(1) + + Spacer() + + if let dueDate = task.dueDate { + Text(dueDate, style: .relative) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } + + if tasks.count > 4 { + Text("+\(tasks.count - 4) more tasks") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Water Tracker Widget + +/// Water intake tracking widget +public struct WaterTrackerWidget: View { + let currentIntake: Double // in ml + let dailyGoal: Double + let glassSize: Double + + public init(currentIntake: Double, dailyGoal: Double = 2000, glassSize: Double = 250) { + self.currentIntake = currentIntake + self.dailyGoal = dailyGoal + self.glassSize = glassSize + } + + private static let maximumGlassIcons = 12 + + var progress: Double { + WidgetMath.fraction(currentIntake, of: dailyGoal) + } + + /// Number of full glasses drunk so far. + var glasses: Int { + guard glassSize > 0, currentIntake.isFinite, currentIntake > 0 else { return 0 } + return Int(currentIntake / glassSize) + } + + /// Number of glasses needed to reach the daily goal, capped so the row fits. + var glassGoal: Int { + guard glassSize > 0, dailyGoal.isFinite, dailyGoal > 0 else { return 0 } + return min(Int((dailyGoal / glassSize).rounded(.up)), Self.maximumGlassIcons) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: "drop.fill") + .foregroundStyle(.cyan) + Text("Water") + .font(.headline) + Spacer() + } + + ZStack { + Circle() + .stroke(Color.cyan.opacity(0.2), lineWidth: 8) + + Circle() + .trim(from: 0, to: progress) + .stroke( + LinearGradient(colors: [.cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing), + style: StrokeStyle(lineWidth: 8, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + + VStack(spacing: 2) { + Text("\(Int(currentIntake))") + .font(.title2) + .fontWeight(.bold) + Text("/ \(Int(dailyGoal)) ml") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .frame(width: 80, height: 80) + + HStack { + ForEach(0..: View { + let url: URL? + @ViewBuilder let content: () -> Content + + var body: some View { + if let url { + Link(destination: url, label: content) + } else { + content() + } + } +} diff --git a/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift b/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift new file mode 100644 index 0000000..01f1526 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift @@ -0,0 +1,39 @@ +import SwiftUI +import Testing +@testable import WidgetDevelopmentKit + +@Suite("Template logic") +struct TemplateLogicTests { + @Test(arguments: [ + ([true, true, false, true, true, true], 3), + ([true, true, true], 3), + ([true, false], 0), + ([Bool](), 0) + ]) + func habitStreakCountsTrailingCompletedDays(days: [Bool], expected: Int) { + let habit = HabitTrackerWidget.Habit(name: "Read", icon: "book", color: .blue, completedDays: days) + #expect(habit.streak == expected) + } + + @Test func waterTrackerCountsGlassesAndGoal() { + let widget = WaterTrackerWidget(currentIntake: 1_100, dailyGoal: 2_000, glassSize: 250) + #expect(widget.glasses == 4) + #expect(widget.glassGoal == 8) + #expect(widget.progress == 0.55) + } + + @Test func waterTrackerHandlesInvalidSizes() { + let zeroGlass = WaterTrackerWidget(currentIntake: 500, dailyGoal: 2_000, glassSize: 0) + #expect(zeroGlass.glasses == 0) + #expect(zeroGlass.glassGoal == 0) + + let zeroGoal = WaterTrackerWidget(currentIntake: 500, dailyGoal: 0, glassSize: 250) + #expect(zeroGoal.progress == 0) + #expect(zeroGoal.glassGoal == 0) + } + + @Test func waterTrackerCapsGlassIcons() { + let widget = WaterTrackerWidget(currentIntake: 0, dailyGoal: 10_000, glassSize: 100) + #expect(widget.glassGoal == 12) + } +} From e99c100300477c19c11e9803ddd91db0eebfc07e Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:05:42 +0300 Subject: [PATCH 12/59] feat(timeline): add update policy, timeline builder and snapshot store Reworked from the TimelineManager removed in e3bcc65. Kept the ideas that hold up and fixed their bugs: - WidgetUpdatePolicy uses calendar arithmetic for next-hour and next-day reloads instead of adding 3600/86400 seconds, and maps to a WidgetKit TimelineReloadPolicy. - TimelineBuilder is a value type and sorts entries by date. - TimelineSchedule generates evenly spaced or boundary-aligned dates. - WidgetSnapshotStore replaces TimelineCacheManager: Codable values with an expiry in injectable UserDefaults (no hard-coded App Group). Dropped SmartTimelineProvider (its placeholder force-cast nil to the data type), the Timer-based reload manager, which cannot run in a widget extension, and the battery and metrics helpers. --- .../Timeline/TimelineBuilder.swift | 101 ++++++++++++++ .../Timeline/WidgetSnapshotStore.swift | 70 ++++++++++ .../Timeline/WidgetUpdatePolicy.swift | 53 +++++++ .../TimelineTests.swift | 132 ++++++++++++++++++ .../WidgetSnapshotStoreTests.swift | 71 ++++++++++ 5 files changed, 427 insertions(+) create mode 100644 Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift create mode 100644 Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift create mode 100644 Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift create mode 100644 Tests/WidgetDevelopmentKitTests/TimelineTests.swift create mode 100644 Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift diff --git a/Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift b/Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift new file mode 100644 index 0000000..3185ce2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift @@ -0,0 +1,101 @@ +import Foundation +import WidgetKit + +/// Assembles a WidgetKit `Timeline` from entries and a reload policy. +/// +/// Entries are sorted by date when the timeline is built, since WidgetKit expects +/// them in chronological order. +/// +/// ```swift +/// let timeline = TimelineBuilder() +/// .adding(entries) +/// .reloading(.every(minutes: 30), from: now) +/// .build() +/// ``` +public struct TimelineBuilder { + public private(set) var entries: [Entry] + public private(set) var policy: TimelineReloadPolicy + + public init(entries: [Entry] = [], policy: TimelineReloadPolicy = .atEnd) { + self.entries = entries + self.policy = policy + } + + /// Returns a copy with `entry` appended. + public func adding(_ entry: Entry) -> Self { + var copy = self + copy.entries.append(entry) + return copy + } + + /// Returns a copy with `newEntries` appended. + public func adding(_ newEntries: [Entry]) -> Self { + var copy = self + copy.entries.append(contentsOf: newEntries) + return copy + } + + /// Returns a copy using a WidgetKit reload policy. + public func reloading(_ policy: TimelineReloadPolicy) -> Self { + var copy = self + copy.policy = policy + return copy + } + + /// Returns a copy whose reload policy is derived from a `WidgetUpdatePolicy`. + public func reloading( + _ policy: WidgetUpdatePolicy, + from date: Date, + calendar: Calendar = .current + ) -> Self { + reloading(policy.reloadPolicy(from: date, calendar: calendar)) + } + + /// The timeline with entries in chronological order. + public func build() -> Timeline { + Timeline(entries: entries.sorted { $0.date < $1.date }, policy: policy) + } +} + +/// Generates evenly spaced dates for timeline entries. +public enum TimelineSchedule { + /// `count` dates starting at `start`, `interval` seconds apart. + /// Returns an empty array when `count` is not positive or `interval` is not positive. + public static func dates(from start: Date, every interval: TimeInterval, count: Int) -> [Date] { + guard count > 0, interval > 0 else { return [] } + return (0.. [Date] { + guard minutes > 0, start < end else { return [] } + var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: start) + let minute = components.minute ?? 0 + components.minute = minute - minute % minutes + guard var current = calendar.date(from: components) else { return [] } + + var dates: [Date] = [] + while current < end { + if current >= start || dates.isEmpty { + dates.append(max(current, start)) + } + guard let next = calendar.date(byAdding: .minute, value: minutes, to: current) else { break } + current = next + } + return dates + } +} + +extension Timeline { + /// A timeline with one entry. + public static func single(_ entry: EntryType, policy: TimelineReloadPolicy = .atEnd) -> Timeline { + Timeline(entries: [entry], policy: policy) + } +} diff --git a/Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift b/Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift new file mode 100644 index 0000000..ddda948 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift @@ -0,0 +1,70 @@ +import Foundation + +/// Stores `Codable` values with an expiry date in `UserDefaults`. +/// +/// The usual setup is an App Group suite shared by the app and its widget +/// extension: the app writes fresh data, and the timeline provider reads it +/// without a network call. +/// +/// ```swift +/// let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.app")!) +/// try store.save(weather, forKey: "weather", lifetime: 15 * 60) +/// let cached: Weather? = store.value(Weather.self, forKey: "weather") +/// ``` +public struct WidgetSnapshotStore: Sendable { + private struct Envelope: Codable { + let value: Value + let expiresAt: Date + } + + // UserDefaults is documented as thread-safe but is not marked Sendable in the SDK. + nonisolated(unsafe) private let defaults: UserDefaults + private let keyPrefix: String + private let now: @Sendable () -> Date + + /// - Parameters: + /// - defaults: Usually `UserDefaults(suiteName:)` for your App Group. + /// - keyPrefix: Namespace for keys written by this store. + /// - now: Clock used for expiry checks; injectable for tests. + public init( + defaults: UserDefaults, + keyPrefix: String = "widget.snapshot.", + now: @escaping @Sendable () -> Date = { Date() } + ) { + self.defaults = defaults + self.keyPrefix = keyPrefix + self.now = now + } + + /// Encodes and stores `value` until `lifetime` seconds from now. + public func save(_ value: Value, forKey key: String, lifetime: TimeInterval) throws { + let envelope = Envelope(value: value, expiresAt: now().addingTimeInterval(lifetime)) + let data = try JSONEncoder().encode(envelope) + defaults.set(data, forKey: keyPrefix + key) + } + + /// The stored value if present, decodable as `Value`, and not expired. + /// Expired entries are removed. + public func value(_ type: Value.Type, forKey key: String) -> Value? { + guard let envelope = envelope(type, forKey: key) else { return nil } + guard envelope.expiresAt > now() else { + removeValue(forKey: key) + return nil + } + return envelope.value + } + + /// The stored value even if it has expired, for showing stale data while a refresh runs. + public func staleValue(_ type: Value.Type, forKey key: String) -> Value? { + envelope(type, forKey: key)?.value + } + + public func removeValue(forKey key: String) { + defaults.removeObject(forKey: keyPrefix + key) + } + + private func envelope(_ type: Value.Type, forKey key: String) -> Envelope? { + guard let data = defaults.data(forKey: keyPrefix + key) else { return nil } + return try? JSONDecoder().decode(Envelope.self, from: data) + } +} diff --git a/Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift b/Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift new file mode 100644 index 0000000..793ca18 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift @@ -0,0 +1,53 @@ +import Foundation +import WidgetKit + +/// Describes when a widget's timeline should be reloaded, and converts that into +/// a WidgetKit `TimelineReloadPolicy`. +/// +/// Calendar-based cases (`.nextHour`, `.startOfNextDay`) use calendar arithmetic, so +/// they stay correct across daylight-saving changes where adding 3600 or 86400 +/// seconds would not. +public enum WidgetUpdatePolicy: Sendable { + /// Reload at a fixed date. + case atDate(Date) + /// Reload after a time interval in seconds. + case afterInterval(TimeInterval) + /// Reload at the start of the next clock hour. + case nextHour + /// Reload at midnight of the following day. + case startOfNextDay + /// Reload every `minutes` minutes. + case every(minutes: Int) + /// Never reload automatically; the app calls `WidgetCenter` itself. + case never + /// Compute the date yourself. + case custom(@Sendable (Date) -> Date?) + + /// The next reload date after `date`, or `nil` for `.never`. + public func nextUpdateDate(from date: Date, calendar: Calendar = .current) -> Date? { + switch self { + case .atDate(let target): + return target + case .afterInterval(let interval): + return date.addingTimeInterval(max(interval, 0)) + case .nextHour: + guard let hour = calendar.dateInterval(of: .hour, for: date) else { return nil } + return hour.end + case .startOfNextDay: + let startOfDay = calendar.startOfDay(for: date) + return calendar.date(byAdding: .day, value: 1, to: startOfDay) + case .every(let minutes): + return calendar.date(byAdding: .minute, value: max(minutes, 1), to: date) + case .never: + return nil + case .custom(let compute): + return compute(date) + } + } + + /// The WidgetKit reload policy for a timeline generated at `date`. + public func reloadPolicy(from date: Date, calendar: Calendar = .current) -> TimelineReloadPolicy { + guard let next = nextUpdateDate(from: date, calendar: calendar) else { return .never } + return .after(next) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/TimelineTests.swift b/Tests/WidgetDevelopmentKitTests/TimelineTests.swift new file mode 100644 index 0000000..dd13c18 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TimelineTests.swift @@ -0,0 +1,132 @@ +import Foundation +import Testing +import WidgetKit +@testable import WidgetDevelopmentKit + +private struct SampleEntry: TimelineEntry, Equatable { + let date: Date + let value: Int +} + +@Suite("WidgetUpdatePolicy") +struct WidgetUpdatePolicyTests { + let calendar = TestCalendar.gregorian() + let now = TestCalendar.date(2026, 10, 14, 9, 41, 30) + + @Test func nextHourIsStartOfFollowingHour() { + #expect(WidgetUpdatePolicy.nextHour.nextUpdateDate(from: now, calendar: calendar) + == TestCalendar.date(2026, 10, 14, 10)) + } + + @Test func nextHourRollsOverMidnight() { + let lateNight = TestCalendar.date(2026, 12, 31, 23, 15) + #expect(WidgetUpdatePolicy.nextHour.nextUpdateDate(from: lateNight, calendar: calendar) + == TestCalendar.date(2027, 1, 1)) + } + + @Test func startOfNextDayUsesCalendarDays() { + let newYork = TestCalendar.gregorian(timeZone: "America/New_York") + // 1 November 2026 is the US fall-back day and has 25 hours. + let evening = TestCalendar.date(2026, 10, 31, 22, calendar: newYork) + let expected = TestCalendar.date(2026, 11, 1, calendar: newYork) + #expect(WidgetUpdatePolicy.startOfNextDay.nextUpdateDate(from: evening, calendar: newYork) == expected) + + let fallBackDay = TestCalendar.date(2026, 11, 1, 12, calendar: newYork) + let nextMidnight = TestCalendar.date(2026, 11, 2, calendar: newYork) + #expect(WidgetUpdatePolicy.startOfNextDay.nextUpdateDate(from: fallBackDay, calendar: newYork) == nextMidnight) + } + + @Test func intervalAndMinutePolicies() { + #expect(WidgetUpdatePolicy.afterInterval(90).nextUpdateDate(from: now) == now.addingTimeInterval(90)) + #expect(WidgetUpdatePolicy.afterInterval(-5).nextUpdateDate(from: now) == now) + #expect(WidgetUpdatePolicy.every(minutes: 15).nextUpdateDate(from: now, calendar: calendar) + == now.addingTimeInterval(15 * 60)) + // Zero or negative minutes would reload in a tight loop; one minute is the floor. + #expect(WidgetUpdatePolicy.every(minutes: 0).nextUpdateDate(from: now, calendar: calendar) + == now.addingTimeInterval(60)) + } + + @Test func fixedCustomAndNever() { + let target = TestCalendar.date(2027, 1, 1) + #expect(WidgetUpdatePolicy.atDate(target).nextUpdateDate(from: now) == target) + #expect(WidgetUpdatePolicy.custom { $0.addingTimeInterval(5) }.nextUpdateDate(from: now) == now.addingTimeInterval(5)) + #expect(WidgetUpdatePolicy.never.nextUpdateDate(from: now) == nil) + } + + @Test func convertsToReloadPolicy() { + #expect(WidgetUpdatePolicy.never.reloadPolicy(from: now) == .never) + #expect(WidgetUpdatePolicy.afterInterval(60).reloadPolicy(from: now) == .after(now.addingTimeInterval(60))) + } +} + +@Suite("TimelineBuilder") +struct TimelineBuilderTests { + let start = TestCalendar.date(2026, 10, 14, 9) + + @Test func sortsEntriesChronologically() { + let entries = [2, 0, 1].map { SampleEntry(date: start.addingTimeInterval(Double($0) * 60), value: $0) } + let timeline = TimelineBuilder().adding(entries).build() + #expect(timeline.entries.map(\.value) == [0, 1, 2]) + #expect(timeline.policy == .atEnd) + } + + @Test func isAValueType() { + let base = TimelineBuilder().adding(SampleEntry(date: start, value: 1)) + let extended = base.adding(SampleEntry(date: start, value: 2)) + #expect(base.entries.count == 1) + #expect(extended.entries.count == 2) + } + + @Test func appliesUpdatePolicy() { + let calendar = TestCalendar.gregorian() + let timeline = TimelineBuilder() + .adding(SampleEntry(date: start, value: 0)) + .reloading(.nextHour, from: start.addingTimeInterval(120), calendar: calendar) + .build() + #expect(timeline.policy == .after(TestCalendar.date(2026, 10, 14, 10))) + } + + @Test func singleEntryTimeline() { + let timeline = Timeline.single(SampleEntry(date: start, value: 7), policy: .never) + #expect(timeline.entries == [SampleEntry(date: start, value: 7)]) + #expect(timeline.policy == .never) + } +} + +@Suite("TimelineSchedule") +struct TimelineScheduleTests { + let calendar = TestCalendar.gregorian() + + @Test func evenlySpacedDates() { + let start = TestCalendar.date(2026, 10, 14, 9) + let dates = TimelineSchedule.dates(from: start, every: 900, count: 3) + #expect(dates == [start, start.addingTimeInterval(900), start.addingTimeInterval(1_800)]) + #expect(TimelineSchedule.dates(from: start, every: 900, count: 0).isEmpty) + #expect(TimelineSchedule.dates(from: start, every: 0, count: 3).isEmpty) + } + + @Test func alignedDatesStartNowThenSnapToBoundaries() { + let start = TestCalendar.date(2026, 10, 14, 9, 7, 30) + let end = TestCalendar.date(2026, 10, 14, 10) + let dates = TimelineSchedule.alignedDates(from: start, until: end, everyMinutes: 15, calendar: calendar) + #expect(dates == [ + start, + TestCalendar.date(2026, 10, 14, 9, 15), + TestCalendar.date(2026, 10, 14, 9, 30), + TestCalendar.date(2026, 10, 14, 9, 45) + ]) + } + + @Test func alignedDatesWhenStartIsOnBoundary() { + let start = TestCalendar.date(2026, 10, 14, 9, 30) + let end = TestCalendar.date(2026, 10, 14, 10, 1) + let dates = TimelineSchedule.alignedDates(from: start, until: end, everyMinutes: 30, calendar: calendar) + #expect(dates == [start, TestCalendar.date(2026, 10, 14, 10)]) + } + + @Test func alignedDatesRejectInvalidInput() { + let start = TestCalendar.date(2026, 10, 14, 9) + #expect(TimelineSchedule.alignedDates(from: start, until: start, everyMinutes: 15, calendar: calendar).isEmpty) + #expect(TimelineSchedule.alignedDates(from: start, until: start.addingTimeInterval(3_600), everyMinutes: 0, calendar: calendar).isEmpty) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift b/Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift new file mode 100644 index 0000000..7717553 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift @@ -0,0 +1,71 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +private struct Weather: Codable, Equatable { + let city: String + let temperature: Int +} + +/// A clock the test can move forward. +private final class TestClock: @unchecked Sendable { + private let lock = NSLock() + private var current: Date + init(_ date: Date) { current = date } + func now() -> Date { lock.withLock { current } } + func advance(by seconds: TimeInterval) { lock.withLock { current += seconds } } +} + +@Suite("WidgetSnapshotStore") +struct WidgetSnapshotStoreTests { + private let suiteName = "WidgetSnapshotStoreTests.\(UUID().uuidString)" + private let clock = TestClock(Date(timeIntervalSince1970: 1_800_000_000)) + + private func makeStore() -> (WidgetSnapshotStore, UserDefaults) { + let defaults = UserDefaults(suiteName: suiteName)! + let clock = clock + return (WidgetSnapshotStore(defaults: defaults, now: { clock.now() }), defaults) + } + + @Test func roundTripsBeforeExpiry() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save(Weather(city: "Istanbul", temperature: 21), forKey: "weather", lifetime: 600) + clock.advance(by: 599) + #expect(store.value(Weather.self, forKey: "weather") == Weather(city: "Istanbul", temperature: 21)) + } + + @Test func expiredValueIsRemovedButStaleReadWorksBeforeThat() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save(Weather(city: "Ankara", temperature: 12), forKey: "weather", lifetime: 60) + clock.advance(by: 61) + #expect(store.staleValue(Weather.self, forKey: "weather")?.city == "Ankara") + #expect(store.value(Weather.self, forKey: "weather") == nil) + #expect(store.staleValue(Weather.self, forKey: "weather") == nil) + } + + @Test func wrongTypeAndMissingKeyReturnNil() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save(42, forKey: "count", lifetime: 60) + #expect(store.value(Weather.self, forKey: "count") == nil) + #expect(store.value(Int.self, forKey: "count") == 42) + #expect(store.value(Int.self, forKey: "missing") == nil) + } + + @Test func keysAreNamespaced() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save("hello", forKey: "greeting", lifetime: 60) + #expect(defaults.data(forKey: "widget.snapshot.greeting") != nil) + #expect(defaults.data(forKey: "greeting") == nil) + + store.removeValue(forKey: "greeting") + #expect(store.value(String.self, forKey: "greeting") == nil) + } +} From 40169f12fa1f32ba9a9150294b6dd00716dff730 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:07:10 +0300 Subject: [PATCH 13/59] feat(configuration): add deep links, hex colors and App Intents options Reworked from the orphaned WidgetConfiguration.swift: - WidgetDeepLink builds and parses widget URLs as a value type, with sorted query items, scheme checking and first-wins duplicate keys, replacing the non-Sendable handler-registry singleton. - HexColor validates 3, 6 and 8 digit input; the old initializer accepted partial matches and ignored alpha, and hexString indexed past the end of grayscale colour components. - RefreshIntervalOption and AccentColorOption keep the AppEntity choices, plus an AppIntentsPackage so apps can register them. Dropped the storage and migration singletons with hard-coded App Group names; WidgetSnapshotStore covers persistence. --- .../Configuration/WidgetConfiguration.swift | 423 ------------------ .../Configuration/HexColor.swift | 55 +++ .../WidgetConfigurationEntities.swift | 112 +++++ .../Configuration/WidgetDeepLink.swift | 78 ++++ .../ConfigurationTests.swift | 98 ++++ 5 files changed, 343 insertions(+), 423 deletions(-) delete mode 100644 Sources/Configuration/WidgetConfiguration.swift create mode 100644 Sources/WidgetDevelopmentKit/Configuration/HexColor.swift create mode 100644 Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift create mode 100644 Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift create mode 100644 Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift diff --git a/Sources/Configuration/WidgetConfiguration.swift b/Sources/Configuration/WidgetConfiguration.swift deleted file mode 100644 index 9e1df62..0000000 --- a/Sources/Configuration/WidgetConfiguration.swift +++ /dev/null @@ -1,423 +0,0 @@ -// WidgetConfiguration.swift -// iOS-Widget-Development-Kit -// -// Widget Configuration and Deep Linking Support -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit -import AppIntents - -// MARK: - Deep Link Manager - -/// Handles widget deep links to main app -public final class WidgetDeepLinkManager { - public static let shared = WidgetDeepLinkManager() - - public typealias DeepLinkHandler = (URL) -> Void - private var handlers: [String: DeepLinkHandler] = [:] - - private init() {} - - /// Build a deep link URL - public func buildURL( - scheme: String = "widget", - host: String, - path: String = "", - parameters: [String: String] = [:] - ) -> URL? { - var components = URLComponents() - components.scheme = scheme - components.host = host - components.path = path.isEmpty ? "" : "/\(path)" - - if !parameters.isEmpty { - components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) } - } - - return components.url - } - - /// Build URL for specific widget action - public func buildActionURL( - action: String, - widgetKind: String, - parameters: [String: String] = [] - ) -> URL? { - var params = parameters - params["action"] = action - params["widgetKind"] = widgetKind - return buildURL(host: "action", parameters: params) - } - - /// Build URL to open specific content - public func buildContentURL( - contentType: String, - contentId: String, - parameters: [String: String] = [:] - ) -> URL? { - var params = parameters - params["type"] = contentType - params["id"] = contentId - return buildURL(host: "content", parameters: params) - } - - /// Register handler for deep link - public func registerHandler(for host: String, handler: @escaping DeepLinkHandler) { - handlers[host] = handler - } - - /// Handle incoming deep link - @discardableResult - public func handle(url: URL) -> Bool { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let host = components.host else { - return false - } - - if let handler = handlers[host] { - handler(url) - return true - } - - return false - } - - /// Parse parameters from URL - public func parseParameters(from url: URL) -> [String: String] { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let queryItems = components.queryItems else { - return [:] - } - - var params: [String: String] = [:] - for item in queryItems { - if let value = item.value { - params[item.name] = value - } - } - return params - } -} - -// MARK: - Widget Link Builder - -/// Convenience builder for widget links -@available(iOS 17.0, *) -public struct WidgetLinkBuilder { - - /// Create a link that opens the app - public static func openApp( - @ViewBuilder content: () -> Content - ) -> some View { - Link(destination: URL(string: "widget://app")!) { - content() - } - } - - /// Create a link with specific action - public static func action( - _ action: String, - parameters: [String: String] = [:], - @ViewBuilder content: () -> Content - ) -> some View { - let url = WidgetDeepLinkManager.shared.buildActionURL( - action: action, - widgetKind: "", - parameters: parameters - ) ?? URL(string: "widget://app")! - - return Link(destination: url) { - content() - } - } - - /// Create a link to specific content - public static func content( - type: String, - id: String, - @ViewBuilder content: () -> Content - ) -> some View { - let url = WidgetDeepLinkManager.shared.buildContentURL( - contentType: type, - contentId: id - ) ?? URL(string: "widget://app")! - - return Link(destination: url) { - content() - } - } -} - -// MARK: - Widget Configuration Protocol - -/// Protocol for configurable widgets -public protocol ConfigurableWidgetProtocol { - associatedtype Configuration: WidgetConfigurationIntent - var configuration: Configuration { get } -} - -// MARK: - Configuration Options - -/// Common widget configuration options -public struct WidgetConfigurationOptions: Codable { - public var refreshInterval: Int - public var showTitle: Bool - public var accentColorHex: String - public var fontSize: FontSize - public var layoutStyle: LayoutStyle - - public enum FontSize: String, Codable, CaseIterable { - case small, medium, large - } - - public enum LayoutStyle: String, Codable, CaseIterable { - case compact, standard, expanded - } - - public init( - refreshInterval: Int = 15, - showTitle: Bool = true, - accentColorHex: String = "#007AFF", - fontSize: FontSize = .medium, - layoutStyle: LayoutStyle = .standard - ) { - self.refreshInterval = refreshInterval - self.showTitle = showTitle - self.accentColorHex = accentColorHex - self.fontSize = fontSize - self.layoutStyle = layoutStyle - } - - public static let `default` = WidgetConfigurationOptions() -} - -// MARK: - Configuration Storage - -/// Manages widget configuration persistence -public final class WidgetConfigurationStorage { - public static let shared = WidgetConfigurationStorage() - - private let defaults: UserDefaults - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private init() { - defaults = UserDefaults(suiteName: "group.widget.configuration") ?? .standard - } - - /// Save configuration - public func save(_ configuration: T, for widgetId: String) throws { - let data = try encoder.encode(configuration) - defaults.set(data, forKey: "config_\(widgetId)") - } - - /// Load configuration - public func load(_ type: T.Type, for widgetId: String) throws -> T? { - guard let data = defaults.data(forKey: "config_\(widgetId)") else { - return nil - } - return try decoder.decode(type, from: data) - } - - /// Remove configuration - public func remove(for widgetId: String) { - defaults.removeObject(forKey: "config_\(widgetId)") - } - - /// Get all configured widget IDs - public func allConfiguredWidgetIds() -> [String] { - defaults.dictionaryRepresentation().keys - .filter { $0.hasPrefix("config_") } - .map { String($0.dropFirst(7)) } - } -} - -// MARK: - App Intent Configurations - -/// Base configuration intent for widgets -@available(iOS 17.0, *) -public protocol WidgetConfigurationIntent: AppIntent { - static var intentIdentifier: String { get } -} - -/// Refresh interval configuration -@available(iOS 17.0, *) -public struct RefreshIntervalConfiguration: AppEntity { - public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Refresh Interval" - public static var defaultQuery = RefreshIntervalQuery() - - public var id: Int - public var displayName: String - - public var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: LocalizedStringResource(stringLiteral: displayName)) - } - - public static let intervals: [RefreshIntervalConfiguration] = [ - RefreshIntervalConfiguration(id: 5, displayName: "5 minutes"), - RefreshIntervalConfiguration(id: 15, displayName: "15 minutes"), - RefreshIntervalConfiguration(id: 30, displayName: "30 minutes"), - RefreshIntervalConfiguration(id: 60, displayName: "1 hour") - ] - - public init(id: Int, displayName: String) { - self.id = id - self.displayName = displayName - } -} - -@available(iOS 17.0, *) -public struct RefreshIntervalQuery: EntityQuery { - public init() {} - - public func entities(for identifiers: [Int]) async throws -> [RefreshIntervalConfiguration] { - RefreshIntervalConfiguration.intervals.filter { identifiers.contains($0.id) } - } - - public func suggestedEntities() async throws -> [RefreshIntervalConfiguration] { - RefreshIntervalConfiguration.intervals - } -} - -// MARK: - Color Configuration - -/// Color selection for widgets -@available(iOS 17.0, *) -public struct ColorConfiguration: AppEntity { - public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Accent Color" - public static var defaultQuery = ColorConfigurationQuery() - - public var id: String - public var displayName: String - public var hexValue: String - - public var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: LocalizedStringResource(stringLiteral: displayName)) - } - - public var color: Color { - Color(hex: hexValue) ?? .blue - } - - public static let colors: [ColorConfiguration] = [ - ColorConfiguration(id: "blue", displayName: "Blue", hexValue: "#007AFF"), - ColorConfiguration(id: "green", displayName: "Green", hexValue: "#34C759"), - ColorConfiguration(id: "red", displayName: "Red", hexValue: "#FF3B30"), - ColorConfiguration(id: "orange", displayName: "Orange", hexValue: "#FF9500"), - ColorConfiguration(id: "purple", displayName: "Purple", hexValue: "#AF52DE"), - ColorConfiguration(id: "pink", displayName: "Pink", hexValue: "#FF2D55"), - ColorConfiguration(id: "teal", displayName: "Teal", hexValue: "#5AC8FA"), - ColorConfiguration(id: "indigo", displayName: "Indigo", hexValue: "#5856D6") - ] - - public init(id: String, displayName: String, hexValue: String) { - self.id = id - self.displayName = displayName - self.hexValue = hexValue - } -} - -@available(iOS 17.0, *) -public struct ColorConfigurationQuery: EntityQuery { - public init() {} - - public func entities(for identifiers: [String]) async throws -> [ColorConfiguration] { - ColorConfiguration.colors.filter { identifiers.contains($0.id) } - } - - public func suggestedEntities() async throws -> [ColorConfiguration] { - ColorConfiguration.colors - } -} - -// MARK: - Color Hex Extension - -extension Color { - /// Initialize Color from hex string - public init?(hex: String) { - var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) - hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") - - var rgb: UInt64 = 0 - - guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { - return nil - } - - let r = Double((rgb & 0xFF0000) >> 16) / 255.0 - let g = Double((rgb & 0x00FF00) >> 8) / 255.0 - let b = Double(rgb & 0x0000FF) / 255.0 - - self.init(red: r, green: g, blue: b) - } - - /// Convert Color to hex string - public var hexString: String? { - #if canImport(UIKit) - guard let components = UIColor(self).cgColor.components else { return nil } - #else - guard let components = NSColor(self).cgColor.components else { return nil } - #endif - - let r = Int(components[0] * 255) - let g = Int(components[1] * 255) - let b = Int(components[2] * 255) - - return String(format: "#%02X%02X%02X", r, g, b) - } -} - -// MARK: - Widget URL Handler View Modifier - -/// View modifier to handle widget URLs -@available(iOS 17.0, *) -public struct WidgetURLHandler: ViewModifier { - let handler: (URL) -> Void - - public func body(content: Content) -> some View { - content - .onOpenURL { url in - handler(url) - } - } -} - -@available(iOS 17.0, *) -extension View { - /// Handle widget deep links - public func onWidgetURL(_ handler: @escaping (URL) -> Void) -> some View { - modifier(WidgetURLHandler(handler: handler)) - } -} - -// MARK: - Configuration Migration - -/// Handles migration of widget configurations between versions -public final class ConfigurationMigrationManager { - public static let shared = ConfigurationMigrationManager() - - private let defaults = UserDefaults(suiteName: "group.widget.configuration") ?? .standard - private let versionKey = "config_version" - - private init() {} - - /// Current configuration version - public var currentVersion: Int { - get { defaults.integer(forKey: versionKey) } - set { defaults.set(newValue, forKey: versionKey) } - } - - /// Run migrations if needed - public func migrateIfNeeded(targetVersion: Int, migrations: [Int: () -> Void]) { - guard currentVersion < targetVersion else { return } - - for version in (currentVersion + 1)...targetVersion { - if let migration = migrations[version] { - migration() - } - } - - currentVersion = targetVersion - } -} diff --git a/Sources/WidgetDevelopmentKit/Configuration/HexColor.swift b/Sources/WidgetDevelopmentKit/Configuration/HexColor.swift new file mode 100644 index 0000000..e4a3c6d --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Configuration/HexColor.swift @@ -0,0 +1,55 @@ +import SwiftUI + +/// An sRGB color parsed from a hex string such as `"#34C759"`, `"0af"` or `"#FF3B3080"`. +public struct HexColor: Hashable, Sendable { + public let red: Double + public let green: Double + public let blue: Double + public let opacity: Double + + /// Accepts 3, 6 or 8 hex digits (`RGB`, `RRGGBB`, `RRGGBBAA`), with or without a + /// leading `#`. Returns `nil` for any other length or a non-hex character. + public init?(_ string: String) { + var hex = string.trimmingCharacters(in: .whitespacesAndNewlines) + if hex.hasPrefix("#") { hex.removeFirst() } + guard hex.allSatisfy(\.isHexDigit) else { return nil } + + switch hex.count { + case 3: + hex = hex.map { "\($0)\($0)" }.joined() + "FF" + case 6: + hex += "FF" + case 8: + break + default: + return nil + } + + guard let value = UInt32(hex, radix: 16) else { return nil } + red = Double((value >> 24) & 0xFF) / 255 + green = Double((value >> 16) & 0xFF) / 255 + blue = Double((value >> 8) & 0xFF) / 255 + opacity = Double(value & 0xFF) / 255 + } + + /// Uppercase `#RRGGBB`, or `#RRGGBBAA` when not fully opaque. + public var hexString: String { + func byte(_ component: Double) -> String { + String(format: "%02X", Int((component * 255).rounded())) + } + let rgb = "#" + byte(red) + byte(green) + byte(blue) + return opacity < 1 ? rgb + byte(opacity) : rgb + } + + public var color: Color { + Color(.sRGB, red: red, green: green, blue: blue, opacity: opacity) + } +} + +extension Color { + /// Creates a color from a hex string; see ``HexColor`` for the accepted formats. + public init?(hex: String) { + guard let parsed = HexColor(hex) else { return nil } + self = parsed.color + } +} diff --git a/Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift b/Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift new file mode 100644 index 0000000..a456ffc --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift @@ -0,0 +1,112 @@ +import AppIntents +import SwiftUI + +/// A refresh interval the user can pick when editing a configurable widget. +/// +/// Use it as a parameter of your `WidgetConfigurationIntent`: +/// +/// ```swift +/// struct ConfigurationIntent: WidgetConfigurationIntent { +/// static let title: LocalizedStringResource = "Configure" +/// @Parameter(title: "Refresh") var refresh: RefreshIntervalOption? +/// } +/// ``` +public struct RefreshIntervalOption: AppEntity, Hashable, Sendable { + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Refresh Interval" + public static let defaultQuery = RefreshIntervalQuery() + + /// Interval in minutes. + public let id: Int + public let displayName: String + + public init(minutes: Int, displayName: String) { + self.id = minutes + self.displayName = displayName + } + + public var minutes: Int { id } + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(displayName)") + } + + /// The update policy matching this interval. + public var updatePolicy: WidgetUpdatePolicy { .every(minutes: minutes) } + + public static let all: [RefreshIntervalOption] = [ + RefreshIntervalOption(minutes: 15, displayName: "15 minutes"), + RefreshIntervalOption(minutes: 30, displayName: "30 minutes"), + RefreshIntervalOption(minutes: 60, displayName: "1 hour"), + RefreshIntervalOption(minutes: 240, displayName: "4 hours") + ] +} + +public struct RefreshIntervalQuery: EntityQuery { + public init() {} + + public func entities(for identifiers: [Int]) async throws -> [RefreshIntervalOption] { + RefreshIntervalOption.all.filter { identifiers.contains($0.id) } + } + + public func suggestedEntities() async throws -> [RefreshIntervalOption] { + RefreshIntervalOption.all + } +} + +/// An accent color the user can pick when editing a configurable widget. +public struct AccentColorOption: AppEntity, Hashable, Sendable { + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Accent Color" + public static let defaultQuery = AccentColorQuery() + + public let id: String + public let displayName: String + public let hex: HexColor + + public init(id: String, displayName: String, hex: HexColor) { + self.id = id + self.displayName = displayName + self.hex = hex + } + + public var color: Color { hex.color } + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(displayName)") + } + + /// Values match the iOS system colors in light mode. + public static let all: [AccentColorOption] = [ + ("blue", "Blue", "#007AFF"), + ("green", "Green", "#34C759"), + ("red", "Red", "#FF3B30"), + ("orange", "Orange", "#FF9500"), + ("purple", "Purple", "#AF52DE"), + ("pink", "Pink", "#FF2D55"), + ("teal", "Teal", "#5AC8FA"), + ("indigo", "Indigo", "#5856D6") + ].compactMap { id, name, hex in + HexColor(hex).map { AccentColorOption(id: id, displayName: name, hex: $0) } + } +} + +public struct AccentColorQuery: EntityQuery { + public init() {} + + public func entities(for identifiers: [String]) async throws -> [AccentColorOption] { + AccentColorOption.all.filter { identifiers.contains($0.id) } + } + + public func suggestedEntities() async throws -> [AccentColorOption] { + AccentColorOption.all + } +} + +/// Lets the App Intents metadata processor find the entities and intents defined in +/// this package. List it in your app's and widget extension's own `AppIntentsPackage`: +/// +/// ```swift +/// struct AppIntentsRoot: AppIntentsPackage { +/// static let includedPackages: [any AppIntentsPackage.Type] = [WidgetDevelopmentKitIntents.self] +/// } +/// ``` +public struct WidgetDevelopmentKitIntents: AppIntentsPackage {} diff --git a/Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift b/Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift new file mode 100644 index 0000000..a039bd7 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift @@ -0,0 +1,78 @@ +import Foundation +import SwiftUI + +/// A deep link from a widget into the host app, e.g. `myapp://content/article?id=42`. +/// +/// Widgets can only open the app through URLs (`widgetURL` or `Link`). Building and +/// parsing them with one type keeps both sides in agreement. +/// +/// ```swift +/// let link = WidgetDeepLink(scheme: "myapp", host: "content", path: "article", parameters: ["id": "42"]) +/// Text(title).widgetURL(link.url) +/// +/// // In the app: +/// .onOpenURL { url in +/// guard let link = WidgetDeepLink(url: url, expectedScheme: "myapp") else { return } +/// router.open(link) +/// } +/// ``` +public struct WidgetDeepLink: Hashable, Sendable { + public let scheme: String + public let host: String + /// Path components without slashes, e.g. `["article"]`. + public let pathComponents: [String] + public let parameters: [String: String] + + public init(scheme: String, host: String, path: String = "", parameters: [String: String] = [:]) { + self.scheme = scheme.lowercased() + self.host = host + self.pathComponents = path.split(separator: "/").map(String.init) + self.parameters = parameters + } + + /// Parses `url`. Returns `nil` if it has no host, or if `expectedScheme` is given and differs. + public init?(url: URL, expectedScheme: String? = nil) { + guard + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let scheme = components.scheme?.lowercased(), + let host = components.host, !host.isEmpty + else { return nil } + if let expectedScheme, expectedScheme.lowercased() != scheme { return nil } + + var parameters: [String: String] = [:] + for item in components.queryItems ?? [] { + guard let value = item.value else { continue } + // First occurrence wins, so a repeated key cannot override an earlier one. + if parameters[item.name] == nil { parameters[item.name] = value } + } + + self.scheme = scheme + self.host = host + self.pathComponents = components.path.split(separator: "/").map(String.init) + self.parameters = parameters + } + + /// The path joined with slashes, without a leading slash. + public var path: String { pathComponents.joined(separator: "/") } + + /// The URL, with query items sorted by name so equal links produce identical URLs. + public var url: URL? { + var components = URLComponents() + components.scheme = scheme + components.host = host + components.path = pathComponents.isEmpty ? "" : "/" + path + if !parameters.isEmpty { + components.queryItems = parameters + .sorted { $0.key < $1.key } + .map { URLQueryItem(name: $0.key, value: $0.value) } + } + return components.url + } +} + +extension View { + /// Sets the widget's tap target to `link`; does nothing if the link cannot form a URL. + public func widgetURL(_ link: WidgetDeepLink) -> some View { + widgetURL(link.url) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift b/Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift new file mode 100644 index 0000000..4649968 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift @@ -0,0 +1,98 @@ +import Foundation +import SwiftUI +import Testing +@testable import WidgetDevelopmentKit + +@Suite("WidgetDeepLink") +struct WidgetDeepLinkTests { + @Test func buildsURLWithSortedQuery() { + let link = WidgetDeepLink( + scheme: "MyApp", + host: "content", + path: "/article/", + parameters: ["id": "42", "source": "widget", "a": "1"] + ) + #expect(link.url?.absoluteString == "myapp://content/article?a=1&id=42&source=widget") + } + + @Test func roundTripsThroughURL() throws { + let link = WidgetDeepLink(scheme: "myapp", host: "action", path: "toggle/item", parameters: ["id": "a b&c"]) + let url = try #require(link.url) + #expect(WidgetDeepLink(url: url) == link) + #expect(WidgetDeepLink(url: url)?.path == "toggle/item") + } + + @Test func linkWithoutPathOrParameters() { + let link = WidgetDeepLink(scheme: "myapp", host: "home") + #expect(link.url?.absoluteString == "myapp://home") + #expect(link.pathComponents.isEmpty) + } + + @Test func rejectsWrongSchemeAndMissingHost() throws { + let url = try #require(URL(string: "other://content/article")) + #expect(WidgetDeepLink(url: url, expectedScheme: "myapp") == nil) + #expect(WidgetDeepLink(url: url, expectedScheme: "OTHER") != nil) + + let noHost = try #require(URL(string: "myapp:///path")) + #expect(WidgetDeepLink(url: noHost) == nil) + } + + @Test func firstRepeatedParameterWinsAndValuelessItemsAreSkipped() throws { + let url = try #require(URL(string: "myapp://content?id=1&id=2&flag")) + let link = try #require(WidgetDeepLink(url: url)) + #expect(link.parameters == ["id": "1"]) + } +} + +@Suite("HexColor") +struct HexColorTests { + @Test func parsesSixDigits() throws { + let color = try #require(HexColor("#34C759")) + #expect(color.red == 0x34 / 255.0) + #expect(color.green == 0xC7 / 255.0) + #expect(color.blue == 0x59 / 255.0) + #expect(color.opacity == 1) + } + + @Test func parsesShortAndAlphaForms() throws { + #expect(try #require(HexColor("0af")).hexString == "#00AAFF") + let translucent = try #require(HexColor("FF3B3080")) + #expect(translucent.opacity == 0x80 / 255.0) + #expect(translucent.hexString == "#FF3B3080") + #expect(HexColor(" #ffffff \n")?.hexString == "#FFFFFF") + } + + @Test(arguments: ["", "#", "12345", "#GG0000", "#12345678FF", "red", "#123"]) + func rejectsInvalidInput(_ input: String) { + #expect(HexColor(input) == nil) + } + + @Test func colorInitializer() { + #expect(Color(hex: "#007AFF") != nil) + #expect(Color(hex: "nope") == nil) + } +} + +@Suite("Configuration entities") +struct ConfigurationEntityTests { + @Test func everyAccentColorHexIsValidAndIdsAreUnique() { + #expect(AccentColorOption.all.count == 8) + #expect(Set(AccentColorOption.all.map(\.id)).count == AccentColorOption.all.count) + } + + @Test func queriesReturnRequestedEntities() async throws { + let colors = try await AccentColorQuery().entities(for: ["green", "missing", "red"]) + #expect(colors.map(\.id) == ["green", "red"]) + + let intervals = try await RefreshIntervalQuery().entities(for: [60]) + #expect(intervals.first?.displayName == "1 hour") + #expect(try await RefreshIntervalQuery().suggestedEntities() == RefreshIntervalOption.all) + } + + @Test func refreshIntervalMapsToUpdatePolicy() throws { + let option = try #require(RefreshIntervalOption.all.first { $0.minutes == 30 }) + let start = Date(timeIntervalSince1970: 0) + #expect(option.updatePolicy.nextUpdateDate(from: start, calendar: TestCalendar.gregorian()) + == start.addingTimeInterval(30 * 60)) + } +} From 2bdc1f4fb3e7572faabe4c245dc1258b0a9cdb04 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:08:25 +0300 Subject: [PATCH 14/59] feat(networking): add widget data loader and endpoint builder Reworked from the orphaned NetworkDataProvider.swift. The old provider retried every non-2xx status (including 404), waited for connectivity inside a widget extension and used non-Sendable singletons. Now: - WidgetDataLoader retries only transient errors (5xx, 408, timeouts, offline) with doubling backoff, and takes an injectable transport and sleep so the retry logic is unit tested. - URLSession.widgetDefault is ephemeral with short timeouts. - Endpoint replaces APIEndpointBuilder as a value type with encoded path segments and ordered query items. Dropped the polling, sync and background-refresh singletons. --- Sources/Network/NetworkDataProvider.swift | 473 ------------------ .../Networking/Endpoint.swift | 65 +++ .../Networking/WidgetDataLoader.swift | 137 +++++ .../NetworkingTests.swift | 160 ++++++ 4 files changed, 362 insertions(+), 473 deletions(-) delete mode 100644 Sources/Network/NetworkDataProvider.swift create mode 100644 Sources/WidgetDevelopmentKit/Networking/Endpoint.swift create mode 100644 Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift create mode 100644 Tests/WidgetDevelopmentKitTests/NetworkingTests.swift diff --git a/Sources/Network/NetworkDataProvider.swift b/Sources/Network/NetworkDataProvider.swift deleted file mode 100644 index 393ae4e..0000000 --- a/Sources/Network/NetworkDataProvider.swift +++ /dev/null @@ -1,473 +0,0 @@ -// NetworkDataProvider.swift -// iOS-Widget-Development-Kit -// -// Network Data Support for Widgets -// Created by Muhittin Camdali - -import Foundation -import Combine - -// MARK: - Network Configuration - -/// Network request configuration -public struct NetworkConfiguration { - public let timeout: TimeInterval - public let cachePolicy: URLRequest.CachePolicy - public let retryCount: Int - public let retryDelay: TimeInterval - - public init( - timeout: TimeInterval = 30, - cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad, - retryCount: Int = 2, - retryDelay: TimeInterval = 1 - ) { - self.timeout = timeout - self.cachePolicy = cachePolicy - self.retryCount = retryCount - self.retryDelay = retryDelay - } - - public static let `default` = NetworkConfiguration() - public static let fastRefresh = NetworkConfiguration(timeout: 10, cachePolicy: .reloadIgnoringLocalCacheData, retryCount: 1) - public static let cachePriority = NetworkConfiguration(timeout: 60, cachePolicy: .returnCacheDataDontLoad, retryCount: 0) -} - -// MARK: - Network Error - -/// Widget-specific network errors -public enum WidgetNetworkError: Error, LocalizedError { - case invalidURL - case noData - case decodingFailed(Error) - case networkError(Error) - case timeout - case serverError(Int) - case rateLimited - case cancelled - - public var errorDescription: String? { - switch self { - case .invalidURL: - return "Invalid URL provided" - case .noData: - return "No data received from server" - case .decodingFailed(let error): - return "Failed to decode data: \(error.localizedDescription)" - case .networkError(let error): - return "Network error: \(error.localizedDescription)" - case .timeout: - return "Request timed out" - case .serverError(let code): - return "Server returned error code: \(code)" - case .rateLimited: - return "Rate limited - try again later" - case .cancelled: - return "Request was cancelled" - } - } -} - -// MARK: - Network Data Provider - -/// Primary network data provider for widgets -public final class NetworkDataProvider { - public static let shared = NetworkDataProvider() - - private let session: URLSession - private let cache = URLCache( - memoryCapacity: 10 * 1024 * 1024, // 10 MB - diskCapacity: 50 * 1024 * 1024, // 50 MB - directory: nil - ) - - private init() { - let configuration = URLSessionConfiguration.default - configuration.timeoutIntervalForRequest = 30 - configuration.timeoutIntervalForResource = 60 - configuration.waitsForConnectivity = true - configuration.urlCache = cache - - session = URLSession(configuration: configuration) - } - - /// Fetch decodable data from URL - public func fetch( - _ type: T.Type, - from url: URL, - configuration: NetworkConfiguration = .default - ) async throws -> T { - var lastError: Error = WidgetNetworkError.networkError(NSError(domain: "", code: -1)) - - for attempt in 0...configuration.retryCount { - if attempt > 0 { - try await Task.sleep(nanoseconds: UInt64(configuration.retryDelay * 1_000_000_000)) - } - - do { - var request = URLRequest(url: url) - request.timeoutInterval = configuration.timeout - request.cachePolicy = configuration.cachePolicy - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw WidgetNetworkError.networkError(NSError(domain: "Invalid response", code: -1)) - } - - switch httpResponse.statusCode { - case 200...299: - do { - let decoded = try JSONDecoder().decode(type, from: data) - return decoded - } catch { - throw WidgetNetworkError.decodingFailed(error) - } - - case 429: - throw WidgetNetworkError.rateLimited - - case 500...599: - throw WidgetNetworkError.serverError(httpResponse.statusCode) - - default: - throw WidgetNetworkError.serverError(httpResponse.statusCode) - } - - } catch is CancellationError { - throw WidgetNetworkError.cancelled - } catch let error as WidgetNetworkError { - lastError = error - // Don't retry certain errors - if case .decodingFailed = error { throw error } - if case .rateLimited = error { throw error } - } catch { - lastError = WidgetNetworkError.networkError(error) - } - } - - throw lastError - } - - /// Fetch with custom decoder - public func fetch( - _ type: T.Type, - from url: URL, - decoder: JSONDecoder, - configuration: NetworkConfiguration = .default - ) async throws -> T { - var request = URLRequest(url: url) - request.timeoutInterval = configuration.timeout - request.cachePolicy = configuration.cachePolicy - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw WidgetNetworkError.serverError((response as? HTTPURLResponse)?.statusCode ?? -1) - } - - return try decoder.decode(type, from: data) - } - - /// Fetch raw data - public func fetchData(from url: URL, configuration: NetworkConfiguration = .default) async throws -> Data { - var request = URLRequest(url: url) - request.timeoutInterval = configuration.timeout - request.cachePolicy = configuration.cachePolicy - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw WidgetNetworkError.serverError((response as? HTTPURLResponse)?.statusCode ?? -1) - } - - return data - } - - /// Fetch with POST request - public func post( - _ type: T.Type, - to url: URL, - body: U, - configuration: NetworkConfiguration = .default - ) async throws -> T { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.timeoutInterval = configuration.timeout - request.httpBody = try JSONEncoder().encode(body) - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw WidgetNetworkError.serverError((response as? HTTPURLResponse)?.statusCode ?? -1) - } - - return try JSONDecoder().decode(type, from: data) - } - - /// Clear URL cache - public func clearCache() { - cache.removeAllCachedResponses() - } -} - -// MARK: - Cached Network Provider - -/// Network provider with built-in caching layer -public final class CachedNetworkProvider { - private let networkProvider = NetworkDataProvider.shared - private let cacheManager = TimelineCacheManager.shared - private let cacheKey: String - private let cacheDuration: TimeInterval - - public init(cacheKey: String, cacheDuration: TimeInterval = 300) { - self.cacheKey = cacheKey - self.cacheDuration = cacheDuration - } - - /// Fetch data with cache fallback - public func fetch(from url: URL, forceRefresh: Bool = false) async throws -> T { - // Check cache first (unless force refresh) - if !forceRefresh, let cached: T = cacheManager.retrieve(T.self, forKey: cacheKey) { - return cached - } - - // Fetch from network - let data = try await networkProvider.fetch(T.self, from: url) - - // Store in cache - cacheManager.store(data, forKey: cacheKey, expiration: cacheDuration) - - return data - } - - /// Get cached data only - public func getCached() -> T? { - cacheManager.retrieve(T.self, forKey: cacheKey) - } - - /// Clear this provider's cache - public func clearCache() { - cacheManager.clear(forKey: cacheKey) - } -} - -// MARK: - API Endpoint Builder - -/// Builder for constructing API endpoints -public final class APIEndpointBuilder { - private var baseURL: String - private var path: String = "" - private var queryItems: [URLQueryItem] = [] - private var headers: [String: String] = [:] - - public init(baseURL: String) { - self.baseURL = baseURL - } - - @discardableResult - public func path(_ path: String) -> Self { - self.path = path - return self - } - - @discardableResult - public func appendPath(_ segment: String) -> Self { - self.path += "/\(segment)" - return self - } - - @discardableResult - public func query(_ name: String, value: String) -> Self { - queryItems.append(URLQueryItem(name: name, value: value)) - return self - } - - @discardableResult - public func query(_ items: [String: String]) -> Self { - for (name, value) in items { - queryItems.append(URLQueryItem(name: name, value: value)) - } - return self - } - - @discardableResult - public func header(_ name: String, value: String) -> Self { - headers[name] = value - return self - } - - public func build() -> URL? { - var components = URLComponents(string: baseURL + path) - if !queryItems.isEmpty { - components?.queryItems = queryItems - } - return components?.url - } - - public func buildRequest() -> URLRequest? { - guard let url = build() else { return nil } - var request = URLRequest(url: url) - for (name, value) in headers { - request.setValue(value, forHTTPHeaderField: name) - } - return request - } -} - -// MARK: - Background Refresh Support - -/// Background refresh coordinator for widgets -public final class BackgroundRefreshCoordinator { - public static let shared = BackgroundRefreshCoordinator() - - private let userDefaults = UserDefaults(suiteName: "group.widget.background") ?? .standard - - private init() {} - - /// Schedule background refresh - public func scheduleBackgroundRefresh(identifier: String, afterInterval: TimeInterval) { - let nextRefresh = Date().addingTimeInterval(afterInterval) - userDefaults.set(nextRefresh, forKey: "bg_refresh_\(identifier)") - } - - /// Check if background refresh is due - public func isRefreshDue(identifier: String) -> Bool { - guard let scheduledDate = userDefaults.object(forKey: "bg_refresh_\(identifier)") as? Date else { - return true - } - return Date() >= scheduledDate - } - - /// Mark refresh as completed - public func markRefreshCompleted(identifier: String) { - userDefaults.removeObject(forKey: "bg_refresh_\(identifier)") - } - - /// Get last refresh date - public func lastRefreshDate(identifier: String) -> Date? { - userDefaults.object(forKey: "bg_last_refresh_\(identifier)") as? Date - } - - /// Record refresh timestamp - public func recordRefresh(identifier: String) { - userDefaults.set(Date(), forKey: "bg_last_refresh_\(identifier)") - } -} - -// MARK: - Data Sync Manager - -/// Manages data synchronization between app and widget -public final class WidgetDataSyncManager { - public static let shared = WidgetDataSyncManager() - - private let sharedDefaults: UserDefaults - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private init() { - sharedDefaults = UserDefaults(suiteName: "group.widget.data") ?? .standard - } - - /// Share data with widget - public func shareData(_ data: T, key: String) throws { - let encoded = try encoder.encode(data) - sharedDefaults.set(encoded, forKey: key) - - // Notify widgets of data change - WidgetReloadManager.shared.reloadAllWidgets() - } - - /// Retrieve shared data - public func retrieveData(_ type: T.Type, key: String) throws -> T? { - guard let data = sharedDefaults.data(forKey: key) else { - return nil - } - return try decoder.decode(type, from: data) - } - - /// Remove shared data - public func removeData(key: String) { - sharedDefaults.removeObject(forKey: key) - } - - /// Observe data changes - public func observeChanges( - _ type: T.Type, - key: String, - handler: @escaping (T?) -> Void - ) -> NSObjectProtocol { - NotificationCenter.default.addObserver( - forName: UserDefaults.didChangeNotification, - object: sharedDefaults, - queue: .main - ) { [weak self] _ in - let data = try? self?.retrieveData(type, key: key) - handler(data) - } - } -} - -// MARK: - Polling Manager - -/// Manages periodic data polling for widgets -public final class PollingManager { - public static let shared = PollingManager() - - private var pollingTasks: [String: Task] = [:] - private let queue = DispatchQueue(label: "polling.manager.queue") - - private init() {} - - /// Start polling for data - public func startPolling( - identifier: String, - url: URL, - type: T.Type, - interval: TimeInterval, - handler: @escaping (Result) -> Void - ) { - stopPolling(identifier: identifier) - - let task = Task { - while !Task.isCancelled { - do { - let data = try await NetworkDataProvider.shared.fetch(type, from: url) - await MainActor.run { - handler(.success(data)) - } - } catch { - await MainActor.run { - handler(.failure(error)) - } - } - - try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) - } - } - - queue.sync { - pollingTasks[identifier] = task - } - } - - /// Stop polling - public func stopPolling(identifier: String) { - queue.sync { - pollingTasks[identifier]?.cancel() - pollingTasks.removeValue(forKey: identifier) - } - } - - /// Stop all polling - public func stopAllPolling() { - queue.sync { - pollingTasks.values.forEach { $0.cancel() } - pollingTasks.removeAll() - } - } -} diff --git a/Sources/WidgetDevelopmentKit/Networking/Endpoint.swift b/Sources/WidgetDevelopmentKit/Networking/Endpoint.swift new file mode 100644 index 0000000..2dce716 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Networking/Endpoint.swift @@ -0,0 +1,65 @@ +import Foundation + +/// A value-type description of an HTTP endpoint that produces a `URLRequest`. +/// +/// ```swift +/// let request = Endpoint(base: apiURL) +/// .appendingPath("v1/quotes/random") +/// .adding(query: "lang", "en") +/// .adding(header: "Accept", "application/json") +/// .request +/// ``` +public struct Endpoint: Hashable, Sendable { + public let base: URL + public private(set) var pathComponents: [String] = [] + public private(set) var queryItems: [URLQueryItem] = [] + public private(set) var headers: [String: String] = [:] + public var method = "GET" + + public init(base: URL) { + self.base = base + } + + /// Appends one or more slash-separated path segments; empty segments are ignored. + public func appendingPath(_ path: String) -> Endpoint { + var copy = self + copy.pathComponents += path.split(separator: "/").map(String.init) + return copy + } + + /// Adds a query item; repeated names are kept in order. + public func adding(query name: String, _ value: String?) -> Endpoint { + var copy = self + copy.queryItems.append(URLQueryItem(name: name, value: value)) + return copy + } + + public func adding(header name: String, _ value: String) -> Endpoint { + var copy = self + copy.headers[name] = value + return copy + } + + /// The full URL, or `nil` if the components do not form a valid URL. + public var url: URL? { + let fullURL = pathComponents.reduce(base) { $0.appending(path: $1) } + guard var components = URLComponents(url: fullURL, resolvingAgainstBaseURL: false) else { + return nil + } + if !queryItems.isEmpty { + components.queryItems = (components.queryItems ?? []) + queryItems + } + return components.url + } + + /// A request for this endpoint, or `nil` if ``url`` is `nil`. + public var request: URLRequest? { + guard let url else { return nil } + var request = URLRequest(url: url) + request.httpMethod = method + for (name, value) in headers { + request.setValue(value, forHTTPHeaderField: name) + } + return request + } +} diff --git a/Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift b/Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift new file mode 100644 index 0000000..379dea5 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift @@ -0,0 +1,137 @@ +import Foundation + +/// Errors thrown by ``WidgetDataLoader``. +public enum WidgetNetworkError: Error, Sendable, Equatable { + /// The response was not an HTTP response. + case invalidResponse + /// The server answered with a non-2xx status code. + case httpStatus(Int) + /// The body could not be decoded into the requested type. + case decodingFailed(String) + /// A transport error, such as no connection or a timeout. + case transport(URLError.Code) + + /// Whether retrying the same request could reasonably succeed. + public var isTransient: Bool { + switch self { + case .httpStatus(let code): + return code == 408 || (500...599).contains(code) + case .transport(let code): + return [.timedOut, .networkConnectionLost, .notConnectedToInternet, .cannotConnectToHost] + .contains(code) + case .invalidResponse, .decodingFailed: + return false + } + } +} + +/// Retry settings sized for a widget extension, which only gets a few seconds of +/// runtime per timeline request. +public struct RetryPolicy: Sendable, Equatable { + /// Total attempts, including the first one. + public let maxAttempts: Int + /// Delay before the second attempt; each later attempt doubles it. + public let initialDelay: Duration + + public init(maxAttempts: Int = 2, initialDelay: Duration = .milliseconds(500)) { + self.maxAttempts = max(maxAttempts, 1) + self.initialDelay = initialDelay + } + + public static let none = RetryPolicy(maxAttempts: 1) + + /// Delay before attempt number `attempt` (1-based); zero for the first attempt. + public func delay(beforeAttempt attempt: Int) -> Duration { + guard attempt > 1 else { return .zero } + return initialDelay * (1 << (attempt - 2)) + } +} + +/// Fetches and decodes JSON for a timeline provider, retrying only transient failures. +/// +/// ```swift +/// let loader = WidgetDataLoader() +/// let quote = try await loader.load(Quote.self, from: URLRequest(url: quoteURL)) +/// ``` +public struct WidgetDataLoader: Sendable { + public typealias Transport = @Sendable (URLRequest) async throws -> (Data, URLResponse) + public typealias Sleep = @Sendable (Duration) async throws -> Void + + private let transport: Transport + private let sleep: Sleep + private let retryPolicy: RetryPolicy + + /// - Parameters: + /// - session: Session used for requests. The default does not wait for connectivity, + /// so a widget without network fails fast and can fall back to cached data. + /// - retryPolicy: How many times to retry transient errors. + public init(session: URLSession = .widgetDefault, retryPolicy: RetryPolicy = RetryPolicy()) { + self.init( + retryPolicy: retryPolicy, + transport: { try await session.data(for: $0) }, + sleep: { try await Task.sleep(for: $0) } + ) + } + + /// Creates a loader with a custom transport, for tests or non-URLSession clients. + public init(retryPolicy: RetryPolicy = RetryPolicy(), transport: @escaping Transport, sleep: @escaping Sleep) { + self.transport = transport + self.sleep = sleep + self.retryPolicy = retryPolicy + } + + /// Loads `request` and decodes the body as `type`. + public func load( + _ type: Value.Type, + from request: URLRequest, + decoder: JSONDecoder = JSONDecoder() + ) async throws -> Value { + let data = try await loadData(from: request) + do { + return try decoder.decode(type, from: data) + } catch { + throw WidgetNetworkError.decodingFailed(String(describing: error)) + } + } + + /// Loads `request` and returns the body of a 2xx response. + public func loadData(from request: URLRequest) async throws -> Data { + var attempt = 1 + while true { + do { + return try await performOnce(request) + } catch let error as WidgetNetworkError where error.isTransient && attempt < retryPolicy.maxAttempts { + attempt += 1 + try await sleep(retryPolicy.delay(beforeAttempt: attempt)) + } + } + } + + private func performOnce(_ request: URLRequest) async throws -> Data { + let data: Data + let response: URLResponse + do { + (data, response) = try await transport(request) + } catch let error as URLError { + throw WidgetNetworkError.transport(error.code) + } + guard let http = response as? HTTPURLResponse else { + throw WidgetNetworkError.invalidResponse + } + guard (200...299).contains(http.statusCode) else { + throw WidgetNetworkError.httpStatus(http.statusCode) + } + return data + } +} + +extension URLSession { + /// An ephemeral session with short timeouts and no waiting for connectivity. + public static let widgetDefault: URLSession = { + let configuration = URLSessionConfiguration.ephemeral + configuration.timeoutIntervalForRequest = 10 + configuration.timeoutIntervalForResource = 15 + configuration.waitsForConnectivity = false + return URLSession(configuration: configuration) + }() +} diff --git a/Tests/WidgetDevelopmentKitTests/NetworkingTests.swift b/Tests/WidgetDevelopmentKitTests/NetworkingTests.swift new file mode 100644 index 0000000..0ce295c --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/NetworkingTests.swift @@ -0,0 +1,160 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +/// Replays scripted responses and records requests and sleeps. +private final class ScriptedTransport: @unchecked Sendable { + enum Step { + case status(Int, String) + case failure(URLError.Code) + case notHTTP + } + + private let lock = NSLock() + private var steps: [Step] + private(set) var requestCount = 0 + private(set) var sleeps: [Duration] = [] + + init(_ steps: [Step]) { self.steps = steps } + + func respond(to request: URLRequest) throws -> (Data, URLResponse) { + let step: Step = lock.withLock { + requestCount += 1 + return steps.removeFirst() + } + let url = request.url! + switch step { + case .status(let code, let body): + let response = HTTPURLResponse(url: url, statusCode: code, httpVersion: nil, headerFields: nil)! + return (Data(body.utf8), response) + case .failure(let code): + throw URLError(code) + case .notHTTP: + return (Data(), URLResponse(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil)) + } + } + + func recordSleep(_ duration: Duration) { + lock.withLock { sleeps.append(duration) } + } + + func loader(maxAttempts: Int = 3) -> WidgetDataLoader { + WidgetDataLoader( + retryPolicy: RetryPolicy(maxAttempts: maxAttempts, initialDelay: .milliseconds(100)), + transport: { try self.respond(to: $0) }, + sleep: { self.recordSleep($0) } + ) + } +} + +private struct Quote: Decodable, Equatable { + let text: String +} + +@Suite("WidgetDataLoader") +struct WidgetDataLoaderTests { + let request = URLRequest(url: URL(string: "https://example.com/quote")!) + + @Test func decodesSuccessfulResponse() async throws { + let transport = ScriptedTransport([.status(200, #"{"text":"Hello"}"#)]) + let quote = try await transport.loader().load(Quote.self, from: request) + #expect(quote == Quote(text: "Hello")) + #expect(transport.requestCount == 1) + } + + @Test func retriesServerErrorsWithBackoff() async throws { + let transport = ScriptedTransport([ + .status(503, ""), .failure(.timedOut), .status(200, #"{"text":"Third time"}"#) + ]) + let quote = try await transport.loader().load(Quote.self, from: request) + #expect(quote.text == "Third time") + #expect(transport.requestCount == 3) + #expect(transport.sleeps == [.milliseconds(100), .milliseconds(200)]) + } + + @Test func doesNotRetryClientErrors() async { + let transport = ScriptedTransport([.status(404, ""), .status(200, "{}")]) + await #expect(throws: WidgetNetworkError.httpStatus(404)) { + try await transport.loader().loadData(from: request) + } + #expect(transport.requestCount == 1) + #expect(transport.sleeps.isEmpty) + } + + @Test func stopsAfterMaxAttempts() async { + let transport = ScriptedTransport([.status(500, ""), .status(502, "")]) + await #expect(throws: WidgetNetworkError.httpStatus(502)) { + try await transport.loader(maxAttempts: 2).loadData(from: request) + } + #expect(transport.requestCount == 2) + } + + @Test func reportsDecodingAndResponseErrors() async { + let badJSON = ScriptedTransport([.status(200, "not json")]) + await #expect { + try await badJSON.loader().load(Quote.self, from: request) + } throws: { error in + guard case WidgetNetworkError.decodingFailed = error else { return false } + return true + } + #expect(badJSON.requestCount == 1) + + let notHTTP = ScriptedTransport([.notHTTP]) + await #expect(throws: WidgetNetworkError.invalidResponse) { + try await notHTTP.loader().loadData(from: request) + } + } + + @Test func offlineIsTransientButCancelledIsNot() { + #expect(WidgetNetworkError.transport(.notConnectedToInternet).isTransient) + #expect(WidgetNetworkError.transport(.cancelled).isTransient == false) + #expect(WidgetNetworkError.httpStatus(429).isTransient == false) + #expect(WidgetNetworkError.httpStatus(408).isTransient) + } +} + +@Suite("RetryPolicy") +struct RetryPolicyTests { + @Test func delaysDoubleAfterFirstRetry() { + let policy = RetryPolicy(maxAttempts: 4, initialDelay: .seconds(1)) + #expect(policy.delay(beforeAttempt: 1) == .zero) + #expect(policy.delay(beforeAttempt: 2) == .seconds(1)) + #expect(policy.delay(beforeAttempt: 3) == .seconds(2)) + #expect(policy.delay(beforeAttempt: 4) == .seconds(4)) + } + + @Test func atLeastOneAttempt() { + #expect(RetryPolicy(maxAttempts: 0).maxAttempts == 1) + #expect(RetryPolicy.none.maxAttempts == 1) + } +} + +@Suite("Endpoint") +struct EndpointTests { + let base = URL(string: "https://api.example.com/v1")! + + @Test func buildsPathQueryAndHeaders() throws { + var endpoint = Endpoint(base: base) + .appendingPath("quotes/random/") + .adding(query: "lang", "en") + .adding(query: "tag", "swift") + .adding(query: "tag", "ios") + .adding(header: "Accept", "application/json") + endpoint.method = "POST" + + let request = try #require(endpoint.request) + #expect(request.url?.absoluteString == "https://api.example.com/v1/quotes/random?lang=en&tag=swift&tag=ios") + #expect(request.value(forHTTPHeaderField: "Accept") == "application/json") + #expect(request.httpMethod == "POST") + } + + @Test func encodesPathSegmentsAndKeepsBaseQuery() throws { + let baseWithQuery = try #require(URL(string: "https://api.example.com?key=abc")) + let url = try #require(Endpoint(base: baseWithQuery).appendingPath("city/New York").adding(query: "q", "a&b").url) + #expect(url.absoluteString == "https://api.example.com/city/New%20York?key=abc&q=a%26b") + } + + @Test func emptyPathLeavesBaseUnchanged() { + #expect(Endpoint(base: base).appendingPath("//").url == base) + } +} From 30b5408d43176e02d912248868d2fc8b1b80b095 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:10:12 +0300 Subject: [PATCH 15/59] feat(interactive): restore App Intents buttons and interactive widgets The orphaned intents wrote to a hard-coded App Group that no app owns, and ActionIntent posted a NotificationCenter notification inside the widget extension, where the app never receives it. Now: - WidgetStateStore holds flags, counters and a pending-action queue in the App Group named by the WidgetDevelopmentKitAppGroup Info.plist key. - ToggleIntent, IncrementIntent, SetValueIntent, RecordActionIntent and RefreshWidgetIntent use that store; counters saturate and can be clamped at zero. - The rating, water, timer and quick-action widgets use these intents, and the rating view no longer traps for a maximum below one. --- Sources/Interactive/InteractiveWidgets.swift | 715 ------------------ .../InteractiveControlWidgets.swift | 212 ++++++ .../InteractiveTrackerWidgets.swift | 344 +++++++++ .../Interactive/WidgetIntents.swift | 118 +++ .../Interactive/WidgetStateStore.swift | 90 +++ .../WidgetStateStoreTests.swift | 68 ++ 6 files changed, 832 insertions(+), 715 deletions(-) delete mode 100644 Sources/Interactive/InteractiveWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift create mode 100644 Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift create mode 100644 Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift create mode 100644 Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift diff --git a/Sources/Interactive/InteractiveWidgets.swift b/Sources/Interactive/InteractiveWidgets.swift deleted file mode 100644 index 7019380..0000000 --- a/Sources/Interactive/InteractiveWidgets.swift +++ /dev/null @@ -1,715 +0,0 @@ -// InteractiveWidgets.swift -// iOS-Widget-Development-Kit -// -// iOS 17+ Interactive Widget support with App Intents -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit -import AppIntents - -// MARK: - Interactive Widget Protocol - -/// Protocol for interactive widget configurations -@available(iOS 17.0, *) -public protocol InteractiveWidgetProtocol { - associatedtype IntentType: AppIntent - var supportedFamilies: [WidgetFamily] { get } -} - -// MARK: - Base Intent Protocol - -/// Base protocol for widget intents -@available(iOS 17.0, *) -public protocol WidgetIntentProtocol: AppIntent { - static var intentIdentifier: String { get } -} - -// MARK: - Toggle Intent - -/// Generic toggle intent for widgets -@available(iOS 17.0, *) -public struct ToggleIntent: AppIntent { - public static var title: LocalizedStringResource = "Toggle" - public static var description = IntentDescription("Toggle a boolean value") - - @Parameter(title: "Item ID") - public var itemId: String - - @Parameter(title: "New State") - public var newState: Bool - - public init() {} - - public init(itemId: String, newState: Bool) { - self.itemId = itemId - self.newState = newState - } - - public func perform() async throws -> some IntentResult { - // Store the state change - UserDefaults(suiteName: "group.widget.interactive")?.set(newState, forKey: "toggle_\(itemId)") - - // Reload widgets - WidgetCenter.shared.reloadAllTimelines() - - return .result() - } -} - -// MARK: - Increment Intent - -/// Intent to increment a counter -@available(iOS 17.0, *) -public struct IncrementIntent: AppIntent { - public static var title: LocalizedStringResource = "Increment" - public static var description = IntentDescription("Increment a counter value") - - @Parameter(title: "Counter ID") - public var counterId: String - - @Parameter(title: "Amount") - public var amount: Int - - public init() {} - - public init(counterId: String, amount: Int = 1) { - self.counterId = counterId - self.amount = amount - } - - public func perform() async throws -> some IntentResult { - let defaults = UserDefaults(suiteName: "group.widget.interactive") - let currentValue = defaults?.integer(forKey: "counter_\(counterId)") ?? 0 - defaults?.set(currentValue + amount, forKey: "counter_\(counterId)") - - WidgetCenter.shared.reloadAllTimelines() - - return .result() - } -} - -// MARK: - Refresh Intent - -/// Intent to manually refresh widget data -@available(iOS 17.0, *) -public struct RefreshWidgetIntent: AppIntent { - public static var title: LocalizedStringResource = "Refresh Widget" - public static var description = IntentDescription("Manually refresh widget data") - - @Parameter(title: "Widget ID") - public var widgetId: String? - - public init() {} - - public init(widgetId: String? = nil) { - self.widgetId = widgetId - } - - public func perform() async throws -> some IntentResult { - if let widgetId = widgetId { - WidgetCenter.shared.reloadTimelines(ofKind: widgetId) - } else { - WidgetCenter.shared.reloadAllTimelines() - } - - return .result() - } -} - -// MARK: - Action Intent - -/// Generic action intent for custom actions -@available(iOS 17.0, *) -public struct ActionIntent: AppIntent { - public static var title: LocalizedStringResource = "Perform Action" - public static var description = IntentDescription("Perform a custom widget action") - - @Parameter(title: "Action Type") - public var actionType: String - - @Parameter(title: "Parameters") - public var parameters: String? - - public init() {} - - public init(actionType: String, parameters: String? = nil) { - self.actionType = actionType - self.parameters = parameters - } - - public func perform() async throws -> some IntentResult { - // Post notification for app to handle - NotificationCenter.default.post( - name: Notification.Name("WidgetAction"), - object: nil, - userInfo: ["actionType": actionType, "parameters": parameters as Any] - ) - - return .result() - } -} - -// MARK: - Interactive Button Styles - -@available(iOS 17.0, *) -public struct InteractiveWidgetButtonStyle: ButtonStyle { - let backgroundColor: Color - let foregroundColor: Color - let cornerRadius: CGFloat - - public init( - backgroundColor: Color = .blue, - foregroundColor: Color = .white, - cornerRadius: CGFloat = 8 - ) { - self.backgroundColor = backgroundColor - self.foregroundColor = foregroundColor - self.cornerRadius = cornerRadius - } - - public func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(backgroundColor.opacity(configuration.isPressed ? 0.8 : 1)) - .foregroundStyle(foregroundColor) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .scaleEffect(configuration.isPressed ? 0.95 : 1) - .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) - } -} - -// MARK: - Interactive Toggle Widget - -/// Interactive toggle widget template -@available(iOS 17.0, *) -public struct InteractiveToggleWidget: View { - let title: String - let itemId: String - let isOn: Bool - let onIcon: String - let offIcon: String - let onColor: Color - let offColor: Color - - public init( - title: String, - itemId: String, - isOn: Bool, - onIcon: String = "checkmark.circle.fill", - offIcon: String = "circle", - onColor: Color = .green, - offColor: Color = .gray - ) { - self.title = title - self.itemId = itemId - self.isOn = isOn - self.onIcon = onIcon - self.offIcon = offIcon - self.onColor = onColor - self.offColor = offColor - } - - public var body: some View { - Button(intent: ToggleIntent(itemId: itemId, newState: !isOn)) { - HStack { - Image(systemName: isOn ? onIcon : offIcon) - .font(.title2) - .foregroundStyle(isOn ? onColor : offColor) - - Text(title) - .font(.caption) - .lineLimit(1) - - Spacer() - } - .padding() - .background(Color.secondary.opacity(0.1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - .buttonStyle(.plain) - } -} - -// MARK: - Interactive Counter Widget - -/// Interactive counter widget template -@available(iOS 17.0, *) -public struct InteractiveCounterWidget: View { - let title: String - let counterId: String - let currentValue: Int - let accentColor: Color - - public init( - title: String, - counterId: String, - currentValue: Int, - accentColor: Color = .blue - ) { - self.title = title - self.counterId = counterId - self.currentValue = currentValue - self.accentColor = accentColor - } - - public var body: some View { - VStack(spacing: 12) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - - Text("\(currentValue)") - .font(.system(size: 48, weight: .bold, design: .rounded)) - .foregroundStyle(accentColor) - - HStack(spacing: 16) { - Button(intent: IncrementIntent(counterId: counterId, amount: -1)) { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundStyle(.red) - } - .buttonStyle(.plain) - - Button(intent: IncrementIntent(counterId: counterId, amount: 1)) { - Image(systemName: "plus.circle.fill") - .font(.title) - .foregroundStyle(.green) - } - .buttonStyle(.plain) - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Task List Widget - -/// Interactive task list widget with completion toggles -@available(iOS 17.0, *) -public struct InteractiveTaskListWidget: View { - public struct Task: Identifiable { - public let id: String - public let title: String - public var isCompleted: Bool - public let priority: Priority - - public enum Priority { - case low, medium, high - - var color: Color { - switch self { - case .low: return .blue - case .medium: return .orange - case .high: return .red - } - } - } - - public init(id: String, title: String, isCompleted: Bool, priority: Priority = .medium) { - self.id = id - self.title = title - self.isCompleted = isCompleted - self.priority = priority - } - } - - let tasks: [Task] - let listTitle: String - - public init(tasks: [Task], listTitle: String = "Tasks") { - self.tasks = tasks - self.listTitle = listTitle - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(listTitle) - .font(.headline) - - Spacer() - - let completed = tasks.filter { $0.isCompleted }.count - Text("\(completed)/\(tasks.count)") - .font(.caption) - .foregroundStyle(.secondary) - } - - ForEach(tasks.prefix(4)) { task in - Button(intent: ToggleIntent(itemId: task.id, newState: !task.isCompleted)) { - HStack(spacing: 10) { - Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") - .foregroundStyle(task.isCompleted ? .green : task.priority.color) - .font(.body) - - Text(task.title) - .font(.caption) - .strikethrough(task.isCompleted) - .foregroundStyle(task.isCompleted ? .secondary : .primary) - .lineLimit(1) - - Spacer() - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Quick Actions Widget - -/// Quick action buttons widget -@available(iOS 17.0, *) -public struct InteractiveQuickActionsWidget: View { - public struct QuickAction: Identifiable { - public let id: String - public let title: String - public let icon: String - public let color: Color - public let url: URL? - - public init(id: String, title: String, icon: String, color: Color, url: URL? = nil) { - self.id = id - self.title = title - self.icon = icon - self.color = color - self.url = url - } - } - - let actions: [QuickAction] - let columns: Int - - public init(actions: [QuickAction], columns: Int = 2) { - self.actions = actions - self.columns = columns - } - - public var body: some View { - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns), spacing: 8) { - ForEach(actions.prefix(4)) { action in - if let url = action.url { - Link(destination: url) { - ActionButtonView(action: action) - } - } else { - Button(intent: ActionIntent(actionType: action.id)) { - ActionButtonView(action: action) - } - .buttonStyle(.plain) - } - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } - - struct ActionButtonView: View { - let action: QuickAction - - var body: some View { - VStack(spacing: 6) { - Image(systemName: action.icon) - .font(.title2) - .foregroundStyle(.white) - .frame(width: 44, height: 44) - .background(action.color) - .clipShape(RoundedRectangle(cornerRadius: 10)) - - Text(action.title) - .font(.caption2) - .lineLimit(1) - } - } - } -} - -// MARK: - Interactive Timer Control Widget - -/// Timer control widget with start/pause/reset -@available(iOS 17.0, *) -public struct InteractiveTimerWidget: View { - let timerId: String - let timeRemaining: TimeInterval - let isRunning: Bool - let title: String - - public init( - timerId: String, - timeRemaining: TimeInterval, - isRunning: Bool, - title: String = "Timer" - ) { - self.timerId = timerId - self.timeRemaining = timeRemaining - self.isRunning = isRunning - self.title = title - } - - private func formatTime(_ interval: TimeInterval) -> String { - let minutes = Int(interval) / 60 - let seconds = Int(interval) % 60 - return String(format: "%02d:%02d", minutes, seconds) - } - - public var body: some View { - VStack(spacing: 12) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - - Text(formatTime(timeRemaining)) - .font(.system(size: 36, weight: .thin, design: .rounded)) - .monospacedDigit() - - HStack(spacing: 20) { - Button(intent: ActionIntent(actionType: "timer_reset", parameters: timerId)) { - Image(systemName: "arrow.counterclockwise") - .font(.title3) - .foregroundStyle(.orange) - } - .buttonStyle(.plain) - - Button(intent: ActionIntent(actionType: isRunning ? "timer_pause" : "timer_start", parameters: timerId)) { - Image(systemName: isRunning ? "pause.circle.fill" : "play.circle.fill") - .font(.title) - .foregroundStyle(isRunning ? .yellow : .green) - } - .buttonStyle(.plain) - - Button(intent: ActionIntent(actionType: "timer_stop", parameters: timerId)) { - Image(systemName: "stop.circle") - .font(.title3) - .foregroundStyle(.red) - } - .buttonStyle(.plain) - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Habit Tracker Widget - -/// Habit tracker with interactive checkmarks -@available(iOS 17.0, *) -public struct InteractiveHabitWidget: View { - public struct Habit: Identifiable { - public let id: String - public let name: String - public let icon: String - public let color: Color - public var weekProgress: [Bool] // 7 days - - public init(id: String, name: String, icon: String, color: Color, weekProgress: [Bool]) { - self.id = id - self.name = name - self.icon = icon - self.color = color - self.weekProgress = weekProgress - } - } - - let habits: [Habit] - let todayIndex: Int - - public init(habits: [Habit], todayIndex: Int = 6) { - self.habits = habits - self.todayIndex = todayIndex - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Habits") - .font(.headline) - Spacer() - Text("Today") - .font(.caption) - .foregroundStyle(.secondary) - } - - ForEach(habits.prefix(3)) { habit in - HStack(spacing: 8) { - Image(systemName: habit.icon) - .font(.caption) - .foregroundStyle(habit.color) - .frame(width: 20) - - Text(habit.name) - .font(.caption) - .lineLimit(1) - - Spacer() - - // Week progress (non-interactive) - HStack(spacing: 2) { - ForEach(0..<6, id: \.self) { day in - Circle() - .fill(habit.weekProgress.indices.contains(day) && habit.weekProgress[day] - ? habit.color - : Color.secondary.opacity(0.3)) - .frame(width: 6, height: 6) - } - } - - // Today's checkbox (interactive) - Button(intent: ToggleIntent( - itemId: "\(habit.id)_\(todayIndex)", - newState: !(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex]) - )) { - Image(systemName: habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] - ? "checkmark.circle.fill" - : "circle") - .font(.body) - .foregroundStyle(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] - ? habit.color - : Color.secondary) - } - .buttonStyle(.plain) - } - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Rating Widget - -/// Star rating widget -@available(iOS 17.0, *) -public struct InteractiveRatingWidget: View { - let ratingId: String - let currentRating: Int - let maxRating: Int - let title: String - let subtitle: String? - - public init( - ratingId: String, - currentRating: Int, - maxRating: Int = 5, - title: String, - subtitle: String? = nil - ) { - self.ratingId = ratingId - self.currentRating = currentRating - self.maxRating = maxRating - self.title = title - self.subtitle = subtitle - } - - public var body: some View { - VStack(spacing: 12) { - Text(title) - .font(.headline) - - if let subtitle = subtitle { - Text(subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - - HStack(spacing: 8) { - ForEach(1...maxRating, id: \.self) { star in - Button(intent: ActionIntent(actionType: "rate", parameters: "\(ratingId):\(star)")) { - Image(systemName: star <= currentRating ? "star.fill" : "star") - .font(.title2) - .foregroundStyle(star <= currentRating ? .yellow : .secondary) - } - .buttonStyle(.plain) - } - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Water Tracker Widget - -/// Water intake tracker with increment buttons -@available(iOS 17.0, *) -public struct InteractiveWaterWidget: View { - let trackerId: String - let currentIntake: Double // in ml - let dailyGoal: Double - let glassSize: Double - - public init( - trackerId: String, - currentIntake: Double, - dailyGoal: Double = 2000, - glassSize: Double = 250 - ) { - self.trackerId = trackerId - self.currentIntake = currentIntake - self.dailyGoal = dailyGoal - self.glassSize = glassSize - } - - var progress: Double { - min(currentIntake / dailyGoal, 1.0) - } - - var glasses: Int { - Int(currentIntake / glassSize) - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Image(systemName: "drop.fill") - .foregroundStyle(.cyan) - Text("Water") - .font(.headline) - Spacer() - } - - ZStack { - Circle() - .stroke(Color.cyan.opacity(0.2), lineWidth: 8) - - Circle() - .trim(from: 0, to: progress) - .stroke( - LinearGradient(colors: [.cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing), - style: StrokeStyle(lineWidth: 8, lineCap: .round) - ) - .rotationEffect(.degrees(-90)) - - VStack(spacing: 2) { - Text("\(Int(currentIntake))") - .font(.title3) - .fontWeight(.bold) - Text("/ \(Int(dailyGoal))ml") - .font(.system(size: 9)) - .foregroundStyle(.secondary) - } - } - .frame(width: 70, height: 70) - - Button(intent: ActionIntent(actionType: "add_water", parameters: "\(trackerId):\(Int(glassSize))")) { - Label("+\(Int(glassSize))ml", systemImage: "plus.circle.fill") - .font(.caption) - .foregroundStyle(.white) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(.cyan) - .clipShape(Capsule()) - } - .buttonStyle(.plain) - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} diff --git a/Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift b/Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift new file mode 100644 index 0000000..813dfb2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift @@ -0,0 +1,212 @@ +import AppIntents +import SwiftUI +import WidgetKit + +// MARK: - Interactive Button Styles + +public struct InteractiveWidgetButtonStyle: ButtonStyle { + let backgroundColor: Color + let foregroundColor: Color + let cornerRadius: CGFloat + + public init( + backgroundColor: Color = .blue, + foregroundColor: Color = .white, + cornerRadius: CGFloat = 8 + ) { + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.cornerRadius = cornerRadius + } + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(backgroundColor.opacity(configuration.isPressed ? 0.8 : 1)) + .foregroundStyle(foregroundColor) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + } +} + +// MARK: - Interactive Toggle Widget + +/// Interactive toggle widget template +public struct InteractiveToggleWidget: View { + let title: String + let itemId: String + let isOn: Bool + let onIcon: String + let offIcon: String + let onColor: Color + let offColor: Color + + public init( + title: String, + itemId: String, + isOn: Bool, + onIcon: String = "checkmark.circle.fill", + offIcon: String = "circle", + onColor: Color = .green, + offColor: Color = .gray + ) { + self.title = title + self.itemId = itemId + self.isOn = isOn + self.onIcon = onIcon + self.offIcon = offIcon + self.onColor = onColor + self.offColor = offColor + } + + public var body: some View { + Button(intent: ToggleIntent(itemId: itemId, newState: !isOn)) { + HStack { + Image(systemName: isOn ? onIcon : offIcon) + .font(.title2) + .foregroundStyle(isOn ? onColor : offColor) + + Text(title) + .font(.caption) + .lineLimit(1) + + Spacer() + } + .padding() + .background(Color.secondary.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + .buttonStyle(.plain) + } +} + +// MARK: - Interactive Counter Widget + +/// Interactive counter widget template +public struct InteractiveCounterWidget: View { + let title: String + let counterId: String + let currentValue: Int + let accentColor: Color + + public init( + title: String, + counterId: String, + currentValue: Int, + accentColor: Color = .blue + ) { + self.title = title + self.counterId = counterId + self.currentValue = currentValue + self.accentColor = accentColor + } + + public var body: some View { + VStack(spacing: 12) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + + Text("\(currentValue)") + .font(.system(size: 48, weight: .bold, design: .rounded)) + .foregroundStyle(accentColor) + + HStack(spacing: 16) { + Button(intent: IncrementIntent(counterId: counterId, amount: -1)) { + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundStyle(.red) + } + .buttonStyle(.plain) + + Button(intent: IncrementIntent(counterId: counterId, amount: 1)) { + Image(systemName: "plus.circle.fill") + .font(.title) + .foregroundStyle(.green) + } + .buttonStyle(.plain) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Task List Widget + +/// Interactive task list widget with completion toggles +public struct InteractiveTaskListWidget: View { + public struct Task: Identifiable { + public let id: String + public let title: String + public var isCompleted: Bool + public let priority: Priority + + public enum Priority { + case low, medium, high + + var color: Color { + switch self { + case .low: return .blue + case .medium: return .orange + case .high: return .red + } + } + } + + public init(id: String, title: String, isCompleted: Bool, priority: Priority = .medium) { + self.id = id + self.title = title + self.isCompleted = isCompleted + self.priority = priority + } + } + + let tasks: [Task] + let listTitle: String + + public init(tasks: [Task], listTitle: String = "Tasks") { + self.tasks = tasks + self.listTitle = listTitle + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(listTitle) + .font(.headline) + + Spacer() + + let completed = tasks.filter { $0.isCompleted }.count + Text("\(completed)/\(tasks.count)") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(tasks.prefix(4)) { task in + Button(intent: ToggleIntent(itemId: task.id, newState: !task.isCompleted)) { + HStack(spacing: 10) { + Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundStyle(task.isCompleted ? .green : task.priority.color) + .font(.body) + + Text(task.title) + .font(.caption) + .strikethrough(task.isCompleted) + .foregroundStyle(task.isCompleted ? .secondary : .primary) + .lineLimit(1) + + Spacer() + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift b/Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift new file mode 100644 index 0000000..78303f6 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift @@ -0,0 +1,344 @@ +import AppIntents +import SwiftUI +import WidgetKit + +// MARK: - Interactive Quick Actions Widget + +/// Quick action buttons widget +public struct InteractiveQuickActionsWidget: View { + public struct QuickAction: Identifiable { + public let id: String + public let title: String + public let icon: String + public let color: Color + public let url: URL? + + public init(id: String, title: String, icon: String, color: Color, url: URL? = nil) { + self.id = id + self.title = title + self.icon = icon + self.color = color + self.url = url + } + } + + let actions: [QuickAction] + let columns: Int + + public init(actions: [QuickAction], columns: Int = 2) { + self.actions = actions + self.columns = columns + } + + public var body: some View { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns), spacing: 8) { + ForEach(actions.prefix(4)) { action in + if let url = action.url { + Link(destination: url) { + ActionButtonView(action: action) + } + } else { + Button(intent: RecordActionIntent(action: action.id)) { + ActionButtonView(action: action) + } + .buttonStyle(.plain) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + struct ActionButtonView: View { + let action: QuickAction + + var body: some View { + VStack(spacing: 6) { + Image(systemName: action.icon) + .font(.title2) + .foregroundStyle(.white) + .frame(width: 44, height: 44) + .background(action.color) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + Text(action.title) + .font(.caption2) + .lineLimit(1) + } + } + } +} + +// MARK: - Interactive Timer Control Widget + +/// Timer control widget with start/pause/reset +public struct InteractiveTimerWidget: View { + let timerId: String + let timeRemaining: TimeInterval + let isRunning: Bool + let title: String + + public init( + timerId: String, + timeRemaining: TimeInterval, + isRunning: Bool, + title: String = "Timer" + ) { + self.timerId = timerId + self.timeRemaining = timeRemaining + self.isRunning = isRunning + self.title = title + } + + private func formatTime(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } + + public var body: some View { + VStack(spacing: 12) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + + Text(formatTime(timeRemaining)) + .font(.system(size: 36, weight: .thin, design: .rounded)) + .monospacedDigit() + + HStack(spacing: 20) { + Button(intent: RecordActionIntent(action: "timer.reset.\(timerId)")) { + Image(systemName: "arrow.counterclockwise") + .font(.title3) + .foregroundStyle(.orange) + } + .buttonStyle(.plain) + + Button(intent: RecordActionIntent(action: "timer.\(isRunning ? "pause" : "start").\(timerId)")) { + Image(systemName: isRunning ? "pause.circle.fill" : "play.circle.fill") + .font(.title) + .foregroundStyle(isRunning ? .yellow : .green) + } + .buttonStyle(.plain) + + Button(intent: RecordActionIntent(action: "timer.stop.\(timerId)")) { + Image(systemName: "stop.circle") + .font(.title3) + .foregroundStyle(.red) + } + .buttonStyle(.plain) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Habit Tracker Widget + +/// Habit tracker with interactive checkmarks +public struct InteractiveHabitWidget: View { + public struct Habit: Identifiable { + public let id: String + public let name: String + public let icon: String + public let color: Color + public var weekProgress: [Bool] // 7 days + + public init(id: String, name: String, icon: String, color: Color, weekProgress: [Bool]) { + self.id = id + self.name = name + self.icon = icon + self.color = color + self.weekProgress = weekProgress + } + } + + let habits: [Habit] + let todayIndex: Int + + public init(habits: [Habit], todayIndex: Int = 6) { + self.habits = habits + self.todayIndex = todayIndex + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Habits") + .font(.headline) + Spacer() + Text("Today") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(habits.prefix(3)) { habit in + HStack(spacing: 8) { + Image(systemName: habit.icon) + .font(.caption) + .foregroundStyle(habit.color) + .frame(width: 20) + + Text(habit.name) + .font(.caption) + .lineLimit(1) + + Spacer() + + // Week progress (non-interactive) + HStack(spacing: 2) { + ForEach(0..<6, id: \.self) { day in + Circle() + .fill(habit.weekProgress.indices.contains(day) && habit.weekProgress[day] + ? habit.color + : Color.secondary.opacity(0.3)) + .frame(width: 6, height: 6) + } + } + + // Today's checkbox (interactive) + Button(intent: ToggleIntent( + itemId: "\(habit.id)_\(todayIndex)", + newState: !(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex]) + )) { + Image(systemName: habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] + ? "checkmark.circle.fill" + : "circle") + .font(.body) + .foregroundStyle(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] + ? habit.color + : Color.secondary) + } + .buttonStyle(.plain) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Rating Widget + +/// Star rating widget +public struct InteractiveRatingWidget: View { + let ratingId: String + let currentRating: Int + let maxRating: Int + let title: String + let subtitle: String? + + public init( + ratingId: String, + currentRating: Int, + maxRating: Int = 5, + title: String, + subtitle: String? = nil + ) { + self.ratingId = ratingId + self.currentRating = currentRating + self.maxRating = maxRating + self.title = title + self.subtitle = subtitle + } + + public var body: some View { + VStack(spacing: 12) { + Text(title) + .font(.headline) + + if let subtitle = subtitle { + Text(subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 8) { + ForEach(1...max(maxRating, 1), id: \.self) { star in + Button(intent: SetValueIntent(key: ratingId, value: star)) { + Image(systemName: star <= currentRating ? "star.fill" : "star") + .font(.title2) + .foregroundStyle(star <= currentRating ? .yellow : .secondary) + } + .buttonStyle(.plain) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Water Tracker Widget + +/// Water intake tracker with increment buttons +public struct InteractiveWaterWidget: View { + let trackerId: String + let currentIntake: Double // in ml + let dailyGoal: Double + let glassSize: Double + + public init( + trackerId: String, + currentIntake: Double, + dailyGoal: Double = 2000, + glassSize: Double = 250 + ) { + self.trackerId = trackerId + self.currentIntake = currentIntake.isFinite ? max(currentIntake, 0) : 0 + self.dailyGoal = dailyGoal + // The button converts the glass size to whole millilitres, so keep it finite. + self.glassSize = glassSize.isFinite ? min(max(glassSize, 0), 10_000) : 0 + } + + var progress: Double { + WidgetMath.fraction(currentIntake, of: dailyGoal) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: "drop.fill") + .foregroundStyle(.cyan) + Text("Water") + .font(.headline) + Spacer() + } + + ZStack { + Circle() + .stroke(Color.cyan.opacity(0.2), lineWidth: 8) + + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(progress)) + .stroke( + LinearGradient(colors: [.cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing), + style: StrokeStyle(lineWidth: 8, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + + VStack(spacing: 2) { + Text("\(Int(currentIntake))") + .font(.title3) + .fontWeight(.bold) + Text("/ \(dailyGoal, format: .number.precision(.fractionLength(0)))ml") + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + .frame(width: 70, height: 70) + + Button(intent: IncrementIntent(counterId: trackerId, amount: Int(glassSize))) { + Label("+\(Int(glassSize))ml", systemImage: "plus.circle.fill") + .font(.caption) + .foregroundStyle(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(.cyan) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift b/Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift new file mode 100644 index 0000000..fe19b40 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift @@ -0,0 +1,118 @@ +import AppIntents +import WidgetKit + +/// Sets a boolean flag in ``WidgetStateStore/shared`` and reloads widget timelines. +public struct ToggleIntent: AppIntent { + public static let title: LocalizedStringResource = "Toggle" + public static let description = IntentDescription("Turns a widget item on or off.") + public static let isDiscoverable = false + + @Parameter(title: "Item ID") public var itemId: String + @Parameter(title: "New State") public var newState: Bool + + public init() {} + + public init(itemId: String, newState: Bool) { + self.itemId = itemId + self.newState = newState + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.setOn(newState, for: itemId) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Adds `amount` (which may be negative) to a counter, never going below zero. +public struct IncrementIntent: AppIntent { + public static let title: LocalizedStringResource = "Increment" + public static let description = IntentDescription("Changes a widget counter.") + public static let isDiscoverable = false + + @Parameter(title: "Counter ID") public var counterId: String + @Parameter(title: "Amount") public var amount: Int + + public init() {} + + public init(counterId: String, amount: Int = 1) { + self.counterId = counterId + self.amount = amount + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.increment(counterId, by: amount, minimum: 0) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Stores an integer, e.g. a star rating. +public struct SetValueIntent: AppIntent { + public static let title: LocalizedStringResource = "Set Value" + public static let description = IntentDescription("Stores a value chosen in a widget.") + public static let isDiscoverable = false + + @Parameter(title: "Key") public var key: String + @Parameter(title: "Value") public var value: Int + + public init() {} + + public init(key: String, value: Int) { + self.key = key + self.value = value + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.setValue(value, for: key) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Queues an action identifier for the app to process later with +/// ``WidgetStateStore/drainPendingActions()``. Use it for work the extension +/// cannot do itself, such as starting a timer owned by the app. +public struct RecordActionIntent: AppIntent { + public static let title: LocalizedStringResource = "Record Action" + public static let description = IntentDescription("Queues a widget action for the app.") + public static let isDiscoverable = false + + @Parameter(title: "Action") public var action: String + + public init() {} + + public init(action: String) { + self.action = action + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.enqueueAction(action) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Reloads one widget kind, or all widgets when `kind` is empty. +public struct RefreshWidgetIntent: AppIntent { + public static let title: LocalizedStringResource = "Refresh Widget" + public static let description = IntentDescription("Reloads widget data.") + public static let isDiscoverable = false + + @Parameter(title: "Widget Kind") public var kind: String? + + public init() {} + + public init(kind: String? = nil) { + self.kind = kind + } + + public func perform() async throws -> some IntentResult { + if let kind, !kind.isEmpty { + WidgetCenter.shared.reloadTimelines(ofKind: kind) + } else { + WidgetCenter.shared.reloadAllTimelines() + } + return .result() + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift b/Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift new file mode 100644 index 0000000..2de8295 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift @@ -0,0 +1,90 @@ +import Foundation + +/// Small key-value state shared by interactive widget intents and the host app. +/// +/// Interactive widget buttons run their `AppIntent` in the widget extension +/// process, so any state they change must live somewhere the app can also read: +/// usually `UserDefaults` for a shared App Group. +/// +/// The intents in this package use ``shared``, which reads the App Group from +/// the `WidgetDevelopmentKitAppGroup` key in the running bundle's Info.plist. +/// Add that key with the same value to both the app and the widget extension. +public struct WidgetStateStore: Sendable { + /// Info.plist key holding the App Group identifier used by ``shared``. + public static let appGroupInfoKey = "WidgetDevelopmentKitAppGroup" + + private static let pendingActionsKey = "actions.pending" + private static let maximumPendingActions = 50 + + // UserDefaults is documented as thread-safe but is not marked Sendable in the SDK. + nonisolated(unsafe) private let defaults: UserDefaults + private let keyPrefix: String + + public init(defaults: UserDefaults, keyPrefix: String = "widget.state.") { + self.defaults = defaults + self.keyPrefix = keyPrefix + } + + /// The store for the App Group named in Info.plist, or standard defaults if the key + /// is missing (state then stays inside the current process's container). + public static var shared: WidgetStateStore { + let group = Bundle.main.object(forInfoDictionaryKey: appGroupInfoKey) as? String + let defaults = group.flatMap { UserDefaults(suiteName: $0) } ?? .standard + return WidgetStateStore(defaults: defaults) + } + + // MARK: Flags + + public func isOn(_ id: String) -> Bool { + defaults.bool(forKey: key("flag", id)) + } + + public func setOn(_ isOn: Bool, for id: String) { + defaults.set(isOn, forKey: key("flag", id)) + } + + // MARK: Integers + + public func value(_ id: String) -> Int { + defaults.integer(forKey: key("value", id)) + } + + public func setValue(_ value: Int, for id: String) { + defaults.set(value, forKey: key("value", id)) + } + + /// Adds `amount` and returns the new value. Saturates instead of overflowing. + @discardableResult + public func increment(_ id: String, by amount: Int = 1, minimum: Int? = nil) -> Int { + let (sum, overflow) = value(id).addingReportingOverflow(amount) + var newValue = overflow ? (amount > 0 ? Int.max : Int.min) : sum + if let minimum { newValue = max(newValue, minimum) } + setValue(newValue, for: id) + return newValue + } + + // MARK: Pending actions + + /// Queues an action for the app to handle on its next launch or foreground. + /// The queue keeps the most recent 50 actions. + public func enqueueAction(_ action: String) { + var actions = pendingActions() + actions.append(action) + defaults.set(Array(actions.suffix(Self.maximumPendingActions)), forKey: keyPrefix + Self.pendingActionsKey) + } + + public func pendingActions() -> [String] { + defaults.stringArray(forKey: keyPrefix + Self.pendingActionsKey) ?? [] + } + + /// Returns queued actions in order and clears the queue. + public func drainPendingActions() -> [String] { + let actions = pendingActions() + defaults.removeObject(forKey: keyPrefix + Self.pendingActionsKey) + return actions + } + + private func key(_ kind: String, _ id: String) -> String { + "\(keyPrefix)\(kind).\(id)" + } +} diff --git a/Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift b/Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift new file mode 100644 index 0000000..51d7bc3 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift @@ -0,0 +1,68 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("WidgetStateStore") +struct WidgetStateStoreTests { + private let suiteName = "WidgetStateStoreTests.\(UUID().uuidString)" + + private func withStore(_ body: (WidgetStateStore, UserDefaults) throws -> Void) rethrows { + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + try body(WidgetStateStore(defaults: defaults), defaults) + } + + @Test func flagsDefaultToOffAndPersist() { + withStore { store, _ in + #expect(store.isOn("lamp") == false) + store.setOn(true, for: "lamp") + #expect(store.isOn("lamp")) + #expect(store.isOn("other") == false) + } + } + + @Test func incrementRespectsMinimum() { + withStore { store, _ in + #expect(store.increment("water", by: 250) == 250) + #expect(store.increment("water", by: 250) == 500) + #expect(store.increment("water", by: -1_000, minimum: 0) == 0) + #expect(store.value("water") == 0) + } + } + + @Test func incrementSaturatesInsteadOfOverflowing() { + withStore { store, _ in + store.setValue(.max - 1, for: "big") + #expect(store.increment("big", by: 10) == .max) + store.setValue(.min + 1, for: "small") + #expect(store.increment("small", by: -10) == .min) + } + } + + @Test func pendingActionsAreFIFOAndDrained() { + withStore { store, _ in + store.enqueueAction("timer.start.tea") + store.enqueueAction("timer.stop.tea") + #expect(store.drainPendingActions() == ["timer.start.tea", "timer.stop.tea"]) + #expect(store.pendingActions().isEmpty) + } + } + + @Test func pendingActionsKeepMostRecentFifty() { + withStore { store, _ in + for index in 0..<60 { store.enqueueAction("action.\(index)") } + let actions = store.pendingActions() + #expect(actions.count == 50) + #expect(actions.first == "action.10") + #expect(actions.last == "action.59") + } + } + + @Test func keysAreNamespaced() { + withStore { store, defaults in + store.setValue(3, for: "rating") + #expect(defaults.integer(forKey: "widget.state.value.rating") == 3) + #expect(defaults.object(forKey: "rating") == nil) + } + } +} From 82fe14904f4853d10fc12cd30fc4b33f33254b3e Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:11:09 +0300 Subject: [PATCH 16/59] feat(live-activities): restore ActivityKit attributes and views Restores ten ActivityAttributes types (delivery, order, ride, flight, timer, workout, parking, sports, now playing, meeting) with their Lock Screen and Dynamic Island compact views. The files compile only where ActivityKit exists (iOS). LiveActivityManager becomes a LiveActivities namespace without a singleton, with optional stale dates; the unused LiveActivityTemplate protocol is removed and time labels use WidgetFormat.playbackTime. --- .../LiveActivityTemplates.swift | 1303 ----------------- .../LiveActivities/DeliveryActivities.swift | 243 +++ .../LiveActivities/LiveActivities.swift | 50 + .../LiveActivities/MediaActivities.swift | 243 +++ .../LiveActivities/MeetingActivity.swift | 116 ++ .../LiveActivities/TimingActivities.swift | 331 +++++ .../LiveActivities/TravelActivities.swift | 287 ++++ 7 files changed, 1270 insertions(+), 1303 deletions(-) delete mode 100644 Sources/LiveActivities/LiveActivityTemplates.swift create mode 100644 Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift create mode 100644 Sources/WidgetDevelopmentKit/LiveActivities/LiveActivities.swift create mode 100644 Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift create mode 100644 Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift create mode 100644 Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift create mode 100644 Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift diff --git a/Sources/LiveActivities/LiveActivityTemplates.swift b/Sources/LiveActivities/LiveActivityTemplates.swift deleted file mode 100644 index 6129405..0000000 --- a/Sources/LiveActivities/LiveActivityTemplates.swift +++ /dev/null @@ -1,1303 +0,0 @@ -// LiveActivityTemplates.swift -// iOS-Widget-Development-Kit -// -// Complete collection of 15+ Live Activity templates with Dynamic Island support -// Created by Muhittin Camdali - -import SwiftUI -import ActivityKit -import WidgetKit - -// MARK: - Live Activity Protocol - -/// Protocol for all Live Activity templates -@available(iOS 16.1, *) -public protocol LiveActivityTemplate { - associatedtype Attributes: ActivityAttributes - associatedtype CompactLeadingView: View - associatedtype CompactTrailingView: View - associatedtype ExpandedView: View - associatedtype MinimalView: View - associatedtype LockScreenView: View - - @ViewBuilder func makeCompactLeading(context: ActivityViewContext) -> CompactLeadingView - @ViewBuilder func makeCompactTrailing(context: ActivityViewContext) -> CompactTrailingView - @ViewBuilder func makeExpanded(context: ActivityViewContext) -> ExpandedView - @ViewBuilder func makeMinimal(context: ActivityViewContext) -> MinimalView - @ViewBuilder func makeLockScreen(context: ActivityViewContext) -> LockScreenView -} - -// MARK: - 1. Delivery Tracking Activity - -/// Food/Package delivery tracking Live Activity -@available(iOS 16.1, *) -public struct DeliveryAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: DeliveryStatus - public var estimatedArrival: Date - public var driverName: String - public var currentStep: Int - - public init(status: DeliveryStatus, estimatedArrival: Date, driverName: String, currentStep: Int) { - self.status = status - self.estimatedArrival = estimatedArrival - self.driverName = driverName - self.currentStep = currentStep - } - } - - public enum DeliveryStatus: String, Codable { - case preparing = "Preparing" - case pickedUp = "Picked Up" - case onTheWay = "On the Way" - case arriving = "Arriving" - case delivered = "Delivered" - - public var icon: String { - switch self { - case .preparing: return "takeoutbag.and.cup.and.straw" - case .pickedUp: return "bag.fill" - case .onTheWay: return "car.fill" - case .arriving: return "location.fill" - case .delivered: return "checkmark.circle.fill" - } - } - - public var color: Color { - switch self { - case .preparing: return .orange - case .pickedUp: return .blue - case .onTheWay: return .purple - case .arriving: return .green - case .delivered: return .green - } - } - } - - public let orderNumber: String - public let restaurantName: String - public let totalSteps: Int - - public init(orderNumber: String, restaurantName: String, totalSteps: Int = 4) { - self.orderNumber = orderNumber - self.restaurantName = restaurantName - self.totalSteps = totalSteps - } -} - -@available(iOS 16.2, *) -public struct DeliveryActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(context.attributes.restaurantName) - .font(.headline) - Text("Order #\(context.attributes.orderNumber)") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: context.state.status.icon) - .font(.title2) - .foregroundStyle(context.state.status.color) - } - - // Progress bar - ProgressView(value: Double(context.state.currentStep), total: Double(context.attributes.totalSteps)) - .tint(context.state.status.color) - - HStack { - Text(context.state.status.rawValue) - .font(.caption) - .fontWeight(.medium) - - Spacer() - - Label { - Text(context.state.estimatedArrival, style: .relative) - } icon: { - Image(systemName: "clock") - } - .font(.caption) - .foregroundStyle(.secondary) - } - } - .padding() - } - - // Dynamic Island Views - public var compactLeading: some View { - Image(systemName: context.state.status.icon) - .foregroundStyle(context.state.status.color) - } - - public var compactTrailing: some View { - Text(context.state.estimatedArrival, style: .timer) - .font(.caption2) - .monospacedDigit() - } - - public var minimal: some View { - Image(systemName: context.state.status.icon) - .foregroundStyle(context.state.status.color) - } -} - -// MARK: - 2. Ride Sharing Activity - -/// Uber/Lyft style ride tracking -@available(iOS 16.1, *) -public struct RideAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: RideStatus - public var driverName: String - public var vehicleInfo: String - public var licensePlate: String - public var eta: Date - public var driverRating: Double - - public init(status: RideStatus, driverName: String, vehicleInfo: String, licensePlate: String, eta: Date, driverRating: Double) { - self.status = status - self.driverName = driverName - self.vehicleInfo = vehicleInfo - self.licensePlate = licensePlate - self.eta = eta - self.driverRating = driverRating - } - } - - public enum RideStatus: String, Codable { - case searching = "Finding Driver" - case driverAssigned = "Driver Assigned" - case arriving = "Driver Arriving" - case inProgress = "In Progress" - case completed = "Completed" - - public var icon: String { - switch self { - case .searching: return "magnifyingglass" - case .driverAssigned: return "person.fill.checkmark" - case .arriving: return "car.fill" - case .inProgress: return "location.fill" - case .completed: return "checkmark.circle.fill" - } - } - } - - public let pickupLocation: String - public let dropoffLocation: String - public let rideType: String - - public init(pickupLocation: String, dropoffLocation: String, rideType: String = "UberX") { - self.pickupLocation = pickupLocation - self.dropoffLocation = dropoffLocation - self.rideType = rideType - } -} - -@available(iOS 16.2, *) -public struct RideActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(context.state.driverName) - .font(.headline) - - HStack(spacing: 4) { - Image(systemName: "star.fill") - .font(.caption2) - .foregroundStyle(.yellow) - Text(String(format: "%.1f", context.state.driverRating)) - .font(.caption) - } - } - - Spacer() - - VStack(alignment: .trailing, spacing: 2) { - Text(context.state.vehicleInfo) - .font(.caption) - Text(context.state.licensePlate) - .font(.caption) - .fontWeight(.bold) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.secondary.opacity(0.2)) - .clipShape(RoundedRectangle(cornerRadius: 4)) - } - } - - Divider() - - HStack { - VStack(alignment: .leading) { - Label(context.attributes.pickupLocation, systemImage: "circle.fill") - .font(.caption2) - .foregroundStyle(.green) - - Label(context.attributes.dropoffLocation, systemImage: "mappin.circle.fill") - .font(.caption2) - .foregroundStyle(.red) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("ETA") - .font(.caption2) - .foregroundStyle(.secondary) - Text(context.state.eta, style: .relative) - .font(.headline) - } - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "car.fill") - .foregroundStyle(.blue) - } - - public var compactTrailing: some View { - Text(context.state.eta, style: .relative) - .font(.caption2) - } -} - -// MARK: - 3. Sports Score Activity - -/// Live sports score tracking -@available(iOS 16.1, *) -public struct SportsScoreAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var homeScore: Int - public var awayScore: Int - public var gameTime: String - public var period: String - public var isLive: Bool - public var lastEvent: String? - - public init(homeScore: Int, awayScore: Int, gameTime: String, period: String, isLive: Bool, lastEvent: String? = nil) { - self.homeScore = homeScore - self.awayScore = awayScore - self.gameTime = gameTime - self.period = period - self.isLive = isLive - self.lastEvent = lastEvent - } - } - - public let homeTeam: String - public let awayTeam: String - public let homeTeamAbbr: String - public let awayTeamAbbr: String - public let league: String - - public init(homeTeam: String, awayTeam: String, homeTeamAbbr: String, awayTeamAbbr: String, league: String) { - self.homeTeam = homeTeam - self.awayTeam = awayTeam - self.homeTeamAbbr = homeTeamAbbr - self.awayTeamAbbr = awayTeamAbbr - self.league = league - } -} - -@available(iOS 16.2, *) -public struct SportsActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 8) { - HStack { - Text(context.attributes.league) - .font(.caption2) - .foregroundStyle(.secondary) - - Spacer() - - if context.state.isLive { - HStack(spacing: 4) { - Circle() - .fill(.red) - .frame(width: 6, height: 6) - Text("LIVE") - .font(.caption2) - .foregroundStyle(.red) - } - } - - Text(context.state.period) - .font(.caption) - .fontWeight(.medium) - } - - HStack { - TeamScoreView( - name: context.attributes.homeTeamAbbr, - score: context.state.homeScore, - isWinning: context.state.homeScore > context.state.awayScore - ) - - Spacer() - - VStack { - Text(context.state.gameTime) - .font(.headline) - .monospacedDigit() - } - - Spacer() - - TeamScoreView( - name: context.attributes.awayTeamAbbr, - score: context.state.awayScore, - isWinning: context.state.awayScore > context.state.homeScore - ) - } - - if let event = context.state.lastEvent { - Text(event) - .font(.caption2) - .foregroundStyle(.secondary) - .lineLimit(1) - } - } - .padding() - } - - public var compactLeading: some View { - HStack(spacing: 4) { - Text(context.attributes.homeTeamAbbr) - .font(.caption2) - .fontWeight(.bold) - Text("\(context.state.homeScore)") - .font(.caption) - } - } - - public var compactTrailing: some View { - HStack(spacing: 4) { - Text("\(context.state.awayScore)") - .font(.caption) - Text(context.attributes.awayTeamAbbr) - .font(.caption2) - .fontWeight(.bold) - } - } - - struct TeamScoreView: View { - let name: String - let score: Int - let isWinning: Bool - - var body: some View { - VStack(spacing: 4) { - Text(name) - .font(.caption) - .fontWeight(.medium) - Text("\(score)") - .font(.title) - .fontWeight(isWinning ? .bold : .regular) - .foregroundStyle(isWinning ? .primary : .secondary) - } - } - } -} - -// MARK: - 4. Timer Activity - -/// General purpose timer Live Activity -@available(iOS 16.1, *) -public struct TimerAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var endTime: Date - public var isPaused: Bool - public var pausedTimeRemaining: TimeInterval? - - public init(endTime: Date, isPaused: Bool = false, pausedTimeRemaining: TimeInterval? = nil) { - self.endTime = endTime - self.isPaused = isPaused - self.pausedTimeRemaining = pausedTimeRemaining - } - } - - public let timerName: String - public let category: String - public let accentColor: String - - public init(timerName: String, category: String = "Timer", accentColor: String = "blue") { - self.timerName = timerName - self.category = category - self.accentColor = accentColor - } -} - -@available(iOS 16.2, *) -public struct TimerActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - var color: Color { - switch context.attributes.accentColor { - case "red": return .red - case "green": return .green - case "orange": return .orange - case "purple": return .purple - default: return .blue - } - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Label(context.attributes.category, systemImage: "timer") - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - if context.state.isPaused { - Text("PAUSED") - .font(.caption2) - .fontWeight(.bold) - .foregroundStyle(.orange) - } - } - - Text(context.state.endTime, style: .timer) - .font(.system(size: 48, weight: .thin, design: .rounded)) - .monospacedDigit() - .foregroundStyle(color) - - Text(context.attributes.timerName) - .font(.headline) - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "timer") - .foregroundStyle(color) - } - - public var compactTrailing: some View { - Text(context.state.endTime, style: .timer) - .font(.caption) - .monospacedDigit() - } - - public var minimal: some View { - Text(context.state.endTime, style: .timer) - .font(.caption2) - .monospacedDigit() - } -} - -// MARK: - 5. Workout Activity - -/// Fitness workout tracking -@available(iOS 16.1, *) -public struct WorkoutAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var elapsedTime: TimeInterval - public var calories: Int - public var heartRate: Int - public var distance: Double - public var pace: String - public var isActive: Bool - - public init(elapsedTime: TimeInterval, calories: Int, heartRate: Int, distance: Double, pace: String, isActive: Bool) { - self.elapsedTime = elapsedTime - self.calories = calories - self.heartRate = heartRate - self.distance = distance - self.pace = pace - self.isActive = isActive - } - } - - public enum WorkoutType: String, Codable { - case running = "Running" - case cycling = "Cycling" - case swimming = "Swimming" - case walking = "Walking" - case hiit = "HIIT" - - public var icon: String { - switch self { - case .running: return "figure.run" - case .cycling: return "figure.outdoor.cycle" - case .swimming: return "figure.pool.swim" - case .walking: return "figure.walk" - case .hiit: return "figure.highintensity.intervaltraining" - } - } - } - - public let workoutType: WorkoutType - public let goalType: String - public let goalValue: Double - - public init(workoutType: WorkoutType, goalType: String = "Distance", goalValue: Double = 5.0) { - self.workoutType = workoutType - self.goalType = goalType - self.goalValue = goalValue - } -} - -@available(iOS 16.2, *) -public struct WorkoutActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - private func formatTime(_ interval: TimeInterval) -> String { - let hours = Int(interval) / 3600 - let minutes = (Int(interval) % 3600) / 60 - let seconds = Int(interval) % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } - return String(format: "%d:%02d", minutes, seconds) - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Label(context.attributes.workoutType.rawValue, systemImage: context.attributes.workoutType.icon) - .font(.headline) - - Spacer() - - if context.state.isActive { - HStack(spacing: 4) { - Circle() - .fill(.green) - .frame(width: 8, height: 8) - Text("Active") - .font(.caption) - .foregroundStyle(.green) - } - } - } - - HStack(spacing: 20) { - MetricView(value: formatTime(context.state.elapsedTime), label: "Time", icon: "clock") - MetricView(value: String(format: "%.2f", context.state.distance), label: "km", icon: "location") - MetricView(value: "\(context.state.calories)", label: "Cal", icon: "flame.fill") - MetricView(value: "\(context.state.heartRate)", label: "BPM", icon: "heart.fill") - } - - HStack { - Text("Pace: \(context.state.pace)") - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - Text("Goal: \(String(format: "%.1f", context.attributes.goalValue)) km") - .font(.caption) - .foregroundStyle(.secondary) - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: context.attributes.workoutType.icon) - .foregroundStyle(.green) - } - - public var compactTrailing: some View { - Text(formatTime(context.state.elapsedTime)) - .font(.caption) - .monospacedDigit() - } - - struct MetricView: View { - let value: String - let label: String - let icon: String - - var body: some View { - VStack(spacing: 2) { - Image(systemName: icon) - .font(.caption2) - .foregroundStyle(.secondary) - Text(value) - .font(.caption) - .fontWeight(.semibold) - Text(label) - .font(.system(size: 9)) - .foregroundStyle(.secondary) - } - } - } -} - -// MARK: - 6. Music Now Playing Activity - -/// Music player Live Activity -@available(iOS 16.1, *) -public struct NowPlayingAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var trackName: String - public var artistName: String - public var albumName: String - public var progress: Double - public var duration: TimeInterval - public var isPlaying: Bool - - public init(trackName: String, artistName: String, albumName: String, progress: Double, duration: TimeInterval, isPlaying: Bool) { - self.trackName = trackName - self.artistName = artistName - self.albumName = albumName - self.progress = progress - self.duration = duration - self.isPlaying = isPlaying - } - } - - public let appName: String - - public init(appName: String = "Music") { - self.appName = appName - } -} - -@available(iOS 16.2, *) -public struct NowPlayingActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - HStack(spacing: 12) { - RoundedRectangle(cornerRadius: 8) - .fill(LinearGradient(colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing)) - .frame(width: 60, height: 60) - .overlay { - Image(systemName: "music.note") - .font(.title2) - .foregroundStyle(.white) - } - - VStack(alignment: .leading, spacing: 4) { - Text(context.state.trackName) - .font(.headline) - .lineLimit(1) - - Text(context.state.artistName) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - - GeometryReader { geo in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 2) - .fill(Color.secondary.opacity(0.3)) - - RoundedRectangle(cornerRadius: 2) - .fill(Color.pink) - .frame(width: geo.size.width * context.state.progress) - } - } - .frame(height: 4) - } - - VStack(spacing: 8) { - Image(systemName: context.state.isPlaying ? "pause.fill" : "play.fill") - .font(.title3) - - Image(systemName: "forward.fill") - .font(.caption) - .foregroundStyle(.secondary) - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: context.state.isPlaying ? "waveform" : "pause.fill") - .foregroundStyle(.pink) - } - - public var compactTrailing: some View { - Text(context.state.trackName) - .font(.caption2) - .lineLimit(1) - } -} - -// MARK: - 7. Flight Tracking Activity - -/// Flight status tracking -@available(iOS 16.1, *) -public struct FlightAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: FlightStatus - public var gate: String - public var departureTime: Date - public var arrivalTime: Date - public var delay: Int? // minutes - public var progress: Double - - public init(status: FlightStatus, gate: String, departureTime: Date, arrivalTime: Date, delay: Int? = nil, progress: Double = 0) { - self.status = status - self.gate = gate - self.departureTime = departureTime - self.arrivalTime = arrivalTime - self.delay = delay - self.progress = progress - } - } - - public enum FlightStatus: String, Codable { - case scheduled = "Scheduled" - case boarding = "Boarding" - case departed = "Departed" - case inFlight = "In Flight" - case landing = "Landing" - case arrived = "Arrived" - case delayed = "Delayed" - case cancelled = "Cancelled" - - public var color: Color { - switch self { - case .scheduled: return .blue - case .boarding: return .orange - case .departed, .inFlight: return .green - case .landing: return .purple - case .arrived: return .green - case .delayed: return .orange - case .cancelled: return .red - } - } - } - - public let flightNumber: String - public let airline: String - public let departure: String - public let arrival: String - public let departureCode: String - public let arrivalCode: String - - public init(flightNumber: String, airline: String, departure: String, arrival: String, departureCode: String, arrivalCode: String) { - self.flightNumber = flightNumber - self.airline = airline - self.departure = departure - self.arrival = arrival - self.departureCode = departureCode - self.arrivalCode = arrivalCode - } -} - -@available(iOS 16.2, *) -public struct FlightActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Text("\(context.attributes.airline) \(context.attributes.flightNumber)") - .font(.headline) - - Spacer() - - Text(context.state.status.rawValue) - .font(.caption) - .fontWeight(.semibold) - .foregroundStyle(context.state.status.color) - } - - HStack { - VStack(alignment: .leading) { - Text(context.attributes.departureCode) - .font(.title2) - .fontWeight(.bold) - Text(context.state.departureTime, format: .dateTime.hour().minute()) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - VStack { - Image(systemName: "airplane") - .rotationEffect(.degrees(90)) - - GeometryReader { geo in - ZStack(alignment: .leading) { - Rectangle() - .fill(Color.secondary.opacity(0.3)) - .frame(height: 2) - - Rectangle() - .fill(context.state.status.color) - .frame(width: geo.size.width * context.state.progress, height: 2) - } - } - .frame(height: 2) - } - .frame(maxWidth: 100) - - Spacer() - - VStack(alignment: .trailing) { - Text(context.attributes.arrivalCode) - .font(.title2) - .fontWeight(.bold) - Text(context.state.arrivalTime, format: .dateTime.hour().minute()) - .font(.caption) - .foregroundStyle(.secondary) - } - } - - HStack { - Label("Gate \(context.state.gate)", systemImage: "door.right.hand.open") - .font(.caption) - - Spacer() - - if let delay = context.state.delay, delay > 0 { - Text("+\(delay) min delay") - .font(.caption) - .foregroundStyle(.orange) - } - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "airplane") - .foregroundStyle(.blue) - } - - public var compactTrailing: some View { - Text(context.state.status.rawValue) - .font(.caption2) - } -} - -// MARK: - 8. Parking Timer Activity - -/// Parking meter countdown -@available(iOS 16.1, *) -public struct ParkingAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var expiresAt: Date - public var isExpired: Bool - public var remainingTime: TimeInterval - - public init(expiresAt: Date, isExpired: Bool = false, remainingTime: TimeInterval) { - self.expiresAt = expiresAt - self.isExpired = isExpired - self.remainingTime = remainingTime - } - } - - public let location: String - public let spotNumber: String - public let rate: String - - public init(location: String, spotNumber: String, rate: String) { - self.location = location - self.spotNumber = spotNumber - self.rate = rate - } -} - -@available(iOS 16.2, *) -public struct ParkingActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - var urgencyColor: Color { - let remaining = context.state.remainingTime - if remaining <= 0 || context.state.isExpired { return .red } - if remaining < 600 { return .orange } // Less than 10 min - return .green - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Label(context.attributes.location, systemImage: "car.fill") - .font(.headline) - - Spacer() - - Text("Spot \(context.attributes.spotNumber)") - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.secondary.opacity(0.2)) - .clipShape(Capsule()) - } - - if context.state.isExpired { - Text("EXPIRED") - .font(.title) - .fontWeight(.bold) - .foregroundStyle(.red) - } else { - Text(context.state.expiresAt, style: .timer) - .font(.system(size: 36, weight: .thin, design: .rounded)) - .monospacedDigit() - .foregroundStyle(urgencyColor) - } - - HStack { - Text(context.attributes.rate) - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - if !context.state.isExpired { - Text("Expires at \(context.state.expiresAt, format: .dateTime.hour().minute())") - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "car.fill") - .foregroundStyle(urgencyColor) - } - - public var compactTrailing: some View { - Text(context.state.expiresAt, style: .timer) - .font(.caption2) - .monospacedDigit() - } -} - -// MARK: - 9. Order Status Activity - -/// E-commerce order tracking -@available(iOS 16.1, *) -public struct OrderAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: OrderStatus - public var currentStep: Int - public var estimatedDelivery: Date - public var trackingNumber: String? - - public init(status: OrderStatus, currentStep: Int, estimatedDelivery: Date, trackingNumber: String? = nil) { - self.status = status - self.currentStep = currentStep - self.estimatedDelivery = estimatedDelivery - self.trackingNumber = trackingNumber - } - } - - public enum OrderStatus: String, Codable { - case confirmed = "Confirmed" - case processing = "Processing" - case shipped = "Shipped" - case outForDelivery = "Out for Delivery" - case delivered = "Delivered" - - public var icon: String { - switch self { - case .confirmed: return "checkmark.circle" - case .processing: return "box.truck.badge.clock" - case .shipped: return "shippingbox" - case .outForDelivery: return "truck.box" - case .delivered: return "house.fill" - } - } - } - - public let orderNumber: String - public let storeName: String - public let totalSteps: Int - - public init(orderNumber: String, storeName: String, totalSteps: Int = 5) { - self.orderNumber = orderNumber - self.storeName = storeName - self.totalSteps = totalSteps - } -} - -@available(iOS 16.2, *) -public struct OrderActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading) { - Text(context.attributes.storeName) - .font(.headline) - Text("Order #\(context.attributes.orderNumber)") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: context.state.status.icon) - .font(.title2) - .foregroundStyle(.blue) - } - - // Progress steps - HStack(spacing: 0) { - ForEach(0.. - - public init(context: ActivityViewContext) { - self.context = context - } - - private func formatDuration(_ interval: TimeInterval) -> String { - let minutes = Int(interval) / 60 - let seconds = Int(interval) % 60 - return String(format: "%d:%02d", minutes, seconds) - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(context.attributes.title) - .font(.headline) - .lineLimit(1) - - Text("by \(context.attributes.organizer)") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - VStack(alignment: .trailing) { - if context.state.hasRecording { - Label("Recording", systemImage: "record.circle") - .font(.caption2) - .foregroundStyle(.red) - } - - HStack { - Image(systemName: "person.2.fill") - Text("\(context.state.participantCount)") - } - .font(.caption) - .foregroundStyle(.secondary) - } - } - - HStack { - Label(context.state.status.rawValue, systemImage: "video.fill") - .font(.caption) - .foregroundStyle(.green) - - Spacer() - - Text(formatDuration(context.state.elapsedTime)) - .font(.headline) - .monospacedDigit() - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "video.fill") - .foregroundStyle(.green) - } - - public var compactTrailing: some View { - Text(formatDuration(context.state.elapsedTime)) - .font(.caption2) - .monospacedDigit() - } -} - -// MARK: - Live Activity Manager - -/// Comprehensive Live Activity manager -@available(iOS 16.1, *) -public final class LiveActivityManager { - public static let shared = LiveActivityManager() - - private init() {} - - /// Check if Live Activities are enabled - public var areActivitiesEnabled: Bool { - ActivityAuthorizationInfo().areActivitiesEnabled - } - - /// Start a new Live Activity - public func startActivity( - attributes: T, - contentState: T.ContentState, - pushType: PushType? = nil - ) throws -> Activity { - let content = ActivityContent(state: contentState, staleDate: nil) - return try Activity.request(attributes: attributes, content: content, pushType: pushType) - } - - /// Update an existing Live Activity - public func updateActivity( - _ activity: Activity, - with state: T.ContentState, - alertConfiguration: AlertConfiguration? = nil - ) async { - let content = ActivityContent(state: state, staleDate: nil) - await activity.update(content, alertConfiguration: alertConfiguration) - } - - /// End a Live Activity - public func endActivity( - _ activity: Activity, - with state: T.ContentState? = nil, - dismissalPolicy: ActivityUIDismissalPolicy = .default - ) async { - if let state = state { - let content = ActivityContent(state: state, staleDate: nil) - await activity.end(content, dismissalPolicy: dismissalPolicy) - } else { - await activity.end(nil, dismissalPolicy: dismissalPolicy) - } - } - - /// Get all active activities of a specific type - public func getActivities(of type: T.Type) -> [Activity] { - Activity.activities - } - - /// End all activities of a specific type - public func endAllActivities(of type: T.Type) async { - for activity in Activity.activities { - await activity.end(nil, dismissalPolicy: .immediate) - } - } -} diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift new file mode 100644 index 0000000..e5ee59b --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift @@ -0,0 +1,243 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Delivery Tracking Activity + +/// Food/Package delivery tracking Live Activity +public struct DeliveryAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: DeliveryStatus + public var estimatedArrival: Date + public var driverName: String + public var currentStep: Int + + public init(status: DeliveryStatus, estimatedArrival: Date, driverName: String, currentStep: Int) { + self.status = status + self.estimatedArrival = estimatedArrival + self.driverName = driverName + self.currentStep = currentStep + } + } + + public enum DeliveryStatus: String, Codable { + case preparing = "Preparing" + case pickedUp = "Picked Up" + case onTheWay = "On the Way" + case arriving = "Arriving" + case delivered = "Delivered" + + public var icon: String { + switch self { + case .preparing: return "takeoutbag.and.cup.and.straw" + case .pickedUp: return "bag.fill" + case .onTheWay: return "car.fill" + case .arriving: return "location.fill" + case .delivered: return "checkmark.circle.fill" + } + } + + public var color: Color { + switch self { + case .preparing: return .orange + case .pickedUp: return .blue + case .onTheWay: return .purple + case .arriving: return .green + case .delivered: return .green + } + } + } + + public let orderNumber: String + public let restaurantName: String + public let totalSteps: Int + + public init(orderNumber: String, restaurantName: String, totalSteps: Int = 4) { + self.orderNumber = orderNumber + self.restaurantName = restaurantName + self.totalSteps = totalSteps + } +} + +public struct DeliveryActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(context.attributes.restaurantName) + .font(.headline) + Text("Order #\(context.attributes.orderNumber)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: context.state.status.icon) + .font(.title2) + .foregroundStyle(context.state.status.color) + } + + // Progress bar + ProgressView(value: Double(context.state.currentStep), total: Double(context.attributes.totalSteps)) + .tint(context.state.status.color) + + HStack { + Text(context.state.status.rawValue) + .font(.caption) + .fontWeight(.medium) + + Spacer() + + Label { + Text(context.state.estimatedArrival, style: .relative) + } icon: { + Image(systemName: "clock") + } + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + } + + // Dynamic Island Views + public var compactLeading: some View { + Image(systemName: context.state.status.icon) + .foregroundStyle(context.state.status.color) + } + + public var compactTrailing: some View { + Text(context.state.estimatedArrival, style: .timer) + .font(.caption2) + .monospacedDigit() + } + + public var minimal: some View { + Image(systemName: context.state.status.icon) + .foregroundStyle(context.state.status.color) + } +} + +// MARK: - Order Status Activity + +/// E-commerce order tracking +public struct OrderAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: OrderStatus + public var currentStep: Int + public var estimatedDelivery: Date + public var trackingNumber: String? + + public init(status: OrderStatus, currentStep: Int, estimatedDelivery: Date, trackingNumber: String? = nil) { + self.status = status + self.currentStep = currentStep + self.estimatedDelivery = estimatedDelivery + self.trackingNumber = trackingNumber + } + } + + public enum OrderStatus: String, Codable { + case confirmed = "Confirmed" + case processing = "Processing" + case shipped = "Shipped" + case outForDelivery = "Out for Delivery" + case delivered = "Delivered" + + public var icon: String { + switch self { + case .confirmed: return "checkmark.circle" + case .processing: return "box.truck.badge.clock" + case .shipped: return "shippingbox" + case .outForDelivery: return "truck.box" + case .delivered: return "house.fill" + } + } + } + + public let orderNumber: String + public let storeName: String + public let totalSteps: Int + + public init(orderNumber: String, storeName: String, totalSteps: Int = 5) { + self.orderNumber = orderNumber + self.storeName = storeName + self.totalSteps = totalSteps + } +} + +public struct OrderActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading) { + Text(context.attributes.storeName) + .font(.headline) + Text("Order #\(context.attributes.orderNumber)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: context.state.status.icon) + .font(.title2) + .foregroundStyle(.blue) + } + + // Progress steps + HStack(spacing: 0) { + ForEach(0..( + _ attributes: Attributes, + state: Attributes.ContentState, + staleAfter: TimeInterval? = nil, + pushType: PushType? = nil + ) throws -> Activity { + let content = ActivityContent(state: state, staleDate: staleAfter.map { Date().addingTimeInterval($0) }) + return try Activity.request(attributes: attributes, content: content, pushType: pushType) + } + + /// Updates an activity's content. + public static func update( + _ activity: Activity, + state: Attributes.ContentState, + staleAfter: TimeInterval? = nil, + alert: AlertConfiguration? = nil + ) async { + let content = ActivityContent(state: state, staleDate: staleAfter.map { Date().addingTimeInterval($0) }) + await activity.update(content, alertConfiguration: alert) + } + + /// Ends every running activity of the given type. + public static func endAll( + _ type: Attributes.Type, + dismissalPolicy: ActivityUIDismissalPolicy = .immediate + ) async { + for activity in Activity.activities { + await activity.end(nil, dismissalPolicy: dismissalPolicy) + } + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift new file mode 100644 index 0000000..5cc2aa2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift @@ -0,0 +1,243 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Sports Score Activity + +/// Live sports score tracking +public struct SportsScoreAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var homeScore: Int + public var awayScore: Int + public var gameTime: String + public var period: String + public var isLive: Bool + public var lastEvent: String? + + public init(homeScore: Int, awayScore: Int, gameTime: String, period: String, isLive: Bool, lastEvent: String? = nil) { + self.homeScore = homeScore + self.awayScore = awayScore + self.gameTime = gameTime + self.period = period + self.isLive = isLive + self.lastEvent = lastEvent + } + } + + public let homeTeam: String + public let awayTeam: String + public let homeTeamAbbr: String + public let awayTeamAbbr: String + public let league: String + + public init(homeTeam: String, awayTeam: String, homeTeamAbbr: String, awayTeamAbbr: String, league: String) { + self.homeTeam = homeTeam + self.awayTeam = awayTeam + self.homeTeamAbbr = homeTeamAbbr + self.awayTeamAbbr = awayTeamAbbr + self.league = league + } +} + +public struct SportsActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 8) { + HStack { + Text(context.attributes.league) + .font(.caption2) + .foregroundStyle(.secondary) + + Spacer() + + if context.state.isLive { + HStack(spacing: 4) { + Circle() + .fill(.red) + .frame(width: 6, height: 6) + Text("LIVE") + .font(.caption2) + .foregroundStyle(.red) + } + } + + Text(context.state.period) + .font(.caption) + .fontWeight(.medium) + } + + HStack { + TeamScoreView( + name: context.attributes.homeTeamAbbr, + score: context.state.homeScore, + isWinning: context.state.homeScore > context.state.awayScore + ) + + Spacer() + + VStack { + Text(context.state.gameTime) + .font(.headline) + .monospacedDigit() + } + + Spacer() + + TeamScoreView( + name: context.attributes.awayTeamAbbr, + score: context.state.awayScore, + isWinning: context.state.awayScore > context.state.homeScore + ) + } + + if let event = context.state.lastEvent { + Text(event) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } + .padding() + } + + public var compactLeading: some View { + HStack(spacing: 4) { + Text(context.attributes.homeTeamAbbr) + .font(.caption2) + .fontWeight(.bold) + Text("\(context.state.homeScore)") + .font(.caption) + } + } + + public var compactTrailing: some View { + HStack(spacing: 4) { + Text("\(context.state.awayScore)") + .font(.caption) + Text(context.attributes.awayTeamAbbr) + .font(.caption2) + .fontWeight(.bold) + } + } + + struct TeamScoreView: View { + let name: String + let score: Int + let isWinning: Bool + + var body: some View { + VStack(spacing: 4) { + Text(name) + .font(.caption) + .fontWeight(.medium) + Text("\(score)") + .font(.title) + .fontWeight(isWinning ? .bold : .regular) + .foregroundStyle(isWinning ? .primary : .secondary) + } + } + } +} + +// MARK: - Music Now Playing Activity + +/// Music player Live Activity +public struct NowPlayingAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var trackName: String + public var artistName: String + public var albumName: String + public var progress: Double + public var duration: TimeInterval + public var isPlaying: Bool + + public init(trackName: String, artistName: String, albumName: String, progress: Double, duration: TimeInterval, isPlaying: Bool) { + self.trackName = trackName + self.artistName = artistName + self.albumName = albumName + self.progress = progress + self.duration = duration + self.isPlaying = isPlaying + } + } + + public let appName: String + + public init(appName: String = "Music") { + self.appName = appName + } +} + +public struct NowPlayingActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 8) + .fill(LinearGradient(colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing)) + .frame(width: 60, height: 60) + .overlay { + Image(systemName: "music.note") + .font(.title2) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(context.state.trackName) + .font(.headline) + .lineLimit(1) + + Text(context.state.artistName) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(Color.pink) + .frame(width: geo.size.width * context.state.progress) + } + } + .frame(height: 4) + } + + VStack(spacing: 8) { + Image(systemName: context.state.isPlaying ? "pause.fill" : "play.fill") + .font(.title3) + + Image(systemName: "forward.fill") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: context.state.isPlaying ? "waveform" : "pause.fill") + .foregroundStyle(.pink) + } + + public var compactTrailing: some View { + Text(context.state.trackName) + .font(.caption2) + .lineLimit(1) + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift b/Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift new file mode 100644 index 0000000..44cbbab --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift @@ -0,0 +1,116 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Meeting Activity + +/// Calendar meeting Live Activity +public struct MeetingAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: MeetingStatus + public var participantCount: Int + public var elapsedTime: TimeInterval + public var hasRecording: Bool + + public init(status: MeetingStatus, participantCount: Int, elapsedTime: TimeInterval, hasRecording: Bool = false) { + self.status = status + self.participantCount = participantCount + self.elapsedTime = elapsedTime + self.hasRecording = hasRecording + } + } + + public enum MeetingStatus: String, Codable { + case upcoming = "Starting Soon" + case inProgress = "In Progress" + case ending = "Ending Soon" + case ended = "Ended" + } + + public let title: String + public let startTime: Date + public let endTime: Date + public let organizer: String + public let meetingLink: String? + + public init(title: String, startTime: Date, endTime: Date, organizer: String, meetingLink: String? = nil) { + self.title = title + self.startTime = startTime + self.endTime = endTime + self.organizer = organizer + self.meetingLink = meetingLink + } +} + +public struct MeetingActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + private func formatDuration(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(context.attributes.title) + .font(.headline) + .lineLimit(1) + + Text("by \(context.attributes.organizer)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + VStack(alignment: .trailing) { + if context.state.hasRecording { + Label("Recording", systemImage: "record.circle") + .font(.caption2) + .foregroundStyle(.red) + } + + HStack { + Image(systemName: "person.2.fill") + Text("\(context.state.participantCount)") + } + .font(.caption) + .foregroundStyle(.secondary) + } + } + + HStack { + Label(context.state.status.rawValue, systemImage: "video.fill") + .font(.caption) + .foregroundStyle(.green) + + Spacer() + + Text(formatDuration(context.state.elapsedTime)) + .font(.headline) + .monospacedDigit() + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "video.fill") + .foregroundStyle(.green) + } + + public var compactTrailing: some View { + Text(formatDuration(context.state.elapsedTime)) + .font(.caption2) + .monospacedDigit() + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift new file mode 100644 index 0000000..a611391 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift @@ -0,0 +1,331 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Timer Activity + +/// General purpose timer Live Activity +public struct TimerAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var endTime: Date + public var isPaused: Bool + public var pausedTimeRemaining: TimeInterval? + + public init(endTime: Date, isPaused: Bool = false, pausedTimeRemaining: TimeInterval? = nil) { + self.endTime = endTime + self.isPaused = isPaused + self.pausedTimeRemaining = pausedTimeRemaining + } + } + + public let timerName: String + public let category: String + public let accentColor: String + + public init(timerName: String, category: String = "Timer", accentColor: String = "blue") { + self.timerName = timerName + self.category = category + self.accentColor = accentColor + } +} + +public struct TimerActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + var color: Color { + switch context.attributes.accentColor { + case "red": return .red + case "green": return .green + case "orange": return .orange + case "purple": return .purple + default: return .blue + } + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Label(context.attributes.category, systemImage: "timer") + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + if context.state.isPaused { + Text("PAUSED") + .font(.caption2) + .fontWeight(.bold) + .foregroundStyle(.orange) + } + } + + Text(context.state.endTime, style: .timer) + .font(.system(size: 48, weight: .thin, design: .rounded)) + .monospacedDigit() + .foregroundStyle(color) + + Text(context.attributes.timerName) + .font(.headline) + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "timer") + .foregroundStyle(color) + } + + public var compactTrailing: some View { + Text(context.state.endTime, style: .timer) + .font(.caption) + .monospacedDigit() + } + + public var minimal: some View { + Text(context.state.endTime, style: .timer) + .font(.caption2) + .monospacedDigit() + } +} + +// MARK: - Workout Activity + +/// Fitness workout tracking +public struct WorkoutAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var elapsedTime: TimeInterval + public var calories: Int + public var heartRate: Int + public var distance: Double + public var pace: String + public var isActive: Bool + + public init(elapsedTime: TimeInterval, calories: Int, heartRate: Int, distance: Double, pace: String, isActive: Bool) { + self.elapsedTime = elapsedTime + self.calories = calories + self.heartRate = heartRate + self.distance = distance + self.pace = pace + self.isActive = isActive + } + } + + public enum WorkoutType: String, Codable { + case running = "Running" + case cycling = "Cycling" + case swimming = "Swimming" + case walking = "Walking" + case hiit = "HIIT" + + public var icon: String { + switch self { + case .running: return "figure.run" + case .cycling: return "figure.outdoor.cycle" + case .swimming: return "figure.pool.swim" + case .walking: return "figure.walk" + case .hiit: return "figure.highintensity.intervaltraining" + } + } + } + + public let workoutType: WorkoutType + public let goalType: String + public let goalValue: Double + + public init(workoutType: WorkoutType, goalType: String = "Distance", goalValue: Double = 5.0) { + self.workoutType = workoutType + self.goalType = goalType + self.goalValue = goalValue + } +} + +public struct WorkoutActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + private func formatTime(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Label(context.attributes.workoutType.rawValue, systemImage: context.attributes.workoutType.icon) + .font(.headline) + + Spacer() + + if context.state.isActive { + HStack(spacing: 4) { + Circle() + .fill(.green) + .frame(width: 8, height: 8) + Text("Active") + .font(.caption) + .foregroundStyle(.green) + } + } + } + + HStack(spacing: 20) { + MetricView(value: formatTime(context.state.elapsedTime), label: "Time", icon: "clock") + MetricView(value: String(format: "%.2f", context.state.distance), label: "km", icon: "location") + MetricView(value: "\(context.state.calories)", label: "Cal", icon: "flame.fill") + MetricView(value: "\(context.state.heartRate)", label: "BPM", icon: "heart.fill") + } + + HStack { + Text("Pace: \(context.state.pace)") + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + Text("Goal: \(String(format: "%.1f", context.attributes.goalValue)) km") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: context.attributes.workoutType.icon) + .foregroundStyle(.green) + } + + public var compactTrailing: some View { + Text(formatTime(context.state.elapsedTime)) + .font(.caption) + .monospacedDigit() + } + + struct MetricView: View { + let value: String + let label: String + let icon: String + + var body: some View { + VStack(spacing: 2) { + Image(systemName: icon) + .font(.caption2) + .foregroundStyle(.secondary) + Text(value) + .font(.caption) + .fontWeight(.semibold) + Text(label) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Parking Timer Activity + +/// Parking meter countdown +public struct ParkingAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var expiresAt: Date + public var isExpired: Bool + public var remainingTime: TimeInterval + + public init(expiresAt: Date, isExpired: Bool = false, remainingTime: TimeInterval) { + self.expiresAt = expiresAt + self.isExpired = isExpired + self.remainingTime = remainingTime + } + } + + public let location: String + public let spotNumber: String + public let rate: String + + public init(location: String, spotNumber: String, rate: String) { + self.location = location + self.spotNumber = spotNumber + self.rate = rate + } +} + +public struct ParkingActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + var urgencyColor: Color { + let remaining = context.state.remainingTime + if remaining <= 0 || context.state.isExpired { return .red } + if remaining < 600 { return .orange } // Less than 10 min + return .green + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Label(context.attributes.location, systemImage: "car.fill") + .font(.headline) + + Spacer() + + Text("Spot \(context.attributes.spotNumber)") + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.secondary.opacity(0.2)) + .clipShape(Capsule()) + } + + if context.state.isExpired { + Text("EXPIRED") + .font(.title) + .fontWeight(.bold) + .foregroundStyle(.red) + } else { + Text(context.state.expiresAt, style: .timer) + .font(.system(size: 36, weight: .thin, design: .rounded)) + .monospacedDigit() + .foregroundStyle(urgencyColor) + } + + HStack { + Text(context.attributes.rate) + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + if !context.state.isExpired { + Text("Expires at \(context.state.expiresAt, format: .dateTime.hour().minute())") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "car.fill") + .foregroundStyle(urgencyColor) + } + + public var compactTrailing: some View { + Text(context.state.expiresAt, style: .timer) + .font(.caption2) + .monospacedDigit() + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift new file mode 100644 index 0000000..b167bb0 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift @@ -0,0 +1,287 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Ride Sharing Activity + +/// Uber/Lyft style ride tracking +public struct RideAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: RideStatus + public var driverName: String + public var vehicleInfo: String + public var licensePlate: String + public var eta: Date + public var driverRating: Double + + public init(status: RideStatus, driverName: String, vehicleInfo: String, licensePlate: String, eta: Date, driverRating: Double) { + self.status = status + self.driverName = driverName + self.vehicleInfo = vehicleInfo + self.licensePlate = licensePlate + self.eta = eta + self.driverRating = driverRating + } + } + + public enum RideStatus: String, Codable { + case searching = "Finding Driver" + case driverAssigned = "Driver Assigned" + case arriving = "Driver Arriving" + case inProgress = "In Progress" + case completed = "Completed" + + public var icon: String { + switch self { + case .searching: return "magnifyingglass" + case .driverAssigned: return "person.fill.checkmark" + case .arriving: return "car.fill" + case .inProgress: return "location.fill" + case .completed: return "checkmark.circle.fill" + } + } + } + + public let pickupLocation: String + public let dropoffLocation: String + public let rideType: String + + public init(pickupLocation: String, dropoffLocation: String, rideType: String = "UberX") { + self.pickupLocation = pickupLocation + self.dropoffLocation = dropoffLocation + self.rideType = rideType + } +} + +public struct RideActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(context.state.driverName) + .font(.headline) + + HStack(spacing: 4) { + Image(systemName: "star.fill") + .font(.caption2) + .foregroundStyle(.yellow) + Text(String(format: "%.1f", context.state.driverRating)) + .font(.caption) + } + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text(context.state.vehicleInfo) + .font(.caption) + Text(context.state.licensePlate) + .font(.caption) + .fontWeight(.bold) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.secondary.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + + Divider() + + HStack { + VStack(alignment: .leading) { + Label(context.attributes.pickupLocation, systemImage: "circle.fill") + .font(.caption2) + .foregroundStyle(.green) + + Label(context.attributes.dropoffLocation, systemImage: "mappin.circle.fill") + .font(.caption2) + .foregroundStyle(.red) + } + + Spacer() + + VStack(alignment: .trailing) { + Text("ETA") + .font(.caption2) + .foregroundStyle(.secondary) + Text(context.state.eta, style: .relative) + .font(.headline) + } + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "car.fill") + .foregroundStyle(.blue) + } + + public var compactTrailing: some View { + Text(context.state.eta, style: .relative) + .font(.caption2) + } +} + +// MARK: - Flight Tracking Activity + +/// Flight status tracking +public struct FlightAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: FlightStatus + public var gate: String + public var departureTime: Date + public var arrivalTime: Date + public var delay: Int? // minutes + public var progress: Double + + public init(status: FlightStatus, gate: String, departureTime: Date, arrivalTime: Date, delay: Int? = nil, progress: Double = 0) { + self.status = status + self.gate = gate + self.departureTime = departureTime + self.arrivalTime = arrivalTime + self.delay = delay + self.progress = progress + } + } + + public enum FlightStatus: String, Codable { + case scheduled = "Scheduled" + case boarding = "Boarding" + case departed = "Departed" + case inFlight = "In Flight" + case landing = "Landing" + case arrived = "Arrived" + case delayed = "Delayed" + case cancelled = "Cancelled" + + public var color: Color { + switch self { + case .scheduled: return .blue + case .boarding: return .orange + case .departed, .inFlight: return .green + case .landing: return .purple + case .arrived: return .green + case .delayed: return .orange + case .cancelled: return .red + } + } + } + + public let flightNumber: String + public let airline: String + public let departure: String + public let arrival: String + public let departureCode: String + public let arrivalCode: String + + public init(flightNumber: String, airline: String, departure: String, arrival: String, departureCode: String, arrivalCode: String) { + self.flightNumber = flightNumber + self.airline = airline + self.departure = departure + self.arrival = arrival + self.departureCode = departureCode + self.arrivalCode = arrivalCode + } +} + +public struct FlightActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Text("\(context.attributes.airline) \(context.attributes.flightNumber)") + .font(.headline) + + Spacer() + + Text(context.state.status.rawValue) + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(context.state.status.color) + } + + HStack { + VStack(alignment: .leading) { + Text(context.attributes.departureCode) + .font(.title2) + .fontWeight(.bold) + Text(context.state.departureTime, format: .dateTime.hour().minute()) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + VStack { + Image(systemName: "airplane") + .rotationEffect(.degrees(90)) + + GeometryReader { geo in + ZStack(alignment: .leading) { + Rectangle() + .fill(Color.secondary.opacity(0.3)) + .frame(height: 2) + + Rectangle() + .fill(context.state.status.color) + .frame(width: geo.size.width * context.state.progress, height: 2) + } + } + .frame(height: 2) + } + .frame(maxWidth: 100) + + Spacer() + + VStack(alignment: .trailing) { + Text(context.attributes.arrivalCode) + .font(.title2) + .fontWeight(.bold) + Text(context.state.arrivalTime, format: .dateTime.hour().minute()) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + HStack { + Label("Gate \(context.state.gate)", systemImage: "door.right.hand.open") + .font(.caption) + + Spacer() + + if let delay = context.state.delay, delay > 0 { + Text("+\(delay) min delay") + .font(.caption) + .foregroundStyle(.orange) + } + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "airplane") + .foregroundStyle(.blue) + } + + public var compactTrailing: some View { + Text(context.state.status.rawValue) + .font(.caption2) + } +} +#endif From ad44636d6815f50f63520e2c743ebf3d4c301ae4 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:14:33 +0300 Subject: [PATCH 17/59] fix(lock-screen): use accessory linear gauge in rectangular progress widget --- .../LockScreen/LockScreenSingleFamilyWidgets.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift index 0949d2a..a9118f9 100644 --- a/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift +++ b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift @@ -268,8 +268,10 @@ public struct RectangularProgressWidget: View { .foregroundStyle(.secondary) } - ProgressView(value: WidgetMath.clampedFraction(progress), total: 1.0) - .tint(.primary) + Gauge(value: WidgetMath.clampedFraction(progress)) { + EmptyView() + } + .gaugeStyle(.accessoryLinearCapacity) } } } From 4d2bfc6f00d49dfe3030d516cef78dddb20fbff4 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:14:33 +0300 Subject: [PATCH 18/59] fix(live-activities): clamp delivery progress when total steps is zero --- .../LiveActivities/DeliveryActivities.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift index e5ee59b..220be12 100644 --- a/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift +++ b/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift @@ -88,7 +88,9 @@ public struct DeliveryActivityView: View { } // Progress bar - ProgressView(value: Double(context.state.currentStep), total: Double(context.attributes.totalSteps)) + ProgressView( + value: WidgetMath.fraction(Double(context.state.currentStep), of: Double(context.attributes.totalSteps)) + ) .tint(context.state.status.color) HStack { From d2367ff77006d207bc96d1f0d63e98a78d47079c Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:14:33 +0300 Subject: [PATCH 19/59] test: run template logic tests on the main actor SwiftUI views are main-actor isolated in the iOS SDK, so the tests produced isolation warnings when built for the simulator. --- Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift b/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift index 01f1526..b85892b 100644 --- a/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift +++ b/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift @@ -2,6 +2,7 @@ import SwiftUI import Testing @testable import WidgetDevelopmentKit +@MainActor @Suite("Template logic") struct TemplateLogicTests { @Test(arguments: [ From 1154c909a1c64e2e619bc68818158c8f66f1fce4 Mon Sep 17 00:00:00 2001 From: Muhittin Camdali Date: Wed, 14 Oct 2026 04:14:34 +0300 Subject: [PATCH 20/59] docs: render README screenshots with ImageRenderer scripts/render-screenshots.sh runs an opt-in test suite on an iOS simulator that renders home screen templates, charts and lock screen widgets in light and dark mode into docs/images. The suite is skipped unless WDK_SCREENSHOT_DIR is set. --- .../ScreenshotRenderer.swift | 223 ++++++++++++++++++ docs/images/charts-dark.png | Bin 0 -> 339612 bytes docs/images/charts-light.png | Bin 0 -> 351388 bytes docs/images/home-screen-dark.png | Bin 0 -> 142222 bytes docs/images/home-screen-light.png | Bin 0 -> 135569 bytes docs/images/lock-screen-dark.png | Bin 0 -> 46208 bytes docs/images/lock-screen-light.png | Bin 0 -> 45004 bytes scripts/render-screenshots.sh | 25 ++ 8 files changed, 248 insertions(+) create mode 100644 Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift create mode 100644 docs/images/charts-dark.png create mode 100644 docs/images/charts-light.png create mode 100644 docs/images/home-screen-dark.png create mode 100644 docs/images/home-screen-light.png create mode 100644 docs/images/lock-screen-dark.png create mode 100644 docs/images/lock-screen-light.png create mode 100755 scripts/render-screenshots.sh diff --git a/Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift b/Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift new file mode 100644 index 0000000..5fea387 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift @@ -0,0 +1,223 @@ +#if os(iOS) +import Foundation +import SwiftUI +import Testing +import UIKit +import WidgetKit +@testable import WidgetDevelopmentKit + +/// Renders the README screenshots with `ImageRenderer`. +/// +/// Disabled unless `WDK_SCREENSHOT_DIR` is set; run `scripts/render-screenshots.sh`. +/// The views are rendered outside WidgetKit, so the system widget chrome +/// (container background, Lock Screen vibrancy, StandBy night mode) is +/// approximated by the frames drawn here. +@MainActor +@Suite("README screenshots", .enabled(if: ProcessInfo.processInfo.environment["WDK_SCREENSHOT_DIR"] != nil)) +struct ScreenshotRenderer { + private static let reference = TestCalendar.date(2026, 10, 14, 9, 41) + private let outputDirectory = URL( + fileURLWithPath: ProcessInfo.processInfo.environment["WDK_SCREENSHOT_DIR"] ?? NSTemporaryDirectory() + ) + + @Test(arguments: [ColorScheme.light, .dark]) + func homeScreenTemplates(scheme: ColorScheme) throws { + try render(name: "home-screen", scheme: scheme) { + Gallery(scheme: scheme) { + HStack(spacing: 16) { + SystemWidget(.small, scheme) { + WeatherWidget(temperature: 21, condition: .partlyCloudy, location: "Istanbul", high: 24, low: 15) + } + SystemWidget(.small, scheme) { + WaterTrackerWidget(currentIntake: 1_250) + } + } + SystemWidget(.medium, scheme) { + FitnessWidget(calories: 420, exercise: 24, standing: 9) + } + HStack(spacing: 16) { + SystemWidget(.small, scheme) { + CountdownWidget( + title: "Launch", + targetDate: Self.reference.addingTimeInterval(3 * 86_400 + 5 * 3_600 + 12 * 60), + referenceDate: Self.reference + ) + } + SystemWidget(.small, scheme) { + QuoteWidget(quote: "Simplicity is prerequisite for reliability.", author: "Edsger W. Dijkstra") + } + } + SystemWidget(.medium, scheme) { + HabitTrackerWidget(habits: [ + .init(name: "Read", icon: "book.fill", color: .orange, + completedDays: [true, true, false, true, true, true, true]), + .init(name: "Run", icon: "figure.run", color: .green, + completedDays: [false, true, true, false, true, false, true]), + .init(name: "Meditate", icon: "brain.head.profile", color: .purple, + completedDays: [true, true, true, true, false, true, false]) + ]) + } + } + } + } + + @Test(arguments: [ColorScheme.light, .dark]) + func charts(scheme: ColorScheme) throws { + let start = Self.reference + let series = [12.0, 18, 15, 22, 19, 27, 24, 31, 29, 35] + try render(name: "charts", scheme: scheme) { + Gallery(scheme: scheme) { + HStack(spacing: 16) { + SystemWidget(.small, scheme) { + WidgetStatsChart(title: "Revenue", value: "$12.4K", change: 8.3, sparklineData: series, accentColor: .green) + .padding() + } + SystemWidget(.small, scheme) { + WidgetProgressRing(progress: 0.68, colors: [.blue, .purple]) + .padding(24) + } + } + SystemWidget(.medium, scheme) { + WidgetLineChart( + data: series.enumerated().map { + TimeSeriesDataPoint(date: start.addingTimeInterval(Double($0.offset) * 3_600), value: $0.element) + }, + accentColor: .orange + ) + .padding() + } + SystemWidget(.medium, scheme) { + WidgetBarChart( + data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].enumerated().map { + ChartDataPoint(label: $0.element, value: [6, 9, 4, 11, 8, 3, 7][$0.offset]) + }, + accentColor: .indigo + ) + .padding() + } + } + } + } + + @Test(arguments: [ColorScheme.light, .dark]) + func lockScreen(scheme: ColorScheme) throws { + try render(name: "lock-screen", scheme: scheme) { + Gallery(scheme: scheme, background: scheme == .dark ? Color(white: 0.08) : Color(white: 0.35)) { + HStack(spacing: 20) { + AccessoryWidget(.circular) { + LockScreenBatteryWidget(level: 0.72, family: .accessoryCircular) + } + AccessoryWidget(.circular) { + LockScreenFitnessWidget(moveProgress: 0.8, exerciseProgress: 0.55, standProgress: 0.9) + } + AccessoryWidget(.circular) { + LockScreenCountdownWidget( + targetDate: Self.reference.addingTimeInterval(12 * 86_400), + eventName: "Trip", + referenceDate: Self.reference + ) + } + } + AccessoryWidget(.rectangular) { + RectangularWeatherWidget(temperature: 21, high: 24, low: 15, condition: "cloud.sun.fill", location: "Istanbul") + } + AccessoryWidget(.rectangular) { + // Linear gauges and progress views render as placeholders in ImageRenderer, + // so the README shows a text-only rectangular widget here. + RectangularStatsWidget(stats: [("Steps", "8.2K"), ("km", "6.1"), ("kcal", "420")], title: "Today") + } + } + } + } + + // MARK: - Rendering + + private func render(name: String, scheme: ColorScheme, @ViewBuilder content: () -> Content) throws { + let view = content() + .environment(\.colorScheme, scheme) + .environment(\.locale, Locale(identifier: "en_US")) + .environment(\.timeZone, TimeZone(identifier: "UTC")!) + let renderer = ImageRenderer(content: view) + renderer.scale = 2 + let image = try #require(renderer.uiImage) + let data = try #require(image.pngData()) + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + let suffix = scheme == .dark ? "dark" : "light" + try data.write(to: outputDirectory.appendingPathComponent("\(name)-\(suffix).png")) + } +} + +/// Lays out a column of widgets on a plain background. +private struct Gallery: View { + let scheme: ColorScheme + var background: Color? + @ViewBuilder let content: Content + + var body: some View { + VStack(spacing: 16) { content } + .padding(24) + .background(background ?? (scheme == .dark ? Color.black : Color(white: 0.93))) + } +} + +/// Approximates a Home Screen widget: fixed size, rounded corners, system background. +private struct SystemWidget: View { + enum Size { + case small, medium + var frame: CGSize { + switch self { + case .small: CGSize(width: 170, height: 170) + case .medium: CGSize(width: 356, height: 170) + } + } + } + + let size: Size + let scheme: ColorScheme + let content: Content + + init(_ size: Size, _ scheme: ColorScheme, @ViewBuilder content: () -> Content) { + self.size = size + self.scheme = scheme + self.content = content() + } + + var body: some View { + content + .frame(width: size.frame.width, height: size.frame.height) + .background(scheme == .dark ? Color(white: 0.11) : .white) + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) + } +} + +/// Approximates a Lock Screen accessory widget: white content on a translucent backing. +private struct AccessoryWidget: View { + enum Shape { + case circular, rectangular + var frame: CGSize { + switch self { + case .circular: CGSize(width: 72, height: 72) + case .rectangular: CGSize(width: 172, height: 76) + } + } + } + + let shape: Shape + let content: Content + + init(_ shape: Shape, @ViewBuilder content: () -> Content) { + self.shape = shape + self.content = content() + } + + var body: some View { + content + .foregroundStyle(.white) + .tint(.white) + .padding(shape == .rectangular ? 8 : 0) + .frame(width: shape.frame.width, height: shape.frame.height) + .background(.white.opacity(0.15), in: RoundedRectangle(cornerRadius: shape == .circular ? 36 : 12)) + .environment(\.colorScheme, .dark) + } +} +#endif diff --git a/docs/images/charts-dark.png b/docs/images/charts-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1784449f19c46f5ba72b952007c048abe3132c9e GIT binary patch literal 339612 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

L0i&c}0lmwkr_7wrQ=lXlM_weA}|CDcMRFQ_= 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≫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 literal 0 HcmV?d00001 diff --git a/docs/images/charts-light.png b/docs/images/charts-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d9793977ffe88fb34d9a311f505a747b199a4076 GIT binary patch literal 351388 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

7EFK1vBkRoDCXUbDU!;QS*bs)lIH8Ul5Q2 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

Mip@Sp-@s^RA|c-dUFRF-@^5 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


xi#Im%cx{@5B{THb=|)_ zS;T#e89J$IhVHl09TVFU)@zg={S8071Gq5%BByMmi??W8b-uO#$6T>kt3RRbosexf z$t?Ep+dbQlDDlfzlO<&<4k=xl5`JKymugRB`IbbHe0&a$+(;l5>YJjykUk1eZP9>$ ziJoem`(xmiXr;NhCkEzH6?9u;9IFYo@??>FgL)Y%7|`>F;m>3#V7#i#FufA)+62_H zXt_}@dQFO@q3t+j$LR-;3d|rMVHIf}6DbV`{MyqgGQqb|3!H2Z_xWV6claY?nu&g` zPJHj_9`rnRfcBWSy{VAhK47_?io$7vB`aa66m(sjm9D*FP1pFQdOl+0Fcl(MHwG#* z}Fzv2?7C13bF6?comIgox8qLXclYztt-F$T%o=kBu{T3p1H_yKQVfp}(%PENrd zATjd4v3(G7mD%0Scb=x3EICoZIMvJDV$l@BS!qh;I zdh70ciX}O(HR2O6?^T=o)&(!RKMs4#EFEmUC0b=kpB0qrR;{Eyb;eXPCwkc7`MsbJ zG^9@q@?M^$T!ktN%S!2zPi_ql)r{)6F0LcpIX5lCqRQ)E+`upAzt(=hee`31-&DjgUNFe*D z|0F&18tnr@%AiR1w$k$@jjmYPZ2*1a7t3BZ%oiJHO7TlCKg~Ttf)DKz8p=D9O}9lO zeQe8#B=~ZMqIh{qs9d~SH=D8KreocaO)l%)5bhlEbc>#<>>^j1>fXcvj+|mM=o*@m zhl?8Ku}Th4-Xlbg@J%{AyGO4ZCB)rGKMP@J87 zr|)Q7sxS5_7+PAM9@-o`w3Y_?Hs0b1!2v1=_TG2VwJ*5*7EkL0od2f*s5O4Q=Hmct z6YK`AkNz~G)SaE_GG?;?vHi z>=WDXrGVad4h4&V$4$XIQ3`DiMIqxIIyt42Uii(W7a0Y0#T^>1O3?&jQ?Ku1B2^DL)Co(w426p*~CwY70|GE=s8y z0R6yEZyKzt#iTDF$WNqhUK9CX^n2R#o7ebz(&SIcIy;n_d9O$Sa41Q)*^HQL=~ zdRKqX6PKECYXg62mv+SskfoMb!#(g}y2c(p6`>c_!^^&A@{X*Ie27@yg z33i9f-BsQUdSn~3=Z3}{!|NQimpSf58Y1L64-3NdOe;}ip{=ku+KlfFpsh`#Y8kQ}n`tjY;kf=QR*?i(TsqWD*j8LwWRIKj`q`=1xT-MbgM!Y0r8 zh_&5#s3m!uB42m#myTAQ^G>R)#!KUkGahZX75CdF{$9i@^GJ*N`eb>;ps$x4HSdgp zZbl(yC!u7bh*vH{oZ;Q?yx||Hqa%#S0xdf0D5Sf!(vI@Y-G_zsa7(%0qCxM?1>)_R zkq-?{ms}HwCu?MuLZjGDT0fJIQ@SI#C+fZF(<1ekxh1$ktB)e@IB)2ISbzIXx0999 z3sUUoey?LA!bKV1VrrlVndxP{=Qq@L&asMwtD3lxJE+op>DDy9w1xw4WgIo8Rr4iu zNvei>dF$D6Zakf|Pxu~p<_VY!F!~=B?<)0`oJ~0uBqI-|GET? z9}5zkq0YQRn!Qw$G{oCBm|sk-fO=#7wfQD)3cbuZdr(AFyoPYA_i)f9+U5Cf1!$6Q zp5XJ(Qlw}e@x`HByZ2C9v385~SP7WdeOBT^fq`?q8_GwAp|6{F%x@gBB2K%R>Gy$( z{fzwrURXR3IioZ0r*UiA*K&4O_~Fmt+0dLssoUn^d&@CCQFbyV3tQtmZZT!^h?!`G zzV*TND%AC`gA4YZj94N!2E;n7( zk&`o$rs_&f6>ynFo9~V?jrvW3?v5N7w<&QPkyxpWe?9s7qwjh7EnRQsepY$wp`9aj zrdxrhIS}A{RP)S+urnU1a~vCbNmn}gN94BMz|Q++mWXeYul1J7$UWd)GcZsfOIrCR z>%R3Py-lTr#P_^!Pg%%e_O@^C{b!&4*TvlcNc|#g#WtnrLH9qj+NHL)V*60}wlLR8 zZLj*K0P|>jwqA_PTfG+TycdYCDla97huRbc1b9Cf>&(f7OesWrO)e3<249>wT_KUT z_1-}1+j*PKS2)M%P}(Eg9A)-TSR_>X9ea@dd%!2ukA(H=Su7fJxL@t|%DIQoz_*iG zKcy&3-I0kN>wLEvOUhW40x95_3cJs*ko#`x`aNJR<@+5_9yft{jfAa`6loIDYLG0> zYh;I=%ryb=z#)-tT`^xp+bavU{J5^Y2;i)&p>#^L6?N8@Oz-)2$~+^eKhS6SHDKNx9Q}pWT&hcH%v+lIMnB%d6vDkKMyW@l-u-E z*~t5R!s1;qvk!Xjc{_eqp7@0M*Tvvf139>1Xl8C*QB{7Oq{d?OiA-ndL93Wb`_%KH z$ziuOM+gnN5*GdRY2DjiD!X>2k*X0=M;ALk;Rxm)K_Ek@OKZ@Z#stsY3Ei=vbaNk7 z%zemn5Iu9La#NIK@^Fv;VMN|DJYNA5nIZqNY|5nvqT?_tacW^wp8ezGX@wQ@HKVOX z-I3cT6luyEw;wuA%Quymo=SN&Ff#pS-OaT&v>+U(ot$@rGc0SQ%=K2bVr z!0)KvXHW3_ z$n@4p(=zSOLk1yd7JNP71!2r{1SPf~<1wdMUw2&QW%%>LKF^{$^d%-3d9)YVl!v zd|T5DHA?m*iD=&j_a|Pcju6r3SZ&HWl~MD_M#Cr`aLq>TtUhSoV+~b7R@MV}h*09k&u}PugQ{{`%W09?&HH7~ww4%9{KZ z)HBI#{2O!Xz zF#9586Q#H$%H#t_hV7sl9ivitKowtJ4YW0Zt(~&t?V{AbNMEXuAihOhfjJ`Ll2waN zsZ}JfK~+J!{pJNR5EK8m+&!R{FC#upHe0RgN2=e4A@VxR&ecjXuj)K~uGSGxbJ@JZ z-X<7(IWkG(e`msW@TW*{x>?nEBJvc%IR6RRjGH{uFXm;p;^jarBk*r|Nqg!L7cZm> z@2D5!_?RQ^b0^J%LNogfXodE?GV_b31O=jF?d>#L6#J_AlJeg3%P${D^L41C-&&i1 zEjGbB{99fcPP$79&-H-`XQ&VgLv%F?+_;L((k)y{KE3*4p69c3H?tc=?!-%Kd=LqB zSZFs~yXCibI&amdR}Q7_v^vl`Zag>%ve!di#cQfivVWS9_*Nn+`_!yA$5+sx?DXm5 zQ9@jiwj!G7%zMKxsb@-16cjI`naf06rj&9E{kiA1B_jXb*d+nzRwUnDX<(|mr9uMTkauXGvk_DLeM$uK$RbsV! zidc0oVbhUYH!wqGhSKC)kn(LZ@(0_GeRGiGBAB@O=S2**=R^CyEn@fiZ3Uu9VJ?OQ zbDtUc+H0I6*I5Ez$Q0!jh^sHzH0ZZl4UdLQ;Y%JT_5_7V79ZUs{F6B%M6=%YnxV{- zAcG@0O9vh7kKMF}530)9)*$mdBVks|i&3}1zafn?_Oc_{3rGW(o8KrdvC^Pk4_(utf_Csu?QE2lv>Elj8_WiI!+k=?R*p7Ukr^Mo^ zqagnRKl6Xwlnuo)L7#qo{qv^wRqe#5x<7Afg+UFMQSGUhHVfXc}GJI*wG7*pi`9vRek_QGrAH3|Re zmzJl+NsiNe8yTjx7nQz)Tx89?0A{sZnap@;sRimPOE9SJ!Zr)~?t<@2{ZW0dwckB1 zg7!!6fSHO<9o3qZ4Y1}k1?4SyjxrMxAj`KE#5}e|u>|AOP6*dt(^`RKsYTNA+NGbro9o2GCzX7}xS3`SBrhcuQcIf$dZV3^YQ z^?;}Y2au&V7&-N^^xof|owZH&c~28(qS=RiFSaUuz*`gBd*|Eqi%QCFaQ5l)gd6bf|$kl}RV+}#&ex=-Y4sP7s z$>yD%Wk9L?pv`w>KH=qO08-7E!qFmD#0b1Mxc__x#Bm!urhhy#|M>R14l8Ute3=iH z@6~phapavifj0z0*UnAbJRKfI6p_krG!jwxp!aF>w^Om={{!v+fT8|R4rN3;l60r; zoHm<4R#s#Kos;Sp;$e^IVt?u75vV+I;i{Dj$5O4`n$K^LX(rrpGX1I8LA~m=&DU!r zQAl~1?c&&LgZfU|^sVTPG;^;P^SHL|TyyeMIB@LKtP%2-##o4Z%F{5$&rX)}?iUS( z**fmiCl}@mMQ4)d<_n+$?q-N*@+9o9o`qS2tYkg|qq8nFsrkyFp2XTmQ&-h}H~gYuaEh>e(W+F@GJfS^X)c3t$wMJ~nEV0zE;k!gZz zPcG)Ze4FxFgW6w(qGOq0O1yns*hEowj$f*MTV~w?A`U=7{ft9l>?XsHsy?sBde4eX2s)_KfJCZkA;M8J3bWK$e8Gf4svy0Xw}o8 zo>u9r=;VR!J74_`Yne88=EUO{I%BVEPIrp8UyUX+gbyfFA;BPm9BJ)KeN1ZTW3@-z zNPW7AZvwmB`I(k9EhEO{HVd+a?XiCddO^;rNuMwjqFo_{VScTba`%Yi3tyYot#Abr zoS$%oMX{&)N^CznTB{rVFVU@Lj=+GnOw7W`XxMtj)^F@h+VtcrUj=9`@J)Xic?Ws<#m-`? zQ@_WFo))IP{NweQ#O46*wV&WvW?fb(M^U1LZa-g(V4xjKd=r41K@RZ`yu#JyX@3;O& z@UFHe$aT4nooN~_iv2zOeXP4K)>A}R1dBFK*}uKtZ!I(W=lI$Ih@@A;%dyLN1&66< zUAq}8#EjiXz7Xw!7;fXJB3$(G`;@iKnka)>I`MP{kIs2WM|CQQyITTww}`g^n5djj z9ju_R#|2E5Q zXbR`5L7$HW8&J;8J8?J?N&lbX7?Z53wZ%$(cDY+tKC z52qFW^<&PT$%_2z4SW)p>Tk{3yV_S30v|6;@r>uim5C%#h1;lh?Qh`48ogk$f9#Ie zuite#?DCX@ga=;)92J$fT~#w3#trvMuLNq0)0$}fb(`No&|X=+l_PQ%7r>FPnab>p z6yjq}r2Y>+?VN6s`(yT*X-(I)hVZ3=^b5%5o};a=)Nrr2zu3J9=#5}A>(Y0rCHPN^ zw;(zn2Zl&0bl43yc~>AFfNP6vbzOz!w#O?HXnfL<=0C=-kZ|LHf3$gC+0cD?C!D=U zIF@}-J?QgXIr$z+dBrCi8NU;YeD z{mR=Bz|Eb>FyW(nO;>)}4zDpC7^)35I8n@d|G9gsS5{i5^Paok`oyn@Q*5KQ_%Cg0 z&#OETt}j{=CifLtq%Q>dUa2IFf59mFX{?{6@`10+s73faP$psdg_Zb_Wm{y;+9#Mc zeWK{X{&F23Z+@@3Uk0l5c6&}0iBqqS? z4i>tz2Sz=`-mBd%|IDjFPmRd`G+vLlvzDSfUM;oTsqFBE($WEql3#ps8PHqPP45H* zQfb9gNcgXTgfQ(X%U&`O!E>aW?zbXGW-8OMF$aSVv$Ka7TyNNM*C73!husoEO z!$?GkQ2-W>eeUw!SCf20=X}FFjtyJJ5sIs>V^0WR2nN} zcMSi-6M7M^wf%WJ8o#;BowOOV!kWtbrd=tRtjQOZxGmdDEHZhxW7?*21Erw6ab>&H zOThpY;#~dVXppwmBAH1EuyrVl4@>DJ#w2G=2oZv$KVf^e8V1`LYQFmec|+=hl>JA) zNp(0tL!32AP!*N0zPPNDJhx`KqvprtE87pN2l1H!V^q^8%LJ`ITz<@TWgX-Q>Cc{;al7b6cZ7t&eNKALSp~38xQjqjSb8&^^IfpqboMMf8%OWt-=;~ zUr{Ke>eSc6bl+Q)dyZWI>Jc>8y!I7YBmTStHJ36iX%>ewTnOiZ2uwm_O;K*DIHUp-~g?Us=^v-A4vtRPx3^_dyd5z zWPDkdt8~<h9W^%jm|{%liI+Abk5gVcMqA}px=5XHuJw(1 z;aJ^6Y9mj4>mZFJB%29oD|Bu%N}`aWUN_ltosN>FoJa#~(O%H>Z_lVOkJ{qVkIb&| zk`&U(g%+>L!u$`k1om3b7(n`;Ue|E;(K4RL+@*S%CW=8=0NbtF3;BhKIr}RCI-b&I7jM%h*ToJm{e=UE?YfwIu;#7u`jk z_^D>5rz{KsftRfwwz@B0H`x^xkkIKSwCkf@Tsx1-*GqL)+bKiU?)5k=8`wv%vxB8Qif6gOd@JqIzI)+`9h~gPTv@I6?DTEHmHd#> z^i7UxXR?;_Ut)|{Jw2Vvw%_Ak@^kY{_bK)w`{OkOBCH@7Bn53JGJ)k z{_dZOYpo5QvM7(Eof^d}zLGKsl^GFc1gZoZoih#^7LwQ0{UIWhR9ksVrcGsX>wJsYwCxr8yIPFSD!)}lc zwfMMGcK6&B#=0c=C^2qZt!g1|MA7s?9nQo)^L(4(bV~6t$n+DnW6J5GUm9FgFg5qk zzY1;o1T_|&_tl5w{SF#SfGzU4(YiXm*k6XJD4(MO2g$45Lsf{Epmqdv+*cmI*Zqyc zeUg3#*}PM28teI3GvZ6qIP#8}bM+lz#<`6gD6PnwD=3P+6Mytzedp;X|4z*ZX&vSP z+0yT{HwGJ}DvVC;g-6Rthrf^GH^M(3F%gJZpI#T^IICJ$s?4vlQR`UzcrlyW*4G_~e$Y4MoCq&cgtuojdKr7{2|03QaCJnV*k>Od1EM!E zh>!i|lyT*#ylqt;wfOO%A`ErX~P^?HcP4k~gmmbP;IhrNb)SIoTQSwVt7mgLF2b0MybVwUZMGfZM%( zMSRFo?$SQ&-d%T)(wl*{WtEG?wa#@dRIrzAkK2mklkE zltKPbyJNOwBiyTDBp}0GUw7|6mvEwZ|8DWHFI(M8qhj)>-scGNc^l6;1!}uZ9Yi`n z2_W6~z#7>Wvsr>~fE-~uP|zbv9W=6!bb1HulR@pZYRrLyN~?aSfu36v87}3>oj!PE z{NxHwu2!6>Hp`vloMA!?d!cpM1m=Ln%x#f*Aa=_(6kpTw;_d-QimzeIpi>{&0T|!~ zytj_h!|{K`BR7!WG&luLp!&b*x!;Wt;N|rqNF1$w6D<%7{^1cUBD_2`b1zQ4>LHk9 zZ$Qr9a|v{JSueJOFFy0Lii1ce>8DY_|Ksbu4X5{ihGB1(}GItoZ=(gFksp$7u#Wilt;=Xu`uIp=-v z`!oM0naQ4+wb#0?@7fy`8gg(i?qj0=ZCYoi&7^cDSmSlOEyCS8Ok-D>-pm&nFW6P# zwoL5(Zk_s$E0R-Jhz#A#ui70t%LwOal{>-i*FY}ZS4n=2eA0J&$aS*s?6-Y=S2-v@ zY#^dvy|OR|dHxsbnUfy*C;VBe^RV>Sg5`*^C~~msj~%Bdl_K}&5aL_3TDaaEETaP| zt<Wd@oQJry#*3VW-zxC<#Q7 z?>xmL)x_uD-qSXD;ATL*L}`)20gpR1XIenpd4QfGnnMkNnE&9!d1))Q!QmpH+fSj) z-12SxBe~_ic4H&wvb2^aQ}uvPoi)}-_8zkfh}Otf3rqM+({TsP(svD&c7_Xc39C3e zyW+||FwI$GYx42DULqmn4I}?OjvBeY^24DDBS~A;A=!NDZD7sNW#LnAOI|e(pGbMs zyobG+8@ejSF3S>l$oX~Ttm4n~C%0`cJ$%93A-yr^wHFT4(4JHIm`45OTw5~eZq2Y& zo=cR!@4tbEUI~v3Sm=-~q!>AM`J7|I;0eKKw9PGqUZd4T}QE&Xm`Ml(ieDCf@0O2&!sf2w0 zhS%2Kye366>(u&sGnJ`ypM?u3>iXlN;-~rYIXg$n^w(N~Ipk&0YOr(PqA&X3SM49I z4i8rJb`KgtUR)0^I~i;Vr3%6&V+89j{Ac2H@%rEN&*Bl5cI4*Nuq0chT*J(-?*B}7 ziPnhJi@PegN`5gDsNS^L>X$5haLJt>mEDW)x=Q@B$TjItJ4+(x#EwhqyiZq+@Tca0 zJgBqcmVE5ZiaZ{vjQNL2hy0rU#;_((;o=-^2N}hu3PbD9NXU{*rP1)oj2IG=XUhuq zGMV#wp8geq>0uM+0AVhCUuw#OwNNDfO-%IcTnS@)+!#bD{X08+vRwUin6X~(|F#LM zzdT(AGRlRi@0R7!yoG0#DzsdM+CNle38C-tW#NO{wNm+{CH}`<)gOGSancvBq!%<8 z`UihAro)n3t|DMC>}C#i738#Sa_`^WoeYQfvnc}DW#xr6ZT-UrtLD*f%V z0_i96aXYJa?6s4he+X3sM=&M7OZ*4=cFrLiZp-N#1n7el`Ln_O#`%j{(jZcYn&MlkVK6n!|6K zW{j8}lAgQ~a8Oro={&H>p6b;mrz|ow`M9sw4{n`9VyHOIC~+?((a!a!Et(|0Z^3Qc zk-8_d7Sn&RM?vY!3Af`D`S(`VICJfd&zE`aQ_P#Ot1IzbDlhOi?s%c!|AIN{opn@1 zTh86Y-~0lpFzF|`$598AslcH6n4kc2QK|qbXiloW2%T*@1SMCuJx%%yl-}e6e|?*N z=mwM|Mq)}@K-ySPZY~{^{1Yh6)cOy)qt}|hf}>>i&Tc>Q_W~1H5b=3IFr11Zh*_B3 zKF2Y8ZX&xbEmH&J&K@>lQ4H2HBC20cO{uIJY%~7AO6oofrkdTcBSzfb)YNC|w=9aj zAA}Vs9}gbSdMu=Af(E9>XGmbFs7< z=jcko9Q^sK>Xy)uinDV-9H0)=gqpuWPy8zVDe)Y}gj-qN1lK*4*SUv|=6n1M|Cmw; zMT10|V9E!*>0b{{G$p(MUaZ^?-Gp$UJg?^BJb>u{i5Y4C?RlZLDkZuaWL&;^E`dk* zZ=a|7#yb5irsI(N>goWwfZ(Ehb>qZvKY@ zBs;JBV0kMJhm$V}q}S&o&xTVI4;pMzQ86lc;{8jL4NatksmV)sdXM^!#NCmx2!Y=A zZp-%F5EfS{{wPHRw>wXsQ0BO3sRt9{S$=wZN(r0oXv}SILKdfA)I5kVPrIe^7f8a2>;L~uNjCOJ4k)Q7KzW5cI zoGk01g~54R=la&oaKGG_X6~^Ty*Sb28x?nHbjQB{kpJPG4r(Z}CTixkXv4Y3-fUiLi7>Fdnbqk(U`*fKua{{^BwoTbPU|<#?7mV*&3Y!VEcs?uaZ$ zN`C{Pbdw|;j&yM45P|~aY#9|FTW*9gPP0wq@=W^dZ zIAZkLb~dFd%~$;;ABVUpP4%0>d5#b}RG-gkn}@A;tTXXf8BVWqRqfTvgXh~_BMENx zku>M$+T6*$X@HNx9L(Q!*Byw9bu8AGL!KLXyRaKd61q7xNE@WHWV1+GM6FFd)Nf-= z@S0vMt4s%OKoL$`${;l5Oo4~&;|wXuTU03dPnqN8sOsd=0~7WoZAv3g6oO$e?+2?3 zNMG{{DY@0m4Qwaw@*xc~5$vAE2{Z2N+pG^m?V2Oi>Os9zD8?W{biXG;bW#nrtsCwH zy+@~+CaMR;C?_`gIkZ8~L_F5j&6qEM;LPqgZi$FgvhTKbk)7rExbS)gza52vT1b6&vRwhD6! zzvxbz)4OFUCC#0lz`{Cjf)2n#ni?<~JT!#Cwa9`rBsjbFbJ5>OJqW)vNgIQ}D%f!D zB&;9W2p76EITN|KCib!lw_OHjo=Y^xQQ+u}79Rrcq>eoBf;s)SQ3@|$|9qVdK4 zCNWF*n(mG}7<`kJ%Rbi;f~SzU0x5g$NW)dEGAI#<449`y{SJrIGYE`QJMe&Q_~7`GPD!4R3+VAdNyT&E4I)2_IUz{S#Xr zyxP);w!NYP9|_!wf)BE1Bl9qO?+S!rUJ=Yxu9-Ju^u^?XY}{i*C5<(5}(*l2_heR75QHSiU3C zS5Wj1wBY@%zwNa=a^w>0@Ku%4VrE#;928?$XxFGDL-`yu#K8h1@8wvIZ^*ZOzU@@I zlpfn?P^L)Y6PR)Yl~v+p*twRckg69;Z^Ul5IAspFryXGoxdX142;>IndPum;lQE4I-G&G&sEO;flX?-~Soe{s-D-thq9EG4gkdTSl>>H@j(cI#zX6--TuN3w<_# z7N&wANKW8ne6j6ox)(gss8HSp+h$yltq~d&Ht9d|r?2fW?AZmPZ<5#zA&O4@&E0R` zSho~kcT53xl|!1feYEHTGm+WuIhwt08ad?R(4TT2I?2D;Hw-vLpYX_vYlqH1@}J6& zX*vvMk`57X8PknB$P==qOmj4ez|+8v62p6v%dpBj>*wQ?5;wAFFQ>h-2zB|$T{Aqf zpvzg$|8iC4b*Wbd`cUmnzc>!k<1T%dhl$PQG=bXXr|I>A0pGr)^$=zD)|&_W1sNIwU3h48>Pv3^ z+?PIjsHEAnvGk@!6JDis<|Yp#%Vr@Tq+dVW0eEC`falP6$Bn2KZ&BvKi)skIVlzkb z(fDUXTM93~o_YhaB5W5!zxDqz#{af>vVB;6m7C5It7W7q-EKm!jC*I67Z=X5t9e7( zCEde)1Y8V&1*r;WMqKSoRHXV->TkwLip1)Nn~{TMCMs3&TN+O&mQa>1kw2sggfcAY zd=)Dm$B7G%{yX3+JO^Tf*`;}12<8=v=21v>URS-=K-@73tUDWp{z}@Fxt@7o*Tw5u zE;Y7<8U*DHKLs;v5f#D0^I2=XC@rdp(V9`@ZO&`^lgw-NFr{}d0>>WZ013mDFXwDX z?ZoNE{I6-3{N1lt_K^iZk*zDRN1XO$c3;RDp1Js+UeqwTBb~uEb?P6Dm4&-;$6^nb z^-T!PH8k17$Mh-Dw~4V!3C#=ZbzWj}gO}W8E9&mX@*=w9=Au6cFy0C}t?m^owOQ|N z@7Hr11(}+&HKIg+$2jh8u&lz?SG*s~O=*sr4E`1&Z6z)}jttG_8&dL*Qti6Dx>K4S zr15Sy1s|;H%~q4F$aB+|{WT%~E_Qo7OdUuBts&$@YHBdD2{b z!tSA$_4`24Zt(nG3hr%ddtclP6SBkR|G2)i??}dlrRD2lcFznWZYf^)kgF)ZU@o#-V?Rg2-o*+{y z6w3E>H~Q11?@xtTGQL7i0g#?-cMh9fRINZK_9GmYV$52h`88EWH2Tzk8A)hX3TU(? zmfkId?qgg-!|V&Q|3Qx=F_XN#s0`n~>1Z%=JGm>Cw82O>RyH6FRifaD=(H>ZfJ`DhqwpP8PqDDv3WR3{{$>?)Q4a28-^UxxU!^RsOfUS zKr_`kujKn+VD+ZB4`qzI#;>(avMV-okKsP%67GG8RAKE#7glVx7_-1LTsoR332Rqw z4NBo8`c9o??#0XAlZ(=~t;oQ>VEo>Si+ckWQw(IRF;~u3X~hFvU|Ub(>D@{%U!P_P zbLrQ!M)Pju`+M>=%6~__;&{g66R#{69OVnaro4e!T;6S>5{><+@eah>#0~>0Sz5B?};r3K*gNW`@){`_#7(TBn}SyXX9f!m7ciGuPr+bpIgD)5GsX7uvXGPC2NTs`(!H z+i-(+ZH6>|n@zQX8bz~!{!v5lP~P2P_LxyE`d9dI`0kgv{wE$(I)9aJLKi|ekT+2G zw8x-bLM88?1UsSKM{Va`siI^nO!RgR_&DeApOT?Xio0DrRR#>?oXMj-dswN2yHWB# za_2ne*u+lTUBU%w{_5%Q-bsa9re^KO%qC%@PcD?5fklOmWJH*Mv@lq7B*B+9kjASc zOq0c0ZwAf>ZI2AzYk|+&4f`@<7qS;O{=xYH`_8OMi#c-l_pS#?k2i)f+C}ofK#AMY&uN-o>X=9I&BKt z!nMWSNG2Jkiaa@&qf{$T$&jzR%o|w}W|BVgF7eT?J$|GR#B|wPgwF!3ws zqJF?1HEQ6M8RhruKdZT^2xVeXDEk&uHfm&CUyy|`DKgt|==#fIdTZHmr3{gP- z0>Wj^r|qo)`mi+jC4v++6b;Fkix@*Xcx*Oc&$deK&JEHAVAQaa=ivRp@d^I!G! z-@0_OTHacr$H3?}sw}3ew&+it-|p&=20VrETZa8O!sin+Vcm5``t^>XFa#I485oT+a(Yzta8hPl&pSMWg{o7b$sIyrL0 z`dyRVWGcC!zwI`GF`_dISi2irH)?m>82}RW?UZC2Jg*K6ABdrC3(~!bft!WS*oIu$hw*n|mD;<4AXQo3jRb8Du-s)P>ThYu>GT$L!KTsJ-~>^N zu@F0^e*UxS`2$jH`ebm$O61GMGl7>wYroXOxlu0=FMG^2yB>t>l@VormDjdX7&p_b zU+m-Ky0eWJyKTqRJG8Ar(!?;&f#<2s+>55%|C6-fnw*~aFJR>$?C%aK$c-1m)a9`6 z2z8$qEW?MjRs`=k$Xs}5#$vDlF7N_jxJ)3ao@-Kg*#;kYo3F90FrTA~v>ee9^}Uu!!X z(2b_iFsjdPZJqdnr%I~|}O}GPQpKK@8V@p*3LyO%J`J~@ITqX1HA*pukbr<~=j~3O%i&W)$Y!A&F{^H^N zgPMmf`N4O95v4@h*ViWPoi&%y!E58E;1#?1dN5ZGGYa$NF%z^^-kt{|M z`e}B3$FD=o1@ukcgkYv7H&e%@u#ioLMu#->^(ZnuuUfhE{R*TYa}cujvxje&da~m*!lp>FnX1wP-@AY-#GqyZ0nJI z=o?_XyQzkUurscuNUGbe7{sABnxSU>+{|RELW#)yRip;zrGgDDE@AI+?mMeDW{=S5vj0iT4FdnD^3KmJd|i}cLPZl%KxNjWXmLhRgO2FV0dQdy z6@|f>=0SVj28TSoZP|KGxKqXneEv`gy@L{MX)6z5--!;BGacMt2KL=4RDKn5YXS+? zrZ!y?xg-3m-dDsR*_}Zq$SK&&8)w#ZA;d|uwOAF&?BOPfM6rY&eSSKnR9>0Ire*87c_hSdqY`Mx* z4)Cgl{!#v}qe@VFRT@R^#bEk+#8sL;`7(U`S^BXFTbSEtz1vsq8*@MJV|9L&K60p$ zE!d~Orw~%}b`1$9)ncL{U7LPX1JtCrkuG0h7BXneE6+7pv|DkJ>T-)>%E5#J zTuU@I+p$F^a?0MrY+3Nx4nlh*xrXN zEZ>hA*u>8Cz>XJH6{a(ERY*)tHINII4`TDxgU=EDUAsU1Rqq;PcRB&gx6DWcpYz?& z^OPWRoA>~a{&nG2P(PGl63b6fAwRTl^!w~C@un@&@y{4xXwJg`U&sy8^tn+Hww)#C zk;HuBzrbpsS30Ib7e;N+;2tNbGYb%OhR`Ol_+^n5+lKo z#AsPl-cVt2_3pW2>pDy3;+MxFMr!r|ElLW zPsJ&etii~ftc67I7795I?7H&0g?TF^S2Mj*dqpt%iHtU0HrJ!F^KS3;GAgd8So>q| zyye9&G6X$$5G1JoA$>=l1CBG)>+B=>Yt`(3Pmh*Ald3*cK6+kgv9{E2?LqtB8_r7I zu&U19_H=a};TrHnugFK%;HX7dElw4`2R$e-Q$@ueW^%CgGiELEO}BFPU9 z4P2@{Ifwm=({t8rom1CrAUwpYhhDjm$3-uZ2is_2am*itcGGcutn7I@zzaGza&+lm zKntJjgFQpg;sa$+3kWq;{r`HxQoUzkgl6pJ#OjT4DmcV(eJ1T~0Mb&<@q5*GCDu`; z_!-oM{WU^!Wlx3omYvsX$$QP|LCv5UF!YyHr7FuItvCK8ch52@Ob6HRhe5f2>+bZpS&2!jA1EdF@4)(<{$c_f0A)sl>%AtXcWZj9`4lS6o;% zojVnYpmAs45GFM)Zjlv&aJke)=h?4>Nj6 z5>lUAliy7&ec-Px#ZuGefMVNjG;fC($`bp{VsDa=Ek+vn$;huzFF`*V8xNB4gm^J& z5E_?a(n*bAX1_Z_*wl#koB1Cm?`q1Nyft1SUTCWvzE5N{!9L8;o6wvSd~Vw)oF zg@E%UN>;lErr5W`)_qrJp#PIUp{@Z#u-?!1MwzJ}U&uaK#@UjOq!!ZX=rN^k6D zocXED>D<_hk~`TRGZL19(Ugb}!yq>Lhxj_1D9D(O z4PRUz=I0bvZ!+<ixtm!-TAHLcaLiaH1v<0p;xdPYv10wfG zTB~%qNj$Ma0QBcij4BNN4Oq0Bzy~n5m^e(@n)G1~`eS+-!$0VyCr+A!g0MtEb=wW7 z#O+}z8&w9~G?LT#K2%M62C{@^^H50xNP6Yl`CkNeMc9sWPZ(j>ULHF;AilX2uwOp$ zTs!7sqRTm?U;dI>#v0h~p@1WDgQmKpz4}^Ny(Kwed00%ULiwdIwDj9r?Nhs1A#d}L zs@eqW)VKbgVS*d#2fa9%$akk(`?w|_NUjH7HIY_C(;ebn3^shmult3)i0#Jx`BouZ zvqeO5d{}+fV(YHG*dI6JDjSK73s(MVlIfV>PvY<9LYJ9O3Qco058<4}k#!8SY>nS} zmb*u`9#UQQ6K0sez}uL$HXp;j@0g* zTSRrHjywfchx1*d>&|ne#XDeWqmtm{wImBk7P&k|PjLG0B;aPWTYUcZ+}oj&LV&Nv zTd`M69)$ATEjnz@EqtvE=ikA89b&(b$;`IpL<+lPw|$a@vCKjBNtncHqh*u9mlF-r<{^#k~>)2x+E<+Aa?3Rb>18(#}m&cP_43ziRL@?!~yG z(oRd2T%zNr@~HGp!`FewoX)1t%qMF`zb=l_6-*Ak*Op}0cPSJH?_bgl%Zh4L$hWG% z@9&AQw|#AKd}UH?@0^A4v>p;J-Z0&ZLpmQ9*-qP2C%q`)40`efXlF+)#^*%ERoY30 z7jPw->qV9oXSYckEhPrv=qTy~?XQ{0oWjT;tbHd&Vlc)Pp;V0!&K7^`)cB$FVrUL{ zKK=p_jH7OGwrr+Np*w)mbOpIZv)MQPITf$=!Hn`E)8CvjX^)KXeOufK7yd}6Ci^(L zhKGJahASs=%&{FPHq5zNZ)rK{Yg=?P-kEaxCJ5dp#Y9>evtka9ZDg)J+#Cf)L)3B1 zmr57%Uf%Un;#JDyuJ$sKQ^Dq_I62=tnnH}}x@TtG%oWrTXP2(o@MM6EYYv`;PjUYBhbkME` zamZ7+njh+(R{r@2uHPJz&q{H^S3%6Bs|*-bn=qjWV-2POC=*@V0b z^hLM$D{O8FG5XBZWL{~DlcNh|bZ?7vqoLz@#0LSn#s4DDg5;xMGvXP!y?OgfSsNu3 z&wN*`Tn6<^B-{8vF}w_NBi|3ADaJB
4xblElv<9jKMj@(npB7kq=lA^5Uo5Oj-6 zzVP3BF3wfn>3YzA714t^2D$KU+cA9~gWvqJ%t&s2clrwR8GXki-xDj$4R$z$o;KK1 zzE|_wi=41N&;81)pB|2ARV&)CKSm+yk98+|?FNUib(^a>##8um`}yf>T?Ui*9cASd zDn73ZIq&Q-H0vCKo&6D6+3PpVt|q5&$f1*a3)MJ*QZ76=C? zbBmiMKEdLeL%F)sG+L1f=k{sb0^CH4DH#SmOcU6C)9S_KeRS+Kp2Vjx1daD8>t9iJ zE#$kD5$*7Nfaq_8BMMC^%nyjUZ{T#a2M2{VrSx_XzFk7jn!Q7{q3zmGXJr#+svqGN zm@5V(i9@+Gnlv5M8q9BDWCr{a@cIbc(qGx7CMkrwASFGK+e7ZdR6cVhJgv45tM+TL z$X!q+8?vmpbryRuq+!!iJUgPcXqqk7PGThujVHB0GxpRd;V?WTFQ1SD&REccJ^B`d z+Of}Q*AZ`L?19gskiHI|`sIJv{#^d=Mi1(wkUKOFRXLYS^^rWMG0~vu0!fTny6Guc zHT;nyZQjCA6$~!fiZ221Y4%>*N+;pG=sS}gs<{LeORzoNZVL5OJlt5U9erVl+QGF z{d4sq?y{T42WyTUC!AJUot})Kk)539st#kyp!$IgtDHC9b=96dtfa>LUa@wjN&Im2 z<6Wx~iVNz3#V$zju01!&q45&@@pjpM6`s8z?$Dq}EW5AwpuLs$9Fh(4=R#;{Rp~2* zoO`g-T$ejvBi=o83n*B!mPUb?psnr(rEj1cza?wKvsW6IfMYxbB4UvO$^_Xq@>w`1 z-5J<{Px!VlPN9%duU3(;+ojQ!GbJ9A_IQAsJwQIv+cnT%E@hlZWx*Qt#2DGVLP|cP z5dY9|j55)3Asmxe+71!f4;S+rmRE!;0vn+tc?Th3^qB^^#S=aqfmHGHCii;R9!&l( z*;<##9?WMTYuMb_Ijiyr_|%B$7e^!pG0$?PbVn%(C8^`{CWCPB!Jx21X#om4gHVu| z50`gv9$&TMd z-u|t3D)I!>qqdn_v+Ds&qX`l30_$Y{f!*_*HAd!xxdX%G`?GgPv;7BZVZl0GtfXUS zlr4qtnt7v6&MqxjDMr$*<;**q7I2kJ|6%efZ+MsIh~`qiq^N~&`iA{!`Z1XQu9fn5 zJ4?dZ*8*>gG+t+V03HQJeT!(#O+wm0quYJGzXCeoaOYw#*7%ZXFYj*fh|#K$Z=)Cw zf$Bt#n}mEHngp!|2OkEsVOTkq9CO?Tu;jF4QtAi&0z~9Lu}}Rr)4ruYIsyHoe+NUS z{loR2nPH)?f)v4jA1GMNPro-b=WIz5N)n+iOq>iNQWO%WjjJPA>N2eh)Z5Nl5=FA} zcLa^Tp_POVMOZlh93!QFq^0(8GwPis7%ketWZ<4~i|;a)?~imy7)q&s3TwQG(CBTa zS@%e9-Yy^B%x0M_2L~4XQF2%GIADO71#!H;gE-=h@RBP(17c?(T5)v;5g_8*Tfk?z7 zjj9Y<9->fP%q1=BR2PZVncGNbpdI)oJ71S=H|tQpF9&wlUXMP0LpOQM#q|Je_18QT zeS!X}=Z2UD<6bKmOwE*VH{J1kAMN-LO%6pj{aN~Kb}YB^tf3*&KZE=vEf~B~Qlf0) zh@QJ5W`JE?nqVSYD z3&|z84Py21Q^3@OjDe0U(u`W%CvET5&>O}>F-tT=U~g&LjYBVCNm5Z#4eAq3STSS& zx@1Y364(M_A7XWCk}gVWgV;|RfhDYSV2!ORh!s=|CPU3kR$y*Q0-!6k*i+;W`kv4E z8WtSN&b9+i&el1!NnL49wm{BedCfjLJ{C5+b@oO!PI_UzV0B(OD7mjxg)Sj!l-=Iss=UWq;5 zUR&6~leBa}g2L@67cMDgNxfj&Qsq=Ldh1yA3Fn_)G}c?G-9_6rOqGRe`J2@3;cbi8 zRihs4S>E5dVcYkER=TvE6Qebm|1_Icsi15MbvTVd1?aZA2d>VZ`95iWKXVcaA|Gs> z?4?VYE|&9K$eIxmpC-(9_TwC;&=JG<8yG&{2D=^Aj=cupq)!LcF}MqgNBLCuV&Dzg zO68P_iXoRY6I?Ew{YYD>ib<<-mhf#vB|^w4mW;EMI*g{*LXL5KnJIy+Lwbaag4F1s zky8J~)%rt8i$pPscCWsw%+ZRT9neD0J{S32>6@CU|E1bid;yo5&lgL1Kx@^V^d~O?*9-;RC+$c7W{Ecu+xTfrg zIJb;*a&{Dbwh;-?I9qn{vYee;F=Xk$5+*m#%xs(0&ddW&HIEg8cMsVxv2{8V6esyp z+fHUZ`pnj)?BvV_t_snaT#}aqJz~ngoftIH=dveCX+?vO)pWolX<&T0P*f*In`(&Z z;v2M;FTf?>$DNQ?vcciGqE>3vc^=zJOtOZ8DU!j3m8}8ANxS+NKnp(Z511!T&Kvd; z6LC-arBqz1aVAY)SOlsbvJG4fJqopBW9KKcWW9qF5QRGUO2<|w2Vjau`SBc}$vw*0 zdG>J?GVnOO^!H>d!~sOWqMwM|i;h!}d>84l&Nyi*VxC~_!9SdKc*t!x1V~$Sw32ZA z8$32GQTqJBi-8qyq#B+YgK3*1oizi*EiU;E4sRUEgw<9+YB96 zC+w?%?(7CVRfB4zb4$-TLkschg|qIx+Fc!wt|;$Al5Ewg-7xDteT)hR(ihWWp%%7mlU4Kdv%ZJ-fs=oqC zepbDMw=RTcP)l~I*o@@wVr;RTrZUVJHqua0#F4-^)|ab-wuGL=EtgW|Iy7XmCuW#j zbQ$Qo}s0*l^k--{F{jX1?p3sU44j`Wy^%n!UqHwSB)qI9LObgM|DcdcoU zKhbXpJ)&f!lT@wF{$x5=I^0LDNEgfUmw(N{2B$~mR>9Bd7%Ntm;jn}XJan>bc~H11 z6*FxIT_1wm@c7pD!5LYo0ou(@73=iBS#?oG*h}PdEMe!>+LPB$WL}_}rd!H8{ZNc0 z{G2w!#nr1A4L^~F(wPPmndIi2@~SuSmB`-m#0Pc(_yWvK+E;j`u%4S?_MR=~zVp>= z%7^oGk-Y(TD-RnB?(?Bq*}_-QL2A`Rq-hURPvvqb#8AI5N4mR3usA636O!=6B3>8xz@Er75hcv_3PHg|$=cweeuw7Z;l@x$nqy-~ z2tNiI#|VKgw#WE7!AU7!zXm!5$=hs?YbUiRXLi_Dnsh+h2|ByNCohbpESNWRQT;Xr zXy}^GQ!cPDUcfhVsRAykMXoK*t>HKKu2F;Oj~$R^{{66Qy532k;m$2-lHtugfd+_b zxYQuYr)eFX1MM99K(7@{25v+xJq7~3ebXaUN7@u*fy^;*Ww76n^pm({)~_hkP39#r zFrYQR_JIuB7quJBE#L3_mRj?RO)Zw7#8D5g%nc|f-_IJUE45O4%Koh;nzD6;d7fUd z$b3|L=f|Rtn)QQ0uHB{u?z<}i-H}eMbr0fr9wF0t>Y&x0sRA#QE5E7~G7|a;;k$%i zQW)7?{7NGp_`FUwiX8DDRf`}uNf1Hwk_qcgW#4xX@=^aqnSEFt-GW;nQY`qc<-xt9 zRsHDtVTAG7UB94T-mhB}KGA)jOHn@MV_PfkI)C17qm_3ozc|ST3AUu{kuFNhpq(o5 zve*UY?oWfo08bY_v?M)cZ$LIFgE3H?df;Kp5r1qq)C&AP*3s5BIo8^)>z%mJ`vd6p zX=M`{F-@*9A-5GuiWHk!js|McShay!uzMovp5T!S!bcJM38+?bI`XCX7xh!i zzUxF%{`zp{A7P#~-XHua!kPxEE!=}Sh|U&T%n(eH2(D07$#TlOINZo4fh@}A#05-Y zzF<;zAx-uT^3NL^n=gB-)kO{4P1{!3ZEp8Oq#y+49`BwQz*?zF=))3Wd0y- zw9Skxw(1KXGQ=IibKKHC0(S?+wzJ|NU9nbu<|$&*>VT1Iko2%5PFb*W`0fAxTC-bB zZo4^k=UlZ{8amljRA}=DBfG#{F8=Sy<}zdPk>gSYN;ebcZ58g;@XP0mc#6rn){T*+ z5XSljlo}=P$^QzHL%Aj7${&?qf1y`Rkg$*u&@oimSFBEx9uM7va$L-Ebbd-MkT&v$ z_pBeh=WFe9Ixu&~`mK%d$vV>;n|s(t*YBK*@$dSr(L3o|m27Clj=v3W!aF|$I#-Ii z*36X|S4asOy)Xmk$x+FkJk>)FdQD!=&WkO|r*9F!1&G18C1#u?xNXOn;;9LX537RC zqhNdYNR>*#`f$r0VAXsBhbmj2IfY+QvG*YrJ9zfI7N`ij5k-q@VCM za)kU9Mad_>l1ev+TJEYz5Z;;zJW6XdwUGt{o()%K8E~*oYv|b;Ajey(Hhb)y7j}Lh zl%$YTH#-T=VrPirxTAbzg@3xn5{w=nCBX?!Kmn$2-$&R8Fa1c4Mce)S`-6v*44b(s zO_d}rR%Ng=ESK&#MII*NH{IOK8~v}JrTqk&A|wkzx=VY9S{1OlaTc;f2Cn}*0^1gj zq+?Zv^_}fT{Hy~eE1IHIvhDyAXyXLrAw#Lw8d_ir-(e@$XT*b+cinQ_WLfu{va zTp|KLYWPyNO0BZDky+3!Xi@ka#<0lgB7K2wEvSYFzdZUSdfs>b>7UwVva2gQp(S^9 z)r+Ot9`3|#^^lu`%$~(zLI+v55_Kbq(a>|BE@J;8#F*Pu03pv+^);waQs;sVrc8;_ zgLEb@hR>TVhxrP0MNG?}qXhGl1cynjWd3nck}y({Z%j*b9yqz=wzA?d{yMbCUFjuq zI7~SAVvN+sj!2MH2|Z{p*;c}Y>B-5Fe5)m7ptZJljs6bKaReQpBh4kxBUY3sByXEH zYukug{LS?UO_3P31k#=i?R6XMb(Q#OY=m&P=<5P&B{0k;pG97%g2Bnum1iKd@mlf~ zC?^hS7lm$97i`(#9id2hx1Q!+qRzaYAHz3(B>M$P;|@BRUM&Fhw&YhQ2zTh@p(6=@ zQVg9Rk~z=`$Q|6b*Z{iiB|PR03Nq5~mWn0%U{0O&__Khb ziTR8rB)E0pUj0Pxe1{8cSGmY~h=lDeLcIQY>>1m~_*@jvPmIceMwNu;MM9w@$*Sr# zC5=(kl2ATU0!>N3;EZGf)}QPr9$cEmIH}LU^e{em2O3DQuqeS+MY_C1YL(|E7m9ac!J!x%;<)B~XreIwR>x)EVF1;4eVNZHt z+Oc_cvj4e+84IVc2-TXc;?chYGprSDU9q#W)A>MaP$S2rFxzL)X3|HX6B;6~Aa4TU zKJ#I=+X*qix`i0`B%xFrD}bSv_E7xdYyT^2Fr+ZaUoqu)(*|XGyenRC+^`9^ZRBJz zm?P5bqKiv_Q-rP^(6q$C3R|6(VU{Tuq!%*kTUjE){v6g>?!4zwOT{m4^pIDT@$FMa z9FI{qwhA+NbjR#OfpUnSBU0pKgT`YJF3%oms@-51x)EaRjf@t^QbtE5A9~E7Q{56+ zhsXiYwV)cTG%7zh42?CpS;0k`V8-*o1ivNvWJ`cDcEJlMlRobY`4sAWm2`$QG={!4 zE{zl5K+kOrAEd=mLbgnvvB8wCK)NrHgM1$AUjQ$k&Zdt}p6rAoFbP(Tn1;;pq^L!V zWX0l5oX~ecKod9!wsU5&UWcF(La#<>3g)Zym{U*77)ejyr$Crcv2BD9NiX>Euw-Z; za0Rqaa$OPtd?b7;3DinQHhy@4&h0^FQUq8DiuDM2TqMxSlR3b2EX(Jp5)d1lO;8OT zaf^zm*5zgQ>?tck4^>MMvuF3#OxEEy_k8j%7luM|((A~rHGXqeD63Gy*`>b<4&0{A z&s$z59M&o%D#MTA=9pw-;4V1j&v`uS%k>ZGm;t8qQ6Fip%#%RMn2>>e3*A{zjyUbI zd-ufYi@Q%8-{yKL>QN5`_4tHFOt-w-?%mIvO6-`&;r?RY`^clxF}UYxPWCy2KUn9_ zOVZqMQ7lfRErXJLyT;S|R!V*bRqW?>RIazfOcu1dGEFz zcL*A?5ZdZ6vi=WYAWNZri!ZypBG3sEO8If*KrVTtmwb+|p2t(4vCV~#2Q)|bC6z!Z zkd%Pg$cI;*ttmQ!x-yBHle)yzo|hi7>Pb>B)LRvPXqX#aRg!IX8`16CTC(WCxsUHf zuF~z>*Lx&S6(06m;B8U?5s;S0N-7t{(3r%bi}-nI%|`}6lPVajeIB8vArKNf=uI6Z zO%hVx0D_IXkt%rijD-)H+`eSnc>rmP*}>mVYi}k|aFWSC+?_(ND2@BA+i&A6Su*8>hjlA@8a2B4Lz7XK~hOmSjsVIwJ0t*lCTT#>pd2aLJ7Fwt7_& z1kxe@f+S|JNWV=$k-(hiMk%|JA(OZx+%?|5zXTXvsUCDh+R?D4Vyc0?ngq06h4i59 zq%|+NGKGBnK{R$f<%fPxJ$kC_hLlvbBtf-1{eLEe-q{HTNpRJ)yhV9}Yte%Xo|#ha z3(Y~@qN?ZP5+YtrPrib#aTe}s6+clk;?`KMXZ_>JNx8SG%ni&b*s}fy&rRBsx1DEb z^g2@Br9x$jZkQ1XL;UPenTX|GCzP4?8D&MzQi8$Q%3=~9`S{BSdh8*O_h*3|T6A6d za|cYNi%uG`H^(gLSS>bNgA<4kCTTjK1W@_OQuAwX5|Ut1=v zv|xm}35USkBF6=hy0FW4C@AP=Wx@%s{Q3O3ZCyj4RgvVM7TGq>HUs%>s8(Rl(o4oP zNIB75!R(6G=zB|~V0K)P+!wtI7-SoIJL$8mc_|0~08CR-ziDdKtddZh_sRrt?CpqX zv5^N@WuvqI2vP_!3D7!HSkljR+D!qq$E=Vg+YlWP)*j0DL9_CJveQx{i~?*FH%s%Z z+8mU&6VWbnl{B#%p^Z?aVT~ZXl-jN8iD4tzq=|(8#n!jSGui+DC&?{0D~fX1I;+Gw zBE~8e)t%j)ZWXqqQi)lGVK$N^hbfiHVU?sJqjJh&giWcCn4C5yrx}}VcD}CP<^Fws z-_Pg%etwU)|LlET@9TYC@7MeFdOn}8*Lxacx;0nsjmovpLGSm{q)Iu?e00p{e=UoEC^1q%DFS-0N%mg8O?hVh6Yn;f=ov+@ zK%T~VgETeu*O?Edha(ri_v)d}BmMc(qc8Sc2C+vG$oPpjDldh>-xpz5U$aU2ify-Z zI)rAn(WkNe*W_g^>CTTq?RJ&lzund^bnU*R3;V6He-lFxZ<+Qi?+ELvpYF_OM+oio z%TB>>h8+?30@a)C1t)#bUdhGMgE0R;C z{@-N|E$Q)aSWT>~I-#$e1reG)8LcDCCi#+j~%`H@(!?nT`!@b}~*M3T}b?KCwIz@#6+Cko~Wdzt= zQHwYtR(IMhh2(dHxjn@qv8)pz)^AB^c(6strH$93gP-+>2hVJl-GR(}Cob zW86=&CbkE@vldwJAKxn-Ym}Nq)~ibqp?}zkzhK|TnI>8uG@wPW9{H?;%FxdtEWVs)>2Z za~7!hf6Z49Keee{|$A8;1F)ZGl2yY~2{@;mD7CPQaraxITY%bK8S_{otmf!hpTdEy)L z1;>NGSbdb_2HXXYjzV)LDr!Pd`Sn(4`#<^2MxC8}fJ3yUeFgj^QUp zL+Wgy)x{7Ukhh6e%IA___3HLt6Fh`rUmuISKoxsoKN=31@JC6)_oMHC^&%bOdJh{H z8*(ms=4g*i;Vd8vY3seL>(Z-5ZbMHGV-&d&+-2Rb+27cJ&BRlLk*4A#eY73aucOD^sNP*~OVM#pDykozB}S_J}Pm2jEruzc4UvUP_?Pye&1 z*Em+O;qX^nm*Il&(Kjy;yxd^UXDQ$D5=MF&HvJ>SPqXEl3K?f3?48U03;KIa?)fV! zkTxD)9jcc0g9?3q-dlB=cY7Kivsz(QJA>eWFW{F)sU7GwM>OYAO|7wa{4}kxsN8sR zlkWUTd^-Px%Qi}0SGX_uRinvJ-~2TFQ!?~}tA{luHG#rNB?t%fD(tFa&=2HHIc~by zj{71IRZ9)mlJA0UzX1sBndtKu;j3yyF5w_B*RMfGWw>MTW#A-Q0p1tx=4lgehOc43 zNJpwWBqzaB-shlVm{#gtJX`f|U?3b{ilU8~wG-F2v)6~cv*`+Y!qDH=<@nr>R?rO=1g zh8$PgjQi4q-g^uuv{mwi$Vu?LqSYj>bsgI{E@ix9_j&5vYIN!a{rpakL&4kI#vB&d zeu<50B4B$sSeSCfW=xMD><@TPy`<2|1tV7yZ%oBG4O@EUWyKRZ z7y{+B>~YuK6#S%d=&2p!epqWl^#(6Zq%1?oRBTygb<(T;3Hj2(XXYqN_+e?UgdbSI!1y%X5Sm!*%FgGiC~GtiKYSM)n8y z`-CY;!1FF-`GAWAxMm?ZT;n0bbsklz68MfC3_B(MyJkd_h+aQc4i16q(dZ5gw9#dX zrVc#b+C*-ywfgco`iT#3}2l(=+^Ixv45IT0%3s zYx-Vp6+g6BFl|F^!WCjDAAp(533zMR+<^AZJ>~WZ#hOH_jpV^n*~~zqprklYZ;ANW z%C_C(4wCeu#L{uLqUb0)Hv6;R45^C7F4R>O;l>5`g zeMao7`<6rX6aTQ4Askl0=P(XCX5VKGfktWq90E6RE3AWRBOAof4L-MD^8eAZ47`lk zXJL^SIq;(VJ6PwYWQcu-7IlBi8)rc%GA6(n@|$uB%46`Wvf~50*ikXne&M}rmdmaW z)sbJ1R!S%nx*CpR4v_R+aV-1~5NF1S$9?4NJ4w*y>_fqNis-7jktg!H&1HIlWnuL2 z8csQUnj@M+&bfy2>MceG)N6B{KRd#R)EPtQ$84R*UT{%fmXsmFH`Eb6nBwnwmJQ7^ zW&Z@EUoteF5AtN_w8Ff0-Qlot`vavc;UVL6rC&SZ9scoi>mBPLwcDTTIe(t)Pk+2S z*Tkr&&K}cgyO~&b#8}VBV8Jqc88722u9C4lfEB4K(yiFHdrDB2@Oi|Q@wu_{jR1?c z4lQ;W>in7?aWJ+%kj+V{bna*HMpSKs@9fGuKQ2|3K8U(Bausx=%)uU?A$E|tQ3ryC z7>hsF2AIo7$~jCvwa7gS$4*h?;7+qq%^z@ zVy3MP%LF*5N#b4b8)5{P*m8+<6>B+t%?1@XvK7qws1H;_;s=Mn2wEJEPj52H4AfV9 z@jSfK@Nq$oHfU>0k7ls(Fu5X2y`1;+TYi z48eag=FePP^;P)EDNYJJ^B%`;y{^@j=;<%hPbFE?2KSs)Bj2a=u6(W2TM%8}P<8`5 zE_rIdmRW32eR%TDX?AL!5k9Ldl<()VTGEP*pO->tB2*t$me6FJX&r|Ym2>G+`_EY_ zJPG@WQtvBa-9dWom?I?419t942<`)%#)Dk#9fMH|=-n-HmfREkz6fQ;({$bJUl+!t zFe$Ilp2O&2w9}jkzG^de6DX4}C-0t%eD66c*?K$@Db&6z#Z6)I)VVY7fO8s^8JqFH zt(?s~L3kQx!zXk`jm**w<{|7^D`!Gq!`W{TQnpP~nSEokHe)jjMy^%=OPDn+x6^N=J>r_Vxt>t}3)VD=65lsOj z&t?3a5ALLN_(5_Zv}U&{X;+mFes*9D7vIZ1y3S6|36S(Z9iNdy#{OZF7HuBwkmgDx zwxFc5#)G4rG=|>#&XtUd*JIO8GQy`J)IzMAmxBasqV17;Wb5uNl&AluzOYJ&kL@>$ zIJo}(17rTzPMkmz5zm)9NLYk|$V()I5!(U5P6VRL>2Wg3HAI<~)r7VXRf1^VE(5K@ zx!NhI+>3#n9(3q5Bhh{pnQN_-#jy1?G=MYYChxNoo;Aph#wmO z%iSoa@SpH18=la!t527KUHZ4%00T~+H&@Ri|3>NdkQ+em`rmC(d-XJOwbE@z3cYMd zrg=bUu}J_mBwd}8+v^2i0%C5 ziD{_&l(tf=`p&)$EsLMz?IEz8?u*^$_&sr3>9E~pZFKJHmq+;<-Pm;Y<}luguw_O+ zh*^G>-IcP4<1KVqYf1|%B#Bo=;UKwhgTZ75S6{B9mkGTj6Ws%&oa-`u1-c=wpN<)b zc#V+no;%_irDUMFFSsF8CmyGF;O!=0Gq6AECh@v_ZunAMFMYgR{HipHLui-(t>0^9C!?sb+Dg$N->wbYp&IMw@*s8VxG5MCuqVp8D`klP|4d$ zttq{THkUQ_OiI0MAod7rLjbzqBRoe#DXFiRV$GMb8#TCc@KB~@_|@BCT*S~oRknXlR<4lXV#b0AD&U>5 zs2}1G7VxHkjWbm+Aas!O>fJuZ$@mXCWc$zc;nlR{&ql+*sLZR^C6{Fj^UOvfECRsZpOs&~Z7I;!*eF?q z_xYKZtG9L4IVc#vtJStOh z(KHB3&1tZquDWIYh0<(8cwT4yPe!6@&FFD7Y+l96|<^4E5e$g#Kt;DxaHP0^y;vEgn&a zNiE}A`I&2S{)nad!o@g*f3#O4pJ?F+T=nkc9&Zdd57}UUfZ+nSUx-^ifWF|GLRc1Y ztEbhxXB2VnjG9_LB+@HS{@3Ux2LCrPVDJCismWpF8bw~&cl09$??>II%8A;u?UZNw zx>m?xOaTI`j0RrymZbAPb^kW>o8*F@g%)^k&l8qB!WqW5z*8f}9)zziG-aP0k;#e| zLrZXmsY1-;vJA>QqeeyADLLNs$m$X<8pf9j4kP=vAR8Uazd3T)=*%h2!LDDxh%Y*# zH#(rP7@}lTu2fG-JH2KWiQTHNHCHX>a~=6492iWd`1FEbG-al+tK3%c{HoWeamBk# z?np!*|2X!Wc7Iglk9_`2#EEouSY|P133>|M(4vY#KpqHbf}mZvgat=)9S%%0SQnia zSs4;CDmCt9&^vJY#=Y41Qw0<8WpMz_sm6xw98_B}*u^V@u5A{&sV2L%-eKrvdeEcv zGnU@|Ssae;1hPJa0B)7hq&P)h6&E3Nk}v6AN(Z*V)S?+j`Oks2Eq6CPS1T zs&i#+?z_n;LUzSp4|p^c>I+kCO&9Ix3~`)NL0H0w*Muug-QweYO55I@n_Xz>$*t_Yj{ga*SZK4w z)f}G7a4%E9csSGah4In2WVA7lAvcxZkfS&sk)vC|Un%|p-1&O_x%?f2<;`m(9-?B& zR!}GCX#CX=29T|U)3vCd(X1uJ@1TEuU`-sZHc^BOLwRZ2H$^hhk0RRsAr!>lo^#C~ z^j2yX>#o9t>ROF`h8-!*XZfiQ?>POFa!vD~rHT3xN%P6}w%NI=IK3gnc`tYJ+=pye zj_N5@(Y#&a1LOIvL|tH2ReT9^Izew*cdQj#7?rg|++5y*d&U@#q}(Pa(b@0%*j-eD zd)LWCpR#yGt5>0Jq+iVT()xZ2qtGJkonJt&OXtbWj3i7S;7cy)q0-qw$T-WxNePfcAvk>Tew@wHwF-Q)@dA}K>F&XAYUh~P~6(G+cAJ@C9%Um%TG3uc&F)}Fgp zOE6F$ap!tUby03LR6)_!_#2i5o@WQjdT@n4g%5E|D~RJlf@gK)N5ebBeg^Pd?jp%P zJ*R-tU67s#=w+qdnfjmmeZ-eByeqwD#|PoH7$6>3OX1SiE8DR2k$zk}VYKessR<7| znD}W~(#$>zqQ@5x?`(|NYXWB1GkgXkhPSx1K{SB`6k5q)>aBax-TV?&+rK8HF9AT3 zq0prLiZgcyv$EKd_-%!+F46SlU`7wi(Rw&tlpvb=&coFRVc4Og*(1v#F95>5=&r5J z&0m$xSHn(U$7gQz#Fqh@&kVv_+^4!v@7mE`KxdD#!A#FI!N~{Xk^MyG6^MtKGjwxMcbR^c*`T?kTis`u&aH_5gfwZ8T^hkEZK0>He&M+9kXk zONnWCbYxeo#@9kz#Mq0KiCfX}S=+iY68j!{b{orR5Ya#rrmul~*(NoI1#4Ks-O8Np zv@q;re>A1oSaS>ie|x!7isJca0sUUX8Q-@kXn4Zil@o|ygw%tJ9g)r)ZKF|#5lKC2 z2!8lap|0%QZxIBAKp+|$t=oS)HqAoUDg&O0DkELfkQMU}T%B`$4oAm(8P267EaDtW zxf!-4%YEI|{fn!e^(PJu7^qs~`xFf!&#zbpnJV3dV(DVNudD#4Fxl~uI3T3Sm_OI_ zOY9$26xMd3x$J5$>s+h3G=}d0i|7TbdG^AUkcl{FNUp;LIJHBLdoyCrF~*}_+Wh_` zf+guAs_8I{aOpg7m0~eHX5^r4<<5nM0?KT$T2{ko);WpWFyp7jMy)UZlAGn^QrA+O zpF1kZ!Y)IGl=Z9^p9CGTF$m#qlsRu{@J;w?$=^4AH4!^yEcpaJQl~3}4B?P?0=)rV zkr>c{U(3hM@BO}z9c?wdDQDX!|JvQ=NXZ-=-4zl@*ZRKpQ~VQtC&y! zN}-@5g5P>OU)Fdjm2lRKRJ@@)*B6m16|Z zsxC%N8wH19w-C1uHtz>NP5KXDZn@sVBvHmar&lf)E+bfqL=Fvdocw68ezpN>9_Mxv zf%`itaU%geu>#S9Vlom$R1s!yi8`<0P2JQ!aF03pmYuq=D~3@AtG8t?17FzE=CaP! z>T7B%n`)uMXVr-}vQ~z*VC<+O{45DE+Yr+~j0j5zvU__hYa-Qt80 zlLo%81%Jsh-U;2A*`6|cjQW3h!&h)Op7@JfUQ9_TxAnn2rbDL;J^GjEuA>gA&j5Rr z!P7etF$g`sitJ|;#94TEMoHS5eNlf333#^YU+!l|S&k)$(9?*}Dnvq1W_)OMKN-%1 z<@UVMxWELL@QBGWb_V)asL>QIco53LnM6#&B;c{ZOH}>A-+74OT^7=rTg^v;dso3- zTY9!*p$($ko4`ah39<>*-LWob;Sh4eq%_8hC}%W&K%IO_wDNxWFM@XYkMbLUO|aOG zhokEA(8OXlV61054w6V_`@)AzLR-K2RCd=poTalyXg2D{nc(&ql8xk zJ&l*V?d23CVXZw!b?7S#sG0%jqYAed-je^+>10~X5$ve`Frxkf@Kl_8lQ;NrT_TKWV~0+sJqy8naZ(G=Z_!p%_mY)34b3%+-9$L54|c|IO<5&|^68 z`cko%o>R&4IIP)3j)3PvK?+3#}T98#x5W20S&& zQ)A*O$-iMaiH5sJuL?$g^W=t>qy1K3fLMwxl^32w%Mh8UqN5%O_MS{+@H<3N+)8>R z$s)9gGR+v#BZg(txds}9Lmgu&Si34c33P_DGzlUEaP${b%~GaIK_Tgr62rUbMImOs zB>4GjdYq>=U|G|NgYzs-g71K-@Vi*54EMHS?>|J(RmEEC`h2k1D`ZEys?0eYiWO>n zS3+q<_es2=F@>hx9`#TRu`t;{NB`=eL#_PQAlCLe{O0UK5p&$G<8ePu_eukvaQs}9 zJ~N^G5bmy06Kt!eQ%Ij<3Yl$Aheh;r`eDOw2vHaEFY9O6bpIT>!02u)=jR}dWL{JB zl4hv%g|$m0*D&+_R}IY&-W#3B8zMP1TpSv>9J*oZl9%8PnBr!R+@iq|2T-ro)1`y+ zNT=Yt8G|P@J<{>S`0c{IARKkMC#3HK`gNYB&>oABj6+iE&J((!$L@l)f#(63%h|M5 zj1BZnHQOVGD2=-1}H}12dcTtwt-eWuE;Vc9P{! zY37VVltlUv;!EmaBl<+G1fnO;t2zI3y(7e5owS^vxtKa0Hc5Zk{+=VXm~yQqAV}oK z>e3je=RI%KoxmkV_t!-TZ-gebm%<%1A{GIj{m0dM+kdc({jGB(U(|kLj!@f( z)V|uM`m5m|rPdaiw@48x?#x%}2+3bFG2`?~??=g;mkF2O&8_Vb$onO1Q(tWG`@k%^ z*kgZ5d-rkY#XD0Ua=Lvoq#@otZ?e1JD7R~s%=CmmkkI&aMOsYnBG_<*Wme#Xa5{@x(x<1X35t@QlSolpkkMa z(-xZ4fU8-vJum>3@KCLbUoPY76SZGyhA30A+8k;7Q3p$YJPw}rn$z7kmjXq&$`$PM zv}-k`zO!k19CPw$j3xAYo(;b9LI7Is^7%nto+L#Yo6dSZUKUz*0uj0?h1^t9;gLlT z?6QJD-s%Be1qO8MjXp+xPw-1FouLw70y~ea?@pc6@@s#*CI^HiN_};Cqw(Gu{2z+$ zn^Cj)LiGU4a~b%*Wd2o8SBy5HJ4(jv4?X8VN5?(-t%F6p{kpg~yTjU9%w?!M*}UM=Xuw)A&FdBc4s3;I3W=sDH>Z%&qd8_`(X z+Vo%0rkB)JHeO|i_kuZ9+6?M`?khRG-iOsgu0OfpT)P$IE+(T(3BOkz&W;Xy!zk4V zP`u-)Tld53>;vQ_c-9c#lQkUih3 zu~E3V>e#cs1FSnz>Oh0sa}Xt*gYRT=mV)_c!8m?a!0$E~WbQxUmMXLgEDJ9TEYn2# z(Vzq{oZ^Xkd<~ygA~KfaGU%nX2H!<~njGSL(Z25Ve@ANP9AU!c>Rg@Zkq1;;4tPO^ z3Hu2-pkmBdiptMydA#n$_lq2kPr4cXxqpu+`sXd=^Js`KA4i{ zc)`z`?kTts*(PQ3?}(h09prp!>4C0a8ddD+M88Bt@b$~m&f0K2)Ei5Az6-M~6g86t zxn^Zmb8!2zpC*esY3W=9@%&w&MQ&R4C%J^Q%WDm>5lVZ-S6vvtRXqSB&i$mezh6}| z-{YV>t>h8dtZ&18S0XP0MB3H0TH)3AZ=0%rC&!EIvXRMfNeGEbe)m-6KK=h{0jv^i z7rJ?^nb-n>PUgs^AQUZ{gE6$7Cg=d+sgJb3vRKf)IMcqj#GeuJi~|kQzs`6o=Joq# ziWtN1_G;dUTpRTP&tO2-Jj?VEsegoL%jBq?kG^T6extksDE^9x>7fUZ_83-5Te9$T z;%b9}gY&S<%H|~54Qf-PLxq%YW*#Pp(2KmZNzE|BIeFKgg9p>=voW#H*g=*>>a^h z`XJYN%dhZ$o_Nmx{n+AE4AUkf;=${k4K>^1PVevas|_+zzgDi>mWvP{ia1T@9a?ec zaYzVY659C!y&(DS(gPoB9m}y@aaqI4ABEkURh#L(BTsP3y)b1tJisrDS(AJaG-CJd z?_v?RqPNRFLCM3=blpS*2>)8GK#+R|-z~*1qDN=A*B6U0&_=KftkwT@C_$`^JAmd< zrrc_^$(?fcB`8M7tNI@CDvg`&&c_Q6K4r`&d$|Yb)_iumxE0W5&u26WA!)0{huC?% zr=oRomYSt5+go+G<{rvaU3(QfW=e^z5&b4$f(3}cJC~r8PZ5KsCD$QGBR}RXCTUUa z0_}O-El@8c>a^x6r4#+%AJYIpdCt|Sv2aQHx(i1ZJ9a+USpyHq5nfy^ajS^)k;9MD_^y~qow-#6Eo*36@s)hugy-Y0(T zaw71J<|AbC%hxCws7;%I5a>{tOX_dxmvW30nW3v;!q`GT_x6B^aHn7v>P`Z{sa|R> zdKVgN-`yN--fhmqX=3kw#4#ztwUC$*dihf6s=fQ6gguRO7wTedlKCz9oH&Q?9z300YwZDTye-fAkt_gZX`wiKPxs(Z^A}qiI zLGG0c4r>PUaEH~ih0xA=U#0B#u{C3VRftaaSIDXXHgPQCQ)yANf2BEava zRg19e(U3OAO}15x#{5`P$)4sguMH+Upbu7*Du4ZcLobvOhK z7(E+5y)z`|1rCEJHD3oG^BR;^I+%NiSsTpM9tMqY1E#}PYjE#?G2v42yZ4)Geh+`i zM-r9#e^W+DNTRLrPmwOlPvbdgtk(N3>d{m@YA3iK4Wvxs4b9qG#s@AWX zm>RfXeTHXeqx#gkBpy%m#90go_UZ{wl!FFSwYX`HW*%@t!irYxgW*dutDTlw=GWp7bNtIXurW-jESXE%m>z_^1 z-#V)?!L@=J4smkdw-U%#LfM2M25op?_peu9)TW9u8KM(p8vj_8T0j|`qbv21!}#b< zhL5)nIyJ0ySj_R}mskF(PC}1#T}@>T)F!u}2~Tu@rMCF{O8b8S-eNdy7Bkrq@Y~&a zC-mL-lT5nyrX&>2r0aHDAaA_Rgc;|56_dT(y6>Dw+(W5O$+bRfsPe@~PPaUl)w@hQ zGq|V;<45HL^3GUw|BCS&#TS~h^?uTEBjJpnjndCbyA=LQ{sHtvu3>yPEb^Idv-_RnZN{1Oy(!B@;;ks&r9Q-soI+T!q7?*n|^x1IUQdCf_P>N1W>N-WS6Sw(Dt@wu=$zKGDao9_XpZMU86%rng|#XrFH zf0gSvIHxpvgVxU>i-e=k=N;28=%kI8OI<0imC4wwk%&dyHJe(VQh{TF&!pA2+m$Y8 zSm;J!bI4P)QOx_iA`ac3-xYN_3c(_IG0fxpEy{ON1~ol3dbTTLoMdQ&s;!WOtqyOE z$wW)Q++-^^`Jh9G1N1S58PQu|=(FmK`RG4l=-kf6Fs_5Ax{^Tim!CpQ@?KNYMX;)T zvcTb_O9}eRLu=r=XNS4!%^?8MaFC@(P9a__*GINdqn>~mW|;+YZLNJLMW07&6#Wos z1Ad~{&{)1GIl+s7-VT!eUX-bKLDn?+~&8$$B+??Nxbm%@djk ztz&!Cos}Ts5PSh+mlGCOQIMDbodk-bVZpn>yGY<~q2a%4b)>_!3b$N}J$J%xVk zxwzpvay4y7VSDb_Bl4ie-r-nPgm)1V3O`n&jIy;%c{u~48VJFEEqKMtOSTVnjPzl& z^fZBH_{mn#p2@H!Zv|wcT;|n;gGRIu4rfIhLgdFaH3)*9=u7t@(&xDNwQMGflMo#t2nn!i&MZm1-=F&SiVqe^XNY)ckQz;XL(gU?X`w zkU(zVN(ljP&^)2BRNcrfG4IFTn3bXYw-d28wZ369d){ijI-^@nk00jMU#AzhuUY-b zQ&3;n#Dq&$1gyLp(h$VP^H)D&q?>k}=S);YRbs!S0k(C%WUHvGoQN$n8B+e6VJ~GM zOT-ktth%f`S2a@!hX}2uL09h^(4tL;3SoP;81{Z3ze^hhlPF1`Xs}k@p#G$3p1v=}$}Oy04&JGH%)Gx~|dq|~_u@2n~< zzg)h}VOdRzUozVTMj#AY<(| z{o0kjp50!gUp>#?_hrw>BDOziR<;^Dy|5BIUVheF>FZAoX`7rdajxJzOD|adOEdBT zbNy%ve)4j?r^J$7Z4%rI-Zod@!gqoGYS+xtawl{6EvvQXdU<+0N_yV!rk@Vp>}gQz zX8#6n>ZusQ*X7lJ3J<;KxtFRyIJ^an(SDj{CF70X8jrKk#!=J1z28ps?!8y<3Ggjo zgA$=Ghj$Zsf%&TEfzyV3Mo!4>ftBH(f-_ElF;>@g$4z+0+hG2p8>wBVvZ{Xr*|n~#;rp-r6#QpQlA?*Z8!){ zgbv26PB)!yl@Sg`yAA*R@Rn>Ke?}J+22*$cF`NOPc!@fJ>FiL*#?uKFTp{popizS- zE~v4lE@+AKwBa0Jv}~niym61BvG*Fb<_h@aQQK%C8RQ_a}%Bc%!%KX9X`X5GkH>dmfo`Y zu&fBU<8UebXKtg9H2sL_2V60*h_uk{cLYmE`gDI?+jN^l3i(a}#fXm>h-cdufG5Cx z-o+zUUol3WwwiwwW+~Y0uK$v_>+YL{eYwGpADgau?2v1t6Lo0$D-#n(4pUl3EEJnn zhGciQeLN&NHvZupdEQwsH%ZD@Du3h5pw3=O^(D87jJ^|Ao%s<v4hta|jKfs*uQGJxy!E*yS2Qme`BLkMuXPk43lLTI8J)}PbH`bi6 zFv`A==?l=qEy^EG@cuZjx!OQG0jXP!(F0-_4cz8(s`rnzD&>7C;}>;R_&H?xD?|sW zm&a1lr0Br{_g`&oBbHxTSGRssvZuGIJ*I!mCmpi`{*ul`NA+J0wga7s+2y&wr+@O8 z&?jMZ+GfScC&|&L<`%tgqaQKu?)!v$>3XVmawL33bZx%QQ9oU=*-34(*+h#*`Rl`T zMg~I6XHC0(4H?*~=@WFQ8=A@X6&x<(Md5cv8;v!CDO4e;?+|N+u2+>=3~aUx_1=uWsNFRt4+Q0kd3cwB!*Cd5C2I9 z_b<`XaR0!%7HE%m923nu z#Pc_yKaZ--Gn-lpE}`9yZ&WR^ucL}6DIey3C%lU&s7n-~oji02r-YXXXEXuCO<*E= zyC`0P6LH0yhw%r19k;Bnr#CF=r0gJW?QrO6=z-4cnK}vnhCQSnBP>8KrfgL6`rx<1V+3!nm`lX@R#lgq% z@C7Cp!}Ly0Y~v3&mJy?S6MuvUK6?=Uws1{Qu_t|1hcpGsO^EJ#r#Ry#`M?hx4q>;A zDU=~T#d%^P7*Fh9l6_4DIV?Kl4dzvWh*p<}EDb&GHO~#m=d^(V76%}eulcQ2D;Ie# z1sid{BhpRcV_?eMWXL&18xVZDq0kj4I@bFT)Xk2@*QF2aBthZ~O?yK|@3an2yf->y*ZOEC4$P2i7F(@F8kIzyxD$Wq_$BL z2S8+ILSu{5BAOX+jnKh7=OTY0FrvO^)O6W1CMM+NssIQQN;kl88|6rp7Jhd9oCHsJ z+Ym|{dd`vuMfapr-;@mIp$5#IID5I((H_RjAj$6xr!Qbry_@2!{DP%1@Io+1?QT(8 z@harC2unnLSzWRecV1%!%wzaztjSkA`^66t7c=!3yE0=M@Q+QZUwx}5-P!JvyA(fR zA5qgE*YLl^&YcUdjg;;_Zv1xjf;MX%uf?H%6|EpVS@<9n5=6RtyKh{(d8+w-*juZe zqh66_Mp*@UyvIx3qQ+Pu1CGJC9g{3(NKNUX+s@YR$EE=T+I-zfhVXaN3NVp#EW4c3 z3dYngChT8L6#qIPa;jgX?1T%N938(w+EMiMUZNJ`y?QP03uO>_-Ss>`mnI7ve5`+!;tFAxr5McPdM4U>deyC!=R#9dUdRgC{du+Hwt5B=tLgzk9C-eVRAz6`Tpu=Nh{ z&vB#TakpJmvUTq<%4k``8)fd3v=H-Z$3pfydp$%T!voS@^}=txbUyf7<-q_%f7$${B9XL&N@K_b}g;Fta(4^m@Zoe?UT#s=BUt+0r%{132cQOiA&0M3L zA^YIihoLk0deHBW!wYF%)%$2FByk!1I&V>L6WA+ERMP<~k`{i?44F7eVdmV#LE3`K z6}F*rGi){1GM&t1#DqPDElziZ%oAwFYQWh{=}{hn9~lR#H+Pi3Kpg=$5;p^Ec{_Kt z{PG*h`k-8!h}z;Y%E3m;)26)?;Y!*xN_YL=#RN`jr>>rZT(?qq@JWln^G8N4Uhnzn z6t-x;lh@qN>u2BI(5ko(RP6r}f<0Mw=*M}E;KZ{wX*2PPQ?7yY9Y;EpP$S90 zmClb-Mt$D;m^Pj!uQ2#+)MZhUY3j{9$;jK5x3KFRII1XIH*NzYw-MMN3~&Q0f=w)( zYEDrm4mMwHH|`Co64+~Aa3l>i2c~Hd&I=)~>ad3+t6jE$ZrQuY8&C zin3`p(ayf}Eh=pqHh<(FU(z|psrlaJV8FA|hW6Dftx0!MTajB>iG^^Eji}HQ0CBl^aSZxA_;P;b0ertNl}I z4qT_XOU(PPUu|On!C+1*A^X>ZDdxeErvp~R2fK?$*G%IG8&FsJ+rTW0u<~>Mi`hsX zK{#BxJUy4eiMrUIAqTB|5R`6lX^eR3)%2w>sP~C;Sibgpud#&!#b(ma zT$#Kr@|ma1Ex1&a+L&Ds8{UO@0GVed8WLQ!J~3BVoBhr{av*wSuf~5t#}mV zuP`}f=Dxl4(rFvyVp6aE0>!@ldJTfA)^x>#IR~Q)V=Cs)ywc|$$4#tAO?)qHFf7c; zz)7z46|u4DNwQGc_nXy0-km=Qn;IYhGYxE<`hsjge?jP&Dk+7cw+#kNbYi|w@BQ+P zfek%L^4r8f*vsv<<}FW<-{qm{B5)m9$7+~iKsp1^3H#-39ri|LZS!fZb_`>*HNaBF zLrML#yJcL&j_D<{^>5=_z7~KR>DR*JA)pu62@abc!Tl;56jPsHPXxGoUH+3A*&E5| zRm-`T1?!bZ-7g;W9n!VFvoO~OUfZ0g|0P2eKEr(~-ZVLLxu)Ajvt{aZR&`2GY*ap{ z=chFPdzf@@w5&(^Ebz!gPLvxRVj#Y|{J>Z%2*VWaiX+0Ja&6Kd@VcW?xjKMY3_QBzJxwbON(l8SwGq@FRHll_(!CWhLV|dnVZO`>@8pK3RoSZvs}Q#gno= zQPgo;vBX>8Qasmlsi#GcY06TBs>Bl3BozDT|G^JSAS)>;^6femJO1N6tP$@?yTAa}!M>+CNP8sAu|ktaf9``ItwnN$*L7ffQgd7NovTJs z>*k&F7IeqN*E|^$bZg&8N;h>}iVQtpX*jy;gXQ|FX6Y?>XI)fpn{L@?nW|O#)LsPW zSWQBe*c=cWXwPlbKa=cN)LGw;%X{huL_Xyx(+Bg)mtx-~<%804vy?6UirlZpnk`7D z{?5?Ipm+eM=;r1+=-`)ub4{6`5G`!b+!8^`c6&kz(|CZD)fAa}%es|KZn!#|Wi{aX z$=(K;gft3!2<9{PK-JlK;kTqTxtpnH+g}V{jXpX?QdO^H4D)XyKd8w5DHV& zqmBLQ$PV!T#}ne<7gi6P+Vy=&F#NP^diS-I8}~LYOeY`uV+)e6eRL~lL6(y!`iIB= z-_ICGs{rrgKN^piDsbxC(Bt}w&L$x*aMt=mL^F7|Y~#0Wq%D**dA&N_P;-d)mbv2o z%=L5yLbU8V;o`IjpTWJu^y#D&P}UR{0qOGfKycWGmXJqM%2)d}jBSK_QhcLZ_))nL zYJ*_x23)z9HcNvUlZqYpsi{)DqhwX`LHapO22`EwpGM&1vqvjizs*FSFiw4cc+{dj zapz^qjvJz!xa1zeg0*dBrC$S>ZxDe^p9-6$vLi)P^1RYP$1D3^eyyrxE!e)yjqwIK z^F|=HEAi8mjfwzkkJ>vb%JZTS>@tdZ_6}M}YXDQnEpn6KxwuXG35-~nkx7wI>Q`(9 zLI_pL3`qW4m^SIz6P;1g2!25xf-3a#(2$tz%#rZ7TLDwbCAEj}72sfwFX-dO+ldjv zn8g+`G-09w>4w61($hEvYN5MDcG$DKLgOC(tc$28d50|y%J88?I)6n~q+Bu$ zJ!26{1f?-rs<2~oJ@4dU*ZF31YwmwEE0-Cm3{T2loz$Hf9t<0d@1Q@oAqvbG$39XYf-AAx%a?i7>KpM^o&@2qredhvHB~*#a_9GpWGk$p zlAA{+N>p0?vkDG<~)@&_hD{ew*{Z_uYHn z`<74pm+Y)H*O+6BIo2NVPoUH@Ucxsv%6|nG=g9xuVI=wL?$0@Nsblr)7f(7G3s_62 z<@<-tDhE_0@2?;o%5)4{)F?B0)qnAQX`6ORK1p|Dvs&cVykw8Avg=LlN|Ju2K8WXpqLdddabayOAa{*1HTToCPjS=&x?;*{c_Vs8JZ(T1HrLb#B}e zHoGC1#FNtlv#=$r`hMF|hLc}l+O(z-)z_9UCRYoHTL_u8_1MA!k+Qqx@*ez2a=xg2 z=}972l69IPk$&{2saMyX6?W?MqT)5*y`fH?0V2{s}$*@5n3=_v**o)wm~r#^Uq*z+`C= zJ#YD2%~hVfi|!8XB5AR(V-H*!%_Y~*@3EldzjGKQ`5OsG4 zbc_}?x;xobUcZT8AiyW)i?X%-m3#kj@%($8+@f!idqHsnVB$-8x_$baO*1z^#WsPB z1A0gJjla@m#lrWCvd}vY<%hj^!nBhOZ}j(j&{yLM2Y#fWw#}d2%XnXv7vNXfz1T&l z-8eyEs z@_AyNe3p*MA=(CmZkyO$+1+2MY5g3+<| zG2_*sF^hp`%9Hr9yMD)AmOD7-!zl^Ojp2mPZT{V?Q8me`xE9IG8+VADQ*LgVWF zfbVq=LSBM*q7vDSVbalOmP~CrjvwkfH_H_Gr=5QD!n#L1;5_c(i2ZtgW7x4u-F!v= z9+(;=AhQe#9w%76by9WxGD|qKVHoS}^mhPR_rQqxq(`GP#yN4=mP#C|9z zS-&v?}ox<_mAFix}fASQkYkny{hNC2cqWc(9G5Cf;mo% z5px&sc)bxEbDkfL->JG&7Zg+sYNe+sRx5v~zHN|RQFOY(=0pU355DjkOlf{DwQL6( zukAo@UJFlAdY2QH8sev^N4}_)99dzfU91s|eL_FyFKYdlO7b6*&hE#@eoWsOqlMh& zhDq)$391EJm!~s$X5E%WF zyz^G*vPgUDq0k$BMc9sF#<#k?j5uypTLajnNg$I$rzqTD-W`VGb4ekhx90B3g%d9l z{T>EBnJ~*ZGm zhQBtNUQc!?)lcLiIa`x9;C9)x*7!CiJ~(tWwGORU4N3%9la9P zgx~u{mG>V}?G}CbX&4(1#PJs^n=Z1PwWniO`uF-8njTyi70u`a z_m;;szvcaGGb%x8l2I-2Oh!E`tmW=)v$9ym?n)oICCbDm!B*m%`1!<%`VEgDv^unh z*g{IDNJ9|O;3H-^BZ%b_q#9(!W{A&WLztPIElSZ_hlg-$d8Rydmwn0YiMyR3+wUpj zIkMisFB>ekJ4LdkePKQq5OCiw1I*E`6b4Ju%&J|^3!VhjssHnkAYI|8`_GMs z_??(kaSg}sCuqGxh!NlPnqnO$+g(>4iZFtMob6!H;5U&n?g;q-sL}1A9GsMxi2SKD zQCv?{9Z5SB`mk!&+gKRELNF=J!`ldlBajUcI(l4^37Dt+!_#cAnE&Bv_Q~CKyJc+? zpZX(zYv;beW8-}i(Y%jl{CXp&lA~Yx=P@qw{d2L;_1Aun{+dD9FGX7E&Bn8eTzX65 zxBYOn{(C8}cg=eQwL@zJUm&?tbwB*?ZEe1U{onH?AE8Yti+V*;;8>02@;<>nx}D#g zZVYa$f6`DSp*og1<_&^CKxaaC63t1hPj3oql4g;9s*iP_@FsH}EfKmSmpkS!z8QJk zIM9x7L6sQUWxOs~!&i%y?m#uJkodPmaP5a8n70irVk{Nc+w*G-k`@VNnulM>l4B4p zm;ch0t1YOt5DYDM3HvPoWzq`=FsM72OIl9lo#A|rS|PGVokig4({A_KnVbd&6_zT5(E0@uhckiNc-tx_Ne8dVqDME7}1g70!-lgfUL6 zXSx`kK0{hVviyzuG4%a>HRKu=_TY5BVXf_)PZ06K;~m9##Oj0o9Az>6c)xSwb(+bQUnmAc$K+=b`(JopxxUGf{YW%U zp4P0}1&+z6CrJyQl79WjUOv253e<+h9Eg85suI~iwHaFWP5OnezO?)eWa>R;8lRWc z?cpK(AWKM!h1O?GnN_zBy#3V3ftHG^K1YZ4-Wx6C zoDoT&v>24)6duqYP$H`pb<~HXemCKM|6-R@^haa#*4sY#N*#3KGsF}Ks_3$s%_=K{A3cV>?vt!Dh z_4ofqH-vQR2zIZ8a4~YO;kWXMzZC^|b>bma4q(b^Si9==9%Q#D6B+e#U8?thGK7z; zjS$|sb9}GQ5hVt?`wolJD!8Nj4u%^jE28xnH62UtB9bsU2n@c7K`C5?ZdPpGYV=%d z)4t6o!fw9ZQMRL0+#d}&bPN$E)>rrG#T5;VOsiO9hoOu&dH2A}?zH4^=}C*Mg@(QP zM2*j19VP>`d@rU09$;iZ(1b$x4=37{=6$WQ`7G5PVm3KU+uN#JgL+Lpdu> zHu%Ne3|J)6M+YADUF#EPqaj~)q0NGmL#x`hqR^`Cgm1+2D&(}03p@v&OyF<4fH*55 zBC?2#ul&R?M(OX~`)6EB;tbY_ssKVf(3c?JZ@sr#cs&Oj=YrrqB?wuZfM!*H# z8Eqi2F%RX3*b20BFySC~2HnU6GF@4JE~5SFJe8>D{Mr_t(=lvgjiNwjn|BL;@gcj^ zD0`WdIl}TQ>`T$H0>W?4MA)a!rc4$rN)G^eCvnslL_?-0Kk96$66yDnk4T?y6$uPx z1^u0I$yk7%iRw4YkIphrmjN(rwQ(DASIYGSbkt28ogF&>hHxH8Y6l)NVQVdZ`y*?_ zh17Ca=1w3Ki09gtcl49bB;V0d;zc!I@%>K3P}f^EHvyD-ph9-%bV&^?!8&1Ag-+2@ z$^A$GjtVhpc5o3Pas{D3eyKf+9y9vqMeW@esn+3%cGK4s@#SHCwws`^v&CoB<06s- zM`pM)dM3Vw#UG;s$tTi%svTJT@^6EkZ8F_usQ9kv!YRc_gJv!C1Aja%P?f-pyN^d0 zyH5T!1W03DwSgzTH41}-!HR)9`OE~J*wv8*<2D1~3n7vokgeS0+mPGhJB=@@{ia1T zD}BgUPyQ>?2WKDuWi%@TVXP`l1-9^D>|jxb=8~C-OxK8Y~bdG^GKM)D;qnGns$ zwq_bRR?^$1yQ_fxuD6SytSM(*%FtMlxV<=fU~PfJWCt*r3|UXWM^?AZIoRDpr`3Ca zRWcbL;#E~O;5sG>NmMx%QLyUHe@*_vq?@5`3(97=U(pASwlb4SMiVh}70DlhEGW9W zsG})J=hLQIar7Q-#d-9?BUhxj@Cq=jTBm!<{LeMC#z!D?1&N&vg0&N3WRlqf`T(!~*iQGFghVRN=f-tGIXv7@C@7iZ&w{>V-ddN3zA z3n90RrQ9t4Sc?w1bin7rsHS#yZ@kmEzu;<Xa$cQse>Tw;cA5A zFji2j!^OZs(yTb zqvF%ce>}Rgb8q!on|;r6B;xZ_*K)|1RXg0Jw?1aCQgN1~{nIB1rgIG7@e?tN2Bpoz znz+9mJgo0iArrrV#(Pt~B80T6<{!}Y%apPtK63E}t!#c=hX!5+7+~;7e(%tCort@U z!o=aez@4h^c7Py&7b-HZ?8ffZR^=!qz&k4GwdS6FF?xJ@@pMd%2X*nFsAXc{hF9tN7o$o{(+w(ZIG8mDKRB12kFc0OL+_6 z9NO#y_`=JO}c=TC-eqSh2LtqEz}ccvI0!ks4dv7K#gmSy3Jv>xQtIhrWB#B-8M zoj*F*e{VX%BTHhRGNeLHOM?G~JkXma%f>J($7=*swyF2yi$IO8t(^XcA<9SKlir2o znMik&fGq!q^jvb{?1!l$8L-+qE&K@}5*4v2hC&SyK2KRnc85@W*l{6y_+z|>fHKGG zn^}GeNLE}R;ZBVMhVl16=92F9SxYK;HoVs=qb0JY>g-0eXGT$CVyQ@98F(EL*zsty z($?GMkA8$!lLAG9*YyPXewH_yo>w@xG+q8=v3tJh`Gc>K`4(cu_&}m-i3|&s{5$IkJn7Y)~&%2kMP#R9oAITx;5n zTmyb1@}&~tpY&|thWA;HZWT}FvopQ_DLl|T)LEa6)PZ?Fe4 z2l_2F4xzPR3l;!Pc%F7=-k>b$Cl8h@P-MUR&(0C&>3@=AULKM`In<;;?jKpcwp>J3 zB~Or5#?q5j&Yo&rXnj^SJ96l2e}FFUwNE>GN@aoh+=A5H7BC`GM|1a;#qOYBm#2avfs!G#3u0#`?kh$(oDb(npa;@urNa#xkvrqO_HrVjqn@K50q zM6F;!IWqmI&!&a?0@PE!Ff+)UIdVM^n*2g|^$mbDD_H>^jl2kYG5mg1O=mBDKlmVh zE|Qj;+|u~ffs55b5WrHg@s#899;ZY`^VjCmqektJGk7fWFWkG&KxTXD9wF1)q;DJO zsuTktGFQ>-JQpQMkX&h(*;?XuZNs*q>>+T-7h%n@=3hIrz^S%MEt-H9gO!ubDlz5M6;FhncWNh;_;U%Ch z#j7e7y%M#MF-;T5xz6CThF&0(@zv@X6+M)y2s@KV(fSM^Er5b6BfUN3b=5ttS4tPt zI1*?CkYIo@vNN2C-&9-F4krwPzr> z-Fo}L{*M#4gU00}k8(*~zao8#4=b^Ps!w7)f3IGaf@!>4Y&qkIUAjEP4MR>Hi(NS$ z*~IJh*IX0`Zo}n{v(l=j4SqB-^W9EJcN^k8DhD;7lcfq^(eP|O=4Y8DgB95mYZUft zK`OM6kK`TzO~K%w{~GlIcedQd>S5JK;1{P+k)ux@VBbX`dColtf)Gn{}~-0tn_{e_ME> z+YVH;DQF58M|77#q4qNARvP0#h>4wEcix9pd%J)x?!9>lfKm%23U5un>(iUTF2D9N zc4fk-wI`rhKs_Y{BEozbw1|%aPvPdHD!eq@?RiE7@0U#vvbf^)@Xd|i*~q&Ftoqkz z?1-;{2qpO+`ra*z>x41i88xM&jQP%Ugb!#w(s?RcwSa*9Bi-`Pz=MJTv9Sov`4L%P zxIwxgcUPHapp=zVpS*H=6s@4&7+!VIzgxo zv?Ds3a}sALbLo883bY$34*f+H-eYNX7*QvIlZXOkS|(00qaGUXT`vek(^FUC$mzzI zs3(=Yq&d%5Aa?qj>b(n441BI2f&v79cX%u(8d`#Huzurb$$Z>71c)t~uSYsP-|Du?;_h-!|FA)0IHGLNdrV->C`PF}4j)m?xJ zNmefiIxw1X>7&S4OzjyzBjZeHU@Ah3L~GI-0KupJ1uDAq1Tl`U4JCir$EOZu1--P! z-uJs%Id*`Z8JGaXhArEO90ESHT$wz=e#q|Vdl!@F3ceJ09M)NTK|rpYN*xCKRhf|5 z7ymrND-?iNTm@`#_2kukLXxHw^Z1%R9bA$%jcfPI_Er4yF{PWsGco1+tyE%p)#j@` zXeINJcHMQS1!_Amc6waZ>Ns}tpuppab~-9Zx4OG?RVLAAC9n|i%?JLgWsh;~-}Y^l zOGI1w`@V>Jc{}^tflICQXqDfzu&5(-E?OkmLhB~-3MA|L`|NMN-78HRMO6gSUPs;1^(%;ktPjPA*d1;Hou^DWPuT+zd_ho8bq*dD3yF z(RBKhT~+#f^rP_{I)i*xD~uPV1{CBi(9AV5hiU%c;Z9FqPM8U>bEx+85bfllr-=i5 zz(kNZ_dWd6IyGPeVVSH_{tSRyC(q>Zdp}JRDq^!VWevI%`PM- z);|aDlLM@lle!<}6Edy4*TdfvqM5c1b}dM-j|Y8-2ZoEV^32*bzba9zdC0%&JfHJ# zaOBT!ZlywY#VwHomrYmct4AJ6NZ{q80xh1#gcuYuG8UXaZ`aDCt%M)(0JO z=*=8HUx}i1wgHUt-qCp4LZh-h#B>dDYbiCHNGj;!zjmJ&9bZ=x8jK7en>;BdZ9|lm zPP#Ei0@2F%%$q%}1dqH!H6EAddeWd+{bTriQHrQA1y<-{wLqYw%T5w%ikhwLbmPFy zHCYDUrp1)m;V=&d?TWH>5|RxUwhN(i+Fe|xRJkeLQGMxoc6tD>6W30eqr`S1-HN$R zLv^T#>@<)dPz(KxaW%IuX~>2c&EUs_MX3PdvjT@=^O?k+kfGhd+OFg-7bNaJ?YYo+y`r%@jof&CTLKsWaxTP;~HYzBXy zi=0>ZzrwdcjO^{&p6?R=ctqChRJgGP=da?AAHcS@@&b>1*dr9b2n65gAdjm}?+l(9 z9q?YizlHfT&kmfUMc*ioA@aQ&yl^kYRHvl;rS zGC;h2(sT2)YL(Tf2_O-sAJf>f>loDM!yDmF4k7{1p~wLlnpWo{L$-k&5@GHrgGfg0 z0c>zrjC=4Ay0^TpEIgX_Wd*L3xTB1f<8dFrgLt}z2QQNiNp9&JS@@zv$zoydb0wce zKiaHVo&3j+O)=Mw-H`Q=?k^D=J1sR;rMc+o%}GusMe4TESKa12A%CH7pOg3_>^&SN zHyHwy>2@*Rx|?8a-npj{O+||HDJ#vFeVj!OhF#IPbm7#sdpzp;F;+9QvSqT15yehV zaXuXrVzj2rnk27Lg&D7h(C9E9rh zbK5~y_dFr|$`X5@Pg>wTU{Meh&BA1{<#{zc^Xae_5RKGanwMwXmlFW`yR9 zJYaz-RPY`kf><`i1Bs!3+Enqh-C)PR3Hm2xl}n0*b6-YXc;Nl5e%#06UTyVNY+BW~ z2r<{R;%|vsUd`+MpY*hr;0-FfblMvKG#8Kg$+}93nqHX_%dsM?K_?&23F8_0WX)G! z~=LS zuz|3Sbw_NQ229tVOY@+dwMP^Um;Vx&ZrHVCaF&+{mHtysm}b-JO zS!&1B{=n0e6D{$C=>P9F+b3N<;npIbkjhxC(U<*5<9|rW4wjiBYvlP=v&62QWO)X7 zdKpvJ2356flk|ZzBgpj12sF>7F;z`&fn1!=q8|7`cN$qS z2VNR}0&jV;9PC{evx}HU8Uy9m=9dot58eE)vx@|K5e=#TX?Zeo&d}Fgehg4>%aC~f z)qJ2EVr+TwUcjtg+fplhVS(Ho#(U>e1iD1yLw>$(TxZ;M%C_1HOGl1bZJ3)6)o5^nhKa)qrw-Ouqg;OF;Gsj1)q$VA@*PT zFzeTy==z2KSs7D)9shyZWWvv1Q=Iz*#F~u11mJve{&&;)8ywRB=hX(64POyJ0~*F79U8EXGrJHSW5wq^lOtLX4Z1jsq*e8RC&v zb~VpIlJNU){Rl#OwX(6Jc^kCwc{ z%LAtEq9J(B5sKn47g&q*ns}94g?7L`TV}jwuvf|LeJEt7QC=!rdVP|f3SFeAzF*hQ zXo8jA57eUoNLhLdG00vC}j;Al^<$*{KmSj?U*H#tB59%#XL zW;eJVXCLu-$kl);@k;bYg9*!ODN&2G_sv(<6|Mon=7@rl6YKi`c)qd#{bBYFxCg^p zYBfnixMh4l&Q@+5uqH`Y^k)x(Wfkn-Fl=4;%e==IM0i5b_IP_#eYs*sR%ffW(_B3B zlO-8AgcRUE%!0f_Ut&wGE&NdxkNYh(NQ5l6V*O9$75gcJA3-Bqv#jqpVO#y-9C#(`zTlN4B%-i!{ztr5(I3^ga$Hw0k{SOQMyzb8X$+LO` z-65DMI$dQHDT8r-d;N*9ezP9mJ=A5_PoAvvCv~30z(WCePB&{rmH0&64{}-pIp>R3 zNO~{L@xo|~9;KbIru8?vNs-INHaGeio}>*?mbaroYF@7oZ~e4tkIwSNj8^(Lh;lsk z3AQNp4(Cz8%{-~QqjUOYUmoECs>pAB3z#V|iLtM+%!uBkcw7RolW+mr3>UmGpFyQH zfH_DDueek}Jl0mzp84qX@EM%~?pY#6jq)3@1_gg(-E>5YR3hv=eh3%?n3|11unBF_ z-Dn|URjYm=lkj)gi0Qui5 zsn%s}j(Bgf6^hi%=x6b~`~7s*Nmi2=_UF}H<8EEjfKt<2ks5y@U8qbFdqz8WP%IDR z6n&1$w8(|Te{m(W05A1w)BEBFO@37*f5Nw^Ow01n-u=^q;RGtYKw1ZXDZ`!>kH43gr=jFdYNZXNs<-L%xK(agbU(s08F#ra48jR^m_z#)!jmK*Z#2d?SaPQ@K4ay z8k>pj0}&Fr@SFURIm-YBY(Vb=bujGNT7Zqs2phVf(z>w9`WaR&sIQSFB#(BwI;^*& zh--?h9IFmfp7dN$iYJgzqDy}nw$e(q`+jkPYX=slHKdp4Db0~G;8mA*E$fgFBA1m> z4{Xo)ZElcAc5C$y2br>rawBoDZ1>s0i`x=7bZ6hYsvu<9O8w-qGX;b}j-~H|UJDOG z^i@XntE|xmRWZs49a3gj?>-@X7YC8Qtw>6MPKKSwBpF!f(Xhe5(k+K&!0`zzFT);c zkEl_apV3|`>2}Chyg&e`Ng!#fv$H#|&h`7qSK(QjRK=WA(5!7YVyjV{c+yti%_oMx zOFrl>K)Y6H-1jYw2$mI!%}iV_$=>{AA%Lo#ix0mMF(rC9fXl))1DPt7zs`;p zCT{nOF+kjLb&Sy)pym}Q#X~68*y?DH3PRvWQz}A<4u%gNv^uh1mn}Foiq3JMtM~HA z+fpbl+S}vD6tVN@dA+jJK*Sba@67d?=Bvo7dY^|nbXci@9jb$W^I+uX25B`CKyTkW zw`w3G3}-!`#`8=-#u2=lFs?|pZ6uJmS_|60$A$lzkRSugB0I)2d)fUAWeY+Nt81C< zZQfO4%1bC@M37igw0Ad!19;cOZ2YJ(Tz_w7k`G4Umru+=M<~*ZmGoPt~>< zvuTm0A0eAP2*{=+7D>alUe;WKXPnQAw3FC2OX{Kuujvw$ccpbjGc~Kw)UUlT;rs-z z$$JV^$sCu~ov^a;e0U^Vrp{fO=qI<6cmi1s*ErDDMrkFMPz$iTdN6H+rDsOcaEq`M z6S;VhMNuTK+d^$%yop}|J>aUI&nM)y(C=;UBVSd%j|>^U5mX@kD{jyE4T9|AgXGMa+#(vud*h?jqatg4QI3 zjbM9w74E}9v|np?5CC=f_vkyoo+X!D;yaHX#2m8Tbs6Y)Z4&%qgpWmCM%P3R_-7$p zQ1e-N4iAM#=ydSmD!o3@JT+Gn1VNc?IY(oB7u^F+6^P#H70>;TOuysyoeNcg`W(nyb7FaZ_>G_3BjfW(ZU-?A z$}`8l5N;TNL@H-@k4R*>Wbd)9^s?FSuXpnR61T zX3L5y&8!)RlqZz>GmgTQ35f(P4NvF$6j|R6VqveKN_2d3Cdd!b>euZ)&1C6*uKJRy zF)`9GE>hn%+2xiwG!^L9LpMHfyyncRKW>!yhWYMErR}!XQuh&K*k~`Y7)Dd8Vkxq) zqDNJKkB%t?9;Y_@DZ%H7Tq6B%Ui^hVZO7?r$?z=LqJu4})kFvFrOzNd((S=%E-4yk z!j~FXQB@?$FAYQR8&upSri(-?2|x?r?9c+(%c%j)<)w2km+uT?8UDlGkBO*+lwaH% zxlj_&BYLQvOCcd#&`pWB-;@c=Zy>YMOlwy_`@W4AXy~wvPY7;}wnueg73hW~iT+vC z0&n~7N4ZpCLUb89ht zJTj#7noTT7cA=0Vea;C=HA!1R1f4l2@@(GI|7ZU7l+;tuO z)S&@7)6V^9RPU-s5SPp{nIoj57e}$N^v=p;7?m<$m``ePIwg3Xk@h6RHNKDHc9Myt zdxN-ajvVU`j^%s?1^)}7-=bd{Y-l1`OoOt0L37!%`%xUqdvlO&mn@J-Fn^a)vA8eb z9RyR5(ct$96R?$YDr<$0 z+cR^<9a!2>mcc-bWkp+~Q?mnekQeR$K@AA$4M&3YVFqo7+zDAjaJ5<3De22I_$9^} zWIMjsE?+7E0ij0>O7aMRI$nX<;8mOeS~->=apjd`AnTH<9d2o)NS+o|(th&vO7lfZ z3^(Q^8ED{(Ge)l?FQZKnf!0zqt&E_BoSDze_A#Hwu#cQpBKR;( zdDWr>;96siZk_5Iqfhv2z3o5_m_GMr_@S&)(nKJq@`gcBzIywG&Z3^SaLgn<8P3ZH zlvWMFv~+3^nU8`zsxfQcC~04pMxI0Oz#})uh3`dZ+J1pSMav$(B!915HOWu{mg(CD z9z=(<-eK!`2t#i6>MT4L9nB@f%oZvGfl?cd3Le*ZvEzT0Ceh^e+g8Ig@{2 z9|(j->->}m%G|G*$*Tdmy&Hz5(FO0{mX$`?a5n&B`R&9gn|qSJy188&ssc|@0AXMW zw?RPVMg68^Q6f@40!sYa9?X7OGfLk~wwwBJER#cN6Z)i}8){2wtG{Ih_~o}}=d;{a zVP@3&yfs3{Bmm3vv_*XvB@BQebBVCxk#26+066!b1Ex0=h#=Olytgv|d!amIr{%gb zl&KIUiI8Sk8y2CaDp51eU2tKMe>)|u-3TP?suehEi;YEsH$8aATtpS-*1tjhceg)i zp~-SiX3tK3#Y|oMsPVl`=3V7I1FS~*YRv;y!yHEt(j;TKmg=aq6m%(;Z#Ynu3yD-~ za#+Ypjls2bpPBB;Yv#RP9a>Ta8Q0^JQ_N_75;0!1pnH@i3Ka~^V>Q;t(xp{xU^MKx zz#^2=5>`~B;Ftc58D+0)H{~$&rm|uPHVKqYmTbxABRm+G zF)rmD-JZII53i7t>&LwZ&f~qRVz`=#4(Zy|#;?QyuDv!BJDJ6`s8AbC0KmFcVk`MX z(FSohWdYO3Q}k2{REt-mc!|VeiG&8y1t_h-^9LXf6`-@qrPwvRk{pK>{1a%E+z$S4 zujxiZ5CQ?W50-aUL}-$k^MglZCKJH$5d>pxZAz&xgo@-Qe~^1ylv)%z2gcJS1o@!1__~I z@;Pz6%n{wy5B`)Q>pTYunM^d~Vj14^@%hN9b?|;P2W+J(2?J|L)o9QySr>B%H)qsy zXEk1fyRn4rz>fJIWCI>*pOv@p6nWa+n6*(sC@*(e&l6Ac-z$3DgcQbcQt#rY7uKg& z@F68k^(qklQHjl(wY{4_$Hx-Zh%LA;5|MLcc?6IR*TpR3v!Euz1|R{q*xsGrBF(~> z5i$fD@@o;nUZ=pfGIhyrk+KkI0tGMvpK85M`zkP$iLVyq!M_vGP998{HFv`204+G2 zmoV`zFSVkf{6FihLxul`NP8yVc>mkM1KXS=_Ief`+3vYbiuNHZHJ^D0Y3#01yzDeU z3;D>-rh8WprGYXPX&3%IYm_=*Q1y2sXlWX8#RV-M%mq9tyE2LM{%hr)z!1I`+Xnx~ zl%(MkP2Wo$OhFE!7?wb13>_wjXg)ou0Gf^Nz&hIImb5=QN>sl{d6{6Xi=18R{Fe(f0q?o74bfEan$22mb}+g||!QN4L+XwG+##_G!(Lajp=5 z?@u~QlvtUV`(!ac{LC_f5Tr@6|+COcQ(IC#K9J^<%k-YcV4qW?qh z!Wp0-+3L5__#;6~;+#dB;u=N`@M+j0B(|v+#fwyYVw+d+zl2sP=Eb^>(}OV%-2npP zat*LwCuQI@ibE$;fib$g&X#)yI0zLqb3h)au^0PE`g1`90yIKnkM>ZcfC@Vy zgHdWfQYcz~o;L*wSJ9r$%sFEQ+OfQe*atuUZ%dqo##4`~c7LamG?eZw3<|cJ3?~AURw28{P9Fb3B#ZsxN1kfhkdfp#`yDdGPr8+&ilB3 z+M#Y2!m_T&!g%k_@^WFN+;X3V(r9nM{H%3xw&^lFVL;!07pHuXA@ zbRIY^?a`_&h3hf85XbXCEwrFzoU1H#<1JCVY#)u&KrqrR=4|O9J*`n`#Yq`@s@3Wp zUF@!Cf05ODz=Z!0j1^U69;FuK&m>H>Q!=aI{?UOt=i8IvGYAoq9>GhW4XgnibZmst zLz0p?z%7^-5q4px^&??#FYAMw8bYDmHZVLf3AAikKB4Vw{OQ!hD_J#l&B3?gj9L|D%-Wc_-?hVbnK6IdEOFk-Kvv2*f`Dl(_(D1P^xK_{tCvT+#WK3oxAk?A zB=Q_}obZbhJF|geW~IhHC=z^OGSiRQ?9>G^@Wonfn@${L`Ikg&#N>i)(v3+>d7p&l zI+ZzUN5Ygh=!TY`j#;fF*XZKcKw}acz4p1KQ5%^6*oFoyoM86Xzx?cZ1&C@@oIJwZ zC4^{+3qghKqSMT+G!DK8{{@m&0+t78sOworYocThKHMOcv{P}?cX3IPx%zVu>j-6k z;eOV;j{@wja0+-&qE&djYZ^R z$fjdY-93k%{?SykDP~De=WgaEpH4*C?Orhpj0ErdgY^h4%Wud7C#)6fc$?o1MjVS} zs-c4R{AA(pf_5MLBXW?CgY^z)@;X%WMjN7;?mgc(iB zsI4KEv>1>F4!%+hEkG2&egQ+K^KBZ}gFyRIvxdJM-$6wE$G()_PfXGS9#(A+?V2lh z%VL^P2_YuSVf%zgGsF1Jp{YR!MLVQ30^m+uY&Ym^RYGQ8Crbh7rd^mStyEb!%g-Gx{k9`PB%PjIt^T#2hi zh58!fgbz{XxEgg03(Z_}uE7ORRY`2oVr_c@nE?WI)8jf5f2)Ix z_bAJ3!H!A>cf&nkU*!#Ku(PY8N7Y;_PWVWu;M3)1Csg$L>{HJlty7)j6*E_rGW=0Z z7C@WVKYU9vBnJ$WZ_|v*^Ar;A@}&3;24cGo!ne6@|9DE~B3F2w5S#UMs95#kV1%Ok z#a{@=q!`P}yv!wu14{5`abnh|mX8eghn2;xj~ERM*1%Syn*5?6tb=;ZljS!LJA9Q` z($|?aXQrpnYu#f){MA#e8qc>iYWP$RA*iDZIYnXL;0E21-|&f`pD7lr%3RdmFZ)2G zED4evkjm}C$6mFiKLe|QWAsjs8W6+n33%T^7@yEYQX4_bKe{{Nx3r-JV6XLtm9+9e zNaZ8Vg+?>4(#Sw*TSK6DkV@v9+7f~cypnx|9~UCM>PDDnt^4_y`?JV-X{nz-pM|Gu zY^aGre*lG@cIhV%m*hT2qrPc+zKVtzU%+7X3sQ4fdg$A+pqa^k66XBDe`LTycOANL zDi+s#{gTDUodvkGvWui4Q+g|JZ zEvX-3nS!1f-35|939fa5pXF*Ro*ugM4~k8unhLd`5^J&Pa(9hm~OkU@};y^dm6GSu|)D2e}%(wnk?7|2dF4*oMM(zB0>h z$YYqRnFCH;uoO3ZR*jxbRB#mOhna@`Vl@4@wsLPvf9a|qA9x@7A@2V*SlMUC|F^+v z8gn$lg9&#L{!X4KQFCxoWPKJLjTIRU02v#5X~p>2mWEdMEgNj*rWg!ZaI!Xg3?82k z3fh#sy)uTv3b`6W0rj;v3g3V-h1r9VSE&a4+^XIBSdAT?hYcx?({8KSCD7c%#**iM zG<2ZD5g2oRH{&;>$LR;;AyT)e{~>p~y%b_!UC z5%$}3DffU2dhK`E1E8jGtEoBA69G>^Zsc5i6S)JqOmToH{Km($6Xr5P2vga_ z13KqX36?WE8AMZW3(gBsTbWs2^_dJaN=Wufq-onW-~_?@?ypoJ7CpURU|t5i`)3z! zRf`*@C_de2H@q_%Z<<|nwUkZ!^K{$rne)5OUi)ElVC+Wu_ybl<xg=C}kZcypQhz zKzERx)ojVBtpo1qLwV{0dcxzmk*9bBoQ{gnyVVcea|jzilVUIZx&_}ZPBZpWU~K0p z%Cw+fAX6z{2BXP6oPh8l5<#MxCMB;L!SB-U(B9@^zK<@&Z zeHQmBB_)pDR~SZ571{xPcsujgU_>|~OX3>*d!%WQqgPtKbSJFYK=>Q-D^VxbV~x6| z>38p>HnkmJ0R8uu$y|x?@767-j2!QMBMbMDW@ifZbk>{gN%v6$G|>|P_ zB+VG6Wy<~haQb%ZB|V|rA3d0OJ`=~*L^;B}OBa8p6WvPPWuw_1BTX(=f9o|mkqp|9 z{M^Z__-N(;ie$p?#!cWpTNzi#f$Vt*vi6v#XTTm@Y_>M!*S4j7fz1uCQ%KbG<#qoC z%ZC0zT^@}q0UbY)^bKfGt=G+xWlL#E1bj#`$Jr1;9YRtFD|cdEo(=`m1SEmcyn;8b z6F@8JmLM_@14;rE!*R4- z&M9(V|3PLn3&ucq?#E(wRJ;W}C~Wh2Angfcm1-uM$;)4qCi#q#Ezg5acaF@VI3tR{{J^`Med>qmhPP?{n*QK5N*TflBfM5tSaG`T-e7;t z;({=om&uW~-aajYjS$vGlS;%=X^HNWWYA}%MF(91{itt%DpheB-yQoq`uCnriSd1o zCMd708QWszPL*sDb`1jw{0%^h6a=HU>7Uw&#NWpRl$o&EJE>Zqka3ISnIKcquU!X# za{|eVh)?R%6z35Q7#>j#ND##1b?ITmUhRz@Ll^VL+7EZ0a1Zz^2X9IkR=o$LKap%m zCt=%%l`Vj#-3;DJ&_v%4MeFn6Xcq0p<`a0iwR`agW`>J*>7m2VElI@sow|?9j(V_gsV$!IY;Y@v~GHnK~pxOG9;z@K!|4 zK|ir&Z7%!?yM;=*bqqCxPHfqgk57P|C;VxU07MwQsQQQ(O-xA3ea<+KxojRxSmm7= zK9^#HiMr4KspY#iO1GJQ4j8Cwi3hwc^cB7#Z(u?M;>(rKTAxffz5n3R{AnGz4 z6A)37fNExC{4YT@&`i!}8Pw{{D|$T&j~MYIsc5Gs_bAg9V8VM?qlkj36+j%or+f&C z`WAnqpz^)W{t8>bZm4zU$#@iyK0L8C&SyKEuP-fbhv=zVkgx=nZ zT$7B(tim6zcV)A*{<3`AzuE7-$_-w*o4Q`-qirTK21!&}{x^wjXXiHG88Fk^$J(X@ zZEi^H?qn!Q=;b# zE0kHty7C3~SvhL)GG;BX{cjgVx&ns9&l1lxOlL~Ua;3UqrYw#MQKRYD_MCK2mg5n-vBQ(Lj zBIb&&&0iB3_JGl^#jXj!ic2Jo}F1DOZFm_Z7SU=*-|0POxYUUQvuu5+F9JkRS~*K7h$RKUKUsE|M(#p5QHtnY8Do4(Z| z&w81u5h~4ie^~sGa{eQ2i*DjG-hE=(<(n)h6; zX@i&EAijaRm-;#l<6DAijB8c-Hi9i?z0-6?C1nja`!mmH-3_z7e0J)yrhi4}xxl_} zE1kHm&Zj=rjOInxN2Uj6ZykTuOT?X*PnfxeeaJeUSAG1D^WJK((?k*6+Vo`!`rP&o z-#C+2dtIjc@E5`QsQvr13Xu)NkewI+^&EXm<7z4z2}ckn*PB={`@aoeC9f2-G`9 zyVo!9y-O*SZot6o)=MATIH_hZdOq{M>Em++O>L*9Zf%$<4veg<7PfZQ3syBwww>j@ zz5}~(L&9ktLyIUEkuY_#-Bs_am(byDY+LjN>scyJ*L!nZs}5{!o04CSR{$3G5)%0!ht3HH8lqOZM;?cnFA#X#|tC7H48$5$7Q1!}ob z2ZJ(g!A%ZVC#l`T^XmsJ&XD&__f2_y_S9`~37Y6%C`p^oU&we`yTQ#Le;=FAGBo>Z z@fM8~ZsZ7dCe87qw%kk4Jdvvi((9%(c?j^p5MolI5Y~=+6z6umyt_2kP}f2cJV0qu z^0oO|?=^p=xHY$DwQC(T&MZpZT3&&CfoNgjhuU`a#h}jnnLd53PbHo$oopHcn~^wK zEJJT>-P;hBI`8Gt+UI={SdqMq)}`?VSFoiYoVR`pBrCanaftrDz)VbZHM4Q1Ul6T% z7r2F~P02*)@!9os>^z8bbP5|LGUqrAy zRIZWTq_1XUKz8$r-id-O_bIwk|Iy)pEb<}_Jq07Vz%mw`U`lZF*u8iG!J&_aaa2x( zFVAm9r2Hii5!~O2*lxSud|^N6d9m8~g=Q@s*XKvwG5>Asf5vV%l3Wh%f8$|B^MIe? zAd2||BVR1z5}9S69D@(N7eD-4+ZQXs9WvCHgEloV;*@ z{)d^?g%&i^-)stOh*KhhX<8qukheb|Q-~z$7To%$AvKOc3-UziWpI;NCG&+hV+Vne zQ&0tLTECpmD&c?3_|wM-C#Cr4Z{50|W-&BiJK*NWl|h3}&`-pjOhDuc{*NR6$2R0O zjw)T(sma?Y=MDY9e&$GabEZejVP-LdSi2S)t`+v%G2LIl9s2h#d-T6K2pSdv8aDM3 zAh*5US4&5KJG)lwY1Lwt-@B=L27b1!rov@v=r+oCW9)EnY=rsXJ0|w?(C8%6m*e+d zV9i;j>oV1Sdj}!$coIl*hzg)NVT_87A?uNl$s4~Hc#)&3;@VUj_+Hzl)T%`pyl3T) zX<4dGL#dlZ6wCZx-r7Tw%jX?2`}g-9;UxA%aJ0tk8=g-RQ(V8?A~O&Bt*ijlN?&r> z)?3(e<-FUJ?&2{zFmQXNQdl7w?&B#)KsRhYBHjCQ7XSNWuF9h+!xw|jEO@QZz)_a~ zlb#HO#o(QnuerrnkpKPk&jmmk1DDC^3Joc|^tqWJ&m}Jzb9t*~mZ1sb#N!MlzRw}Q zHBtV4XhB>ZwDm`%muf0s>j4!w}2`H z`M2h*G^Wh@fPrW1VBB+I2KVoM@R&Rw{ZWCL3Hd@A%Ut)w2n1IG&1Rr z3+TCiW|;Y#kO~m?2}T6BbPC4=TUf~D_aS))X@Y1e{K8|wfznJP6Sw^VFV2{h`2KEY zDW5@Sa-IsTG1OFq+zE!rY#i0W?!{Y#R3Yy8s1<-_wS+^A?2v#@-9cr z9X)n4;sp#bb1I~HkPU8r5#7grYoV*^YsGDuC%C?ecfvc=yXxKDe$Las^{59kFlBCt$?}V}8V7#*BL%%b}-hIfhA<%rX!Wo;A(ol-)`CtCzc9aIt)g7k} z*HjEQ_ts{jAe%w@!ptcCoQ`Il zcf{;k-s;_PVnw{GzkGDPHwlL)3o9t8CXt5;OJ^tM1JgL&cR_kgT@5TCqZ;|cGqaJZeJ zj>THmhVIzM;XxCvlTsv;Yo8B8cbB6S)7+Mx;tJ(L*TTV}lN_~lEOtOrTv|g^o#2q) zBxH3wnqhdabLHD2&L%dvpi@37xNtQ^x*%^0{aV577P@M)!L-3>Pf1Dz#sbF6ytoBJ!}z1EjfTMxkktL+M`7+VhDd5N%b;9oz;&%OZ=q5Z>M z@y>^J)1Ko!dX>r6_yXWCq6Kmt5J&%CKZ;evCBd`pH^HhOJe~PpANcQ&|MA-%@cG>^ zPuqkYYjOg|WmAnc{w2fz^-9)b*oH0~gtlGQVm!R;fld{V{#*8czekq3j{hMMJYIko z5$?qi9+Gz6xpS+k#_vRQH#va*j~a0E-z)sn3KzlWo!CP;6!=?C-oiWNbt)}4uzC*u zUq<@*POcGR3j>n=`^Zp1PNP360lq@H{=>df?g!YT zLf*Zz&--BYI}iDLD2-6w9}?XJEZz@L+<6Dw^Y={gXbrI;ekbDbBj2sHp$6z*f3VWu zlTSVhKK}vF2a$sYnDCoVZ3cavmZ9}0TaA;ck>D;p z@JA&!UP+DpZeMBm@mhNiH4TxeB-*Y2UE=>5D!c{q-LpWCG|>CygwNr>-kTQPdt8_TFPXImyPvjU^G(ZJPuHJx`>$*%G6NvgXX7$TgyfQD5QUe3_Y@5$Ym{J8BgyM_t+59lLR8G`K8$8TSTIAYY6 zIlVi~S2l6-*qGS^xnT3|Mp)<&MQ^pMPiXZmM=dzjJq-_nI2a8!@*X53e^+Iy$xtv2 zRk{vSN;SHVXAXR;kRjkRggNkSr5@6tlp`iEh5nhPcud;sw!>JtcFVY`-+mN8P?F)J z#}**B(r?4L`gWQFMe98q3(vm@XjtBW^oz&i*3=Dhk3uFmevew^IIH@vMFyMpR(}pY z3Wd{QaCe%A%El6AlmQK+Nn#5|UqSTfcIVoXG2^!gpHJf~h`6`XAG~dRMo)$FVzu3Y zj1S=(Y`?@NVo>rDh5OE9Dqth>JHyNsa#jiND40D$#G@OsXQmyrDy>A{RM~=DtL8amY&;ql-)1s?OkKuRe$4-D$ z0mz~B9!K7%gosQJeSrt^Z7>>TniDy{Me5W=RY+FPA zG;d%jRggzFRbRS{>qPTF5>IjxuWiJelo(W&G`EfXA7-PfB6nuto31J$zLNb=Uy?c` zQIL}eatpABe`H_e_~>P`Zy%bKR?!G7^wp{#zd(1j-xz(ABH41wENFckRL<&-&vv{{$Tlb zkO;cT8DZmw9)20bNfa{$oPy-S>)u_ejp?|FD_ZyMW|f;w#HEQ<#*l1X-JRg?b1R#y zeCRzhCLE0Q(OGpC_UMF!mgXC!lFP1MI`FrdMr)-X`(2pR?%GA`;WL>10k*8h(b<|s z7ZcVMdeJlCkd*)21yCtLPh=L(=zSweSgaw#n~QM9itAT`7>C;2-WnmPjiYY+N) z4GzMz<+VK)e!#p^SmH$WNuNdaFRqpJ&Um=0Yab|F_(Y$O=r3S2O}d!|bTneVdVdZm zAq{*o)gtYj_R<>~I(L?t&72oBn>|-P^J!0`FO8ud5~nx3O!_8qX5Edw1XJFegK-O3 z`U-wu*wWf8t0Hz$;#$+pL@=v%-D+sX)&y6$?!B^pua(rbzA|B!lE^$y$IUX8Ckhx0 zS4QtDvNTaz=kDa-0ZI2H_^sH|T#Qry5*y(OXVn4i_7PJIa_BJS07zh%OcmPHiWq&# zn&qBUDVPz&_&m;7>k#w<{r&IV7TTFKAb0GOXVxJ}vA|AEb4hnW&Z?-hWyQT!fy3#R zN6G468)~}WD=>>@a5GK5itF+M0QJj#@z@BpfuF+hiECqtu3(Ju`oj zTULL)35M*pF@q{MdlCocZB0v!O0e*w(s%&^(hT=ygxsh;@k!^Z`Y=$Z-s? z`nynlG$jQIVw_9EYbXk>2Bz-Wx3-gTdw7MPI)ptw!qlNG<LOI=k+=et;c0)zd;}O)snWt|&gip;(IRk`oVc((VEi4{M#ZImB*&h0I~| zi_uRwe$4&f#`rP!W7f5weW(62?HgeZoOV6$0I{}H<$YJrWCM1prvpH;XrQyQDQ@I# zF)xRW9Kr8IlyFh@R7!AfU~S0q zMa3PpTRp6Y*zelcIEitH-QLi?_2P@ef)U<+FB*|LxKN*ZH4fna6HN?C~zp%1y$+C=O?a2K^Y8lrJq!8G+(Lp-c$m^_U#s z&>@INRhg?6`Wy`H`&5NvG(UK2@1a-}S zZJ>*duO9Fx%lgyiy12RZmr&M`s%9Wqp*A%mOGtSZy!QG#AW4BGIEfu z#kIpMzCAfs$!vUC>CmE$BEbm?x={Soz6X!Vi zOSA_QS5O9+xTtV_mBu8Xp%sz{7G~|aTs?ZfSe*5iUbI|0C}(~fhU|6LH0Ph7V6cAn z1{K4_E`%_ZWZdg0jQ_}V01|y|unci=&68=sdn#z{YikTbZB6V_0zL|3#I!KTog{qe z4)!rRxwYAXCa`@)kigG+IWj4QFs_(b$Xi;wveH$qo!(z7f?KT0b6!!%z)Y;OvWD{B z&IXP`{t*X&?}qg5YY!f3bX`5kx&l)LFLUE1mdz}oxBP-r+mBOlolM^py-fo5<}e;q zZ}{&0D8F$}{6O|LWB<#L$WO4Xw!HDif!sks`vOWif|h8eNv$l?;GDPwg@g~tj z5E0I&*7%|9oT5pcn1F7xjI;Jm^a-!hTou-H722a3=w1PIf#+7etQ3y%)zx#zS1*>B zq^%PDCtMe71AOa9H>G73e%UK=4+VFSKx= zCj4x=KdvMr2chAUp>wGl_azX!NK{D8vG{psr20Zx|Ocxc3G&G~>Qb6i|-ZTpEAPO*z!Lbr}tRUl5)qamhN2 zKtA{*wTWMMXAdEJ3tB7S7xG%&=z7a*Y_|{!`!fQ7O4#<0ta?(9h$s3`1xLv=BvS0D zh$nuq@f38&mi*PPBxOY^-3()0+M9*!#@AS#lki=c2v#zqmB9}v1Jan=)+KO#SF@UF zZT#+0RkI7JrG2C?$&>zf7g6YWtgQV)RjvQPgsG`j0BbB|QP-vIHA|l&+j>kXqC72=j`g8D;ql3gPxUn9}2{lpajbE@^@Qw_ni6da$O6 zq`?~f?TUvHYw*b;+BCYGM%|Rr_i{d9*4e7!x<7@G5Y$`R?0ODMub&Jm&*)vC&tFel zF$3i0VIX^e21XjpTR-s&d{;DXPM}1VgW>QaQt_qWt(8j_>M>p^ajUTfknVsOf{ky8mP2!*UptZ*K1waGFWhZ|3u$W};q4P%6uUB-sT<)e7sbDq#uD{1e}@%x2G&ALBh` zT*78o(W4J+#~oa7xL_P7mjxEgVVf3i9^rMrj&kobq3AcrsqsU?#!&Ia zj?`MSbbsbeHWh}IeX!=Rk3hcPdFpM%Q830EldEbHmhQfu)z0UiHbh?YLk~B`6p8An z-Chvhf$_=cd$U|>=7n~q*SGa#tmxe(H=1(CU(>NKqz{*TS;RMgon@JWie#wT{1d{9 zC&0k7`cjnnne-J87Bw3%;TyGhqAiX7%iid!BGfFLgi^-01`;b_+;VdT^^jYuo^FSL9G z*VLX>DwV%3600+?SNsn1cHeud> zy-1FUrnlB=Hjb23M6a|gtjn7*%51aXVb^+IZ-|@34JrC(Lpl{bC5rK>%viJ9C{8z8 zf2Us_p#t1PkaE*mLp*xesSyWlI#Sh=;$JoI3C(MbUXq$ z4FC-p2(asg`zlMud@^uw^6k*-(8cp(lWzlOe0IVI<4)H|U^du1p|waDpM(1t1z zZ>O_*F;eR@}_BeS(BBP78 zG|z3SPK(+yArBQV2}&VZCjdusNsDXUsSO&w(+2`r3z?%i<&1Cn&h4ywd8Sa6yv1To zybMiCGb0aEuQb>bB~*w*^XOMh_A0_0c1l3`TNgElOh^ z>x*5l&%;d)83IltVB{TnE3dJh7YW9seLB%{=C}PQ6kiOacLzoXJ3^(c zHs}pAsDdfN+()nO)A1N@M&LBGI5w-Suau`2M zE-RHCKq_3k_`9oph?pAGkP4)j|?ig;f`nsuuiCRICsCpyaa_o-_nEx~;E$^G^3KVY@P{>sbya zq?Faon8^wpC-M5_!1YCy3hl_HHKPk~4zYAl!OA_(Rh54yQT5$22Py7wKsFVEl$854e9&O0T~BT>njKG;;#A&@-pYL0 z1~drw)@n-bT1`fYBKKa?#Zn*7s;p4tFZ@b_e#Ga|K(&f^qjiRTcDTGeC|3gF9QCnu6Ev;e~ zBq4i)43md!9OXVoV>xw8+*e5brM1VJjW9iz4BA`!3;O*Sm?7iQ25^m;Gqlk|G;yT< za6mcYkRj{+g&{(`%9vh%68a_r?THicE14bAonxk-+eYAH9U56YzxL#ojWbFwiTsg! zcI@Hn74ipRevWP0d_wE4+LCw5Fsv__tK}Q(%W~JV;v+vx7xxCRCE}L^0!r!lFG>;& z$DPZh0?dFM>A@rz+JX{cd?^gMjhzkx7pJ{;K3N|wyT*`9<%NtRw<%GHm_*$+VCR1hd>^a8CNw&R2y-QR(5YeA`4JO5>rs*Pu9 zVIoXSz}$ZV5*sAWFS%9sZ3exb^fDXn>O%`4MND>&f_cZm^;|H^Vr20hOye2+qICR% za$M029c=f39o--H56v&HPx+ad!94|eSz((UKLsoebyCK+PUp{IXE|Y zOnRrEfVKa#ktg8U5lDi3`)m_zhZb`DxhW_9Nv%s~$bhYQV|wRcFw*IP2TH^eVy_@eSP=;M~W=6>i* z*4(r9JLtN_9+hNMicE(n!^+uPQx&o~TNU#{`{R1hI6;sl;KZ1Vq^cWAR@Khxy>+1? z+$2c1vsZxQ@KfKYbPbwYVXDp?5QmfU+J6_T%qN5CTn#pQn@iU_1&sI(&DP*Lp9oEy zXfjaYf|B9^oIX<+nn^qHgOP|^b12c^D3c*{T15um^_?0WPO<%&*TKy}e*S}i@Vh4B zpE89fB=I0Cq6}`VjP~%^0^wtM4j<&SFM9*jmm`~%$qCv3h`XINDN@ri?w;}p&nCiEnIA=o3+&S__+82TQ@|6}$Kx;s zXJ?7NLUaggry%W+4B{xtFJQLvoM!y|fGM}W_8hvdD|{uVw~3_IMH5!^)iy7n1}H*R znNCI-F!7X*`Rd3btwnt8nF&1v+Q)8&fGp`_JTX}y*Ei)2#j>8~t`0hl4F%X6K()=h zh}E#YuSusjAeY>B9ml>?P|z>G-5IG!{GQ`^H*@sp|0qP%=TW#OonL2bWb8wmQeFsP zr)hCOTlxoUHr`q+#9_1b2WzG@c`sJdod>>!i^!NBRTt$ZUK?uUL0*WKD0@RThVPYD z6Cm6zM9v0~M`GDzXZ%W6Q{+pLZexom_mY(X@y7n!3`^+#B1Rc&M^54Dp^;dGA?ia@ zKTWbIaBkr6RZ=4+(HIy#h^lQiLXX`A?^7Vtqv%oSrZc=8E_SQbDq3-KQZJ~PBOfGp z;4eN&yywYu*_DF0@2XYg@jCHoc>ho?3#&I9u>Maq$w4fe?0F*oGfd#gn*~<8dojbR z=5Lq=b#1G+D@n#BNo^mmB8^v*C2o_FU{Vs=1xv9A?U_WN{(kb<;)?bh|1(xhQXz-C z_8idI4%@?TJddv#eS#z0*0vX*&W?~{;1=m${YIkhAnjIHo0ZF{2X%3cB<+%+6=6bJ zQa|e@X14(eIBlTW`Fpm5#gD?3SJgH0X$~ zL?T?vSRcX5C?tmo$$@}DP2j{(*&81-Ika>~@Rkm`B&L7#GVQb-GRN-KqN-E-MnJ>P zL_b9Rw3#&m@(*|dcUK?o?UDQWM-5LX?hdx?9C)~o>EO7ihCO{3p*%C9dQ=wgagl&v zrO2gpqwe{Q5)$Q693^*PQvB8lK4vd^qZlJ~H84;;3GL=}fAYv>l&>J<0U{kYFcg3` z%x#9s36Sdv@y>EYFZH-Pd4XU8OCM1++wZGe{CI36D%nOA^q;x=m|E@)(gCEsgC2SM z|J=}_kpFF7iej_-&p!fCItRmrHMz8|fO((~$U^0ix@B&Vn}r-2pbMz#7CViY>D3V; z!X>QB3(sk`NMW$UKC0U8;EC@*6tNC+r&Kfk1a!BVPW&Jyv@ThpC5o^zTn+YouBb1A$9A9z!c0$oJtW-Jouj-Z30!` z*PP+RPaTLu@4S(A5UDT8EI!joBZN79XO0*J4XZ;*2=UZ+lJ315TSOZa*W21fCd+40 z_#7QNa)#GW8#@ZQEw8Dzu+F8n)j5I&@ymvlFW8r0f$AP;DS9Hcz>v5nKAE!KP3 zp8v;sjk2lY=Lz`9bZy)eeF1>jz_mh95#P}&Y*V8epN7JZIP`-vbA@uVnKQSqSJzaa zBgxfWbTefNNaw63or%jhO43NcL?*!mnbozU%Lt@$zGrjekd-%0c!z*>J>iyCN#Cd? z&0_e&SDj$i>liv2ww)jRkXjdHkTw+^jmd)hyueJb(qH4o$0jvO7o2d_*qEA>A&ch`evo(DX8ikg1o%q6;_ICJ1;C!bmu_}; zfm@+|(-B+k4}OoW!ev5XVH+I_!ms^2O5YI1MWCj9);sVoX;d&}#LbqmTA*8-b>clb zNMa>aEN@~DP7iUw&W|YjErXW#{_XU%1X%Jq)7+kGWA8Pp7wO>nA$LnPTNYL$ivpM5 z?Ga^B=_@<=263hOUrBEk!fD(|wMBinE41W!x#+BB-C~!KW*rGqLnBOS#59p3v+M)m z(W=A6=@ncbOYzAxRTLZKFEBxktK;C)&<44?aOb9oaGCE^kA&+5!yDv*AC;Au>+sOF ziC-aElmZl5%;CeX!$)wRIzNWkcK5pr%s<+q>iQp_y~{Jd~wwjV09P&?waz6V)Wm8{BdUP5K=yt?wf*@!&i&H1$F^`hCj?jX}{c(V5egV_vr zLv-j=PvCUGQsc$gnY{Vqj^e1vg?+mz>2A6|a()V~mOdN*lY0g|a(&Moi=RjtAcP34 zrX1rXYFA8R+*AGW0|dmO^oo_wP^1}y5hB*&x3n8!jncufV$)VsO;JX`6UHqSWPVlh z_W6+pZe&Z@9PrT#ncve0*-Ud7IXI(^OITR#3shBRwZQiH>hK9prw5|9V_$dZAfK)Z zE5|kU1zw!S1Q5s5J8*+5CEzOiEUbXo=gR&IA?4=`P41n`Vt!!CcD?dHm!K!2R==TJ&i{n;YFwVQwAR(GwWXTI( z*GebHbjb7L%3!Y6Spw>(#mIh3%7`nnugUd~o%?hvluJ@bvI&#*grsh4aI+^c_DQfg z!=E^!JT_wG)!R1dxAYZukf~HNjNK`@IF99QdGQ@`^`<_O|0$chAP3(sYsFLWdT8J; zfZvSbwuW(|x)F`>15entrB^r;;*1gkhyw)(OO%eEDncXuE>LxnJ5hVi2b92Ytr2po zR8wtn+&^_i)zX#ZOxQ%*Y`fxOg9PLBc7KLEQ{BY1e5GfNMDIJC?b|_;ESV+T;fU^; zM}OdECE1#JqQ^d!ucTYkyHVC9EGBnzw}yay!Q#~oW=LKyspOXg{^Z{o=852ge@XxV z-WP+1n;+mNYA=y%_pls>EqS%I10KKxGuByC<@DRr)}DRgMr2VS4zCWNAZ`GotwU4z zbNLkvm&Vwsw9y`od-qXIX`;8q@8;xR4!zL2MR6F*2Q3}}D;ly7-dqSZ z@X45%xvY^u+qrae8}jO3T=djKGm-bcT+Ok$U*d_m;>KtyYJs4O0m)s7vYKS}L#H@9{j}l&fL3xpPh+^O!(Nhce_I62b-(?Vz&4s&zx8}XadyfyeZzv zUv#jCPf{+yr%k4Rp;LTX)dfhoIg5cbsID+Sojwc6!CRu0h?`ruF0svAWfO|%QQ<4O zth1S@fbME^j_QM7QFZDd?m12F>G@FBIp0!>v555)*1_3)9nYYh?v6-Ii)E#$R^~WT|)BJ9(9u*g8opM{mr801N<6C zIQ^>b9MT``f?j-s0wQ+ax_6AzUcfQU_j43UFj5fO>1W;|Qp)FHTEEf?NQu(%hi)!^ zq&Q8`Gq{Om9TgqS%x&y#ebjD~^{Z^lCm`jK4Bh9syT+d5X(-o<TZ%s}7AR;u%}^V!gGj!RnQ7Bdve-Ys z9};Q%u0E!+#R*4&0(Q6aj};!tpZ)G}x_wLA7Aq{#kcQyki{2PVwht@ri{&=ik2V*_ zZC2|z{lWV#y||WCpY9s)4m0g0T&#dTfnQMf&**y_g9Z8mj(iy~Jq1||?=5S>Jo>Dg z+(56Sf1>bIl5&`{<99BJFp*<2j-xGcFN62_=F-c`BQxIT zUH*7rkKXhc_qX5mm`y#&e}6(<|~1$oV&A(lc-q(y8*_g#G-9e=RpKc$jdVj+5KK>@i77TI%U13IBVYg6d zxpcDO$D#dXL!|ln*biL5{zIK(k9Z#X@n~=)%9iy3B9AX@jf0wUzDlgi4z=ZsZ#B#5 z^sdX^OE&!Kjg$2-f$UB}uL)(u0o_8-fV&kqHU-kxZY!&+Y`W(lYzi4UVa%1_>>rDx zzi52|`w9w9f%j&wG^b~$PyFIiPR}yHZi*bT!;ZQ6nW0Oan=18=vxsX7+roUi^R!f3 z&-?Y+wM)GEY}tL|9IN%VkJ8Z2nsArqp;DJ7m%xd4$J?h{@>Hfh7;k&`JJOE_+%K?w z6;t9efGe7ANv86zP28RC_fUbIQK!AOZ)Ro~)n%0*^((vW-5g#Y7mZA&X|Q5(6ED+3 zycL&E*lw{_oi=^T@=eP#7+O62gmf<0m`=+cS8X$655C0q3c6ANy^a*Z!4*V>DG+=J zNMWl!O~x z@HYbX2f>Y9>$+m9YS&Ca(PqB$_*zNI;M?-(>bQBOodlt)Vk8v&g?5~Mvg@=*DmiRb zou&^+Rdm``>Ot;+Uk|$oXeZ$LZne#q?SX8ouT{6_SYEln>Obbis*ZJCJLg@RVGyFb zAz|Ptg2dD?N@0?n{2syi0nc`uyoBCha zor0^i9&5%w6mZ`~h)KgEzU?6v$L_DbWJJsyZtwqGrgGbxog7~d+`80O&?`wC_CG*9 zZ0C%xZo+SYsKE`D?GW*vwk|p|MRjdeH+P4P?pYp#Q8XejF0eE5J4ZSyV;3_P%CqP! z>T=2AdxrbX_tx=;WjTs@iF>jZUR@zdT2PLXzSqD@LhS_;BBdgWHSWjLn@=E2iyc7 z_rvNWne1H2-aCPdy;9idpetR%A5GIygIgN=NYF$pR{0C9jhy@4OdBcKJ!Nz81RL%( z$(Hg6Fn~0;RAhWf2LFFf;1s2(mGzkges^{zv_;Lxl4ec>&^ARWVT2IaO?EOHgqvNk zS&wZ?E}l!lRQil>8r1$2bZ>6(iI+CWl=x|JKu}N~sxYsv**pulvPWA12Y4A)g6{l* zh1htR_qo9kl@5NPy%qIWhYDCXr%0D@`SfRdcrsS?MA-xSs1HGCQ1*87Dpm=&DI^9p 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
Z`Oqjc^WfMrF+U#SW4ypXzn~!>97(M_HSra=5CZ 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 E0c}p5UH||9 literal 0 HcmV?d00001 diff --git a/docs/images/home-screen-dark.png b/docs/images/home-screen-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f310f90f8534c696ffe0878c462f16211b2a45ff GIT binary patch literal 142222 zcmeFZbzBtf*FR3TND4^TE(l0V$0D^fONmIgl%&$FfJ!Y&Eh!+~(j_3>N|#bgr*wUX zTl_qq`+5HT{rQ8}GPCoVx#nCa-sgSJTtm2qn!+8t`*}BbT$+eEFv5% z;7IDS<40hF>IzekK`9=fSp&X=SU!PTsj8wp0QPZEFi}ZR&~Huweo;{GqhS8CkAk9v zO8uXG7%JQCIcO*-Z){O8ZqIoNY;S&|fe&!??G`;9^?yc8NBes=I$JvW-+e5io6Ejw zDBWCz`$AFQ6$RxU>&*uh3S-#?4$|1ls%ryV;D$Fp=s3V1U|Kg@;DgFc<8>w*f`THA z0+oemze3&g#jYVAtv$MgBcZE}uA<163`ASTF$gF3iKacyeQnuqa~hg0uY<@j$;iy# z-ha=Vlx)`B$lFh%oFwG+zJtX}u&lk9ZR2|G%kh-;kz~b2VWYLxpx7Y4wUxSo-Iy2B zU_-+AJnH%+>RRL3Bvv~LuQUM>`u}W*hzLrV(ME__B3OF==jfkvz(^LG|9y;?h=HJ# zAMFQ~Mc+jU?!N|;{=q99Boid_ukmk23j8W<@_@uWr|TH$uN;PI7C^~>^`8x&9Erfc zj{x6rh_EK8P}d3gtggmG{`+h*hIL8?2qT2?e+Kw-abSxH0}pw)sMgZ?K{MK{@;4~MxJ-lJ#buXd$Q`@&Tg3?-H*C|1ETwD3+pzqQ-v|@pm`|9!eY)xy%8w)2j=oe`^?! zI!7@`{{JW#wG~B-Svm#M(RZEnZ=q#r(OKA7*#5UZc!?+pFmjkQts{K=ud4s8VL)mv z76|kIQ80>WKnijC0E798zv{n*&WuDQAtNFCJAwb07l!}~O)DmsU#>~&+Th_oxE!^Qi*R>VRN%v1c4$TXjJ&Gl~$GhlU* zL5Kb=EAWj$n$pA?BbUD`;nbM)UqT1wng?KnVub!*D+UL)XtS)V#Ow@SfBd(GiO?+~ z{{Jxfms$S550eOp#?}1wx&Qix@paumScEjW;}H>f-#$115l@Z2;{C1qtrO)uUz4rR^hw8F(PDCE z^<63yeUwC$NezO;HtTy%oS($+9#t#qcVF#Ap!OUrbgSPr*J9uxPeP?ZrvT9hvvU7$!glAPt8#H(8|&H(96;JLfXGwmxb*&_%w_3XdO9}@ko0b2}~MRV!wH9 zvDCZGlejwdrQpiRwhUzu2}>YBA8tbt;ECpn<~{9UH8P%fBJWApxbv?3gRc=Dfkr9? z2u^FBh{TpPWBA5^1;3Nv8izK)8H^T38TJT^!H&UIQFk*+GY&a?RmEm7d7a&Xvg`?Q zX-H3If+(eL?+j(23$p$WXJlu^^7)2xMT)I2%U{~s$ywaYj zAKzq25a*%p*cqQERUH$GM(a!A830kpLm3c-XF(CJv@b00IpEap!#qQRZ~7IOP&rM! zb5Va_*By`fixnZ;0l6aFFQ9g5HeM8d)GnT${TrnX;MzU`Wfg+Ul5Q-o89LJNxoeK{nMt@q$W_?|>iQ0_wsuip$J8PL0ce&T4c7^XAH zmf!9p;3Fd1AT1t}rm6^$ExjG|uN}?{<{?#DEz*A?&7M9}Kqx7uTwb%Mzn6GYwWm!}mdK z#^+Px35=aVnc2dk=Bu5I)l^_0P@?pJD_Kyxv;Ysv zBo0+#C&RCeFF@F|urA*{Po(UkM>mBdJtM`I6*NRWHWgiDT>eQ{()S6Z{Xku_RI7?{@%!3RWA1FJrWO}isO-e`I(2IjVOxeW2a65~_a z(#EjBRZ|AeeQ|Y{ApeLD6p%F_B)g$W*GaG!a_TS21}BkgJSh59Zl~&iqvf=5N0Cyu zOLJhgG7REVI`zCwA3Dw=FTHp*c@LOzG$DHQ2TTG=*2Xvch!fQ}#&qCEVS%~?A&^uG zp1HWYkW&B{AzQd#!2hUcDGar%4-M;m?95}&InmRfvhN%q0CSk+A+qo^`YEg>i4p3& ze~M~-9^jcu$%yromuN}&EDl+8Dce`hb(1Ekv4S`UnbkD_7;U*mtem8Mx0gnG;ypG? zzW{~qIffWZZRlu4>e=Yaj0xsXt}R23^P5iJ3R zW~w)(aZhkM&y=#8q%@wv?I7>2WIP>9{7;KYxxxciO&G%V#YZqOyBkhW<&o$X#uX*u zsP&XMuo3wl*LvSxr_e1Zfh;~cBo;~61@%mLHEbmEl%776>pYpV^lc!+B$rm|^p=4i zhrPNP!&6F}Sn()v$l8xQQb9J)sT3^}Ic1Zw=E?a)9u%r|XNEElDIiXga0|p0e2CB! zd5Ks&x1FEiHs4hmOqGECC3S2>=+PN;*d%txTB+8lJ}+E4jsSiVsB$y$34GV-ijLpF#p9>Y7LH_o7Rdz#v zr51viwo~A}O~=O{<3>;x<>hPd81lmMHlF^jQ?9E261RgPfD>cf3K~ovv)9z}Z{{dl z5NbYt;kUCXbYvi6%7C*!!z}Vusr53%W9nRM(!p2OlrT=_@~Mho=7v-?-)I|cdq`R|sO z&QswaRoKfpPiwQrt*YrlNvZj>U9Z@{(tixs`ijKyj?(e=EPIWx2rJ;6k0>Y1Ro>r! zW^jTr{0Nd)T&QubmaF4F`juvr@m3%=Ps!h_{H{#k*I5J-D}|40I9Ft`?-L=*{Vvbm zElcgTpbzvOUI9qWKX>pm_%3MnppE{DXYnw!VQ0>JCq=tsAVsVwUQ@C0l{$|l#d)?v zGX^BpfKnasp9J?BE}qk}a@EXtfmn>J+{JVmv@_R@rdgt&TP|3+9_RlkwlimZ{T_Z1 z!;kOkH|Jzl8W%${@$Vt~bssMK;gac>tO)%$R!J8;;xc{Le0MX-mQ8w)Q#`Esu zhEURT1$tir^GT=XOOKJmlf4P^D9Z-qj{mjyhy)Vhs@AcAA0x-YM$?A(_0Ep%H`PD> zvoqINgs%|PK)z&;UnMue6QF4DTEpb+1uJqpW%8cXtk_($kIjO`!w!j!S+La2|IckyT!vy43ikWbC4>If&6u@G$~9Gbn+ZJR`cp<%2UQc`@-J&XMOJx>1r<@ z+7TdTf;hkMq`&f#*I4%4%1U%_O%)~)xX-sx_hE+Z3*7ocR&#Q4@@TPPJ;@TP7{B#^ z8`pU&KDSEU!0BQDI^l~(WX)ovtI@~NJi3sT(l$aZyRkye-ItT*Q4&AWA=|Yf(pl0T zrF7n#Wzc1x)8+VZS79l~8R79Wk!4~u6dLy*ns}1@8)HRUx#!#UJE@0FrfsiTKa7ry z?C?`S`ZX%R7+82v=%e(; z1ZrdBbi9RFiaCy?VL2dabsn^@?atFTcK4zNcd!+fQx}@o>=g`?EZ81tpa_chQ*w1ss zcOJO-@C$=zn$JFTr1sleiqF206>2kJUUtF%n%MdA=YXF}aX|SQ%fsVg^TEr#NDM0C z{POtv=M;^!2ffbsjzat-(rEbOoO>&sW)*&pgMUp)wCp+i&hz>DPSd`u#tZ-1zLAiH z*#=M75tg%5?``PWqoHF4VtT(5(*;r4ZWoedV5)Wf5;Z%g$TantU|Jhi#FHdJJKYi2 z*oSJTd%)2GxSA-tbMu`bmoa^7cHtk$a-Z|*c@IHLS7x^Jo#tk=G9^Qxe<6-IxjW)V zv{W0q$1;Oq*%E+?i%P-rqVyrzv-W^yN=oe5@?tKi=V>c(etB2Oz*@aoUtf@lT7XU$ z3?Dt>vo(<3qvnpxot9V}@g8}dt1I*A(t53CKo3^+j6AbdYXKK=8WH3Ptvi~sBkkm& za^w{3UMltE)@ncjtm>Fr3dAQ%p4Nf)n*yW`0UwkhVTPd(2eqpssP=c}CPO2^t zr80GOkaq1>MDcK{&fPBaYDEHu+vDVaZeVzv^`^r=I{k!WQ8mqZx{CfkK8B7>XwPhd zGgf6{nOv%x85X!F12a-)WWPy`qbtPg)s`=^lXwcB>Od2IKAYYhw@vZT!PtVzrm_cc2DAT1P}7n^?8R%57pyZGp<9?lUF`Rlh)7!+mjsOl3?7A`b{Sb z_=2x?O&nY-BTex~78b|Hlu5-)ttEwfue=osWgaYX*9Z^c> zv-@>GS%8-hS&FI8H`wm+BUcj-6&2B2;&;fAB0YkEOUZBbPHFUEpv6~~zSBbOAhT55 zOxN?RYP)%OC_cq=DXNoN;hkl_ZN69sotY}UAn|IR)adlx$L!m0T8}Kx&%%K@KO_po zPa+WRb$TKoyqIN{Av+s{wG@Sn(x4Z~bn^9cf9rMH3Of7_f1S73ZrhY2+W7r~w|71cB$ zM*rH1qEbhQ&@RXktQGsbc#(IoA;``P(x0qydNxw{P^uI%I$lFczN9Ilw4c*rE5-S$ z6adYUbc~`DkmQ+O0fyzmr;>OK{P<@s&=Ho4*VK-Rarj&4l_0Cy)y%gfNFIHmUS;ma zIN?Y?XVKBcq{oCJENec+zS!@v5c?_0>q}C`LHqH|7vD~WG5M?A4g;&Y*3{8DU()c8 z*AvIN#SHaH%)D0UkjpE%_=vpMbGI6cRmo(r9)?4Le#9HF`&CN3OmH6h$Sl_D>TafO zB42CDNk~yMCjpz~V-2GF6SBX`Yl~9K4b1qSar*S7NlDqZUj5KgXYCQc*lmLkY*iW9 z`4Wka*S2utxntzLI-Y$=`dON1eI4)aQS|*^b{+4i!p|}qd~oo(RM+`@PIj}9*;TIj zCGYq%pFDtg9`pFM2&16tOp2pO7r>#85Q*Y3Dbgpy48rF_R(Ha}ik>FIE!qa_eHJ+IRV#5@f( zae8*Oo(kS`^fg(0qNDS{3z_ngBqWj*Ig;fobb!Y zTA*AzXg}@xG>vLrP)lr=rc+Uh_h+?uwT2{aL>h|OB$-LOv=u>%>^cVSor|~M-xNgA zN;2kUpjE{nvEe~85kf#uqJLU{r> zXM9yYO>wEp$jUCAP3CpSRJnLMR@q6vlJ*&C)EbI1_{S#i>USnHiQ=4(rSwRA5S}ZE z(1ZOicH`@P$N`Mkoiwg0$#u_O&bo)6U+r99si$2JyljFCzot!y&NlNr`@Ob4=zrxp z;+`k<^BlE5QcF45u7N0Sxi=n69&G*CJZ?}aD7M*0ZOi|P(1`ksYmh5WJ;-daO?0P0 zaJ`2^D6bp-xsrJ1QR>b(KcUFdi*R!Pw;>37v1sxFHNQCh!k52X@kyt`f&C_1|Ob9oti%vml89a4BICk^Q-1bkGlg&{5|hbMHB&<`x{z(&ZhW2dbP_LhAx8LWw!;~%hbod-yW8Aa#yod5NuLox6C>$5O?a?BKLViYYx!8KL7lx&ssWdxv5FbE7B4jk=o%~q> zo6U~Jd{job4p#b=4WhoQiE+(3^l*85Jr^02x^CF0_$7P&*7zc`zd{QaudfFd|A9lV zPLkB225^$B-V83K{vxxfy6kMZ#;dapTzYch-%)fDqI>ovajWm8u&9@-4sV6VV&f-{ z`_-nPxRCp8z4H42Y$y`(YJvUvuhw8~cXyZew6?p(nQFqnEZh%Ag>3Hh00jBaiKd{) zfx6YL<6YwV>ZkYuoq~rN1$re~oir6PTeV0u;z|dwy!Y0ZD8GAs9nYUXAN4!u(gvrv zB^BSTaz$3Td&$;k#fGkZw_ZITD{j>8F)Sh!+r$|i8cJH~nfwS;m8B0|XKMCvf9;4z z)%~2k$2kFB^Ld^);9X;!8r-S6K#j06F zMY>zj>rbb2-*w(Gd^wwasZQiVc?DM7JOL_@8i}g+SeRBOd4s`|lzbiB&uTO}Y9-+7 zyk_kua%|ES2%e~PiyQ#l^=A}cm+xmSH? z9Js5Z+4r=(<7)d_ku|ECjyTgQwelBBF})du0{wEO1lE_Y zm%`yHM~ffYD#uo?A3Esfaj47;(d9hMp8Jy|1vb53Y2qlg`Mp)1Y95q0M!3!uAIHSA zP*+`@bV^m#-UXSjZ3nBro!X(rKjNfL~;5)BOI7%qa`oqkwgJ4QZC*TC0lG zkjLhM*DM)@D=RT(o!*g>gboqx^iN~!>L7SixY49Gg1giOS{!H&I0MG+qleGO3MXX< z@BjACJ(_WKa{orIx8XWulz=p|!@=ABdI0bPP*U9eVLyS~U_w#Z`C#iDc}9K<)Im&k z{mNAhK^Kd+#iXsM$#p^R7^X`rR7lA1yufrbM|SxSMvdErSd4>y9eUY7tv=&I?ALKC zD#%^2@}!C$_HnOFwLSfGFVR}k-Q&pG*Rx+yuqbbopnt|7qUcXno|^W^{RIo9^f3EI z^=Lw+F9Z6*XtB!w@^C!4mvs75l%(jEFSA0Vwxb1`bH!$hEqxP<8rTQ%l9{c-deCuE z#VK`JUSpRKC>|{aM4k{O_Vp9I!w6mcIDJgiVeZ%KioXm`pFl~ahCpPwRnAPuK0L)z z{6?%mlV>vaLA0#%u!cJYegTkH4o$5{*kY%;{lT-xkE8<-miL2PmOqT+CfaVac<*VW3Rw8=_9_5cYkn1yPvt!nMXo>3?MY>a&`v-4)B+qd^k{vB*V zn<7k^yW`_5aQhz=j1fC0x>v-_Fxq8~(A^dx*7!4~K9}bP9~PYY2j!=-D{e>V}tNJh_XZ^#B=kZUXwSCVA1e5h=DkBuO{mEp_=@>lAzZ9;O!O zOd3AqtNZTDS|E_g1!%kM%sv9OrKp31v}l#X^haGgG=9O832nXlYf|9Z>Y^^_-J3PU zUzb7wSUN}zQ3$?FXt_)M(N~)1uuEQ}&)w1Bf&$;dY_Y=*j#0zNrdYS?0hHUc={sV} zaZ5N{uhZ=(qA$de{DkD!_v22BoLG@cjF61W&QgZgLMvm zR(0PSG}I{ZSoxLaavE4N1qE!`k&{X?5&Hx`>NuQX^rdYx7Wixp! z-}CRs+Hw&eYvX(k*2_&qzt{{j2|7GgF>w0iL&Rb&`9Wo{u&77}=#G$G4?g}TbwF9B zm{9ODD{YM~bumFljdJ%dwlmD);9Wg-9hTcTT)LdHtG40iI5D>%Ej=Y7BT7r;w}x8H zON=(1)1`lWc=_?+-JE9w$9x!=pr-?iR`iEx-O_~jTnP*wYYd3H>Wnxe1$hoz8()mQ zqys}TN?$H8y8G`{j-011DY>Pu)#xKu46wj<+Jq?&2I}>YIhZ0weQmILsw*zLBRisg z)lX-s(I0-e_bxC#w^DZ5efPtf?BK=)`fwtA+RT!sT}CBR6_t8N7Y-Of&EGb~B$M|) z0?i1-ew(UIv1)@?|PkfjN8y;aV=*BUhL+6JH7012(0)X=kFn0oyZxXabk9uAjH zp*!#{Q`HzreEsV(MZnDnIxk?-bW8MFS6PUv}%E;Q_?VtBFFpLNxH@*pzD=+DrnCV_?jzZ1xsgkCdrt2 z)pzhLbjRB=t!$9bwB-+Fk5i#VY}DFgd^@E{@Poij6_xT0?Lk3k*e_k_1t;?Tt?Z{`!hfDlbQG z))!}^!OJQK>fetbbg_3IEm=LVOnyPO-*Eg*$TC%UIN2jKs;7DEv-&Phr?;y^!ao@r z#FoR&fi0L@M|JSyXK<8en|kdNLE}nQCv19`LdnR3m8Xtv;Is+r%?g+Q()KEl6&TV- zZS3u-1@SE%CtkcyO;=CgVYlVsKh*$egV37rb|SC&$0qDrg_(8V-LG%f4ypv%aCpW2 zEPD=!c_NF3EtxtaD%3jYv4(pPo&8O|=@+RCp8*2?9!3`q$?PqbB$*MIuzv(1IaPp^ySQslir32bk^2%s@ zyfzvxW;IZF4oesY=yYH|+LD@kP81F{Uw8)^2zul%3L~`dB9=0Y0R4ZW3hTuT4qkbI zvEQkV7YX5eEFT4vF^mP&*g*0X&Aer5IJ?T6^ZcZB8io28<#|^OJ00@H_4??>59iL5 z+=`J!p-S=$|FX31$d zS?C>gA9o=4M=U}$nD`Q@9_7L$Ts?$+)cA-$#y_-&jb@_q&5sSLsrr33Mo5aUHPk-j z1ke!=S4p;Mt<)sxkVPcy0oFfF2{5D#)kr8P2+4#m-W3S7!r2c9GfG%$tDPybw^5b0 zKpeMp&pms4F!W4#ka-7}ubY`zmlp@`0`l`3?XwRNV_e{2dGDhKvMBuS%Ck%nR5E12 z(Fxwt8oS%rG(-n&8Ccm3*hEXePufl=`E#wAuppw=X|H zH~}m_B8jV9gk;n#`7TSym-@f`-h7*Y}Zopt~@UL7`HuresRRvq|lAN`=A-r(N3%JJSaXAHf(De{5cX0Z~VF5Iw@Gjs#h znxhnr0@a38sh1&1!r~${&UsT4RwDlq;e%@7zWy&d>IwgEJ5sl7^CW}p`mrgsLdAQ% z0oorO3e1XA(K72U^ZOa$xDz(RgtKLZnqQl9Xm}h=rGd3=5caoD88l7SO^-dhmSxU| zZ=pg)EbUnCe9`*adfR^q6}RHCkLW(ydyT55H!C)Km7<{*ar_EZufYQzY4b<`kZ#s} z={MhPb{^R0?{%zF_pFS%MaT=v-bHf@DXaqeC^F;sZtx2p`s?Z?r|#EZ+|R+*$;~gC zb?M9PkIy{kS_6Rfd_l2?=ewkfoMKNFG0s1|y8QL0J%T3{z2M7WKfPMD#flFHubF~a z2kM&3bJ#a9HZk|0&U3++Jkew~egk;U*iP`Gj9Vw>k?-2s+}~~mPxiUV#MrRyhIbV0 zlvQl1sO{awowSrB#7^g(??PO*ZobFq@Y@moEd9bjLBF+XjVjM8PEk_ZBX{Ssv%HGw z=ftgwim(lzY0dZg*z;wN3w~@+2)>ETdO`_2jz#0)%~0WpR57nFH*5<*y|qd!hVa-2 zG(Jwsli~2TXW-R5(yP)-N!z*fLJUmbkkYm_#hwhU?{5f?EBtTODdJ1-w}7y8rFoC` z*Z;%OG)^PQy0SZ3a@w%sX(6ZMXDbadcE@xn? zFLuyW=>NSf6twgKvi%PZ0QOAhrCZvGa>uXoAuzbUg{f{I3Z!=ENEuD&!;FzV8qo1o6;&MR)S1_KY=l6^qvm={;hg8w& z7$#=00m|iixySd)(i)E|{`xRcgFlY_<0?}SK_9K*r@5v=2Y~`@Rk>)tW^d1mr+jVo z`g%DYxKkpx_r3O319Xo1pS%T0W+|0KY{oNVbqMI@6)6yC(qqgl;b(*hanogXDEao5 z(epLJZ$O(Bg%qyh+viGS92RI}a;tJOzrx{@CzFF;SCJi%59>j`)IWkd@9GQ|-_N*W z4{71<@76PGJ?voZ$J5{0F_6@zZ9Y$L*hj=s=Ku}bo5=jdbfhzy@1oI||D2{cKUtw5 ziI-GoE5tV6W8X2~acVo?u|r&^@u%YGaZsmDwF7__v)y%ctib?mzwq#MHKfL~08PNti~`^A+A{3z~xqrr2eq ztp@bmD~`Hqj%s&#M=z(9+&_G&kA^#zZ!y_FE{ZGq!2>ui`rM#U_T;5PzT>Z^-nT11-spIZgy+bwxN>4`;eXJ46}0 z%yVP_JC~23W5^we%`hP?o8k7;-S=29L$cfY%eefin{7X@muvP2pVEywMJ?xNdT8)W zMN&R|>vv1nvlGxI)B15)0Y({uiGHgOpX3?aJKbGnya$Der=%oc=$iMh#e8oBf-*K} zekfJiE{J@^XiE@EJi^3+(OEjmB3H%0M!VQ9OB#F9BngtuZYEjF|Jt-ZMP7c+b&uty}75)yG?~7 zX5OE7{%Ck^+V`E)MwYMgLw4N2Vz`{9{8j#>dVTQ((DF~L)_3D!Bt1sO`;{$H; zIx7GXkkztOpaW+LVOE3vOf&O=m6r53O3R02xz*@*sUQ+6Ni#<@#|RlXGIhRA*?zCj zKQ5d8(GsH-G1nCLX>Q3zV`}g=@udfZAF|!eXz{afttIb@PWSQQCSo8q#Vs1}B-n$zUTo7wQ7G;c&7oIMX?;g@ZdHiui-BYYC-hpeLZ1 zJ-Ok5-_Ca;9v#PC63VDGu}@P%!yOJK!+KORzfA5X-RF5pm9lwSWO*@mOLuTLEP@VK zWD|g@$lk2wCOx)7wK||$O|Oay^K?cR^vu48a8Nr3dg@W=*UvsA+?7!(#bg}rgupCR zqsbUKr;cT-p2C@|OeCbuq>U`T)oy2Uh1+VHvB-%Hv{fpPsWGsWw+2e)Kx4Qn!7iK0 znGjTc-XyZkYHVoCm&-@tG*xR7qfWz^cn)z+_4{Z0^kTPy{d)8CivGcQg0BjJMQ6Yb zZiQle>#?XUcZ@#ys1hg6GWi(QnwjP_b;somQ7i`3wg9@ugz+uG+g3HgMw`fwGApaX z{ym6Q9%jZEmZ2Y5)gHpoQI&F%+j1?da2x?A---&tF-b~j0cY#LgUpAr0P}7Z1Q(`Q zZdD$-j(Sg8;eHaIP@2}#wTk~>YQy|DrcngO%EU3S&SNeqEeLMYj<{;9ATJd^`)3-~ zQB|GkKSu8?q<89PL=ZU`~SQ5P|SmpipXW{Z0Pya}lp) zlYK-g*@sFQ>f&wGrPbag#*bxnIi-N*!l11)$~d@+5xmDrMC3#yB7`tBS8mdvCt`iQ zPDEtFLyzH^UmnQFKNVzmiHat1ICB3q!*Rt<`|}f%T~vi%=7WvAY`a?LFUYAtD6_uGJqok3Pmx;};ndgoGkHX_3r~PO>}+&7VqE*%Nv+7Sa@jILnsHKD-(O0}nSJ_XV!+cI3QH4}i?aFbg{$m|H3t5qUI;jf#cw8-@-iMtU_> zJeM3dqOFLrI>opW*$z({ir7p&;qKm~p{y`{iZGl=k+!?ldr;mhFm5Xe(85jd4hLn! z$8t7djqOs|BBF^xXkY8H63;3hk+NXk|5&4oIo$ZOz-<51>bnD%z69??jrO7> zG7*aLEE^Z5bDp*5+&M-A7Lh4Slq&U=p7HjOX9yNdNp^+@v>B?z2PNAZekBiHN4jhk zq<6E9eJ(8xwCCS&7_k~L5G;gNf?R`@)!HG=9{ZfUV#%C@=jJTt zmpD3;JhFWm>)LYeUo7lPRQ$jf#d&(@Zv+ZZnLp=WX&q@^B7Kz)rEET|S8v`QJ&&u` zPJ5dMo2~ZE{lab-67BZpHIO#PfGiMmFQrej5-cHWm-&UQJj!9pBGQ=)Pf>|8@smB| zNjrs{Snq~!G?571cxpg&i{7_9=-1OogXRd!EU*>)yjo?d?RE-k->xkDNzg<8 z%mD_1Q*+Em%SMf41Y2(r)3`9n=6SRV=*$Wz>5M6xwLM^D__oQ<$d7K2Q{aa`sOhVF zsp#JPi;Iz6hB74Arqhd&88?lL2rdla1sRJbeu}1W*v3@F!-@FLE$3(F$~_sB+u}@D zKqx^o?}18%Y8o>tJYWasR)xi_yY{J$5Wf4ZqfJ9bYHpjl^_LR{#_niMEWO+6@OUfw zhkDq8@HN|Xm(#s;#B6h}+cBnM_Ox^kcUKbRS=cW5eG1nre&da9Of`f18tZUH))-C0 zQf_h>usZulHI;1fZ4eFn6c`f&GelNV3Cw#va=uTUm8AnGIiH0_f-Biw=q$23LkDq8 z&FA7=&eSe%Rycqm>|p7AL*Z^k!yFE5qaI98l};SSGi)+YAJHP{URou8A>yktsFfcq# zUT{N1h0=}zNdp?l-Sgh;UX@PqdplU2{aJNBrBHp%{cMIR|HQHGKZc8|l=-9r={;V? zxM_jT@cbiA_(=`w_q@79$pp84M9E_cy*qUG31P8>;L$Jn*K^>!xp>JE(GJzGm1D9v zP;c4uSO;rHjwxrlr=&#WCn;TcX+VgF&QHzYBST6lE4UYh=ZzGh@iibhQgWhuj2tPU zmA|Kt1!CjeE=e;N|5mzpeu#JA(g2K9_74|}BT_XfTrlwAy4V%UOA!@YQHL37dBE3u z&NA}H!>ixwkL%%LdoB8-sYXPeWLIahf7R6`B9%QJQTxpfr2%N_0pJTE2+B0FEgo)w z0{FU4hFI*rdma1F3-mI(AGd1ne^Y3sy`@0o42Q|0G7 zKM?>8FSN{g?^!OaUO_pX&BdJ}x&_j9wT3a|DmfY0yM(z7HrrF2+U=E;lf1paF2h!{bO) zB_l!uebNl{wLU!ut8pjt`)q}*@y^GFeW_TB_d7=v);+scAElFImr;~ftDKCt->awf z3r^^|5?aKNOMcburxF}%w#{mRnI(Ik6;Nt3PZoVoKQ!0P;5t{XA*EOF3jW7&ngCF=MD6ShD-^4uP01!Pgt*khD8vOBzS-GftlqP zy-0m2f7~ikB!Oz`oZ!2*YQ(@+?m@&Ap|(N^X6xX_ByOrJ_BB#e;+mGUn7CajzzQe& zS-YV6dZNnSz>ML^pUf3vPolLd6vpye(Q);o=j=M~9&g_LsrGK?`_B%amTSV}2F#8y zt-TU_MJ_edRc(K?iJL+S<|e?q&XOKz zDBSbOa7W-prWb!`C9Jrd-czkZmHoa*pp>%Zr3neC@(C4>ZD91!0I@o5W0EjsVQ^Hk0rA%v&CU9Y zle-<`e%wO_Hs@G0hX_V?(L{6J^?vK8jFPu-gWb59qPCb(8lMN0^Oa z!i#?~7pZhyoEWvjtOh^4w8Qo)f)MkOFhAhAj!1lE8%qM_d<$E#ig&n?02&Cwv@#MsXs~~;uGHrY6itv-Q&n9z zO0(<-9sM9E?$4Jyw+SwTUVLZKA_mY~5AGi{K~dAj`(|^K--LXN-M64cvZw`x)a_7* z@G?G=-vpzUy>Fb2$#@S*_{{hQ>`1wtlcI>p8F^!eh%>@sCU|4Om}>h&nO*C@p=(}S z=kMxt7n=H(lW!N$u*OHO2qMBr(A%YJ&HthCBrV}ekQ%x6Vx|z%Vp=I39o_M0#7PLe zCrNblaJ_kBDRL+s;7*zP5I626snYQ!u49qr+=cGFT99JWRIFzhN#}?u=N#=W;fyB@mlIuq75HXfX=@1beC)qjE-gH>+M7w+o#;{xzq%Wf3 zW;!B&?y``;5JFC^)0R}2>^}M~vAwhs()!w_O91K|LLYgWg(CvgtTsC75aQO6`?DM# z1hR6;ez=;>qJwu6>ff=jGF)!n8s-T$@mMn490;4z@P8ayNg4zWUC1o=h<0x#jfDsF zuwz35^Wu5;p7+B6RSd6~FmRN6#E|D0J$VnTb@Q01Q`QksD({c>$6nU(zpXlZP{VxQ z1Q4Q-jnL%Eg!~yd>}=X$CYM&KKmSNJ*_}7T_{DNMBYUPG)FOqVzguYeCE!6-s!LE} z7#g-$N+0oxVflTqSAq{CfpfKc^WE+9*B9q*=T_fo|6;Zwa$gvHd6OG&kxWJ$HJ@(S z>;ocTa|p?T!AB;^?%e75NiJOOI6~np;yjC?HQqTvh=sM=*@!eQ;lSq_lQ^JD=I{Rk z;DEw>K4RWRp{y-h>&rV|O@38qFw1AYCBBJuCB)mdb^*;9apy(vUw3Eu8R{kXBBOc>6w zwG=)Y7MRdspiFV{fxVMbDuM5SA$yv1w~b)fs>#54D>z`%dXwLbLE;aglvGp1`&Kn@ zZ--S|z_dWZ--x9na=1k{MN%oFwj3hGECr{f5 zA19-Ao~p%CXTwtC%Nks4t5F)?+@kG;jW8|%5~ybBt)#W3Je1C8S_|E(Ku1-#3z~A0 zqCIxk>iR>?bp%%hmdev~(Xe|!ake#Wfrv`<_>%NWM5Q?-H(`ctC`bD3$U$Ek2|a7a zn>&e{2mFc}5tlIsr-+LiY@p=%d0^6xtAY#W2`kqTa&00Hg3Y$~{W3ji!p}1YGOV@& zBU{+^hZjlXP2l?2SP_p{a|Y({RipiBC@dYCXOra*WAxQnmAKUn+0a8MUQ^$Zb}GMNX&vPi`bZXyqevDh)up>>Gt8kt(Y4=+5ov zad@1ZZ{R9`;5dALjLOKs7FMN$>@$S@^mFf>h`mw}?wVJt9WUBQ0t-UdY=Ev!bjx*C zTmK$WI|Ca7i%TI~_Uxu+irk+V@20kF9xU%~R|&TWxwYr=!zU+x2r^PY>kt31#WVm%@lkKC6J&g35m&Pvb;gL0}Xso0^V#YLKp zWv8q{w1MUa?c7-?8Ww&L_D-)a=Pg8p1`N{f+H+I^B)I9%O)z{$o=UhBl5asrOF?RU z=yT&$mhL62)b83P17bkmiFcR^DfbD@jO2SxWs*0Q8Co;YctzWaC}d#2yjKMBPzs>E zMQD5fb|0ftwVxB!o6{~Ge@GezX;cvJ6aDPhe|`__!FU+k0jmp9kA zigKWO3yHvt(S`VSdXiUHH+`FQLL6S@0=_^M?{oqsj^rOVl1p_XJEPSZ8}$Z(dAFeEx!{k7hllIApCVy5 z?_e2J*=yds)ihe&eChDTv<=I>gUUJ=3I~OrT>@`aj7pwwe$p*|miNeUCU?6QN#_nM zlFvIB5L4$eYIySg{<1`#6~MB_B-t34yLj%pmH=(Kz90CEVJX9nFVsj$iS9Xn#I{A% zrxJEi%AKwoaw^k4KK)a!!5);BB7Uk5O(<-BV!LHL8bPq{d;|Odg01!+XTf5xiVW*@ zeSrn+Jr17hd7mR5nHrXw1fIoaVW&)of`Er{XY%17%Tp6j8f8 z-0VVAT7F8TuTfg15+qmE7&W_+N0{fnu(Px`eXHOgQK}#IcdM{@iBxYXC{*LYHxa)` z>mb;uaN$qfiGoUO`Nr#-Q7nJaQ-169u^1t{5$kOQIWo^S{|L~M3 z&i8m$ukmyx*^@l#s0P^T=;)XZtT#s0sGMi4zG#yHTA2X*%H-nB2qTR=a3P3ks;s4` za*iEYt9zO4z~TDbI1+jq1-%6kqLWkfG>48y%ve2lZz(HDT>fju)NUy4ekz%8qWI|& zd=#7BC)yp&Avv9Zicc0oyG88y9Bh4*3D%Ur5ZHJQYeW=j`E@0*+|Ck{-nQKjzarA ztLDqTC0qw~-Lh@UAvr4xDGcN$WnJOna%ePiA5bb|Y)7NnUC&SV3u6%R&dXWRG$H$C zI(TgKPp-BWv*^DC~Op4n)0`Hb{4snzcz~1KQQ4K33!IYN^ zcsodyx^6a6F8jd}6vs=g)Gw5MgBQQpMJ~_5;@(Jn-3Yo99&fT^crM zh6=nY%mW`w7Liwzz3BMz)N4W<$}H_?zLv~IUnX_PTWy0vxe;J(vGcd_!lx3?q8^ym zbU{62uQ8K}(xEIEVSx7}q{dcy0A1xW5VzUIL!S_$w=D@jDny4?-_5HzJ9=D7PgUCy z{g6-#uv~Ok%;vY!BK5}7g>0u4JdVO*9=?$8JAIQ<1N3wk63-43q&6|t!AjKPV)?4u z(soRgSqg)xt^iDWqEcoN8R9m_N86v;*jd#`1kk)!lFt_$I*ZvW*_}GJ{-VgUC zZg|Qw^^E-+3FbwD z6Z1^-W`E+@fc?kU0ZFO-(?a_bwwhC)zJBG(r7!_0$b9`AtaoA|emW^pq2i}An*?2k zwpxxx$X8i2)P)LdcsyCMwfLsrO7<(&;k)<6QK21GUU9XRg!2d=x@+PgZyNtgdY4l> z@wuJ+Jl3NO>$r<_l_bgeGP{x@48F?u#xoYIkM6r)ws}eT(%w-5gPju%Wa0o8OBiS|I=<VU)33puCU&gNA_Yg}ouccN5z zi>`L5WoMR9lyBO$qn8}kBz^t5 z_&6BTrfAx?*?Fg&pRPJz)*UI=w5mB^8Q{*I{_k%s-)20K8!uzuVe2?^YkTYQdVPiP zR?EF*Am>x$gxR*7QJ}yG zQUe_O60HJbzq5FS??(+pqLp&|3$Kl)nEj6pt|WiGqIq~Q z7ZjFY?q177c*aHsGv&=_hl)7$r6TioZ9hM>N&Lp!qWL1Z`%%9jo@_aB%Tt<$pzjvqQqOJW*vIX< z4}TYSZ#jQt`P|*d%EOO^Tq zP$5UJj$hETSaG3aGaoy+n!>5~ zzJE`i81vJ?drzIY*R!8E&M;KT^Z;U{e1iMYlHIm_vCYr6KntuX=r|$u>5s8}P~_LCKTy#^8l$pd3#gt9GjpmcPf0q3P^Vo z-c;d`^rP}SckZOJ{@}JsfINf0i+fvh#x|(g;@6wnDAkr1H7C>(5KJnkGCTe@Mb+~h z{PB*a`*ZQ16sTeN1Q~JYj5jzljzout;i_9&0vj8uqJT({^;yZf%WlR=~8PnQ%tP)ihMEH+Fq zvrk#d8QBdt1#>!MJ{KOEXT(S3-M|0H*tYza_C>PRI}Kmsm%`RGt08(FomUL|Ar8hy zjU82uFmShOKL5DArHzL~eRn#%;Q`GdA6zT5CCF9Atp5YZ+O8~AJhNqJbAM4&@i#Gq zn%Jxjt(63+-pt;Asu-N~)pvacChqxt*5iENqn{ML?zZldU0&`u-;t|3gGsO)1)`*| zBL-y}Oluenrn{mR!5`73lG7suKPpREU1BGoiE^~3yaD(ZFF9(!B-&UK51;zi2WFf@ z)R&-NdRM=%Cu|EwIm`^X3H|G7r{Ci8j(e|4T$lm<87nc!|F%7)`Fz{Qh2g^dg4{mz z7o|8Bi6amJ^;=`vujH1NaAma>cUI})8cmm^n)>*pGykyp{k3^v!s??+m=xpH4UA8H#ZRMoW`w^Kz9=gEfmzb&Q5hUUqsFJvlx8UQt(PU6?Mt7CBq zA#0;Wwn2IYje2OB0z;#OZDf^Tere2b+l3A#;Hpxx;zdoiyWK5XGrjMf zPf!oNcRR}VbTHpN&1P48)V5n#cjdqL%IUJ^%B9UTE*-nhmCxC$A4<_hTSoKSn#OLS zej?IA4_YR(3tU>{R`A5pzuFC}`c~ac6 zpjQmyo5iySZWURd^Q*OfSU2CjxW#+Wc0e#nfPLemdlrcZdXGJa2v*l9=6G3RUlA9- zNyZ_+rTXL<)IL;08eIU^l?lYy!}cQ8wu|!i_4t!={(A0*Cc}&Ov|ZS@h-SQgL~zuc zpazFZ>~G^HOP5l6oNV4|Z`|(letb4Qbr!KXsWX{2Rq#rMk{%{(*hQ{ifk(gOKF*G# z%wcf>A4%nh@vjkwGA!u!r0fWsigeqzq$MbQ`+oT_bXUlY=YZN#rRMm@dSC0^;Wv^KRh(mH(6)>0~3Q(g!v&vf@wRd zukTnm`Vj7)MAvW(x|wN#xAn3FNHW`8+w>J;`4J1xrwwvfXI+P8d}#9kIPkD^oSb?z zemKT$UKP2ohsPUt`>=h&hpnTwj}6$El)ZB5Ixbw@k5p_JQ=(yH%0qgtBkN&I)rolG zl~>(aQVc`bQe1W`r)z|U>9y~EviV2!uBjzhZQlCXPClDEC(5K32`cKR@osyg*~!WD zZ)gwdy$N-nSKYgsufN)DQ>dUZ`n31uy+2QK1yA}%(T^$;%vgC!+fNBR0_urPqTGSv zva-?QJn%qYjzC4zfAjOoaK5_jVppe&{ljE5s26b%*lI*s1<-E(wQqBMp=3hqN$+pI zv`uu9PZwgwOTNx}-IlGSCyuV@jldL{nZkwwVo})p_kQ9&+u)NTrXOB$VqqzF=g49( z-xeT?UKqRJxt>%i+k8twz@T6WdqmNNc`L<6RO7K3pl|F7En=baNvf+YUZVcd`DD6#R_!ByM5_v6 zx0oI@&<##RK^UD9IdeqLs%HG48xw0GXwW7S6SqT5n$lSg#7wKqsgkkwO-R44Fous? ztKOM_T_I@1Y$%-XJ}50*F!+#Q(W1pTF;N_idYO~}dU^AEZ^a00y7(6}&HK}{j*gs% ziHftcrRgJ%L!QxO(K!F2-v!PxPpTQ&zT`GRss=VE;w|vEHi6Rz^C0W4j~p8N1C&(n zbcV=7ij8@R7jH7iedk_rx5)9glKkzv-xNYgnXVrQQEj>uNh+E;w=?L6gjP-L@hakp zTgMta5uADaJ>ISliR7MI2(p9Vbp2d%)!kyFO&|9y3eWvPB(zbRgMdXAcmRJ8V>VW8 z-HyWxR?u20;b<_JCyf9EpdiRhZ_FBSGCq3%rrB3OCP~_q5H#AW9d}dPd%A9Hb@XG`i+_#iv$A*Wg9K zIUPTxHU!6F0~}gG+IV|f1kOwMYq#m}TmM_ij056+$G&<*T71B1g8f5b(~wdGrmyNn zH3Vlt(K6!dk}J;%9o$jf%(Bm$uuFihiNg_iVieudE-#T0zPWAAVdB1DF=fx>l=XA| z$Ypxkba3^inXJ)ms2s{zR2(%M13dtg&38qO++EE)EP(KY0=MY!k>rxVWzj5+rC}zF zf-MF=Q>>G?WAy3fhOj5U>N0loG3qc)Ob}97G=Ot|6%-6FbAz_yZUD_sz57s^*n3E* z7yQ1Ip8ZxU?DLH~k29yv_DZ|jcSMRDEyANXP|?2 zm(aHC@YjA0G`bqhaSeS4HE94q6~_b1q7o}PwDY6sAH3>iHC|$|cP_5}P|7~UT&i@u zDOILK!*JG{y!nFyfy50k=HhEm5_0&1(O9Bw&$M3TyRK5It4N z)vFq-enX7qhZJRS;-fst$cl===Hoou_c$*tl&T^sz%FF%|IR66%dUq{;5&SN(tXZH zo8^8oNBbhs2z_po7DM>@N889sdeFB{_Jj7~4}S>i;&kxBHktQmDRs;CgUvaz=Ki`qdlbRsFDA{XFZOzYfNH#%{?c3t9d z&^sEOH8$)Uw?=_wLckG;#6jvy?!2;v+bbp#4~tAYZx5TX5Q!Wlbq&tG)*pxb1lDj4 zk8>X<+XfWO=3Lzq&qNio>*4wia32VklbwFqtNN>X2xH>7x%G|L>_8Y`9bxlA?v*=z zGM=9^v@7Z&Azt`p@*6>Iz?(@yFz?=TLF)e4n3$Yu+#IYVCCxIMw36H30NP{cWcI{M zwX3PLX;s)pgc*|dP_}Z7I2VrxK7y;{xrYcN?~Q8_ksy90is3U~V%CBpwk7QH@%sau>#pBZxU|WN1aW1qSG4xSV!@cq<%Z>ky zRN>>zWtN6QW|)LZd1I@8Zz>~ySAZX-1Th%=iDK5qMNDh7menuMK1!5RyJnNt{RN7V z&urZJtw^`$y=vP{Qaw=G>j4IR@eRy&T2l&gEwPR_oJ+Y9o^nKi6)t3FVlOaRuE(K zjM{KQ`&2uscr-&!*Jg!+;QD~}Dg2WUk6D8V|dL*srhQ;Bn>h@o`CM&^!>sMpP>)!{nDlqGm=J4^lX`SYzGDL;Cf;s@y3 zrk^d88Pj*}wNwfJi$*Mkfv9!f=ok_j`k&iw*Ydk{Z#tT@#%C(EAc=A$5C&@|JaD{6 z6RK)tKM0L+#=x{-Zy*Lr|7e&~=~LvM!{lY}=U@25Vzr^BZt2`pO4K$DLglW?MKZ!QHm6NuYek(KxN;TUk$*i#G*a}4RK7?B)-uYb;)xMqsm6kc>|#b-EsxlFfaFNaWEkPe^>jb~IQY5GJ3Qh$ev zKGaLBV^>dS@p^keW9W-G>!*7Au?1j(^V(dXM%ju)gWC-9!eg;-WaQV9(?ZzM$0_5}d^19$n!xo*btyzQ}8)l0{?xVi6a z#m|CWCwF;k4nL67-qwaNJu2Mz)%$7NnRc;4@cHkCTm4QSkINJ+N7ZJm{*>PSX|3Lo z>si3}O#zbTJWthDcV*`j<@E!5tM&dhRMi@(GMi?F0U_xuV0Hr%xp)Hf?kJ(DUQddz zpZYnuY|QJ=>;Io+7HucNtB24YjCQ+I;2EBS;(~E`XQy9bQ5A;hw5irlJ}$u{SiWlE zqsd&g17Y4{{c7fH`{W)eoK{Z2 z<>^Ps1LxeAK1glh0z{XW>XgKkRraA6-Iuj9E55=q|LBQLCB$ZymXEh}t1cV`=|sLQ zCVGe{aS+4D*W=r_!O6maHaXPhadj;umv|}!jF+Z_B&;aewB}_h=KJ1O{s+mtWaDeZ z=r}H|WPhIl>sg)GyjagO4yuin$u&U{m8xXnGTTUIc$LZctqTbEZ5(CjEYJS(T2JP? zFV{z6nO#ecE;~M`wgG3iXBi1|x2Z5lAXn*shxjx-u&UhrkHfwIsc706W1{!WDiM^V zfk+0q^QhHIr)?%oK`fr6)RA4cEwdccslxVmih+npv48 z&*kOgR2)N_+=Fm7f$>@jwz2WlqxO42LbL7D&tsDYhKE0k0q5tpY-Uj7tpSQiRAR% z4NLjeU_;h7?o$9cltAV0t_*(`T>I9rTkb>auA!>FUE}RO4PHpB*O;Z~g&XAg*r+VnpAPyqbS@0r$LTJb&vMP%_@lsFrI~`W z&ZZF8GxRL=6Dh@dcBoAj5XgV-tkkuNe4sfSKnK4jUUy7HE21NO^y?FHJ!-WXr1LEH zvUyVdG5Sc6xkq&|mCuqzr4-w6ZG9V6Vd+}lc|Zf9^jItI!BcVfovJ0a!BfEs9&zy~ zLo-xSS>)?t@KcqiALPr`7>~1Bj^VHo3}H1-Np)y8{xdgGBBlmbz>Y@MHWF6`$NSjI2=B6f4@sNA=>gMhDDzKK&-A){2XuR!=X5hC4l^u0I>Q8(r zfh`(V*d-w2gk1jh;QetKxD`IXDI&+0<~w2mSD>)e@Pod1$b@`8S5nV( zr(*)Ma_ttW0_LWRM9!Fu3|aE~xMM#quSy^FL8n5Kkul3+@z1^Z2%Z(MC0_%RkBdqb z7WZNmoeBc~IkT7~G{@bDr_BUtspHO1+gwE3Ec62%_Lt%Mg6`S7n<>)ul)$3v>G2JU zx%k75-_JX~7&h3`4h)PCE(yL_nHqRk;gD4=feK`njCH|R@;pa=o^$P(C%hm#8Z6^j zNC&Oxef?OR`TUZlhOxQE#h}7ZS@SS0EXkNwsO?seYpKD0jW-sP6>9cD*uj^8pqO5= z%4jEg!%qi$qrW4~`%N=*wXKw%QCl0!cT1WbIBoAK0)xG{@{Rg{L12oTbyH}=;E~<0 zp13uIT)~#!&E~|^MEkSDSA)IJIMf^dyU~7>gPMxevWl%y&l6JJuLGN2SnVMHbLdf~ zdkX&&$I2DWJ9Ed)g@ocqhn+6SMgnn5g4jYR+?Fml=ByYSoPdXG4!v0fJKC*=z0 z(0*JhiJk_-i87(Rk>~i+N4s5&@V78J_uYH#Y~AiVF2wipe$QsVY~i$mNOn|aU*pxf z%S*wbmkn!vXxhAt?h z54B?8S8ry%!S4d?bC0;GJY5llb5S*R7=(F#`!<;LqwN zye7ESVsdm4u5KCn>qFTgTi~sS@?@dFo)o!Sv5rO1eTzF)>F(PRk|WJwokD)4|CFzV zABnseZ5k_;Q#`UdDqMC6I*7x&;4N!``5Fm!DCd43Bhyn2Kv40$C{V1u?&f87e)KUa z22U~k?~{|Ej9xKN^nVLj4OUy4{%0n)X4drofMvmW{sU=14P~VT z^}17Hyr8P9`^oZ~VeI4lHfGjoTF+M;W%C5;q}baJ%v71>V&eKouJv>3)YvVRj_Fu+ zv1!_{Sh!tVlG$bI7q{D;)=o0#QyRy z`lPR%c=dq|NIW?muDx!V`*%w6aR;x?-nkgOu>9{*Dal|(YAcAC?@os|QeB62y z^$gD_42Hv)c1}XcG+9^quyoSop9R{_9r`$cTgqFeTDH#XGp#Wp?t8FChJVdg@yDC6Oxkl$gxhDy)HfhSmTFDDm>hA_T|<4HkU{~l;a9s zUsV7Fn=}E-32;5v?u_EPmn*jSX^j!y8@qGl`7lD0s_RcHg>?^pKyksulMCRstup;ak=HV@s zkC1WGFL&SsP}~Wi_LS5g&y8))!LnKNE9Hlevp?qzleEf?&rS@gPbGDKxGN~KRAm8R zYv-)yDpF6G7C^wmBN%c|k@Fml?1$kXm_XJ){z}{^@!Hb$UY6z3*t_QK$fbXr%c~n1 zatcvgSF`MC3&gcCj>d({N9;JR+zt?ym`Gwr!T3hkcxFQn)zOO3B))|Ajzq5-<< z?P^-WcP2AFV6hdR;J98oRUem7{ANb{=m>cB3a#$q317Ep?1-*l%46Ae%d#+azk|jm z4UMUo2ln>;yLzQ-1&FFnH_o62!McHV>V_c-@mp`kR#H#}+0`>_4{r=by0`WB?d0%b+yloj_P|e49p#x$iK-Cn549U)zpw zE6)GekYF*wRc}10G*|RW8j$nWR4d3*6q(uvURFECPNUpbGsZ!}dyc2nhy&?Iy3*=@ ztRvU?r{7)e{}|BXgKj+m#_ZwW(g*g_KG)F0@yg0?Gd3Yp0(5JjRb~=6`SM!Mjspms2h|_LUOB|=&R3Ui)?cP8 zP7g?f50N?taIq!jFPr-)80w`IJQ)OOimk;*=@+p79FF|A6T|`K=C(p%nC$Pr@o)6E zdP@e}91Q!ZQ7*fFvCf{ERLzHHHV;BNHWwUzx)H#t z(rLWl=B4%0vjQr zsl(r>sl}WiThU<1Pr<}&PYE!s|FTDp22^xXY=+~WJa7qn(fwy0$;^_%@AnfKG67>P zOWLX<$Lu%*v}j_kb9_w25vN zpfIW%m>x=V;i8pYUQ$izm^1?GDzmM3$GAjYiPHu;JoDdkJF>GiP61KGsd`|azy0kR z?uR55nl}Zv-{{LKU);88uOHEeSa=hY21j@nc#BGNXp$}H6#%F-EWeFHuy-cTqUm(V z`qF(E^9Ad6J+w?NuATP$;MpU)%y^C6rq@KI7_}Ltj<*Yiqg~#Asn(>5Xlx-s@12QG z1(bsX9=~p!7jxe~3`4Na?v?4RS6&M#dTceB$6jUk9+CgD|uNDPur;e!>F7cBQ5wCH}iq2NK0l?)<*V59G*!&un83J{qrwqzBJv@j=Uw@Tj3Fvwj=!f$;>LnkA|;E}8EVbXP22|iaa2qv!J z-Ys}khlSh7;FJq2bn8VAWreTZ$c|L3inTj6?T8BJv+RmI5>d{xy~Bt`6(1*Sx%AQA z#vC`svC5;a`O^{ru@+FAnJE{|RR)s)X|D}Kb0D+HbVB=BQSo+s*}B`rCBvCXmbQMO zGd8THRokj6jaeaqI)*47(p1rdPo}3i{AL$e`*}l{Y9VP7vbjQw2$F{!W=30wB_FOH z^(2=Gjs{Wnrf$cHtzfOT@-ODR)vejheIB}8U_DnH^O{buvhX8kfXMkt@zKP8H-@FC z&hA0i9wBdp^z=AYjdJX|rRyv{mI7B$uGMD(df3&qNYyY*lwTzM{ho9KZ)PyF<)-(Q zOTbi@R%g7ETokmKI@9=9DrkrVwsX`7`Nds~PhNJ#@h2a(fVzw@`O9?V?sfO6KTbzz zS;bV#!F$bPy(!mUwORkp8o%3AM0 zBDkVQIv*N}l)N7OnxdNZwhFiVB4yhXT^Hwn`zafW`lE}dyWf*y5DN?@ir@#icNdG9 zn*h5sfFUNLi89q(DsK$6&1ll#dN-Syrgl z&i;B7#U-f>4P*kKn=dE6X%ywj-kMX&C%N}D zQ?tg0fnW0<-t1!c~ z;L{(=V25Rc!{P-UvYBKey!NgFfsg~Xmh|s}oQ>V$KL-;6NkdV$oQVDxD7XF(_Xem#D>{KLn}JqLNzv%Mym9U5O>&!L;xBTq%%>EcSE+hC^@xE^jPdQ1TOrP!h) zPq&CABKben4$x;DRD*9c6r@OI#q;S>C-lF#>ht^vnCwY&Th;~sjmQN%QaVOocLUBuJWsynZrV} zYhNwC4bpg?>0+3177J-Ceu$LCUV13<-iN>7@l4kQuYBtqt>tFks zgdYiZSJ~}g#s}X99lzbp5p+%f=v59|cZ${ncg+JN|J)e(FgiUhorlh7ayIE7*Q1tv5e{0aKcr81fi!By+{O&oJ|; zjRpUHE*-u%m~zNffdLV6-WX6)O}4=pq$nPG4c{Y2rs%l&!Z-&ti}a1OQhT-&19VQv ztoR4Wo3_5oKhvr!OTD(T^2B|c7GEZr$se^Z4Z=vv>Ogxs}7%)6C z0BQ4V>J+7Vv+eii#_Hm!(tq6apAAvmCjJt$l8Xgk5?w$KGkZY_J(hsO4@`vXr%L}6 zAjBk4*TTWhQ2QURT4oMTeQ!SHCwAvgI)1Z~_=p7y)YG>%+fi$6 z9wU2U$B`?{KZ`wEk6U*6p#W)O$TIN!9srHt+C1}1@ZlbleWkD@OzZT|_g`+*J7}%h z|M4R6S{=^}{k;|1-$;SXqLCMU|E1lpP1WI*1h=9TeoLzu*S5d8qLH?likKR(Z_*;m zMTSfP^|(YB={7PXHIVmImFJbJ32!9+6WU8<<}7ayxUvR7ti>VZFBre7(e&K@Q}*md z;A;S+7>Tb9$NoWg|7`LIXb(%Xt-n1&V|!q->&jrF^A5Tv>jL?YMSiK!Gtx0>cpKo6 zVDV=KPxYiw*obuHWprza5_B@VAOFQ8V5Be}uASfij++4xVyOo3CW`&I=Q z3l09a|79O|5{KYE2h7QWAT~VURVATzBuYG-+KBXL=IR0R8(@B4bo_7yF6n1&>*7U8!RJ6;B>fCY{ z+o-iE;U81NZsi_#nB{@tsPA`Z-j^kxpGhCY4b>A>9}Y7t5J5C_E%O#&5T;54GFzu-nTo3;^(Y z;o(2`)L{geiTruf+0z-}iJ2A&PYKUiWlyw2c4jrd35WFIfMoWOSAc5gt>NtK;>D~2 zKU)>#BJp?H15aF%P)Si|b@BjCPqAAYge13~rkZ(Thme}hnh3E7yb&2WS&<41U}#UC ztU_c$%>HDA>6}qRdg!o_U{2;oy%yA2sv5lncef~ zZTnlIZzBIK4U((qT=;EYZ_`8I?1C?MaV9A_cyOd+y#2aaMWbDZ9rNck|Loeq$=w?~ zySKxFrOYc`)Ww2No+4TarHcbN%|o(u12DCZbB6Wyi6#9h%ZRN}I}Hx1y;q5uU5dm! z6+K~TVuY9^I7zaR5PZy*!3YQJCP@IpBpDkjRqOfF7>-Pf)_|kH;r6K45J8kJj zjrv^YtEtkqd-Lx+tQpS%eK%8(IR)N%n@bC%?#(*$z#(B3$u-%7e<<*VZewQ4DEo6WS`U zMnB)sn~n-Sl`_m7;~malvM3RRm2hH~8#S)_77*)&>zVa zpqED{gxw^e!?GrgyR+_RzQ(kYE1ul9Gq9MQnZ~-_R-H3%2&L%NR{!c7hH{jnfzo3e zGANh%-fT=X=u3cZ#{7!HBvJL&Z8IS3WC=>dF%WNjBNPGf;1WQ!RJrb=&HJ4A$=TQfLav1VXMgZ~om?W{s1)xIZvxrWorfQHO{qEaA0`0G)4rc}_M zwTd=ARI5AL8L9Xeyi1iEr|}i;r_ROT%e28_hoCAM1h?1Lo@Bb1b%xO>twXtQ2u^r6z`zJPyzGH-9Uuv~n~lGo+->*9m9qc;Oo!lkLMJWJSvCEL zB;Q{-HHzK$2?Nd~840U*;qb$Aovs>{sj188Mn2k}i!TKh@w))L@oB`T0NO+mKpllS zTHY3Rw)=klI*#;Fc#GqNHElod-D?k^a44K6Hz&0Lr|XgU_OABYP(4CKXzk%YFMt42 z5;!qqdRe3&QruPAs>d~{j8X_rKZCyFyqQGjSiR(gpHQ=kqpH}Z)ZE|sNn|lnEptoz zJgIGlpM(p=Y>P)Whx9b)orR#8;HW^w-UR5X+-U#!gbV`2fw$Npx>K}O<41D0ReSG# z$HIEY-)MuzibcHwU%T-jB!lVcm2I`LJ~TCkD{6Q?UdLOUd|r2Py@Y-y&p>0VPV|1t zR=1$=EO=(`&)4g@Rq)yf^@YY4Ir=HqO+46Op3bQ)IW*Sv$@e1Gn-`QDdp+t$%*fXLJK>$mir++;aprS8sAmk zoPPfOcx-On`CqIVW?s;oIbMZ+dGSeXQ%|z@2D6jgg007Ojf*TLq-VYq zSC+2vuG!Ap+|D>2M@U7yu9QZU!5HpeYH4@r7D$}W8cUBqS@xu2a6oQX@ zER%J94gLMZHh;QAY+bM5{MxPl|AgNrpAicSp1J15?qp#x?9Rj}+9)$=jYM)?1fulzphRwymAW2BhCX2q;_2gSi`5IcI z5g#EPyR$KP6zF=GSl+lzUTZwXQ5wW?yHqNCYCr4#c`+VI^nE&1ks>}G8Jm2dSe*_t z&;I?eB&Ys~&j(n~zyj~4FwG-FzF|m7U>AhJw7DJP7qUrZvsgCZ}D3L$Kh$Wd>lWrtIBCetv9Z_`h zMT-cCFF2m|{S)nuD7)!ON2O^)mTLT2uS3JRyKRk+3M{(WCK9ok$dWc_&hjg{h@ySU zy8gVyw3;^KkH0x`vQ_T}xzp7ngjo8({f9#2c*VH*@p&rZJE7>rjDPTK*W+z#3{8@2 zey3Irc<{*~|E(`{GqXtO)H_?p4NFq`x~24xg)%vj-o6#D# zn6Dbs_5VGbt}33$NDdVZ?RBSn&@gNc0xtrDpXHgiJ}3B=cM9jH)7^h zvq`4eRGbt9Go-sz+Li;4yfK|ec$Y)9Y>Qeg_`NH6P<(!y$KLQeIEf)@iI~M~%zL*W z35HY8I4~g-VV|$}{XK&%StND{Hdg-~K3Z`3rz|!`PF!xYbotYm#X<(;wH7gUPJ?8z zWUk0Pjm{T}M-fdYEilc7PXN8kN2~MSBBZkln~Q~* zWg`{R==V{kj=QlCV&l!v;9v*MqTY z9{yS6rnw0-+uO8f8Amc}!L?2#bae}QW|QOuag_OfACX)Pk}h)S1WXbqKF_{HA;)W? z$A_fieO|jslsMRu4#)@F6nNp`3@WL^*+mIIhz?aXGNp%f((lupNT>=i)Jvq>$P%q9 zh0}X7$dw10)#-R4;mndTE=znE1KWJXX`xCMVg{`w2tqmwFWjg~SBQnT^7~}@N$s=1 z7ndm%6US@GHT`ql)H?<&8g}(h8SGkI$^d;SU(NH24gY&utu6W+G7|K-u#>T^q}y$6@-{Pt4!yGl=ANo*49D3@q$Q<14NtVs z$R;I0-GVX`Gm=MrxoJ(R(=0SEOhS+PZiYw&TJ0cIxk=D3#?8PwUL%9w_4Qy-bnm9GsyX>%%}=A7t~mT5KEmk3W2$fP^|K$ za6}3azhD?APDE=!c@8DW@;wX-kne;CtfT$|mI5cCDtWZq2kRAAEzCn~g)AK6JF3=y{t& z)1f558G8molXsh->jU3K0p?4mCh3AIf?GQ(8sAMITU8BV))Qd@Q z@=Rk~a|1UV3K`_DwY&@alV7F52ZrKwQVVnj2GYl9j_F2X)#)5*xLBgD`ES%htW#X} zL;1-1CkST|`$stI1daZX_E(+^^$@wLI4%~mtwCxii@-a*h+?U_$Brt-lLtAdUC}cR zRr@{)RUyZmMir=Y3bx+JNM75=sqB~TM|l(s#X-0TJTIwSys+#fsVsLyC9SwM>Mj&c zAA+qw;3fSgFV?=bzg6S->tpi@^+&i zajP?p;go9;JLCEjrO9GTx%2*1nKe7?t4Q&6J!c(gpjc28l!PI<)=87M$`z*(`NfG6 zLEv>ABtTikA|8pN+As#?&kvrY*1k%Kb#0~AgI-iH=ApaHB=>Wp62d0yXbv?7Cw7YN z>pSJ$U#4EL%<*othL#YirUme3`CHTg-}6(+N4`{^cv=6!og^Z-z=yW>ZkvyJ^YzZWm4;+4n3yzj7 zQmiR>)>GC`64n(NYlUK+bN_oba(G=4{O&{DV&Pru+E{ENKVNdt_W7wxrHc8sK*Z7b zbvxjH_a$vi6-)ZX?kkD;6$SMZ?iJq_Pew;!YMtqr!O_9(e-hU+KowVdf{XCSqatDb z(Cx2LS1v18bEG(UHqD!T)P}1%s41!#PTG!oya-VhdRxNdayyw|Sxd!}r|GsL9SxOi z+RyBybfv~-163iTg8R|sI^pa;a_-BQX{jchKUcMHm&x8!Ps|n|p&R6WB<5?t50@^I za+2`!C1D|E28&78#Y!gKNKNune>AIPd{aC{n1w+~A_3|{nI<8`J{C4h3{Sr^8~hCC z7!idzU%sO%%?=9$LrBO8d}#4v86NBf~BJck4z zs}NA|?7yTSl9uU-#kTIdVoeHV>FkC?+~JDEU+GkkA>)dEuFbaK>=yJg_-A>>ysM^F ztxo!FS%qs6U-Q)>UP*clwCh+=VxeFw#1A&&2HFKGVNZ+1FlJW%PcG1wD9k%4{Kiz3 zYgMeO;#LGPPPY{N*9K*V+P~cBtYQ?K#Dm%iQxinhm}F4s7KOErBWqGMFCX{-=ds9( zwcDf%Y}-SGfA4j;VhRDY!Qrs+FdK00BvT>I+kN-Cd=UP=_`Z~Z?-`WoXqa5DJo5$4 zIU!|xsV$Wbbk9g=#TER4L0<^Am7lB`wrM5YY;6!A2t0?N7gSG`vgKma*vtHW9*jgt z1$4xs8F01*M$;F?4Y7OsHZZ=#z8KhkrWPJq)dOq@HWQ!Wt@>0YW;W?*;Sdu?HHn0< z=BqT}TEMCimbSp9IYfhbw}`-PZx@JhHCOQOU?s`G{>mWi9G$Kg_3A6bBT;~<@G4A&<9Xo zC<#p9nq-8WEr&0r7D{~Ilik-uo0Ay_yom2ed}rmN7m`9uG~T<)89bzVBpCM@-LmdG58B?Te2{88K{&4Fk4%Y^x@aW0W_{fk zqt}aiTi1Pou+QWk=nwIAQ+N-H-L+S-$>Btw-;mJ(&LgM>Hpx#mjN7D zV-i(90RkD17kuBBSo5*xwkNax>?84q9HZ(PRq!f_CZ7Fl?`8yH_&Wgb$QMyE^%SuV zR75%|ww3&jHhZ+)y51}j|3esdGK2ENYt~*;_#ifS z(YR?h(2c&Jl8J);Cfn?NSL?(Eo-`U)h^0WzODe!o#%`M(k5ZG!5rwb=k4uO^-h)W@ zd_u9x)euL5N*BPL2;u;2B5Ae-1~E|WkAAf0Sq%kNjGm6Y37B^V_&aOlztL3V@3BGC zy|d=chw1ThBU)8sYVR^3(mg!TYqcsd3@dcK0iXa`4SuGppc*i)5ql0kOyZ)UsFU)t z>zjOW&6kQ&LL(ApVupBPdhTWjk@N^3C|nty(y4U#Zf_**5QK?Y=@UN~$$O4MAAhCB zi;J9-lO+&hB&Opo_J@IL;mJEl=|!L6vQ$=_1Z znXJZ80{Azc3wl_FAdeC=JBM0qq#AG=NqXT9$@N^k*m^`hE)GE@%tV_Oe@7r9Biwc@ z^f!P9XaB?Yrdza1ZmE1R8Rn^XkwVSgil6K4EJ#+T<2WB@kJ`QW4SlC5?$F5mMlXO2 zkF@_csPKdI>-Z7A%Bf!aYvj=T8yH9xGhW;?1$_ZW8a=XTJ2Fm^d%FeH#4Mf*`sG=a zUIBO^3oo!~!WC5vjoX;`FmgMg%qMg$~8P`W#olyo;pcb~a%yLJEn`M%3@ zbsp|`=bABkjyZyPMHYS=@1v&U4;1T@K$*h3l@j=hlNtp|5XN5w*{xKbQ?N>23D4q? z@491N#ey~kZe<1-h?lk+sKB0l#TB(7(Q(3}+xwFDo|=_Zu0!>9cvvFPG{C|IAQE!} z`}adBm;LT;I6>eb&VqX_MZChD4al-tg_%X7DKI1h(-vg5}fJ|lh*B!7{DTx zw5^q^eO+(?WwD9U07!1As7__^!z3+DgQiM?a)KXm`#(1E9lsMj9ImUxD^n^^Iu?=c zQ;}2m&DzjtOag-VyfN-VqV&%~E3&w_xYnmAks(mYs4r=&M98O`=Ya}^v-whGrtS7aZP^f zb!HkroM^mYQa+~MlLEI)IQ{o!GNhwEh&LWM;s6QFe{!T z$i0~rnwbhXY&di_c#kR9CNZe#R(Kj_!hz9JaI;k|dR{V(iVfvWwsQ(W;A32I#HYr_ zYTv7fgqee6Ip?#)3I>s-FpO2GG*W6H;t4DC2E7erQVE8yWcD|+Z@M&)__aqcrgH&c$8RfJHg%8`sHQy5b8bPICN1o9R=5fEaV(9Rd(x_pkzeF z=8in}9I^E!odm623W0+Z@JBfeumK++kdR6XqS9&Y6G72YKo6iLd5XtAFrH~Wd#JXU zY^(17Nu(gEEjMcK0?W&b6%83Z)&VbCEC8uv-_R`zfh+1h(&~sY>2l8LgfP2TB75FT zjiL3gAKw{V2@nLr?IlUz2x=y!?o!+?3_7-u8E*Pau_PP4VX;Q|V*;a5DhI47OpEm_ zq5Z&=1o$7dzE5BnqpbHdG>|$HHH*V!$&FP^N1L+%7;zXs_c(}S_5nUY2A-xM#g7%s z9_mh=JbAelF&Tatw3evWBMU4Bz7zy^Y)^E)ac(6ZnWn}2unBa?qOv^q_M)Bwj$SGbQVnYT>a1b6)Mk{-QQE+)N zW9g%1={Jl#E*XoAxa>Pch(-`Zm^i#z;UZ$H&3E)xKoT9?gaQB3g4dx1*;`@K2BBK9 zlyx6FH4U=Rt?r(PD#vEEi2Y+DeJ?JJqJdLnaWD}{`#m9Ll{`6V#iu+U3^J28-PKXG z&vh~4=z|~K3VJK1AeUP~3yx8BzK#MV5$y5k@5_7#h& z2qy6kfCJQa2G&i!tT$zU5Ukxf@%mp;|J<%UZ-m1h`#uIY)bU^T?dR?6K^ry~k1-0a znTM$VxVQY-p;!9COC)fVPXb&p&pDkyM+fRG?+&13IKjlj|{a zJ|VZJMXqWf-P}uwHoxa;;U%c1z``P-Lh>~IJyp^2f2P_ok$MB`M&|%y%1Mf^eg0|2 z#>p7^k>vrH$C1BRoDE&BBZ_ow6{bNt{54EDZF^k+{EEVm>K}Qj)mLKBXOzUH*x`Le z{ClcKXPDO(#a@`JS@Q3>O0ZChWF9>NaG?C3raIS(p5ZP8K@|?;Urz+N3EPu<(^YN4 zCeTr{-*Z_%YFO|44rJ4e%>OjiLk-&F?t&XJ2waBAzkIki@6(r8%p6#}Xa`6Bt?g=E zE+IP!=H1*}hQD0B=qTuQQ1Gi6gx`qqCnP^3B+p?Qhy^hIcPjUz^P^2pSb}*u$?v(w zmABmk*-Lr2SH8NuiHsWjCR9s6iXk)54#ATfetXjDL% zkbuB6TwPSgr8aA?R|cB=4_$*-^v+Xl^Y=xC)w^!Em?9UoE1(w1a9iQCvchQ-n$+yv z+{d4^N~CQj-O4IYmd5wLc${Gmo-7+=1YnXR4IcdXBqaOrCKS?>gl`PRm>-&&`k14b z{1HS!q|3?4nNX0PRCg3I#6)!HSJl)!`hf4b6+)Pwc^Tl35%1TJ+$NPA^`m4{@9EQz z(vcLIAYLyAgbW%}5Uso0I>WTMZO)W*Fi_<5F=F4$;SHUHUa9yxZ}h1i^s9b0Mmi>& zE#MYu9LOv#Rt}8;F=R{%ii$njv#~H-D0_KV?lr$1eH9Y`2~MTE#!_z_I_PxDw1~M( zwd!Ep_Df_QKV=y{Z5?SXdr)EW+6v33LV9@kJ|3*|Ss58^dd!DUYnrr0$gd>jN|O5c zo-lo|4sxTY<_0y*#79bo^{ez89WcY(s`p$~wR!94q8${@Pf@H|!$#3pO+5EN5(?SD ziIVV+nn$-H-(;S#A_(MvK4{+-CZHkCk_0h~I5XW_SDUk3C`@0lcX{YxB+m+LT)igd zLt%COb*1UMcnmm9lQ6H`kh@$4C-e4xbuSenxA!e7^FGXdppjwD6Ka29@HoxApp|YUT zmkt+{CcRKq649H+&?i|gwHp=5?Ea!CVpF4>cA}k@cIzoD{|X%+ypyx?iA41srNVG8 zZ*u_DA}v0wrneYFfoN-J4hXZA#g2g8x^wwk)H!##iWB*WCp%E~}U=82ogJF$x6Io4A? zY~m>0WZf*f{xA9YUxN*djH1WNH=g-Vn?mF4ox`~X_zw2{S<95uvK+CBM)uVBt*$yS zJeq?o*VH*}UbkA`NToR^H+1v+Afp5`oK&m&_Km-pHpxZ&OInzM|Akubti9i|{C!K} zEi(2I&@er?PJ|KOPrX$?*M);twd9FHn!@cfy`dqV({{}#dy?H5GLmH zn0r}hdHb{Vq?_WPNAEm!eBZ=vOBD?b3MV#UE{j8y5gO}Qb(%Er*vs1Oy4CWS$Ho9Va7I zN=rjRJTJ6|)iAPL;NcQ2RR(Qv9=*XJRh*p*ma}JB-Xyiod42vM;18}$wV0x?MUcD> z9==9Fmz3)X8F*BFFL9m0jU|;Psn&|``NH9C;El*vW$3X^8W~90C+V<`SuI)WP%D_f6{|J8^%7!lA8MQA(1i818Cc@_?kR=d zMbFB3W6gGu(KjpX7N2?awuYO_op%tRyhIm91oim*nZ+GDvP`@z@oegF@%oZnX7g_& zqZ`ZIB_$;k88w|Fg;bdV~t3mq=y}(J%cH>P$0JH!aG31{w!|FsI?9@%lI^}(Bjq#qHyw*7s!?D{pR_n=~{D=UL_*ic3XYD2$-V30*P{dw^|u6LglI^v=t8BT=% zk>!tqioz^aLCbD?h^yhV{TvkQkML{i3zrrsL%07(n>h9r=RKdfJR*M>uB~|@#q8=m zO456x94#*zpC{?JoZ`f9L+%}gIsz0sjWx@as+B_;pgN37=d8-NMfSfZuuI%eOj;Aj zF}AtC6WqOl7K;eN7(T3o@Fd|0AWge@$@_5i)KMdipW!nu-9<~F6m|)E(bzoEmSw_= zjGh#n=z3&9b`ch@#v~WijW^hdkawg^ zr4cqzQEZd!-TRjW?b`3XHuxsV)!)jQV=UsliMSyxMfI$}a$^kNQEDVWjI*a4M+Yze zh3-CDK*-VRhnJf%8o}rUq?WPmJPLwlkJ6~>X2=CMzY%)$--1PK68G#0m?+|%=n2!? z2P9g)fA*~D-7`kz{>lK&pRO60Jo@_=S;&BCo|?Q+JA1n#l1bjSjAjm9#f4H>i2Lla zp5E*pj0|rzuC@UZ{7Xc<#_Hyuy@K%UAK@PN4HH6MP!qn;;bdGA)YNl)l&UY#{gRQL zeK<4S#68x+KnX;LaTDkGdbdM+6x{5gx^7%G)iL_T*>})mzZA|ALZFQC8_uFJU&KBp znAVLl07)xi3?yw^y^mg{t0uK>W}9+QA~1{A9htA`_$}o}iur%(7$nYrs+0V9!$>|2 zlZ^lI(dWKtezI2PH}%#IH_6r$!vj$`@8i%!<0o-7RJ~a;ilJBR!>^=_a1{Ca71%hv z=CTha=UUSEi-=y31dy)!r=4iEy@D)-aSV*<{|8vQjv@!1{fOC4lcPUuZo)SSR|hvm zQ=Cw5n*EI2=GfYb?X%9mcbbuc?kEspteitb{Mqd=4h6 zWlJCq0u>co&h=1vn=g_nRgiXKW z;ZN7RvNBe)F<1hp&N8l`1jo^`u)TD=<;H(^n&Mcu{S@qUb5Oou{I;mfF{A-*SQ<42 zofY|x=wc$41n^|}?5%}; zxLkK1yuT#g?kA%Zm}Z=0rHP!o)!uIZ4JrW4i6&9r>3gu9g(W0Zm#i z7L3JSSti9H+>G2x+u%Z9k^@W`MSjKV`^ts2r>zN~4LWJbgOXk9sY4eEf)qZ_b4gZ} zsNSG1?OOLZiUCwT3=`6%A*{QDas4&(B1A_mW~r7jiC=vF=t{{%=FJrik-7^Oqv(G3 zAtk*Ma=_TmesG4TqocDr-o_PF7S+cH4hTSHeKI!oGt{D56>hxA4)$i*r4aqB@ZW8o zMnt*w3gnSg8SlsVG}`#QX+U@?{=`(m{*z?tWIkec-L!zsGXrcH>bi=DK5t&s3q3~; zcD)@W0lX*;#P}81ksT@b%$|Kn{ZX8L2OXbzc>9frQozTtd6?|uG(*~x#tn;9q^XWo?m!5?r=-VFAm(Q=qB` zJ2y~B??0_EVB$-1lB$KkK@trI2@g*hUMQDB{0qkM;xL4elCWDo`@vyxcpZA5*RP?% zdvPi>C4VT~W7l$p*zz(H`{*XoQPtTlmq%i)DOCyrd${0FO~~!7$Kd*-)^swiK5i5F zap}d@)0L8ZcU`NgDK(Nsd%x12{h-H@jnw{kNP2o$E2YFBkU$C<)`1%5M){^?G+46Q z^hx_(wJB^*KkfL_*7@UvZ=1dHoes|{-}*EYZ>yx9d|3`&BpEdmj7H$X2}fWb-K!>I z^juHkx~n`P4Zrz|m4jjNv0s+mu|FSBY}cBy6A^E}6@fu~=DtD=>}FABUZ#Qcr-~-; zcsKWS5g2))1x-S{Db*gPO9$L!7e|B}N0@l#X~Mt-VM-;7YA7_oB3Y2C1c9E&VKKi| zu;-fbLanxd4KNy>+Tz#Re$^V}+AgdRk1*2v03AoOH(P{Zc-5|F zuR~dsYeotJl|Fe(is--b&N6Jbk9dbK=FEX)+ZghE%--%j+pusd<{}M}{+DG}cIBuW zGUSc~HH`Ziz;7Uiif4L*mpi<%X~^CZrrprphEDIrU7-|*z&^f5(3Lm=-qhMCfkG`v zpw;4(JwTapeE>D3awhifQ7D;tbs;>qH!&N=>+33uI$>Vlbtv3oNAHY`o{aMC3h{e8 zM|g+WCdbU;(v2_66-^{IsC#cQzrnoYH(aMM2xxCWyk1|?rKKC)jX}8Lo(Y`Nw}4&1 zNNiB{qTr(_f4ZVm(>V(>9`BrO)jdphElDJT!!3;x)sR`0k%P!$0x+d-@PL{L$jl5EYELYp&R<3)1a>#6k$8viHyHvDmAgG5TTwM z!-;_k;Wq!w4G^PF0HDz>hKXuOqsqwg+3xm+0CUobO5eg3?fOTK83qW{v?4}_g5cc6 z-O*(HdF{%_>J{O@i3LHV;&9JxXu_MY!;~%4!n&Aq53mI4xfv{6Pw5pDf{60a9TeJH z93CJxP*U~4b)g7nB-wrles$~4ZKy!R;l$NMpA!dm2XF_STfkx%z3f!MrbZ%bFXDYr z11#lG&%3-+&pJMc9I=Bcf89cG56#2zJ!C1lP4rlhsT+#0x1aGnMjCjKIK0K=Mw*c^ zP+{aU5Zk&pO>#7FZ%qtB9T?x0K0(O%*!!i|**LsNxfzOooEkXt?mL7Lv5`h(3=|lJ z3}h45Nx7!e+d@EL_f)$Vq$mBPi(onD<_(_72LpRW0gjtVDuS|SAM?7X< z8|dkQeKKJ>jEYoY3Oi$pw=)3yuibmUrJS{mAqa5h+IGX*;Gw_w*5LrOG{ZbIqwPf$ zO&w=c=%MhFlUPb13b!Y>Jo&cnGk8Kyj;mQX+Py_k4fp1Vy$rT8MZUJ(nij~RBiR;3 zs<4Kc#HyaqD1T4AOb?`xi*77h0Y0P!VWeV5&%0Z+(g=jAl6_D}okWWq`(6;{h!NHB zhAZY->O-(by+luMmqcch11-|jb1DOL?lJo(kzu*k42w`~Z>bA}p5XZ%^Cr-wPt#<6wB7xxG z@i+`$jV<0h(hdIou>cJ>X1@PQtTDdm6bj7*p`BH705if@_Q#8cbU7=f(F3 zBX_^Z25iU=-2a3TKMI!jIk^#-9*}BaGQAjXhTq zRnremMjl_{45#G{vTS((q$K>E<655eaz@!d42sjxnCQ3x7oWdBczcngiKVYyzZL!( zdi}Yc8`EV;JlF0b-!9P^$QmFL|NWT`CUCX00-x~T|Kyq>K^5MkDkmT+dUOE1*#C|O zqt3f-pr8+N{k<&wGt{V_8(Si7-5{>3+GGmQ=b!mTjJ*;4t>8}(fBiEG0md23^v2nr6I%jf?y`qDcOiaMW9 zac=zwjMkk0Gi*mxbgOR<3H`k#fLZqc8GUH#gxNn~lpbH8 zsF=iQ{G3>SZGJ`+Dd*C^l6cKDH#t2bpz`jTb#7Jq?Z8U1HHX~s^oYPpD5>LPebpDh zEA1e5BjpC+cSK_`tnZe}6MT#cCOMq{9xO@`ReAZ*B(|g2Aj(sQtXCx&tOdHXMFg}n zLdP?{WtF{4@(qvy#=pq29)ekqkW9o?>Y;es+oXr@A%U~uW1xzWXf5*X}R&GEE2U$lk3k)v$wh&zD z^Z-8p&?+GkDy8DbXm= zROgTS2OWvVHz)`PMeuL&wU?SODo4#iHp=2{DHE8eTq*B9{A=DQ1P*Uv!a=D$1;6IA z8UjXmA>q-KPv6o_p#>o7Gzq> zAz3TzaWE4Cf9}=YqbI^QXQ;5jU)J;tJ}Nrw?#=$X#YKrH9##p zN=P7Ne+)Qob|sj`{gQ?=qW=tN_pSp6=5Ldf+jLRp-wduZV7n``^arqFsBg`wZnjBv zko(vTK9%Y7Z9L5R?-p8atkv&>^t42xWM{gf@)Q3zmcVHLtyFv_45`|ZTo(gwE`nl0 zz0BDPCT~ew{Z885|LnnsZEv>@eh2bI z4u&ZpftUN=3xF5kP#?0pU;3^P{dXALOKfUJs&8vk{|kZhPO;l9>Bk0XWG*v4`)S2r zZ~u!UU^Mtto854nBMW}CQzs^{|NYV4UFo_!cTv7N9zm;;W{)WL3=q3Hk=n z-$3E7<-uJe31KjpNm-a$IYRVwr8*$mfa0eIa9(q<{!!p{T;775C?PkI(IMLzxI6i{ zv-UB1|L&xo7fGN$Cce%@`EUyI(oJcl_d43|w^pP=YP_-5Dlj7jyz@pxP3OCYXI)4| zGC6;CQO6ZUqla~-lLoiR)Vj`%|3kE5sD228;j7{uc4X*nVzWfI>pW*?{?GNf?u3A+ zDWEuThPdhy{w#fsZKE5GCT_}~e~JqLrGiF-h4(M#U0XRgsLFgfVjZ8wW*K)K!pEG# zcil`w{uLbC%t!)UN!4}9E}(Xt(f_==&o>tVKQV+oc>Esq1-%qKTmT zUp}|fMAH5*pFtZ*>i^|4xINPU51&CI8#@vf_1}l~W&qYu$7g;Y!TlGML+-YRMgK7q zXHb#$(@eFqzD0kmrVmH?&o2$&HmX=UQ6SqN3V<81Fb-ff_{@hVyg>j;H@Rdc>0Goj^lud0RQvk{7b@Jo00JROsG#^ z(fw3d54+l5;4W?h1{nPB{~!TiNWy1=)wi=_AJz(UuqM9y_gCKk90*Vr{=X#s{~zcV z3;lnOp@bfGY!u^IISh0)J)`I~ka^i8CqGp3e9_8E%0Ed@`{RXL^vbE`Pp`ofr z0r^ETVH#EooNlMmO9t(yGELHXM*+7r>()u~^nb2Bty?3@+O)9I90%2(0xAW0sxst; zhzOY`n3L<`j?7N~m~Z-Kz)VJ@+q&O{L#;?nKqWyRubJygE&5JPJwKZ>$B|mW$pQP; zEj$_pmG_UnS2IxGL|iI3MdkVYMIn@iofKV^6oczu@}W|_Cin$VjNofVWwEkjd~B{P zwe%e|^%er^KIG2BC5;!2Sr5hc(p?Op&2uTo@Wz)JJ_o5j z&*7m=|L_ql49*jBPt@F*o8`H_WvTV#<|=eP!ef%_cB21X+Qu-UNoWz0>T zsV=US&DEssfl1PD9kP8BSoZ{swV!@cze861Y8{M6^90BKZIDI#5~mlju-bdp9cFa` z=Gw)0|E!1TvP<}T!EoUjpjLPon4UZIVAxP;&J$)J7b?wXDFc~Mh-Vb%C73tX!0obMBRuLkg)g>bZHh_6_cTID$&^nowS=r=0>jxQXpf*`{;z%U^gAW4Wf6{zB{9%&Z52!&#NjSgmlyxiVyXQpO5% zVC&4%Il*FyRO2!*8%^KkFCP!JKjp2;wB@iW7~djsFRy^qu$02@gga!*AL3=0T+*~(j+DU)O7 z)p#^4);$;f&CKt#DmFWrw8>=_fm~oL#)lohqQqeML%nyeMF}T!v=mouOKeC;L2yh< zPnp$^tSL6%z#dD@xQj47WBD`}k;kI%I9}VPUd<>Rbol)YPv4zLvqcHWAfd1|sr*A$ zCD(E@Slfr}eTxtA^3j7l5_?3NI+Ir|cZibhrSN(5H_S$u#PGB!_IJq+=8|@_U+g*} zwLK0#@oeGhoajYN{ssEo_t%7zw2*MHTOlq5PXdsCmu-n3}2B zl-ad+IiGpZ?_H5|DNGJeeSGriQ|S+6OrPyi`kP4(iqZO&f~LYTkNcuJ4s>@ih|@o> z5OEJJ&vQ*LtiLeI$H3w@9pC5^(fyRviWia|`y`xXV{Wc=S1GVeGgEvu4a1LoZuHxQLG|z&P`A|Y>X>r0 zVrM}{9Pa18lpQECmFzsf7j9LF++Z@(a?gHDcT1H2=~ETne8o6+vb_86A~n_5%|2G< z59j{SWLMh|da0sUb*YLqb>Oc6c{4DaXFhDs<&jqDeDURQqsvnnOY2qO!MN-t` zL_XbCzFQG!4!r>si-`W2Vntiy``$6#hbeV+sFM;XU?w>pi(Jep&0= z2m@v7>O&>=RaT=Va>!l6Ns^CD7d-k2&%PR5QQ=_G`hU5+txNa~h6Gwsw zg@3H@W>YwGOJT7ymEM067NxDOhp(Q+-y#bMfUMn5ozBer(fgA|&g#iSef1)I&d;X) zc_|(#%?&JAiW|-%=ISZ?ge(TKF~u^?rJn^~NYKSVV@2?)n)PdTkMfD@g1EN+0`IPF z8bOfT^eA^Tq0jDmueb+tNQ3}x(Jf&H5_|@(Q}>p)PxDS=>5QOnlMd$YX_u&|_hJ#z zK6$ND>h9Y$mo=P^INM*gj-6-IhC;2zHI2*?!^s45K42^!PW9<*y_8rrtCFi6Ei5Qd zSUCxybHZqg=dPts-|v)$?n0;BhZEQ@yNO1smzI{k6Tl7$AID6kKMKr)x0GhpyT6lQ zr|)&555lyTV5vpCHe*@vKJ4EGy5iY2L4j12NYPV^k%RXj;-DfUgZih-a_MXZyiG@$ zQ4=bUcP^xOocjX<1vq!YOnAa?3!f=fv3ep^Re*s40 z!Z*&mr7w#$ji&Rr^4xl?)CFE@w?CRAppEX@geWzlgzOaV|2%y>r*7NzO}WE#OW&{w zizck_GiSEj#%+?LWl#!Em2c7^Kkp8^q1Ui>joW+COHj&n>uBV%u;t?8GU-5sr3@-p zQ$6d~iHsDl5AU6xsg3j5FDV=h*If>|=IVQbMzqc4(qFImmqFVdY@y{)_F837BLDRu zBtf_ibSgg(0DTX|R=%~iwid5T>VOLF_7ZK$FJ@_mV4jEE@DgCfs!p2|A5m|UjV!y^ z>*v92dn5T?T3D!o%m~eaoF3)(?~$Zf2wUAF5Rjr~Ae1O_ z6c_}_K07(unhWR6NzYhSIC?wwD}1IZxo~j|N=&oz^y^q?6|A1xUGUqT7w9XeJ883IyZD z>?Ds~fQZXZ5Xe0b)d3;$g>{z@kVSyjXu2EFW|o84>0l<KE>D(|D{EblbNogZ+bF$sw|p5v)bjVUZQOS2K$qYj zk_F}EytMA1{WfLg66lYye{i|~;h}%|LHwpg=X1|tP#=xb>IceJ&`z$d>tdWop*fUu z)I9P?7z+yvJ!1i$t+9=oXj0-a-^yL46-@sQo^zAxt8v4^)4JEbmr423fIcbe`2v06 z1r7s8qApu}Q70ufqY)E7C3q(4qC@1;SBFg7kqr({n-D*`bf!NKKI@L+*JW?lj<5sF}Y&{l!ZM^AY zf_IQb?DIwfq0wZaz3(ZOFcjCIoUPU+v~o5;2NZp9zeh%kl+gRWO6>isMkcG%eeJwY zpG1DTy=r0yA@6rZ-AzZFVo>MXlvF^aF?A3rXmpmJmp53_7NPlK7@B*BO*g~M4YiA? zqt@WC+mYWq8@x~XI1qpP`MJx%{|ejd*tc9JhnkW}8K>@ONYh}|1f;8wn0+nArXW7I z8U0N61-82J4RoV>G`>>}0%9@W+c%&P`E4{^g34yA>lg5Xc{op(_&|n}9B5a4F#Kq| zyH;z@;Cznglq@K{onNwa3zWXcs?ryPQLGCTlRI5>G8bv~&6yfr!T9H`d9(r?U+HIN zWvS^oPxe?PgjoiDwghQEMrS=G1dJqow@E5wdvC=uHGlsqzYD5!M<&fqc6-atT z*U28CxNs_+)}6$oi5|GVAD?Hj?C5-Cfxzv!<}lgK-0%CwyAz0dMM__G?RdR%)&-l_ zyyQ(p#h~T$)AwGNbRJObGterd>hkWAJF0M|5Nu~cwwY*W{FdB14EV?5^YCB|2X?)f%@!0Gh?>SooxM8fIg@#&L?$U2&L+w`RK zG`V2gITVzM*Vyv?+G^e8zPHrlR&^^yCMNgwd~1vKU4MH8)e^6>uU;5ccMi#&Ctjbr z9S@I=l8mMHmI|fv*^wNc?+@?=ZsJ!w96JT69)s$dlW7Cwi`^(hx_1xU(PP6b0>dU~ zKOoCx6Gs+hg?nZYR;fjv?S+-B?Pot4Bv>uU_cSYIR;+TsIVbV)A#_aX$XL9EQpa## z?K2@QrDB4l?TcyG(sk;^7M^*%Wn}lEoE0hY6xj^-xe*Ig5xZnuM$nR{-=J7|C-RPh z%Utk>HFovves@gcdn#!xF+29J*p@7EywB3=rK=3BQ#p)OR$MhecGTAf$1QgX7xh;x zh%`NiV`GA4HsfV$Aq@-H<*L$cve)eZ~J!(s`=Qm%By{2I?I!Z4zX};lL zI_2(5i#^c!>yg1t{h)X6-hD4qn7Ap--sjfk2HxEdT#KrA)=dTf3N&=Z`LojczUKe8fR5j?NuFx16W$N3q zL>5w?t2pIe7Bff4N{y6kwb_~|Iq>enZo*+J$ zsNAtyZDbpoZPjA1=r1#KD;hQ>ucq%nsb`>8;hOAum4&wY(qpFgrk>3%&&r~}TFfmI z-Q~sJATy<%W1h3`szh&QX+}fKB24gCC%c%GsZh&bUim!xY^t5$69~CzC9NycD8wW{ zjz&aL{8W7Pvy^>BbtFUniHli^K{?R5Sr~6eOPBp8Z`uUUdVI`X+1MrSX`y;nnU!xA zbH}XKazNnO11aX29?q1~4B&GU|iA`TQRcsD;JGK9vXRYRVfsQ#mCe1UYAGrC)Wxj3 z;#d$hGSr?6nm}*aXbFwmtNqELm_Tnz#fds|;cro&S}J>|pPj_#v(v_yGFbLhvrAkA4RKNp;~+JRJso-pYQFvmqc)nD^`F`ythD}(ENcP ze}~O@VGQ>3S&^KlX{M4wrVI1}G|CiFG>0tf`C^$GpgyCB@~QzRe% zQ=d0tuJ$bV_xgmZ>2FZBNwt2WqEx`=cGz3*CPG6wzW@!EokyI=eED&ai-jQdT+0&P zGuTJm4cp(?TXh?z%WcL_bzwpGex`DpKe6xv_9v9?2U(9VZdK50#SHR+Nf9kc?JX?^ zw2(ma%dD6mf%Nz{gVS7>c^H+sGRV2?Vp4DzS6bS0aDts!9hA;~?#!2okywsG-mv-g zq=oW)C=3-sbZ1GA?sY1HsBoCctw%T(q7k5_V&}-7=>!wKesQYc7RQV${a*S@-E%(Q z1zcaeZ(crZwBL0x)}{IRTV%4tINSJ5WK!x-48BrvSed3!l}!LHZF2494PH`WWX8O^8a8 zre7PrQ6>c%Dd3-9oG(Ar+{ToF+#U$SY!g8G-cwR{Hn~;I%z}z~78bt%5>Z3k^z1zs zCYX3z!$|N2`SdwN{ob9}x5Bbce@4RyDn#9n(e$2lr;IWObaR?fCfzhiwoIBJ$sQmH zOMH_cYtOdyTUkFri?v_qD$t3Ox1ZFLZ?k-Dx0lzak?e%h+=0F!@KMzD6Y~yza1arw zU6#9Foid3)W&%;Gbx_P=sna}-b$*&QGqz5*)MP1nrsGtaJge`+C6?~(ohC-qu%eB^ zN&93L2be0EZ|3hwW?Te-WW>i+lQc&*V-DqasJ`(gIfLx#O4v!h28wiHg@bRs za`0HBRSp~*S{GH>$dt?~gXNlH+c&*^XBrP+K?0uJGY#ooMqz}dIW(6_rW@dmX_r^-z z&DzKa&x89zwsR@(24~^}%e>7=+(mx@?B^|u@dhN%_{KTgT=Z5}`y76}u>oXt>KWuf zF`wd=CGWXhF{;}uhEw&z@oc74HH{Wg+2_W|CJu@EPzySJZE>b0Q4W1*jZ?aI{7#Bm zD(#%i(i@7Z0Q)6{e&v*C91*HkU-d6J;a?HDdbSY40(Pd|({^|ZESw<&Ax5`l0 z6ICp8gAan27hm(1%h*f%SumjlUy~p;<+@{MmF?dR0wR{!_746$6CGs zP0ah-ghBX+o43?Y=O@vK^+8Dc+m79wJ~^K(p#Elh$B!n^+$0_xd4Fg*$0F(fYHU+U z8IZ=IM>DVc;Eapr$D`(ibt6%$!QAimnQdIEA7wDZ0r93L*vEJ{+3S1RKL~upz=}i3 zh|iH;4Ki}n+e!VgpKa%vR2NtbH{Ik$hAAaIp!-6J*rE#)Xd}MgR~Q{`L%`_D_4XWx zWTB@gWRZ_6cUnFlNwJDeTXZ7nNb+X$O%qU0w)y#M6;8{-`{crMgg+jWc~|eR*2emm zh3An789Ke zqz6fmyP6f?`G+z|w;`(Od#1oU0g62f#O#U+DZai&F`cgXqIX5!s{1^zSjNz0@Nvze zSlv0BIC~-d@rwAIVf0}Qlso>IbGKkbir>KF8W;04Ttal+K#kz>ibmqeAc+rNV+C+A{9*#6}Lg70@;lMQpem&Nqb?j&`;D z@Jc&0uHC+`DdD84efw(f;gyNTkV&M-odf+2QOJS_zP3ytBco)blWD2*; zn&7$%x{l=?oMBh6sUgm1jckm*Xb-g~*qva7zM8zhs@U@B3#`@g9wT*C{g(r>SJOht zA|sV-MFU^!P2puSz0WXbXfP0m`@%34OBL94qAjtpF)rC^kLNtQFv1@CzvxL3P&E?R zeb)Emp6z#?VLZ#Z9|Hr<6CW*>X-|Ll^)8mbDhG}!u{4&vJf||QlRA5(N%V(KjV>?d zkZGESZAHb0I{6vOtMCHava)oH$wRo=6{xY4ja=ibsxFu`{XjvhzOAZ5&M}?cjN}jh?pLrQvyG6Zz+N8tL?Jh@hGjh?wXBhfKzz1}GUv(<)ye z8~n04n6pvJQ890Qv`+gDQ`K|#*#C#UuZ)UvZ5vfeO1eXkkfEhfYCu7np}VEK8wBZ& zp;Jm4DQS?Fkd_pXZjf#`&*0wM{eI`X=kNJ*)|!R0oaedYs{6XHTvZW8O*yTnh3M=* z*=h9Txae`a#m^sbSi28*0Alli*_a?Cd93!e3{iD^#f;s>e5`3D8ls+@`@z(*WLGH} zaZflCK8xZrA!=c4MOa5c>iO&87-%qkFijKHEc4^(byLJhIp#N{iYJEcYPRd#I%@ZCc zBPR*i7HRi)5C?1)wsaCZhA_4Ui9c*25jN$g`$cbl1(JdQ8SDy#ibKipL5K>%cjI|& z#Ao7q$LDk=AL$?~7M^o*aLf5gY;xukQhL_|d1fehooVL{>Z(lowBMR7yK^0PnwWR5 zIkFu)9jfq?Y;#^L?qTQPs~NYo{gaRpEd=<^R|>#|4s5q8qMP;oO0r|=iOiMim)2zuO1G(W+TatO)aEGOjW&XJMv@ty&(3aX7Pt}rVoVE0rDR_Rc z&2jnKyBXr5sgl>-Z#~(JbU4ZZ!Y4OFGib5ajqN zpq`+!6GDNc&&jIY;Gp^bh4b#^`x?*AuZMaa5ym>@=TKBK9mZSB{ zlnCya3^R%BV)mMhdf30;&JZX!9{5)SB%SkYZI1ACk5I63Nz`n*Vn)OJ)_bI)?D^T^ zryrX!5s101sN!pcM`5r{N5dTD_K+-S-xT&j-7(h{-MVb0$_J~t3i+DW7E$GL^>q6k ziLPqVqEFc`_Yov7&ec^A`o5$`sr3(~mL)BPf_+tyq^qL8%06v#G->$D$Da>4uX=@j z*{iF^YeJX$GG{}DlFMhDb@FF)>a~;yH4`eq^qWs&YVjvO&t_IqDzxDEvM+>+C!a~3 zAz6{B_kk~@2{D=|7-@cECK{GH0W302ap7ku_`>*@t>}uD?|{x!W4y(o*xYL=8`>q& z1w|aSP&J!Jg|;6upurhMt#o`>L6BT$bigOFqsCZe(toKb7JFv&Q0DItn<^MIv%qfl zdWnqjovpGpgg&DUhE)Wh$2>OD>RC{9l9aRfiHZG>LVcc-gOjDmc%3vinVq2erx5S& zFF^Z8DFKGYXm9d)#_s*2(XfymV3nRU!d=!%I6ijWuch#(-A0?X6__DKlcf?8S(K-_ zSu{S)bls)|wJslbMPV+eUR6Ho{sq?Xy0&?bSAmLIpNjSWb_Bb%KR}d>M8OMh*cC&> zp5(tM+XQyhAGuu3D~wy-M_d>|ru)*8DMD#j{IUB=lvx-wfxNh6W%rVAju;a$UC{RR z#g11myI+#S+QZ*&%C~+n3daRKW{IQ}+CcZ3BJHt@S=Ib{f31qc;GPezFMtT?w+RVDi zQr@myr7GZn@Z;7hvq=`0cHK_Gs$WY^mZ_Gm?(^jQK!Ijz%XfK3eRUzonq~$k-%9(y zHvEnchK?k#Iz)Xn6zpa(STVuyX+wwTTcf$GMrtgWo}51v|9fz#`BhY=oKzOz{u$7? zZF#Yf{*aVs{5#tM^CAbD-@sAwg3E_Zw!k${RazQ?y&%6Ox4gTB)h$q z=A-^vyQ4-NEaY}O3T>jpA0sS&7!WQRj0XZ#U^6u6X?mh2HpoSbcN!mQ-_*+F1N2dV z0?n+}(4MMvzR(yD$KVSKVu&?_TP*{0MTVeBn^c3m!drx^k}In|_am_%x{dN;oWnvW~n z+1a(P`dC?&np;~>C>k0X{x|{}bW7A&OtRECZjK7}e3uwoaywcp>b%?FKkLOL8yw@J>@jh-f1xw)_BfE+=Xn@WDL z73g861YEmHbZ2KrMn2ePieh5oizts zTpyOHj?C8vlDA-PKk}#mpx>^Dw&y_?+fYbTphL4n3O6SCOa3z6SMG~R`T6<70)74c zC*12joLa>nO>8%a*1bN3W2#}jjhU^g0o+Xhj^oSP%8;Sog;Mna9DhF2Vum#6xUw$x z9Rt=T)DY;>gl~qMTjj}UcY{sG^dxOszO24MLZfrD0c})NqfI^|`fVBA>ExYXtcbGR zDERxY?ZTo9{7no^;QB;*+9g?KIvW9gakYcC(m&l^5VydOZ3Kke4kQ^W7akuH?(#U0 zGIqfZW8a6a5-^XGcH713X1@(%ob zD8$5PDTHnu$b2mrnyxa5()gq&oat~W<#1$Yw$w?_Hk8iqmU8<5AgaS*w)aR*+g7Ee zRv)a^Gyuypnw7)+UBkAu%GgJbkMTgrWF!id=Mknuo;~mO2Xz&Oaw|So6(E>_V>7h+ zAQw-?TI0%vTl?umN7EIU zJ+JwAX1J*#k$l!h?dz`_F={K??^D~vBYlZEjQtsoO;wCWF~3#MyKgf$WBRpOMy0)K z`neC>h8i1ACNxj%`F-vCnQJ!;flkjA!(8Rfry9+>Fz+Ye@j$f-Qtd*Hp0#NesGj48 ze~o@iDQU~}IWZpSDD{4qm+;%*08sWPq5zCLr`Dru+mT2+-6oX zg_h_JpTc)H>(Eecmfb1Oq%^W&^d*@X_mn(HIgr6guHA2j)w9-? z$jFIED#(9b5BSS+1x{QqP+hY%>TE-t>j}j+&-H=3q?cezpOd5G)>VmatNdcdvmrE8 zRQ$+PJ{RAY2c3#1O~u80)eB@nM+Zzm$Sd|0YPAVsz)uOf(#d(MbwVBo!L{>-V2^K^ zt>UGcF0m=k_3g(+2Hh|cY6mqmT>@EQg!;uu1%PW*e*fyQpB3K# zkl(2B*3^`;yo(4mM*Rd{p8URtGZy8WhPEa3^Fuzj4ky5K#otnSf<7mE0d3VX1)qFt z9pD}a!lWpxU0w(v)FJw8v+_0O!gX+5b;w?@li|v-5`IliX+TLCy7Qj4f%Bqw8pO|bTUnu^$%Yq}5bvV^tQOA`opNv(s@8UXs`dAPq>yx!&Xi*UK`h)U9(P9@bX5t7 z!kJI{ar53lc|xF{ycP)m-Tro=jf3Aj(9;$0Oi!%q*E5bxQA#i#&GMRP0IDBQmcx|{ zi){ZI%P_JL=qFx{?7E)rl0Ab{byxT?WAqBBL{;}F#b6P$f_hD`Vhu|aOZNQzdY4<8 z8ABl8zKfyoL)xJ9+-U@wk#-C^=upqFBlN9(TW5e`pBLb}4FI(ZnbMIhl>iTlw+P&7 zb{I<{rj*mxj74o2Q8Kx+04mEnI&N1 zcT0JRIv>g!wvd$7iA^PuIxvOb)nPiw;i1lc)-DEO3R0r!ataF6JZNl7jr7WV3^HG>M0B50Z{-&GJytMZ72G7c4cFRvc3k|a%!zv z<%prA_^-4$bOJb&y}C(bw|-nK*}urff*Oky<`=(UO>PQzDbXYgT&+JI&x2pW#RKtj z$ILf}gM|agbizy(9UP?8o^n9khdS)eQ+K`FpO`!hut=2TkmQU9F`$O?_-}WN zRhf-V>$g7YadtU7#(pulFt8fzqBI(A)(%|ldfDD(!OjtcGwAre3sfI?gXp2FO?rn zr5|xQd&+>IBDSp4;4>XNbUs8$&!_3RVBw#OhpWXovZj7m>d{PUpVU+*7JciMLM`&J z(C(;$+8@YNkdUS-y?XU(>TM@ngD>YK7hKmS3fmQyB3#9Lj})tewGjkVcl$Q3;b(5w z=OOHSQp)2NHN8`kycp?7uLXHB2;0#Q7!US_yzJ&RLkTKhAsMhgXguC3F5l`CfLln{ zet73|I>;{`xiolY5&T8#VK+is;fUA5qZ~uW>?=X61|}4LFb>fGZKv0AkG?NB4f6uK zF?L#r+w#J$Tz~Q*Kh`H5ERq)lmhggK5c&KKstjb3 z6=fBbwT~P=3j^0iGww!n4NkWQE(=~fCM&hfz6PpSfYPV9bF&XL#XSOA9Hs)@H+x!x z#593JkYq zdyw1ASBT`hPJ4iCxZte$+OR-_C;mss#Tr?rB+_bHb?1B5p_7xeCx;d!aW{4c85YOS zyNXZDf+b`QD#^^1UM;=uZc6)URp(XfP5Sc58LBKw6`W`)(+uxUW4&v&48^^u>J(PDtRoB2Mvk>5v-f0qn@h9zBQQNPo(8gltYbO<_U2{UiY6gQ;zh4! zSs;4h*&A`C>2ggMWi(kXaw!A7sZWa)<<@g4Z`d?&K{(6}axE4Unladpv9+5CpA_EL zI&N;tM4sCJsug5i?v6Oy&r|-gG+Jg-dvKtW#wOMhWAl7?;NA7sjyGt==PjWA#yEwO z3&cEkT?`Mz~PpXqmPFqS2C?Cg5w9nlz)I^Yk9CamDCI??Hp+CJ=v4Gs7qsqc~3CbGh5n ztTGw;rtkE<$ieHC(A2hZd6c`2OjZG%4BV=gP5nZF#VYk!txapbVJQmq=gF2b|9%tN zP2KL{*ObA0k5aArBKnq}T%Ok4MPJbl;gBRvz>IDZmK$=4+4<6DI+iqPts1Dj3qs8F zmgTK0u~OIl?TVGvRZaP^W{#rS9T_$YKEK{maL!rJRIkMx#!xrQRJL9;0#0m)va!LV z(|tf)`S!wiIl==I4UhSx%&ev;ZhJdbHJ?c|OB`J?3nYsWdXM)aLeR{>U;rgb&xdbk zsMu6?>*gF^6c9qWsl%_HPP+4wa*3fq?JdU(UNjZ`;A+<+Cs;jZ$$Z?OcXAO8sn9>w z&LPzBVg%^`7;lgrjMzFDeRh_g#-l82KsO2vG> zO63%l6@!uj>a<;U{91ih%5kRpmrldOH&0@N)Q!N!kIWxA-*{cDCeF{xhJ~x3qqkNs zHywgW=`*}9H)G0y(6k>>Z~R`ZBe?iXnugw1mrL-n0?L)q-4vkvrY|2+VP`+Tfi*^d z5tagqEY+;N?BOK0TI#$hmNkDb!6zd#){%Wp*l~vG4M2x{4B5qXxf|H?64~x_Br@K* zd%K&z5k9>zmbRyNmA)etoKe@%Iv+Nu!=*(f7QE?k(oGN_66V=2Le`h6(3#oyOjeBm zhef3wZnDdzf?QR0#$R5s?%F6wz{It5DsKq2-{rj+X{~mblC2DwUNczoON&5QR=-)NO8ITe0`k{_$z!GvESX1&9asb`Rw#H}o+<44%_@U0Eh2aj z4Lafy*`w`s;9B&Acd!r3EusdVm7Q1H5n zksL0MlW$`?P{VLoPG8Qm9r8F8NA&3H$^@?)QDUgO8v4~HmF>dc8D=RM^lW{+YlGv+G|9G0 zoW1?NU&I`~2XG9JWuh7{Cwv_)5QSiETz*Cf9hBFod6~pLr{Jj(AWF?9;K6N!QFxS% z5ZZ1sn6_*~WHy%V%GvYcn(3t~-?+tmRdZrlOdTq#Vu^!W%%kTI;Jcpdxy06rdH?XB zhZCiqRk4)L;D1rsVs2{seD<@I1rCv9kjk&v4dlp9|4jL@k!m@~UvkvEgxb)?9nbTZ zEw$IpN?=IclPqu+LYsGyw;EOV;_lgiok7zCk05Xhn)`H9{I!oJ=~2z)U)u+|EQ+*u zg`x@LM82{#DYUHVH2lzUV>iUQIQCI)fq;r`@7lI5-_7hU!Pa%$Wlrjr**R2n{Dpsw zrDZSfAp+89+dC)INu`q0g`G1zRqmp7AU<=yoRZ*26Zf8HKyj7XXs*P(=}~e^63YJU z=u8cng)xHfQfaPoue9ysB7g*b!$2~L zf^ebh^Y#MTLCrikT4)6m7=421lkE25_s^B5Y2R=8^JXUL>z?@H_}g0T&xa5N?K34W zb-KoX(A?4iy}Gp{4Kx?e0?A2YgBapOUW;3-hvF0Z(U1jAVN?#9pwkh(dr0)oSA@83 zn}xaXN-u2gCt5_%QJfJgB3#Zmu?jNuXVhEa$vt3Pj~+Qk<1ZKs7w%@=$J@Gk1%U{X z?-S|rkn!RrEV6T=4Gn0SSYY%jJaLg`xeUIIZ3Sot_{1AL@Qr1ofC^@GB%&EUXNWvt zZI~`OSm}Nw$)0Z%?4sG3Z})i=q64Lpbu8`6#lpsMDI*Fo=n+kt`aCqi7x4`J%LCZw zZQ?@&$!-KmzH$?9L{JV{qJd`jG6BLIHXBQ)Epq6TwGsi<*Hq{K6lK3EoFy1zK(>fx%r{e8)Z>=!#NOBS)u-!y5es4?- z2@6Dw2U2w-{T%R^pK#$!Hpz5LS97H^iF~x~!`lvxKsW8g8K<;~sze4^$L79l$Wt7r zmMJW17B#ewA%IK!}s^2 z0Ie}N-hq3wz#~zoacm!H447JK2Ic}I;9W|b4c^kBt2()%B=;{+6-Q(nH?mH0VNOb383;DfUq36)d}w#Ts8tD{<66p zV<9}^_V+G7es)eJw9UJp4`eb{e%`}E()c7>plV0~X z(@?@AL&H&_66tXcP#=qFC6mD2TLJkW(yHdJhP7r3e2v{D88Sl~B3Ya}KUU*Eq&CaZ zsCD`?0z&N#(<4S^!Li`B`@MYufBwBY)5%?dDNUsx;!dt2gq3PR5>-LZ{mUD0l*KjCP}+jub!U&>6G^5QT&SiZn^7ZBCa< z(!E^Rf#L2G_(k!Xp-%K3ITLAn5M(9HK8^!kEAToisIscRDWfZ%SDwFb+dxcmsSF!U zeyLqFi75YRpZBZR#ERX&C4exDzzIO<&CS%Y8tM|=>!Ur|Rx@=z%AXebYWcW2Z(^Y4 z8GEaFbE{uNIztY5WliyLS&+1lV+wdX%ZM;k)I9V5A*a8VG=g~^flZCeL76G!)1YYR z@-s3%13bN~l>5~thlG4R-Z)W^?&^)+DbcDO8z84i*sy3>wERP~Nj~iGh(Otl&|#A4 zIhHXXn(Tb-CCUXuh@(9OdNwX630`u!%2{7~e1sDXtXF9%%BKY?%c&MS?Giq_y5?7& z??#C*XjeWp_yU91tDQhbrj|dLJcYqrE6edHA4Qbdr>xz&(IiqaM-}6U+PP(4EB7m; zJn$QtwpBY3-m}XRey949PY4lx^Z0M%4}$;COUs74`}TDtgg91L#=fzt34KABk45}- z)_LG%f1`Aw*~h%WU&<+Bu4ZU37LNbe6&~BEos+(-d@A1#AR_6ps*2|yR-Dm$;Hv-+ zx0)j8h08%>6fWo()=wLB#y5az=J}k>P+RN{kPd_6PMbP*)M{oB4K0+I0#|6Fok-4< zZG1i2Qyjvc?@y{YMQTQnnd0NN ziEKiqV?wDIuf(8pL=yr|`yk;(c4;@Y66BCZS))?+C|p98_Abl1%A(FQ+4Psm*wiEg z)n*(rUN?}j7Oxv?HS6v3rt2L(x}8p&Xr|4!czJN>eR**A9dMiw^z7a^Wd@)TrU02k zQ_{)oT_EyPqWu7eP;lWx-)xF*A5I#cZ&FB`xZB3OJpQX3Y)uC>oe{1me)>aA77ffA z@vcVM9veC=qT*UweSDMNbU4fm7KBtZEEp9U3P$sTxIFG|KM1d4v4GU0qi^t-*Az!U zr6ZnZz^LD2+~(J%(^TJ2;j%f}#mO1Y?sb{lU*QAXmu{-hcBYFrnn2u`^<}QNU-=F( zkU|ZAwp{ITWcu9Jqug#@H8^c+l)~3qPA};m0w(_TG#-Oa2bL0Ut@Bvf9-qOxXHRI{a8hRm4#=r z0<8+KBQ!#Q&*OlsM>O26&G>uh~KFHlG?2{#JiJJ3sKCDUiS)Fmm^y5W1l(QY8= zZQ|xEkHA= zn!o>@R3?gpl}$T8)A;3h6uZ-0^OMbn(_5)b{CvZgZ`QX+v`NE@wKC&X`>N{bHS+w6 z!?lB>f!-qpYL8XBOTG4a4blqlS|cjlN)2Crtl{E^iZhr*{o%dw;++GlzS#G<9 zVS|K)#|BeHv&?#dUM!{4pB4pe+$&AWJ;y$?XuCJ4x8ClNEsg??3LEGqL#r3s5Z0&3 z_$zW>1!_1^J?$UGF4^6A;e+v-ax$IIpHwev4Lv9gZ_{PbUtAN^bt}$FPZcLFuw3>J zD7IzO5T`8|iJ9mO`KFj2w=dpzeSzlXn;jEZPz`ig_>pST)OozWuprc8I&v1?8SY*V z7^&UMV>I;F2GbXoW`IjVqn%egH(8n*cY{8X+z#Am{A%Bs@#+9Pz~>2mjl6-;?dhD+9gBGHtOO`nGkY zn{%Lm{zVU09}a*1P=5mRQIr`&Quf!S8Y%(Ld%PmE=C-Dfh@3%ee`)v(#w>fNOC%37v?&YlY( zpHZNZ8Er??QwJd=$rRtx(}L|vbbN^C<4Ghc0k+$4Y25ez^22#H{Y88$!&Mkd!?;jcOroZ> z`o51!PKEud`TmpoAADR8zhe zx9;msPH3%oX2dx>w+8OvqkZqvW>3xl1HkKvSs+KT!N>FJbeB02D7rXz%NBxj<;gSe znll0lZt1Tbz70T>TpvvLqVv3XI%BbW1qAlq^mEP)1g>7~NoGlB3ijK;yCc55ZDuUm0sN)93UAE-w%vG)rT@a2d2bY&6x)XH54@fzzolO_2VAq%~ep&MkJl znk57`zEDamdkH5sK*jTrqnBsj$y)HBxUGUpMC`URBu-|1keM`7PpeesAcfs1WdWS# zb()f|fBHng-Tl@6Lz!%bDuWj){v}%VeSED| zJ|!;uf-PTTC}TefXX&XSxvc`^67i6ceTBo|jqnbKIhWWr?_Dol=E`{H%bv?<5Qc}$ zuVyY^9DdewxAR|%Ye2#4R8|OZ$lQK)eZL+gB$Z|P<2O#ii{y zqhwaW9ONTZhveyqOO2s9T6WST&gIt6N^*Og*BU5HTR$ZIpF+%~vydy9-v`l{TlSgW zWNyX{eDZN=sIA8@G8CwuT#`UOJ;F=f zu`bJ`gb*dQA6xn=Z`u4}1>O&^1AP(D;OQ;|U7RK3{e7gqP=dDh6rVYKXmnj&h zJOQ(yw}9o5<(87oBH_NA>|UFZ$F5y7Ar+*-W#4+ov9k4gmDTu1-8G>p%6wH4y^g)- zWN@=i7$NJ)EYig@q~(Y!e&Yc(hof(`7rSXH7omLRX^Y5E5wz6JED{(*I^L zUP}w9*3TAuu>)n)w$YV`6I~A+`+$w34S2RRF4gqyFm9YL3E>c_xd(6;^rcoB&egpaZDzzOd&1=;)a)Ak1S` zb-lhv$tixL@%B5wJFF-2#o3#R9m@8kakwFyC6;kRi>hO^a#j+W`iviIJh@HEG!jj zmR$+{ah{rHF_j0tU7p6N#(9 zk#piwiPMHif6S|H;TScFW^8WI+SaIh89B8Qe2{B ze6O4vd>Tg`JQ0gWOJsfN9q);;ukp+?1C| zKqswlKqlRKP``dNP+UOfBsX%pRX!)>3N8Km8K(9K2c1&armAA zqKfBF7Wc~#Lh~&O9}mxKoIXiGt$o|e?i2rfR_tq`&TgW%)|U*;Uf9Wv2E}-idnd34 ztt85hV2qtM5*!3mG%e>#?<8k8%N#S=4JE&>>tm3G066t97#2k=h{2ONi%^SKqh-D{iSRXEcNB@`=Mjd4-? z$^h@&<60PiDReyyfGEc-$n`~kke3LMy`-h9=6QY4BXi=UG++G;V%@mQEfo3rmRW16 zvV(C;XB9mTvN>7aI$7^zAD6|rely!Z2%OA_p+9*$NM{_(fJ~~%xNT$T_u&bpf;iG< zawV6&q6TdIJaseH9>=n*ukyq-DuFCfAqt8!e^xp0F5k6RSm68&2+eXOe=~!}Q@caqeXf634NEU)jzk=? zlF6D%UwMBPc9|yTi{shN=`EHuUI#W?ztO9GYy(P_zuZ_|C7@5at(hVXsB25TJV_l# z!_Sj9W3GK3BQ8H(heui3MZ4CiL&Dglmr^|%l7KJ~sA`_;*Rv#O@wL^<|1LB?=wx?x zD3v1p2Tr8@`1rGEJ2Ptjn*n(-C^kB@``wm?%mpm+Pskjn*3#P0FsGc>8qgZI2Jk-j z00Ny5RD}GH?pmpFbAoSYCa1H}f!@PRaSBc*)M`7^iK|R_JP2 zU#AgqEkU9ht^;R#9#{Jc#l<6--K2{xtFXzV=|=vT$3%9oMP~&rg9IBG{4R*zWEE2^in;iMMsEx> zYat+C@$rv}zM~7*FR4hU4RmUP4lHVTAD1Qs9Lpu87yG={ZgM17$zH^mVjGr`EnAdU zuDP*Y!z_Vc9>W9q>_?stC^&iMr|cEA4yR$vO01nYNmKt)rJb@~u8Ri>=Y5 z{k6kn;B+vajT3WXwzykS)8jR#dcm-~Tt@b#j}`uZ8Gd(wPA$S&og`6b?xf@K{lSkH z*5kRx05wW?V_e+D?3b#PR^O{tBVM2W6K(Vny|*9RvY6buy7#LBI zWOpSrMwd)0_k6WjjV-rH@kxSIxc@%2b0_2l1N)w^V8>s!)Zbvz;G%4t_?~=6!ck@* zw%2li`>mTV=}@^qTt0(-zdied)nnSw zp$vR3_forNuT>?EBVD;y1^)h_hp*iXj|yY=4FUqGG#?|bN&r#hF#8!a?*26!!^2Z= zcm@3j`zIGjbtbJYM~7f)Pa9$(18S>H}R+3AC4#9+8#;WnkIoD_%TL+O7T?dkzL zB_#xvn}O%J{KsL3yx6&F+_RL~y=22J-TvF)&*cq-V&d4#LsZ^|lvi&PA2XfSkDjt1b&Ak3uJO?+#DZsKm4PkywV{i+O#<&-|mdc-(D4Qco;i{;`&l0)d=g>gg@vcUbGkS zG3muSVkMO2NwrBcvOZH87=;$&3uW4@2|A=C|GFbc!oK_b+@v8X--!N?hyk&5C5t^e ztMd#-Rh_Oxp?<7NLc%ioAn1O%RBo~&)#;j**ELS*$Jn;wM& zO3tfA!F8AsPyX^+eR*dD<2{KV$mCNk?=O}MDuRJJj)!@b3dz%wel8^F_fi4oskCQ@ z2^DA;Rw-a&w<1@F5|^kK2Xp`vNmX@k+|`nedcr`!0}JpLn2YWH(eFg5BY9yLdYn1* z@fq#-Ev1QXh>Hbh6K3RUn?+R!si9Lj9+-3;D&t39L&-FI_s>p?((eT*E|N9R5TrF%mEx#7 zI41eW2OYka?djui_X+cIS8<^Ty1yw6Om;v1TKL>x24=UqfO`sIP(;C&@|SAV#0=q~ zY?II{Y+PAH%=^O+#`v`Nn2{!JIqXjwudk}F(e+uFx})jhn_#rUp^+>e4VOcSkVzmR z%|Oif4bsDVV(zU5hg?2&JQSmr-5j6RTD`F4U$xRZzb&XRwi)PC%VRntbt&D;gDFGr zN3%Ia^WvW~IQ&EB8ip{2XnTGtrp~D`(la}33wYs;0T2JJAXVz4#|NUFj2)x{sq>QR z`%zCUoZWKBbX^t8{rtrKF$kkzb$t+!xL1aVh(CNNtO*QU4{-Z^yo%iG5?G)NUiy_c z9rY`Oe?#X1I!zCZa{TNf!M?xG@%z8xkME&ou9r(kRiA-;n9%eW?>ra_x*HjaiUu`|e1Sw%82yi^fqnS`n-?yx9c~q95P>Ad zK=WUw1)3`TU#8_-v9S$F@VyD|9x5;uWijQ;sFSM zxCmg`C-9XK*Z`D44DWE|_^~Mqq5p=4dCJ4-PGT%N_rTWT%SSL!o0D|M6@-I){y?R` z4e2eeN!#Zkf3QB!MV$HVqY&}GLPKC&uVF$--T2kS5Fwx$QV?z4EV6WQ@%SGqdHWU< zYKe@OK(h8oBJa1${{G0U83cGovOo2#29$O_@~aHG@_k@gLfiJ+#uDRnPoz z*4jTFJq&d7>f>ZxOaJGb1>iQ00AK0FK_k6W^;Rm&^nD7DK~nbYUGC(&4C;U3L>uYt zk+G?R7^CcQ2D5MWI;_b%7U($TAHV}&-U7&NQmXo`NCA~4~He~)>f?L+oFD&8DQN`eF14Hu4~_llV5FH<@Bpw$*#d_X-5(@8fLH$Z&T@tX zAl&X9G(3T27od@65mKd6*SC}7nLjW9pRiMEepcBzO#y=EhW7!RbTFQ~>_0OBUy4E> zPJiN}ku^6#5Dh4*$p&hSZ4|-9W<+l7Zf5O(`2OCguL5!OZpz|~?2^B#mGnVn5pIl# zsJyvo+sX5P_zi-AWUiDpK7lL=$$dT-7Me$fk9PKs#zs~zC1}PCA z;fp*{BI;5i(t_vtXKxbh5JQXu6)3`jg=&^~u4z5@slE z5Z9j;{(EIGHm+o+BvtxcV5y+v{UjP&7z;!2T+acpY$xDLPNJrrZ+!oA4*MQ+P^Kz= zHW?;Z<$$BbK`u?Qozf3t+YOt?^=J6QPP``IF`&}f#4iTz7;aZ^8k--^*YLCf9L}I8 z?jULgCFw!NXYMtl3&oAO5pf+qQR>f){aLk)X_= z)aXr3+3J8eaV*9UMvDmRzK@=4ym$ZlH0XTziCri5EmkI_;98R5H8zp37ho?XeSC*v z-}x9nKMXaF_b2jSNI5k8GNW7B^){}Tdki_)l55&BC4-h!7Vb57`s-jzet+?75~`7} z*ep*M6*e5%2ZQAPjUy7E9(s8N+#60Rl7CcxU~i)XFoo~m0`RwPr4Sd*SuV!e|iAJ zay_th=Y;4Zqa(iq&b#LrO_wGdZF7NyeF&2F112!=FpMSv8 z&%cBQONd5;VwrIN$B&$Y62rj{4c-w^BK`S?QjAE?sI!3mYtqGc|NrVNIw{vBT;{de)$IenCdYpS^au}NRa zo(CwXD5(GYvXI~_=*9CmL|cSf6aI5uSnzK{X-xm;=Gd^P(Lb>pC=tIMxHkV!z(6*M zD1}kpXI%sJ0BbLSn!u%kN6AA4Ltc9hTVcA_*_We9=v?m@Dx40gR^71zhG#2j-ZfHU1-(d-6cQ&jk?$ZUcU%!KKz>=#142des!kywxBf_?gTSPtOK%Bl?1j?uh0nUEv;bAO)M5u{>SAwsOz{9)5g1Z}V*q zD` z-lXRr`t%tkUFOY>$qR$_^3@n`X4c$s*GzJXmM_Ap#12kwdlfYFCog7EPiWb&TmW!f z9bzqz;(uTC+EUCMJ!)RYc$*N_mjEyd0{ky|-#se%-n|}R9PO&vGiHTio2?hRS zuRbQ+o~8)xq6*Hnj5G}j{ILG8QM{}IFurx%xV5^<(swjq0t{9=S%xx)E1$dlJKDpr zLc%P!h|ox2Nm*kI@wt#p+`Dn#PH@U|Nblvf(2d$EHeSirVe8#hD-B)R=qrGo3p*Y- zYSNY-ih9ZBaLS0$F9f)9vq8)*f1LQ1RwARmtb&nfA%~h%9N%6?$k4&l##*$hMIl$ z*-e&Ti3DjhcjzcO?id%(a;D2mw%`kcjPHS0h#M~au&>$FK+I)Y9k<;#houiVY|#W> zI`7hQQ?on))GlEhXo)4{m;hr&w>@wM1nJ+80>5&M2amNf9yl6&iG1pOP%vax;6`lTB&MarfD~JD|1=Es(z&Iyv*f(;Q$fy%NcYs`w?dUX0T7iGDFOPx) zAMg8cLu}(K)yoj^Wra*tB$h%SwV_V%0g-r$AZz7<1O*TQPS^sr7qP#WN$)U`vZ%v$ zKvZFJUOG)9BCn#@sgW2S!Ap#;i=OgrQi0BAkk+4a5B8a*l~_!0u|ddy=i{D-ig@X0 zHCK{DbUMMS`jqsSP2%=`Q3k;CFvgUS^=wH|uv5-hGDxo)ym|gWo7m+>&P(?C@_(`R zmQhuGZ`7!Ccb9YtNJ+OSEiEO|Au1ps-3O46?gphp%AgM2EhQ)&hwg6gIx4^Sf8R0g zx2xYA!d`3bwV#^LGv`9mL@18gyfA8P_K@%>Wiz#Hmg_ltdbnl@1u)GbrG!3$`+4+{+=TRN@@ z-m*4%JTtGl2zkiTmpdDWy)CrPIGd^D6~X?(8;68F7?O~@fNZbxKI4@&40*LbUW<(o z>3fF5CR*XpHU;N{8#ZOLY)2k^d2b`R-?6xm)^VeI1qhlytI9$VJi%|>;2v8&*+{Yt zs$zn_5{I!>k|*jwJeA4>_CWfF0Q_0+{e^9RDR|pe+x^sGQ!~r1;=yP2HbNCq>>}nK z)GaIRl(+1=082NcQ$0*98ruHBHH8UN#MUN7UCbTg$jtWIe{Z~ut`u`PSX6OO*|f4z zf9{;;4vm48yxF$-+xEU`$U(?K@Cd`7koUjk9KH!Br3{GY;8mv}4~diH{%ou$D6Jxdtm3``mWFG-f9453vTiuFP5Fv@$qrYT>-mkVZfH6`PF_-(iu$K7D;+v zxHVc&@}Y}e^K3ANTH?y$svNo*lc9OreUO50AvV4Xp{G+(AA}XsMPLsj@eUKKfSz0( z@Kxt}0LF}QJfFP)P9zV|>$m|Yckux1_2aS3%2hi8uYRb&xo$Q9X{(OU0-z4kXu23L z9~&#u`ka|?0X-T~9tTCRQqbBgAz(i%J|s*iq}T;dljND)hVnskF4X+r%9#JIjK6;O zeSl=S8|0vN$osy0C_&**={#{ZJP;8aoI%Kk*t*OOIKw1S3o&0+`l~Um6R|(GV*SW*^Dr@>12n)K=JAMUJi031e zgRZeOK*;cT4y`3w(%U0)A;JyJxNE`Zq@BgD;62PbP*fbsLNFLQQ0oi2=B#J9zm+YK z{|3FLZ)fpVclh3M1yqc>Hbq6uDRO>}IJKe-x~vvnI}BrGGX?>hBQZtGGjeD=f#oCM zHCa%zmAT%sS>_SwdN%@26kBmAPy`XV9)OFN?3xmH#Ip{T1F?**wb^=C`$2DhNrhgp z6nh%l9F7%<^fvh=Kr8ZHtAkSTb+XeOjg(E(E+GDq*Wu7DJMwC(u*7${Fdb7Q?4y21 zoowaeOZxXJouC6>b3mf@SgyhXYE|fo zrk72>LDy=S*JWrKa+ocR17aK-Yu=Y9&Lh0L1;>C z#z6<-D>ye(VM$q?lBpC?j6lAfY0-4`tcP;T_r%SR0A_z?=*32fM-x%4@mk2LnCng{ z4?}?E`-L{g?XMG{(@3iMm%*3s=cdI(2W@W)kkD|~9#j@!B{q74 zYUWrBqdZ)JB^UJEC>gzh43(8eC>0l7f!qta&Ur^b_>}g{Ge6ZmdP({2t zH;q?XjE|L^)kjQB=v2_Jwwoc&hv85QtE;I2IMfA6qfGPrWp`b*23w{vap0<8REu-nFHhBm7`$K*!*{a<&Yh3;eSS+CF3 zyZ#{TvJ&5#g1l9?BOftB2t-A#Ny+lGP{@?^G4Bi?e)OcQhrqM#?e44u2MzxK7}JpZ zxbcJ&W~5G`;3e{rdL@&e?zzyM*Jr`O7=l?chk$%Mky${Zv9UIsqbl-V#9@Nspi}8J zAHg3J3wP6 z4u>Nrq)?g-32sEuZuh2;Cb>JDBcGTf9{!Mgb5bi${kik@ucux7G)Mc;sc75jno7XL z1o>6#Wc|a4UGL_sgUnbPtf*S>LQQ=Ckp#r=0lo?G(x!-<)!tK@K$Jdq?q}aP`^r%n znXL4grO10%W;wwO_9zyriNI%|3$?*#s&9ocIX}|ii#^h@hS$r`;?dU34aX>$) z@LLb|hc5Y`HU%V7ICQvTYT-R0M5HYMstnZz#D#5OdI+U=sze-0I^@Ip-+_Fx_kRE9 zK$!mT1Idx>8e@OGw}zW+rUm|}yAGua^tlt~Af9iKFUun)GIgRyQqUr|80v3@^AD;6 z${uJdu^@k@$iX4-LiaZ_Dbx}l%Vj2y1XZP;nyRENBWY#zUkanu#`@uK&7AIw6=DOG z88$`sXrgF#MZD4S{tDozSB=1SA@2y#Fjk6fW22}o&u!XgY8-OnlBT=5$(FGpcPLO= zAmZ=MKk%Jyj4Ou~K~EA|)_MVX!YBEs{cqhfBYl|u+Z+g9#0nNZ`{(8OSKif+YM0&y zLG9SXU`-uLoAK2+R<)D#yMY9_+&MGt5<1v%cXB46)|qXmvWVfcQaAfinT(`Be4_Nx z=x7tqFVp;hLvXRh9C!qJy=j|fO5)GC5b2EfW3rQW`+x@1>N4!qN5;ZW<4!m-y~$DQ zwiMENGNa^*=Ag_-msnep9O6OqLAOy=i-1!ga?UfO{R&L#5&xm6iEFP3%mnxAi(^gg z?YQLx(hbx`IipjzPbY8f$*Up*B&)w?MP~;h;O3`nbq-vZUq7WpUHtwbFCqJmGSAw; z%wmT}`x>U4{?(q9PbgUvL3vFwg9R#g*XykTNcjrkl>$n*4u+BOTujqB%5Ol%GDwfC z+1VD1r>yzwMbf4t_JNPgq93wC^B9NXoVeK+)OLixVdz~Z@Aj_3_sHbHv+g)?YSOj5 zB2sG)Zj*#epuZQBINg0{*@=iiXymyq5JW)YN?DNZ7}h!_b?SVh>hl}KQbzm8K{)K^ zIHAtnqD-iZ{s{0ug^|ss3jw{g_-RR1zJ5zc`xpAAgfbOtd-6#iK0X{X;SA4?09ptI zQ~^7a-_?1p!Zg6zz{=-55w)|}n$YLua~3Q&$mTiW)ogh*_Ow=c*vwj``UQisllF2| z-~B#!vd~~gFGr?%k;A)#mBI-59Shf%HZNV%y_&ERD70!SHqryZJc{fT~)0|&4BKXIatyDk_}x6GR#20w(v@=9Wv5*dFQDAM6!*nd2I zO-)_P+^mLx3)>Msn3|44>8vyqiUQUvpQ+#mxFnBGhuw`GA(VlB?m4@S>KT``IM)2g z6c`5lkdxc7cq$q@4dFCt5agl+Tmj-oVk+(3u7x0OC!1%bFJL5Sk#mG5Tb`ewu)(3}nvo2a zD-Nb+_Bm*j_cNxm24j4RWx)Pau+ox{~GM(&cSEJhiv44l$bc)4*bhe_6j&&FSosJiZSLf(Bl!I4@e?;}PSd7gYE zHHO8RY3(zNNuJ?$BM`#C5?Lh9Po}$9SLtmsy-$9sdibPq*SUg;g9cBX6eccysiyM1 z$>2xmr04`LpORJ`{#Y@(Phc2h#$71=ECM&>9E~eL{4iMRs-*{sqq4`jiE-8#F<*=R zE;>TuRR>Fi&k#wSkpj!)@m++#F%GU#-@NadJw`)^296iqg~zo)SBmPB^Rc7zdb*_? zW_03z?m=pHKghFQYYKQ@yj4RQZr@e&IUr(f^yF^>Y)vAkcirX@=Dy6ou~nTp__d4Z zp2~O!4VQXPWoUZ*8bo);;YijF{8CPBvWGMM8j07Jr>nUn%h!;0nM`sn9x{wODSL-) zGUt{@Y%ealm-f48T{n;ziC5}9GR&1SkmGS4;pSLs6{vEZmFjvpeo{|C2}l#NKk2sy z^mm$^u+>vW_kHrrvQIca%xfNunDf}=rriYNTr+(ozP2p(Hvpzeut4tzu*F61s4wWNB`Shkr!yxLSF`wwi z!|>S7s{*99tlXThHU%!s8)?1neQ(_J*_(;}X4ToliqsBL$rusASFgdqWgO|E3BPg= zR?~aM-+*+{aTdcS84C&fuDGx@iePRO3Kl-VJdU>@Sogc{;Vlwh>K68Uo@@=fx5m(N zbkvT@dn=HOzI`$1?dk<22qG65KL2|yi-;_VjJ08Y&RuoNNT{4|*+3yiTA@GVmnnK{ za*$dtP*lq=U|5*GqQsNmPfbmwmK~l(Cs@A%!GPAA2d-+|n2XSeqk(zBX_R_}DQs@& z9z@jRsIU%XG#Lje=kz-q<E&YvhFX zy?Bh00LA7TDxYoi%Zm9%Wb#Rn$h+h}S`guv+KRpb*^1^zF?oCp@zo@}vZd?21Y^X^ zmLsOa6cC4I`O`-xbo6(ispCzek2ZXlu3R^VaXE1lr!zi3^G>#mb~TOzD%DGP_`l+i{7ctncjz=C7&qjf+lyeo8xeZk z>_Lb6vE%R0b|5paX;&)Qb?FsT_{gL0G#KC0zwdSWYhqht(+TT5F1uo3%%|-`Kf99| zozGvVo|~MVT;kMn8fYtAk7KR`mrcp<=~-wCMpARmq5ce0aGwNeB*<%SnAl!pyHFy* z6hsJ-df;Xi_}LC*Fdt?=(J6G(Vw%2v?dI8a!@rmnkkHm|8Nyz$Er5fq&vs=cD z0zQ(*wQ{Yfsk5lPb(S-PNG6g4Q2`jg(Vf!cRF#@}s_le0Lq0z{5R9{W+i?NTS*KH7-Ki@{W} z+p+%qH)29!9|=~lbaE5Af8Lo=ai1?%kG2%gjE?f}&Z$MeOB8yDvTV>cKP<4_+Q?dy$uJ|8~N zKf>1g`=&b_GqfGEnq3Q8)Ue|t|6Ai7CHp&q783=|U+Z2QXdSN%{!rSsCtBNuIXx@# zrl=aJ)Tf~=dJqzVp#1lZ>lI^B)g{?JawGiP94*zzW=gJ5<@`= z+T9oIIaQc7XjhzSAqTE;qL-^npdinmqobr4iEVxw$>($u4QnHNpApAaJw z-3W+IzZMLYX?XK9@|FfKZulR3gsS{^NXgE>xi|A#efwB8UcmA>#_bOB2>mFNT#-dE zlpaaWPRMgbOPdD8a>r}J#;O_nKli2{Pg}f( zUwwKRha;Dlp}O$b zO-Q3yv)uo_tcT5LXkJBCtuLR;$V)RBi6Eoc7>a(yNU-IiR4&MBRLF4XG<#ZoU#wPh zN^xvRAHLz2l>?6>l7v_B%&&5yBu9Yt0VVh6I+vhUizoXHo)0CWEjx}VvKE0rZgcLS z9I&SASQ2-N@eItL82tMTk-gqDCoynrIcfH4w!#SGt6jh93{V9Mg;iM%paNzXgmJ(& zaql=>!(|~TsaQ6+v4HZy{6mzyFLA=+=6_EiG)OY=t_LXNwV%MnjcdmpgEAUzy?GoK zAIX!#^#K{*&t0tud8AIj#imzNx1lLUHG+v*UO9hrto;&0qw3MZ3XGiaFk&apq-?r! z97d|3{I{SuQ-*VpDd8EMC?CtC05L0Qc%;7yH+jJ5O|G>85{e=!yBZld3Q!Evg&%z+ zE*FB%Em1y4jKSF|8KMHpq^1{ZlU``~PYr2_{#_~=W9qf{F}kDus9_yj`1XY4RLHQO z5dxU1Cmr!3FANFeU)RQFTl6uqMJ^H#!Sm9G5W=v=2rV)8e82)bqzkWAi~}O|5^u>c z_a8cjrIT=y?^h7StaPh^)XKA&(2f9Av3sL@xsmtLHTWE?XK48>Y8oRi-xvbE*Kjz+I$-E7 zkv!Rz;^Am~CJ3luG~oKhARyC76U@lT5;f!+PFWbaic-k z=0v&g59b!x+A!JF1OF@0qv`&;1EB|Gt`c8C_#9rVee- zc&hs!DHw_P6BjhTTNfk5>vBANNZGM$ zy%OP6Y8T`R$P9iL-rSrvmB5qpjacRT8g1`zMq%xZ^wCta@|mB?%5-XcK!92TBD!J# zUa$+>ra3U(??u(mbm*jb3DP(NJMhorHa#l=fC}j;5JMk6UBLJGp>HkXt|;pGXckDE zf46LZQ0fbwwE;TtzOUQOLiPK!7D>}2d$|$TT>!fExll%9B= z^~2jh>Q0`J-@__Tizj{E7`+RlZ7KH8FlAZ_QL%4k>W7BjYO-2U3sDuZJV~_>oi+u8 z_&}OfPDQ0QQCU5DqBCMjJL;|kKO?)dGg?3lK0W)+({A;Au4jXzr%9apJK_^xUkM2~ ztr&XKSmZt}`JoRZhCr_J)v`Xxs}vU(KVS!f5g9(XeEf3}s|#NnG%a3zkX6Ky@o(88 z!M!V?e1uw)#9
z7sGV{8qOBq5Awrh(}|j_IJIJUb@+nvgu%ZBmoK;i0ILPl zbeu*A>nZNd3NC1zDIB?WfCXn~>Y|!#Sf-8+5nf_H><8ApIEm2*prIMB3avT2-k-^g z$WH?%d$nEuMW#h3vtH@h&(!D2l)^icm9lVTQc{GrN!>!?Y!E^V&zripSm2p&JlP(1BDH zA3sXproGkNMI<4oOO9L*l6*8%(?Nn$12Kf}^(4CUM$st0@z=WLctwRaed|yCb-MRu z0L1D04jJac1{BZ7a}i*?FghlS>x%)A9FdUV232W0tN!GS<8*xDaQu|@1!|_Pt3&P| zLnIf7t3HWMZLhNIej2Q)Ys#vF;;!^Njh8zGm4N;S<>MVQ|CwrnRY2SB$SFzw=6I8K z;f4>^pYC{G$Ad;Gl%fbVM>GT{#w8ac6|q7+xN zd>cXiD9uiDWy7esc1zM1=y<$vXFbw7EdEs_7ueN$~-W2!`!Z;^6pTGN`HNEg@=9^j;i_D(#c8|X!(rbxPf5ML$DnbzUcR zTk8A!3DEXOBT4Z&EKv-MA4wjMkIkO{_TkC-;;qdzx4GJ79hSt-dggw#9#GpIFdjmX zGd@T-b;OC#I6P936}(XbwL`0F2Vh87# z*g#9d#Kd%tu#5!*^5cB$nk@agw?V3D$js1_l|=W-CeTw*#K^WX;TTSM_No2t*`8_T z%ovHTVa(`C|63rcOtG!9qbs#C@mht~sjKZR)A_@Qb!|Eft8}x2RwvkUFuOA0{F;4Y zn|QDalVPI6iO_D3NO~1>v%Gv+n*8gU7pk>l3xPzAi~jKnlZ`V|ySAXZL-=@CIEsLT zpElk6XrY}*t-lsPHBJw@qoNgkz*ju7Cw2pP^c-eKv69?ybKD4dz7BAIN=>B+e9}ARvia2KOTaOYH7QzY(XACC8}&J2J{>k+hQZ!-}^&=_|Zp zBYDcYK{V^IO#N?;^6FvqY-?8ARb8U*@b@ZzAI<1QtSDIEdGmzj?+EHjfXoa zU1odMl3h;q-U_!TLF_^m#6INCDj1TUjeP9P+efnu)q^0)J5%uP|02qj4OY!Wek^x4 zayu^eneH1$`MMqEfGBQ$YA z=EI2mXP+W&6J4z+JP&&zt z@f2=-k?2?Sc17Y;L@pFAIH2%$HH6^a*|VA@v?p^CI5=7d(2>@!j?24A`G#FeMpk
CiyC-kx_Q36N~;hUSg=9!}HR~l+`H(UT(d1 z3t2a`%A6#EHT%C8$LRG#2sm=)Buk%Z(PrODVaVYF(?Fo6u)&EDey!i{Xcl~SeTla& z?s>Ws=N!ezw(yPd^W9d_>hB+N!?K&3WDw=RPf=PfUl?NkEN9#B}4XH zb?e}#xseHJSnL*X%Hu`H=9?ZeA|t)U z8kw1CZfC2Xy&_}~2+hcX_RcAq6j2YFSVw?*bFy)@otD-1ZpJ#{v30nxW z;Ah7g<4P_MaIj+Zu-XIS{H?PdudB2DNCzxq)YCBz{tOT~j|#`vdFhA@84W!ZN7?gnua; z9)?6o?L^QGNZ2shMIQkIG`jMSr&=-A4Uw=P)w-#Uwt(YMRfGpcqb#ipL~X(j2d30R z**i=o@jWW?gYP@C#Jh5?eHd>&JN6|`8y6rvy=PxEubJSk+3wHxaQY<_wHi6g5Mw4V{RHaKC8-s#*>VWf^zvPG>Tn}Fn(_;hJBZ35bm=W*z)&7oTO6H8nMCM8wj<|M+*4P zO=H+ad>`ZdQoFgqSVv&Wzms019Ymc{+?w6&d znVWs-kZSpG< zTe-NDM>L5geNlHVh9q%^)wLfXM8|qL`OLr&TWm)2Kdlb#?bLkpX{)8hJ>+3{o!9er zG4#pbJF%|2cq~98+*#FTMjW921yFnyWF+H`8ll$xybJF_Ir%7QHr5O^UV|n>qjf~i zy+OC5Yj4=oerT`NPMd$XQQre|rgm(joC-SPm7_9HY_gfq+#0SV8E$U|i1{AkQ=Hg6{WLqRe-XJS6FTtznL&|GL|7UrLGD$! zmYDE|^qjcQz?cdkqeKp_B>e^yuO5{4D`ZP>XtCWvOvRk2-iy5Jpn9dM9QenaU3u(k zHI+*D57e+_Yte7NkIQ`r+_^dZIh7gO>N?&<8tvk;I%4>6*vky=&Bfel0-BL4nVde6 zCqOOchoYNA)KO;y6{|j5HJ)Hey=tSu~iVpxO3r}2u& zpJ|CO4!qrQqZzFq`$=E_?5^YO+jo{LUssU?HRAnXHVrOVLZdA=6f(d3MG`R9+jyrW z_yV}Ecf9qLdaz*Z0#e~@$(Lryo-dl|;8fq(jYp z)}cc;>R>^Oe`T^T96Fwed7M`UyB8bB+b8mxMqpkNn^~l1Lp`)_z9zS{QH!*;;yXnL zW_^ARf9aFbd=%D&o;X&HEk`?1A0QS3YaHBKX`}(ExaYhpv8(~-b1xh~D0+E(L?4fG z8EKNhLR6i%67)c+2X81Pa+Hlj4v$9wSCV`EXRh|#oFg-d=G|rnKdA3-KqP_)PZV5kTl#bMuG0z^`Ua|jh^VUHVH&^lE{#kn8ftQ@Y0=sb;!wht0Gq`=2^qRbG-OAm-ob7&as z>&Vhgh3a@@5#N?grPpz?secOeb0T;R$Fo?T=Rv*fIKDco=D{_{5XV%KyFN>?eSedB{qUpZN^-z;btezQte7w?}&O z$%T_IY>@@zc?KajOA8f<)*i;s2?z)nbONMw-_QnYG)H=HewS`2Sw5OVWdM_bwY}vF z(^+2jvM&jTu zG3%{$8%|pc3s&VHmyG>dz!{JE98iAX{JS^trRi_fVjpL>%XECQ;YG+nx%W9dV~|ld zQAz#qyx!%(2wx6j$G09WHHmS8C4hM9936J7be(l=ptn;<3Zm=j-=F)!*<0#n$okmo zUrfe?^4f;m`e{BA^=wg@foS1}UqT-$>wJ9pkDCE>HVfk#8+S@AdS38A2Mg_i`t`37 z49RW1`OC+nHt=eWjCw!K=WCaF7C9{Qv9|`lhogKRK{OG2I8dy;FOD2S=<`_rvh?04!#j3kHaOTu*iYBf_m{W|Zvz)#a z;`|WqXIa&tb5FTV?2rcYwCLZm@z;Xvtsu0EIv00I9czUBj^smsvO2<+926gYG84WFPgy@Ot=7XX8CGBexG8 zfGz(BH3bdt?s9kN<8zkU=l>i_ti_$Gtqv;FcnV87Y&UotU#uSFp^BS1#PNe+h`)u| zEkrBWXdx*1gK9GZ4DNtOgE7;lS;s*5#HyL!EsXkNc~ZcUO|3VMT!G~;#>mT~GIxya{?!3VkGp(iEUAjTt+oE4U zuB&dRZc{r;(p$Vqfzb}|KT7QWY&UuGB!W`hts+&(KJ(W%+qAmHFpe?MO{z3FQ9kY9 z{oBz}zuZXTt4RwY7z`?;PWfsxDhEun3NVXYt46iw2j{&z?{C{{7|XWg`Y)`niIBSf zV%O{e+2TjvWqWBrMb#YMSwp>aJfh6~wB{6y>)0&flfKg}H`=}*d*cmI3Rpb50>wds zSmZA4at3Bc?ZP`MY6>_Nvh%V|laKc?V9{Okgb&gHxxF%{a`jZ`kWUHz?Xk0K_dwMi z!}5@_yX2F}C9lFh$M9*=2>$N{+4mIT`X!*#}4SC#siTgMADIaN4I zXpbuGwVd+->=__=_xkO%OSEhRS_VIFz`Ns%0d`c^n6H|V$~+3{|GZy3ur+l$6?LV^ zZk>%~9VPQ-+_55d{JYzSKYrs0OVAa4$1w=2qo#WBA{*Y=%Q^_gm@}_bat=70ilnU1}|&R3~faD6di9r zbwxjO{n2u5y{^)w!oTNTDGg^q_vy-h4n#l-hB2i`RA}k|Sn(7#a z+7x6I+;K3l(8(%m5zB)e~|1(0fl%7*?%LVXnoaUX* z&cg$}dY4a$bMVR9biR<@Z1th zMYtreHjn@{H~k_B(j84skfW`A@3pwR?!cf40vFul1#EDlY~INcsIC@)^V9`{ID#eE z1E~U9zrjR$0Z?La9|z^{QqS{|Wr>Z0hqq=RPo4Qtk^})59c{$Fw#kw(9ssPukM0;> zFZtC2j7m6imey;}AHuDu2dg^2;W-=dWA<*lSFKw|M+?9)4cBRhU>4Rrkr=lOQRfk5 z#oqvN(0~`>v_Te|;Yk=Qe&bcY*jr@r&wvZ~s<#Zu#7*2y#65pnCKUmy9~B8O-hkPNUco zt3Wh;8rJ9wj!%HS*|9MXncE!_qId?NKF8T%FXKb;IVJ0J(7C;rE?TFJ{peq-=u6n4 z$UR(edliV_cd3Y;jhw*W9}j72+R36jnto%xCBUp)RG$12f>P?n9JvEvES)!3of7zI zQrGKhgZzA9^h2h)Zsg5`D&|d~spBO;qc((^&LgIO$n!_$1~6jVs&qYcoA*Z}mP1&U zyfUbeZJ>a6Ax|f{If{FoRS-FYamv(rKv{i`S(W_2CYDbTMNV6#DaEYUcImp3@iL5r z<0z{(3tO(K!d&b|UFwYiT2Z?~{1}N5;VG;E<^~j>g?FRkQuwV`+GA?OtJFfUZq02d zRg7wvMFj(K&EPJSTMyihqQZ$rLIpndNAT!DX#*Yeij)a@ zbFsOxUXb+zwN_W6rP~*%C^;Vi=6z#*Mhrq)t$HSvbYb4U??L4u$W0A$Dv%`-v>Y-% z$2;b~wlC>mdcf6M!8oZV zwZuaPWX+v4d8a@NU>rm&9GI;zxL2tXjD{l{=cW+!)YTq#~{psEhNd6#5z;WbYY zZehaAj;s=QV4y=fTE}Ek7pH5K{DZlL1}zXx`G-=nxOt5H7)~jm6#IQN21Ls7ai}A$&ZGNHfZ;xKAD=AuQ7ZuF zXxQ#1P9*Rx=@m{3E-5t95fhSf8N@%m8_FnuhNyVMk^A?Y-#zbTHaT@HWeRGLmln(%ILk9R>X0aA z7+Mqt)6MRN2YwydGeTd-m?&sWtQcbb8N8BnOvcc^4%CdFvpcx6bLEcy#e%AtYu zZ$zVH*wW#sK|u)KuPrwxNHc0q+>NV9rYP*i=p}x1H$43nUReDyjObCTA@&>{)yWlM@x3HSP^~!pdx7u2c&|ghJ!@ zzRTi?>{lA{bz=w?A9*GMSbr((HZNGjyr1|!EDA*LFNxshP@8}_?ky0(Wi5}vlm#;q zK_puClb_qBQ)XV>dm1eaAIi={|25I^Hz)x_`X7k!F2A{TvACa~OknyYS)g>!r>XH2&hLim7|!NaTDxZMAVp0o^8_ z(9000`LOZpW2#;sxssrR&7ZCw2Qh;-{2|s?A2V})+#bhZL2e~uf>)D_lJ&NXBfn5n%l^|S~vkQ5>I#FKN?wA4yi zpBIXJnL9f-`b&%D^uE2uSUt|Rp6+v@@)QrJ0#}dx*R)2XnxCj<6~;fs(Y7fj*xDvd z{fUdfTQcm4! zL1p{`43ChY2W^GikAWu&5(z!qVhoWhLneY^V%1<5S~rz@BFM>@LY<5-?lEk{}RKVi_&-Fr{)GfjRhgQ6nvi+k2;%Q226 zN!jgioGIhJ9aG6W!Twin9u#3auZHYO6`2)Neu(ii(+w$TR-VQxsFUKn$ex|laEoU| zm$A`oQj&nyS?cq3tj%-}uHUW2dZr6{g+UvY&&^?$m-l99w>EjBk!ALgSH!k0<
orm2%pQOv@Ud4fb)I7v%-((q;#fpe`!7sx?Tnq$TRDA| zEm>*dCso@h_f!|Q5@S)zeOFJ?onKKU7G3E{&b{Nl(KHkmq9An43V$2_d~qcSUvJM> zC{jgC<{Z6M@Or1L`s*=S@K%m@4kyWZvxeV8=iB%Iw@Hxo)j^_g&`9gyWEwKZTT{$r zkq(U0Ju0nlpxm*UmZVF;bHD=#O}5j9tL=^FG>A?=%k*+Nyl#-{^<#e%&29nO`|flj z<>KT#2JNAkOhhW7ZIZk@uCU#=$H$6gL;v!WSnOt=-dVOKrvu-qs{-eSx2hb@PF5E` zGWR3KSHeGEeWh51F;X2|7R)Ns*o{&hy96dz^Atjz{W0qPn^J zR)r>BWW_(BkV>K*cwL;?i*Ho!wft*nRY;lS$4jE6KW)U}riN+GozUJmp%7|8>qo-d zUqHn9t3&YNK=qZ1U$h<`XHX^G=_<*t(oXDlu3QUs7dMY#PcM zV_JQ_z%*On?5ybfiLzr(7mcYmBABk}kbpQm)6dT>?{Iil@TH8ez1e$$(kL&lr*9fO zl_NjKk7WKC4|uRgn6n8Iw=y!$J0kwRPuOkW4OvX##mjpw|JT2)?GjMun^($tbSyFNS45xvJ?TBix)+T6DGP-}ziq9vxBq~$xSWj=*ggjbM> z_~(f4z&Mr;=x;{15^-N9pZ@e(*Ex{*Q(nYSV)%78zo+G#c42R!&A*`JX_e1>z4erm zL((mb?trFmn~2xSU+}#>4 zNoswFjIOuPznv3z;xc%uOghZ=weeqNjQ#7u=$YYVe^qlZ*@M3G%h@j`{||d_9TjEQ z#SKeJcZYz~(5W;GNSAbXr$~czcOxJmjPonK1+WwCc5|(Z($W*to}vbfOnV&q-bPr@FMG zD3ZmLO45@Aa1d%u>L6k&a%g ztO)S?m$9a!X`Mi)fyvgB0!E;=SHCRi!~v-B=0xI9%y zk_z^&)ZC`I9&6 zp^f>oM*MiuGP9nNuMOS~^r;$za-=+xd^5vck^|OQ7~+ld>V@m)ZkNiIvmiVzTzH@C zJ!_u-bKR>1$YWD{r{1Z=Gj+~ksO<@@PXba(?k4bg@6HB7`+M$U@EuG$A$?rRN|QsW#kwv~WJgTwV(77CLUUqtvW2jY2} zzWk@iDEVnVo2!)XPCvLAo3!6i8gNI`$Vap9E#1(XOK3~{bx3E!D~%rC$_1bkcOv=m zRNxSiL3DGi)>%Ik85DmM0tPBGrgu&d9bUbSXeDc-{rTmQZ4~5z>hpJd3gMy{Nl!WP z3oa{vFC}+7NG)@QzF(|O742TvX2@@25_&-uczj%2j7OH7!6Q9c7@>VYM2jpd9M~09 z@#T#a4%9XXV)Sw8oyJ-Tgw9F0-O{w1rjIL<^H;f!v=UBff3}s^K+;nt$&1|nZU<*kj>IyCbz0So!U{wlwJE}HnPo9}8ZB+E5v#&0 z1{ni5gEA=!Q}77Bv{R~`ECLtgE)2Ug{k*$(m%v>C8cuO{+Y3|qcjCF#_nRVXA3%$K z0Z#=oVP83~iFQEoRv$gwKgOVZ^i3drLkk3P?UFG7!j6iA+vk_tl;~K*T)v}2?KGf6 z(>fk#PcI;PRka!@tvRD~J%L;g8sN_^4Dd){!Tr^3d8ny|*5AC*<~$n+K>xnDGJx%+ zsVCg{;gw7AS*WK;Lup@he#b>)Bm?P!_CmiPqT|qhyf$)w+?w(GfR99=Dibr~F7zl! z<-mESVC4*jq1G+$2RWXMBYA;#ddCqDyO?d#3reMjy8 zwYbB7t%zWUdT8&hdBvE6N^G*J)?Z_l`K6jk9eq{tPW{hr@~Di0KuEG~slJX!DJ){U zmsa;2`Aq*{X?0eOf)e}8clj*!)sl=!d39%<)QP9&zlCF2Wbe$V;Xs%;mHppQ?IF>Z zKG#p}uFqn>?tZy7ZD{y1;gxHBiu?nYi8=!wfoA_=a6t|^sK;2xII*BzVqWs*_|JKx z(FnV!LN%cGlzDv5O)`%USM%znNHckYKY5B;KeS)O-r0Ib980 zw}-XB$kjiW>2Bhx&;&OCM-ixyAzyMF3t0inp|SfHb?1+ zoyy%jUJ>Yq=fwWPl(-WZcd?_t;zE`TJ*2R2jYkMIwokPG`x2vH!MAkD-o=)qa474y z3`qoCI--8$j2u~~+mWWH9}QfcnrU9kDIuDN&lYWX9^juWFFf7;o|M@9#(elGp1R;K zCpj8gQsH5{cT!Qaw)<95{k;@skK-A5@`i2PguOnFDHQMal@MD6HGh*!{kzVC$?Agk zhY0=1UX2t@*MuZ6Z<43K95oI^S{Oi0B|g%a0;NWr<<3GsB)g<~ z39)1w+%3sM)NX&?@m!d_yi7CtN{HhL+S*`TCS_w%J?$jW{a2S!l#iAgT61EwKUWr` z{?pAh>UMPj)ID288s~eLEqr$x`7WWD4h;t0q&I>v383gjV752-SWHh<_8gYvu^jgya$v0eBR1^*BpGYAUafLqCQL4%aboh1p?ydJ#uq_Jy13bWZ5G_l3yzV z0;2NzohK`^Fy@kP@?h>AmbozD)r;@7gBX7d+xS_!i{uRNTz2^ntyV8m?S4P}vFK21 z#Kz~LF96gDJo)V7#AC^AE+3e9tq7#d-j%ntm2Uip&@v&>78H45W;{KIO7}#WV)w}H z)fwlPLE9+=Z>KH>!9mVWX_OT5r;7rc7sYQFjI4?H5-w>)`4-&7>0|_eIKQ+KP=8B< z?#}FR?D}wr-Pe`qJ{qQR8#+;uuj3<9;q2Ztc+vs-392C5oqr=Fs)24>c!u@?e%eIp zgzMo`B|ojNVm_JLj9LJ>JDh3{6=aTMk9=Ka0~4FcFM3%NC;#m5hg%SS!D(MPjp0V9 zcGw^LIv6>=GB0LJI*P07JY^(iT!Q%FVLw-gxuu1vfvTagjn>RUZQ5CDIMOxa z=gD5$H|qVLOVP;IIR@>0n-Lk&uUhb^F}oBr9@_lg&<~r!{`_p({A`0i@c($i(qa1C zbAYIQcmU*gIQRcN#lu4xM^>A6ia`4BsBlDpaiT@^@qA%WM1avfKZY1#BD(cOI6X!j zjrkSMx(knI0l3uv$ODv^5*JeaPljLFIIz$FKN3&?36x8Bc(FIADkiI@-~xx{}*vhD&c#_*>RA%)&Mu8>ZRj0!SA^d^+^ z_pAHwk%l*AYeqUR&2MnvT7QQbf#r!cJZu+O7w5|(zW>2oVu&{nqY>biLyG(#%ai^8 z2?f9s{J$fVz}68^rdagq$M)(q5Nv-oQMw3$JpNEpQuI#Ly9SNlWwY?$iQN=v!(hIy zJ8Ne&Pxosh@UUgpR&iHM21GLMQBj@SUOrF*SHAM)Bx0qe2X)?l{>kG}LY{`a#lp9V1D&tqN@>K2|yCdD4|gC1?UU0tW)z)}!M6 zg{lQZiew483JOJ;wm`|`%aS(-F&kdrH`hI((cLF2J#T)gD~O@Uj)r9W3~YT&G573G z8`JXrRl#({MiEe6ochE4uFx2rwE825Y1ajb7yVoA7SDY1e!JBZz-KXfrYPd0Jam#o z_x9sK46$>$bE)gjQ=6Tydy21Avex@23jyFYfun{#!`Vr31NMe_&!DOIyOwXOMr$W? zq@bB+VwtF{=OyWU&M!iPCmdHEH?{_aeLR6S^X?FjcQX{o4|GIP7(4g3+~}?We$2~H zXWXlz81X<$RMJe|$2{@e5loZ*uK{xMO|IW=PgoWKN#*N$saJw27BBw*9Za$QM*>Nn zN;EMC<<*a!(wUkAh!iUjhs!hCZMDi2ICD`iP^`vApJvuB*Rx-2(j*u3h#(R1Rf$~* zNQm7S#7@yD*0FB)UhT5i4!uZW+4!Jek5LGeLw~4q-T4rTPHOXv+?q}6C6G^~{57Vh zNGp&NsQ5*A9NG*(BYg}(O|`nne6{VeaRc}>3QB?83`X42suTf9)NfFcXNC`tqK@a~ zdUe+jn3vwpSwou6_Js-;*+8PAIzY4>1{`aHIAwO&P!Tz22!@4*#HOkwrA|z!Qk#eAH?3u6lICey~cG!$^cY)o`H~ zw$ySM>NpLQxX?F#8`Ryj1?ty|@t7?>-g?Z!qLHvq$m5XxX6WivpU4A#ea_i^%Tkf| zy`jb{8qhKk;xAc=_H}hmQTypv%wGVyzZ0VJR7i@5;$H*?o`u06iNQ2usXn5XjQ%>w zv2s__=(BGwXljN4Z9nWnLLcIIY?>cpEyu2iN%ln=ort}Mifh_&C5QL8w}>ck^ot{| z!tvLV`OTgjpkKX?B$08Om8_n*l6qNQK#Bq^l78$jcM*2ru>W9JPhbXk*A{2K?igp*0-lnmL{@T1%dSl+#R$ z((-1egg#*sXs?TZ%{U-JH0AM-jRMaZ$Y)+W`NlHrIJl#Qw-n7qu(Km=Ia^ImEatA$ zbyRQ3Oo)c`*efAb-jI~{<>#Ozx4oGicZnOqXG&lX4zY)N#_-R*ou!r9wp{*P$%^%u z**rw_nbq>ODkXnzWHs4m1aPBKkQPGt&|?njL#_5u#~9oE1NbKY_OOnA%{)t}L2Rp# z%O=vIB1+eGG;sx&FN;^^IXhT^B87K6q6ZZoU9#$eKK9lC9zk`5eSOfyKa3*M>j^vW zpy?V44f+A5_9|vD(huy1#CWQR34{x%@NF(BVTKql;U4QUi;5Bh=jDSr*Q@Vk2!wZ& z_5L&2h%x=nO};L4sWYXL({++RS9=~e8V6+~k}Pm0Dq*Y=vAvJNTG9M)yyv?qG_KpK zgnbkbcY~M0U`=(KTnH0c$a;YgV8K`icNPQ+bav^w@jmVYYAm}>k{QOnQ{-28D!4nP zVp224Ge~}-G-T_3dB){<#c~WQ+PaCn0QVzG{R?}Q!}d5a30EE#UISAPrG_|ME3IhR z;v3N}zoq)5KksKJ;AAvXpC zPVJ75yaLlEUoL#o=GrTTeZ1L3c((YM4GWLC;ep9hpUd3=yryLxM6GG`10_aL>zT?o zinugH$3Wb!UGvy6wSMsU7cvDWQH?geeAMxsZlv2f4vMxaL1TD5fDKq1;#+GjiF9;|#<<0>|y%)?eN@rp%qRKr{_#sNt( zgtEj=i_f^tOv>P`fcu2gRF!SB!3Ek_{b!SO<|5J>+SEMX6LJXyF(pbKD%@{9dH~|b zMSl|~4d_(B_qXj7nX_@cg}P9&=*ytmH{Nj%oMRD^f1!sa%|XKPj-p7}m;@&UJH@?J zwLqtp0$y>~K$t2mG?*JGJ>C(dhJK?4g$G$H9MZ3XbUxqp}_pq?zcHojV`Wg^7C@eaG5DR!s;To6;l}o7|1l-N;!gBcg4Q# z($D>^P-3+wn0&#x!fJBfn`V_sbp4aeECWl{FDr2eC2jm0oaoL(xkK##2Rq2v^;AAJ zlm<{?f$k6Y%CJlu@I7)FwBUc-AiQnXWXVkHvS>8_8#_q3mor<-@DrN}CD~of_?PYS z*(Z2R7^KC;mc? zQD=7D4c-AR|6dDykNujNI`T*7!8q zwI%U(qJBsK$K-eBB#;wsGvVJIu;7sYWwL3#6&lNiEe%HLJ45n+o#*7w{9gHT&+h1%7<|9LBpPFYA!BKPow#QPJF;bN3}A3t z#Kix#K$Mt5Gh*dWY+niQj>#|G>@FW&xXqdZkSXaj_dJ?N(^B^70^bbKq15W#_SEO= zew-tOg#t)fG{3(k(U-|D)T~(G4WclCaNmDT?9ei(8MymwhBC*1yy~3^K*2YU-QjHB zC<2m-yBC%OR--$&hu{|iYI3?S&XB~mofk;_yGh(i%)4Jm`RiqQ2w4K#~B<~a9 zUtJ!IS||Z&ppiInHX*aZgZQx$dfiQX%{vhu?3 z=XYBK{8it{x_|Q9Ke*(GFEj%fI5xeB>oYB3331JUt@YyT0T)zI4#2!xj64n~L&