diff --git a/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature+Dependencies.swift b/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature+Dependencies.swift index 4f3d5226..9e17b7f6 100644 --- a/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature+Dependencies.swift +++ b/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature+Dependencies.swift @@ -39,7 +39,7 @@ extension DependencyValues { set { self[HomeFetchWebPagesUseCaseKey.self] = newValue } } - var networkConnectivityUseCase: ObserveNetworkConnectivityUseCase { + var homeNetworkConnectivityUseCase: ObserveNetworkConnectivityUseCase { get { self[HomeNetworkConnectivityUseCaseKey.self] } set { self[HomeNetworkConnectivityUseCaseKey.self] = newValue } } diff --git a/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature.swift b/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature.swift index fd548696..1d522aa1 100644 --- a/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature.swift +++ b/Application/Presentation/HomeTab/Sources/Home/Home/HomeFeature.swift @@ -207,7 +207,7 @@ struct HomeFeature { @Dependency(\.homeUndoDeleteWebPageUseCase) var undoDeleteWebPageUseCase @Dependency(\.homeFetchTodosUseCase) var fetchTodosUseCase @Dependency(\.homeFetchWebPagesUseCase) var fetchWebPagesUseCase - @Dependency(\.networkConnectivityUseCase) var networkConnectivityUseCase + @Dependency(\.homeNetworkConnectivityUseCase) var networkConnectivityUseCase @Dependency(\.trackAnalyticsEventUseCase) var trackAnalyticsEventUseCase @Dependency(\.continuousClock) var clock diff --git a/Application/Presentation/HomeTab/Sources/Home/Home/HomeViewCoordinator.swift b/Application/Presentation/HomeTab/Sources/Home/Home/HomeViewCoordinator.swift index 85816d00..dcd67992 100644 --- a/Application/Presentation/HomeTab/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/Presentation/HomeTab/Sources/Home/Home/HomeViewCoordinator.swift @@ -40,7 +40,7 @@ public final class HomeViewCoordinator { $0.homeFetchWebPagesUseCase = container.resolve(FetchWebPagesUseCase.self) $0.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self) $0.upsertTodoUseCase = container.resolve(UpsertTodoUseCase.self) - $0.networkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) + $0.homeNetworkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) $0.trackAnalyticsEventUseCase = container.resolve(TrackAnalyticsEventUseCase.self) } self.store.send(.view(.startObserving)) diff --git a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift index 514a0020..092bd162 100644 --- a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift +++ b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift @@ -73,7 +73,7 @@ struct HomeStoreTestAdapter { $0.homeUndoDeleteWebPageUseCase = undoDeleteWebPageUseCase $0.homeFetchTodosUseCase = fetchTodosUseCase $0.homeFetchWebPagesUseCase = fetchWebPagesUseCase - $0.networkConnectivityUseCase = networkConnectivityUseCase + $0.homeNetworkConnectivityUseCase = networkConnectivityUseCase $0.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase $0.continuousClock = clock configureDependencies?(&$0) diff --git a/Application/Presentation/ProfileTab/Sources/.swiftlint.yml b/Application/Presentation/ProfileTab/Sources/.swiftlint.yml new file mode 100644 index 00000000..6a9d7380 --- /dev/null +++ b/Application/Presentation/ProfileTab/Sources/.swiftlint.yml @@ -0,0 +1 @@ +parent_config: ../../../../.swiftlint.yml diff --git a/Application/Presentation/Sources/Profile/HeatmapView.swift b/Application/Presentation/ProfileTab/Sources/Profile/HeatmapView.swift similarity index 99% rename from Application/Presentation/Sources/Profile/HeatmapView.swift rename to Application/Presentation/ProfileTab/Sources/Profile/HeatmapView.swift index f7b28db7..42cd53b8 100644 --- a/Application/Presentation/Sources/Profile/HeatmapView.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/HeatmapView.swift @@ -1,6 +1,6 @@ // // HeatmapView.swift -// Presentation +// ProfileTab // // Created by 최윤진 on 3/2/26. // diff --git a/Application/Presentation/Sources/Profile/ProfileFeature+Dependencies.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature+Dependencies.swift similarity index 99% rename from Application/Presentation/Sources/Profile/ProfileFeature+Dependencies.swift rename to Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature+Dependencies.swift index 52eb9a3c..87ce0f95 100644 --- a/Application/Presentation/Sources/Profile/ProfileFeature+Dependencies.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature+Dependencies.swift @@ -1,6 +1,6 @@ // // ProfileFeature+Dependencies.swift -// Presentation +// ProfileTab // // Created by opfic on 6/15/26. // diff --git a/Application/Presentation/Sources/Profile/ProfileFeature+State.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature+State.swift similarity index 99% rename from Application/Presentation/Sources/Profile/ProfileFeature+State.swift rename to Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature+State.swift index 0a69cbaa..73f85462 100644 --- a/Application/Presentation/Sources/Profile/ProfileFeature+State.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature+State.swift @@ -1,6 +1,6 @@ // // ProfileFeature+State.swift -// Presentation +// ProfileTab // // Created by opfic on 6/15/26. // diff --git a/Application/Presentation/Sources/Profile/ProfileFeature.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature.swift similarity index 99% rename from Application/Presentation/Sources/Profile/ProfileFeature.swift rename to Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature.swift index 286956d5..e374e36e 100644 --- a/Application/Presentation/Sources/Profile/ProfileFeature.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileFeature.swift @@ -1,6 +1,6 @@ // // ProfileFeature.swift -// Presentation +// ProfileTab // // Created by opfic on 6/15/26. // @@ -73,7 +73,7 @@ struct ProfileFeature { @Dependency(\.profileFetchImageDataUseCase) var fetchProfileImageDataUseCase @Dependency(\.profileFetchTodosUseCase) var fetchTodosUseCase @Dependency(\.profileUpsertStatusMessageUseCase) var upsertStatusMessageUseCase - @Dependency(\.networkConnectivityUseCase) var networkConnectivityUseCase + @Dependency(\.profileNetworkConnectivityUseCase) var networkConnectivityUseCase @Dependency(\.profileFetchHeatmapActivityTypesUseCase) var fetchHeatmapActivityTypesUseCase @Dependency(\.profileUpdateHeatmapActivityTypesUseCase) var updateHeatmapActivityTypesUseCase diff --git a/Application/Presentation/Sources/Profile/ProfileHeatmapBuilder.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileHeatmapBuilder.swift similarity index 99% rename from Application/Presentation/Sources/Profile/ProfileHeatmapBuilder.swift rename to Application/Presentation/ProfileTab/Sources/Profile/ProfileHeatmapBuilder.swift index 0f9cfeb0..46dd4271 100644 --- a/Application/Presentation/Sources/Profile/ProfileHeatmapBuilder.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileHeatmapBuilder.swift @@ -1,6 +1,6 @@ // // ProfileHeatmapBuilder.swift -// Presentation +// ProfileTab // // Created by opfic on 6/15/26. // diff --git a/Application/Presentation/ProfileTab/Sources/Profile/ProfileRegularDetailView.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileRegularDetailView.swift new file mode 100644 index 00000000..0e52d21d --- /dev/null +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileRegularDetailView.swift @@ -0,0 +1,94 @@ +// +// ProfileRegularDetailView.swift +// ProfileTab +// +// Created by opfic on 7/6/26. +// + +import SwiftUI +import PresentationShared + +public struct ProfileRegularDetailView: View { + let coordinator: ProfileViewCoordinator + + public init(coordinator: ProfileViewCoordinator) { + self.coordinator = coordinator + } + + public var body: some View { + NavigationStack(path: navigationPath) { + Group { + if let route = coordinator.router.root { + ProfileDestinationView( + route: route, + coordinator: coordinator, + identifiesActivityDetail: true + ) + } else { + ContentUnavailableView( + String(localized: "profile_select_detail"), + systemImage: "person.crop.circle" + ) + } + } + .navigationDestination(for: ProfileRoute.self) { route in + ProfileDestinationView( + route: route, + coordinator: coordinator, + identifiesActivityDetail: true + ) + } + } + .background(Color(.systemGroupedBackground).ignoresSafeArea()) + } + + private var navigationPath: Binding<[ProfileRoute]> { + Binding( + get: { coordinator.router.detailPath }, + set: { coordinator.router.detailPath = $0 } + ) + } +} + +struct ProfileDestinationView: View { + let route: ProfileRoute + let coordinator: ProfileViewCoordinator + let identifiesActivityDetail: Bool + + init( + route: ProfileRoute, + coordinator: ProfileViewCoordinator, + identifiesActivityDetail: Bool = false + ) { + self.route = route + self.coordinator = coordinator + self.identifiesActivityDetail = identifiesActivityDetail + } + + var body: some View { + switch route { + case .settings: + SettingsView(store: coordinator.settingsStore) + .environment(coordinator.router) + case .activity(let todoId): + activityDetailView(todoId: todoId) + case .theme: + @Bindable var settingsStore = coordinator.settingsStore + ThemeView(theme: $settingsStore.theme) + case .pushNotification: + PushNotificationSettingsView(store: coordinator.makePushNotificationSettingsStore()) + case .account: + AccountView(store: coordinator.makeAccountStore()) + } + } + + @ViewBuilder + private func activityDetailView(todoId: String) -> some View { + if identifiesActivityDetail { + TodoDetailView(store: coordinator.makeTodoDetailStore(todoId: todoId)) + .id(todoId) + } else { + TodoDetailView(store: coordinator.makeTodoDetailStore(todoId: todoId)) + } + } +} diff --git a/Application/Presentation/Sources/Profile/ProfileView.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift similarity index 94% rename from Application/Presentation/Sources/Profile/ProfileView.swift rename to Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift index 23fc4075..fb6807da 100644 --- a/Application/Presentation/Sources/Profile/ProfileView.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift @@ -1,6 +1,6 @@ // // ProfileView.swift -// Presentation +// ProfileTab // // Created by opfic on 5/7/25. // @@ -11,13 +11,13 @@ import Core import Domain import PresentationShared -struct ProfileView: View { +public struct ProfileView: View { @Bindable var store: StoreOf @FocusState private var focused: Bool let coordinator: ProfileViewCoordinator let isCompactLayout: Bool - init( + public init( coordinator: ProfileViewCoordinator, isCompactLayout: Bool ) { @@ -26,13 +26,13 @@ struct ProfileView: View { self.isCompactLayout = isCompactLayout } - var body: some View { + public var body: some View { Group { if isCompactLayout { NavigationStack(path: navigationPath) { profileContentView .navigationDestination(for: ProfileRoute.self) { route in - profileDestinationView(route) + ProfileDestinationView(route: route, coordinator: coordinator) } } } else { @@ -242,24 +242,6 @@ struct ProfileView: View { } } - @ViewBuilder - private func profileDestinationView(_ route: ProfileRoute) -> some View { - switch route { - case .settings: - SettingsView(store: coordinator.settingsStore) - .environment(coordinator.router) - case .activity(let todoId): - TodoDetailView(store: coordinator.makeTodoDetailStore(todoId: todoId)) - case .theme: - @Bindable var settingsStore = coordinator.settingsStore - ThemeView(theme: $settingsStore.theme) - case .pushNotification: - PushNotificationSettingsView(store: coordinator.makePushNotificationSettingsStore()) - case .account: - AccountView(store: coordinator.makeAccountStore()) - } - } - private var quarterPickerSheet: some View { NavigationStack { VStack(alignment: .leading, spacing: 20) { diff --git a/Application/Presentation/Sources/Profile/ProfileViewCoordinator.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileViewCoordinator.swift similarity index 88% rename from Application/Presentation/Sources/Profile/ProfileViewCoordinator.swift rename to Application/Presentation/ProfileTab/Sources/Profile/ProfileViewCoordinator.swift index 5e6a0cf3..3a7648f1 100644 --- a/Application/Presentation/Sources/Profile/ProfileViewCoordinator.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileViewCoordinator.swift @@ -1,6 +1,6 @@ // // ProfileViewCoordinator.swift -// Presentation +// ProfileTab // // Created by opfic on 5/21/26. // @@ -12,13 +12,13 @@ import PresentationShared @MainActor @Observable -final class ProfileViewCoordinator { +public final class ProfileViewCoordinator { let store: StoreOf let settingsStore: StoreOf var router = NavigationRouter() private let container: DIContainer - init(container: DIContainer) { + public init(container: DIContainer) { self.container = container self.store = Store(initialState: ProfileFeature.State()) { ProfileFeature() @@ -27,7 +27,7 @@ final class ProfileViewCoordinator { $0.profileFetchImageDataUseCase = container.resolve(FetchProfileImageDataUseCase.self) $0.profileFetchTodosUseCase = container.resolve(FetchTodosUseCase.self) $0.profileUpsertStatusMessageUseCase = container.resolve(UpsertStatusMessageUseCase.self) - $0.networkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) + $0.profileNetworkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) $0.profileFetchHeatmapActivityTypesUseCase = container.resolve(FetchHeatmapActivityTypesUseCase.self) $0.profileUpdateHeatmapActivityTypesUseCase = container.resolve(UpdateHeatmapActivityTypesUseCase.self) } @@ -36,8 +36,8 @@ final class ProfileViewCoordinator { } withDependencies: { $0.deleteAuthUseCase = container.resolve(DeleteAuthUseCase.self) $0.signOutUseCase = container.resolve(SignOutUseCase.self) - $0.networkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) - $0.systemThemeUseCase = container.resolve(ObserveSystemThemeUseCase.self) + $0.profileNetworkConnectivityUseCase = container.resolve(ObserveNetworkConnectivityUseCase.self) + $0.profileSystemThemeUseCase = container.resolve(ObserveSystemThemeUseCase.self) $0.updateSystemThemeUseCase = container.resolve(UpdateSystemThemeUseCase.self) $0.fetchWebPageImageDirSizeUseCase = container.resolve(FetchWebPageImageDirSizeUseCase.self) $0.clearWebPageImageDirectoryUseCase = container.resolve(ClearWebPageImageDirectoryUseCase.self) @@ -46,7 +46,7 @@ final class ProfileViewCoordinator { self.settingsStore.send(.startObserving) } - func fetchData() { + public func fetchData() { store.send(.fetchData) } diff --git a/Application/Presentation/Sources/Settings/AccountFeature.swift b/Application/Presentation/ProfileTab/Sources/Settings/AccountFeature.swift similarity index 99% rename from Application/Presentation/Sources/Settings/AccountFeature.swift rename to Application/Presentation/ProfileTab/Sources/Settings/AccountFeature.swift index ccef52fe..4eb4bda7 100644 --- a/Application/Presentation/Sources/Settings/AccountFeature.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/AccountFeature.swift @@ -1,6 +1,6 @@ // // AccountFeature.swift -// Presentation +// ProfileTab // // Created by opfic on 6/11/26. // diff --git a/Application/Presentation/Sources/Settings/AccountView.swift b/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift similarity index 99% rename from Application/Presentation/Sources/Settings/AccountView.swift rename to Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift index 8f25017c..96ae101a 100644 --- a/Application/Presentation/Sources/Settings/AccountView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift @@ -1,6 +1,6 @@ // // AccountView.swift -// Presentation +// ProfileTab // // Created by opfic on 5/14/25. // diff --git a/Application/Presentation/Sources/Settings/PushNotificationSettingsFeature.swift b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsFeature.swift similarity index 99% rename from Application/Presentation/Sources/Settings/PushNotificationSettingsFeature.swift rename to Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsFeature.swift index 194a46bc..639f7064 100644 --- a/Application/Presentation/Sources/Settings/PushNotificationSettingsFeature.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsFeature.swift @@ -1,6 +1,6 @@ // // PushNotificationSettingsFeature.swift -// Presentation +// ProfileTab // // Created by opfic on 6/12/26. // diff --git a/Application/Presentation/Sources/Settings/PushNotificationSettingsView.swift b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift similarity index 99% rename from Application/Presentation/Sources/Settings/PushNotificationSettingsView.swift rename to Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift index 853b7985..d45e6b2a 100644 --- a/Application/Presentation/Sources/Settings/PushNotificationSettingsView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift @@ -1,6 +1,6 @@ // // PushNotificationSettingsView.swift -// Presentation +// ProfileTab // // Created by opfic on 5/14/25. // diff --git a/Application/Presentation/Sources/Settings/SettingsFeature.swift b/Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift similarity index 97% rename from Application/Presentation/Sources/Settings/SettingsFeature.swift rename to Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift index da60dd9e..feb71af6 100644 --- a/Application/Presentation/Sources/Settings/SettingsFeature.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/SettingsFeature.swift @@ -1,6 +1,6 @@ // // SettingsFeature.swift -// Presentation +// ProfileTab // // Created by opfic on 6/12/26. // @@ -79,8 +79,8 @@ struct SettingsFeature { @Dependency(\.deleteAuthUseCase) var deleteAuthUseCase @Dependency(\.signOutUseCase) var signOutUseCase - @Dependency(\.networkConnectivityUseCase) var networkConnectivityUseCase - @Dependency(\.systemThemeUseCase) var systemThemeUseCase + @Dependency(\.profileNetworkConnectivityUseCase) var networkConnectivityUseCase + @Dependency(\.profileSystemThemeUseCase) var systemThemeUseCase @Dependency(\.updateSystemThemeUseCase) var updateSystemThemeUseCase @Dependency(\.fetchWebPageImageDirSizeUseCase) var fetchWebPageImageDirSizeUseCase @Dependency(\.clearWebPageImageDirectoryUseCase) var clearWebPageImageDirectoryUseCase @@ -158,12 +158,12 @@ extension DependencyValues { set { self[SignOutUseCaseKey.self] = newValue } } - var networkConnectivityUseCase: ObserveNetworkConnectivityUseCase { + var profileNetworkConnectivityUseCase: ObserveNetworkConnectivityUseCase { get { self[ObserveNetworkConnectivityUseCaseKey.self] } set { self[ObserveNetworkConnectivityUseCaseKey.self] = newValue } } - var systemThemeUseCase: ObserveSystemThemeUseCase { + var profileSystemThemeUseCase: ObserveSystemThemeUseCase { get { self[ObserveSystemThemeUseCaseKey.self] } set { self[ObserveSystemThemeUseCaseKey.self] = newValue } } diff --git a/Application/Presentation/Sources/Settings/SettingsView.swift b/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift similarity index 99% rename from Application/Presentation/Sources/Settings/SettingsView.swift rename to Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift index 1b6a20d8..7b275190 100644 --- a/Application/Presentation/Sources/Settings/SettingsView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift @@ -1,6 +1,6 @@ // // SettingsView.swift -// Presentation +// ProfileTab // // Created by opfic on 5/6/25. // diff --git a/Application/Presentation/Sources/Settings/ThemeView.swift b/Application/Presentation/ProfileTab/Sources/Settings/ThemeView.swift similarity index 98% rename from Application/Presentation/Sources/Settings/ThemeView.swift rename to Application/Presentation/ProfileTab/Sources/Settings/ThemeView.swift index 63382f28..73f9a996 100644 --- a/Application/Presentation/Sources/Settings/ThemeView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/ThemeView.swift @@ -1,6 +1,6 @@ // // ThemeView.swift -// Presentation +// ProfileTab // // Created by opfic on 5/6/25. // diff --git a/Application/Presentation/Sources/Structure/Profile/ActivityKindItem.swift b/Application/Presentation/ProfileTab/Sources/Structure/Profile/ActivityKindItem.swift similarity index 98% rename from Application/Presentation/Sources/Structure/Profile/ActivityKindItem.swift rename to Application/Presentation/ProfileTab/Sources/Structure/Profile/ActivityKindItem.swift index 2c755766..eef45612 100644 --- a/Application/Presentation/Sources/Structure/Profile/ActivityKindItem.swift +++ b/Application/Presentation/ProfileTab/Sources/Structure/Profile/ActivityKindItem.swift @@ -1,6 +1,6 @@ // // ActivityKindItem.swift -// Presentation +// ProfileTab // // Created by opfic on 4/4/26. // diff --git a/Application/Presentation/Sources/Structure/Profile/HeatmapActivityItem.swift b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapActivityItem.swift similarity index 98% rename from Application/Presentation/Sources/Structure/Profile/HeatmapActivityItem.swift rename to Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapActivityItem.swift index f102ec36..cee266a5 100644 --- a/Application/Presentation/Sources/Structure/Profile/HeatmapActivityItem.swift +++ b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapActivityItem.swift @@ -1,6 +1,6 @@ // // HeatmapActivityItem.swift -// Presentation +// ProfileTab // // Created by opfic on 3/2/26. // diff --git a/Application/Presentation/Sources/Structure/Profile/HeatmapDay.swift b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapDay.swift similarity index 94% rename from Application/Presentation/Sources/Structure/Profile/HeatmapDay.swift rename to Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapDay.swift index 083dfddf..69d8ade6 100644 --- a/Application/Presentation/Sources/Structure/Profile/HeatmapDay.swift +++ b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapDay.swift @@ -1,6 +1,6 @@ // // HeatmapDay.swift -// Presentation +// ProfileTab // // Created by opfic on 3/2/26. // diff --git a/Application/Presentation/Sources/Structure/Profile/HeatmapMonth.swift b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapMonth.swift similarity index 93% rename from Application/Presentation/Sources/Structure/Profile/HeatmapMonth.swift rename to Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapMonth.swift index bfb86347..47de8b0b 100644 --- a/Application/Presentation/Sources/Structure/Profile/HeatmapMonth.swift +++ b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapMonth.swift @@ -1,6 +1,6 @@ // // HeatmapMonth.swift -// Presentation +// ProfileTab // // Created by opfic on 3/2/26. // diff --git a/Application/Presentation/Sources/Structure/Profile/HeatmapQuarter.swift b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapQuarter.swift similarity index 94% rename from Application/Presentation/Sources/Structure/Profile/HeatmapQuarter.swift rename to Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapQuarter.swift index 70c29c0e..57ea2f91 100644 --- a/Application/Presentation/Sources/Structure/Profile/HeatmapQuarter.swift +++ b/Application/Presentation/ProfileTab/Sources/Structure/Profile/HeatmapQuarter.swift @@ -1,6 +1,6 @@ // // HeatmapQuarter.swift -// Presentation +// ProfileTab // // Created by opfic on 3/2/26. // diff --git a/Application/Presentation/Sources/Structure/Profile/ProfileAvatarImageData.swift b/Application/Presentation/ProfileTab/Sources/Structure/Profile/ProfileAvatarImageData.swift similarity index 94% rename from Application/Presentation/Sources/Structure/Profile/ProfileAvatarImageData.swift rename to Application/Presentation/ProfileTab/Sources/Structure/Profile/ProfileAvatarImageData.swift index 1676c1f4..6491e18f 100644 --- a/Application/Presentation/Sources/Structure/Profile/ProfileAvatarImageData.swift +++ b/Application/Presentation/ProfileTab/Sources/Structure/Profile/ProfileAvatarImageData.swift @@ -1,6 +1,6 @@ // // ProfileAvatarImageData.swift -// Presentation +// ProfileTab // // Created by opfic on 6/17/26. // diff --git a/Application/Presentation/ProfileTab/Tests/.swiftlint.yml b/Application/Presentation/ProfileTab/Tests/.swiftlint.yml new file mode 100644 index 00000000..7bb38c07 --- /dev/null +++ b/Application/Presentation/ProfileTab/Tests/.swiftlint.yml @@ -0,0 +1 @@ +parent_config: ../../../../.swiftlint-tests.yml diff --git a/Application/Presentation/Tests/Profile/ProfileFeatureTests.swift b/Application/Presentation/ProfileTab/Tests/Profile/ProfileFeatureTests.swift similarity index 76% rename from Application/Presentation/Tests/Profile/ProfileFeatureTests.swift rename to Application/Presentation/ProfileTab/Tests/Profile/ProfileFeatureTests.swift index 80e77728..93b30288 100644 --- a/Application/Presentation/Tests/Profile/ProfileFeatureTests.swift +++ b/Application/Presentation/ProfileTab/Tests/Profile/ProfileFeatureTests.swift @@ -1,6 +1,6 @@ // // ProfileFeatureTests.swift -// PresentationTests +// ProfileTabTests // // Created by opfic on 6/11/26. // @@ -10,7 +10,7 @@ import PresentationShared import Foundation import Core import Domain -@testable import Presentation +@testable import ProfileTab @MainActor struct ProfileFeatureTests { @@ -64,6 +64,66 @@ struct ProfileFeatureTests { } } +private final class FetchTodosUseCaseSpy: FetchTodosUseCase { + var todoPage = TodoPage(items: [], nextCursor: nil) + private(set) var queries: [TodoQuery] = [] + + func execute(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage { + queries.append(query) + return todoPage + } +} + +private final class FetchUserDataUseCaseSpy: FetchUserDataUseCase { + var profile: UserProfile + + init(profile: UserProfile) { + self.profile = profile + } + + func execute() async throws -> UserProfile { + profile + } +} + +private final class FetchProfileImageDataUseCaseSpy: FetchProfileImageDataUseCase { + var data: Data + private(set) var calledURLs: [URL] = [] + + init(data: Data) { + self.data = data + } + + func execute(from url: URL) async throws -> Data { + calledURLs.append(url) + return data + } +} + +private final class UpsertStatusMessageUseCaseSpy: UpsertStatusMessageUseCase { + private(set) var messages: [String] = [] + + func execute(_ message: String) async throws { + messages.append(message) + } +} + +private final class FetchHeatmapActivityTypesUseCaseSpy: FetchHeatmapActivityTypesUseCase { + var activityTypes: [String] = [] + + func execute() -> [String] { + activityTypes + } +} + +private final class UpdateHeatmapActivityTypesUseCaseSpy: UpdateHeatmapActivityTypesUseCase { + private(set) var activityTypes: [[String]] = [] + + func execute(_ activityTypes: [String]) { + self.activityTypes.append(activityTypes) + } +} + @MainActor private struct ProfileStoreTestAdapter { private let store: TestStoreOf @@ -92,7 +152,7 @@ private struct ProfileStoreTestAdapter { $0.profileFetchImageDataUseCase = fetchProfileImageDataUseCase $0.profileFetchTodosUseCase = FetchTodosUseCaseSpy() $0.profileUpsertStatusMessageUseCase = upsertStatusMessageUseCase - $0.networkConnectivityUseCase = ObserveNetworkConnectivityUseCaseSpy() + $0.profileNetworkConnectivityUseCase = ObserveNetworkConnectivityUseCaseSpy() $0.profileFetchHeatmapActivityTypesUseCase = fetchHeatmapActivityTypesUseCase $0.profileUpdateHeatmapActivityTypesUseCase = updateHeatmapActivityTypesUseCase $0.continuousClock = ImmediateClock() diff --git a/Application/Presentation/Tests/Settings/AccountFeatureTests.swift b/Application/Presentation/ProfileTab/Tests/Settings/AccountFeatureTests.swift similarity index 99% rename from Application/Presentation/Tests/Settings/AccountFeatureTests.swift rename to Application/Presentation/ProfileTab/Tests/Settings/AccountFeatureTests.swift index 7b7b4197..945bef91 100644 --- a/Application/Presentation/Tests/Settings/AccountFeatureTests.swift +++ b/Application/Presentation/ProfileTab/Tests/Settings/AccountFeatureTests.swift @@ -1,6 +1,6 @@ // // AccountFeatureTests.swift -// PresentationTests +// ProfileTabTests // // Created by opfic on 6/11/26. // @@ -11,7 +11,7 @@ import Testing import Foundation import Domain import PresentationShared -@testable import Presentation +@testable import ProfileTab @MainActor struct AccountFeatureTests { diff --git a/Application/Presentation/Tests/Settings/PushNotificationSettingsFeatureTests.swift b/Application/Presentation/ProfileTab/Tests/Settings/PushNotificationSettingsFeatureTests.swift similarity index 99% rename from Application/Presentation/Tests/Settings/PushNotificationSettingsFeatureTests.swift rename to Application/Presentation/ProfileTab/Tests/Settings/PushNotificationSettingsFeatureTests.swift index e63890ac..6e0e1f8e 100644 --- a/Application/Presentation/Tests/Settings/PushNotificationSettingsFeatureTests.swift +++ b/Application/Presentation/ProfileTab/Tests/Settings/PushNotificationSettingsFeatureTests.swift @@ -1,6 +1,6 @@ // // PushNotificationSettingsFeatureTests.swift -// PresentationTests +// ProfileTabTests // // Created by opfic on 6/12/26. // @@ -11,7 +11,7 @@ import Testing import Foundation import Domain import PresentationShared -@testable import Presentation +@testable import ProfileTab @MainActor struct PushNotificationSettingsFeatureTests { diff --git a/Application/Presentation/Tests/Settings/SettingsFeatureTestDoubles.swift b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift similarity index 78% rename from Application/Presentation/Tests/Settings/SettingsFeatureTestDoubles.swift rename to Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift index 9afbd88a..79fca2ab 100644 --- a/Application/Presentation/Tests/Settings/SettingsFeatureTestDoubles.swift +++ b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTestDoubles.swift @@ -1,6 +1,6 @@ // // SettingsFeatureTestDoubles.swift -// PresentationTests +// ProfileTabTests // // Created by opfic on 6/12/26. // @@ -9,6 +9,28 @@ import Combine import Core import Domain +@MainActor +func waitUntil( + timeout: Duration = .seconds(1), + pollInterval: Duration = .milliseconds(20), + _ condition: @escaping () -> Bool +) async { + let continuousClock = ContinuousClock() + let deadline = continuousClock.now + timeout + + while !condition() && continuousClock.now < deadline { + try? await Task.sleep(for: pollInterval) + } +} + +final class ObserveNetworkConnectivityUseCaseSpy: ObserveNetworkConnectivityUseCase { + let currentValueSubject = CurrentValueSubject(true) + + func observe() -> AnyPublisher { + currentValueSubject.eraseToAnyPublisher() + } +} + final class DeleteAuthUseCaseSpy: DeleteAuthUseCase { var error: Error? private(set) var executeCallCount = 0 diff --git a/Application/Presentation/Tests/Settings/SettingsFeatureTests.swift b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift similarity index 98% rename from Application/Presentation/Tests/Settings/SettingsFeatureTests.swift rename to Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift index 0bb8fe1c..2c855e62 100644 --- a/Application/Presentation/Tests/Settings/SettingsFeatureTests.swift +++ b/Application/Presentation/ProfileTab/Tests/Settings/SettingsFeatureTests.swift @@ -1,6 +1,6 @@ // // SettingsFeatureTests.swift -// PresentationTests +// ProfileTabTests // // Created by opfic on 6/12/26. // @@ -9,7 +9,7 @@ import Testing import Core import Domain import PresentationShared -@testable import Presentation +@testable import ProfileTab @MainActor struct SettingsFeatureTests { @@ -203,8 +203,8 @@ private struct SettingsStoreTestAdapter { } withDependencies: { $0.deleteAuthUseCase = deleteAuthUseCase $0.signOutUseCase = signOutUseCase - $0.networkConnectivityUseCase = networkUseCase - $0.systemThemeUseCase = themeUseCase + $0.profileNetworkConnectivityUseCase = networkUseCase + $0.profileSystemThemeUseCase = themeUseCase $0.updateSystemThemeUseCase = updateThemeUseCase $0.fetchWebPageImageDirSizeUseCase = fetchDirSizeUseCase $0.clearWebPageImageDirectoryUseCase = clearDirectoryUseCase diff --git a/Application/Presentation/Project.swift b/Application/Presentation/Project.swift index ca97b301..3a59b11b 100644 --- a/Application/Presentation/Project.swift +++ b/Application/Presentation/Project.swift @@ -187,6 +187,52 @@ let project = Project( ] ) ), + .target( + name: "ProfileTab", + destinations: .iOS, + product: .staticFramework, + bundleId: "com.opfic.DevLog.ProfileTab", + infoPlist: .file(path: frameworkInfoPlistPath), + sources: ["ProfileTab/Sources/**/*.swift"], + scripts: [ + DevLogScripts.swiftLint( + sourcePath: "ProfileTab/Sources", + configPath: "ProfileTab/Sources/.swiftlint.yml" + ) + ], + dependencies: [ + .project(target: "Domain", path: "../Domain"), + .project(target: "Core", path: "../Core"), + .target(name: "PresentationShared") + ], + settings: frameworkBuildSettings + ), + .target( + name: "ProfileTabTests", + destinations: .iOS, + product: .unitTests, + bundleId: "com.opfic.DevLog.ProfileTabTests", + infoPlist: .file(path: testsInfoPlistPath), + sources: ["ProfileTab/Tests/**/*.swift"], + scripts: [ + DevLogScripts.swiftLint( + sourcePath: "ProfileTab/Tests", + configPath: "ProfileTab/Tests/.swiftlint.yml" + ) + ], + dependencies: [ + .project(target: "Domain", path: "../Domain"), + .project(target: "Core", path: "../Core"), + .target(name: "ProfileTab"), + .target(name: "PresentationShared") + ], + settings: .devlog( + base: [ + "ENABLE_USER_SCRIPT_SANDBOXING": "NO", + "TEST_TARGET_NAME": "ProfileTab" + ] + ) + ), .target( name: "Presentation", destinations: .iOS, @@ -206,6 +252,7 @@ let project = Project( .target(name: "HomeTab"), .target(name: "TodayTab"), .target(name: "NotificationTab"), + .target(name: "ProfileTab"), .target(name: "PresentationShared") ], settings: frameworkBuildSettings diff --git a/Application/Presentation/Sources/Main/MainView.swift b/Application/Presentation/Sources/Main/MainView.swift index 102ea7d5..43bb12b0 100644 --- a/Application/Presentation/Sources/Main/MainView.swift +++ b/Application/Presentation/Sources/Main/MainView.swift @@ -10,6 +10,7 @@ import Core import Domain import HomeTab import NotificationTab +import ProfileTab import PresentationShared import TodayTab @@ -339,46 +340,7 @@ struct MainView: View { } private var profileRegularDetailView: some View { - NavigationStack(path: Binding( - get: { profileViewCoordinator.router.detailPath }, - set: { profileViewCoordinator.router.detailPath = $0 } - )) { - Group { - if let profileRoute = profileViewCoordinator.router.root { - profileRegularDestinationView(profileRoute) - } else { - ContentUnavailableView( - String(localized: "profile_select_detail"), - systemImage: "person.crop.circle" - ) - } - } - .navigationDestination(for: ProfileRoute.self) { route in - profileRegularDestinationView(route) - } - } - .background(Color(.systemGroupedBackground).ignoresSafeArea()) - } - - @ViewBuilder - private func profileRegularDestinationView(_ route: ProfileRoute) -> some View { - switch route { - case .activity(let todoId): - TodoDetailView(store: profileViewCoordinator.makeTodoDetailStore(todoId: todoId)) - .id(todoId) - case .settings: - SettingsView(store: profileViewCoordinator.settingsStore) - .environment(profileViewCoordinator.router) - case .theme: - @Bindable var settingsStore = profileViewCoordinator.settingsStore - ThemeView(theme: $settingsStore.theme) - case .pushNotification: - PushNotificationSettingsView( - store: profileViewCoordinator.makePushNotificationSettingsStore() - ) - case .account: - AccountView(store: profileViewCoordinator.makeAccountStore()) - } + ProfileRegularDetailView(coordinator: profileViewCoordinator) } } diff --git a/Application/Presentation/Sources/Root/RootFeature.swift b/Application/Presentation/Sources/Root/RootFeature.swift index 8e72c049..db950f84 100644 --- a/Application/Presentation/Sources/Root/RootFeature.swift +++ b/Application/Presentation/Sources/Root/RootFeature.swift @@ -55,8 +55,8 @@ struct RootFeature { } @Dependency(\.observeAuthSessionUseCase) var observeAuthSessionUseCase - @Dependency(\.networkConnectivityUseCase) var networkConnectivityUseCase - @Dependency(\.systemThemeUseCase) var systemThemeUseCase + @Dependency(\.rootNetworkConnectivityUseCase) var networkConnectivityUseCase + @Dependency(\.rootSystemThemeUseCase) var systemThemeUseCase @Dependency(\.trackAnalyticsEventUseCase) var trackAnalyticsEventUseCase @Dependency(\.setApplicationBadgeCount) var setApplicationBadgeCount @@ -139,6 +139,16 @@ extension DependencyValues { get { self[ObserveAuthSessionUseCaseKey.self] } set { self[ObserveAuthSessionUseCaseKey.self] = newValue } } + + var rootNetworkConnectivityUseCase: ObserveNetworkConnectivityUseCase { + get { self[RootNetworkConnectivityUseCaseKey.self] } + set { self[RootNetworkConnectivityUseCaseKey.self] = newValue } + } + + var rootSystemThemeUseCase: ObserveSystemThemeUseCase { + get { self[RootSystemThemeUseCaseKey.self] } + set { self[RootSystemThemeUseCaseKey.self] = newValue } + } } private enum ObserveAuthSessionUseCaseKey: DependencyKey { @@ -151,6 +161,26 @@ private enum ObserveAuthSessionUseCaseKey: DependencyKey { } } +private enum RootNetworkConnectivityUseCaseKey: DependencyKey { + static var liveValue: ObserveNetworkConnectivityUseCase { + preconditionFailure("ObserveNetworkConnectivityUseCase must be provided.") + } + + static var testValue: ObserveNetworkConnectivityUseCase { + liveValue + } +} + +private enum RootSystemThemeUseCaseKey: DependencyKey { + static var liveValue: ObserveSystemThemeUseCase { + preconditionFailure("ObserveSystemThemeUseCase must be provided.") + } + + static var testValue: ObserveSystemThemeUseCase { + liveValue + } +} + private extension RootFeature { func clearApplicationBadgeCountEffect() -> Effect { .run { [setApplicationBadgeCount] _ in diff --git a/Application/Presentation/Sources/Root/RootView.swift b/Application/Presentation/Sources/Root/RootView.swift index 4d150275..77636d36 100644 --- a/Application/Presentation/Sources/Root/RootView.swift +++ b/Application/Presentation/Sources/Root/RootView.swift @@ -33,8 +33,8 @@ public struct RootView: View { RootFeature() } withDependencies: { $0.observeAuthSessionUseCase = sessionUseCase - $0.networkConnectivityUseCase = networkConnectivityUseCase - $0.systemThemeUseCase = systemThemeUseCase + $0.rootNetworkConnectivityUseCase = networkConnectivityUseCase + $0.rootSystemThemeUseCase = systemThemeUseCase $0.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase }) self.widgetURLTab = widgetURLTab diff --git a/Application/Presentation/Tests/Root/RootFeatureTestSupport.swift b/Application/Presentation/Tests/Root/RootFeatureTestSupport.swift index 85de38c5..6d999159 100644 --- a/Application/Presentation/Tests/Root/RootFeatureTestSupport.swift +++ b/Application/Presentation/Tests/Root/RootFeatureTestSupport.swift @@ -69,8 +69,8 @@ struct RootStoreTestAdapter: RootStateDriving { RootFeature() } withDependencies: { $0.observeAuthSessionUseCase = sessionUseCase - $0.networkConnectivityUseCase = networkConnectivityUseCase - $0.systemThemeUseCase = systemThemeUseCase + $0.rootNetworkConnectivityUseCase = networkConnectivityUseCase + $0.rootSystemThemeUseCase = systemThemeUseCase $0.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase $0.setApplicationBadgeCount = { count in try await badgeCountSpy.setBadgeCount(count) diff --git a/docs/graph.png b/docs/graph.png index 3232c54d..7e980b7a 100644 Binary files a/docs/graph.png and b/docs/graph.png differ