Skip to content

Commit 48e3bd2

Browse files
committed
refactor: PushNotificationSettingsView TCA 적용
1 parent ad09ae7 commit 48e3bd2

6 files changed

Lines changed: 271 additions & 242 deletions

File tree

Application/DevLogPresentation/Sources/Main/MainView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ struct MainView: View {
372372
)
373373
case .pushNotification:
374374
PushNotificationSettingsView(
375-
viewModel: profileViewCoordinator.makePushNotificationSettingsViewModel()
375+
store: profileViewCoordinator.makePushNotificationSettingsStore()
376376
)
377377
case .account:
378378
AccountView(store: profileViewCoordinator.makeAccountStore())

Application/DevLogPresentation/Sources/Profile/ProfileView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ struct ProfileView: View {
170170
)
171171
)
172172
case .pushNotification:
173-
PushNotificationSettingsView(viewModel: coordinator.makePushNotificationSettingsViewModel())
173+
PushNotificationSettingsView(store: coordinator.makePushNotificationSettingsStore())
174174
case .account:
175175
AccountView(store: coordinator.makeAccountStore())
176176
}

Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ final class ProfileViewCoordinator {
5454
}
5555
}
5656

57-
func makePushNotificationSettingsViewModel() -> PushNotificationSettingsViewModel {
58-
PushNotificationSettingsViewModel(
59-
fetchPushSettingsUseCase: container.resolve(FetchPushSettingsUseCase.self),
60-
updatePushSettingsUseCase: container.resolve(UpdatePushSettingsUseCase.self)
61-
)
57+
func makePushNotificationSettingsStore() -> StoreOf<PushNotificationSettingsFeature> {
58+
Store(initialState: PushNotificationSettingsFeature.State()) {
59+
PushNotificationSettingsFeature()
60+
} withDependencies: {
61+
$0.fetchPushSettingsUseCase = self.container.resolve(FetchPushSettingsUseCase.self)
62+
$0.updatePushSettingsUseCase = self.container.resolve(UpdatePushSettingsUseCase.self)
63+
}
6264
}
6365

6466
func makeTodoDetailStore(todoId: String) -> StoreOf<TodoDetailFeature> {
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//
2+
// PushNotificationSettingsFeature.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/12/26.
6+
//
7+
8+
import ComposableArchitecture
9+
import DevLogDomain
10+
import Foundation
11+
import SwiftUI
12+
13+
@Reducer
14+
struct PushNotificationSettingsFeature {
15+
@ObservableState
16+
struct State: Equatable {
17+
@Presents var alert: AlertState<Never>?
18+
@Presents var timePicker: TimePickerState?
19+
var pushNotificationEnable = false
20+
var viewPushNotificationTime = Date()
21+
var loading = LoadingFeature.State()
22+
23+
var isLoading: Bool {
24+
loading.isLoading
25+
}
26+
var pushNotificationHour: Int {
27+
Calendar.current.component(.hour, from: viewPushNotificationTime)
28+
}
29+
var pushNotificationMinute: Int {
30+
Calendar.current.component(.minute, from: viewPushNotificationTime)
31+
}
32+
}
33+
34+
@ObservableState
35+
struct TimePickerState: Equatable {
36+
var time: Date
37+
var height: CGFloat = .pi
38+
}
39+
40+
enum Action: BindableAction {
41+
case alert(PresentationAction<Never>)
42+
case binding(BindingAction<State>)
43+
case timePicker(PresentationAction<TimePicker>)
44+
case fetchSettings
45+
case setAlert
46+
case tapCustomTime
47+
case selectPresetTime(Date)
48+
case loading(LoadingFeature.Action)
49+
50+
enum TimePicker: BindableAction, Equatable {
51+
case binding(BindingAction<TimePickerState>)
52+
case tapCloseButton
53+
case tapDoneButton
54+
}
55+
}
56+
57+
@Dependency(\.fetchPushSettingsUseCase) var fetchPushSettingsUseCase
58+
@Dependency(\.updatePushSettingsUseCase) var updatePushSettingsUseCase
59+
60+
var body: some ReducerOf<Self> {
61+
Scope(state: \.loading, action: \.loading) {
62+
LoadingFeature()
63+
}
64+
BindingReducer()
65+
Reduce { state, action in
66+
switch action {
67+
case .alert:
68+
break
69+
case .binding(\.pushNotificationEnable):
70+
return updatePushNotificationSettingsEffect(settings: settings(from: state))
71+
case .binding(\.viewPushNotificationTime):
72+
let time = state.viewPushNotificationTime
73+
state.timePicker?.time = time
74+
case .binding:
75+
break
76+
case .timePicker(.dismiss):
77+
state.timePicker = nil
78+
case .timePicker(.presented(.tapCloseButton)):
79+
state.timePicker = nil
80+
case .timePicker(.presented(.tapDoneButton)):
81+
guard let time = state.timePicker?.time else { break }
82+
state.timePicker = nil
83+
state.viewPushNotificationTime = time
84+
return updatePushNotificationSettingsEffect(settings: settings(from: state))
85+
case .timePicker:
86+
break
87+
case .fetchSettings:
88+
return fetchPushNotificationSettingsEffect()
89+
case .setAlert:
90+
state.alert = alertState()
91+
case .tapCustomTime:
92+
state.timePicker = TimePickerState(time: state.viewPushNotificationTime)
93+
case .selectPresetTime(let date):
94+
state.viewPushNotificationTime = date
95+
state.timePicker?.time = date
96+
return updatePushNotificationSettingsEffect(settings: settings(from: state))
97+
case .loading:
98+
break
99+
}
100+
101+
return .none
102+
}
103+
.ifLet(\.$alert, action: \.alert)
104+
.ifLet(\.$timePicker, action: \.timePicker) {
105+
TimePickerFeature()
106+
}
107+
}
108+
}
109+
110+
private struct TimePickerFeature: Reducer {
111+
typealias State = PushNotificationSettingsFeature.TimePickerState
112+
typealias Action = PushNotificationSettingsFeature.Action.TimePicker
113+
114+
var body: some ReducerOf<Self> {
115+
BindingReducer()
116+
}
117+
}
118+
119+
extension DependencyValues {
120+
var fetchPushSettingsUseCase: FetchPushSettingsUseCase {
121+
get { self[FetchPushSettingsUseCaseKey.self] }
122+
set { self[FetchPushSettingsUseCaseKey.self] = newValue }
123+
}
124+
125+
var updatePushSettingsUseCase: UpdatePushSettingsUseCase {
126+
get { self[UpdatePushSettingsUseCaseKey.self] }
127+
set { self[UpdatePushSettingsUseCaseKey.self] = newValue }
128+
}
129+
}
130+
131+
private enum FetchPushSettingsUseCaseKey: DependencyKey {
132+
static var liveValue: FetchPushSettingsUseCase {
133+
preconditionFailure("FetchPushSettingsUseCase must be provided.")
134+
}
135+
136+
static var testValue: FetchPushSettingsUseCase {
137+
liveValue
138+
}
139+
}
140+
141+
private enum UpdatePushSettingsUseCaseKey: DependencyKey {
142+
static var liveValue: UpdatePushSettingsUseCase {
143+
preconditionFailure("UpdatePushSettingsUseCase must be provided.")
144+
}
145+
146+
static var testValue: UpdatePushSettingsUseCase {
147+
liveValue
148+
}
149+
}
150+
151+
private extension PushNotificationSettingsFeature {
152+
func fetchPushNotificationSettingsEffect() -> Effect<Action> {
153+
.run { [fetchPushSettingsUseCase] send in
154+
await send(.loading(.begin(target: .default, mode: .delayed)))
155+
do {
156+
let settings = try await fetchPushSettingsUseCase.execute()
157+
await send(.binding(.set(\.pushNotificationEnable, settings.isEnabled)))
158+
if let hour = settings.scheduledTime.hour,
159+
let minute = settings.scheduledTime.minute,
160+
let date = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) {
161+
await send(.binding(.set(\.viewPushNotificationTime, date)))
162+
}
163+
await send(.loading(.end(target: .default, mode: .delayed)))
164+
} catch {
165+
await send(.loading(.end(target: .default, mode: .delayed)))
166+
await send(.setAlert)
167+
}
168+
}
169+
}
170+
171+
func updatePushNotificationSettingsEffect(settings: PushNotificationSettings) -> Effect<Action> {
172+
.run { [updatePushSettingsUseCase] send in
173+
await send(.loading(.begin(target: .default, mode: .delayed)))
174+
do {
175+
try await updatePushSettingsUseCase.execute(settings)
176+
await send(.loading(.end(target: .default, mode: .delayed)))
177+
} catch {
178+
await send(.loading(.end(target: .default, mode: .delayed)))
179+
await send(.setAlert)
180+
await send(.fetchSettings)
181+
}
182+
}
183+
}
184+
185+
func settings(from state: State) -> PushNotificationSettings {
186+
let date = state.timePicker?.time ?? state.viewPushNotificationTime
187+
let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: date)
188+
return PushNotificationSettings(
189+
isEnabled: state.pushNotificationEnable,
190+
scheduledTime: dateComponents
191+
)
192+
}
193+
194+
func alertState() -> AlertState<Never> {
195+
AlertState {
196+
TextState(String(localized: "common_error_title"))
197+
} actions: {
198+
ButtonState(role: .cancel) {
199+
TextState(String(localized: "common_close"))
200+
}
201+
} message: {
202+
TextState(String(localized: "common_error_message"))
203+
}
204+
}
205+
}

0 commit comments

Comments
 (0)