From adc69da207acce2c6d15dbae84bb30d19b3d6c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:01:17 +0100 Subject: [PATCH] Fix Apple watch items customization not sync in the same session --- HomeAssistant.xcodeproj/project.pbxproj | 16 +- .../Watch/Home/WatchHomeCoordinatorView.swift | 51 ++++- .../Home/WatchHomeCoordinatorViewModel.swift | 165 -------------- .../Extensions/Watch/Home/WatchHomeView.swift | 130 ----------- .../Watch/Home/WatchHomeViewModel.swift | 212 ++++++++++++++++++ .../Extensions/Watch/HostingController.swift | 6 +- Sources/Shared/Environment/AppConstants.swift | 14 ++ 7 files changed, 272 insertions(+), 322 deletions(-) delete mode 100644 Sources/Extensions/Watch/Home/WatchHomeCoordinatorViewModel.swift delete mode 100644 Sources/Extensions/Watch/Home/WatchHomeView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index c69223ee9..9b7dfe18f 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -711,7 +711,7 @@ 4285C5532D35658000DADE45 /* TileCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5522D35658000DADE45 /* TileCard.swift */; }; 4285C5552D3568A100DADE45 /* WidgetAddItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4285C5542D3568A100DADE45 /* WidgetAddItemView.swift */; }; 428830EB2C6E3A8D0012373D /* WatchHomeCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EA2C6E3A8D0012373D /* WatchHomeCoordinatorView.swift */; }; - 428830ED2C6E3A9A0012373D /* WatchHomeCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EC2C6E3A9A0012373D /* WatchHomeCoordinatorViewModel.swift */; }; + 428830ED2C6E3A9A0012373D /* WatchHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */; }; 4289DDAA2C85AB4C003591C2 /* AssistAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425FF0552C8216B3000AA641 /* AssistAppIntent.swift */; }; 4289DDAB2C85AB56003591C2 /* ControlAssistValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E65F072C8079FE00C4A6F2 /* ControlAssistValueProvider.swift */; }; 4289DDAF2C85D5C4003591C2 /* ControlScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4289DDAE2C85D5C4003591C2 /* ControlScene.swift */; }; @@ -845,8 +845,6 @@ 42E95C592CA46AD50010ECE3 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C582CA46AD50010ECE3 /* ActivityView.swift */; }; 42E9AFFF2CE63944009DDA46 /* AudioOutputSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E9AFFE2CE63944009DDA46 /* AudioOutputSensor.swift */; }; 42E9B0002CE63944009DDA46 /* AudioOutputSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E9AFFE2CE63944009DDA46 /* AudioOutputSensor.swift */; }; - 42EB03062C6E42F900A184A6 /* WatchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03052C6E42F900A184A6 /* WatchHomeView.swift */; }; - 42EB03082C6E430300A184A6 /* WatchHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03072C6E430300A184A6 /* WatchHomeViewModel.swift */; }; 42EB030A2C6E4D0E00A184A6 /* WatchMagicViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EB03092C6E4D0E00A184A6 /* WatchMagicViewRow.swift */; }; 42EFFAEC2C8882DD002F10FC /* CarPlayConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EFFAEB2C8882DD002F10FC /* CarPlayConfigurationView.swift */; }; 42F158462CA15C99009C7201 /* ControlSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42F158452CA15C99009C7201 /* ControlSwitch.swift */; }; @@ -2061,7 +2059,7 @@ 4285C5522D35658000DADE45 /* TileCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileCard.swift; sourceTree = ""; }; 4285C5542D3568A100DADE45 /* WidgetAddItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetAddItemView.swift; sourceTree = ""; }; 428830EA2C6E3A8D0012373D /* WatchHomeCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeCoordinatorView.swift; sourceTree = ""; }; - 428830EC2C6E3A9A0012373D /* WatchHomeCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeCoordinatorViewModel.swift; sourceTree = ""; }; + 428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeViewModel.swift; sourceTree = ""; }; 4289DDAE2C85D5C4003591C2 /* ControlScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlScene.swift; sourceTree = ""; }; 4289DDB02C85D629003591C2 /* ControlScenesValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlScenesValueProvider.swift; sourceTree = ""; }; 4289DDB22C85D6B3003591C2 /* IntentSceneEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentSceneEntity.swift; sourceTree = ""; }; @@ -2175,8 +2173,6 @@ 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingErrorView.swift; sourceTree = ""; }; 42E95C582CA46AD50010ECE3 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; 42E9AFFE2CE63944009DDA46 /* AudioOutputSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioOutputSensor.swift; sourceTree = ""; }; - 42EB03052C6E42F900A184A6 /* WatchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeView.swift; sourceTree = ""; }; - 42EB03072C6E430300A184A6 /* WatchHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeViewModel.swift; sourceTree = ""; }; 42EB03092C6E4D0E00A184A6 /* WatchMagicViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMagicViewRow.swift; sourceTree = ""; }; 42EFFAEB2C8882DD002F10FC /* CarPlayConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayConfigurationView.swift; sourceTree = ""; }; 42F158452CA15C99009C7201 /* ControlSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlSwitch.swift; sourceTree = ""; }; @@ -4270,9 +4266,7 @@ children = ( 4207EB742C87547000286A2D /* MagicItemRow */, 428830EA2C6E3A8D0012373D /* WatchHomeCoordinatorView.swift */, - 428830EC2C6E3A9A0012373D /* WatchHomeCoordinatorViewModel.swift */, - 42EB03052C6E42F900A184A6 /* WatchHomeView.swift */, - 42EB03072C6E430300A184A6 /* WatchHomeViewModel.swift */, + 428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */, ); path = Home; sourceTree = ""; @@ -7523,9 +7517,8 @@ buildActionMask = 2147483647; files = ( 426490772C0F2403002155CC /* WatchAudioRecorder.swift in Sources */, - 42EB03062C6E42F900A184A6 /* WatchHomeView.swift in Sources */, 423F45212C19D89100766A99 /* AssistDefaultComplication.swift in Sources */, - 428830ED2C6E3A9A0012373D /* WatchHomeCoordinatorViewModel.swift in Sources */, + 428830ED2C6E3A9A0012373D /* WatchHomeViewModel.swift in Sources */, 428830EB2C6E3A8D0012373D /* WatchHomeCoordinatorView.swift in Sources */, 1178AB00263E2DF7007BA9D0 /* WKInterfaceLabel+Additions.swift in Sources */, 42EB030A2C6E4D0E00A184A6 /* WatchMagicViewRow.swift in Sources */, @@ -7543,7 +7536,6 @@ 11684B7A263F994600B48EC3 /* NotificationSubControllerMJPEG.swift in Sources */, 42B1A7432C11E65100904548 /* WatchAssistService.swift in Sources */, 423F44FF2C186E4500766A99 /* WatchCommunicatorService.swift in Sources */, - 42EB03082C6E430300A184A6 /* WatchHomeViewModel.swift in Sources */, 4207EB762C8754BF00286A2D /* WatchMagicViewRowViewModel.swift in Sources */, 427756CB2C3ED5F700E11D0B /* VolumeView.swift in Sources */, 11FA936A263FAA920015F1FC /* NotificationSubController.swift in Sources */, diff --git a/Sources/Extensions/Watch/Home/WatchHomeCoordinatorView.swift b/Sources/Extensions/Watch/Home/WatchHomeCoordinatorView.swift index 252a28416..f912ba375 100644 --- a/Sources/Extensions/Watch/Home/WatchHomeCoordinatorView.swift +++ b/Sources/Extensions/Watch/Home/WatchHomeCoordinatorView.swift @@ -1,8 +1,9 @@ import Shared import SwiftUI -struct WatchHomeCoordinatorView: View { - @StateObject private var viewModel = WatchHomeCoordinatorViewModel() +struct WatchHomeView: View { + @Environment(\.scenePhase) private var scenePhase + @StateObject private var viewModel = WatchHomeViewModel() @State private var showAssist = false init() { @@ -92,19 +93,22 @@ struct WatchHomeCoordinatorView: View { Text(L10n.Watch.Labels.noConfig) .font(.footnote) } else { - WatchHomeView( - watchConfig: $viewModel.watchConfig, - magicItemsInfo: $viewModel.magicItemsInfo, - showAssist: $showAssist - ) { - viewModel.requestConfig() - } + mainContent } if viewModel.watchConfig.items.isEmpty || viewModel.showError { reloadButton } } + .id(viewModel.refreshListID) .navigationTitle("") + .onChange(of: scenePhase) { newScenePhase in + switch newScenePhase { + case .active: + viewModel.fetchNetworkInfo(completion: nil) + default: + break + } + } } private var navReloadButton: some View { @@ -129,8 +133,31 @@ struct WatchHomeCoordinatorView: View { .listRowBackground(Color.clear) } } -} -#Preview { - WatchHomeCoordinatorView() + @ViewBuilder + private var mainContent: some View { + if #unavailable(watchOS 10), + viewModel.watchConfig.assist.showAssist, + !viewModel.watchConfig.assist.serverId.isEmpty, + !viewModel.watchConfig.assist.pipelineId.isEmpty { + assistButton + } + ForEach(viewModel.watchConfig.items, id: \.serverUniqueId) { item in + WatchMagicViewRow( + item: item, + itemInfo: info(for: item) + ) + } + reloadButton + } + + private func info(for magicItem: MagicItem) -> MagicItem.Info { + viewModel.magicItemsInfo.first(where: { + $0.id == magicItem.serverUniqueId + }) ?? .init( + id: magicItem.id, + name: magicItem.id, + iconName: "" + ) + } } diff --git a/Sources/Extensions/Watch/Home/WatchHomeCoordinatorViewModel.swift b/Sources/Extensions/Watch/Home/WatchHomeCoordinatorViewModel.swift deleted file mode 100644 index 5234e792d..000000000 --- a/Sources/Extensions/Watch/Home/WatchHomeCoordinatorViewModel.swift +++ /dev/null @@ -1,165 +0,0 @@ -import Communicator -import Foundation -import PromiseKit -import Shared - -enum WatchHomeType { - case undefined - case empty - case config(watchConfig: WatchConfig, magicItemsInfo: [MagicItem.Info]) - case error(message: String) -} - -final class WatchHomeCoordinatorViewModel: ObservableObject { - @Published var isLoading = false - @Published var showAssist = false - @Published var showError = false - @Published var errorMessage = "" - @Published private(set) var homeType: WatchHomeType = .undefined - - @Published var watchConfig: WatchConfig = .init() - @Published var magicItemsInfo: [MagicItem.Info] = [] - - private let watchConfigCacheKey = "watch-config" - private let magicItemsInfoCacheKey = "magic-items-info" - - @MainActor - func initialRoutine() { - isLoading = true - loadCache() - requestConfig() - } - - @MainActor - func requestConfig() { - homeType = .undefined - isLoading = true - guard Communicator.shared.currentReachability != .notReachable else { - Current.Log.error("iPhone reachability is not immediate reachable") - loadCache() - return - } - Communicator.shared.send(.init( - identifier: InteractiveImmediateMessages.watchConfig.rawValue, - reply: { [weak self] message in - self?.handleMessageResponse(message) - } - )) - } - - @MainActor - private func handleMessageResponse(_ message: ImmediateMessage) { - switch message.identifier { - case InteractiveImmediateResponses.emptyWatchConfigResponse.rawValue: - clearCacheAndLoad() - case InteractiveImmediateResponses.watchConfigResponse.rawValue: - setupConfig(message) - default: - Current.Log - .error("Received unmapped response id for watch config request, id: \(message.identifier)") - loadCache() - } - updateLoading(isLoading: false) - } - - @MainActor - func loadCache() { - let configPromise: Promise = Current.diskCache.value(for: watchConfigCacheKey) - configPromise.pipe { [weak self] result in - self?.handleCacheResponse(result) - } - } - - @MainActor - private func clearCacheAndLoad() { - let emptyConfig: WatchConfig? = nil - let emptyMagicItems: [MagicItem]? = nil - _ = Current.diskCache.set(emptyConfig, for: watchConfigCacheKey) - _ = Current.diskCache.set(emptyMagicItems, for: magicItemsInfoCacheKey) - loadCache() - } - - @MainActor - private func handleCacheResponse(_ result: Result) { - let magicItemsPromise: Promise<[MagicItem.Info]> = Current.diskCache.value(for: magicItemsInfoCacheKey) - - switch result { - case let .fulfilled(config): - magicItemsPromise.pipe { [weak self] result in - self?.handleMagicItemsCacheResponse(result: result, watchConfig: config) - } - case let .rejected(error): - Current.Log.error("Failed to retrieve watch config cache, error: \(error.localizedDescription)") - displayError(message: L10n.Watch.Config.Cache.Error.message) - updateConfig(config: .init(), magicItemsInfo: []) - } - - updateLoading(isLoading: false) - } - - @MainActor - private func handleMagicItemsCacheResponse(result: Result<[MagicItem.Info]>, watchConfig: WatchConfig) { - switch result { - case let .fulfilled(magicItemsInfo): - updateConfig(config: watchConfig, magicItemsInfo: magicItemsInfo) - resetError() - case let .rejected(error): - Current.Log.error("Failed to retrieve magic items cache, error: \(error.localizedDescription)") - displayError(message: L10n.Watch.Config.Error.message(error.localizedDescription)) - } - } - - @MainActor - private func setupConfig(_ message: ImmediateMessage) { - guard let configData = message.content["config"] as? Data, - let watchConfig = WatchConfig.decodeForWatch(configData) else { - Current.Log.error("Failed to get config data from watch config response") - return - } - - guard let magicItemsInfo = message.content["magicItemsInfo"] as? [Data] else { - Current.Log.error("Failed to get magicItemsInfo data array from watch config response") - return - } - let itemsInfo = magicItemsInfo.map({ MagicItem.Info.decodeForWatch($0) }) - Current.diskCache.set(watchConfig, for: watchConfigCacheKey).cauterize() - Current.diskCache.set(itemsInfo, for: magicItemsInfoCacheKey).cauterize() - - loadCache() - } - - private func updateConfig(config: WatchConfig, magicItemsInfo: [MagicItem.Info]) { - DispatchQueue.main.async { [weak self] in - self?.watchConfig = config - self?.magicItemsInfo = magicItemsInfo - - if config.assist.showAssist, - !config.assist.serverId.isEmpty, - !config.assist.pipelineId.isEmpty { - self?.showAssist = true - } else { - self?.showAssist = false - } - } - } - - private func updateLoading(isLoading: Bool) { - DispatchQueue.main.async { [weak self] in - self?.isLoading = isLoading - } - } - - private func displayError(message: String) { - DispatchQueue.main.async { [weak self] in - self?.errorMessage = message - self?.showError = true - } - } - - private func resetError() { - DispatchQueue.main.async { [weak self] in - self?.errorMessage = "" - self?.showError = false - } - } -} diff --git a/Sources/Extensions/Watch/Home/WatchHomeView.swift b/Sources/Extensions/Watch/Home/WatchHomeView.swift deleted file mode 100644 index eda29d528..000000000 --- a/Sources/Extensions/Watch/Home/WatchHomeView.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Shared -import SwiftUI - -struct WatchHomeView: View { - @Environment(\.scenePhase) private var scenePhase - @StateObject private var viewModel = WatchHomeViewModel() - @Binding var watchConfig: WatchConfig - @Binding var magicItemsInfo: [MagicItem.Info] - @Binding var showAssist: Bool - let reloadAction: () -> Void - - init( - watchConfig: Binding, - magicItemsInfo: Binding<[MagicItem.Info]>, - showAssist: Binding, - reloadAction: @escaping () -> Void - ) { - self._watchConfig = watchConfig - self._magicItemsInfo = magicItemsInfo - self._showAssist = showAssist - self.reloadAction = reloadAction - } - - var body: some View { - content - .onChange(of: scenePhase) { newScenePhase in - switch newScenePhase { - case .active: - viewModel.fetchNetworkInfo(completion: nil) - default: - break - } - } - } - - private var content: some View { - Group { - if #unavailable(watchOS 10), - watchConfig.assist.showAssist, - !watchConfig.assist.serverId.isEmpty, - !watchConfig.assist.pipelineId.isEmpty { - assistButton - } - ForEach(watchConfig.items, id: \.serverUniqueId) { item in - WatchMagicViewRow( - item: item, - itemInfo: info(for: item) - ) - } - reloadButton - } - } - - private var assistButton: some View { - Button { - showAssist = true - } label: { - HStack { - Image(uiImage: MaterialDesignIcons.messageTextOutlineIcon.image( - ofSize: .init(width: 24, height: 24), - color: Asset.Colors.haPrimary.color - )) - Text("Assist") - } - .frame(maxWidth: .infinity, alignment: .center) - } - } - - @ViewBuilder - private var reloadButton: some View { - // When watchOS 10 is available, reload is on toolbar - if #unavailable(watchOS 10.0) { - Button { - reloadAction() - } label: { - Label(L10n.reloadLabel, systemImage: "arrow.circlepath") - .frame(maxWidth: .infinity, alignment: .center) - .font(.footnote) - } - .listRowBackground(Color.clear) - } - } - - private func info(for magicItem: MagicItem) -> MagicItem.Info { - magicItemsInfo.first(where: { - $0.id == magicItem.serverUniqueId - }) ?? .init( - id: magicItem.id, - name: magicItem.id, - iconName: "" - ) - } -} - -#if DEBUG -#Preview { - MaterialDesignIcons.register() - if #available(watchOS 9.0, *) { - return NavigationStack { - WatchHomeView( - watchConfig: .constant(WatchConfig.fixture), - magicItemsInfo: .constant([ - .init(id: "1", name: "This is a script", iconName: "mdi:access-point-check"), - .init(id: "2", name: "This is an action", iconName: "fire_alert"), - ]), showAssist: .constant(false), reloadAction: {} - ) - } - } else { - return Text("Check preview watch version") - } -} - -extension WatchConfig { - static var fixture: WatchConfig = { - var config = WatchConfig() - config.assist = .init(showAssist: true) - config.items = [ - .init(id: "1", serverId: "1", type: .script), - .init( - id: "2", serverId: "1", type: .action, - customization: .init( - textColor: "#ff00ff", - backgroundColor: "#ff00ff" - ) - ), - ] - return config - }() -} -#endif diff --git a/Sources/Extensions/Watch/Home/WatchHomeViewModel.swift b/Sources/Extensions/Watch/Home/WatchHomeViewModel.swift index 2b2aced22..ff6e8f987 100644 --- a/Sources/Extensions/Watch/Home/WatchHomeViewModel.swift +++ b/Sources/Extensions/Watch/Home/WatchHomeViewModel.swift @@ -4,11 +4,223 @@ import NetworkExtension import PromiseKit import Shared +enum WatchHomeType { + case undefined + case empty + case config(watchConfig: WatchConfig, magicItemsInfo: [MagicItem.Info]) + case error(message: String) +} + final class WatchHomeViewModel: ObservableObject { + @Published var isLoading = false + @Published var showAssist = false + @Published var showError = false + @Published var errorMessage = "" + @Published private(set) var homeType: WatchHomeType = .undefined + + @Published var watchConfig: WatchConfig = .init() + @Published var magicItemsInfo: [MagicItem.Info] = [] + + // If the watchConfig items are the same but it's customization properties + // are different, the list won't refresh. This is a workaround to force a refresh + @Published var refreshListID: UUID = .init() + + private let watchConfigCacheKey = "watch-config" + private let magicItemsInfoCacheKey = "magic-items-info" + func fetchNetworkInfo(completion: (() -> Void)? = nil) { NEHotspotNetwork.fetchCurrent { hotspotNetwork in WatchUserDefaults.shared.set(hotspotNetwork?.ssid, key: .watchSSID) completion?() } } + + @MainActor + func initialRoutine() { + isLoading = true + requestConfig() + } + + @MainActor + func requestConfig() { + homeType = .undefined + isLoading = true + guard Communicator.shared.currentReachability != .notReachable else { + Current.Log.error("iPhone reachability is not immediate reachable") + loadCache() + return + } + Communicator.shared.send(.init( + identifier: InteractiveImmediateMessages.watchConfig.rawValue, + reply: { [weak self] message in + self?.handleMessageResponse(message) + } + )) + } + + @MainActor + private func handleMessageResponse(_ message: ImmediateMessage) { + switch message.identifier { + case InteractiveImmediateResponses.emptyWatchConfigResponse.rawValue: + clearCacheAndLoad() + case InteractiveImmediateResponses.watchConfigResponse.rawValue: + setupConfig(message) + default: + Current.Log + .error("Received unmapped response id for watch config request, id: \(message.identifier)") + loadCache() + } + updateLoading(isLoading: false) + } + + @MainActor + private func setupConfig(_ message: ImmediateMessage) { + guard let configData = message.content["config"] as? Data, + let watchConfig = WatchConfig.decodeForWatch(configData) else { + Current.Log.error("Failed to get config data from watch config response") + return + } + + guard let magicItemsInfo = message.content["magicItemsInfo"] as? [Data] else { + Current.Log.error("Failed to get magicItemsInfo data array from watch config response") + return + } + let itemsInfo = magicItemsInfo.map({ MagicItem.Info.decodeForWatch($0) }) + + do { + try Current.database.write { db in + try watchConfig.insert(db, onConflict: .replace) + } + saveItemsInfoInCache(itemsInfo.compactMap({ $0 })) + } catch { + Current.Log + .error( + "Failed to save watch config and/or magic item info in database on Apple watch, error: \(error.localizedDescription)" + ) + } + + loadCache() + } + + @MainActor + func loadCache() { + do { + if let watchConfig = try Current.database.read({ db in + try WatchConfig.fetchOne(db) + }) { + loadInformationCache(watchConfig: watchConfig) + } else { + updateConfig(config: .init(), magicItemsInfo: []) + } + } catch { + Current.Log.error("Failed to fetch watch config from database, error: \(error.localizedDescription)") + displayError(message: L10n.Watch.Config.Cache.Error.message) + updateConfig(config: .init(), magicItemsInfo: []) + } + } + + @MainActor + private func loadInformationCache(watchConfig: WatchConfig) { + let magicItemsInfo = getItemsInfoFromCache() + if !magicItemsInfo.isEmpty { + updateConfig(config: watchConfig, magicItemsInfo: magicItemsInfo) + resetError() + } else { + Current.Log.error("Failed to retrieve magic items cache") + displayError(message: L10n.Watch.Config.Error.message("No information cached")) + } + updateLoading(isLoading: false) + } + + @MainActor + private func clearCacheAndLoad() { + do { + _ = try Current.database.write { db in + try WatchConfig.deleteAll(db) + } + } catch { + Current.Log + .error( + "Failed to delete watch config and/or magic item info in database on Apple watch, error: \(error.localizedDescription)" + ) + } + + deleteItemsInfoInCache() + loadCache() + } + + private func saveItemsInfoInCache(_ itemsInfo: [MagicItem.Info]) { + do { + let fileURL = AppConstants.watchMagicItemsInfo + let jsonData = try JSONEncoder().encode(itemsInfo) + try jsonData.write(to: fileURL) + Current.Log + .verbose("JSON saved successfully for watch magic items info, file URL: \(fileURL.absoluteString)") + } catch { + Current.Log.error("Error saving JSON for magic items info: \(error)") + } + } + + private func deleteItemsInfoInCache() { + do { + let fileURL = AppConstants.watchMagicItemsInfo + try FileManager.default.removeItem(at: fileURL) + } catch { + Current.Log.error("Error deleting JSON for magic items info: \(error)") + } + } + + private func getItemsInfoFromCache() -> [MagicItem.Info] { + let fileURL = AppConstants.watchMagicItemsInfo + guard FileManager.default.fileExists(atPath: fileURL.path) else { + Current.Log.error("Watch magic items info cache file doesn't exist at path: \(fileURL.absoluteString)") + return [] + } + + let data = FileManager.default.contents(atPath: fileURL.path) ?? Data() + + do { + let infos = try JSONDecoder().decode([MagicItem.Info].self, from: data) + return infos + } catch { + Current.Log.error("Failed to decode watch magic item info data from cache, error: \(error)") + return [] + } + } + + private func updateConfig(config: WatchConfig, magicItemsInfo: [MagicItem.Info]) { + DispatchQueue.main.async { [weak self] in + self?.watchConfig = config + self?.magicItemsInfo = magicItemsInfo + + if config.assist.showAssist, + !config.assist.serverId.isEmpty, + !config.assist.pipelineId.isEmpty { + self?.showAssist = true + } else { + self?.showAssist = false + } + self?.refreshListID = UUID() + } + } + + private func updateLoading(isLoading: Bool) { + DispatchQueue.main.async { [weak self] in + self?.isLoading = isLoading + } + } + + private func displayError(message: String) { + DispatchQueue.main.async { [weak self] in + self?.errorMessage = message + self?.showError = true + } + } + + private func resetError() { + DispatchQueue.main.async { [weak self] in + self?.errorMessage = "" + self?.showError = false + } + } } diff --git a/Sources/Extensions/Watch/HostingController.swift b/Sources/Extensions/Watch/HostingController.swift index 96cc5a1e0..d6294d7f0 100644 --- a/Sources/Extensions/Watch/HostingController.swift +++ b/Sources/Extensions/Watch/HostingController.swift @@ -1,8 +1,8 @@ import Foundation import SwiftUI -final class HostingController: WKHostingController { - override var body: WatchHomeCoordinatorView { - WatchHomeCoordinatorView() +final class HostingController: WKHostingController { + override var body: WatchHomeView { + WatchHomeView() } } diff --git a/Sources/Shared/Environment/AppConstants.swift b/Sources/Shared/Environment/AppConstants.swift index aca861ba2..dda6c5d5f 100644 --- a/Sources/Shared/Environment/AppConstants.swift +++ b/Sources/Shared/Environment/AppConstants.swift @@ -110,6 +110,20 @@ public enum AppConstants { return eventsURL } + public static var watchMagicItemsInfo: URL { + let fileManager = FileManager.default + let directoryURL = Self.AppGroupContainer.appendingPathComponent("caches", isDirectory: true) + if !fileManager.fileExists(atPath: directoryURL.path) { + do { + try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) + } catch { + Current.Log.error("Failed to magic items info file") + } + } + let eventsURL = directoryURL.appendingPathComponent("magicItemsInfo.json") + return eventsURL + } + public static var LogsDirectory: URL { let fileManager = FileManager.default let directoryURL = AppGroupContainer.appendingPathComponent("logs", isDirectory: true)