Skip to content

Commit 6b6e953

Browse files
authored
[#49 NWpathConnectivityProvider을 사용하여 네트워크 연결성을 관리한다 (#76)
* refactor: AsyncStream을 CurrentValueSubject 기반 Publisher을 방출하도록 개선 * refactor: RootView에서 LoginViewModel 생성 형태 제거 * style: 불필요 코드 스니펫 제거 * feat: 얼럿 추가 * refactor: LoginView에서 쓰는 것만 사용하도록 개선 * fix: UI 요소가 백그라운드에서 변경되는 현상 해결
1 parent 1283855 commit 6b6e953

9 files changed

Lines changed: 223 additions & 106 deletions

File tree

DevLog/App/DevLogApp.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ struct DevLogApp: App {
1919

2020
var body: some Scene {
2121
WindowGroup {
22-
RootView(viewModel: LoginViewModel(
23-
signInUseCase: container.resolve(SignInUseCase.self),
24-
signOutUseCase: container.resolve(SignOutUseCase.self),
25-
sessionUseCase: container.resolve(AuthSessionUseCase.self)
26-
))
22+
RootView(
23+
viewModel: RootViewModel(
24+
sessionUseCase: container.resolve(AuthSessionUseCase.self),
25+
signOutUseCase: container.resolve(SignOutUseCase.self)
26+
))
2727
.preferredColorScheme(theme.colorScheme)
2828
}
2929
}

DevLog/App/RootView.swift

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@
88
import SwiftUI
99

1010
struct RootView: View {
11-
@AppStorage("isFirstLaunch") var isFirstLaunch = true // 앱을 최초 설치했을 때 기존 로그인 세션이 남아있으면 자동 로그인됨을 막음
12-
@StateObject var viewModel: LoginViewModel
11+
@Environment(\.diContainer) var container: DIContainer
12+
@StateObject var viewModel: RootViewModel
1313

1414
var body: some View {
1515
ZStack {
1616
Color(UIColor.systemGroupedBackground).ignoresSafeArea()
1717
if let signIn = viewModel.state.signIn {
18-
if signIn && !isFirstLaunch {
18+
if signIn && !viewModel.state.isFirstLaunch {
1919
MainView()
2020
} else {
21-
LoginView(viewModel: viewModel)
21+
LoginView(viewModel: LoginViewModel(
22+
signInUseCase: container.resolve(SignInUseCase.self),
23+
signOutUseCase: container.resolve(SignOutUseCase.self),
24+
sessionUseCase: container.resolve(AuthSessionUseCase.self))
25+
)
2226
.onAppear {
23-
if isFirstLaunch {
24-
isFirstLaunch = false
27+
if viewModel.state.isFirstLaunch {
28+
viewModel.send(.setFirstLaunch(false))
2529
viewModel.send(.signOutAuto)
2630
}
2731
}
@@ -30,31 +34,24 @@ struct RootView: View {
3034
Color.clear.onAppear {
3135
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
3236
if viewModel.state.signIn == nil {
33-
isFirstLaunch = true
37+
viewModel.send(.setFirstLaunch(true))
3438
viewModel.send(.signOutAuto)
3539
}
3640
}
3741
}
3842
}
39-
if viewModel.state.isLoading {
40-
LoadingView()
41-
}
4243
}
43-
.alert("네트워크 문제", isPresented: Binding(
44-
get: { viewModel.state.showToast },
45-
set: { _, _ in }
44+
.alert(viewModel.state.alertTitle, isPresented: Binding(
45+
get: { viewModel.state.showAlert },
46+
set: { viewModel.send(.setAlert($0)) }
4647
)) {
47-
Button(role: .cancel, action: {
48-
viewModel.send(.tapCloseToast)
49-
}) {
50-
Text("확인")
51-
}
48+
Button("확인", role: .cancel) { }
5249
} message: {
53-
Text(viewModel.state.toastMessage)
50+
Text(viewModel.state.alertMessage)
5451
}
55-
.onChange(of: isFirstLaunch) { _ in
56-
if isFirstLaunch {
57-
isFirstLaunch = false
52+
.onChange(of: viewModel.state.isFirstLaunch) { newValue in
53+
if newValue {
54+
viewModel.send(.setFirstLaunch(false))
5855
viewModel.send(.signOutAuto)
5956
}
6057
}

DevLog/Domain/UseCase/Auth/SignIn/SignInUseCaseImpl.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ final class SignInUseCaseImpl: SignInUseCase {
1313
}
1414

1515
func execute(_ provider: AuthProvider) async throws {
16-
return try await repository.signIn(provider)
16+
try await repository.signIn(provider)
1717
}
1818
}

DevLog/Domain/UseCase/WebPage/Upsert/DeleteWebPageUseCaseImpl.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,3 @@ final class DeleteWebPageUseCaseImpl: DeleteWebPageUseCase {
1616
try await repository.delete(urlString)
1717
}
1818
}
19-

DevLog/Infra/Service/NWPathConnectivityProvider.swift

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,35 @@
55
// Created by 최윤진 on 11/2/25.
66
//
77

8-
import Foundation
98
import Network
9+
import Combine
1010

1111
final class NWPathConnectivityProvider {
1212
private let networkPathMonitor = NWPathMonitor()
1313
private let monitoringQueue = DispatchQueue(label: "NWPathConnectivityProviderQueue")
14-
15-
private var isConnectedValue: Bool
16-
private var connectivityContinuations: [UUID: AsyncStream<Bool>.Continuation] = [:]
17-
18-
init() {
19-
self.isConnectedValue = (networkPathMonitor.currentPath.status == .satisfied)
20-
21-
self.networkPathMonitor.pathUpdateHandler = { [weak self] path in
22-
let connected = (path.status == .satisfied)
23-
Task { @MainActor in
24-
self?.handlePathStatusChange(isConnected: connected)
25-
}
26-
}
27-
self.networkPathMonitor.start(queue: monitoringQueue)
28-
}
29-
30-
deinit {
31-
self.networkPathMonitor.cancel()
32-
self.connectivityContinuations.values.forEach { $0.finish() }
33-
self.connectivityContinuations.removeAll()
14+
private let isConnectedSubject = CurrentValueSubject<Bool, Never>(false)
15+
16+
var isConnectedPublisher: AnyPublisher<Bool, Never> {
17+
isConnectedSubject.eraseToAnyPublisher()
3418
}
35-
19+
3620
var isConnected: Bool {
37-
self.isConnectedValue
21+
isConnectedSubject.value
3822
}
3923

40-
func connectivityStream() -> AsyncStream<Bool> {
41-
let identifier = UUID()
42-
return AsyncStream(bufferingPolicy: .bufferingNewest(1)) { [weak self] continuation in
43-
guard let self else { return }
44-
self.connectivityContinuations[identifier] = continuation
45-
continuation.yield(self.isConnectedValue)
24+
init() {
25+
let initialStatus = networkPathMonitor.currentPath.status == .satisfied
26+
isConnectedSubject.send(initialStatus)
4627

47-
continuation.onTermination = { [weak self] _ in
48-
Task { @MainActor in
49-
self?.connectivityContinuations.removeValue(forKey: identifier)
50-
}
51-
}
28+
networkPathMonitor.pathUpdateHandler = { [weak self] path in
29+
let connected = (path.status == .satisfied)
30+
self?.isConnectedSubject.send(connected)
5231
}
32+
networkPathMonitor.start(queue: monitoringQueue)
5333
}
5434

55-
private func handlePathStatusChange(isConnected: Bool) {
56-
guard isConnected != self.isConnectedValue else { return }
57-
self.isConnectedValue = isConnected
58-
for continuation in self.connectivityContinuations.values {
59-
continuation.yield(isConnected)
60-
}
35+
deinit {
36+
networkPathMonitor.cancel()
37+
isConnectedSubject.send(completion: .finished)
6138
}
6239
}

DevLog/Presentation/ViewModel/LoginViewModel.swift

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,18 @@ final class LoginViewModel: Store {
1414
struct State {
1515
var signIn: Bool?
1616
var isLoading = false
17-
var showToast: Bool = false
18-
var toastMessage: String = ""
17+
var showAlert: Bool = false
18+
var alertTitle: String = ""
19+
var alertMessage: String = ""
1920
}
2021

2122
enum Action {
2223
case signOutAuto
23-
case tapCloseToast
24+
case setAlert(Bool)
2425
case tapSignInButton(AuthProvider)
2526
case tapSignOutButton
26-
case didStartLoading
27-
case didFinishLoading
28-
case didLogined(result: Bool)
29-
case didLoginFail(message: String)
27+
case setLoading(Bool)
28+
case setLogined(Bool)
3029
}
3130

3231
enum SideEffect {
@@ -54,65 +53,71 @@ final class LoginViewModel: Store {
5453
.removeDuplicates()
5554
.receive(on: DispatchQueue.main)
5655
.sink { [weak self] signIn in
57-
self?.send(.didLogined(result: signIn))
56+
self?.send(.setLogined(signIn))
5857
}
5958
.store(in: &cancellables)
6059
}
6160

6261
func reduce(with action: Action) -> [SideEffect] {
62+
var state = self.state
63+
6364
switch action {
64-
case .tapCloseToast:
65-
state.showToast = false
65+
case .setAlert(let isPresented):
66+
setAlert(&state, isPresented: isPresented)
6667
case .tapSignInButton(let authProvider):
68+
self.state = state
6769
return [.signIn(authProvider)]
6870
case .tapSignOutButton, .signOutAuto:
71+
self.state = state
6972
return [.signOut]
70-
case .didStartLoading:
71-
state.isLoading = true
72-
case .didFinishLoading:
73-
state.isLoading = false
74-
case .didLogined(let result):
73+
case .setLoading(let value):
74+
state.isLoading = value
75+
case .setLogined(let result):
7576
state.signIn = result
76-
case .didLoginFail(let message):
77-
state.toastMessage = message
78-
state.showToast = true
7977
}
78+
79+
self.state = state
8080
return []
8181
}
8282

8383
func run(_ effect: SideEffect) {
84+
send(.setLoading(true))
8485
switch effect {
8586
case .signIn(let authProvider):
8687
Task {
87-
send(.didStartLoading)
8888
do {
89-
defer { send(.didFinishLoading) }
90-
91-
_ = try await self.signInUseCase.execute(authProvider)
92-
93-
send(.didFinishLoading)
94-
send(.didLogined(result: true))
89+
defer { send(.setLoading(false)) }
90+
try await self.signInUseCase.execute(authProvider)
91+
send(.setLogined(true))
9592
sessionUseCase.execute(true)
9693
} catch {
97-
send(.didFinishLoading)
98-
send(.didLogined(result: false))
94+
send(.setLogined(false))
9995
sessionUseCase.execute(false)
100-
send(.didLoginFail(message: error.localizedDescription))
96+
send(.setAlert(true))
10197
}
10298
}
10399
case .signOut:
104100
Task {
105-
send(.didStartLoading)
106101
do {
107-
defer { send(.didFinishLoading) }
102+
defer { send(.setLoading(false)) }
108103
try await self.signOutUseCase.execute()
109-
send(.didLogined(result: false))
104+
send(.setLogined(false))
110105
sessionUseCase.execute(false)
111106
} catch {
112-
send(.didFinishLoading)
113-
send(.didLoginFail(message: error.localizedDescription))
107+
send(.setAlert(true))
114108
}
115109
}
116110
}
117111
}
118112
}
113+
114+
private extension LoginViewModel {
115+
func setAlert(
116+
_ state: inout State,
117+
isPresented: Bool,
118+
) {
119+
state.alertTitle = "오류"
120+
state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요."
121+
state.showAlert = isPresented
122+
}
123+
}

0 commit comments

Comments
 (0)