From 4b9a42edc3c80ddab8ce0a2bd4dd44eec10d550e Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 15 Feb 2026 22:06:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=ED=95=84=ED=84=B0=EB=A7=81=20UI=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Resource/Localizable.xcstrings | 18 ++ .../PushNotificationView.swift | 208 +++++++++++++++++- 2 files changed, 214 insertions(+), 12 deletions(-) diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index a59bc5f..5e160fa 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -6,6 +6,12 @@ }, "%@ 검색" : { + }, + "%lld" : { + + }, + "%lld개 필터가 적용됨" : { + }, "lime_green" : { "extractionState" : "manual", @@ -265,6 +271,9 @@ }, "계정 연동" : { + }, + "기간" : { + }, "로그아웃" : { @@ -277,6 +286,9 @@ }, "마감일" : { + }, + "모든 필터 지우기" : { + }, "미리보기" : { @@ -334,12 +346,18 @@ }, "완료" : { + }, + "읽지 않음" : { + }, "작년" : { }, "작성된 내용이 없습니다." : { + }, + "정렬" : { + }, "정렬 옵션" : { diff --git a/DevLog/UI/PushNotification/PushNotificationView.swift b/DevLog/UI/PushNotification/PushNotificationView.swift index 3536d13..1f53f9a 100644 --- a/DevLog/UI/PushNotification/PushNotificationView.swift +++ b/DevLog/UI/PushNotification/PushNotificationView.swift @@ -10,23 +10,36 @@ import SwiftUI struct PushNotificationView: View { @StateObject private var router = NavigationRouter() @StateObject var viewModel: PushNotificationViewModel + @Environment(\.colorScheme) private var colorScheme + @State private var previousStandardAppearance: UINavigationBarAppearance? + @State private var previousScrollEdgeAppearance: UINavigationBarAppearance? + @State private var sortOption: SortOption = .latest + @State private var timeFilter: TimeFilter = .none + @State private var showUnreadOnly: Bool = false var body: some View { NavigationStack(path: $router.path) { - VStack { - if viewModel.state.notifications.isEmpty { - Spacer() - Text("받은 알림이 없습니다.") - .foregroundStyle(Color.gray) - Spacer() - } else { - List(viewModel.state.notifications, id: \.id) { notification in - notificationRow(notification) + List { + Section { + if displayedNotifications.isEmpty { + HStack { + Spacer() + Text("받은 알림이 없습니다.") + .foregroundStyle(Color.gray) + Spacer() + } + .listRowSeparator(.hidden) + } else { + ForEach(displayedNotifications, id: \.id) { notification in + notificationRow(notification) + } } - .listStyle(.plain) + } header: { + headerView } + .listRowBackground(Color.clear) } - .frame(maxWidth: .infinity, alignment: .center) + .listStyle(.plain) .background(Color(.secondarySystemBackground)) .onAppear { viewModel.send(.fetchNotifications) } .navigationTitle("받은 푸시 알람") @@ -56,6 +69,131 @@ struct PushNotificationView: View { } } + private var headerView: some View { + ScrollView(.horizontal) { + HStack(spacing: 8) { + if 0 < appliedFilterCount { + Menu { + Text("\(appliedFilterCount)개 필터가 적용됨") + Button(role: .destructive) { + resetFilters() + } label: { + Text("모든 필터 지우기") + } + } label: { + HStack(spacing: 6) { + Image(systemName: "line.3.horizontal.decrease") + filterBadge + } + } + .adaptiveButtonStyle() + } + + Menu { + ForEach(SortOption.allCases, id: \.self) { option in + Button { + sortOption = option + } label: { + HStack { + Text(option.title) + Spacer() + if sortOption == option { + Image(systemName: "checkmark") + .tint(.blue) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } label: { + Text("정렬") + } + .adaptiveButtonStyle() + + Menu { + ForEach(TimeFilter.availableOptions, id: \.id) { option in + Button { + timeFilter = option + } label: { + HStack { + Text(option.title) + Spacer() + if timeFilter == option { + Image(systemName: "checkmark") + .tint(.blue) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } label: { + Text("기간") + } + .adaptiveButtonStyle() + + Button { + showUnreadOnly.toggle() + } label: { + Text("읽지 않음") + } + .adaptiveButtonStyle(showUnreadOnly ? .blue : .clear) + } + } + .scrollIndicators(.never) + } + + private var displayedNotifications: [PushNotification] { + var items = viewModel.state.notifications + + if showUnreadOnly { + items = items.filter { $0.isRead == false } + } + + if case let .hours(value) = timeFilter { + let threshold = Date().addingTimeInterval(-Double(value) * 3600.0) + items = items.filter { $0.receivedAt >= threshold } + } else if case let .days(value) = timeFilter { + let threshold = Date().addingTimeInterval(-Double(value) * 86400.0) + items = items.filter { $0.receivedAt >= threshold } + } + + switch sortOption { + case .latest: + return items.sorted { $0.receivedAt > $1.receivedAt } + case .oldest: + return items.sorted { $0.receivedAt < $1.receivedAt } + } + } + + private var appliedFilterCount: Int { + var count = 0 + if sortOption != .latest { count += 1 } + if timeFilter != .none { count += 1 } + if showUnreadOnly { count += 1 } + return count + } + + private var filterBadge: some View { + let isDark = colorScheme == .dark + let blue = Color(uiColor: .systemBlue) // 흰 배경에 따른 청록색화 방지 + let textColor: Color = isDark ? blue : .white + let backgroundColor: Color = isDark ? .white : blue + + return Text("\(appliedFilterCount)") + .font(.caption2.weight(.bold)) + .foregroundColor(textColor) + .lineLimit(1) + .minimumScaleFactor(0.6) + .frame(width: 20, height: 20) + .background(Circle().fill(backgroundColor)) + } + + private func resetFilters() { + sortOption = .latest + timeFilter = .none + showUnreadOnly = false + } + private func notificationRow(_ notification: PushNotification) -> some View { HStack { Circle() @@ -82,7 +220,6 @@ struct PushNotificationView: View { } } .padding(.vertical, 5) - .listRowBackground(Color.clear) .swipeActions(edge: .leading) { Button { viewModel.send(.toggleRead(notification)) @@ -119,4 +256,51 @@ struct PushNotificationView: View { return "\(days)일 전" } } + + private enum SortOption: CaseIterable { + case latest + case oldest + + var title: String { + switch self { + case .latest: return "최신 순" + case .oldest: return "예전 순" + } + } + } + + private enum TimeFilter: Equatable { + case none + case hours(Int) + case days(Int) + + var id: String { + switch self { + case .none: return "none" + case .hours(let value): return "hours-\(value)" + case .days(let value): return "days-\(value)" + } + } + + var title: String { + switch self { + case .none: + return "전체" + case .hours(let value): + return "최근 \(value)시간" + case .days(let value): + return "최근 \(value)일" + } + } + + static var availableOptions: [TimeFilter] {[ + .none, + .hours(1), + .hours(6), + .hours(24), + .days(3), + .days(7) + ] + } + } } From a9e0b6a2a6b059b55f3043bcb390784cc2fcd434 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 15 Feb 2026 22:24:19 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=A1=9C=EC=A7=81=20=EB=B7=B0?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/PushNotificationViewModel.swift | 95 ++++++++++++ DevLog/Resource/Localizable.xcstrings | 4 +- .../PushNotificationView.swift | 136 +++--------------- 3 files changed, 114 insertions(+), 121 deletions(-) diff --git a/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift b/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift index 2f26829..d7a7cd9 100644 --- a/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift +++ b/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift @@ -19,6 +19,9 @@ final class PushNotificationViewModel: Store { var toastType: ToastType? var isLoading: Bool = false var pendingTask: (PushNotification, Int)? + var sortOption: SortOption = .latest + var timeFilter: TimeFilter = .none + var showUnreadOnly: Bool = false } enum Action { @@ -31,6 +34,10 @@ final class PushNotificationViewModel: Store { case setToast(isPresented: Bool, type: ToastType? = nil) case setLoading(Bool) case setNotifications([PushNotification]) + case setSortOption(SortOption) + case setTimeFilter(TimeFilter) + case toggleUnreadOnly + case resetFilters } enum SideEffect { @@ -47,6 +54,53 @@ final class PushNotificationViewModel: Store { case delete } + enum SortOption: CaseIterable { + case latest + case oldest + + var title: String { + switch self { + case .latest: return "최신순" + case .oldest: return "예전순" + } + } + } + + enum TimeFilter: Equatable { + case none + case hours(Int) + case days(Int) + + var id: String { + switch self { + case .none: return "none" + case .hours(let value): return "hours-\(value)" + case .days(let value): return "days-\(value)" + } + } + + var title: String { + switch self { + case .none: + return "전체" + case .hours(let value): + return "최근 \(value)시간" + case .days(let value): + return "최근 \(value)일" + } + } + + static var availableOptions: [TimeFilter] {[ + .none, + .hours(1), + .hours(6), + .hours(24), + .days(3), + .days(7) + ] + } + } + @Published private(set) var state: State = .init() private let fetchUseCase: FetchPushNotificationsUseCase private let deleteUseCase: DeletePushNotificationUseCase @@ -62,6 +116,37 @@ final class PushNotificationViewModel: Store { self.toggleReadUseCase = toggleReadUseCase } + var displayedNotifications: [PushNotification] { + var items = state.notifications + + if state.showUnreadOnly { + items = items.filter { $0.isRead == false } + } + + if case let .hours(value) = state.timeFilter { + let threshold = Date().addingTimeInterval(-Double(value) * 3600.0) + items = items.filter { $0.receivedAt >= threshold } + } else if case let .days(value) = state.timeFilter { + let threshold = Date().addingTimeInterval(-Double(value) * 86400.0) + items = items.filter { $0.receivedAt >= threshold } + } + + switch state.sortOption { + case .latest: + return items.sorted { $0.receivedAt > $1.receivedAt } + case .oldest: + return items.sorted { $0.receivedAt < $1.receivedAt } + } + } + + var appliedFilterCount: Int { + var count = 0 + if state.sortOption != .latest { count += 1 } + if state.timeFilter != .none { count += 1 } + if state.showUnreadOnly { count += 1 } + return count + } + func reduce(with action: Action) -> [SideEffect] { var state = self.state var effects: [SideEffect] = [] @@ -96,6 +181,16 @@ final class PushNotificationViewModel: Store { state.isLoading = value case .setNotifications(let notifications): state.notifications = notifications + case .setSortOption(let option): + state.sortOption = option + case .setTimeFilter(let filter): + state.timeFilter = filter + case .toggleUnreadOnly: + state.showUnreadOnly.toggle() + case .resetFilters: + state.sortOption = .latest + state.timeFilter = .none + state.showUnreadOnly = false } self.state = state diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index 5e160fa..da35c22 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -356,10 +356,10 @@ "작성된 내용이 없습니다." : { }, - "정렬" : { + "정렬 옵션" : { }, - "정렬 옵션" : { + "정렬: %@" : { }, "정말 탈퇴하시겠습니까?" : { diff --git a/DevLog/UI/PushNotification/PushNotificationView.swift b/DevLog/UI/PushNotification/PushNotificationView.swift index 1f53f9a..83b8ed0 100644 --- a/DevLog/UI/PushNotification/PushNotificationView.swift +++ b/DevLog/UI/PushNotification/PushNotificationView.swift @@ -11,17 +11,12 @@ struct PushNotificationView: View { @StateObject private var router = NavigationRouter() @StateObject var viewModel: PushNotificationViewModel @Environment(\.colorScheme) private var colorScheme - @State private var previousStandardAppearance: UINavigationBarAppearance? - @State private var previousScrollEdgeAppearance: UINavigationBarAppearance? - @State private var sortOption: SortOption = .latest - @State private var timeFilter: TimeFilter = .none - @State private var showUnreadOnly: Bool = false var body: some View { NavigationStack(path: $router.path) { List { Section { - if displayedNotifications.isEmpty { + if viewModel.displayedNotifications.isEmpty { HStack { Spacer() Text("받은 알림이 없습니다.") @@ -30,7 +25,7 @@ struct PushNotificationView: View { } .listRowSeparator(.hidden) } else { - ForEach(displayedNotifications, id: \.id) { notification in + ForEach(viewModel.displayedNotifications, id: \.id) { notification in notificationRow(notification) } } @@ -72,11 +67,11 @@ struct PushNotificationView: View { private var headerView: some View { ScrollView(.horizontal) { HStack(spacing: 8) { - if 0 < appliedFilterCount { + if 0 < viewModel.appliedFilterCount { Menu { - Text("\(appliedFilterCount)개 필터가 적용됨") + Text("\(viewModel.appliedFilterCount)개 필터가 적용됨") Button(role: .destructive) { - resetFilters() + viewModel.send(.resetFilters) } label: { Text("모든 필터 지우기") } @@ -89,36 +84,23 @@ struct PushNotificationView: View { .adaptiveButtonStyle() } - Menu { - ForEach(SortOption.allCases, id: \.self) { option in - Button { - sortOption = option - } label: { - HStack { - Text(option.title) - Spacer() - if sortOption == option { - Image(systemName: "checkmark") - .tint(.blue) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - } + Button { + let next: PushNotificationViewModel.SortOption = viewModel.state.sortOption == .latest ? .oldest : .latest + viewModel.send(.setSortOption(next)) } label: { - Text("정렬") + Text("정렬: \(viewModel.state.sortOption.title)") } .adaptiveButtonStyle() Menu { - ForEach(TimeFilter.availableOptions, id: \.id) { option in + ForEach(PushNotificationViewModel.TimeFilter.availableOptions, id: \.id) { option in Button { - timeFilter = option + viewModel.send(.setTimeFilter(option)) } label: { HStack { Text(option.title) Spacer() - if timeFilter == option { + if viewModel.state.timeFilter == option { Image(systemName: "checkmark") .tint(.blue) } @@ -132,54 +114,23 @@ struct PushNotificationView: View { .adaptiveButtonStyle() Button { - showUnreadOnly.toggle() + viewModel.send(.toggleUnreadOnly) } label: { Text("읽지 않음") } - .adaptiveButtonStyle(showUnreadOnly ? .blue : .clear) + .adaptiveButtonStyle(viewModel.state.showUnreadOnly ? .blue : .clear) } } .scrollIndicators(.never) } - private var displayedNotifications: [PushNotification] { - var items = viewModel.state.notifications - - if showUnreadOnly { - items = items.filter { $0.isRead == false } - } - - if case let .hours(value) = timeFilter { - let threshold = Date().addingTimeInterval(-Double(value) * 3600.0) - items = items.filter { $0.receivedAt >= threshold } - } else if case let .days(value) = timeFilter { - let threshold = Date().addingTimeInterval(-Double(value) * 86400.0) - items = items.filter { $0.receivedAt >= threshold } - } - - switch sortOption { - case .latest: - return items.sorted { $0.receivedAt > $1.receivedAt } - case .oldest: - return items.sorted { $0.receivedAt < $1.receivedAt } - } - } - - private var appliedFilterCount: Int { - var count = 0 - if sortOption != .latest { count += 1 } - if timeFilter != .none { count += 1 } - if showUnreadOnly { count += 1 } - return count - } - private var filterBadge: some View { let isDark = colorScheme == .dark let blue = Color(uiColor: .systemBlue) // 흰 배경에 따른 청록색화 방지 let textColor: Color = isDark ? blue : .white let backgroundColor: Color = isDark ? .white : blue - return Text("\(appliedFilterCount)") + return Text("\(viewModel.appliedFilterCount)") .font(.caption2.weight(.bold)) .foregroundColor(textColor) .lineLimit(1) @@ -188,12 +139,6 @@ struct PushNotificationView: View { .background(Circle().fill(backgroundColor)) } - private func resetFilters() { - sortOption = .latest - timeFilter = .none - showUnreadOnly = false - } - private func notificationRow(_ notification: PushNotification) -> some View { HStack { Circle() @@ -239,10 +184,10 @@ struct PushNotificationView: View { } } } - + private func timeAgoText(from date: Date, now: Date) -> String { let seconds = Int(now.timeIntervalSince(date)) - + if seconds < 60 { return "\(max(0, seconds))초 전" } else if seconds < 3600 { @@ -256,51 +201,4 @@ struct PushNotificationView: View { return "\(days)일 전" } } - - private enum SortOption: CaseIterable { - case latest - case oldest - - var title: String { - switch self { - case .latest: return "최신 순" - case .oldest: return "예전 순" - } - } - } - - private enum TimeFilter: Equatable { - case none - case hours(Int) - case days(Int) - - var id: String { - switch self { - case .none: return "none" - case .hours(let value): return "hours-\(value)" - case .days(let value): return "days-\(value)" - } - } - - var title: String { - switch self { - case .none: - return "전체" - case .hours(let value): - return "최근 \(value)시간" - case .days(let value): - return "최근 \(value)일" - } - } - - static var availableOptions: [TimeFilter] {[ - .none, - .hours(1), - .hours(6), - .hours(24), - .days(3), - .days(7) - ] - } - } } From e203be5f51e1a793f636ac7d2ada28fa06b348af Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 15 Feb 2026 22:48:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?ui:=20=ED=95=84=ED=84=B0=EB=A7=81=EC=9D=B4?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=EB=90=9C=20=EB=B2=84=ED=8A=BC=EC=9D=80=20?= =?UTF-8?q?=ED=8C=8C=EB=9E=80=EC=83=89=EC=9C=BC=EB=A1=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/PushNotification/PushNotificationView.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/DevLog/UI/PushNotification/PushNotificationView.swift b/DevLog/UI/PushNotification/PushNotificationView.swift index 83b8ed0..aeab0f2 100644 --- a/DevLog/UI/PushNotification/PushNotificationView.swift +++ b/DevLog/UI/PushNotification/PushNotificationView.swift @@ -85,12 +85,11 @@ struct PushNotificationView: View { } Button { - let next: PushNotificationViewModel.SortOption = viewModel.state.sortOption == .latest ? .oldest : .latest - viewModel.send(.setSortOption(next)) + viewModel.send(.toggleSortOption) } label: { Text("정렬: \(viewModel.state.sortOption.title)") } - .adaptiveButtonStyle() + .adaptiveButtonStyle(viewModel.state.sortOption == .oldest ? .blue : .clear) Menu { ForEach(PushNotificationViewModel.TimeFilter.availableOptions, id: \.id) { option in @@ -111,7 +110,7 @@ struct PushNotificationView: View { } label: { Text("기간") } - .adaptiveButtonStyle() + .adaptiveButtonStyle(viewModel.state.timeFilter == .none ? .clear : .blue) Button { viewModel.send(.toggleUnreadOnly) From f6d168376cd787095f8f4991f9d2cbb5d9dd3c80 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 15 Feb 2026 22:50:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20UserDefatuls=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/PushNotificationViewModel.swift | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift b/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift index d7a7cd9..8698902 100644 --- a/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift +++ b/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift @@ -19,9 +19,9 @@ final class PushNotificationViewModel: Store { var toastType: ToastType? var isLoading: Bool = false var pendingTask: (PushNotification, Int)? - var sortOption: SortOption = .latest - var timeFilter: TimeFilter = .none - var showUnreadOnly: Bool = false + var sortOption: SortOption + var timeFilter: TimeFilter + var showUnreadOnly: Bool } enum Action { @@ -34,7 +34,7 @@ final class PushNotificationViewModel: Store { case setToast(isPresented: Bool, type: ToastType? = nil) case setLoading(Bool) case setNotifications([PushNotification]) - case setSortOption(SortOption) + case toggleSortOption case setTimeFilter(TimeFilter) case toggleUnreadOnly case resetFilters @@ -99,21 +99,49 @@ final class PushNotificationViewModel: Store { .days(7) ] } + + init(id: String) { + if id == "none" { + self = .none + } else if id.hasPrefix("hours-") { + let value = Int(id.replacingOccurrences(of: "hours-", with: "")) ?? 0 + self = value > 0 ? .hours(value) : .none + } else if id.hasPrefix("days-") { + let value = Int(id.replacingOccurrences(of: "days-", with: "")) ?? 0 + self = value > 0 ? .days(value) : .none + } else { + self = .none + } + } } - @Published private(set) var state: State = .init() + @Published private(set) var state: State private let fetchUseCase: FetchPushNotificationsUseCase private let deleteUseCase: DeletePushNotificationUseCase private let toggleReadUseCase: TogglePushNotificationReadUseCase + private let userDefaults: UserDefaults + + private enum DefaultsKey { + static let sortOption = "PushNotification.sortOption" + static let timeFilter = "PushNotification.timeFilter" + static let showUnreadOnly = "PushNotification.showUnreadOnly" + } init( fetchUseCase: FetchPushNotificationsUseCase, deleteUseCase: DeletePushNotificationUseCase, - toggleReadUseCase: TogglePushNotificationReadUseCase + toggleReadUseCase: TogglePushNotificationReadUseCase, + userDefaults: UserDefaults = .standard ) { self.fetchUseCase = fetchUseCase self.deleteUseCase = deleteUseCase self.toggleReadUseCase = toggleReadUseCase + self.userDefaults = userDefaults + self.state = State( + sortOption: Self.loadSortOption(userDefaults: userDefaults), + timeFilter: Self.loadTimeFilter(userDefaults: userDefaults), + showUnreadOnly: userDefaults.bool(forKey: DefaultsKey.showUnreadOnly) + ) } var displayedNotifications: [PushNotification] { @@ -181,16 +209,22 @@ final class PushNotificationViewModel: Store { state.isLoading = value case .setNotifications(let notifications): state.notifications = notifications - case .setSortOption(let option): - state.sortOption = option + case .toggleSortOption: + state.sortOption = state.sortOption == .latest ? .oldest : .latest + saveSortOption(state.sortOption) case .setTimeFilter(let filter): state.timeFilter = filter + saveTimeFilter(filter) case .toggleUnreadOnly: state.showUnreadOnly.toggle() + userDefaults.set(state.showUnreadOnly, forKey: DefaultsKey.showUnreadOnly) case .resetFilters: state.sortOption = .latest state.timeFilter = .none state.showUnreadOnly = false + saveSortOption(.latest) + saveTimeFilter(.none) + userDefaults.set(false, forKey: DefaultsKey.showUnreadOnly) } self.state = state @@ -261,4 +295,25 @@ private extension PushNotificationViewModel { } state.showToast = isPresented } + + static func loadSortOption(userDefaults: UserDefaults) -> SortOption { + guard let rawValue = userDefaults.string(forKey: DefaultsKey.sortOption) else { + return .latest + } + return rawValue == "oldest" ? .oldest : .latest + } + + static func loadTimeFilter(userDefaults: UserDefaults) -> TimeFilter { + let id = userDefaults.string(forKey: DefaultsKey.timeFilter) ?? "none" + return TimeFilter(id: id) + } + + func saveSortOption(_ option: SortOption) { + let value = option == .oldest ? "oldest" : "latest" + userDefaults.set(value, forKey: DefaultsKey.sortOption) + } + + func saveTimeFilter(_ filter: TimeFilter) { + userDefaults.set(filter.id, forKey: DefaultsKey.timeFilter) + } }