Skip to content

Commit 23d21fa

Browse files
committed
PushNotifiactionSettingsView 로딩 표시 조정
1 parent a5838ac commit 23d21fa

3 files changed

Lines changed: 118 additions & 11 deletions

File tree

Application/DevLogPresentation/Sources/Settings/PushNotificationSettingsFeature.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ import SwiftUI
1212

1313
@Reducer
1414
struct PushNotificationSettingsFeature {
15+
enum ActiveLoadingRow: Equatable {
16+
case enable
17+
case presetTime(hour: Int, minute: Int)
18+
case customTime
19+
}
20+
1521
@ObservableState
1622
struct State: Equatable {
1723
@Presents var alert: AlertState<Never>?
1824
@Presents var timePicker: TimePickerState?
1925
var pushNotificationEnable = false
2026
var viewPushNotificationTime = Date()
27+
var activeLoadingRow: ActiveLoadingRow?
2128
var loading = LoadingFeature.State()
2229

2330
var isLoading: Bool {
@@ -46,6 +53,7 @@ struct PushNotificationSettingsFeature {
4653
case setAlert
4754
case tapCustomTime
4855
case selectPresetTime(Date)
56+
case clearActiveLoadingRow
4957
case loading(LoadingFeature.Action)
5058

5159
enum TimePicker: BindableAction, Equatable {
@@ -68,6 +76,7 @@ struct PushNotificationSettingsFeature {
6876
case .alert:
6977
break
7078
case .binding(\.pushNotificationEnable):
79+
state.activeLoadingRow = .enable
7180
return updatePushNotificationSettingsEffect(settings: Self.settings(from: state))
7281
case .binding(\.viewPushNotificationTime):
7382
let time = state.viewPushNotificationTime
@@ -82,10 +91,12 @@ struct PushNotificationSettingsFeature {
8291
guard let time = state.timePicker?.time else { break }
8392
state.timePicker = nil
8493
state.viewPushNotificationTime = time
94+
state.activeLoadingRow = .customTime
8595
return updatePushNotificationSettingsEffect(settings: Self.settings(from: state))
8696
case .timePicker:
8797
break
8898
case .fetchSettings:
99+
state.activeLoadingRow = .enable
89100
return fetchPushNotificationSettingsEffect()
90101
case .applyFetchedSettings(let settings):
91102
state.pushNotificationEnable = settings.isEnabled
@@ -101,7 +112,10 @@ struct PushNotificationSettingsFeature {
101112
case .selectPresetTime(let date):
102113
state.viewPushNotificationTime = date
103114
state.timePicker?.time = date
115+
state.activeLoadingRow = Self.activeLoadingRow(for: date)
104116
return updatePushNotificationSettingsEffect(settings: Self.settings(from: state))
117+
case .clearActiveLoadingRow:
118+
state.activeLoadingRow = nil
105119
case .loading:
106120
break
107121
}
@@ -156,6 +170,15 @@ private enum UpdatePushSettingsUseCaseKey: DependencyKey {
156170
}
157171
}
158172

173+
extension PushNotificationSettingsFeature {
174+
static func activeLoadingRow(for date: Date) -> ActiveLoadingRow? {
175+
let components = Calendar.current.dateComponents([.hour, .minute], from: date)
176+
guard let hour = components.hour,
177+
let minute = components.minute else { return nil }
178+
return .presetTime(hour: hour, minute: minute)
179+
}
180+
}
181+
159182
private extension PushNotificationSettingsFeature {
160183
func fetchPushNotificationSettingsEffect() -> Effect<Action> {
161184
.run { [fetchPushSettingsUseCase] send in
@@ -164,8 +187,10 @@ private extension PushNotificationSettingsFeature {
164187
let settings = try await fetchPushSettingsUseCase.execute()
165188
await send(.applyFetchedSettings(settings))
166189
await send(.loading(.end(target: .default, mode: .delayed)))
190+
await send(.clearActiveLoadingRow)
167191
} catch {
168192
await send(.loading(.end(target: .default, mode: .delayed)))
193+
await send(.clearActiveLoadingRow)
169194
await send(.setAlert)
170195
}
171196
}
@@ -177,8 +202,10 @@ private extension PushNotificationSettingsFeature {
177202
do {
178203
try await updatePushSettingsUseCase.execute(settings)
179204
await send(.loading(.end(target: .default, mode: .delayed)))
205+
await send(.clearActiveLoadingRow)
180206
} catch {
181207
await send(.loading(.end(target: .default, mode: .delayed)))
208+
await send(.clearActiveLoadingRow)
182209
await send(.setAlert)
183210
await send(.fetchSettings)
184211
}

Application/DevLogPresentation/Sources/Settings/PushNotificationSettingsView.swift

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,35 @@ struct PushNotificationSettingsView: View {
1414
var body: some View {
1515
List {
1616
Section(content: {
17-
Toggle(isOn: $store.pushNotificationEnable) {
17+
HStack {
1818
Text(String(localized: "push_settings_enable"))
19+
Spacer()
20+
if store.isLoading && store.activeLoadingRow == .enable {
21+
ProgressView()
22+
.id(UUID())
23+
} else {
24+
Toggle("", isOn: $store.pushNotificationEnable)
25+
.labelsHidden()
26+
.tint(.blue)
27+
.disabled(store.activeLoadingRow != nil)
28+
}
1929
}
20-
.tint(.blue)
2130
}, footer: {
2231
Text(String(localized: "push_settings_footer"))
2332
})
2433
Section {
2534
ForEach([9, 15, 18, 21], id: \.self) { hour in
2635
if let date = Calendar.current.date(bySettingHour: hour, minute: 0, second: 0, of: Date()) {
36+
let loadingRow = PushNotificationSettingsFeature.activeLoadingRow(for: date)
2737
HStack {
2838
Text(formattedTimeString(date))
2939
Spacer()
30-
if store.pushNotificationHour == hour &&
31-
store.pushNotificationMinute == 0 {
40+
if let loadingRow,
41+
store.isLoading && store.activeLoadingRow == loadingRow {
42+
ProgressView()
43+
.id(UUID())
44+
} else if store.pushNotificationHour == hour &&
45+
store.pushNotificationMinute == 0 {
3246
Image(systemName: "checkmark")
3347
.foregroundStyle(Color.blue)
3448
}
@@ -40,22 +54,26 @@ struct PushNotificationSettingsView: View {
4054
HStack {
4155
Text(String(localized: "push_settings_custom"))
4256
Spacer()
43-
Text(formattedTimeString(store.viewPushNotificationTime))
44-
.foregroundStyle(.secondary)
45-
if store.pushNotificationMinute != 0 {
46-
Image(systemName: "checkmark")
47-
.foregroundStyle(Color.blue)
57+
if store.isLoading && store.activeLoadingRow == .customTime {
58+
ProgressView()
59+
.id(UUID())
60+
} else {
61+
Text(formattedTimeString(store.viewPushNotificationTime))
62+
.foregroundStyle(.secondary)
63+
if store.pushNotificationMinute != 0 {
64+
Image(systemName: "checkmark")
65+
.foregroundStyle(Color.blue)
66+
}
4867
}
4968
}
5069
.contentShape(Rectangle())
5170
.onTapGesture { store.send(.tapCustomTime) }
5271
}
53-
.disabled(!store.pushNotificationEnable)
72+
.disabled(!store.pushNotificationEnable || store.activeLoadingRow != nil)
5473
.opacity(store.pushNotificationEnable ? 1.0 : 0.2)
5574
}
5675
.listStyle(.insetGrouped)
5776
.navigationTitle(String(localized: "nav_push_settings"))
58-
.overlay { if store.isLoading { LoadingView() } }
5977
.onAppear { store.send(.fetchSettings) }
6078
.alert($store.scope(state: \.alert, action: \.alert))
6179
.sheet(item: $store.scope(state: \.timePicker, action: \.timePicker)) { store in

Application/DevLogPresentation/Tests/Settings/PushNotificationSettingsFeatureTests.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// Created by opfic on 6/12/26.
66
//
77

8+
// swiftlint:disable file_length
9+
810
import Testing
911
import ComposableArchitecture
1012
import Foundation
@@ -143,14 +145,48 @@ struct PushNotificationSettingsFeatureTests {
143145
await adapter.receiveDelayedLoading()
144146

145147
#expect(adapter.isLoading)
148+
#expect(adapter.activeLoadingRow == .enable)
146149

147150
fetchSpy.resume()
148151
await adapter.drainReceivedActions()
149152

150153
#expect(!adapter.isLoading)
154+
#expect(adapter.activeLoadingRow == nil)
151155
#expect(adapter.pushNotificationHour == 9)
152156
}
153157

158+
@Test("프리셋 시간 업데이트가 지연되면 해당 시간 row에 로딩 상태를 표시한다")
159+
func 프리셋_시간_업데이트가_지연되면_해당_시간_row에_로딩_상태를_표시한다() async {
160+
let clock = TestClock()
161+
let updateSpy = UpdatePushSettingsUseCaseSpy()
162+
updateSpy.shouldSuspend = true
163+
let adapter = PushNotificationSettingsStoreTestAdapter(
164+
updateUseCase: updateSpy,
165+
configureDependencies: {
166+
$0.continuousClock = clock
167+
}
168+
)
169+
let date = makeDate(hour: 15, minute: 0)
170+
171+
await adapter.selectPresetTime(date)
172+
173+
#expect(updateSpy.executeCallCount == 1)
174+
#expect(adapter.activeLoadingRow == .presetTime(hour: 15, minute: 0))
175+
#expect(!adapter.isLoading)
176+
177+
await clock.advance(by: .milliseconds(300))
178+
await adapter.receiveDelayedLoading()
179+
180+
#expect(adapter.isLoading)
181+
#expect(adapter.activeLoadingRow == .presetTime(hour: 15, minute: 0))
182+
183+
updateSpy.resume()
184+
await adapter.drainReceivedActions()
185+
186+
#expect(!adapter.isLoading)
187+
#expect(adapter.activeLoadingRow == nil)
188+
}
189+
154190
@Test("푸시 설정 조회에 실패하면 공통 에러 알림을 표시한다")
155191
func 푸시_설정_조회에_실패하면_공통_에러_알림을_표시한다() async {
156192
let fetchSpy = FetchPushSettingsUseCaseSpy()
@@ -193,6 +229,7 @@ private struct PushNotificationSettingsStoreTestAdapter {
193229
var sheetPushNotificationTime: Date { store.state.timePicker?.time ?? store.state.viewPushNotificationTime }
194230
var showTimePicker: Bool { store.state.timePicker != nil }
195231
var isLoading: Bool { store.state.isLoading }
232+
var activeLoadingRow: PushNotificationSettingsFeature.ActiveLoadingRow? { store.state.activeLoadingRow }
196233
var sheetHeight: CGFloat { store.state.timePicker?.height ?? .pi }
197234
var alert: AlertState<Never>? { store.state.alert }
198235
var pushNotificationHour: Int { store.state.pushNotificationHour }
@@ -349,15 +386,40 @@ private final class FetchPushSettingsUseCaseSpy: FetchPushSettingsUseCase {
349386

350387
private final class UpdatePushSettingsUseCaseSpy: UpdatePushSettingsUseCase {
351388
var error: Error?
389+
var shouldSuspend = false
352390
private(set) var executeCallCount = 0
391+
private var continuation: CheckedContinuation<Void, Never>?
392+
private var shouldResume = false
353393

354394
func execute(_: PushNotificationSettings) async throws {
355395
executeCallCount += 1
396+
397+
if shouldSuspend {
398+
await withCheckedContinuation { continuation in
399+
if shouldResume {
400+
shouldResume = false
401+
continuation.resume()
402+
} else {
403+
self.continuation = continuation
404+
}
405+
}
406+
}
407+
356408
if let error {
357409
self.error = nil
358410
throw error
359411
}
360412
}
413+
414+
func resume() {
415+
guard let continuation else {
416+
shouldResume = true
417+
return
418+
}
419+
420+
self.continuation = nil
421+
continuation.resume()
422+
}
361423
}
362424

363425
private enum PushNotificationSettingsTestError: Error {

0 commit comments

Comments
 (0)