diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c347466c..d98934a3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -206,7 +206,12 @@ "Bash(time make:*)", "Bash(# Fix UIComponents imports to UI-Components\nfind . -name \"\"*.swift\"\" -type f | while read -r file; do\n if grep -q \"\"import UIComponents\"\" \"\"$file\"\"; then\n echo \"\"Fixing: $file\"\"\n sed -i '''' ''s/import UIComponents/import UI_Components/g'' \"\"$file\"\"\n fi\ndone)", "mcp__filesystem__search_files", - "mcp__filesystem__edit_file" + "mcp__filesystem__edit_file", + "mcp__github__list_pull_requests", + "mcp__puppeteer__puppeteer_screenshot", + "mcp__github__get_pull_request", + "mcp__github__get_pull_request_comments", + "mcp__github__get_pull_request_files" ], "deny": [] }, diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..20b81770 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "anthropic.claude-code" + ] +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8b0da518 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# ModularHomeInventory - Agent Guide + +## Project Overview +- **Architecture**: 12-module SPM structure with Domain-Driven Design +- **Target**: iOS 17.0+ (iPhone & iPad Universal) +- **Swift Version**: 5.9 (DO NOT upgrade to Swift 6) +- **Bundle ID**: com.homeinventory.app +- **Team ID**: 2VXBQV4XC9 + +## Development Environment +- **Build System**: XcodeGen + Makefile with parallel module compilation +- **Key Files**: + - `project.yml` - XcodeGen configuration + - `Makefile` - Build system commands + - `CLAUDE.md` - Project-specific instructions + +## Module Structure +Work within this 4-layer architecture: +Foundation Layer: Foundation-Core, Foundation-Models, Foundation-Resources +Infrastructure Layer: Infrastructure-Network, Infrastructure-Storage, Infrastructure-Security, Infrastructure-Monitoring +Services Layer: Services-Authentication, Services-Business, Services-External, Services-Search, Services-Sync +UI Layer: UI-Core, UI-Components, UI-Styles, UI-Navigation +Features Layer: Features-Inventory, Features-Scanner, Features-Settings, Features-Analytics, Features-Locations + +## Development Workflow +1. **Always validate SPM**: Run `make validate-spm` before making changes +2. **Use parallel builds**: Prefer `make build-fast run` over `make build run` +3. **Clean rebuilds**: Use `make clean-all build run` if build issues occur +4. **Code quality**: Run `make lint format` before finalizing changes + +## Testing & Validation +- **Primary validation**: `make build-fast` (tests currently disabled due to dependency issues) +- **UI validation**: `./UIScreenshots/run-screenshot-tests.sh both` +- **Feature tests**: `./UIScreenshots/test-data-features.sh` +- **Module validation**: `make validate-spm` + +## Deployment +- **TestFlight**: `make testflight` or `fastlane testflight force:true` +- **Archive**: `make archive` +- **Release build**: `make build-release` + +## Critical Notes +- Tests are currently disabled (see TODO-SPECIAL.md) +- Swift 6 upgrade is blocked due to compatibility requirements +- Use `make build-fast` for performance (parallel module builds) +- DerivedData conflicts resolved with `make clean-all` + +## Agent Instructions +- Always check CLAUDE.md for latest project context +- Validate changes with build system before proposing diffs +- Follow existing code patterns and module dependencies +- Never bypass the established 12-module architecture \ No newline at end of file diff --git a/App-Main/Package.swift b/App-Main/Package.swift index b477899a..c5d9a19b 100644 --- a/App-Main/Package.swift +++ b/App-Main/Package.swift @@ -44,6 +44,7 @@ let package = Package( .package(path: "../Features-Analytics"), .package(path: "../Features-Settings"), .package(path: "../Features-Scanner"), + .package(path: "../Features-Receipts"), ], targets: [ .target( @@ -80,6 +81,7 @@ let package = Package( .product(name: "FeaturesAnalytics", package: "Features-Analytics"), .product(name: "FeaturesSettings", package: "Features-Settings"), .product(name: "FeaturesScanner", package: "Features-Scanner"), + .product(name: "FeaturesReceipts", package: "Features-Receipts"), ] ), ] diff --git a/App-Main/Sources/AppMain/AppContainer.swift b/App-Main/Sources/AppMain/AppContainer.swift index 62e767f8..7f3621a7 100644 --- a/App-Main/Sources/AppMain/AppContainer.swift +++ b/App-Main/Sources/AppMain/AppContainer.swift @@ -1,5 +1,6 @@ import Foundation import CoreGraphics +import Observation import FoundationCore import FoundationModels import ServicesAuthentication @@ -15,7 +16,8 @@ import InfrastructureMonitoring /// Central dependency injection container for the entire application @MainActor -public final class AppContainer: ObservableObject { +@Observable +public final class AppContainer { // MARK: - Singleton diff --git a/App-Main/Sources/AppMain/AppCoordinator.swift b/App-Main/Sources/AppMain/AppCoordinator.swift index 6518abfe..71d0b69e 100644 --- a/App-Main/Sources/AppMain/AppCoordinator.swift +++ b/App-Main/Sources/AppMain/AppCoordinator.swift @@ -1,5 +1,6 @@ import SwiftUI import Foundation +import Observation import FoundationModels import FoundationCore import FeaturesInventory @@ -11,15 +12,16 @@ import FeaturesSettings /// Modern app coordinator for the new modular architecture @MainActor -public final class AppCoordinator: ObservableObject { +@Observable +public final class AppCoordinator { // MARK: - Published Properties - @Published public var isInitialized = false - @Published public var showOnboarding = false - @Published public var selectedTab = 0 - @Published public var isLoading = false - @Published public var error: AppError? + public var isInitialized = false + public var showOnboarding = false + public var selectedTab = 0 + public var isLoading = false + public var error: AppError? // MARK: - Dependencies @@ -233,9 +235,10 @@ public final class AppCoordinator: ObservableObject { /// Stub implementation of SettingsCoordinator for build compatibility @MainActor -public final class SettingsCoordinator: ObservableObject { - @Published public var navigationPath = NavigationPath() - @Published public var presentedSheet: String? +@Observable +public final class SettingsCoordinator { + public var navigationPath = NavigationPath() + public var presentedSheet: String? public init() {} diff --git a/App-Main/Sources/AppMain/AppMain.swift b/App-Main/Sources/AppMain/AppMain.swift index af468fef..83282ee3 100644 --- a/App-Main/Sources/AppMain/AppMain.swift +++ b/App-Main/Sources/AppMain/AppMain.swift @@ -1,4 +1,5 @@ import SwiftUI +import UIStyles /// Main entry point for the modular App-Main module public struct AppMain { @@ -7,6 +8,7 @@ public struct AppMain { @MainActor public static func createMainView() -> some View { ContentView() - .environmentObject(AppContainer.shared) + .environment(AppContainer.shared) + .themedWithManager() } } \ No newline at end of file diff --git a/App-Main/Sources/AppMain/ConfigurationManager.swift b/App-Main/Sources/AppMain/ConfigurationManager.swift index b3116f08..cbd097a5 100644 --- a/App-Main/Sources/AppMain/ConfigurationManager.swift +++ b/App-Main/Sources/AppMain/ConfigurationManager.swift @@ -1,7 +1,9 @@ import Foundation +import Observation /// Manages application configuration and environment settings -public final class ConfigurationManager: ObservableObject { +@Observable +public final class ConfigurationManager { // MARK: - Environment Detection @@ -192,7 +194,7 @@ public final class ConfigurationManager: ObservableObject { public func updateConfiguration(key: String, value: Any) { userDefaults.set(value, forKey: key) - objectWillChange.send() + // No need for manual change notification with @Observable } public func resetToDefaults() { @@ -200,7 +202,7 @@ public final class ConfigurationManager: ObservableObject { for key in configKeys { userDefaults.removeObject(forKey: key) } - objectWillChange.send() + // No need for manual change notification with @Observable } } diff --git a/App-Main/Sources/AppMain/FeatureFlagManager.swift b/App-Main/Sources/AppMain/FeatureFlagManager.swift index d50cabe7..906f0439 100644 --- a/App-Main/Sources/AppMain/FeatureFlagManager.swift +++ b/App-Main/Sources/AppMain/FeatureFlagManager.swift @@ -1,7 +1,9 @@ import Foundation +import Observation /// Manages feature flags for gradual feature rollouts and A/B testing -public final class FeatureFlagManager: ObservableObject { +@Observable +public final class FeatureFlagManager { // MARK: - Storage @@ -10,7 +12,7 @@ public final class FeatureFlagManager: ObservableObject { // MARK: - Published Properties - @Published public private(set) var flags: [String: FeatureFlag] = [:] + public private(set) var flags: [String: FeatureFlag] = [:] // MARK: - Feature Flags @@ -93,7 +95,7 @@ public final class FeatureFlagManager: ObservableObject { // Persist override userDefaults.set(enabled, forKey: "Flag_\(flag.rawValue)") - objectWillChange.send() + // No need for manual change notification with @Observable } /// Reset flag to default value @@ -104,7 +106,7 @@ public final class FeatureFlagManager: ObservableObject { // Remove override userDefaults.removeObject(forKey: "Flag_\(flag.rawValue)") - objectWillChange.send() + // No need for manual change notification with @Observable } } @@ -227,7 +229,7 @@ public final class FeatureFlagManager: ObservableObject { } } - objectWillChange.send() + // No need for manual change notification with @Observable } private func getCurrentUser() -> FeatureFlagUser { diff --git a/Features-Analytics/Sources/FeaturesAnalytics/Coordinators/AnalyticsCoordinator.swift b/Features-Analytics/Sources/FeaturesAnalytics/Coordinators/AnalyticsCoordinator.swift index 3418f3cf..8a1e16f8 100644 --- a/Features-Analytics/Sources/FeaturesAnalytics/Coordinators/AnalyticsCoordinator.swift +++ b/Features-Analytics/Sources/FeaturesAnalytics/Coordinators/AnalyticsCoordinator.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import UINavigation import FoundationModels @@ -34,15 +35,16 @@ public enum AnalyticsSheet: Identifiable { /// Coordinator for analytics feature navigation @MainActor -public final class AnalyticsCoordinator: ObservableObject { +@Observable +public final class AnalyticsCoordinator { // MARK: - Properties - @Published public var path = NavigationPath() - @Published public var sheet: AnalyticsSheet? - @Published public var isShowingAlert = false - @Published public var alertTitle = "" - @Published public var alertMessage = "" + public var path = NavigationPath() + public var sheet: AnalyticsSheet? + public var isShowingAlert = false + public var alertTitle = "" + public var alertMessage = "" // MARK: - Initialization @@ -128,7 +130,7 @@ public final class AnalyticsCoordinator: ObservableObject { /// Root view for analytics feature with navigation public struct AnalyticsCoordinatorView: View { - @StateObject private var coordinator = AnalyticsCoordinator() + @State private var coordinator = AnalyticsCoordinator() public var body: some View { NavigationStack(path: $coordinator.path) { diff --git a/Features-Analytics/Sources/FeaturesAnalytics/ViewModels/AnalyticsDashboardViewModel.swift b/Features-Analytics/Sources/FeaturesAnalytics/ViewModels/AnalyticsDashboardViewModel.swift index 23332fc6..80dafa13 100644 --- a/Features-Analytics/Sources/FeaturesAnalytics/ViewModels/AnalyticsDashboardViewModel.swift +++ b/Features-Analytics/Sources/FeaturesAnalytics/ViewModels/AnalyticsDashboardViewModel.swift @@ -1,25 +1,27 @@ import SwiftUI import Foundation import Combine +import Observation import FoundationModels // MARK: - Analytics Dashboard View Model /// View model for managing the analytics dashboard state and business logic @MainActor -public final class AnalyticsDashboardViewModel: ObservableObject { +@Observable +public final class AnalyticsDashboardViewModel { - // MARK: - Published Properties + // MARK: - Properties - @Published public var selectedPeriod: AnalyticsPeriod = .month - @Published public var isLoading: Bool = false - @Published public var totalItemsCount: Int = 0 - @Published public var totalValue: Decimal = 0 - @Published public var categoryBreakdown: [CategoryData] = [] - @Published public var locationBreakdown: [LocationData] = [] - @Published public var recentActivity: [ActivityItem] = [] - @Published public var valueOverTime: [ChartDataPoint] = [] - @Published public var itemsOverTime: [ChartDataPoint] = [] + public var selectedPeriod: AnalyticsPeriod = .month + public var isLoading: Bool = false + public var totalItemsCount: Int = 0 + public var totalValue: Decimal = 0 + public var categoryBreakdown: [CategoryData] = [] + public var locationBreakdown: [LocationData] = [] + public var recentActivity: [ActivityItem] = [] + public var valueOverTime: [ChartDataPoint] = [] + public var itemsOverTime: [ChartDataPoint] = [] // MARK: - Private Properties diff --git a/Features-Analytics/Sources/FeaturesAnalytics/Views/AnalyticsDashboardView.swift b/Features-Analytics/Sources/FeaturesAnalytics/Views/AnalyticsDashboardView.swift index 2c0b78c9..27d25183 100644 --- a/Features-Analytics/Sources/FeaturesAnalytics/Views/AnalyticsDashboardView.swift +++ b/Features-Analytics/Sources/FeaturesAnalytics/Views/AnalyticsDashboardView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UIComponents import UIStyles @@ -12,7 +13,7 @@ public struct AnalyticsDashboardView: View { // MARK: - Properties - @StateObject private var viewModel = AnalyticsDashboardViewModel() + @State private var viewModel = AnalyticsDashboardViewModel() @Environment(\.theme) private var theme // MARK: - Body diff --git a/Features-Analytics/Sources/FeaturesAnalytics/Views/CategoryBreakdownView.swift b/Features-Analytics/Sources/FeaturesAnalytics/Views/CategoryBreakdownView.swift index b6bc75e5..50291f87 100644 --- a/Features-Analytics/Sources/FeaturesAnalytics/Views/CategoryBreakdownView.swift +++ b/Features-Analytics/Sources/FeaturesAnalytics/Views/CategoryBreakdownView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UIComponents import UIStyles @@ -12,7 +13,7 @@ public struct CategoryBreakdownView: View { // MARK: - Properties let category: ItemCategory - @StateObject private var viewModel: CategoryBreakdownViewModel + @State private var viewModel: CategoryBreakdownViewModel @Environment(\.theme) private var theme @EnvironmentObject private var coordinator: AnalyticsCoordinator @@ -265,17 +266,18 @@ private struct ItemRow: View { // MARK: - View Model @MainActor -final class CategoryBreakdownViewModel: ObservableObject { +@Observable +final class CategoryBreakdownViewModel { let category: ItemCategory - @Published var itemCount: Int = 0 - @Published var totalValue: Decimal = 0 - @Published var averageValue: Decimal = 0 - @Published var highestValue: Decimal = 0 - @Published var lowestValue: Decimal = 0 - @Published var valueTrend: Double = 0 - @Published var topItems: [ItemSummary] = [] + var itemCount: Int = 0 + var totalValue: Decimal = 0 + var averageValue: Decimal = 0 + var highestValue: Decimal = 0 + var lowestValue: Decimal = 0 + var valueTrend: Double = 0 + var topItems: [ItemSummary] = [] init(category: ItemCategory) { self.category = category diff --git a/Features-Analytics/Sources/FeaturesAnalytics/Views/LocationInsightsView.swift b/Features-Analytics/Sources/FeaturesAnalytics/Views/LocationInsightsView.swift index 5399d646..2183d10c 100644 --- a/Features-Analytics/Sources/FeaturesAnalytics/Views/LocationInsightsView.swift +++ b/Features-Analytics/Sources/FeaturesAnalytics/Views/LocationInsightsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UIComponents import UINavigation @@ -12,7 +13,7 @@ public struct LocationInsightsView: View { // MARK: - Properties - @StateObject private var viewModel = LocationInsightsViewModel() + @State private var viewModel = LocationInsightsViewModel() @Environment(\.theme) private var theme // MARK: - Body @@ -352,12 +353,13 @@ private struct LocationInsightRow: View { // MARK: - Location Insights View Model @MainActor -private final class LocationInsightsViewModel: ObservableObject { +@Observable +private final class LocationInsightsViewModel { - // MARK: - Published Properties + // MARK: - Properties - @Published var locationData: [LocationData] = [] - @Published var isLoading: Bool = false + var locationData: [LocationData] = [] + var isLoading: Bool = false // MARK: - Computed Properties diff --git a/Features-Analytics/Sources/FeaturesAnalytics/Views/TrendsView.swift b/Features-Analytics/Sources/FeaturesAnalytics/Views/TrendsView.swift index 28407163..c9aa1904 100644 --- a/Features-Analytics/Sources/FeaturesAnalytics/Views/TrendsView.swift +++ b/Features-Analytics/Sources/FeaturesAnalytics/Views/TrendsView.swift @@ -1,5 +1,6 @@ import SwiftUI import Combine +import Observation import FoundationModels import UIComponents import UINavigation @@ -13,7 +14,7 @@ public struct TrendsView: View { // MARK: - Properties - @StateObject private var viewModel = TrendsViewModel() + @State private var viewModel = TrendsViewModel() @Environment(\.theme) private var theme // MARK: - Body @@ -351,17 +352,18 @@ private struct InsightCard: View { // MARK: - Trends View Model @MainActor -private final class TrendsViewModel: ObservableObject { +@Observable +private final class TrendsViewModel { - // MARK: - Published Properties + // MARK: - Properties - @Published var selectedPeriod: AnalyticsPeriod = .month - @Published var selectedMetric: TrendMetric = .totalValue - @Published var currentTrendData: [ChartDataPoint] = [] - @Published var periodOverPeriodChange: Double = 0 - @Published var yearOverYearChange: Double = 0 - @Published var insights: [TrendInsight] = [] - @Published var isLoading: Bool = false + var selectedPeriod: AnalyticsPeriod = .month + var selectedMetric: TrendMetric = .totalValue + var currentTrendData: [ChartDataPoint] = [] + var periodOverPeriodChange: Double = 0 + var yearOverYearChange: Double = 0 + var insights: [TrendInsight] = [] + var isLoading: Bool = false // MARK: - Computed Properties diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Backup/AutoBackupSettingsView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Backup/AutoBackupSettingsView.swift index 0e0f2fb5..276265ad 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Backup/AutoBackupSettingsView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Backup/AutoBackupSettingsView.swift @@ -54,7 +54,7 @@ import SwiftUI @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct AutoBackupSettingsView: View { - @StateObject private var backupService = BackupService.shared + @State private var backupService = BackupService.shared @AppStorage("backup_interval") private var backupInterval = BackupService.BackupInterval.weekly.rawValue @AppStorage("backup_wifi_only") private var wifiOnly = true @AppStorage("backup_include_photos") private var includePhotos = true diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/CollaborativeLists/CollaborativeListsView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/CollaborativeLists/CollaborativeListsView.swift index f3af5b1b..8aca4e66 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/CollaborativeLists/CollaborativeListsView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/CollaborativeLists/CollaborativeListsView.swift @@ -55,7 +55,7 @@ import SwiftUI @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct CollaborativeListsView: View { - @StateObject private var listService = CollaborativeListService() + @State private var listService = CollaborativeListService() @State private var showingCreateList = false @State private var selectedList: CollaborativeListService.CollaborativeList? @State private var searchText = "" diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyConverterView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyConverterView.swift index 5205a1a2..2320c1ff 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyConverterView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyConverterView.swift @@ -50,13 +50,14 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct CurrencyConverterView: View { public init() {} - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @Environment(\.dismiss) private var dismiss @State private var amount: Decimal = 0 @@ -442,10 +443,11 @@ struct CurrencyPickerSheet: View { #if DEBUG @available(iOS 17.0, macOS 11.0, *) -class MockCurrencyExchangeService: ObservableObject { - @Published var isUpdating: Bool = false - @Published var lastUpdateDate: Date? = Date().addingTimeInterval(-3600) // 1 hour ago - @Published var ratesNeedUpdate: Bool = false +@Observable +class MockCurrencyExchangeService { + var isUpdating: Bool = false + var lastUpdateDate: Date? = Date().addingTimeInterval(-3600) // 1 hour ago + var ratesNeedUpdate: Bool = false static let shared = MockCurrencyExchangeService() diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyQuickConvertWidget.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyQuickConvertWidget.swift index beaf86db..05055efa 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyQuickConvertWidget.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencyQuickConvertWidget.swift @@ -50,6 +50,7 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @@ -58,7 +59,7 @@ public struct CurrencyQuickConvertWidget: View { let amount: Decimal let currency: CurrencyExchangeService.Currency - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @State private var showingConverter = false @State private var showingMultiCurrency = false @State private var targetCurrency: CurrencyExchangeService.Currency = .USD @@ -204,7 +205,7 @@ public struct InlineCurrencyDisplay: View { let showSymbol: Bool let showCode: Bool - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared public init( amount: Decimal, @@ -251,8 +252,9 @@ public struct InlineCurrencyDisplay: View { #if DEBUG @available(iOS 17.0, macOS 11.0, *) -class MockCurrencyQuickConvertExchangeService: ObservableObject { - @Published var isUpdating: Bool = false +@Observable +class MockCurrencyQuickConvertExchangeService { + var isUpdating: Bool = false static let shared = MockCurrencyQuickConvertExchangeService() diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencySettingsView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencySettingsView.swift index d996313a..ddba0c11 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencySettingsView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/CurrencySettingsView.swift @@ -50,13 +50,14 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct CurrencySettingsView: View { public init() {} - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @Environment(\.dismiss) private var dismiss @State private var showingAddManualRate = false @@ -227,7 +228,7 @@ public struct CurrencySettingsView: View { @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) struct AddManualRateView: View { - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @Environment(\.dismiss) private var dismiss @State private var fromCurrency: CurrencyExchangeService.Currency = .USD diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/MultiCurrencyValueView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/MultiCurrencyValueView.swift index 9ac44315..2ede3db7 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/MultiCurrencyValueView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Currency/MultiCurrencyValueView.swift @@ -50,6 +50,7 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @@ -58,7 +59,7 @@ public struct MultiCurrencyValueView: View { let baseAmount: Decimal let baseCurrency: CurrencyExchangeService.Currency - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @State private var showingAllCurrencies = false @State private var selectedCurrencies: Set = [] @@ -177,7 +178,7 @@ struct ConvertedValueRow: View { let fromCurrency: CurrencyExchangeService.Currency let toCurrency: CurrencyExchangeService.Currency - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @State private var convertedAmount: Decimal? @State private var isLoading = true @@ -267,7 +268,7 @@ struct CurrencySelectionView: View { @Binding var selectedCurrencies: Set let baseCurrency: CurrencyExchangeService.Currency - @StateObject private var exchangeService = CurrencyExchangeService.shared + @State private var exchangeService = CurrencyExchangeService.shared @Environment(\.dismiss) private var dismiss @State private var searchText = "" @@ -359,10 +360,11 @@ struct CurrencySelectionView: View { #if DEBUG @available(iOS 17.0, macOS 11.0, *) -class MockMultiCurrencyExchangeService: ObservableObject, CurrencyExchangeService { - @Published var isUpdating: Bool = false - @Published var lastUpdateDate: Date? = Date().addingTimeInterval(-3600) // 1 hour ago - @Published var ratesNeedUpdate: Bool = false +@Observable +class MockMultiCurrencyExchangeService: CurrencyExchangeService { + var isUpdating: Bool = false + var lastUpdateDate: Date? = Date().addingTimeInterval(-3600) // 1 hour ago + var ratesNeedUpdate: Bool = false static let shared = MockMultiCurrencyExchangeService() diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/FamilySharing/FamilySharingView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/FamilySharing/FamilySharingView.swift index 16e0484d..3e3d9fb6 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/FamilySharing/FamilySharingView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/FamilySharing/FamilySharingView.swift @@ -50,13 +50,14 @@ import FoundationModels // import SwiftUI +import Observation import CloudKit @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct FamilySharingView: View { - @StateObject private var sharingService = FamilySharingService() + @State private var sharingService = FamilySharingService() @State private var showingInviteSheet = false @State private var showingSettingsSheet = false @State private var showingShareOptions = false @@ -410,13 +411,14 @@ private struct InvitationRow: View { #if DEBUG @available(iOS 17.0, macOS 11.0, *) -class MockFamilySharingService: ObservableObject, FamilySharingService { - @Published var isSharing: Bool = false - @Published var shareStatus: ShareStatus = .notSharing - @Published var syncStatus: SyncStatus = .idle - @Published var familyMembers: [FamilyMember] = [] - @Published var pendingInvitations: [Invitation] = [] - @Published var sharedItems: [SharedItem] = [] +@Observable +class MockFamilySharingService: FamilySharingService { + var isSharing: Bool = false + var shareStatus: ShareStatus = .notSharing + var syncStatus: SyncStatus = .idle + var familyMembers: [FamilyMember] = [] + var pendingInvitations: [Invitation] = [] + var sharedItems: [SharedItem] = [] static let shared = MockFamilySharingService() diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/CreateMaintenanceReminderView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/CreateMaintenanceReminderView.swift index b4a16034..08ed955b 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/CreateMaintenanceReminderView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/CreateMaintenanceReminderView.swift @@ -7,12 +7,13 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct CreateMaintenanceReminderView: View { - @StateObject private var reminderService = MaintenanceReminderService.shared + @State private var reminderService = MaintenanceReminderService.shared @Environment(\.dismiss) private var dismiss // Form state @@ -430,10 +431,11 @@ struct TemplatePickerView: View { // MARK: - Mock Services for Preview @MainActor -private class MockMaintenanceReminderService: ObservableObject { +@Observable +private class MockMaintenanceReminderService { static let shared = MockMaintenanceReminderService() - @Published var reminders: [MaintenanceReminder] = [] + var reminders: [MaintenanceReminder] = [] func createReminder(_ reminder: MaintenanceReminder) async throws { reminders.append(reminder) diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/EditMaintenanceReminderView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/EditMaintenanceReminderView.swift index ff9400e3..fcbdc762 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/EditMaintenanceReminderView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/EditMaintenanceReminderView.swift @@ -7,13 +7,14 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct EditMaintenanceReminderView: View { @Binding var reminder: MaintenanceReminderService.MaintenanceReminder - @StateObject private var reminderService = MaintenanceReminderService.shared + @State private var reminderService = MaintenanceReminderService.shared @Environment(\.dismiss) private var dismiss // Form state - initialized from reminder diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/ItemMaintenanceSection.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/ItemMaintenanceSection.swift index 71dca683..de284013 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/ItemMaintenanceSection.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/ItemMaintenanceSection.swift @@ -7,6 +7,7 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @@ -15,7 +16,7 @@ public struct ItemMaintenanceSection: View { let itemId: UUID let itemName: String - @StateObject private var reminderService = MaintenanceReminderService.shared + @State private var reminderService = MaintenanceReminderService.shared @State private var showingCreateReminder = false @State private var showingReminderDetail: MaintenanceReminderService.MaintenanceReminder? @State private var showingAllReminders = false @@ -268,10 +269,11 @@ struct ItemMaintenanceListView: View { // MARK: - Mock Services for Preview @MainActor -private class MockMaintenanceReminderService: ObservableObject { +@Observable +private class MockMaintenanceReminderService { static let shared = MockMaintenanceReminderService() - @Published var allReminders: [MaintenanceReminder] = [] + var allReminders: [MaintenanceReminder] = [] func reminders(for itemId: UUID) -> [MaintenanceReminder] { allReminders.filter { $0.itemId == itemId } diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceReminderDetailView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceReminderDetailView.swift index 60a8ae26..99ec1e72 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceReminderDetailView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceReminderDetailView.swift @@ -7,12 +7,13 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct MaintenanceReminderDetailView: View { - @StateObject private var reminderService = MaintenanceReminderService.shared + @State private var reminderService = MaintenanceReminderService.shared @Environment(\.dismiss) private var dismiss @State var reminder: MaintenanceReminderService.MaintenanceReminder diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceRemindersView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceRemindersView.swift index 6fda6a35..9f339376 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceRemindersView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Maintenance/MaintenanceRemindersView.swift @@ -7,12 +7,13 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct MaintenanceRemindersView: View { - @StateObject private var reminderService = MaintenanceReminderService.shared + @State private var reminderService = MaintenanceReminderService.shared @State private var selectedTab = 0 @State private var showingCreateReminder = false @State private var showingReminderDetail: MaintenanceReminderService.MaintenanceReminder? @@ -269,10 +270,11 @@ struct MaintenanceReminderRow: View { // MARK: - Mock Service for Preview -class MockMaintenanceReminderService: ObservableObject, MaintenanceReminderService { - @Published var reminders: [MaintenanceReminder] = [] - @Published var upcomingReminders: [MaintenanceReminder] = [] - @Published var overdueReminders: [MaintenanceReminder] = [] +@Observable +class MockMaintenanceReminderService: MaintenanceReminderService { + var reminders: [MaintenanceReminder] = [] + var upcomingReminders: [MaintenanceReminder] = [] + var overdueReminders: [MaintenanceReminder] = [] static let shared = MockMaintenanceReminderService() diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateItemView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateItemView.swift index 8805295a..f5c4d6b9 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateItemView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateItemView.swift @@ -7,13 +7,14 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct PrivateItemView: View { let item: Item - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared @State private var showingAuthentication = false @State private var showingPrivacySettings = false @@ -56,7 +57,7 @@ struct PrivateItemRowView: View { let privacySettings: PrivateModeService.PrivateItemSettings? let onAuthenticate: () -> Void - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared var body: some View { HStack(spacing: 12) { @@ -160,7 +161,7 @@ struct BlurredImageView: View { @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) struct AuthenticationView: View { - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared @Environment(\.dismiss) private var dismiss @State private var isAuthenticating = false @State private var showingError = false @@ -249,7 +250,7 @@ struct AuthenticationView: View { @available(iOS 17.0, macOS 11.0, *) public struct ItemPrivacySettingsView: View { let item: Item - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared @Environment(\.dismiss) private var dismiss @State private var privacyLevel: PrivateModeService.PrivacyLevel @@ -361,7 +362,7 @@ public struct ItemPrivacySettingsView: View { @available(iOS 17.0, macOS 10.15, *) public struct PrivateValueModifier: ViewModifier { let itemId: UUID - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared public func body(content: Content) -> some View { if privateModeService.shouldHideValue(for: itemId) { @@ -377,7 +378,7 @@ public struct PrivateValueModifier: ViewModifier { @available(iOS 17.0, macOS 10.15, *) public struct PrivateImageModifier: ViewModifier { let itemId: UUID - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared public func body(content: Content) -> some View { if privateModeService.shouldBlurPhotos(for: itemId) { diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateModeSettingsView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateModeSettingsView.swift index 3424a9e0..dd5d9659 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateModeSettingsView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Privacy/PrivateModeSettingsView.swift @@ -7,13 +7,14 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct PrivateModeSettingsView: View { public init() {} - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared @Environment(\.dismiss) private var dismiss @State private var showingDisableConfirmation = false @@ -217,7 +218,7 @@ public struct PrivateModeSettingsView: View { @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) struct PrivateCategoriesTagsView: View { - @StateObject private var privateModeService = PrivateModeService.shared + @State private var privateModeService = PrivateModeService.shared @State private var selectedTab = 0 var body: some View { @@ -279,21 +280,22 @@ struct PrivateCategoriesTagsView: View { #if DEBUG @available(iOS 17.0, macOS 11.0, *) -class MockPrivateModeService: ObservableObject { - @Published var isPrivateModeEnabled = false - @Published var requireAuthenticationToView = true - @Published var sessionTimeout: TimeInterval = 900 // 15 minutes - @Published var isAuthenticated = false - @Published var lastAuthenticationTime: Date? - @Published var hideValuesInLists = true - @Published var blurPhotosInLists = true - @Published var maskSerialNumbers = true - @Published var hideFromSearch = true - @Published var hideFromAnalytics = true - @Published var hideFromWidgets = true - @Published var privateItemIds: Set = ["item1", "item2", "item3"] - @Published var privateCategories: Set = ["Jewelry", "Important Documents"] - @Published var privateTags: Set = ["Valuable", "Confidential", "Personal"] +@Observable +class MockPrivateModeService { + var isPrivateModeEnabled = false + var requireAuthenticationToView = true + var sessionTimeout: TimeInterval = 900 // 15 minutes + var isAuthenticated = false + var lastAuthenticationTime: Date? + var hideValuesInLists = true + var blurPhotosInLists = true + var maskSerialNumbers = true + var hideFromSearch = true + var hideFromAnalytics = true + var hideFromWidgets = true + var privateItemIds: Set = ["item1", "item2", "item3"] + var privateCategories: Set = ["Jewelry", "Important Documents"] + var privateTags: Set = ["Valuable", "Confidential", "Personal"] static let shared = MockPrivateModeService() diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/AutoLockSettingsView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/AutoLockSettingsView.swift index c7d48fa2..51f79ec3 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/AutoLockSettingsView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/AutoLockSettingsView.swift @@ -7,13 +7,14 @@ import FoundationModels // import SwiftUI +import Observation @available(iOS 15.0, *) @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct AutoLockSettingsView: View { public init() {} - @StateObject private var lockService = AutoLockService.shared + @State private var lockService = AutoLockService.shared @Environment(\.dismiss) private var dismiss @State private var showingTestLock = false @@ -246,7 +247,7 @@ public struct AutoLockSettingsView: View { @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct AutoLockQuickToggle: View { - @StateObject private var lockService = AutoLockService.shared + @State private var lockService = AutoLockService.shared public var body: some View { HStack { @@ -284,7 +285,7 @@ public struct AutoLockQuickToggle: View { @available(iOS 17.0, macOS 10.15, *) @available(iOS 17.0, macOS 11.0, *) public struct LockStatusIndicator: View { - @StateObject private var lockService = AutoLockService.shared + @State private var lockService = AutoLockService.shared public var body: some View { if lockService.autoLockEnabled { diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/LockScreenView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/LockScreenView.swift index 806cda1a..7214688f 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/LockScreenView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Security/LockScreenView.swift @@ -7,10 +7,11 @@ import SwiftUI import LocalAuthentication +import Observation @available(iOS 17.0, macOS 11.0, *) public struct LockScreenView: View { - @StateObject private var lockService = AutoLockService.shared + @State private var lockService = AutoLockService.shared @State private var showingError = false @State private var errorMessage = "" @State private var passcode = "" diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/SharedLinksManagementView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/SharedLinksManagementView.swift index b13d799f..a65ca8a6 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/SharedLinksManagementView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/SharedLinksManagementView.swift @@ -7,6 +7,7 @@ import FoundationModels // import SwiftUI +import Observation #if canImport(UIKit) import UIKit #else @@ -15,7 +16,7 @@ import AppKit @available(iOS 17.0, macOS 12.0, *) public struct SharedLinksManagementView: View { - @StateObject private var viewOnlyService = ViewOnlyModeService.shared + @State private var viewOnlyService = ViewOnlyModeService.shared @State private var showingRevokeAlert = false @State private var linkToRevoke: ViewOnlyModeService.SharedLink? @State private var searchText = "" diff --git a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/ViewOnlyShareView.swift b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/ViewOnlyShareView.swift index a89d34c4..6e46a250 100644 --- a/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/ViewOnlyShareView.swift +++ b/Features-Inventory/Sources/Features-Inventory/Legacy/Views/Sharing/ViewOnlyShareView.swift @@ -7,6 +7,7 @@ import FoundationModels // import SwiftUI +import Observation #if canImport(UIKit) import UIKit #else @@ -15,7 +16,7 @@ import AppKit @available(iOS 17.0, macOS 12.0, *) public struct ViewOnlyShareView: View { - @StateObject private var viewOnlyService = ViewOnlyModeService.shared + @State private var viewOnlyService = ViewOnlyModeService.shared @Environment(\.dismiss) private var dismiss let items: [Item] diff --git a/Features-Inventory/Sources/FeaturesInventory/Coordinators/InventoryCoordinator.swift b/Features-Inventory/Sources/FeaturesInventory/Coordinators/InventoryCoordinator.swift index b9437c83..387224e5 100644 --- a/Features-Inventory/Sources/FeaturesInventory/Coordinators/InventoryCoordinator.swift +++ b/Features-Inventory/Sources/FeaturesInventory/Coordinators/InventoryCoordinator.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UINavigation @@ -6,13 +7,14 @@ import UINavigation /// Coordinator for managing navigation flow within the inventory feature @MainActor -public final class InventoryCoordinator: ObservableObject { +@Observable +public final class InventoryCoordinator { - // MARK: - Published Properties + // MARK: - Properties - @Published public var navigationPath = NavigationPath() - @Published public var presentedSheet: InventorySheet? - @Published public var presentedFullScreenCover: InventorySheet? + public var navigationPath = NavigationPath() + public var presentedSheet: InventorySheet? + public var presentedFullScreenCover: InventorySheet? // MARK: - Initialization @@ -162,7 +164,7 @@ public enum InventorySheet: Identifiable { /// A view that provides the inventory coordinator to its content public struct InventoryCoordinatorView: View { - @StateObject private var coordinator = InventoryCoordinator() + @State private var coordinator = InventoryCoordinator() private let content: (InventoryCoordinator) -> Content public init(@ViewBuilder content: @escaping (InventoryCoordinator) -> Content) { diff --git a/Features-Inventory/Sources/FeaturesInventory/ViewModels/ItemsListViewModel.swift b/Features-Inventory/Sources/FeaturesInventory/ViewModels/ItemsListViewModel.swift index a0b3da83..c92c7a9c 100644 --- a/Features-Inventory/Sources/FeaturesInventory/ViewModels/ItemsListViewModel.swift +++ b/Features-Inventory/Sources/FeaturesInventory/ViewModels/ItemsListViewModel.swift @@ -1,6 +1,6 @@ import SwiftUI import Foundation -import Combine +import Observation import FoundationModels import ServicesSearch @@ -8,24 +8,29 @@ import ServicesSearch /// View model for managing the items list state and business logic @MainActor -public final class ItemsListViewModel: ObservableObject { +@Observable +public final class ItemsListViewModel { - // MARK: - Published Properties + // MARK: - Properties - @Published public var items: [InventoryItem] = [] - @Published public var filteredItems: [InventoryItem] = [] - @Published public var searchQuery: String = "" - @Published public var selectedCategory: ItemCategory? - @Published public var selectedItem: InventoryItem? - @Published public var alertItem: AlertItem? - @Published public var isLoading: Bool = false - @Published public var showSearchSuggestions: Bool = false - @Published public var searchSuggestions: [String] = [] + public var items: [InventoryItem] = [] + public var filteredItems: [InventoryItem] = [] + public var searchQuery: String = "" { + didSet { + setupSearchDebounce() + } + } + public var selectedCategory: ItemCategory? + public var selectedItem: InventoryItem? + public var alertItem: AlertItem? + public var isLoading: Bool = false + public var showSearchSuggestions: Bool = false + public var searchSuggestions: [String] = [] // MARK: - Private Properties private lazy var searchService: SearchService = SearchService() - private var cancellables = Set() + private var searchTask: Task? private let debounceDelay: TimeInterval = 0.5 // MARK: - Computed Properties @@ -37,7 +42,7 @@ public final class ItemsListViewModel: ObservableObject { // MARK: - Initialization public init() { - setupObservers() + // No longer need observers with @Observable } // MARK: - Public Methods @@ -92,24 +97,25 @@ public final class ItemsListViewModel: ObservableObject { // MARK: - Private Methods - private func setupObservers() { - // Debounce search query changes - $searchQuery - .debounce(for: .seconds(debounceDelay), scheduler: DispatchQueue.main) - .sink { [weak self] query in - self?.handleSearchQueryChange(query) - } - .store(in: &cancellables) + private func setupSearchDebounce() { + // Cancel previous task + searchTask?.cancel() - // Monitor search query for suggestions - $searchQuery - .sink { [weak self] query in - self?.showSearchSuggestions = !query.isEmpty && query.count >= 2 - if self?.showSearchSuggestions == true { - self?.generateSearchSuggestions() - } + // Create new debounced task + searchTask = Task { @MainActor in + do { + try await Task.sleep(nanoseconds: UInt64(debounceDelay * 1_000_000_000)) + handleSearchQueryChange(searchQuery) + } catch { + // Task cancelled } - .store(in: &cancellables) + } + + // Update suggestions immediately + showSearchSuggestions = !searchQuery.isEmpty && searchQuery.count >= 2 + if showSearchSuggestions { + generateSearchSuggestions() + } } private func handleSearchQueryChange(_ query: String) { diff --git a/Features-Inventory/Sources/FeaturesInventory/Views/ItemsListView.swift b/Features-Inventory/Sources/FeaturesInventory/Views/ItemsListView.swift index 326e9f4b..12ef7c5b 100644 --- a/Features-Inventory/Sources/FeaturesInventory/Views/ItemsListView.swift +++ b/Features-Inventory/Sources/FeaturesInventory/Views/ItemsListView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UIComponents import UINavigation @@ -13,7 +14,7 @@ public struct ItemsListView: View { // MARK: - Properties - @StateObject private var viewModel = ItemsListViewModel() + @State private var viewModel = ItemsListViewModel() @EnvironmentObject private var router: Router @Environment(\.theme) private var theme diff --git a/Features-Locations/Sources/FeaturesLocations/Coordinators/LocationsCoordinator.swift b/Features-Locations/Sources/FeaturesLocations/Coordinators/LocationsCoordinator.swift index d40da251..8e66c11f 100644 --- a/Features-Locations/Sources/FeaturesLocations/Coordinators/LocationsCoordinator.swift +++ b/Features-Locations/Sources/FeaturesLocations/Coordinators/LocationsCoordinator.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UINavigation @@ -6,13 +7,14 @@ import UINavigation /// Coordinator for managing navigation flow within the locations feature @MainActor -public final class LocationsCoordinator: ObservableObject { +@Observable +public final class LocationsCoordinator { - // MARK: - Published Properties + // MARK: - Properties - @Published public var navigationPath = NavigationPath() - @Published public var presentedSheet: LocationsSheet? - @Published public var presentedFullScreenCover: LocationsSheet? + public var navigationPath = NavigationPath() + public var presentedSheet: LocationsSheet? + public var presentedFullScreenCover: LocationsSheet? // MARK: - Initialization @@ -182,7 +184,7 @@ public enum LocationsSheet: Identifiable { /// A view that provides the locations coordinator to its content public struct LocationsCoordinatorView: View { - @StateObject private var coordinator = LocationsCoordinator() + @State private var coordinator = LocationsCoordinator() private let content: (LocationsCoordinator) -> Content public init(@ViewBuilder content: @escaping (LocationsCoordinator) -> Content) { diff --git a/Features-Locations/Sources/FeaturesLocations/ViewModels/LocationsListViewModel.swift b/Features-Locations/Sources/FeaturesLocations/ViewModels/LocationsListViewModel.swift index 80609ce4..e5d63f31 100644 --- a/Features-Locations/Sources/FeaturesLocations/ViewModels/LocationsListViewModel.swift +++ b/Features-Locations/Sources/FeaturesLocations/ViewModels/LocationsListViewModel.swift @@ -1,24 +1,26 @@ import SwiftUI import Foundation import Combine +import Observation import FoundationModels // MARK: - Locations List View Model /// View model for managing the locations list state and business logic @MainActor -public final class LocationsListViewModel: ObservableObject { - - // MARK: - Published Properties - - @Published public var locations: [Location] = [] - @Published public var filteredLocations: [Location] = [] - @Published public var searchQuery: String = "" - @Published public var selectedLocation: Location? - @Published public var alertItem: AlertItem? - @Published public var isLoading: Bool = false - @Published public var viewMode: LocationViewMode = .list - @Published public var expandedLocationIds: Set = [] +@Observable +public final class LocationsListViewModel { + + // MARK: - Properties + + public var locations: [Location] = [] + public var filteredLocations: [Location] = [] + public var searchQuery: String = "" + public var selectedLocation: Location? + public var alertItem: AlertItem? + public var isLoading: Bool = false + public var viewMode: LocationViewMode = .list + public var expandedLocationIds: Set = [] // MARK: - Private Properties diff --git a/Features-Locations/Sources/FeaturesLocations/Views/LocationsListView.swift b/Features-Locations/Sources/FeaturesLocations/Views/LocationsListView.swift index 2105038f..b0757815 100644 --- a/Features-Locations/Sources/FeaturesLocations/Views/LocationsListView.swift +++ b/Features-Locations/Sources/FeaturesLocations/Views/LocationsListView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UIComponents import UINavigation @@ -13,7 +14,7 @@ public struct LocationsListView: View { // MARK: - Properties - @StateObject private var viewModel = LocationsListViewModel() + @State private var viewModel = LocationsListViewModel() @EnvironmentObject private var router: Router @Environment(\.theme) private var theme diff --git a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptDetailViewModel.swift b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptDetailViewModel.swift index 9e036167..4f8c0a3e 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptDetailViewModel.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptDetailViewModel.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import Observation import FoundationModels import FoundationCore import InfrastructureStorage @@ -7,14 +8,15 @@ import InfrastructureStorage /// Enhanced view model for receipt detail view with improved error handling and state management /// Swift 5.9 - No Swift 6 features @MainActor -public final class ReceiptDetailViewModel: ObservableObject { - @Published public var receipt: Receipt - @Published public var linkedItems: [InventoryItem] = [] - @Published public var isLoadingItems = false - @Published public var showingEditView = false - @Published public var showingDeleteConfirmation = false - @Published public var errorMessage: String? - @Published public var showingLinkItemsView = false +@Observable +public final class ReceiptDetailViewModel { + public var receipt: Receipt + public var linkedItems: [InventoryItem] = [] + public var isLoadingItems = false + public var showingEditView = false + public var showingDeleteConfirmation = false + public var errorMessage: String? + public var showingLinkItemsView = false private let receiptRepository: any FoundationModels.ReceiptRepositoryProtocol private let itemRepository: any ItemRepository diff --git a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptImportViewModel.swift b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptImportViewModel.swift index 5d22e11d..e125b516 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptImportViewModel.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptImportViewModel.swift @@ -2,6 +2,7 @@ import Foundation import InfrastructureStorage import SwiftUI import PhotosUI +import Observation import FoundationModels import FoundationCore import ServicesExternal @@ -9,14 +10,15 @@ import ServicesExternal /// Enhanced view model for receipt import with multiple import methods /// Swift 5.9 - No Swift 6 features @MainActor -public final class ReceiptImportViewModel: ObservableObject { - @Published public var isLoading = false - @Published public var errorMessage: String? - @Published public var importProgress: Double = 0.0 - @Published public var currentStep: ImportStep = .selecting - @Published public var selectedPhotosItems: [PhotosPickerItem] = [] - @Published public var parsedReceipts: [ParsedReceiptData] = [] - @Published public var showingPreview = false +@Observable +public final class ReceiptImportViewModel { + public var isLoading = false + public var errorMessage: String? + public var importProgress: Double = 0.0 + public var currentStep: ImportStep = .selecting + public var selectedPhotosItems: [PhotosPickerItem] = [] + public var parsedReceipts: [ParsedReceiptData] = [] + public var showingPreview = false private let emailService: (any EmailServiceProtocol)? private let ocrService: any OCRServiceProtocol diff --git a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptPreviewViewModel.swift b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptPreviewViewModel.swift index 261e4540..a29be2ed 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptPreviewViewModel.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptPreviewViewModel.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import Observation import FoundationModels import FoundationCore import InfrastructureStorage @@ -7,14 +8,15 @@ import InfrastructureStorage /// Enhanced view model for receipt preview and editing with validation /// Swift 5.9 - No Swift 6 features @MainActor -public final class ReceiptPreviewViewModel: ObservableObject { - @Published public var parsedData: ParsedReceiptData - @Published public var isLoading = false - @Published public var errorMessage: String? - @Published public var validationErrors: [ValidationError] = [] - @Published public var editedItems: [ParsedReceiptItem] = [] - @Published public var showingItemEditor = false - @Published public var selectedItemIndex: Int? +@Observable +public final class ReceiptPreviewViewModel { + public var parsedData: ParsedReceiptData + public var isLoading = false + public var errorMessage: String? + public var validationErrors: [ValidationError] = [] + public var editedItems: [ParsedReceiptItem] = [] + public var showingItemEditor = false + public var selectedItemIndex: Int? private let receiptRepository: any FoundationModels.ReceiptRepositoryProtocol private let itemRepository: (any ItemRepository)? diff --git a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptsListViewModel.swift b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptsListViewModel.swift index 4fac2f8a..d6028f5b 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptsListViewModel.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/ViewModels/ReceiptsListViewModel.swift @@ -23,6 +23,7 @@ import Foundation import SwiftUI +import Observation import FoundationCore import FoundationModels import ServicesExternal @@ -33,12 +34,13 @@ import Combine /// View model for receipts list /// Swift 5.9 - No Swift 6 features @MainActor -public final class ReceiptsListViewModel: ObservableObject { - @Published public var receipts: [Receipt] = [] - @Published public var isLoading = false - @Published public var errorMessage: String? - @Published public var searchText = "" - @Published public var selectedFilter: ReceiptFilter = .all +@Observable +public final class ReceiptsListViewModel { + public var receipts: [Receipt] = [] + public var isLoading = false + public var errorMessage: String? + public var searchText = "" + public var selectedFilter: ReceiptFilter = .all private let receiptRepository: any FoundationModels.ReceiptRepositoryProtocol private let itemRepository: (any ItemRepository)? diff --git a/Features-Receipts/Sources/FeaturesReceipts/Views/EmailReceiptImportView.swift b/Features-Receipts/Sources/FeaturesReceipts/Views/EmailReceiptImportView.swift index ec10f9dc..54ba6b99 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/Views/EmailReceiptImportView.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/Views/EmailReceiptImportView.swift @@ -22,6 +22,7 @@ // import SwiftUI +import Observation import FoundationModels import ServicesExternal import UIComponents @@ -34,7 +35,7 @@ public struct EmailReceiptImportView: View { let ocrService: any OCRServiceProtocol let receiptRepository: any ReceiptRepositoryProtocol - @StateObject private var viewModel: EmailImportViewModel + @State private var viewModel: EmailImportViewModel @Environment(\.dismiss) private var dismiss public init( @@ -47,7 +48,7 @@ public struct EmailReceiptImportView: View { self.emailService = emailService self.ocrService = ocrService self.receiptRepository = receiptRepository - self._viewModel = StateObject(wrappedValue: EmailImportViewModel( + self._viewModel = State(wrappedValue: EmailImportViewModel( emailService: emailService, ocrService: ocrService, receiptRepository: receiptRepository, @@ -315,14 +316,15 @@ struct EmailRowView: View { /// View model for email import functionality @MainActor -public final class EmailImportViewModel: ObservableObject { - @Published public var isLoading = false - @Published public var isConnected = false - @Published public var loadingMessage = "Loading..." - @Published public var errorMessage: String? - @Published public var receiptEmails: [ReceiptEmail] = [] - @Published public var selectedEmails: Set = [] - @Published public var importProgress: Double = 0 +@Observable +public final class EmailImportViewModel { + public var isLoading = false + public var isConnected = false + public var loadingMessage = "Loading..." + public var errorMessage: String? + public var receiptEmails: [ReceiptEmail] = [] + public var selectedEmails: Set = [] + public var importProgress: Double = 0 private let emailService: any EmailServiceProtocol private let ocrService: any OCRServiceProtocol diff --git a/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptDetailView.swift b/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptDetailView.swift index 46550028..e2ab2ecc 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptDetailView.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptDetailView.swift @@ -30,11 +30,11 @@ import InfrastructureStorage /// Swift 5.9 - No Swift 6 features public struct ReceiptDetailView: View { let receipt: Receipt - @StateObject private var viewModel: ReceiptDetailViewModel + @State private var viewModel: ReceiptDetailViewModel public init(receipt: Receipt, viewModel: ReceiptDetailViewModel) { self.receipt = receipt - self._viewModel = StateObject(wrappedValue: viewModel) + self._viewModel = State(wrappedValue: viewModel) } public var body: some View { diff --git a/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptImportView.swift b/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptImportView.swift index a63b1e67..bad4b3f2 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptImportView.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptImportView.swift @@ -30,12 +30,12 @@ import UIComponents /// Swift 5.9 - No Swift 6 features public struct ReceiptImportView: View { let completion: (Receipt) -> Void - @StateObject private var viewModel: ReceiptImportViewModel + @State private var viewModel: ReceiptImportViewModel @Environment(\.dismiss) private var dismiss public init(completion: @escaping (Receipt) -> Void, viewModel: ReceiptImportViewModel) { self.completion = completion - self._viewModel = StateObject(wrappedValue: viewModel) + self._viewModel = State(wrappedValue: viewModel) } public var body: some View { diff --git a/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptsListView.swift b/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptsListView.swift index 66e0df87..d377a1ab 100644 --- a/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptsListView.swift +++ b/Features-Receipts/Sources/FeaturesReceipts/Views/ReceiptsListView.swift @@ -7,11 +7,11 @@ import InfrastructureStorage /// Modern receipts list view using new architecture /// Swift 5.9 - No Swift 6 features public struct ReceiptsListView: View { - @StateObject private var viewModel: ReceiptsListViewModel + @State private var viewModel: ReceiptsListViewModel @State private var showingAddReceipt = false public init(viewModel: ReceiptsListViewModel) { - self._viewModel = StateObject(wrappedValue: viewModel) + self._viewModel = State(wrappedValue: viewModel) } public var body: some View { diff --git a/Features-Scanner/Sources/FeaturesScanner/Coordinators/ScannerCoordinator.swift b/Features-Scanner/Sources/FeaturesScanner/Coordinators/ScannerCoordinator.swift index b7ed95cd..6ee86fd4 100644 --- a/Features-Scanner/Sources/FeaturesScanner/Coordinators/ScannerCoordinator.swift +++ b/Features-Scanner/Sources/FeaturesScanner/Coordinators/ScannerCoordinator.swift @@ -21,17 +21,19 @@ // import SwiftUI +import Observation import FoundationModels import FoundationCore import UINavigation /// Coordinator for managing scanner module navigation @MainActor -public final class ScannerCoordinator: ObservableObject { - @Published public var navigationPath = NavigationPath() - @Published public var selectedScannedItem: InventoryItem? - @Published public var showingBatchResults = false - @Published public var showingDocumentView = false +@Observable +public final class ScannerCoordinator { + public var navigationPath = NavigationPath() + public var selectedScannedItem: InventoryItem? + public var showingBatchResults = false + public var showingDocumentView = false private let dependencies: ScannerModuleDependencies diff --git a/Features-Scanner/Sources/FeaturesScanner/Services/OfflineScanService.swift b/Features-Scanner/Sources/FeaturesScanner/Services/OfflineScanService.swift index dc8c19b1..5b642d8b 100644 --- a/Features-Scanner/Sources/FeaturesScanner/Services/OfflineScanService.swift +++ b/Features-Scanner/Sources/FeaturesScanner/Services/OfflineScanService.swift @@ -24,6 +24,7 @@ // import Foundation +import Observation import ServicesExternal import FoundationModels import InfrastructureStorage @@ -33,9 +34,10 @@ import Combine /// Service for managing offline scan queue /// Swift 5.9 - No Swift 6 features @MainActor -public final class OfflineScanService: ObservableObject { - @Published public private(set) var pendingScans: [OfflineScanEntry] = [] - @Published public private(set) var isProcessing: Bool = false +@Observable +public final class OfflineScanService { + public private(set) var pendingScans: [OfflineScanEntry] = [] + public private(set) var isProcessing: Bool = false private let offlineScanQueueRepository: any OfflineScanQueueRepository private let barcodeLookupService: any BarcodeLookupService diff --git a/Features-Scanner/Sources/FeaturesScanner/ViewModels/ScannerTabViewModel.swift b/Features-Scanner/Sources/FeaturesScanner/ViewModels/ScannerTabViewModel.swift index 2f907acf..3c5d44b6 100644 --- a/Features-Scanner/Sources/FeaturesScanner/ViewModels/ScannerTabViewModel.swift +++ b/Features-Scanner/Sources/FeaturesScanner/ViewModels/ScannerTabViewModel.swift @@ -21,6 +21,7 @@ // import SwiftUI +import Observation import AVFoundation import Vision import FoundationModels @@ -29,14 +30,15 @@ import ServicesExternal /// ViewModel for the scanner tab view @MainActor -public final class ScannerTabViewModel: ObservableObject { - @Published public var selectedScanningMode: ScanningMode = .single - @Published public var isScanning = false - @Published public var hasOfflineItems = false - @Published public var showingCameraPermissionAlert = false - @Published public var showingError = false - @Published public var errorMessage = "" - @Published public var lastScanResult: ScanResult? +@Observable +public final class ScannerTabViewModel { + public var selectedScanningMode: ScanningMode = .single + public var isScanning = false + public var hasOfflineItems = false + public var showingCameraPermissionAlert = false + public var showingError = false + public var errorMessage = "" + public var lastScanResult: ScanResult? private let dependencies: FeaturesScanner.Scanner.ScannerModuleDependencies private let coordinator: ScannerCoordinator diff --git a/Features-Scanner/Sources/FeaturesScanner/Views/BarcodeScannerView.swift b/Features-Scanner/Sources/FeaturesScanner/Views/BarcodeScannerView.swift index 3f04bd40..59ca8197 100644 --- a/Features-Scanner/Sources/FeaturesScanner/Views/BarcodeScannerView.swift +++ b/Features-Scanner/Sources/FeaturesScanner/Views/BarcodeScannerView.swift @@ -52,6 +52,7 @@ import SwiftUI import UIKit import AVFoundation +import Observation import FoundationCore import FoundationModels import UIComponents @@ -62,7 +63,7 @@ import InfrastructureStorage /// Barcode scanner view /// Swift 5.9 - No Swift 6 features public struct BarcodeScannerView: View { - @StateObject private var viewModel: BarcodeScannerViewModel + @State private var viewModel: BarcodeScannerViewModel @Environment(\.dismiss) private var dismiss @State private var showingAlert = false @State private var alertMessage = "" @@ -209,11 +210,12 @@ public struct CameraPreview: UIViewRepresentable { // MARK: - View Model @MainActor -public final class BarcodeScannerViewModel: NSObject, ObservableObject { - @Published public var isScanning = false - @Published public var lastScannedCode: String? - @Published public var isFlashOn = false - @Published public var showingPermissionAlert = false +@Observable +public final class BarcodeScannerViewModel: NSObject { + public var isScanning = false + public var lastScannedCode: String? + public var isFlashOn = false + public var showingPermissionAlert = false public let captureSession = AVCaptureSession() private let metadataOutput = AVCaptureMetadataOutput() diff --git a/Features-Scanner/Sources/FeaturesScanner/Views/BatchScannerView.swift b/Features-Scanner/Sources/FeaturesScanner/Views/BatchScannerView.swift index 82b3ebb0..7739b454 100644 --- a/Features-Scanner/Sources/FeaturesScanner/Views/BatchScannerView.swift +++ b/Features-Scanner/Sources/FeaturesScanner/Views/BatchScannerView.swift @@ -23,6 +23,7 @@ // import SwiftUI +import Observation import AVFoundation import UIComponents import UIStyles @@ -31,7 +32,7 @@ import FoundationCore /// Batch scanner view for scanning multiple items consecutively public struct BatchScannerView: View { - @StateObject private var viewModel: BatchScannerViewModel + @State private var viewModel: BatchScannerViewModel @Environment(\.dismiss) private var dismiss @State private var showingAddItemView = false @State private var currentBarcode: String? @@ -311,20 +312,21 @@ private struct AddItemView: View { // MARK: - View Model @MainActor -final class BatchScannerViewModel: NSObject, ObservableObject { - // MARK: - Published Properties - @Published var isScanning = false - @Published var isFlashOn = false - @Published var showingPermissionAlert = false - @Published var showingCompleteAlert = false - @Published var scannedItems: [ScannedItem] = [] - @Published var recentScans: [String] = [] - @Published var isContinuousMode = false { +@Observable +final class BatchScannerViewModel: NSObject { + // MARK: - Properties + var isScanning = false + var isFlashOn = false + var showingPermissionAlert = false + var showingCompleteAlert = false + var scannedItems: [ScannedItem] = [] + var recentScans: [String] = [] + var isContinuousMode = false { didSet { scanMode = isContinuousMode ? .continuous : .manual } } - @Published var scanMode: ScanMode = .manual + var scanMode: ScanMode = .manual // MARK: - Types enum ScanMode { diff --git a/Features-Scanner/Sources/FeaturesScanner/Views/DocumentScannerView.swift b/Features-Scanner/Sources/FeaturesScanner/Views/DocumentScannerView.swift index c1d93e7e..12264a45 100644 --- a/Features-Scanner/Sources/FeaturesScanner/Views/DocumentScannerView.swift +++ b/Features-Scanner/Sources/FeaturesScanner/Views/DocumentScannerView.swift @@ -24,6 +24,7 @@ import SwiftUI import UIKit import VisionKit +import Observation import FoundationModels import FoundationCore import UIComponents @@ -31,7 +32,7 @@ import UIStyles /// Document scanner view for receipts and documents public struct DocumentScannerView: View { - @StateObject private var viewModel: DocumentScannerViewModel + @State private var viewModel: DocumentScannerViewModel @Environment(\.dismiss) private var dismiss public init(dependencies: FeaturesScanner.Scanner.ScannerModuleDependencies, completion: @escaping (UIImage) -> Void) { @@ -288,10 +289,11 @@ private struct DocumentCameraView: UIViewControllerRepresentable { // MARK: - View Model @MainActor -final class DocumentScannerViewModel: ObservableObject { - @Published var showingDocumentCamera = false - @Published var isProcessing = false - @Published var scannedImages: [UIImage] = [] +@Observable +final class DocumentScannerViewModel { + var showingDocumentCamera = false + var isProcessing = false + var scannedImages: [UIImage] = [] private let dependencies: FeaturesScanner.Scanner.ScannerModuleDependencies private let completion: (UIImage) -> Void diff --git a/Features-Settings/Package.resolved b/Features-Settings/Package.resolved index 9b695ff7..8434c820 100644 --- a/Features-Settings/Package.resolved +++ b/Features-Settings/Package.resolved @@ -1,5 +1,41 @@ { "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", diff --git a/Features-Settings/Sources/FeaturesSettings/Utils/SettingsStorageWrapper.swift b/Features-Settings/Sources/FeaturesSettings/Utils/SettingsStorageWrapper.swift index c13b516c..3a4a9994 100644 --- a/Features-Settings/Sources/FeaturesSettings/Utils/SettingsStorageWrapper.swift +++ b/Features-Settings/Sources/FeaturesSettings/Utils/SettingsStorageWrapper.swift @@ -100,14 +100,19 @@ // import SwiftUI +import Observation import FoundationCore // import FeaturesScanner // Removed to fix circular dependency +// Type alias for clarity +public typealias SettingsStorageProtocol = SettingsStorage + /// Observable wrapper for SettingsStorage to work with SwiftUI @MainActor -public class SettingsStorageWrapper: ObservableObject { +@Observable +public class SettingsStorageWrapper { let storage: any SettingsStorage - @Published private var updateTrigger = false + private var updateTrigger = false public init(storage: any SettingsStorage) { self.storage = storage diff --git a/Features-Settings/Sources/FeaturesSettings/ViewModels/MonitoringDashboardViewModel.swift b/Features-Settings/Sources/FeaturesSettings/ViewModels/MonitoringDashboardViewModel.swift index 4d44f704..3965ab1e 100644 --- a/Features-Settings/Sources/FeaturesSettings/ViewModels/MonitoringDashboardViewModel.swift +++ b/Features-Settings/Sources/FeaturesSettings/ViewModels/MonitoringDashboardViewModel.swift @@ -4,24 +4,27 @@ import SwiftUI import FoundationCore import Combine import InfrastructureMonitoring +import Observation /// View model for the monitoring dashboard -final class MonitoringDashboardViewModel: ObservableObject { - // MARK: - Published Properties +@MainActor +@Observable +final class MonitoringDashboardViewModel { + // MARK: - Observable Properties - @Published var isMonitoringActive = false - @Published var appLaunchCount = 0 - @Published var crashFreeRate = 100.0 - @Published var averageSessionDuration: TimeInterval = 0 - @Published var activeDays = 0 + var isMonitoringActive = false + var appLaunchCount = 0 + var crashFreeRate = 100.0 + var averageSessionDuration: TimeInterval = 0 + var activeDays = 0 - @Published var recentEvents: [RecentEvent] = [] - @Published var launchTimeData: [DataPoint] = [] - @Published var performanceMetrics: [PerformanceMetricData] = [] - @Published var featureUsageData: [FeatureUsageItem] = [] - @Published var businessMetrics = BusinessMetricsData() - @Published var privacySettings = PrivacySettingsData() - @Published var localStorageSize = "0 MB" + var recentEvents: [RecentEvent] = [] + var launchTimeData: [DataPoint] = [] + var performanceMetrics: [PerformanceMetricData] = [] + var featureUsageData: [FeatureUsageItem] = [] + var businessMetrics = BusinessMetricsData() + var privacySettings = PrivacySettingsData() + var localStorageSize = "0 MB" // Placeholder for monitoring functionality - stub implementation private let monitoringManager = SimpleMonitoringManager() @@ -109,17 +112,9 @@ final class MonitoringDashboardViewModel: ObservableObject { // MARK: - Private Methods private func setupBindings() { - // Monitor changes to monitoring active state - $isMonitoringActive - .dropFirst() - .sink { [weak self] isActive in - if isActive { - self?.monitoringManager.initialize(with: .granted) - } else { - self?.monitoringManager.optOut() - } - } - .store(in: &cancellables) + // With @Observable, state changes are automatically tracked + // The view will react to changes in isMonitoringActive property + // No manual binding setup needed } private func loadMonitoringStatus() { diff --git a/Features-Settings/Sources/FeaturesSettings/ViewModels/SettingsViewModel.swift b/Features-Settings/Sources/FeaturesSettings/ViewModels/SettingsViewModel.swift index edc73ca4..a30a2bab 100644 --- a/Features-Settings/Sources/FeaturesSettings/ViewModels/SettingsViewModel.swift +++ b/Features-Settings/Sources/FeaturesSettings/ViewModels/SettingsViewModel.swift @@ -50,6 +50,8 @@ import Foundation import Combine +import SwiftUI +import Observation import FoundationCore import FoundationModels import InfrastructureStorage @@ -58,10 +60,11 @@ import InfrastructureStorage /// View model for managing settings state /// Swift 5.9 - No Swift 6 features @MainActor -public final class SettingsViewModel: ObservableObject { - @Published public var settings: AppSettings - @Published public var hasConflicts = false - @Published public var conflictCount = 0 +@Observable +public final class SettingsViewModel { + public var settings: AppSettings + public var hasConflicts = false + public var conflictCount = 0 public let settingsStorage: SettingsStorage private var cancellables = Set() diff --git a/Features-Settings/Sources/FeaturesSettings/Views/AccessibilitySettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/AccessibilitySettingsView.swift index 5314525f..ff1ceabd 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/AccessibilitySettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/AccessibilitySettingsView.swift @@ -78,10 +78,10 @@ enum TextSizePreference: String, CaseIterable, Codable { } struct AccessibilitySettingsView: View { - @StateObject private var settingsWrapper: SettingsStorageWrapper + @State private var settingsWrapper: SettingsStorageWrapper init(settingsStorage: any SettingsStorage) { - self._settingsWrapper = StateObject(wrappedValue: SettingsStorageWrapper(storage: settingsStorage)) + self._settingsWrapper = State(wrappedValue: SettingsStorageWrapper(storage: settingsStorage)) } @State private var selectedTextSize: TextSizePreference = .medium @State private var showPreview = false diff --git a/Features-Settings/Sources/FeaturesSettings/Views/AccountSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/AccountSettingsView.swift index 0a6bc8c5..1224e81c 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/AccountSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/AccountSettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import ServicesAuthentication import UIComponents @@ -13,7 +14,7 @@ public struct AccountSettingsView: View { // MARK: - Properties - @StateObject private var viewModel = AccountSettingsViewModel() + @State private var viewModel = AccountSettingsViewModel() @Environment(\.theme) private var theme @Environment(\.dismiss) private var dismiss @@ -375,7 +376,7 @@ private struct SettingsActionRow: View { // MARK: - Profile Editor Sheet private struct ProfileEditorSheet: View { - @ObservedObject var viewModel: AccountSettingsViewModel + var viewModel: AccountSettingsViewModel @Environment(\.dismiss) private var dismiss @Environment(\.theme) private var theme @@ -432,20 +433,21 @@ private struct ProfileEditorSheet: View { // MARK: - Account Settings View Model @MainActor -private final class AccountSettingsViewModel: ObservableObject { - - // MARK: - Published Properties - - @Published var displayName: String = "" - @Published var email: String = "" - @Published var isVerified: Bool = false - @Published var memberSince: String = "" - @Published var profileImageData: Data? - @Published var biometricEnabled: Bool = false - @Published var twoFactorEnabled: Bool = false - @Published var isLoading: Bool = false - @Published var alertItem: AlertItem? - @Published var showingProfileEditor: Bool = false +@Observable +private final class AccountSettingsViewModel { + + // MARK: - Properties + + var displayName: String = "" + var email: String = "" + var isVerified: Bool = false + var memberSince: String = "" + var profileImageData: Data? + var biometricEnabled: Bool = false + var twoFactorEnabled: Bool = false + var isLoading: Bool = false + var alertItem: AlertItem? + var showingProfileEditor: Bool = false // MARK: - Private Properties diff --git a/Features-Settings/Sources/FeaturesSettings/Views/AppearanceSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/AppearanceSettingsView.swift index f69c378b..2ed44b18 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/AppearanceSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/AppearanceSettingsView.swift @@ -1,5 +1,6 @@ import FoundationModels import SwiftUI +import Observation import UIComponents import UINavigation import UIStyles @@ -12,8 +13,9 @@ public struct AppearanceSettingsView: View { // MARK: - Properties - @StateObject private var viewModel = AppearanceSettingsViewModel() + @State private var viewModel = AppearanceSettingsViewModel() @Environment(\.theme) private var theme + @Environment(\.themeManager) private var themeManager // MARK: - Body @@ -47,12 +49,12 @@ public struct AppearanceSettingsView: View { private var themeSection: some View { SettingsSection(title: "Theme") { VStack(spacing: theme.spacing.small) { - ForEach(ThemeMode.allCases) { themeMode in + ForEach(UIStyles.ThemeMode.allCases) { themeMode in ThemeModeRow( themeMode: themeMode, - isSelected: viewModel.selectedTheme == themeMode + isSelected: themeManager.currentThemeMode == themeMode ) { - viewModel.selectTheme(themeMode) + themeManager.setThemeMode(themeMode) } } } @@ -108,7 +110,7 @@ public struct AppearanceSettingsView: View { // MARK: - Theme Mode Row private struct ThemeModeRow: View { - let themeMode: ThemeMode + let themeMode: UIStyles.ThemeMode let isSelected: Bool let onSelect: () -> Void @Environment(\.theme) private var theme @@ -207,67 +209,28 @@ private struct ColorOption: View { // MARK: - View Model @MainActor -private final class AppearanceSettingsViewModel: ObservableObject { +@Observable +private final class AppearanceSettingsViewModel { - @Published var selectedTheme: ThemeMode = .system - @Published var accentColor: AccentColor = .blue - @Published var showItemImages: Bool = true - @Published var largeText: Bool = false - @Published var reduceMotion: Bool = false + var accentColor: AccentColor = .blue + var showItemImages: Bool = true + var largeText: Bool = false + var reduceMotion: Bool = false func loadSettings() { // Load from UserDefaults - selectedTheme = ThemeMode(rawValue: UserDefaults.standard.string(forKey: "ThemeMode") ?? "") ?? .system accentColor = AccentColor(rawValue: UserDefaults.standard.string(forKey: "AccentColor") ?? "") ?? .blue showItemImages = UserDefaults.standard.bool(forKey: "ShowItemImages") largeText = UserDefaults.standard.bool(forKey: "LargeText") reduceMotion = UserDefaults.standard.bool(forKey: "ReduceMotion") } - func selectTheme(_ theme: ThemeMode) { - selectedTheme = theme - UserDefaults.standard.set(theme.rawValue, forKey: "ThemeMode") - } - func selectAccentColor(_ color: AccentColor) { accentColor = color UserDefaults.standard.set(color.rawValue, forKey: "AccentColor") } } -// MARK: - Theme Mode - -private enum ThemeMode: String, CaseIterable, Identifiable { - case light - case dark - case system - - var id: String { rawValue } - - var displayName: String { - switch self { - case .light: return "Light" - case .dark: return "Dark" - case .system: return "System" - } - } - - var description: String { - switch self { - case .light: return "Always use light mode" - case .dark: return "Always use dark mode" - case .system: return "Follow system settings" - } - } - - var iconName: String { - switch self { - case .light: return "sun.max.fill" - case .dark: return "moon.fill" - case .system: return "gear" - } - } -} // MARK: - Accent Color @@ -303,5 +266,5 @@ private enum AccentColor: String, CaseIterable, Identifiable { #Preview { AppearanceSettingsView() - .themed() + .themedWithManager() } \ No newline at end of file diff --git a/Features-Settings/Sources/FeaturesSettings/Views/BarcodeFormatSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/BarcodeFormatSettingsView.swift index 9a980c30..f50a2055 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/BarcodeFormatSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/BarcodeFormatSettingsView.swift @@ -172,7 +172,7 @@ struct BarcodeFormat: Identifiable, Hashable { /// View for managing enabled barcode formats /// Swift 5.9 - No Swift 6 features struct BarcodeFormatSettingsView: View { - @ObservedObject var viewModel: SettingsViewModel + var viewModel: SettingsViewModel @State private var searchText = "" @State private var selectedGroup: BarcodeFormat.FormatGroup? diff --git a/Features-Settings/Sources/FeaturesSettings/Views/BiometricSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/BiometricSettingsView.swift index 6df89f7c..11616c0f 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/BiometricSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/BiometricSettingsView.swift @@ -100,6 +100,7 @@ // import SwiftUI +import Observation import FoundationCore import UIComponents import UIStyles @@ -108,7 +109,7 @@ import UICore /// View for managing biometric authentication settings /// Swift 5.9 - No Swift 6 features struct BiometricSettingsView: View { - @StateObject private var biometricService = SimpleBiometricAuthService.shared + @State private var biometricService = SimpleBiometricAuthService.shared @AppStorage("biometric_enabled") private var biometricEnabled = false @AppStorage("biometric_app_lock") private var appLockEnabled = false @AppStorage("biometric_sensitive_data") private var protectSensitiveData = true @@ -383,7 +384,8 @@ struct BiometricSettingsView: View { // MARK: - Stub Services /// Stub implementation of biometric authentication service for build compatibility -private class SimpleBiometricAuthService: ObservableObject { +@Observable +private class SimpleBiometricAuthService { static let shared = SimpleBiometricAuthService() var isAvailable: Bool = false diff --git a/Features-Settings/Sources/FeaturesSettings/Views/CategoryManagementView.swift b/Features-Settings/Sources/FeaturesSettings/Views/CategoryManagementView.swift index b540428b..2a29854e 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/CategoryManagementView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/CategoryManagementView.swift @@ -50,6 +50,7 @@ // import SwiftUI +import Observation import FoundationModels import InfrastructureStorage import UIComponents @@ -59,7 +60,7 @@ import UICore /// View for managing custom categories with subcategory support /// Swift 5.9 - No Swift 6 features public struct CategoryManagementView: View { - @StateObject private var viewModel: CategoryManagementViewModel + @State private var viewModel: CategoryManagementViewModel @State private var showingAddCategory = false @State private var selectedCategory: ItemCategoryModel? @State private var showingDeleteAlert = false @@ -68,7 +69,7 @@ public struct CategoryManagementView: View { @State private var expandedCategories: Set = [] public init(categoryRepository: any CategoryRepository) { - _viewModel = StateObject(wrappedValue: CategoryManagementViewModel(categoryRepository: categoryRepository)) + _viewModel = State(wrappedValue: CategoryManagementViewModel(categoryRepository: categoryRepository)) } public var body: some View { @@ -337,12 +338,13 @@ private struct SubcategoryRowView: View { // MARK: - View Model @MainActor -final class CategoryManagementViewModel: ObservableObject { - @Published var builtInCategories: [ItemCategoryModel] = [] - @Published var customCategories: [ItemCategoryModel] = [] - @Published var subcategories: [UUID: [ItemCategoryModel]] = [:] - @Published var isLoading = false - @Published var errorMessage: String? +@Observable +final class CategoryManagementViewModel { + var builtInCategories: [ItemCategoryModel] = [] + var customCategories: [ItemCategoryModel] = [] + var subcategories: [UUID: [ItemCategoryModel]] = [:] + var isLoading = false + var errorMessage: String? let categoryRepository: any CategoryRepository diff --git a/Features-Settings/Sources/FeaturesSettings/Views/CrashReportingSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/CrashReportingSettingsView.swift index 8c7bb3e2..fe355ae0 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/CrashReportingSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/CrashReportingSettingsView.swift @@ -50,6 +50,7 @@ // import SwiftUI +import Observation import FoundationCore import UIComponents import UIStyles @@ -57,12 +58,12 @@ import UICore /// Settings view for crash reporting configuration struct CrashReportingSettingsView: View { - @StateObject private var settingsWrapper: SettingsStorageWrapper + @State private var settingsWrapper: SettingsStorageWrapper init(settingsStorage: any SettingsStorageProtocol) { - self._settingsWrapper = StateObject(wrappedValue: SettingsStorageWrapper(storage: settingsStorage)) + self._settingsWrapper = State(wrappedValue: SettingsStorageWrapper(storage: settingsStorage)) } - @StateObject private var crashService = SimpleCrashReportingService.shared + @State private var crashService = SimpleCrashReportingService.shared @State private var showingReportDetails = false @State private var showingPrivacyInfo = false @State private var isSendingReports = false @@ -693,7 +694,8 @@ extension SettingsKey { // MARK: - Stub Services /// Stub implementation of crash reporting service for build compatibility -private class SimpleCrashReportingService: ObservableObject { +@Observable +private class SimpleCrashReportingService { static let shared = SimpleCrashReportingService() var isEnabled: Bool = false diff --git a/Features-Settings/Sources/FeaturesSettings/Views/EnhancedSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/EnhancedSettingsView.swift index f51b48b9..99a00b3d 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/EnhancedSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/EnhancedSettingsView.swift @@ -58,7 +58,7 @@ import ServicesSync /// Simplified enhanced settings view with sophisticated UI/UX public struct EnhancedSettingsView: View { - @StateObject private var viewModel: SettingsViewModel + @State private var viewModel: SettingsViewModel @State private var searchText = "" @State private var showingSheet = false @State private var sheetContent: SheetContent? = nil @@ -70,7 +70,7 @@ public struct EnhancedSettingsView: View { public init(viewModel: SettingsViewModel) { print("EnhancedSettingsView.init called") print("Stack trace: \(Thread.callStackSymbols)") - self._viewModel = StateObject(wrappedValue: viewModel) + self._viewModel = State(wrappedValue: viewModel) } public var body: some View { @@ -331,7 +331,7 @@ public struct EnhancedSettingsView: View { struct SettingsListView: View { let searchText: String - @ObservedObject var viewModel: SettingsViewModel + var viewModel: SettingsViewModel let onItemTap: (SettingsItemData) -> Void @State private var expandedSections: Set = [] @@ -490,7 +490,7 @@ enum SettingsItemType { struct SettingsSectionCard: View { let section: SettingsSectionData let isExpanded: Bool - @ObservedObject var viewModel: SettingsViewModel + var viewModel: SettingsViewModel let onTap: () -> Void let onItemTap: (SettingsItemData) -> Void @@ -551,7 +551,7 @@ struct SettingsSectionCard: View { struct SettingsItemRow: View { let item: SettingsItemData - @ObservedObject var viewModel: SettingsViewModel + var viewModel: SettingsViewModel let onTap: () -> Void var body: some View { diff --git a/Features-Settings/Sources/FeaturesSettings/Views/MonitoringDashboardView.swift b/Features-Settings/Sources/FeaturesSettings/Views/MonitoringDashboardView.swift index 5298c48f..b2f859b0 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/MonitoringDashboardView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/MonitoringDashboardView.swift @@ -1,12 +1,13 @@ import SwiftUI +import Observation import FoundationCore import Charts /// Dashboard view for monitoring app health, crashes, and performance @available(iOS 17.0, *) public struct MonitoringDashboardView: View { - @StateObject private var crashService = SimpleCrashReportingService() - @StateObject private var monitoringManager = SimpleMonitoringManager() + @State private var crashService = SimpleCrashReportingService() + @State private var monitoringManager = SimpleMonitoringManager() @State private var selectedTab = 0 @State private var crashStats: [String: Int] = [:] @State private var performanceMetrics: PerformanceMetrics? @@ -384,11 +385,12 @@ struct PerformanceChart: View { // MARK: - Stub Services /// Stub implementation of crash reporting service for build compatibility -private class SimpleCrashReportingService: ObservableObject { - @Published var isEnabled: Bool = false - @Published var crashFreeRate: Double = 0.995 - @Published var pendingReportsCount: Int = 0 - @Published var privacyMode: CrashReportingPrivacyMode = .standard +@Observable +private class SimpleCrashReportingService { + var isEnabled: Bool = false + var crashFreeRate: Double = 0.995 + var pendingReportsCount: Int = 0 + var privacyMode: CrashReportingPrivacyMode = .standard var crashReports: [MockCrashReport] = [] @@ -414,7 +416,8 @@ private class SimpleCrashReportingService: ObservableObject { } /// Stub implementation of monitoring manager for build compatibility -private class SimpleMonitoringManager: ObservableObject { +@Observable +private class SimpleMonitoringManager { var isActive: Bool = false // Remove shared instance to avoid conflicts with @StateObject usage diff --git a/Features-Settings/Sources/FeaturesSettings/Views/MonitoringPrivacySettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/MonitoringPrivacySettingsView.swift index 74b86e18..5b75219d 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/MonitoringPrivacySettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/MonitoringPrivacySettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationCore import UIComponents import UIStyles @@ -8,7 +9,7 @@ import InfrastructureMonitoring /// Privacy settings view for monitoring configuration struct MonitoringPrivacySettingsView: View { @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel = MonitoringPrivacySettingsViewModel() + @State private var viewModel = MonitoringPrivacySettingsViewModel() @State private var showingConsentDialog = false @State private var showingDataDeletionConfirmation = false @@ -237,27 +238,28 @@ struct MonitoringPrivacySettingsView: View { // MARK: - View Model -final class MonitoringPrivacySettingsViewModel: ObservableObject { - @Published var currentConsent: MonitoringConfiguration.UserConsent = .notAsked - @Published var consentLastUpdated: Date? +@Observable +final class MonitoringPrivacySettingsViewModel { + var currentConsent: MonitoringConfiguration.UserConsent = .notAsked + var consentLastUpdated: Date? // Data collection toggles - @Published var performanceMetricsEnabled = true - @Published var featureUsageEnabled = true - @Published var errorTrackingEnabled = true - @Published var userFlowsEnabled = false - @Published var businessMetricsEnabled = true + var performanceMetricsEnabled = true + var featureUsageEnabled = true + var errorTrackingEnabled = true + var userFlowsEnabled = false + var businessMetricsEnabled = true // External services - @Published var metricKitEnabled = true - @Published var telemetryDeckEnabled = false - @Published var sentryEnabled = false + var metricKitEnabled = true + var telemetryDeckEnabled = false + var sentryEnabled = false // Privacy settings - @Published var dataRetentionDays = 90 - @Published var autoDeleteEnabled = true - @Published var anonymizeDataEnabled = true - @Published var requireExplicitConsent = true + var dataRetentionDays = 90 + var autoDeleteEnabled = true + var anonymizeDataEnabled = true + var requireExplicitConsent = true private var configuration: MonitoringConfiguration diff --git a/Features-Settings/Sources/FeaturesSettings/Views/MonitoringSettingsView+Example.swift b/Features-Settings/Sources/FeaturesSettings/Views/MonitoringSettingsView+Example.swift new file mode 100644 index 00000000..f03d43d0 --- /dev/null +++ b/Features-Settings/Sources/FeaturesSettings/Views/MonitoringSettingsView+Example.swift @@ -0,0 +1,27 @@ +import SwiftUI +import Observation + +// Example implementation showing how to properly handle state changes with @Observable + +struct MonitoringSettingsExampleView: View { + @State private var viewModel = MonitoringDashboardViewModel() + + var body: some View { + VStack { + Toggle("Enable Monitoring", isOn: $viewModel.isMonitoringActive) + .padding() + } + .onChange(of: viewModel.isMonitoringActive) { oldValue, newValue in + // Handle monitoring state changes + if newValue { + // Enable monitoring + Task { + await viewModel.monitoringManager.initialize(with: .granted) + } + } else { + // Disable monitoring + viewModel.monitoringManager.optOut() + } + } + } +} \ No newline at end of file diff --git a/Features-Settings/Sources/FeaturesSettings/Views/NotificationSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/NotificationSettingsView.swift index c29982db..3de1d270 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/NotificationSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/NotificationSettingsView.swift @@ -50,13 +50,14 @@ // import SwiftUI +import Observation import FoundationCore import UserNotifications /// View for managing notification settings /// Swift 5.9 - No Swift 6 features struct NotificationSettingsView: View { - @StateObject private var notificationManager = NotificationManager.shared + @State private var notificationManager = NotificationManager.shared @State private var showingPermissionAlert = false @State private var isLoadingPermission = false @@ -306,12 +307,13 @@ struct QuietHoursRow: View { /// Mock implementation of NotificationManager for build compatibility @MainActor -class NotificationManager: ObservableObject { +@Observable +class NotificationManager { static let shared = NotificationManager() - @Published var authorizationStatus: UNAuthorizationStatus = .notDetermined - @Published var isEnabled = false - @Published var notificationSettings = NotificationSettings() + var authorizationStatus: UNAuthorizationStatus = .notDetermined + var isEnabled = false + var notificationSettings = NotificationSettings() var isAuthorized: Bool { return authorizationStatus == .authorized @@ -437,7 +439,6 @@ class NotificationManager: ObservableObject { func setNotificationType(_ type: NotificationType, enabled: Bool) { // Mock implementation - objectWillChange.send() } func scheduleNotification(for type: NotificationType, at date: Date, message: String) { @@ -447,12 +448,13 @@ class NotificationManager: ObservableObject { // MARK: - Notification Settings -class NotificationSettings: ObservableObject { - @Published var soundEnabled = true - @Published var badgeEnabled = true - @Published var quietHoursEnabled = false - @Published var quietHoursStart = DateComponents(hour: 22, minute: 0) - @Published var quietHoursEnd = DateComponents(hour: 7, minute: 0) +@Observable +class NotificationSettings { + var soundEnabled = true + var badgeEnabled = true + var quietHoursEnabled = false + var quietHoursStart = DateComponents(hour: 22, minute: 0) + var quietHoursEnd = DateComponents(hour: 7, minute: 0) private var enabledTypes: Set = Set(NotificationManager.NotificationType.allCases) @@ -466,7 +468,6 @@ class NotificationSettings: ObservableObject { } else { enabledTypes.insert(type) } - objectWillChange.send() } } diff --git a/Features-Settings/Sources/FeaturesSettings/Views/ScannerSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/ScannerSettingsView.swift index 6ad2875e..ab01be92 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/ScannerSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/ScannerSettingsView.swift @@ -59,7 +59,7 @@ import UICore /// Swift 5.9 - No Swift 6 features struct ScannerSettingsView: View { @Binding var settings: AppSettings - @ObservedObject var viewModel: SettingsViewModel + var viewModel: SettingsViewModel @Environment(\.dismiss) private var dismiss var body: some View { diff --git a/Features-Settings/Sources/FeaturesSettings/Views/SettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/SettingsView.swift index 3b531843..bdfae8ec 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/SettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/SettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Observation import FoundationModels import UIComponents import UINavigation @@ -12,10 +13,10 @@ public struct SettingsView: View { // MARK: - Properties - @StateObject private var settingsViewModel = SettingsViewModel( + @State private var settingsViewModel = SettingsViewModel( settingsStorage: FoundationCore.UserDefaultsSettingsStorage() ) - @StateObject private var viewModel = SettingsViewAdapter() + @State private var viewModel = SettingsViewAdapter() @EnvironmentObject private var router: Router @Environment(\.theme) private var theme @@ -302,11 +303,12 @@ private struct SettingsRow: View { // MARK: - Settings View Adapter @MainActor -private final class SettingsViewAdapter: ObservableObject { - @Published var iCloudSyncEnabled: Bool = false - @Published var notificationsEnabled: Bool = true - @Published var biometricAuthEnabled: Bool = false - @Published var alertItem: AlertItem? +@Observable +private final class SettingsViewAdapter { + var iCloudSyncEnabled: Bool = false + var notificationsEnabled: Bool = true + var biometricAuthEnabled: Bool = false + var alertItem: AlertItem? var appVersion: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" diff --git a/Features-Settings/Sources/FeaturesSettings/Views/SpotlightSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/SpotlightSettingsView.swift index abda977a..31b003fc 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/SpotlightSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/SpotlightSettingsView.swift @@ -100,6 +100,7 @@ // import SwiftUI +import Observation import FoundationCore import UIComponents import UIStyles @@ -110,14 +111,15 @@ import UICore /// Mock spotlight integration manager for compilation /// Swift 5.9 - No Swift 6 features @MainActor -class MockSpotlightIntegrationManager: ObservableObject { +@Observable +class MockSpotlightIntegrationManager { static let shared = MockSpotlightIntegrationManager() - @Published var isEnabled = true - @Published var indexedItemsCount = 0 - @Published var lastIndexTime: Date? = Date() - @Published var indexingStatus = "Ready" - @Published var isIndexing = false + var isEnabled = true + var indexedItemsCount = 0 + var lastIndexTime: Date? = Date() + var indexingStatus = "Ready" + var isIndexing = false init() {} @@ -156,7 +158,7 @@ class MockSpotlightIntegrationManager: ObservableObject { /// Settings view for configuring Spotlight search integration /// Swift 5.9 - No Swift 6 features struct SpotlightSettingsView: View { - @StateObject private var spotlightManager = MockSpotlightIntegrationManager() + @State private var spotlightManager = MockSpotlightIntegrationManager() @State private var showingReindexConfirmation = false @State private var showingClearConfirmation = false @State private var isReindexing = false diff --git a/Features-Settings/Sources/FeaturesSettings/Views/VoiceOverSettingsView.swift b/Features-Settings/Sources/FeaturesSettings/Views/VoiceOverSettingsView.swift index a1a629f7..4b969c04 100644 --- a/Features-Settings/Sources/FeaturesSettings/Views/VoiceOverSettingsView.swift +++ b/Features-Settings/Sources/FeaturesSettings/Views/VoiceOverSettingsView.swift @@ -107,10 +107,10 @@ import UICore /// Settings view for VoiceOver preferences struct VoiceOverSettingsView: View { - @StateObject private var settingsWrapper: SettingsStorageWrapper + @State private var settingsWrapper: SettingsStorageWrapper init(settingsStorage: any SettingsStorageProtocol) { - self._settingsWrapper = StateObject(wrappedValue: SettingsStorageWrapper(storage: settingsStorage)) + self._settingsWrapper = State(wrappedValue: SettingsStorageWrapper(storage: settingsStorage)) } @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @State private var showingGuide = false diff --git a/Features-Sync/Sources/FeaturesSync/Views/ConflictResolutionView.swift b/Features-Sync/Sources/FeaturesSync/Views/ConflictResolutionView.swift index 82b6a77a..07a5dbd7 100644 --- a/Features-Sync/Sources/FeaturesSync/Views/ConflictResolutionView.swift +++ b/Features-Sync/Sources/FeaturesSync/Views/ConflictResolutionView.swift @@ -10,7 +10,7 @@ import InfrastructureStorage /// View for resolving sync conflicts with multiple resolution strategies extension Features.Sync { public struct ConflictResolutionView: View { - @StateObject private var conflictService: ConflictResolutionService + @State private var conflictService: ConflictResolutionService @State private var selectedConflict: SyncConflict? @State private var showingBatchResolution = false @@ -20,7 +20,7 @@ extension Features.Sync { conflictService: ConflictResolutionService, onResolutionComplete: @escaping @MainActor () async -> Void = {} ) { - self._conflictService = StateObject(wrappedValue: conflictService) + self._conflictService = State(wrappedValue: conflictService) self.onResolutionComplete = onResolutionComplete } @@ -185,7 +185,7 @@ extension Features.Sync { extension Features.Sync { struct ConflictDetailView: View { let conflict: SyncConflict - @StateObject private var conflictService: ConflictResolutionService + @State private var conflictService: ConflictResolutionService @State private var selectedResolution: ConflictResolution = .keepLocal @State private var isResolving = false @State private var conflictDetails: ConflictDetails? @@ -198,7 +198,7 @@ extension Features.Sync { onResolved: @escaping @MainActor () async -> Void ) { self.conflict = conflict - self._conflictService = StateObject(wrappedValue: conflictService) + self._conflictService = State(wrappedValue: conflictService) self.onResolved = onResolved } diff --git a/Features-Sync/Sources/FeaturesSync/Views/SyncSettingsView.swift b/Features-Sync/Sources/FeaturesSync/Views/SyncSettingsView.swift index d1b9654a..fbdf2b88 100644 --- a/Features-Sync/Sources/FeaturesSync/Views/SyncSettingsView.swift +++ b/Features-Sync/Sources/FeaturesSync/Views/SyncSettingsView.swift @@ -8,12 +8,12 @@ import UIStyles /// Settings view for configuring sync behavior and preferences extension Features.Sync { public struct SyncSettingsView: View { - @StateObject private var syncService: SyncService + @State private var syncService: SyncService @State private var configuration: SyncConfiguration @State private var showingStorageInfo = false public init(syncService: SyncService) { - self._syncService = StateObject(wrappedValue: syncService) + self._syncService = State(wrappedValue: syncService) self._configuration = State(initialValue: syncService.syncConfiguration) } diff --git a/Features-Sync/Sources/FeaturesSync/Views/SyncStatusView.swift b/Features-Sync/Sources/FeaturesSync/Views/SyncStatusView.swift index 80df1231..a1890b35 100644 --- a/Features-Sync/Sources/FeaturesSync/Views/SyncStatusView.swift +++ b/Features-Sync/Sources/FeaturesSync/Views/SyncStatusView.swift @@ -8,12 +8,12 @@ import UIStyles /// Main sync status view showing current sync state and controls extension Features.Sync { public struct SyncStatusView: View { - @StateObject private var syncService: SyncService + @State private var syncService: SyncService @State private var showingSettings = false @State private var showingConflictResolution = false public init(syncService: SyncService) { - self._syncService = StateObject(wrappedValue: syncService) + self._syncService = State(wrappedValue: syncService) } public var body: some View { diff --git a/HomeInventoryModular.xcodeproj/project.pbxproj b/HomeInventoryModular.xcodeproj/project.pbxproj index ecc93878..f2216110 100644 --- a/HomeInventoryModular.xcodeproj/project.pbxproj +++ b/HomeInventoryModular.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ C05A79BD8C659560BD30C8F9 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 98F3DC077160EA8EE81BCF13 /* GoogleSignIn */; }; C9632A254D1200C6F958E23C /* ServicesSearch in Frameworks */ = {isa = PBXBuildFile; productRef = 920BDBE9B320DB81016BEC7B /* ServicesSearch */; }; DF2D9BB96AB650F40C19DF06 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 74A8362BCB458EAED3AFE268 /* Assets.xcassets */; }; - E5833933A3D1B5D3F195C387 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F887BCCEDBBA976C8B557D3 /* ContentView.swift */; }; EE22292C5B094FC6B25F52F2 /* HomeInventoryApp in Frameworks */ = {isa = PBXBuildFile; productRef = B4FA974C0C49AF5A4F894C70 /* HomeInventoryApp */; }; F110E061FDBC925483D96631 /* FoundationModels in Frameworks */ = {isa = PBXBuildFile; productRef = 6E6636B9EA8C4584AC65198E /* FoundationModels */; }; F8A2732FDDE9E4A0B3DA3F8A /* FeaturesSettings in Frameworks */ = {isa = PBXBuildFile; productRef = 3672CAC154D000D45723E135 /* FeaturesSettings */; }; @@ -81,7 +80,6 @@ 67B7BECE5F108404825BB188 /* Infrastructure-Storage */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "Infrastructure-Storage"; path = "Infrastructure-Storage"; sourceTree = SOURCE_ROOT; }; 6A4B8AF3261DA4F51C3EF2EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 6A837B2E402B473AD1043664 /* Infrastructure-Monitoring */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "Infrastructure-Monitoring"; path = "Infrastructure-Monitoring"; sourceTree = SOURCE_ROOT; }; - 6F887BCCEDBBA976C8B557D3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 6FA9E85F9D0016AF30814111 /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; 74A8362BCB458EAED3AFE268 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7B27D7EB582782C9CB1091E0 /* Foundation-Core */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "Foundation-Core"; path = "Foundation-Core"; sourceTree = SOURCE_ROOT; }; @@ -148,7 +146,6 @@ children = ( D845322EEA5B77A6F6B55FE5 /* App.swift */, 74A8362BCB458EAED3AFE268 /* Assets.xcassets */, - 6F887BCCEDBBA976C8B557D3 /* ContentView.swift */, 6A4B8AF3261DA4F51C3EF2EB /* Info.plist */, ); path = "Supporting Files"; @@ -408,7 +405,6 @@ buildActionMask = 2147483647; files = ( 27CC7F1F10AA5764E8E61A57 /* App.swift in Sources */, - E5833933A3D1B5D3F195C387 /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/HomeInventoryModular.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HomeInventoryModular.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 8434c820..00000000 --- a/HomeInventoryModular.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,77 +0,0 @@ -{ - "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", - "version" : "1.7.6" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS.git", - "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - }, - { - "identity" : "swift-custom-dump", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", - "state" : { - "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", - "version" : "1.3.3" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", - "state" : { - "revision" : "b198a568ad24c5a22995c5ff0ecf9667634e860e", - "version" : "1.18.5" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", - "state" : { - "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", - "version" : "601.0.1" - } - }, - { - "identity" : "xctest-dynamic-overlay", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state" : { - "revision" : "23e3442166b5122f73f9e3e622cd1e4bafeab3b7", - "version" : "1.6.0" - } - } - ], - "version" : 2 -} diff --git a/Services-Authentication/Sources/ServicesAuthentication/AuthenticationService.swift b/Services-Authentication/Sources/ServicesAuthentication/AuthenticationService.swift index b88a5f6e..6d50d084 100644 --- a/Services-Authentication/Sources/ServicesAuthentication/AuthenticationService.swift +++ b/Services-Authentication/Sources/ServicesAuthentication/AuthenticationService.swift @@ -1,17 +1,19 @@ import Foundation +import Observation import FoundationCore import FoundationModels // MARK: - Authentication Service @MainActor -public final class AuthenticationService: ObservableObject { +@Observable +public final class AuthenticationService { - // MARK: - Properties + // MARK: - Observable Properties - @Published public private(set) var currentUser: User? - @Published public private(set) var authenticationState: AuthenticationState = .unauthenticated - @Published public private(set) var isLoading = false + public private(set) var currentUser: User? + public private(set) var authenticationState: AuthenticationState = .unauthenticated + public private(set) var isLoading = false // For now, using simplified interfaces until the infrastructure layer is fully defined // TODO: Replace with actual provider protocols from infrastructure modules diff --git a/Services-Export/Sources/ServicesExport/ExportService.swift b/Services-Export/Sources/ServicesExport/ExportService.swift index b74299b7..d39bcdc4 100644 --- a/Services-Export/Sources/ServicesExport/ExportService.swift +++ b/Services-Export/Sources/ServicesExport/ExportService.swift @@ -1,17 +1,19 @@ import Foundation +import Observation import FoundationCore import FoundationModels // MARK: - Unified Export Service @MainActor -public final class ExportService: ObservableObject { +@Observable +public final class ExportService { - // MARK: - Published Properties + // MARK: - Observable Properties - @Published public private(set) var exportJobs: [ExportJob] = [] - @Published public private(set) var isExporting = false - @Published public private(set) var exportHistory: [ExportHistoryEntry] = [] + public private(set) var exportJobs: [ExportJob] = [] + public private(set) var isExporting = false + public private(set) var exportHistory: [ExportHistoryEntry] = [] // MARK: - Private Properties diff --git a/Services-Search/Sources/ServicesSearch/SearchService.swift b/Services-Search/Sources/ServicesSearch/SearchService.swift index de5417bc..1c2fc03b 100644 --- a/Services-Search/Sources/ServicesSearch/SearchService.swift +++ b/Services-Search/Sources/ServicesSearch/SearchService.swift @@ -1,4 +1,5 @@ import Foundation +import Observation import FoundationCore import FoundationModels import InfrastructureStorage @@ -14,15 +15,16 @@ public typealias LocationRepository = any InfrastructureStorage.LocationReposito // MARK: - Search Service @MainActor -public final class SearchService: ObservableObject { +@Observable +public final class SearchService { // MARK: - Properties - @Published public private(set) var searchResults: [SearchResult] = [] - @Published public private(set) var isSearching = false - @Published public private(set) var lastSearchQuery: String? - @Published public private(set) var searchHistory: [String] = [] - @Published public private(set) var suggestionResults: [SearchSuggestion] = [] + public private(set) var searchResults: [SearchResult] = [] + public private(set) var isSearching = false + public private(set) var lastSearchQuery: String? + public private(set) var searchHistory: [String] = [] + public private(set) var suggestionResults: [SearchSuggestion] = [] // Private properties private let maxHistoryItems = 50 diff --git a/Services-Sync/Sources/ServicesSync/SyncService.swift b/Services-Sync/Sources/ServicesSync/SyncService.swift index 5d194df6..a3a0f096 100644 --- a/Services-Sync/Sources/ServicesSync/SyncService.swift +++ b/Services-Sync/Sources/ServicesSync/SyncService.swift @@ -1,4 +1,5 @@ import Foundation +import Observation import FoundationCore import FoundationModels import CloudKit @@ -6,15 +7,16 @@ import CloudKit // MARK: - Sync Service @MainActor -public final class SyncService: ObservableObject { +@Observable +public final class SyncService { // MARK: - Properties - @Published public private(set) var syncState: SyncState = .idle - @Published public private(set) var lastSyncDate: Date? - @Published public private(set) var syncProgress: Double = 0.0 - @Published public private(set) var pendingChangesCount: Int = 0 - @Published public private(set) var isOnline = true + public private(set) var syncState: SyncState = .idle + public private(set) var lastSyncDate: Date? + public private(set) var syncProgress: Double = 0.0 + public private(set) var pendingChangesCount: Int = 0 + public private(set) var isOnline = true private let container: CKContainer? private let database: CKDatabase? diff --git a/Source/App/ContentView.swift b/Source/App/ContentView.swift index 10e8403d..f9462efc 100644 --- a/Source/App/ContentView.swift +++ b/Source/App/ContentView.swift @@ -15,7 +15,7 @@ struct ContentView: View { // MARK: - Properties - @StateObject private var appCoordinator = AppCoordinator() + @State private var appCoordinator = AppCoordinator() @Environment(\.theme) private var theme // MARK: - Body diff --git a/Source/App/ModernAppCoordinator.swift b/Source/App/ModernAppCoordinator.swift index 7212073d..e2796153 100644 --- a/Source/App/ModernAppCoordinator.swift +++ b/Source/App/ModernAppCoordinator.swift @@ -1,5 +1,6 @@ import SwiftUI import Foundation +import Observation import FoundationModels import FoundationCore import ServicesAuthentication @@ -16,15 +17,16 @@ import InfrastructureStorage /// Modern app coordinator for the new modular architecture @MainActor -final class AppCoordinator: ObservableObject { +@Observable +final class AppCoordinator { - // MARK: - Published Properties + // MARK: - Observable Properties - @Published var isInitialized = false - @Published var showOnboarding = false - @Published var selectedTab = 0 - @Published var isLoading = false - @Published var error: AppError? + var isInitialized = false + var showOnboarding = false + var selectedTab = 0 + var isLoading = false + var error: AppError? // MARK: - Core Services diff --git a/Source/ViewModels/AnalyticsViewModel.swift b/Source/ViewModels/AnalyticsViewModel.swift index 5283b89b..2a5f67ea 100644 --- a/Source/ViewModels/AnalyticsViewModel.swift +++ b/Source/ViewModels/AnalyticsViewModel.swift @@ -1,12 +1,14 @@ import Foundation import SwiftUI +import Observation @MainActor -class AnalyticsViewModel: ObservableObject { - @Published var totalItems = 0 - @Published var totalValue = 0.0 - @Published var categoryCounts: [String: Int] = [:] - @Published var monthlySpending: [Double] = [] +@Observable +class AnalyticsViewModel { + var totalItems = 0 + var totalValue = 0.0 + var categoryCounts: [String: Int] = [:] + var monthlySpending: [Double] = [] init() { generateSampleAnalytics() diff --git a/Source/ViewModels/ItemsViewModel.swift b/Source/ViewModels/ItemsViewModel.swift index e3caff1a..17fb5b4d 100644 --- a/Source/ViewModels/ItemsViewModel.swift +++ b/Source/ViewModels/ItemsViewModel.swift @@ -1,12 +1,14 @@ import Foundation import SwiftUI import FoundationModels +import Observation @MainActor -class ItemsViewModel: ObservableObject { - @Published var items: [InventoryItem] = [] - @Published var isLoading = false - @Published var searchText = "" +@Observable +class ItemsViewModel { + var items: [InventoryItem] = [] + var isLoading = false + var searchText = "" private let repository: InventoryRepository diff --git a/Source/ViewModels/SettingsViewModel.swift b/Source/ViewModels/SettingsViewModel.swift index eb7e5615..3b0f20da 100644 --- a/Source/ViewModels/SettingsViewModel.swift +++ b/Source/ViewModels/SettingsViewModel.swift @@ -1,12 +1,14 @@ import Foundation import SwiftUI +import Observation @MainActor -class SettingsViewModel: ObservableObject { - @Published var notificationsEnabled = true - @Published var darkModeEnabled = false - @Published var autoBackupEnabled = true - @Published var currency = "USD" +@Observable +class SettingsViewModel { + var notificationsEnabled = true + var darkModeEnabled = false + var autoBackupEnabled = true + var currency = "USD" // User preferences @AppStorage("notifications") private var storedNotifications = true diff --git a/Source/Views/ContentView.swift b/Source/Views/ContentView.swift index 5740bd9d..b3bb2ba7 100644 --- a/Source/Views/ContentView.swift +++ b/Source/Views/ContentView.swift @@ -4,7 +4,7 @@ import SharedUI import CoreUI struct ContentView: View { - @StateObject private var coordinator = AppCoordinator() + @State private var coordinator = AppCoordinator() @State private var selectedTab = 0 var body: some View { diff --git a/Supporting Files/App.swift b/Supporting Files/App.swift index 444d7983..f4252152 100644 --- a/Supporting Files/App.swift +++ b/Supporting Files/App.swift @@ -1,11 +1,11 @@ import SwiftUI -// import AppMain // Temporarily disabled - using direct ContentView +import HomeInventoryApp @main struct HomeInventoryModularApp: App { var body: some Scene { WindowGroup { - ContentView() + AppMain.createMainView() } } } \ No newline at end of file diff --git a/Supporting Files/ContentView.swift b/Supporting Files/ContentView.swift deleted file mode 100644 index 10e8403d..00000000 --- a/Supporting Files/ContentView.swift +++ /dev/null @@ -1,151 +0,0 @@ -import SwiftUI -import UIComponents -import UINavigation -import UIStyles -import FeaturesInventory -import FeaturesLocations -import FeaturesAnalytics -import FeaturesSettings - -// MARK: - Content View - -/// Main app content view with tab-based navigation -@MainActor -struct ContentView: View { - - // MARK: - Properties - - @StateObject private var appCoordinator = AppCoordinator() - @Environment(\.theme) private var theme - - // MARK: - Body - - var body: some View { - if appCoordinator.showOnboarding { - OnboardingFlow() - .environmentObject(appCoordinator) - } else { - MainTabView() - .environmentObject(appCoordinator) - } - } -} - -// MARK: - Main Tab View - -private struct MainTabView: View { - @EnvironmentObject private var appCoordinator: AppCoordinator - @Environment(\.theme) private var theme - @State private var selectedTab = 0 - - var body: some View { - TabView(selection: $selectedTab) { - // Inventory Tab - NavigationStack { - InventoryListView() - } - .tabItem { - Image(systemName: "archivebox.fill") - Text("Inventory") - } - .tag(0) - - // Locations Tab - NavigationStack { - LocationsListView() - } - .tabItem { - Image(systemName: "location.fill") - Text("Locations") - } - .tag(1) - - // Analytics Tab - NavigationStack { - AnalyticsDashboardView() - } - .tabItem { - Image(systemName: "chart.bar.fill") - Text("Analytics") - } - .tag(2) - - // Settings Tab - NavigationStack { - SettingsView() - } - .tabItem { - Image(systemName: "gear.circle.fill") - Text("Settings") - } - .tag(3) - } - .tint(theme.colors.primary) - .onChange(of: selectedTab) { newValue in - appCoordinator.selectedTab = newValue - } - } -} - -// MARK: - Onboarding Flow - -private struct OnboardingFlow: View { - @EnvironmentObject private var appCoordinator: AppCoordinator - @Environment(\.theme) private var theme - - var body: some View { - VStack(spacing: theme.spacing.large) { - Spacer() - - // App Icon - Image(systemName: "archivebox.circle.fill") - .font(.system(size: 80)) - .foregroundColor(theme.colors.primary) - - // Welcome Text - VStack(spacing: theme.spacing.medium) { - Text("Welcome to Home Inventory") - .font(theme.typography.largeTitle) - .fontWeight(.bold) - .foregroundColor(theme.colors.label) - .multilineTextAlignment(.center) - - Text("Organize and track your belongings with ease. Get started by adding your first item or location.") - .font(theme.typography.body) - .foregroundColor(theme.colors.secondaryLabel) - .multilineTextAlignment(.center) - .padding(.horizontal, theme.spacing.large) - } - - Spacer() - - // Get Started Button - Button("Get Started") { - appCoordinator.completeOnboarding() - } - .font(theme.typography.headline) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, theme.spacing.medium) - .background(theme.colors.primary) - .cornerRadius(theme.radius.medium) - .padding(.horizontal, theme.spacing.large) - - // Skip Button - Button("Skip") { - appCoordinator.completeOnboarding() - } - .font(theme.typography.body) - .foregroundColor(theme.colors.secondaryLabel) - .padding(.bottom, theme.spacing.large) - } - .background(theme.colors.background) - } -} - -// MARK: - Preview - -#Preview { - ContentView() - .themed() -} \ No newline at end of file diff --git a/UI-Core/Sources/UICore/ViewModels/BaseViewModel.swift b/UI-Core/Sources/UICore/ViewModels/BaseViewModel.swift index a825050f..6d82de25 100644 --- a/UI-Core/Sources/UICore/ViewModels/BaseViewModel.swift +++ b/UI-Core/Sources/UICore/ViewModels/BaseViewModel.swift @@ -4,12 +4,13 @@ import Combine import SwiftUI import FoundationCore import InfrastructureNetwork +import Observation // MARK: - Base View Model Protocol /// Protocol defining the basic requirements for a view model @MainActor -public protocol ViewModelProtocol: ObservableObject { +public protocol ViewModelProtocol { associatedtype State associatedtype Action @@ -21,13 +22,14 @@ public protocol ViewModelProtocol: ObservableObject { /// Base view model providing common functionality for all view models @MainActor -public class BaseViewModel: ObservableObject { +@Observable +public class BaseViewModel { - // MARK: - Published Properties + // MARK: - Observable Properties - @Published public private(set) var isLoading = false - @Published public private(set) var error: ErrorState? - @Published public private(set) var alerts: [AlertItem] = [] + public private(set) var isLoading = false + public private(set) var error: ErrorState? + public private(set) var alerts: [AlertItem] = [] // MARK: - Private Properties diff --git a/UI-Navigation/Sources/UINavigation/Routing/Router.swift b/UI-Navigation/Sources/UINavigation/Routing/Router.swift index 9afe9ab8..e3e6a35c 100644 --- a/UI-Navigation/Sources/UINavigation/Routing/Router.swift +++ b/UI-Navigation/Sources/UINavigation/Routing/Router.swift @@ -1,20 +1,22 @@ import SwiftUI import Foundation +import Observation import FoundationModels // MARK: - Router /// A centralized routing system for navigation management @MainActor -public final class Router: ObservableObject, @unchecked Sendable { +@Observable +public final class Router: @unchecked Sendable { // MARK: - Properties - @Published public var navigationPath = NavigationPath() - @Published public var selectedTab = 0 - @Published public var presentedSheet: AnyView? - @Published public var presentedFullScreen: AnyView? - @Published public var presentedAlert: AlertItem? + public var navigationPath = NavigationPath() + public var selectedTab = 0 + public var presentedSheet: AnyView? + public var presentedFullScreen: AnyView? + public var presentedAlert: AlertItem? // MARK: - Initialization @@ -184,12 +186,25 @@ public enum AlertActionStyle { case destructive } +// MARK: - Environment Key + +private struct RouterKey: EnvironmentKey { + static let defaultValue = Router() +} + +public extension EnvironmentValues { + var router: Router { + get { self[RouterKey.self] } + set { self[RouterKey.self] = newValue } + } +} + // MARK: - RouterView /// A view wrapper that provides routing capabilities to its content public struct RouterView: View { - @StateObject private var router = Router() + @State private var router = Router() private let content: () -> Content public init(@ViewBuilder content: @escaping () -> Content) { @@ -198,7 +213,7 @@ public struct RouterView: View { public var body: some View { content() - .environmentObject(router) + .environment(\.router, router) .sheet(item: Binding( get: { router.presentedAlert }, set: { _ in router.dismissAlert() } @@ -225,15 +240,3 @@ public extension View { } } -// MARK: - Environment Key - -private struct RouterEnvironmentKey: EnvironmentKey { - static let defaultValue: Router? = nil -} - -public extension EnvironmentValues { - var router: Router? { - get { self[RouterEnvironmentKey.self] } - set { self[RouterEnvironmentKey.self] = newValue } - } -} \ No newline at end of file diff --git a/UI-Styles/Sources/UIStyles/ThemeManager.swift b/UI-Styles/Sources/UIStyles/ThemeManager.swift new file mode 100644 index 00000000..0a337c02 --- /dev/null +++ b/UI-Styles/Sources/UIStyles/ThemeManager.swift @@ -0,0 +1,132 @@ +import SwiftUI +import Observation +import Foundation + +// MARK: - Theme Mode + +/// Represents the available theme modes for the application +public enum ThemeMode: String, CaseIterable, Identifiable, Sendable { + case light + case dark + case system + + public var id: String { rawValue } + + public var displayName: String { + switch self { + case .light: return "Light" + case .dark: return "Dark" + case .system: return "System" + } + } + + public var description: String { + switch self { + case .light: return "Always use light mode" + case .dark: return "Always use dark mode" + case .system: return "Follow system settings" + } + } + + public var iconName: String { + switch self { + case .light: return "sun.max.fill" + case .dark: return "moon.fill" + case .system: return "gear" + } + } + + /// Convert ThemeMode to SwiftUI ColorScheme + public var colorScheme: ColorScheme? { + switch self { + case .light: return .light + case .dark: return .dark + case .system: return nil // Let system decide + } + } +} + +// MARK: - Theme Manager + +/// Manages application theme state and provides centralized theme switching functionality +@MainActor +@Observable +public final class ThemeManager: Sendable { + + // MARK: - Properties + + /// Current selected theme mode + public private(set) var currentThemeMode: ThemeMode = .system + + /// Current computed color scheme based on theme mode and system settings + public private(set) var currentColorScheme: ColorScheme? + + // MARK: - UserDefaults Keys + + private enum UserDefaultsKeys { + static let themeMode = "ThemeMode" + } + + // MARK: - Initialization + + public init() { + loadThemeFromStorage() + updateColorScheme() + } + + // MARK: - Public Methods + + /// Updates the current theme mode and persists the selection + /// - Parameter themeMode: The new theme mode to apply + public func setThemeMode(_ themeMode: ThemeMode) { + currentThemeMode = themeMode + saveThemeToStorage() + updateColorScheme() + } + + /// Updates the color scheme based on current theme mode and system settings + public func updateColorScheme() { + currentColorScheme = currentThemeMode.colorScheme + } + + // MARK: - Private Methods + + private func loadThemeFromStorage() { + let storedValue = UserDefaults.standard.string(forKey: UserDefaultsKeys.themeMode) ?? "" + currentThemeMode = ThemeMode(rawValue: storedValue) ?? .system + } + + private func saveThemeToStorage() { + UserDefaults.standard.set(currentThemeMode.rawValue, forKey: UserDefaultsKeys.themeMode) + } + + // MARK: - Singleton + + /// Shared theme manager instance + public static let shared = ThemeManager() +} + +// MARK: - Environment Integration + +/// Environment key for theme manager +private struct ThemeManagerEnvironmentKey: EnvironmentKey { + static let defaultValue = ThemeManager.shared +} + +public extension EnvironmentValues { + var themeManager: ThemeManager { + get { self[ThemeManagerEnvironmentKey.self] } + set { self[ThemeManagerEnvironmentKey.self] = newValue } + } +} + +// MARK: - View Extensions + +public extension View { + /// Apply the theme manager's color scheme to this view + func themedWithManager() -> some View { + self + .environment(\.themeManager, ThemeManager.shared) + .preferredColorScheme(ThemeManager.shared.currentColorScheme) + } +} \ No newline at end of file diff --git a/scripts/build-parallel.sh b/scripts/build-parallel.sh new file mode 100755 index 00000000..7644ce76 --- /dev/null +++ b/scripts/build-parallel.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +# Build modules in parallel for faster compilation +# This script builds all Swift Package Manager modules concurrently + +set -e + +# Colors for output +BLUE='\033[0;34m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Configuration +CONFIG="Debug" +if [[ "$1" == "--release" ]]; then + CONFIG="Release" +fi + +echo -e "${BLUE}Building all SPM modules in parallel...${NC}" +echo -e "${BLUE}Configuration: ${CONFIG}${NC}" + +# Define all modules +MODULES=( + "Foundation-Core" + "Foundation-Models" + "Foundation-Resources" + "Infrastructure-Network" + "Infrastructure-Storage" + "Infrastructure-Security" + "Infrastructure-Monitoring" + "Services-Authentication" + "Services-Business" + "Services-External" + "Services-Search" + "Services-Sync" + "Services-Export" + "UI-Core" + "UI-Components" + "UI-Styles" + "UI-Navigation" + "Features-Inventory" + "Features-Scanner" + "Features-Settings" + "Features-Analytics" + "Features-Locations" + "Features-Receipts" + "App-Main" +) + +# Function to build a module +build_module() { + local module=$1 + echo -e "${BLUE}Building ${module}...${NC}" + + # Navigate to module directory and build + cd "$module" 2>/dev/null || { + echo -e "${RED}Module directory not found: ${module}${NC}" + return 1 + } + + if swift build -c "$CONFIG" > "../build-${module}.log" 2>&1; then + echo -e "${GREEN}✓ ${module} built successfully${NC}" + # Mark successful build + echo "exit_code: 0" >> "../build-${module}.log" + cd .. + return 0 + else + echo -e "${RED}✗ ${module} build failed${NC}" + echo -e "${RED}See build-${module}.log for details${NC}" + # Mark failed build + echo "exit_code: 1" >> "../build-${module}.log" + cd .. + return 1 + fi +} + +# Export function and variables for parallel execution +export -f build_module +export CONFIG BLUE GREEN RED NC + +# Build modules in parallel using xargs +# -P 0 uses as many processes as possible +# -I {} replaces {} with the module name +printf "%s\n" "${MODULES[@]}" | xargs -P 0 -I {} bash -c 'build_module "$@"' _ {} + +# Check if all builds succeeded +FAILED=0 +for module in "${MODULES[@]}"; do + if [[ -f "build-${module}.log" ]]; then + # Check exit code instead of "Build complete" string + if grep -q "exit_code: 1" "build-${module}.log" 2>/dev/null; then + FAILED=1 + fi + # Clean up successful build logs + if grep -q "exit_code: 0" "build-${module}.log" 2>/dev/null; then + rm -f "build-${module}.log" + fi + fi +done + +if [[ $FAILED -eq 0 ]]; then + echo -e "${GREEN}All modules built successfully!${NC}" +else + echo -e "${RED}Some modules failed to build. Check the log files.${NC}" + exit 1 +fi \ No newline at end of file