Skip to content

Commit 4191160

Browse files
authored
[#318] 네트워크 연결이 끊겼을 떄 컨텐츠를 추가 및 수정하지 못하도록 막는다 (#329)
* feat: HomeView에서 네트워크가 끊어지면 컨텐츠를 추가하는 버튼을 잠금 * feat: SettingsView에서 네트워크가 끊어지면 서버와 연관된 버튼들은 비활성화 * feat: 테마뷰에서 네트워크가 끊어지면 업데이트 불가능하도록 추가 * remove: 테마는 disabled 제거 * feat: 프로필 쪽 네트워크를 감지하여 상태 메시지 잠금
1 parent 36541a7 commit 4191160

8 files changed

Lines changed: 78 additions & 8 deletions

File tree

DevLog/Presentation/ViewModel/HomeViewModel.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import Combine
910

1011
@Observable
1112
final class HomeViewModel: Store {
@@ -15,6 +16,7 @@ final class HomeViewModel: Store {
1516
}
1617
var recentTodos: [RecentTodoItem] = []
1718
var webPages: [WebPageItem] = []
19+
var isNetworkConnected: Bool = true
1820
var showContentPicker: Bool = false
1921
var showTodoEditor: Bool = false
2022
var showSearchView: Bool = false
@@ -35,6 +37,7 @@ final class HomeViewModel: Store {
3537

3638
enum Action {
3739
case onAppear
40+
case networkStatusChanged(Bool)
3841
case setPresentation(Presentation, Bool)
3942
case setAlert(isPresented: Bool, type: AlertType? = nil)
4043
case setToast(isPresented: Bool, type: ToastType? = nil)
@@ -96,30 +99,38 @@ final class HomeViewModel: Store {
9699
private let undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase
97100
private let fetchTodosUseCase: FetchTodosUseCase
98101
private let fetchWebPagesUseCase: FetchWebPagesUseCase
102+
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
99103
private let loadingState = LoadingState()
100104
private var deletedWebPageURLString: String?
105+
private var cancellables = Set<AnyCancellable>()
101106

102107
init(
103108
addWebPageUseCase: AddWebPageUseCase,
104109
deleteWebPageUseCase: DeleteWebPageUseCase,
105110
undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase,
106111
upsertTodoUseCase: UpsertTodoUseCase,
107112
fetchTodosUseCase: FetchTodosUseCase,
108-
fetchWebPagesUseCase: FetchWebPagesUseCase
113+
fetchWebPagesUseCase: FetchWebPagesUseCase,
114+
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
109115
) {
110116
self.addWebPageUseCase = addWebPageUseCase
111117
self.deleteWebPageUseCase = deleteWebPageUseCase
112118
self.undoDeleteWebPageUseCase = undoDeleteWebPageUseCase
113119
self.upsertTodoUseCase = upsertTodoUseCase
114120
self.fetchTodosUseCase = fetchTodosUseCase
115121
self.fetchWebPagesUseCase = fetchWebPagesUseCase
122+
self.networkConnectivityUseCase = networkConnectivityUseCase
123+
124+
setupNetworkObserving()
116125
}
117126

118127
func reduce(with action: Action) -> [SideEffect] {
119128
var state = self.state
120129
var effects: [SideEffect] = []
121130

122131
switch action {
132+
case .networkStatusChanged(let isConnected):
133+
state.isNetworkConnected = isConnected
123134
case .onAppear, .setPresentation, .setAlert, .setToast, .tapTodoCategory,
124135
.orderTodoCategoryPreferences, .addTodo, .updateWebPageURLInput,
125136
.addWebPage, .deleteWebPage, .undoDeleteWebPage:
@@ -432,4 +443,14 @@ private extension HomeViewModel {
432443
self?.send(.setLoading(target, isLoading))
433444
}
434445
}
446+
447+
func setupNetworkObserving() {
448+
networkConnectivityUseCase.observe()
449+
.removeDuplicates()
450+
.receive(on: DispatchQueue.main)
451+
.sink { [weak self] isConnected in
452+
self?.send(.networkStatusChanged(isConnected))
453+
}
454+
.store(in: &cancellables)
455+
}
435456
}

DevLog/Presentation/ViewModel/ProfileViewModel.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
//
77

88
import Foundation
9+
import Combine
910

1011
@Observable
1112
final class ProfileViewModel: Store {
1213
struct State: Equatable {
1314
var name: String = ""
1415
var email: String = ""
16+
var isNetworkConnected: Bool = true
1517
var statusMessage: String = ""
1618
var avatarURL: URL?
1719
var earliestQuarterStart: Date?
@@ -31,6 +33,7 @@ final class ProfileViewModel: Store {
3133

3234
enum Action {
3335
case onAppear
36+
case networkStatusChanged(Bool)
3437
case setAlert(Bool)
3538
case tapResetStatusMessageButton
3639
case willUpdateStatusMessage
@@ -66,22 +69,27 @@ final class ProfileViewModel: Store {
6669
private let fetchUserDataUseCase: FetchUserDataUseCase
6770
private let fetchTodosUseCase: FetchTodosUseCase
6871
private let upsertStatusMessageUseCase: UpsertStatusMessageUseCase
72+
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
6973
private let fetchHeatmapActivityTypesUseCase: FetchProfileHeatmapActivityTypesUseCase
7074
private let updateHeatmapActivityTypesUseCase: UpdateProfileHeatmapActivityTypesUseCase
7175
private let calendar = Calendar.current
76+
private var cancellables = Set<AnyCancellable>()
7277

7378
init(
7479
fetchUserDataUseCase: FetchUserDataUseCase,
7580
fetchTodosUseCase: FetchTodosUseCase,
7681
upsertStatusMessageUseCase: UpsertStatusMessageUseCase,
82+
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
7783
fetchHeatmapActivityTypesUseCase: FetchProfileHeatmapActivityTypesUseCase,
7884
updateHeatmapActivityTypesUseCase: UpdateProfileHeatmapActivityTypesUseCase
7985
) {
8086
self.fetchUserDataUseCase = fetchUserDataUseCase
8187
self.fetchTodosUseCase = fetchTodosUseCase
8288
self.upsertStatusMessageUseCase = upsertStatusMessageUseCase
89+
self.networkConnectivityUseCase = networkConnectivityUseCase
8390
self.fetchHeatmapActivityTypesUseCase = fetchHeatmapActivityTypesUseCase
8491
self.updateHeatmapActivityTypesUseCase = updateHeatmapActivityTypesUseCase
92+
setupNetworkObserving()
8593
}
8694

8795
// swiftlint:disable cyclomatic_complexity
@@ -107,6 +115,8 @@ final class ProfileViewModel: Store {
107115
if let selectedQuarterStart = state.selectedQuarterStart {
108116
effects.append(.fetchCompletionQuarter(selectedQuarterStart))
109117
}
118+
case .networkStatusChanged(let isConnected):
119+
state.isNetworkConnected = isConnected
110120
case .setAlert(let isPresented):
111121
setAlert(&state, isPresented: isPresented)
112122
case .tapResetStatusMessageButton:
@@ -169,6 +179,7 @@ final class ProfileViewModel: Store {
169179
}
170180
effects = [.updateHeatmapActivityTypes(state.selectedActivityTypes)]
171181
case .willUpdateStatusMessage:
182+
if !state.isNetworkConnected { break }
172183
let message = self.state.statusMessage
173184
effects = [.updateStatusMessage(message)]
174185
case .updateStatusMessage(let message):
@@ -237,6 +248,16 @@ final class ProfileViewModel: Store {
237248
}
238249

239250
extension ProfileViewModel {
251+
private func setupNetworkObserving() {
252+
networkConnectivityUseCase.observe()
253+
.removeDuplicates()
254+
.receive(on: DispatchQueue.main)
255+
.sink { [weak self] isConnected in
256+
self?.send(.networkStatusChanged(isConnected))
257+
}
258+
.store(in: &cancellables)
259+
}
260+
240261
var quarterTitle: String {
241262
guard let start = state.selectedQuarterStart else { return "" }
242263
let year = calendar.component(.year, from: start)

DevLog/Presentation/ViewModel/SettingViewModel.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final class SettingViewModel: Store {
1313
struct State: Equatable {
1414
var theme: SystemTheme = .automatic
1515
var dirSize: Int64 = 0
16+
var isNetworkConnected = true
1617
var isLoading = false
1718
var showAlert: Bool = false
1819
var alertTitle: String = ""
@@ -21,6 +22,7 @@ final class SettingViewModel: Store {
2122
}
2223

2324
enum Action {
25+
case networkStatusChanged(Bool)
2426
case setAlert(isPresented: Bool, type: AlertType? = nil)
2527
case setLoading(Bool)
2628
case setTheme(SystemTheme)
@@ -43,6 +45,7 @@ final class SettingViewModel: Store {
4345
private(set) var state = State()
4446
private let deleteAuthuseCase: DeleteAuthUseCase
4547
private let signOutUseCase: SignOutUseCase
48+
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
4649
private let systemThemeUseCase: ObserveSystemThemeUseCase
4750
private let updateSystemThemeUseCase: UpdateSystemThemeUseCase
4851
private let loadingState = LoadingState()
@@ -55,13 +58,16 @@ final class SettingViewModel: Store {
5558
init(
5659
deleteAuthUseCase: DeleteAuthUseCase,
5760
signOutUseCase: SignOutUseCase,
61+
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
5862
systemThemeUseCase: ObserveSystemThemeUseCase,
5963
updateSystemThemeUseCase: UpdateSystemThemeUseCase
6064
) {
6165
self.deleteAuthuseCase = deleteAuthUseCase
6266
self.signOutUseCase = signOutUseCase
67+
self.networkConnectivityUseCase = networkConnectivityUseCase
6368
self.systemThemeUseCase = systemThemeUseCase
6469
self.updateSystemThemeUseCase = updateSystemThemeUseCase
70+
setupNetworkObserving()
6571
setupThemeMonitoring()
6672
}
6773

@@ -70,6 +76,8 @@ final class SettingViewModel: Store {
7076
var effects: [SideEffect] = []
7177

7278
switch action {
79+
case .networkStatusChanged(let isConnected):
80+
state.isNetworkConnected = isConnected
7381
case .setAlert(let isPresented, let type):
7482
setAlert(&state, isPresented: isPresented, type: type)
7583
case .setLoading(let value):
@@ -164,6 +172,16 @@ private extension SettingViewModel {
164172
.store(in: &cancellables)
165173
}
166174

175+
func setupNetworkObserving() {
176+
networkConnectivityUseCase.observe()
177+
.removeDuplicates()
178+
.receive(on: DispatchQueue.main)
179+
.sink { [weak self] isConnected in
180+
self?.send(.networkStatusChanged(isConnected))
181+
}
182+
.store(in: &cancellables)
183+
}
184+
167185
func dirSizeInBytes() -> Int64 {
168186
do {
169187
let cachesDir = try FileManager.default.url(

DevLog/UI/Common/MainView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ struct MainView: View {
1919
undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self),
2020
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
2121
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
22-
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self)
22+
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self),
23+
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self)
2324
))
2425
.tabItem {
2526
Image(systemName: "house.fill")
@@ -53,6 +54,7 @@ struct MainView: View {
5354
fetchUserDataUseCase: container.resolve(FetchUserDataUseCase.self),
5455
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
5556
upsertStatusMessageUseCase: container.resolve(UpsertStatusMessageUseCase.self),
57+
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
5658
fetchHeatmapActivityTypesUseCase: container.resolve(FetchProfileHeatmapActivityTypesUseCase.self),
5759
updateHeatmapActivityTypesUseCase: container.resolve(UpdateProfileHeatmapActivityTypesUseCase.self)
5860
))

DevLog/UI/Home/HomeView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ struct HomeView: View {
259259
} label: {
260260
Image(systemName: "plus")
261261
}
262+
.disabled(!viewModel.state.isNetworkConnected)
262263
}
263264
if #available(iOS 26.0, *) {
264265
ToolbarSpacer(.fixed, placement: .topBarTrailing)

DevLog/UI/Home/TodoListView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,6 @@ struct TodoListView: View {
305305
.onChange(of: geometry.size.height, initial: true) { _, height in
306306
headerHeight = height.rounded()
307307
}
308-
309308
}
310309
}
311310
}

DevLog/UI/Profile/ProfileView.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,19 @@ struct ProfileView: View {
3737
.foregroundStyle(Color.gray)
3838
}
3939
}
40+
let connected = viewModel.state.isNetworkConnected
4041
HStack {
4142
HStack {
4243
Image(systemName: "face.smiling")
4344
TextField(text: Binding(
4445
get: { viewModel.state.statusMessage },
4546
set: { viewModel.send(.updateStatusMessage($0)) })
4647
) {
47-
HStack {
48-
Text("상태 설정")
49-
}
48+
Text("상태 설정")
5049
}
50+
.frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight)
5151
.focused($focused)
52+
.disabled(!connected)
5253

5354
if !viewModel.state.statusMessage.isEmpty && viewModel.state.showDoneButton {
5455
Button(action: {
@@ -63,7 +64,7 @@ struct ProfileView: View {
6364
.padding(8)
6465
.background(
6566
RoundedRectangle(cornerRadius: 10)
66-
.fill(Color(UIColor.systemGray5))
67+
.fill(Color(.secondarySystemGroupedBackground))
6768
)
6869
if viewModel.state.showDoneButton {
6970
Button(action: {
@@ -75,6 +76,7 @@ struct ProfileView: View {
7576
.transition(.move(edge: .trailing).combined(with: .opacity))
7677
}
7778
}
79+
.opacity(connected ? 1 : 0.7)
7880
activityHeatmapSection
7981
}
8082
.padding(.horizontal, 16)
@@ -98,6 +100,7 @@ struct ProfileView: View {
98100
SettingView(viewModel: SettingViewModel(
99101
deleteAuthUseCase: container.resolve(DeleteAuthUseCase.self),
100102
signOutUseCase: container.resolve(SignOutUseCase.self),
103+
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
101104
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self),
102105
updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self)
103106
))

DevLog/UI/Setting/SettingView.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct SettingView: View {
1313
@Environment(NavigationRouter.self) var router
1414

1515
var body: some View {
16+
let connected = viewModel.state.isNetworkConnected
1617
Form {
1718
Section {
1819
Button {
@@ -31,8 +32,9 @@ struct SettingView: View {
3132
router.push(Path.pushNotification)
3233
} label: {
3334
Text("알림")
34-
.foregroundStyle(Color.primary)
35+
.foregroundStyle(connected ? Color.primary : Color.secondary)
3536
}
37+
.disabled(!connected)
3638

3739
let dirSize = viewModel.state.dirSize
3840
Button {
@@ -85,11 +87,13 @@ struct SettingView: View {
8587
} label: {
8688
Text("계정 연동")
8789
}
90+
.disabled(!connected)
8891
Button(role: .destructive, action: {
8992
viewModel.send(.setAlert(isPresented: true, type: .signOut))
9093
}) {
9194
Text("로그아웃")
9295
}
96+
.disabled(!connected)
9397
}
9498

9599
HStack {
@@ -100,6 +104,7 @@ struct SettingView: View {
100104
Text("회원 탈퇴")
101105
.font(.headline)
102106
}
107+
.disabled(!connected)
103108
Spacer()
104109
}
105110
}

0 commit comments

Comments
 (0)