Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion DevLog/Presentation/ViewModel/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import Combine

@Observable
final class HomeViewModel: Store {
Expand All @@ -15,6 +16,7 @@ final class HomeViewModel: Store {
}
var recentTodos: [RecentTodoItem] = []
var webPages: [WebPageItem] = []
var isNetworkConnected: Bool = true
var showContentPicker: Bool = false
var showTodoEditor: Bool = false
var showSearchView: Bool = false
Expand All @@ -35,6 +37,7 @@ final class HomeViewModel: Store {

enum Action {
case onAppear
case networkStatusChanged(Bool)
case setPresentation(Presentation, Bool)
case setAlert(isPresented: Bool, type: AlertType? = nil)
case setToast(isPresented: Bool, type: ToastType? = nil)
Expand Down Expand Up @@ -96,30 +99,38 @@ final class HomeViewModel: Store {
private let undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase
private let fetchTodosUseCase: FetchTodosUseCase
private let fetchWebPagesUseCase: FetchWebPagesUseCase
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
private let loadingState = LoadingState()
private var deletedWebPageURLString: String?
private var cancellables = Set<AnyCancellable>()

init(
addWebPageUseCase: AddWebPageUseCase,
deleteWebPageUseCase: DeleteWebPageUseCase,
undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase,
upsertTodoUseCase: UpsertTodoUseCase,
fetchTodosUseCase: FetchTodosUseCase,
fetchWebPagesUseCase: FetchWebPagesUseCase
fetchWebPagesUseCase: FetchWebPagesUseCase,
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
) {
self.addWebPageUseCase = addWebPageUseCase
self.deleteWebPageUseCase = deleteWebPageUseCase
self.undoDeleteWebPageUseCase = undoDeleteWebPageUseCase
self.upsertTodoUseCase = upsertTodoUseCase
self.fetchTodosUseCase = fetchTodosUseCase
self.fetchWebPagesUseCase = fetchWebPagesUseCase
self.networkConnectivityUseCase = networkConnectivityUseCase

setupNetworkObserving()
}

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

switch action {
case .networkStatusChanged(let isConnected):
state.isNetworkConnected = isConnected
case .onAppear, .setPresentation, .setAlert, .setToast, .tapTodoCategory,
.orderTodoCategoryPreferences, .addTodo, .updateWebPageURLInput,
.addWebPage, .deleteWebPage, .undoDeleteWebPage:
Expand Down Expand Up @@ -432,4 +443,14 @@ private extension HomeViewModel {
self?.send(.setLoading(target, isLoading))
}
}

func setupNetworkObserving() {
networkConnectivityUseCase.observe()
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] isConnected in
self?.send(.networkStatusChanged(isConnected))
}
.store(in: &cancellables)
}
Comment on lines +447 to +455
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

setupNetworkObserving() 함수와 관련 로직이 HomeViewModel, ProfileViewModel, SettingViewModel에 중복되어 있습니다. 코드 중복은 유지보수성을 저하시킬 수 있으므로, 이 로직을 공통 프로토콜과 확장(extension)으로 추출하는 리팩토링을 고려해보세요.

예를 들어, 네트워크 상태를 관찰하고 networkStatusChanged 액션을 보내는 로직을 담은 프로토콜을 만들어 채택하도록 하면, 각 ViewModel에서 중복 코드를 제거하고 코드 재사용성을 높일 수 있습니다.

}
21 changes: 21 additions & 0 deletions DevLog/Presentation/ViewModel/ProfileViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
//

import Foundation
import Combine

@Observable
final class ProfileViewModel: Store {
struct State: Equatable {
var name: String = ""
var email: String = ""
var isNetworkConnected: Bool = true
var statusMessage: String = ""
var avatarURL: URL?
var earliestQuarterStart: Date?
Expand All @@ -31,6 +33,7 @@ final class ProfileViewModel: Store {

enum Action {
case onAppear
case networkStatusChanged(Bool)
case setAlert(Bool)
case tapResetStatusMessageButton
case willUpdateStatusMessage
Expand Down Expand Up @@ -66,22 +69,27 @@ final class ProfileViewModel: Store {
private let fetchUserDataUseCase: FetchUserDataUseCase
private let fetchTodosUseCase: FetchTodosUseCase
private let upsertStatusMessageUseCase: UpsertStatusMessageUseCase
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
private let fetchHeatmapActivityTypesUseCase: FetchProfileHeatmapActivityTypesUseCase
private let updateHeatmapActivityTypesUseCase: UpdateProfileHeatmapActivityTypesUseCase
private let calendar = Calendar.current
private var cancellables = Set<AnyCancellable>()

init(
fetchUserDataUseCase: FetchUserDataUseCase,
fetchTodosUseCase: FetchTodosUseCase,
upsertStatusMessageUseCase: UpsertStatusMessageUseCase,
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
fetchHeatmapActivityTypesUseCase: FetchProfileHeatmapActivityTypesUseCase,
updateHeatmapActivityTypesUseCase: UpdateProfileHeatmapActivityTypesUseCase
) {
self.fetchUserDataUseCase = fetchUserDataUseCase
self.fetchTodosUseCase = fetchTodosUseCase
self.upsertStatusMessageUseCase = upsertStatusMessageUseCase
self.networkConnectivityUseCase = networkConnectivityUseCase
self.fetchHeatmapActivityTypesUseCase = fetchHeatmapActivityTypesUseCase
self.updateHeatmapActivityTypesUseCase = updateHeatmapActivityTypesUseCase
setupNetworkObserving()
}

// swiftlint:disable cyclomatic_complexity
Expand All @@ -107,6 +115,8 @@ final class ProfileViewModel: Store {
if let selectedQuarterStart = state.selectedQuarterStart {
effects.append(.fetchCompletionQuarter(selectedQuarterStart))
}
case .networkStatusChanged(let isConnected):
state.isNetworkConnected = isConnected
case .setAlert(let isPresented):
setAlert(&state, isPresented: isPresented)
case .tapResetStatusMessageButton:
Expand Down Expand Up @@ -169,6 +179,7 @@ final class ProfileViewModel: Store {
}
effects = [.updateHeatmapActivityTypes(state.selectedActivityTypes)]
case .willUpdateStatusMessage:
if !state.isNetworkConnected { break }
let message = self.state.statusMessage
effects = [.updateStatusMessage(message)]
case .updateStatusMessage(let message):
Expand Down Expand Up @@ -237,6 +248,16 @@ final class ProfileViewModel: Store {
}

extension ProfileViewModel {
private func setupNetworkObserving() {
networkConnectivityUseCase.observe()
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] isConnected in
self?.send(.networkStatusChanged(isConnected))
}
.store(in: &cancellables)
}

var quarterTitle: String {
guard let start = state.selectedQuarterStart else { return "" }
let year = calendar.component(.year, from: start)
Expand Down
18 changes: 18 additions & 0 deletions DevLog/Presentation/ViewModel/SettingViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ final class SettingViewModel: Store {
struct State: Equatable {
var theme: SystemTheme = .automatic
var dirSize: Int64 = 0
var isNetworkConnected = true
var isLoading = false
var showAlert: Bool = false
var alertTitle: String = ""
Expand All @@ -21,6 +22,7 @@ final class SettingViewModel: Store {
}

enum Action {
case networkStatusChanged(Bool)
case setAlert(isPresented: Bool, type: AlertType? = nil)
case setLoading(Bool)
case setTheme(SystemTheme)
Expand All @@ -43,6 +45,7 @@ final class SettingViewModel: Store {
private(set) var state = State()
private let deleteAuthuseCase: DeleteAuthUseCase
private let signOutUseCase: SignOutUseCase
private let networkConnectivityUseCase: ObserveNetworkConnectivityUseCase
private let systemThemeUseCase: ObserveSystemThemeUseCase
private let updateSystemThemeUseCase: UpdateSystemThemeUseCase
private let loadingState = LoadingState()
Expand All @@ -55,13 +58,16 @@ final class SettingViewModel: Store {
init(
deleteAuthUseCase: DeleteAuthUseCase,
signOutUseCase: SignOutUseCase,
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
systemThemeUseCase: ObserveSystemThemeUseCase,
updateSystemThemeUseCase: UpdateSystemThemeUseCase
) {
self.deleteAuthuseCase = deleteAuthUseCase
self.signOutUseCase = signOutUseCase
self.networkConnectivityUseCase = networkConnectivityUseCase
self.systemThemeUseCase = systemThemeUseCase
self.updateSystemThemeUseCase = updateSystemThemeUseCase
setupNetworkObserving()
setupThemeMonitoring()
}

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

switch action {
case .networkStatusChanged(let isConnected):
state.isNetworkConnected = isConnected
case .setAlert(let isPresented, let type):
setAlert(&state, isPresented: isPresented, type: type)
case .setLoading(let value):
Expand Down Expand Up @@ -164,6 +172,16 @@ private extension SettingViewModel {
.store(in: &cancellables)
}

func setupNetworkObserving() {
networkConnectivityUseCase.observe()
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] isConnected in
self?.send(.networkStatusChanged(isConnected))
}
.store(in: &cancellables)
}

func dirSizeInBytes() -> Int64 {
do {
let cachesDir = try FileManager.default.url(
Expand Down
4 changes: 3 additions & 1 deletion DevLog/UI/Common/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ struct MainView: View {
undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self),
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self)
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self),
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self)
))
.tabItem {
Image(systemName: "house.fill")
Expand Down Expand Up @@ -53,6 +54,7 @@ struct MainView: View {
fetchUserDataUseCase: container.resolve(FetchUserDataUseCase.self),
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
upsertStatusMessageUseCase: container.resolve(UpsertStatusMessageUseCase.self),
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
fetchHeatmapActivityTypesUseCase: container.resolve(FetchProfileHeatmapActivityTypesUseCase.self),
updateHeatmapActivityTypesUseCase: container.resolve(UpdateProfileHeatmapActivityTypesUseCase.self)
))
Expand Down
1 change: 1 addition & 0 deletions DevLog/UI/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ struct HomeView: View {
} label: {
Image(systemName: "plus")
}
.disabled(!viewModel.state.isNetworkConnected)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(.fixed, placement: .topBarTrailing)
Expand Down
1 change: 0 additions & 1 deletion DevLog/UI/Home/TodoListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ struct TodoListView: View {
.onChange(of: geometry.size.height, initial: true) { _, height in
headerHeight = height.rounded()
}

}
}
}
Expand Down
11 changes: 7 additions & 4 deletions DevLog/UI/Profile/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,19 @@ struct ProfileView: View {
.foregroundStyle(Color.gray)
}
}
let connected = viewModel.state.isNetworkConnected
HStack {
HStack {
Image(systemName: "face.smiling")
TextField(text: Binding(
get: { viewModel.state.statusMessage },
set: { viewModel.send(.updateStatusMessage($0)) })
) {
HStack {
Text("상태 설정")
}
Text("상태 설정")
}
.frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight)
.focused($focused)
.disabled(!connected)

if !viewModel.state.statusMessage.isEmpty && viewModel.state.showDoneButton {
Button(action: {
Expand All @@ -63,7 +64,7 @@ struct ProfileView: View {
.padding(8)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color(UIColor.systemGray5))
.fill(Color(.secondarySystemGroupedBackground))
)
if viewModel.state.showDoneButton {
Button(action: {
Expand All @@ -75,6 +76,7 @@ struct ProfileView: View {
.transition(.move(edge: .trailing).combined(with: .opacity))
}
}
.opacity(connected ? 1 : 0.7)
activityHeatmapSection
}
.padding(.horizontal, 16)
Expand All @@ -98,6 +100,7 @@ struct ProfileView: View {
SettingView(viewModel: SettingViewModel(
deleteAuthUseCase: container.resolve(DeleteAuthUseCase.self),
signOutUseCase: container.resolve(SignOutUseCase.self),
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self),
updateSystemThemeUseCase: container.resolve(UpdateSystemThemeUseCase.self)
))
Expand Down
7 changes: 6 additions & 1 deletion DevLog/UI/Setting/SettingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct SettingView: View {
@Environment(NavigationRouter.self) var router

var body: some View {
let connected = viewModel.state.isNetworkConnected
Form {
Section {
Button {
Expand All @@ -31,8 +32,9 @@ struct SettingView: View {
router.push(Path.pushNotification)
} label: {
Text("알림")
.foregroundStyle(Color.primary)
.foregroundStyle(connected ? Color.primary : Color.secondary)
}
.disabled(!connected)

let dirSize = viewModel.state.dirSize
Button {
Expand Down Expand Up @@ -85,11 +87,13 @@ struct SettingView: View {
} label: {
Text("계정 연동")
}
.disabled(!connected)
Button(role: .destructive, action: {
viewModel.send(.setAlert(isPresented: true, type: .signOut))
}) {
Text("로그아웃")
}
.disabled(!connected)
}

HStack {
Expand All @@ -100,6 +104,7 @@ struct SettingView: View {
Text("회원 탈퇴")
.font(.headline)
}
.disabled(!connected)
Spacer()
}
}
Expand Down