Skip to content

Commit 9734b65

Browse files
authored
[#556] LoginVIew에서 signIn이 다 되기 전 LoadingView가 사라지는 현상을 해결한다 (#563)
* refactor: Binding 적용 * fix 로그인 로딩 상태 유지 * refactor: 별도 Bindable 처리 제거 * fix: 바인딩 코드로 수정 * test: 로직 변경에 따른 테스트 코드 수정 * feat: AlertState 적용
1 parent bdcd1bc commit 9734b65

6 files changed

Lines changed: 83 additions & 89 deletions

File tree

Application/DevLogApp/Sources/App/DevLogApp.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ struct DevLogApp: App {
3030
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
3131
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self),
3232
trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self),
33-
signInUseCase: container.resolve(SignInUseCase.self),
3433
widgetURLTab: { MainTab(widgetURL: $0) },
3534
windowEvent: windowEvent,
3635
pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(),

Application/DevLogPresentation/Sources/Login/LoginFeature.swift

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,13 @@ import Foundation
1313
struct LoginFeature {
1414
@ObservableState
1515
struct State: Equatable {
16+
@Presents var alert: AlertState<Never>?
1617
var isLoading = false
17-
var showAlert = false
18-
var alertType: AlertType?
19-
var alertTitle = ""
20-
var alertMessage = ""
2118
}
2219

2320
enum Action {
24-
case setAlert(Bool, AlertType? = nil)
21+
case alert(PresentationAction<Never>)
2522
case tapSignInButton(AuthProvider)
26-
case signInSucceeded
2723
case signInFailed(AlertType)
2824
case signInCancelled
2925
}
@@ -38,14 +34,13 @@ struct LoginFeature {
3834
var body: some ReducerOf<Self> {
3935
Reduce { state, action in
4036
switch action {
41-
case .setAlert(let isPresented, let alertType):
42-
setAlert(&state, isPresented: isPresented, alertType: alertType)
37+
case .alert:
38+
break
4339
case .tapSignInButton(let provider):
4440
state.isLoading = true
4541
return .run { [signInUseCase] send in
4642
do {
4743
try await signInUseCase.execute(provider)
48-
await send(.signInSucceeded)
4944
} catch {
5045
if error.isSocialLoginCancelled {
5146
await send(.signInCancelled)
@@ -54,14 +49,15 @@ struct LoginFeature {
5449
await send(.signInFailed(alertType(for: error)))
5550
}
5651
}
57-
case .signInSucceeded, .signInCancelled:
52+
case .signInCancelled:
5853
state.isLoading = false
5954
case .signInFailed(let alertType):
6055
state.isLoading = false
61-
setAlert(&state, isPresented: true, alertType: alertType)
56+
state.alert = alertState(for: alertType)
6257
}
6358
return .none
6459
}
60+
.ifLet(\.$alert, action: \.alert)
6561
}
6662
}
6763

@@ -95,24 +91,28 @@ extension DependencyValues {
9591
}
9692

9793
private extension LoginFeature {
98-
func setAlert(
99-
_ state: inout State,
100-
isPresented: Bool,
101-
alertType: AlertType?
102-
) {
94+
func alertState(for alertType: AlertType) -> AlertState<Never> {
95+
let title: String
96+
let message: String
97+
10398
switch alertType {
10499
case .emailUnavailable:
105-
state.alertTitle = String(localized: "login_alert_email_unavailable_title")
106-
state.alertMessage = String(localized: "login_alert_email_unavailable_message")
100+
title = String(localized: "login_alert_email_unavailable_title")
101+
message = String(localized: "login_alert_email_unavailable_message")
107102
case .error:
108-
state.alertTitle = String(localized: "common_error_title")
109-
state.alertMessage = String(localized: "common_error_message")
110-
case .none:
111-
state.alertTitle = ""
112-
state.alertMessage = ""
103+
title = String(localized: "common_error_title")
104+
message = String(localized: "common_error_message")
105+
}
106+
107+
return AlertState {
108+
TextState(title)
109+
} actions: {
110+
ButtonState(role: .cancel) {
111+
TextState(String(localized: "common_close"))
112+
}
113+
} message: {
114+
TextState(message)
113115
}
114-
state.showAlert = isPresented
115-
state.alertType = alertType
116116
}
117117

118118
func alertType(for error: Error) -> AlertType {

Application/DevLogPresentation/Sources/Login/LoginView.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@
77

88
import SwiftUI
99
import ComposableArchitecture
10+
import DevLogDomain
1011

1112
struct LoginView: View {
1213
@Environment(\.colorScheme) var colorScheme
1314
@Environment(\.sceneWidth) var sceneWidth
14-
let store: StoreOf<LoginFeature>
15+
@State private var store: StoreOf<LoginFeature>
16+
17+
init(signInUseCase: SignInUseCase) {
18+
self._store = State(initialValue: Store(
19+
initialState: LoginFeature.State()
20+
) {
21+
LoginFeature()
22+
} withDependencies: {
23+
$0.signInUseCase = .live(signInUseCase)
24+
})
25+
}
1526

1627
var body: some View {
1728
ZStack {
@@ -46,13 +57,6 @@ struct LoginView: View {
4657
LoadingView()
4758
}
4859
}
49-
.alert(store.alertTitle, isPresented: Binding(
50-
get: { store.showAlert },
51-
set: { store.send(.setAlert($0)) }
52-
)) {
53-
Button(String(localized: "common_close"), role: .cancel) { }
54-
} message: {
55-
Text(store.alertMessage)
56-
}
60+
.alert($store.scope(state: \.alert, action: \.alert))
5761
}
5862
}

Application/DevLogPresentation/Sources/Root/RootView.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ public struct RootView: View {
1616
@State var viewModel: RootViewModel
1717
@State private var selectedRoute: Route?
1818
@State private var selectedMainTab = MainTab.home
19-
private let loginStore: StoreOf<LoginFeature>
2019
private let widgetURLTab: (URL) -> MainTab?
2120
private let windowEvent: TodoEditorWindowEvent
2221
private let pushNotificationTodoIdPublisher: AnyPublisher<String, Never>
@@ -27,7 +26,6 @@ public struct RootView: View {
2726
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
2827
systemThemeUseCase: ObserveSystemThemeUseCase,
2928
trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase,
30-
signInUseCase: SignInUseCase,
3129
widgetURLTab: @escaping (URL) -> MainTab?,
3230
windowEvent: TodoEditorWindowEvent,
3331
pushNotificationTodoIdPublisher: AnyPublisher<String, Never>,
@@ -39,13 +37,6 @@ public struct RootView: View {
3937
systemThemeUseCase: systemThemeUseCase,
4038
trackAnalyticsEventUseCase: trackAnalyticsEventUseCase
4139
))
42-
self.loginStore = Store(
43-
initialState: LoginFeature.State()
44-
) {
45-
LoginFeature()
46-
} withDependencies: {
47-
$0.signInUseCase = .live(signInUseCase)
48-
}
4940
self.widgetURLTab = widgetURLTab
5041
self.windowEvent = windowEvent
5142
self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher
@@ -63,7 +54,7 @@ public struct RootView: View {
6354
selectedTab: $selectedMainTab
6455
)
6556
} else {
66-
LoginView(store: loginStore)
57+
LoginView(signInUseCase: container.resolve(SignInUseCase.self))
6758
}
6859
}
6960
}

Application/DevLogPresentation/Tests/Login/LoginFeatureTests.swift

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ struct LoginFeatureTests {
2727
#expect(spy.calledProviders == [.github])
2828
}
2929

30-
@Test("로그인 요청 중에는 로딩 상태가 켜지고 요청이 끝나면 꺼진다")
31-
func 로그인_요청_중에는_로딩_상태가_켜지고_요청이_끝나면_꺼진다() async {
30+
@Test("로그인 성공 후에도 메인 화면 전환 전까지 로딩 상태를 유지한다")
31+
func 로그인_성공_후에도_메인_화면_전환_전까지_로딩_상태를_유지한다() async {
3232
let spy = SignInUseCaseSpy()
3333
spy.shouldSuspend = true
3434
let driver = LoginTestDriver(useCase: spy)
@@ -44,10 +44,10 @@ struct LoginFeatureTests {
4444
spy.resume()
4545

4646
await waitUntil {
47-
!driver.isLoading
47+
spy.successfulProviders == [.google]
4848
}
4949

50-
#expect(!driver.isLoading)
50+
#expect(driver.isLoading)
5151
}
5252

5353
@Test("로그인 실패 후에도 로딩 상태가 꺼진다")
@@ -68,7 +68,7 @@ struct LoginFeatureTests {
6868
spy.resume()
6969

7070
await waitUntil {
71-
!driver.isLoading && driver.showAlert
71+
!driver.isLoading && driver.hasAlert
7272
}
7373

7474
#expect(!driver.isLoading)
@@ -83,12 +83,13 @@ struct LoginFeatureTests {
8383
driver.tapSignInButton(.google)
8484

8585
await waitUntil {
86-
driver.showAlert
86+
driver.hasAlert
8787
}
8888

89-
#expect(driver.alertKind == .emailUnavailable)
90-
#expect(driver.alertTitle == String(localized: "login_alert_email_unavailable_title"))
91-
#expect(driver.alertMessage == String(localized: "login_alert_email_unavailable_message"))
89+
#expect(driver.alert == expectedAlert(
90+
title: String(localized: "login_alert_email_unavailable_title"),
91+
message: String(localized: "login_alert_email_unavailable_message")
92+
))
9293
}
9394

9495
@Test("일반 로그인 에러가 발생하면 공통 에러 알림을 표시한다")
@@ -100,12 +101,13 @@ struct LoginFeatureTests {
100101
driver.tapSignInButton(.apple)
101102

102103
await waitUntil {
103-
driver.showAlert
104+
driver.hasAlert
104105
}
105106

106-
#expect(driver.alertKind == .error)
107-
#expect(driver.alertTitle == String(localized: "common_error_title"))
108-
#expect(driver.alertMessage == String(localized: "common_error_message"))
107+
#expect(driver.alert == expectedAlert(
108+
title: String(localized: "common_error_title"),
109+
message: String(localized: "common_error_message")
110+
))
109111
}
110112

111113
@Test("소셜 로그인 취소 에러가 발생하면 알림을 표시하지 않는다")
@@ -121,9 +123,7 @@ struct LoginFeatureTests {
121123
}
122124

123125
#expect(!driver.showAlert)
124-
#expect(driver.alertKind == nil)
125-
#expect(driver.alertTitle.isEmpty)
126-
#expect(driver.alertMessage.isEmpty)
126+
#expect(driver.alert == nil)
127127
}
128128

129129
@Test("알림을 닫으면 알림 상태와 문구가 초기화된다")
@@ -135,23 +135,16 @@ struct LoginFeatureTests {
135135
driver.tapSignInButton(.google)
136136

137137
await waitUntil {
138-
driver.showAlert
138+
driver.hasAlert
139139
}
140140

141-
driver.setAlert(false)
141+
driver.dismissAlert()
142142

143143
#expect(!driver.showAlert)
144-
#expect(driver.alertKind == nil)
145-
#expect(driver.alertTitle.isEmpty)
146-
#expect(driver.alertMessage.isEmpty)
144+
#expect(driver.alert == nil)
147145
}
148146
}
149147

150-
private enum LoginAlertKind: Equatable {
151-
case emailUnavailable
152-
case error
153-
}
154-
155148
@MainActor
156149
private struct LoginTestDriver {
157150
private let feature: StoreOf<LoginFeature>
@@ -161,26 +154,15 @@ private struct LoginTestDriver {
161154
}
162155

163156
var showAlert: Bool {
164-
feature.state.showAlert
157+
hasAlert
165158
}
166159

167-
var alertKind: LoginAlertKind? {
168-
switch feature.state.alertType {
169-
case .emailUnavailable:
170-
return .emailUnavailable
171-
case .error:
172-
return .error
173-
case .none:
174-
return nil
175-
}
160+
var hasAlert: Bool {
161+
alert != nil
176162
}
177163

178-
var alertTitle: String {
179-
feature.state.alertTitle
180-
}
181-
182-
var alertMessage: String {
183-
feature.state.alertMessage
164+
var alert: AlertState<Never>? {
165+
feature.state.alert
184166
}
185167

186168
init(useCase: SignInUseCase) {
@@ -197,7 +179,22 @@ private struct LoginTestDriver {
197179
feature.send(.tapSignInButton(provider))
198180
}
199181

200-
func setAlert(_ isPresented: Bool) {
201-
feature.send(.setAlert(isPresented))
182+
func dismissAlert() {
183+
feature.send(.alert(.dismiss))
184+
}
185+
}
186+
187+
private func expectedAlert(
188+
title: String,
189+
message: String
190+
) -> AlertState<Never> {
191+
AlertState {
192+
TextState(title)
193+
} actions: {
194+
ButtonState(role: .cancel) {
195+
TextState(String(localized: "common_close"))
196+
}
197+
} message: {
198+
TextState(message)
202199
}
203200
}

Application/DevLogPresentation/Tests/Support/TestSupport.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ final class SignInUseCaseSpy: SignInUseCase {
5454
var error: Error?
5555
var shouldSuspend = false
5656
private(set) var calledProviders: [AuthProvider] = []
57+
private(set) var successfulProviders = [AuthProvider]()
5758
private var continuation: CheckedContinuation<Void, Never>?
5859
private var shouldResume = false
5960

@@ -74,6 +75,8 @@ final class SignInUseCaseSpy: SignInUseCase {
7475
if let error {
7576
throw error
7677
}
78+
79+
successfulProviders.append(provider)
7780
}
7881

7982
func resume() {

0 commit comments

Comments
 (0)