Files
iOS/Sources/App/Settings/AppleWatch/HomeCustomization/WatchConfigurationViewModel.swift
Joel Hawksley b816f4c376 Fix watch folder bugs (#4404)
## Summary

This PR aims to fix a couple of bugs I noticed while testing the new
watch folders feature in the latest Test Flight build:

1) Changing the folder name, icon, and icon after it was created did not
work.
2) When adding a folder, the folder name should be a placeholder, not a
pre-filled field.
3) When switching between a folder and the root view, the items should
not resize after rendering (see the last item here):


https://github.com/user-attachments/assets/262cb313-f307-455f-ba21-7a189a3f7589

Note: I could not reproduce the resizing bug in the simulator, and
haven't been able to get Xcode to let me build to my devices locally.
Would you be up for trying to reproduce it/verify my fix on your end?

## Link to pull request in Documentation repository

N/A
2026-03-04 09:04:01 +01:00

222 lines
8.1 KiB
Swift

import Foundation
import GRDB
import PromiseKit
import Shared
final class WatchConfigurationViewModel: ObservableObject {
@Published var watchConfig = WatchConfig()
@Published var showAddItem = false
@Published var showError = false
@Published private(set) var errorMessage: String?
@Published var assistPipelines: [Pipeline] = []
@Published var servers: [Server] = []
private let magicItemProvider = Current.magicItemProvider()
// An item that should be added as soon as screen finishes loading
// like when using frontend "Add to" functionality from more-info dialog
private let prefilledItem: MagicItem?
init(prefilledItem: MagicItem? = nil) {
self.prefilledItem = prefilledItem
}
@MainActor
func loadWatchConfig() {
servers = Current.servers.all
magicItemProvider.loadInformation { [weak self] _ in
guard let self else { return }
loadDatabase()
}
}
func magicItemInfo(for item: MagicItem) -> MagicItem.Info? {
magicItemProvider.getInfo(for: item)
}
func addItem(_ item: MagicItem) {
watchConfig.items.append(item)
}
func addFolder(named name: String) {
let folderItem = MagicItem(
id: UUID().uuidString,
serverId: "",
type: .folder,
customization: .init(iconColor: UIColor.haPrimary.hexString()),
action: .default,
displayText: name,
items: []
)
watchConfig.items.append(folderItem)
}
func updateFolder(_ folder: MagicItem) {
guard folder.type == .folder else { return }
if let indexToUpdate = watchConfig.items.firstIndex(where: { $0.type == .folder && $0.id == folder.id }) {
var updatedFolder = folder
// Preserve existing items in the folder
updatedFolder.items = watchConfig.items[indexToUpdate].items
watchConfig.items[indexToUpdate] = updatedFolder
}
}
func updateItem(_ item: MagicItem) {
// Try root level first
if let indexToUpdate = watchConfig.items
.firstIndex(where: { $0.id == item.id && $0.serverId == item.serverId }) {
watchConfig.items.remove(at: indexToUpdate)
watchConfig.items.insert(item, at: indexToUpdate)
return
}
// Try inside folders
for (folderIndex, folder) in watchConfig.items.enumerated() where folder.type == .folder {
if let items = folder.items,
let index = items.firstIndex(where: { $0.id == item.id && $0.serverId == item.serverId }) {
var updatedFolder = folder
var updatedItems = items
updatedItems.remove(at: index)
updatedItems.insert(item, at: index)
updatedFolder.items = updatedItems
watchConfig.items[folderIndex] = updatedFolder
return
}
}
}
func addItemToFolder(folderId: String, item: MagicItem) {
if let index = watchConfig.items.firstIndex(where: { $0.type == .folder && $0.id == folderId }) {
var folder = watchConfig.items[index]
var folderItems = folder.items ?? []
folderItems.append(item)
folder.items = folderItems
watchConfig.items[index] = folder
}
}
func updateItemInFolder(folderId: String, item: MagicItem) {
guard let folderIndex = watchConfig.items
.firstIndex(where: { $0.type == .folder && $0.id == folderId }) else { return }
var folder = watchConfig.items[folderIndex]
var folderItems = folder.items ?? []
if let itemIndex = folderItems
.firstIndex(where: { $0.id == item.id && $0.serverId == item.serverId }) {
folderItems[itemIndex] = item
folder.items = folderItems
watchConfig.items[folderIndex] = folder
}
}
func deleteItemInFolder(folderId: String, at offsets: IndexSet) {
guard let index = watchConfig.items.firstIndex(where: { $0.type == .folder && $0.id == folderId }) else { return }
var folder = watchConfig.items[index]
var folderItems = folder.items ?? []
folderItems.remove(atOffsets: offsets)
folder.items = folderItems
watchConfig.items[index] = folder
}
func moveItemWithinFolder(folderId: String, from source: IndexSet, to destination: Int) {
guard let index = watchConfig.items.firstIndex(where: { $0.type == .folder && $0.id == folderId }) else { return }
var folder = watchConfig.items[index]
var folderItems = folder.items ?? []
folderItems.move(fromOffsets: source, toOffset: destination)
folder.items = folderItems
watchConfig.items[index] = folder
}
func moveItemToFolder(itemId: String, serverId: String, toFolderId: String) {
// Remove from root if present
if let rootIndex = watchConfig.items.firstIndex(where: { $0.id == itemId && $0.serverId == serverId }) {
let item = watchConfig.items.remove(at: rootIndex)
addItemToFolder(folderId: toFolderId, item: item)
return
}
// Remove from any folder if present
for (folderIndex, folder) in watchConfig.items.enumerated() where folder.type == .folder {
if var items = folder.items,
let index = items.firstIndex(where: { $0.id == itemId && $0.serverId == serverId }) {
let item = items.remove(at: index)
var updatedFolder = folder
updatedFolder.items = items
watchConfig.items[folderIndex] = updatedFolder
addItemToFolder(folderId: toFolderId, item: item)
return
}
}
}
func deleteItem(at offsets: IndexSet) {
watchConfig.items.remove(atOffsets: offsets)
}
func moveItem(from source: IndexSet, to destination: Int) {
watchConfig.items.move(fromOffsets: source, toOffset: destination)
}
func deleteConfiguration(completion: (Bool) -> Void) {
do {
try Current.database().write { db in
try WatchConfig.deleteAll(db)
completion(true)
}
} catch {
showError(message: L10n.Watch.Debug.DeleteDb.Alert.Failed.message(error.localizedDescription))
}
}
@MainActor
// Returns success boolean
func save() -> Bool {
do {
try Current.database().write { db in
if watchConfig.id != WatchConfig.watchConfigId {
// Previous config needs to be explicit deleted because when WatchConfig was released
// the ID wasn't static, so it was possible to have multiple rows in the table
try WatchConfig.deleteAll(db)
watchConfig.id = WatchConfig.watchConfigId
}
try watchConfig.insert(db, onConflict: .replace)
}
return true
} catch {
Current.Log.error("Failed to save new Watch config, error: \(error.localizedDescription)")
showError(message: L10n.Grdb.Config.MigrationError.failedToSave(error.localizedDescription))
return false
}
}
@MainActor
private func loadDatabase() {
defer {
if let prefilledItem {
addItem(prefilledItem)
}
}
do {
if let config = try WatchConfig.config() {
setConfig(config)
Current.Log.info("Watch configuration exists")
} else {
Current.Log.error("No watch config found")
}
} catch {
Current.Log.error("Failed to access database (GRDB), error: \(error.localizedDescription)")
showError(message: L10n.Grdb.Config.MigrationError.failedAccessGrdb(error.localizedDescription))
}
}
@MainActor
private func setConfig(_ config: WatchConfig) {
watchConfig = config
}
private func showError(message: String) {
DispatchQueue.main.async { [weak self] in
self?.errorMessage = message
self?.showError = true
}
}
}