From 17dff342e71784419503e4f677b71db7f180a8f9 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:04:46 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20SettingsView=20TCA=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Main/MainView.swift | 10 +- .../Sources/Profile/ProfileView.swift | 10 +- .../Profile/ProfileViewCoordinator.swift | 23 +- .../Sources/Settings/SettingsFeature.swift | 365 ++++++++++++++++++ .../Sources/Settings/SettingsView.swift | 68 +--- .../Sources/Settings/SettingsViewModel.swift | 219 ----------- .../Sources/Settings/ThemeView.swift | 1 - 7 files changed, 398 insertions(+), 298 deletions(-) create mode 100644 Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift delete mode 100644 Application/DevLogPresentation/Sources/Settings/SettingsViewModel.swift diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index bc9eb995..dd09ee42 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -361,15 +361,11 @@ struct MainView: View { TodoDetailView(store: profileViewCoordinator.makeTodoDetailStore(todoId: todoId)) .id(todoId) case .settings: - SettingsView(viewModel: profileViewCoordinator.settingsViewModel) + SettingsView(store: profileViewCoordinator.settingsStore) .environment(profileViewCoordinator.router) case .theme: - ThemeView( - theme: Binding( - get: { profileViewCoordinator.settingsViewModel.state.theme }, - set: { profileViewCoordinator.settingsViewModel.send(.setTheme($0)) } - ) - ) + @Bindable var settingsStore = profileViewCoordinator.settingsStore + ThemeView(theme: $settingsStore.theme) case .pushNotification: PushNotificationSettingsView( store: profileViewCoordinator.makePushNotificationSettingsStore() diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift index 617e85a5..20cf24a2 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift @@ -158,17 +158,13 @@ struct ProfileView: View { private func profileDestinationView(_ route: ProfileRoute) -> some View { switch route { case .settings: - SettingsView(viewModel: coordinator.settingsViewModel) + SettingsView(store: coordinator.settingsStore) .environment(coordinator.router) case .activity(let todoId): TodoDetailView(store: coordinator.makeTodoDetailStore(todoId: todoId)) case .theme: - ThemeView( - theme: Binding( - get: { coordinator.settingsViewModel.state.theme }, - set: { coordinator.settingsViewModel.send(.setTheme($0)) } - ) - ) + @Bindable var settingsStore = coordinator.settingsStore + ThemeView(theme: $settingsStore.theme) case .pushNotification: PushNotificationSettingsView(store: coordinator.makePushNotificationSettingsStore()) case .account: diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift b/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift index dd8300c6..7fe9ab22 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift @@ -14,7 +14,7 @@ import DevLogDomain @Observable final class ProfileViewCoordinator { let viewModel: ProfileViewModel - let settingsViewModel: SettingsViewModel + let settingsStore: StoreOf var router = NavigationRouter() private let container: DIContainer @@ -29,15 +29,18 @@ final class ProfileViewCoordinator { fetchHeatmapActivityTypesUseCase: container.resolve(FetchHeatmapActivityTypesUseCase.self), updateHeatmapActivityTypesUseCase: container.resolve(UpdateHeatmapActivityTypesUseCase.self) ) - self.settingsViewModel = SettingsViewModel( - deleteAuthUseCase: container.resolve(DeleteAuthUseCase.self), - signOutUseCase: container.resolve(SignOutUseCase.self), - networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), - systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), - updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self), - fetchWebPageImageDirSizeUseCase: container.resolve(FetchWebPageImageDirSizeUseCase.self), - clearWebPageImageDirectoryUseCase: container.resolve(ClearWebPageImageDirectoryUseCase.self) - ) + self.settingsStore = Store(initialState: SettingsFeature.State()) { + SettingsFeature() + } withDependencies: { + $0.deleteAuthUseCase = container.resolve(DeleteAuthUseCase.self) + $0.signOutUseCase = container.resolve(SignOutUseCase.self) + $0.networkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) + $0.systemThemeUseCase = container.resolve(ObserveSystemThemeUseCase.self) + $0.updateSystemThemeUseCase = container.resolve(UpdateSystemThemeUseCase.self) + $0.fetchWebPageImageDirSizeUseCase = container.resolve(FetchWebPageImageDirSizeUseCase.self) + $0.clearWebPageImageDirectoryUseCase = container.resolve(ClearWebPageImageDirectoryUseCase.self) + } + self.settingsStore.send(.startObserving) } func fetchData() { diff --git a/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift b/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift new file mode 100644 index 00000000..b506bb3e --- /dev/null +++ b/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift @@ -0,0 +1,365 @@ +// +// SettingsFeature.swift +// DevLogPresentation +// +// Created by opfic on 6/12/26. +// + +import Combine +import ComposableArchitecture +import DevLogCore +import DevLogDomain +import Foundation + +@Reducer +struct SettingsFeature { + @ObservableState + struct State: Equatable { + @Presents var alert: AlertState? + var theme: SystemTheme = .automatic + var dirSize: Int64 = 0 + var isNetworkConnected = true + var loading = LoadingFeature.State() + var alertType: Action.AlertType? + var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + var appstoreUrl = Bundle.main.object(forInfoDictionaryKey: "TESTFLIGHT_URL") as? String + var policyURL = Bundle.main.object(forInfoDictionaryKey: "PRIVACY_POLICY_URL") as? String + + var isLoading: Bool { + loading.isLoading + } + } + + enum Action: BindableAction { + case alert(PresentationAction) + case binding(BindingAction) + case startObserving + case networkStatusChanged(Bool) + case setAlert(AlertType) + case setDirSize(Int64) + case updateDirSize + case tapDeleteAuthButton + case tapSignOutButton + case tapRemoveCacheButton + case confirmRemoveCache + case loading(LoadingFeature.Action) + + enum Alert: Equatable { + case tapDeleteAuthButton + case tapSignOutButton + case confirmRemoveCache + } + + enum AlertType: Equatable { + case signOut + case deleteAuth + case error + case removeCache + } + } + + @Dependency(\.deleteAuthUseCase) var deleteAuthUseCase + @Dependency(\.signOutUseCase) var signOutUseCase + @Dependency(\.networkConnectivityUseCase) var networkConnectivityUseCase + @Dependency(\.systemThemeUseCase) var systemThemeUseCase + @Dependency(\.updateSystemThemeUseCase) var updateSystemThemeUseCase + @Dependency(\.fetchWebPageImageDirSizeUseCase) var fetchWebPageImageDirSizeUseCase + @Dependency(\.clearWebPageImageDirectoryUseCase) var clearWebPageImageDirectoryUseCase + + var body: some ReducerOf { + Scope(state: \.loading, action: \.loading) { + LoadingFeature() + } + BindingReducer() + Reduce { state, action in + switch action { + case .alert(.presented(.tapDeleteAuthButton)): + state.alert = nil + state.alertType = nil + return deleteAuthEffect() + case .alert(.presented(.tapSignOutButton)): + state.alert = nil + state.alertType = nil + return signOutEffect() + case .alert(.presented(.confirmRemoveCache)): + state.alert = nil + state.alertType = nil + return clearWebPageImageDirectoryEffect() + case .alert(.dismiss): + state.alert = nil + state.alertType = nil + case .alert: + break + case .binding(\.theme): + return updateSystemThemeEffect(state.theme) + case .binding: + break + case .startObserving: + return .merge( + observeNetworkConnectivityEffect(), + monitorSystemThemeEffect() + ) + case .networkStatusChanged(let isConnected): + state.isNetworkConnected = isConnected + case .setAlert(let type): + state.alert = alertState(for: type) + state.alertType = type + case .setDirSize(let value): + state.dirSize = value + case .updateDirSize: + return fetchWebPageImageDirSizeEffect() + case .tapDeleteAuthButton: + state.alert = nil + state.alertType = nil + return deleteAuthEffect() + case .tapSignOutButton: + state.alert = nil + state.alertType = nil + return signOutEffect() + case .tapRemoveCacheButton: + state.alert = alertState(for: .removeCache) + state.alertType = .removeCache + case .confirmRemoveCache: + state.alert = nil + state.alertType = nil + return clearWebPageImageDirectoryEffect() + case .loading: + break + } + + return .none + } + .ifLet(\.$alert, action: \.alert) + } +} + +extension DependencyValues { + var deleteAuthUseCase: DeleteAuthUseCase { + get { self[DeleteAuthUseCaseKey.self] } + set { self[DeleteAuthUseCaseKey.self] = newValue } + } + + var signOutUseCase: SignOutUseCase { + get { self[SignOutUseCaseKey.self] } + set { self[SignOutUseCaseKey.self] = newValue } + } + + var networkConnectivityUseCase: ObserveNetworkConnectivityUseCase { + get { self[ObserveNetworkConnectivityUseCaseKey.self] } + set { self[ObserveNetworkConnectivityUseCaseKey.self] = newValue } + } + + var systemThemeUseCase: ObserveSystemThemeUseCase { + get { self[ObserveSystemThemeUseCaseKey.self] } + set { self[ObserveSystemThemeUseCaseKey.self] = newValue } + } + + var updateSystemThemeUseCase: UpdateSystemThemeUseCase { + get { self[UpdateSystemThemeUseCaseKey.self] } + set { self[UpdateSystemThemeUseCaseKey.self] = newValue } + } + + var fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase { + get { self[FetchWebPageImageDirSizeUseCaseKey.self] } + set { self[FetchWebPageImageDirSizeUseCaseKey.self] = newValue } + } + + var clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase { + get { self[ClearWebPageImageDirectoryUseCaseKey.self] } + set { self[ClearWebPageImageDirectoryUseCaseKey.self] = newValue } + } +} + +private enum DeleteAuthUseCaseKey: DependencyKey { + static var liveValue: DeleteAuthUseCase { + preconditionFailure("DeleteAuthUseCase must be provided.") + } + + static var testValue: DeleteAuthUseCase { + liveValue + } +} + +private enum SignOutUseCaseKey: DependencyKey { + static var liveValue: SignOutUseCase { + preconditionFailure("SignOutUseCase must be provided.") + } + + static var testValue: SignOutUseCase { + liveValue + } +} + +private enum ObserveNetworkConnectivityUseCaseKey: DependencyKey { + static var liveValue: ObserveNetworkConnectivityUseCase { + preconditionFailure("ObserveNetworkConnectivityUseCase must be provided.") + } + + static var testValue: ObserveNetworkConnectivityUseCase { + liveValue + } +} + +private enum ObserveSystemThemeUseCaseKey: DependencyKey { + static var liveValue: ObserveSystemThemeUseCase { + preconditionFailure("ObserveSystemThemeUseCase must be provided.") + } + + static var testValue: ObserveSystemThemeUseCase { + liveValue + } +} + +private enum UpdateSystemThemeUseCaseKey: DependencyKey { + static var liveValue: UpdateSystemThemeUseCase { + preconditionFailure("UpdateSystemThemeUseCase must be provided.") + } + + static var testValue: UpdateSystemThemeUseCase { + liveValue + } +} + +private enum FetchWebPageImageDirSizeUseCaseKey: DependencyKey { + static var liveValue: FetchWebPageImageDirSizeUseCase { + preconditionFailure("FetchWebPageImageDirSizeUseCase must be provided.") + } + + static var testValue: FetchWebPageImageDirSizeUseCase { + liveValue + } +} + +private enum ClearWebPageImageDirectoryUseCaseKey: DependencyKey { + static var liveValue: ClearWebPageImageDirectoryUseCase { + preconditionFailure("ClearWebPageImageDirectoryUseCase must be provided.") + } + + static var testValue: ClearWebPageImageDirectoryUseCase { + liveValue + } +} + +private extension SettingsFeature { + func observeNetworkConnectivityEffect() -> Effect { + .publisher { [networkConnectivityUseCase] in + networkConnectivityUseCase.observe() + .receive(on: DispatchQueue.main) + .map(Action.networkStatusChanged) + } + } + + func monitorSystemThemeEffect() -> Effect { + .publisher { [systemThemeUseCase] in + systemThemeUseCase.observe() + .removeDuplicates() + .receive(on: DispatchQueue.main) + .map { .binding(.set(\.theme, $0)) } + } + } + + func updateSystemThemeEffect(_ theme: SystemTheme) -> Effect { + .run { [updateSystemThemeUseCase] _ in + updateSystemThemeUseCase.execute(theme) + } + } + + func fetchWebPageImageDirSizeEffect() -> Effect { + .run { [fetchWebPageImageDirSizeUseCase] send in + let dirSize = await fetchWebPageImageDirSizeUseCase.execute() + await send(.setDirSize(dirSize)) + } + } + + func clearWebPageImageDirectoryEffect() -> Effect { + .run { [clearWebPageImageDirectoryUseCase, fetchWebPageImageDirSizeUseCase] send in + do { + try await clearWebPageImageDirectoryUseCase.execute() + let dirSize = await fetchWebPageImageDirSizeUseCase.execute() + await send(.setDirSize(dirSize)) + } catch { + await send(.setAlert(.error)) + } + } + } + + func deleteAuthEffect() -> Effect { + .run { [deleteAuthUseCase] send in + await send(.loading(.begin(target: .default, mode: .delayed))) + do { + try await deleteAuthUseCase.execute() + await send(.loading(.end(target: .default, mode: .delayed))) + } catch { + await send(.loading(.end(target: .default, mode: .delayed))) + await send(.setAlert(.error)) + } + } + } + + func signOutEffect() -> Effect { + .run { [signOutUseCase] send in + await send(.loading(.begin(target: .default, mode: .delayed))) + do { + try await signOutUseCase.execute() + await send(.loading(.end(target: .default, mode: .delayed))) + } catch { + await send(.loading(.end(target: .default, mode: .delayed))) + await send(.setAlert(.error)) + } + } + } + + func alertState(for type: Action.AlertType) -> AlertState { + switch type { + case .signOut: + return AlertState { + TextState(String(localized: "settings_alert_sign_out_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_cancel")) + } + ButtonState(role: .destructive, action: .tapSignOutButton) { + TextState(String(localized: "common_confirm")) + } + } message: { + TextState(String(localized: "settings_alert_sign_out_message")) + } + case .deleteAuth: + return AlertState { + TextState(String(localized: "settings_alert_delete_account_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_cancel")) + } + ButtonState(role: .destructive, action: .tapDeleteAuthButton) { + TextState(String(localized: "settings_delete_account_action")) + } + } message: { + TextState(String(localized: "settings_alert_delete_account_message")) + } + case .error: + return AlertState { + TextState(String(localized: "common_error_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_close")) + } + } message: { + TextState(String(localized: "common_error_message")) + } + case .removeCache: + return AlertState { + TextState(String(localized: "settings_alert_clear_temp_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_cancel")) + } + ButtonState(role: .destructive, action: .confirmRemoveCache) { + TextState(String(localized: "common_confirm")) + } + } message: { + TextState(String(localized: "settings_alert_clear_temp_message")) + } + } + } +} diff --git a/Application/DevLogPresentation/Sources/Settings/SettingsView.swift b/Application/DevLogPresentation/Sources/Settings/SettingsView.swift index 059e32e9..9a60fcdd 100644 --- a/Application/DevLogPresentation/Sources/Settings/SettingsView.swift +++ b/Application/DevLogPresentation/Sources/Settings/SettingsView.swift @@ -6,14 +6,14 @@ // import SwiftUI -import DevLogDomain +import ComposableArchitecture struct SettingsView: View { @Environment(NavigationRouter.self) private var router - @State var viewModel: SettingsViewModel + @Bindable var store: StoreOf var body: some View { - let connected = viewModel.state.isNetworkConnected + let connected = store.isNetworkConnected Form { Section { Button { @@ -23,7 +23,7 @@ struct SettingsView: View { Text(String(localized: "settings_theme")) .foregroundStyle(Color.primary) Spacer() - Text(viewModel.state.theme.localizedName) + Text(store.theme.localizedName) .foregroundStyle(Color.gray) } } @@ -36,9 +36,9 @@ struct SettingsView: View { } .disabled(!connected) - let dirSize = viewModel.state.dirSize + let dirSize = store.dirSize Button { - viewModel.send(.tapRemoveCacheButton) + store.send(.tapRemoveCacheButton) } label: { HStack { Text(String(localized: "settings_clear_temp_data")) @@ -52,14 +52,14 @@ struct SettingsView: View { } Section { - if let appVersion = viewModel.appVersion { + if let appVersion = store.appVersion { HStack { Text(String(localized: "settings_version")) Spacer() Text(appVersion) } } - if let policyString = viewModel.policyURL, + if let policyString = store.policyURL, let url = URL(string: policyString) { Link(destination: url) { Text(String(localized: "settings_privacy_policy")) @@ -67,7 +67,7 @@ struct SettingsView: View { } } Button(action: { - if let appStoreString = viewModel.appstoreUrl, + if let appStoreString = store.appstoreUrl, let url = URL(string: appStoreString) { UIApplication.shared.open(url) } @@ -89,7 +89,7 @@ struct SettingsView: View { } .disabled(!connected) Button(role: .destructive, action: { - viewModel.send(.setAlert(isPresented: true, type: .signOut)) + store.send(.setAlert(.signOut)) }) { Text(String(localized: "settings_sign_out")) } @@ -99,7 +99,7 @@ struct SettingsView: View { HStack { Spacer() Button(role: .destructive, action: { - viewModel.send(.setAlert(isPresented: true, type: .deleteAuth)) + store.send(.setAlert(.deleteAuth)) }) { Text(String(localized: "settings_delete_account")) .font(.headline) @@ -110,54 +110,14 @@ struct SettingsView: View { } .navigationTitle(String(localized: "nav_settings")) .navigationBarTitleDisplayMode(.inline) - .alert( - viewModel.state.alertTitle, - isPresented: Binding( - get: { viewModel.state.showAlert }, - set: { viewModel.send(.setAlert(isPresented: $0)) } - )) { - alertButtons - } message: { - Text(viewModel.state.alertMessage) - } + .alert($store.scope(state: \.alert, action: \.alert)) .overlay { - if viewModel.state.isLoading { + if store.isLoading { LoadingView() } } .onAppear { - viewModel.send(.updateDirSize) - } - } - - @ViewBuilder - private var alertButtons: some View { - switch viewModel.state.alertType { - case .signOut: - Button(String(localized: "common_cancel"), role: .cancel) { - viewModel.send(.setAlert(isPresented: false)) - } - Button(String(localized: "common_confirm"), role: .destructive) { - viewModel.send(.tapSignOutButton) - } - case .deleteAuth: - Button(String(localized: "common_cancel"), role: .cancel) { - viewModel.send(.setAlert(isPresented: false)) - } - Button(String(localized: "settings_delete_account_action"), role: .destructive) { - viewModel.send(.tapDeleteAuthButton) - } - case .removeCache: - Button(String(localized: "common_cancel"), role: .cancel) { - viewModel.send(.setAlert(isPresented: false)) - } - Button(String(localized: "common_confirm"), role: .destructive) { - viewModel.send(.confirmRemoveCache) - } - case .error, .none: - Button(String(localized: "common_close"), role: .cancel) { - viewModel.send(.setAlert(isPresented: false)) - } + store.send(.updateDirSize) } } diff --git a/Application/DevLogPresentation/Sources/Settings/SettingsViewModel.swift b/Application/DevLogPresentation/Sources/Settings/SettingsViewModel.swift deleted file mode 100644 index 33bb1295..00000000 --- a/Application/DevLogPresentation/Sources/Settings/SettingsViewModel.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// SettingsViewModel.swift -// DevLogPresentation -// -// Created by 최윤진 on 11/22/25. -// - -import Foundation -import Combine -import DevLogCore -import DevLogDomain - -@Observable -final class SettingsViewModel: StorePattern { - struct State: Equatable { - var theme: SystemTheme = .automatic - var dirSize: Int64 = 0 - var isNetworkConnected = true - var isLoading = false - var showAlert: Bool = false - var alertTitle: String = "" - var alertType: AlertType? - var alertMessage: String = "" - } - - enum Action { - case networkStatusChanged(Bool) - case setAlert(isPresented: Bool, type: AlertType? = nil) - case setDirSize(Int64) - case setLoading(Bool) - case setTheme(SystemTheme) - case updateDirSize - case tapDeleteAuthButton - case tapSignOutButton - case tapRemoveCacheButton - case confirmRemoveCache - } - - enum SideEffect { - case clearWebPageImageDirectory - case deleteAuth - case fetchWebPageImageDirSize - case signOut - } - - enum AlertType { - case signOut, deleteAuth, error, removeCache - } - - private(set) var state = State() - private let deleteAuthuseCase: DeleteAuthUseCase - private let signOutUseCase: SignOutUseCase - private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase - private let systemThemeUseCase: ObserveSystemThemeUseCase - private let updateSystemThemeUseCase: UpdateSystemThemeUseCase - private let fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase - private let clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase - private let loadingState = LoadingState() - private var cancellables = Set() - - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let appstoreUrl = Bundle.main.object(forInfoDictionaryKey: "TESTFLIGHT_URL") as? String - let policyURL = Bundle.main.object(forInfoDictionaryKey: "PRIVACY_POLICY_URL") as? String - - init( - deleteAuthUseCase: DeleteAuthUseCase, - signOutUseCase: SignOutUseCase, - networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, - systemThemeUseCase: ObserveSystemThemeUseCase, - updateSystemThemeUseCase: UpdateSystemThemeUseCase, - fetchWebPageImageDirSizeUseCase: FetchWebPageImageDirSizeUseCase, - clearWebPageImageDirectoryUseCase: ClearWebPageImageDirectoryUseCase - ) { - self.deleteAuthuseCase = deleteAuthUseCase - self.signOutUseCase = signOutUseCase - self.networkConnectivityUseCase = networkConnectivityUseCase - self.systemThemeUseCase = systemThemeUseCase - self.updateSystemThemeUseCase = updateSystemThemeUseCase - self.fetchWebPageImageDirSizeUseCase = fetchWebPageImageDirSizeUseCase - self.clearWebPageImageDirectoryUseCase = clearWebPageImageDirectoryUseCase - setupNetworkObserving() - setupThemeMonitoring() - } - - func reduce(with action: Action) -> [SideEffect] { - var state = self.state - var effects: [SideEffect] = [] - - switch action { - case .networkStatusChanged(let isConnected): - state.isNetworkConnected = isConnected - case .setAlert(let isPresented, let type): - setAlert(&state, isPresented: isPresented, type: type) - case .setDirSize(let value): - state.dirSize = value - case .setLoading(let value): - state.isLoading = value - case .setTheme(let value): - state.theme = value - updateSystemThemeUseCase.execute(value) - case .updateDirSize: - effects = [.fetchWebPageImageDirSize] - case .tapDeleteAuthButton: - effects = [.deleteAuth] - case .tapSignOutButton: - effects = [.signOut] - case .tapRemoveCacheButton: - setAlert(&state, isPresented: true, type: .removeCache) - case .confirmRemoveCache: - setAlert(&state, isPresented: false) - effects = [.clearWebPageImageDirectory] - } - - if self.state != state { self.state = state } - return effects - } - - func run(_ effect: SideEffect) { - switch effect { - case .clearWebPageImageDirectory: - Task { - do { - try await clearWebPageImageDirectoryUseCase.execute() - let dirSize = await fetchWebPageImageDirSizeUseCase.execute() - send(.setDirSize(dirSize)) - } catch { - send(.setAlert(isPresented: true, type: .error)) - } - } - case .deleteAuth: - beginLoading(.delayed) - Task { - do { - send(.setAlert(isPresented: false)) - defer { endLoading(.delayed) } - try await deleteAuthuseCase.execute() - } catch { - send(.setAlert(isPresented: true, type: .error)) - } - } - case .fetchWebPageImageDirSize: - Task { - let dirSize = await fetchWebPageImageDirSizeUseCase.execute() - send(.setDirSize(dirSize)) - } - case .signOut: - beginLoading(.delayed) - Task { - do { - send(.setAlert(isPresented: false)) - defer { endLoading(.delayed) } - try await signOutUseCase.execute() - } catch { - send(.setAlert(isPresented: true, type: .error)) - } - } - } - } -} - -private extension SettingsViewModel { - func setAlert( - _ state: inout State, - isPresented: Bool, - type: AlertType? = nil - ) { - switch type { - case .signOut: - state.alertTitle = String(localized: "settings_alert_sign_out_title") - state.alertMessage = String(localized: "settings_alert_sign_out_message") - case .deleteAuth: - state.alertTitle = String(localized: "settings_alert_delete_account_title") - state.alertMessage = String(localized: "settings_alert_delete_account_message") - case .error: - state.alertTitle = String(localized: "common_error_title") - state.alertMessage = String(localized: "common_error_message") - case .removeCache: - state.alertTitle = String(localized: "settings_alert_clear_temp_title") - state.alertMessage = String(localized: "settings_alert_clear_temp_message") - case .none: - state.alertTitle = "" - state.alertMessage = "" - } - state.showAlert = isPresented - state.alertType = type - } - - func setupThemeMonitoring() { - systemThemeUseCase.observe() - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - self?.send(.setTheme(theme)) - } - .store(in: &cancellables) - } - - func setupNetworkObserving() { - networkConnectivityUseCase.observe() - .receive(on: DispatchQueue.main) - .sink { [weak self] isConnected in - self?.send(.networkStatusChanged(isConnected)) - } - .store(in: &cancellables) - } - - private func beginLoading(_ mode: LoadingState.Mode) { - loadingState.begin(mode: mode) { [weak self] isLoading in - self?.send(.setLoading(isLoading)) - } - } - - private func endLoading(_ mode: LoadingState.Mode) { - loadingState.end(mode: mode) { [weak self] isLoading in - self?.send(.setLoading(isLoading)) - } - } - -} diff --git a/Application/DevLogPresentation/Sources/Settings/ThemeView.swift b/Application/DevLogPresentation/Sources/Settings/ThemeView.swift index a939fdc7..ff3999e0 100644 --- a/Application/DevLogPresentation/Sources/Settings/ThemeView.swift +++ b/Application/DevLogPresentation/Sources/Settings/ThemeView.swift @@ -7,7 +7,6 @@ import SwiftUI import DevLogCore -import DevLogDomain struct ThemeView: View { @Binding var theme: SystemTheme From b448d4dcc3c4ca829eef6d7611706ffb0a118459 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:05:02 +0900 Subject: [PATCH 2/5] =?UTF-8?q?test:=20SettingsFeature=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Settings/SettingsFeatureTestDoubles.swift | 107 ++++++ .../Tests/Settings/SettingsFeatureTests.swift | 331 ++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 Application/DevLogPresentation/Tests/Settings/SettingsFeatureTestDoubles.swift create mode 100644 Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift diff --git a/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTestDoubles.swift b/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTestDoubles.swift new file mode 100644 index 00000000..f17cc968 --- /dev/null +++ b/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTestDoubles.swift @@ -0,0 +1,107 @@ +// +// SettingsFeatureTestDoubles.swift +// DevLogPresentationTests +// +// Created by opfic on 6/12/26. +// + +import Combine +import DevLogCore +import DevLogDomain + +final class DeleteAuthUseCaseSpy: DeleteAuthUseCase { + var error: Error? + private(set) var executeCallCount = 0 + + func execute() async throws { + executeCallCount += 1 + + if let error { + throw error + } + } +} + +final class SignOutUseCaseSpy: SignOutUseCase { + var error: Error? + var shouldSuspend = false + private(set) var executeCallCount = 0 + private var continuation: CheckedContinuation? + private var shouldResume = false + + func execute() async throws { + executeCallCount += 1 + + if shouldSuspend { + await withCheckedContinuation { continuation in + if shouldResume { + shouldResume = false + continuation.resume() + } else { + self.continuation = continuation + } + } + } + + if let error { + throw error + } + } + + func resume() { + guard let continuation else { + shouldResume = true + return + } + + self.continuation = nil + continuation.resume() + } +} + +final class ObserveSystemThemeUseCaseSpy: ObserveSystemThemeUseCase { + let subject = PassthroughSubject() + + func observe() -> AnyPublisher { + subject.eraseToAnyPublisher() + } +} + +final class UpdateSystemThemeUseCaseSpy: UpdateSystemThemeUseCase { + private(set) var themes = [SystemTheme]() + + func execute(_ theme: SystemTheme) { + themes.append(theme) + } +} + +final class FetchWebPageImageDirSizeUseCaseSpy: FetchWebPageImageDirSizeUseCase { + var dirSize: Int64 + private(set) var executeCallCount = 0 + + init(dirSize: Int64 = 0) { + self.dirSize = dirSize + } + + func execute() async -> Int64 { + executeCallCount += 1 + return dirSize + } +} + +final class ClearWebPageImageDirectoryUseCaseSpy: ClearWebPageImageDirectoryUseCase { + var error: Error? + private(set) var executeCallCount = 0 + + func execute() async throws { + executeCallCount += 1 + + if let error { + throw error + } + } +} + +enum SettingsTestError: Error { + case failure +} diff --git a/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift b/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift new file mode 100644 index 00000000..e1e472cb --- /dev/null +++ b/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift @@ -0,0 +1,331 @@ +// +// SettingsFeatureTests.swift +// DevLogPresentationTests +// +// Created by opfic on 6/12/26. +// + +import Testing +import ComposableArchitecture +import DevLogCore +import DevLogDomain +@testable import DevLogPresentation + +@MainActor +struct SettingsFeatureTests { + @Test("네트워크 상태 관찰 결과를 상태에 반영한다") + func 네트워크_상태_관찰_결과를_상태에_반영한다() async { + let networkSpy = ObserveNetworkConnectivityUseCaseSpy() + let adapter = SettingsStoreTestAdapter(networkUseCase: networkSpy) + + await adapter.startObserving() + networkSpy.currentValueSubject.send(false) + await adapter.drainReceivedActions() + + #expect(!adapter.isNetworkConnected) + } + + @Test("테마를 변경하면 상태를 갱신하고 설정 저장을 요청한다") + func 테마를_변경하면_상태를_갱신하고_설정_저장을_요청한다() async { + let updateSpy = UpdateSystemThemeUseCaseSpy() + let adapter = SettingsStoreTestAdapter(updateThemeUseCase: updateSpy) + + await adapter.setTheme(.dark) + + #expect(adapter.theme == .dark) + #expect(updateSpy.themes == [.dark]) + } + + @Test("테마 관찰 결과를 상태에 반영한다") + func 테마_관찰_결과를_상태에_반영한다() async { + let themeSpy = ObserveSystemThemeUseCaseSpy() + let updateSpy = UpdateSystemThemeUseCaseSpy() + let adapter = SettingsStoreTestAdapter( + themeUseCase: themeSpy, + updateThemeUseCase: updateSpy + ) + + await adapter.startObserving() + themeSpy.subject.send(.light) + await adapter.drainReceivedActions() + + #expect(adapter.theme == .light) + #expect(updateSpy.themes == [.light]) + } + + @Test("캐시 크기 조회 결과를 상태에 반영한다") + func 캐시_크기_조회_결과를_상태에_반영한다() async { + let fetchSpy = FetchWebPageImageDirSizeUseCaseSpy(dirSize: 2_048) + let adapter = SettingsStoreTestAdapter(fetchDirSizeUseCase: fetchSpy) + + await adapter.updateDirSize() + + #expect(fetchSpy.executeCallCount == 1) + #expect(adapter.dirSize == 2_048) + } + + @Test("캐시 삭제를 누르면 삭제 확인 알림을 표시한다") + func 캐시_삭제를_누르면_삭제_확인_알림을_표시한다() async { + let adapter = SettingsStoreTestAdapter() + + await adapter.tapRemoveCacheButton() + + #expect(adapter.showAlert) + #expect(adapter.alertType == .removeCache) + #expect(adapter.alertTitle == String(localized: "settings_alert_clear_temp_title")) + #expect(adapter.alertMessage == String(localized: "settings_alert_clear_temp_message")) + } + + @Test("캐시 삭제 확인에 성공하면 캐시를 비우고 크기를 다시 조회한다") + func 캐시_삭제_확인에_성공하면_캐시를_비우고_크기를_다시_조회한다() async { + let clearSpy = ClearWebPageImageDirectoryUseCaseSpy() + let fetchSpy = FetchWebPageImageDirSizeUseCaseSpy(dirSize: 0) + let adapter = SettingsStoreTestAdapter( + fetchDirSizeUseCase: fetchSpy, + clearDirectoryUseCase: clearSpy + ) + + await adapter.tapRemoveCacheButton() + await adapter.confirmRemoveCache() + + #expect(!adapter.showAlert) + #expect(adapter.dirSize == 0) + } + + @Test("캐시 삭제에 실패하면 공통 에러 알림을 표시한다") + func 캐시_삭제에_실패하면_공통_에러_알림을_표시한다() async { + let clearSpy = ClearWebPageImageDirectoryUseCaseSpy() + clearSpy.error = SettingsTestError.failure + let adapter = SettingsStoreTestAdapter(clearDirectoryUseCase: clearSpy) + + await adapter.confirmRemoveCache() + + #expect(adapter.showAlert) + #expect(adapter.alertTitle == String(localized: "common_error_title")) + #expect(adapter.alertMessage == String(localized: "common_error_message")) + } + + @Test("로그아웃 작업이 지연되면 로딩 상태를 표시하고 완료되면 해제한다") + func 로그아웃_작업이_지연되면_로딩_상태를_표시하고_완료되면_해제한다() async { + let signOutSpy = SignOutUseCaseSpy() + signOutSpy.shouldSuspend = true + let adapter = SettingsStoreTestAdapter(signOutUseCase: signOutSpy) + + await adapter.tapSignOutButton() + + #expect(!adapter.isLoading) + + await adapter.advanceDelayedLoading() + + #expect(adapter.isLoading) + + signOutSpy.resume() + await adapter.drainReceivedActions() + + #expect(!adapter.isLoading) + } + + @Test("회원 탈퇴 실패 시 공통 에러 알림을 표시한다") + func 회원_탈퇴_실패_시_공통_에러_알림을_표시한다() async { + let deleteSpy = DeleteAuthUseCaseSpy() + deleteSpy.error = SettingsTestError.failure + let adapter = SettingsStoreTestAdapter(deleteAuthUseCase: deleteSpy) + + await adapter.tapDeleteAuthButton() + + #expect(deleteSpy.executeCallCount == 1) + #expect(adapter.showAlert) + #expect(adapter.alertTitle == String(localized: "common_error_title")) + } +} + +private enum SettingsAlertType { + case signOut + case deleteAuth + case error + case removeCache +} + +@MainActor +private struct SettingsStoreTestAdapter { + private let store: TestStoreOf + private let clock: TestClock + + var theme: SystemTheme { store.state.theme } + var dirSize: Int64 { store.state.dirSize } + var isNetworkConnected: Bool { store.state.isNetworkConnected } + var isLoading: Bool { store.state.isLoading } + var showAlert: Bool { store.state.alert != nil } + var alertTitle: String { + guard let alert = store.state.alert else { return "" } + return String(state: alert.title) + } + var alertMessage: String { + store.state.alert?.message.map { String(state: $0) } ?? "" + } + var alertType: SettingsAlertType? { + guard let type = store.state.alertType else { return nil } + return SettingsAlertType(type) + } + + init( + deleteAuthUseCase: DeleteAuthUseCase = DeleteAuthUseCaseSpy(), + signOutUseCase: SignOutUseCase = SignOutUseCaseSpy(), + networkUseCase: ObserveNetworkConnectivityUseCase = ObserveNetworkConnectivityUseCaseSpy(), + themeUseCase: ObserveSystemThemeUseCase = ObserveSystemThemeUseCaseSpy(), + updateThemeUseCase: UpdateSystemThemeUseCase = UpdateSystemThemeUseCaseSpy(), + fetchDirSizeUseCase: FetchWebPageImageDirSizeUseCase = FetchWebPageImageDirSizeUseCaseSpy(), + clearDirectoryUseCase: ClearWebPageImageDirectoryUseCase = ClearWebPageImageDirectoryUseCaseSpy() + ) { + let clock = TestClock() + self.clock = clock + store = TestStore(initialState: SettingsFeature.State()) { + SettingsFeature() + } withDependencies: { + $0.deleteAuthUseCase = deleteAuthUseCase + $0.signOutUseCase = signOutUseCase + $0.networkConnectivityUseCase = networkUseCase + $0.systemThemeUseCase = themeUseCase + $0.updateSystemThemeUseCase = updateThemeUseCase + $0.fetchWebPageImageDirSizeUseCase = fetchDirSizeUseCase + $0.clearWebPageImageDirectoryUseCase = clearDirectoryUseCase + $0.continuousClock = clock + } + store.exhaustivity = .off(showSkippedAssertions: false) + } + + func startObserving() async { + await store.send(.startObserving) + } + + func setTheme(_ theme: SystemTheme) async { + await store.send(.binding(.set(\.theme, theme))) { + $0.theme = theme + } + } + + func updateDirSize() async { + await store.send(.updateDirSize) + await drainReceivedActions() + } + + func tapRemoveCacheButton() async { + await store.send(.tapRemoveCacheButton) { + $0.alert = expectedSettingsAlert(for: .removeCache) + $0.alertType = .removeCache + } + } + + func confirmRemoveCache() async { + await store.send(.confirmRemoveCache) { + $0.alert = nil + $0.alertType = nil + } + await drainReceivedActions() + } + + func tapSignOutButton() async { + await store.send(.tapSignOutButton) { + $0.alert = nil + $0.alertType = nil + } + await drainReceivedActions() + } + + func tapDeleteAuthButton() async { + await store.send(.tapDeleteAuthButton) { + $0.alert = nil + $0.alertType = nil + } + await drainReceivedActions() + } + + func advanceDelayedLoading() async { + let target = LoadingFeature.Target.default + await clock.advance(by: .milliseconds(300)) + await store.receive(\.loading.delayedLoadingDidBecomeVisible, target) { + $0.loading.scheduledDelayedTargets = [] + $0.loading.visibleDelayedTargets = [target] + $0.loading.visibleTargets = [target] + $0.loading.isLoading = true + } + } + + func drainReceivedActions() async { + await store.skipReceivedActions(strict: false) + await store.skipReceivedActions(strict: false) + await store.skipReceivedActions(strict: false) + await store.skipReceivedActions(strict: false) + } +} + +private extension SettingsAlertType { + init(_ type: SettingsFeature.Action.AlertType) { + switch type { + case .signOut: + self = .signOut + case .deleteAuth: + self = .deleteAuth + case .error: + self = .error + case .removeCache: + self = .removeCache + } + } +} + +private func expectedSettingsAlert( + for type: SettingsFeature.Action.AlertType +) -> AlertState { + switch type { + case .signOut: + return AlertState { + TextState(String(localized: "settings_alert_sign_out_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_cancel")) + } + ButtonState(role: .destructive, action: .tapSignOutButton) { + TextState(String(localized: "common_confirm")) + } + } message: { + TextState(String(localized: "settings_alert_sign_out_message")) + } + case .deleteAuth: + return AlertState { + TextState(String(localized: "settings_alert_delete_account_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_cancel")) + } + ButtonState(role: .destructive, action: .tapDeleteAuthButton) { + TextState(String(localized: "settings_delete_account_action")) + } + } message: { + TextState(String(localized: "settings_alert_delete_account_message")) + } + case .error: + return AlertState { + TextState(String(localized: "common_error_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_close")) + } + } message: { + TextState(String(localized: "common_error_message")) + } + case .removeCache: + return AlertState { + TextState(String(localized: "settings_alert_clear_temp_title")) + } actions: { + ButtonState(role: .cancel) { + TextState(String(localized: "common_cancel")) + } + ButtonState(role: .destructive, action: .confirmRemoveCache) { + TextState(String(localized: "common_confirm")) + } + } message: { + TextState(String(localized: "settings_alert_clear_temp_message")) + } + } +} From 744092ce823717e42533f06757646c61ea8f2957 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:07:49 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20=EB=A7=89=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Settings/SettingsFeature.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift b/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift index b506bb3e..f5c77ae7 100644 --- a/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift +++ b/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift @@ -13,6 +13,11 @@ import Foundation @Reducer struct SettingsFeature { + private enum CancelID: Hashable { + case networkConnectivity + case systemTheme + } + @ObservableState struct State: Equatable { @Presents var alert: AlertState? @@ -247,6 +252,7 @@ private extension SettingsFeature { .receive(on: DispatchQueue.main) .map(Action.networkStatusChanged) } + .cancellable(id: CancelID.networkConnectivity, cancelInFlight: true) } func monitorSystemThemeEffect() -> Effect { @@ -256,6 +262,7 @@ private extension SettingsFeature { .receive(on: DispatchQueue.main) .map { .binding(.set(\.theme, $0)) } } + .cancellable(id: CancelID.systemTheme, cancelInFlight: true) } func updateSystemThemeEffect(_ theme: SystemTheme) -> Effect { From 84d9a0db774f837fa0cd41ffb3b44ff4ba78da66 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:39:20 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test:=20Settings=20alert=20action=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/Settings/SettingsFeatureTests.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift b/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift index e1e472cb..07fabd7c 100644 --- a/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift +++ b/Application/DevLogPresentation/Tests/Settings/SettingsFeatureTests.swift @@ -98,6 +98,7 @@ struct SettingsFeatureTests { clearSpy.error = SettingsTestError.failure let adapter = SettingsStoreTestAdapter(clearDirectoryUseCase: clearSpy) + await adapter.tapRemoveCacheButton() await adapter.confirmRemoveCache() #expect(adapter.showAlert) @@ -217,7 +218,7 @@ private struct SettingsStoreTestAdapter { } func confirmRemoveCache() async { - await store.send(.confirmRemoveCache) { + await store.send(.alert(.presented(.confirmRemoveCache))) { $0.alert = nil $0.alertType = nil } @@ -225,7 +226,11 @@ private struct SettingsStoreTestAdapter { } func tapSignOutButton() async { - await store.send(.tapSignOutButton) { + await store.send(.setAlert(.signOut)) { + $0.alert = expectedSettingsAlert(for: .signOut) + $0.alertType = .signOut + } + await store.send(.alert(.presented(.tapSignOutButton))) { $0.alert = nil $0.alertType = nil } @@ -233,7 +238,11 @@ private struct SettingsStoreTestAdapter { } func tapDeleteAuthButton() async { - await store.send(.tapDeleteAuthButton) { + await store.send(.setAlert(.deleteAuth)) { + $0.alert = expectedSettingsAlert(for: .deleteAuth) + $0.alertType = .deleteAuth + } + await store.send(.alert(.presented(.tapDeleteAuthButton))) { $0.alert = nil $0.alertType = nil } From 3ab870e87f0c13ce94afa19f3878268c3d943677 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:56:44 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20=EC=96=BC=EB=9F=BF=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=95=A1=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Settings/SettingsFeature.swift | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift b/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift index f5c77ae7..54802972 100644 --- a/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift +++ b/Application/DevLogPresentation/Sources/Settings/SettingsFeature.swift @@ -43,10 +43,7 @@ struct SettingsFeature { case setAlert(AlertType) case setDirSize(Int64) case updateDirSize - case tapDeleteAuthButton - case tapSignOutButton case tapRemoveCacheButton - case confirmRemoveCache case loading(LoadingFeature.Action) enum Alert: Equatable { @@ -113,21 +110,9 @@ struct SettingsFeature { state.dirSize = value case .updateDirSize: return fetchWebPageImageDirSizeEffect() - case .tapDeleteAuthButton: - state.alert = nil - state.alertType = nil - return deleteAuthEffect() - case .tapSignOutButton: - state.alert = nil - state.alertType = nil - return signOutEffect() case .tapRemoveCacheButton: state.alert = alertState(for: .removeCache) state.alertType = .removeCache - case .confirmRemoveCache: - state.alert = nil - state.alertType = nil - return clearWebPageImageDirectoryEffect() case .loading: break }