From fbf2d9bf2cf50eb681afe18deac06a96c04004e6 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 17 Jul 2026 10:45:42 -0500 Subject: v4.7.0: Add watchlist tags and saved filter views - TrackedDomain.tags: [String] (backward-compatible custom decode), with updateTags(_:for:) and a normalized/deduplicated write path. - Tag editing in TrackedDomainDetailView (comma-separated field, chip display). - Tag filter chips in WatchlistView (TagFilterChipRowView), integrated into filteredTrackedDomains alongside the existing filter/search. - Saved views: name + snapshot the current tag/filter/sort as a WatchlistSavedView preset (UserDefaults-backed, not part of backup/restore), with a management sheet to apply or delete presets. --- DomainDig/DomainViewModel.swift | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) (limited to 'DomainDig/DomainViewModel.swift') diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift index 3c92ff6..33756df 100644 --- a/DomainDig/DomainViewModel.swift +++ b/DomainDig/DomainViewModel.swift @@ -312,6 +312,8 @@ final class DomainViewModel { var watchlistSearchText = "" var watchlistFilter: WatchlistFilterOption = .all var watchlistSortOption: WatchlistSortOption = .pinned + var watchlistTagFilter: String? + var watchlistSavedViews: [WatchlistSavedView] = DomainViewModel.loadWatchlistSavedViews() var dashboardSearchText = "" var dashboardFilter: PortfolioFilterOption = .all var monitoringSettings: MonitoringSettings = MonitoringStorage.loadSettings() @@ -432,6 +434,10 @@ final class DomainViewModel { return false } + if let watchlistTagFilter, !trackedDomain.tags.contains(watchlistTagFilter) { + return false + } + switch watchlistFilter { case .all: return true @@ -888,6 +894,69 @@ final class DomainViewModel { persistTrackedDomains() } + func updateTags(_ tags: [String], for trackedDomain: TrackedDomain) { + guard canEdit(trackedDomain) else { return } + guard let index = trackedDomains.firstIndex(where: { $0.id == trackedDomain.id }) else { return } + let normalized = Self.normalizedTags(tags) + trackedDomains[index].tags = normalized + trackedDomains[index].updatedAt = Date() + persistTrackedDomains() + } + + /// All tags currently in use across the watchlist, sorted for stable display. + var allWatchlistTags: [String] { + Array(Set(trackedDomains.flatMap(\.tags))).sorted() + } + + func saveCurrentWatchlistView(name: String) { + let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedName.isEmpty else { return } + let view = WatchlistSavedView( + name: trimmedName, + tag: watchlistTagFilter, + filter: watchlistFilter, + sort: watchlistSortOption + ) + watchlistSavedViews.append(view) + persistWatchlistSavedViews() + } + + func applyWatchlistSavedView(_ view: WatchlistSavedView) { + watchlistTagFilter = view.tag + watchlistFilter = view.filter + watchlistSortOption = view.sort + } + + func deleteWatchlistSavedViews(at offsets: IndexSet) { + watchlistSavedViews.remove(atOffsets: offsets) + persistWatchlistSavedViews() + } + + private static let watchlistSavedViewsKey = "watchlistSavedViews" + + private static func loadWatchlistSavedViews() -> [WatchlistSavedView] { + guard let data = UserDefaults.standard.data(forKey: watchlistSavedViewsKey), + let views = try? JSONDecoder().decode([WatchlistSavedView].self, from: data) + else { return [] } + return views + } + + private func persistWatchlistSavedViews() { + guard let data = try? JSONEncoder().encode(watchlistSavedViews) else { return } + UserDefaults.standard.set(data, forKey: Self.watchlistSavedViewsKey) + } + + private static func normalizedTags(_ tags: [String]) -> [String] { + var seen = Set() + var normalized: [String] = [] + for tag in tags { + let trimmed = tag.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, seen.insert(trimmed.lowercased()).inserted else { continue } + normalized.append(trimmed) + } + return normalized.sorted() + } + func removeHistoryEntries(at offsets: IndexSet) { history.remove(atOffsets: offsets) persistHistory() -- cgit v1.2.3