Skip to content

Commit ad55cb1

Browse files
committed
feat: store 1차 구현
1 parent 94a5dc0 commit ad55cb1

4 files changed

Lines changed: 531 additions & 16 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//
2+
// RootFeature.swift
3+
// DevLogPresentation
4+
//
5+
// Created by opfic on 6/17/26.
6+
//
7+
8+
import Combine
9+
import ComposableArchitecture
10+
import DevLogCore
11+
import DevLogDomain
12+
import Foundation
13+
14+
@Reducer
15+
struct RootFeature {
16+
private enum CancelID: Hashable {
17+
case networkConnectivity
18+
case session
19+
case theme
20+
}
21+
22+
@ObservableState
23+
struct State: Equatable {
24+
var showAlert = false
25+
var alertTitle = ""
26+
var alertMessage = ""
27+
var isNetworkConnected = true
28+
var signIn: Bool?
29+
var theme: SystemTheme = .automatic
30+
var isObservingNetworkConnectivity = false
31+
var isObservingSession = false
32+
var isObservingTheme = false
33+
}
34+
35+
enum Action: Equatable {
36+
case view(ViewAction)
37+
case store(StoreAction)
38+
39+
enum ViewAction: Equatable {
40+
case onAppear
41+
case setAlert(Bool)
42+
}
43+
44+
enum StoreAction: Equatable {
45+
case networkStatusChanged(Bool)
46+
case setTheme(SystemTheme)
47+
case didLogined(Bool)
48+
}
49+
}
50+
51+
@Dependency(\.observeAuthSessionUseCase) var observeAuthSessionUseCase
52+
@Dependency(\.networkConnectivityUseCase) var networkConnectivityUseCase
53+
@Dependency(\.systemThemeUseCase) var systemThemeUseCase
54+
@Dependency(\.trackAnalyticsEventUseCase) var trackAnalyticsEventUseCase
55+
@Dependency(\.setApplicationBadgeCount) var setApplicationBadgeCount
56+
57+
var body: some ReducerOf<Self> {
58+
Reduce { state, action in
59+
switch action {
60+
case .view(.onAppear):
61+
var effect = clearApplicationBadgeCountEffect()
62+
63+
if !state.isObservingNetworkConnectivity {
64+
state.isObservingNetworkConnectivity = true
65+
effect = .merge(effect, observeNetworkConnectivityEffect())
66+
}
67+
68+
if !state.isObservingSession {
69+
state.isObservingSession = true
70+
effect = .merge(effect, observeSessionEffect())
71+
}
72+
73+
if !state.isObservingTheme {
74+
state.isObservingTheme = true
75+
effect = .merge(effect, observeThemeEffect())
76+
}
77+
78+
return effect
79+
case .view(.setAlert(let isPresented)):
80+
Self.setAlert(&state, isPresented: isPresented)
81+
case .store(.networkStatusChanged(let isConnected)):
82+
let wasConnected = state.isNetworkConnected
83+
state.isNetworkConnected = isConnected
84+
if wasConnected && !isConnected {
85+
Self.setAlert(&state, isPresented: true)
86+
}
87+
case .store(.setTheme(let theme)):
88+
state.theme = theme
89+
case .store(.didLogined(let result)):
90+
state.signIn = result
91+
if !result {
92+
return trackLoginScreenEffect()
93+
}
94+
}
95+
96+
return .none
97+
}
98+
}
99+
}
100+
101+
extension DependencyValues {
102+
var observeAuthSessionUseCase: ObserveAuthSessionUseCase {
103+
get { self[ObserveAuthSessionUseCaseKey.self] }
104+
set { self[ObserveAuthSessionUseCaseKey.self] = newValue }
105+
}
106+
}
107+
108+
private enum ObserveAuthSessionUseCaseKey: DependencyKey {
109+
static var liveValue: ObserveAuthSessionUseCase {
110+
preconditionFailure("ObserveAuthSessionUseCase must be provided.")
111+
}
112+
113+
static var testValue: ObserveAuthSessionUseCase {
114+
liveValue
115+
}
116+
}
117+
118+
private extension RootFeature {
119+
func clearApplicationBadgeCountEffect() -> Effect<Action> {
120+
.run { [setApplicationBadgeCount] _ in
121+
try? await setApplicationBadgeCount(0)
122+
}
123+
}
124+
125+
func observeNetworkConnectivityEffect() -> Effect<Action> {
126+
.publisher { [networkConnectivityUseCase] in
127+
networkConnectivityUseCase.observe()
128+
.map { Action.store(.networkStatusChanged($0)) }
129+
}
130+
.cancellable(id: CancelID.networkConnectivity, cancelInFlight: true)
131+
}
132+
133+
func observeSessionEffect() -> Effect<Action> {
134+
.publisher { [observeAuthSessionUseCase] in
135+
observeAuthSessionUseCase.observe()
136+
.removeDuplicates()
137+
.map { Action.store(.didLogined($0)) }
138+
}
139+
.cancellable(id: CancelID.session, cancelInFlight: true)
140+
}
141+
142+
func observeThemeEffect() -> Effect<Action> {
143+
.publisher { [systemThemeUseCase] in
144+
systemThemeUseCase.observe()
145+
.removeDuplicates()
146+
.map { Action.store(.setTheme($0)) }
147+
}
148+
.cancellable(id: CancelID.theme, cancelInFlight: true)
149+
}
150+
151+
func trackLoginScreenEffect() -> Effect<Action> {
152+
.run { [trackAnalyticsEventUseCase] _ in
153+
trackAnalyticsEventUseCase?.execute(.screenView("login"))
154+
}
155+
}
156+
157+
static func setAlert(_ state: inout State, isPresented: Bool) {
158+
state.alertTitle = String(localized: "root_network_disconnected_title")
159+
state.alertMessage = String(localized: "root_network_disconnected_message")
160+
state.showAlert = isPresented
161+
}
162+
}

Application/DevLogPresentation/Sources/Root/RootView.swift

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import DevLogDomain
1313

1414
public struct RootView: View {
1515
@Environment(\.diContainer) var container: DIContainer
16-
@State var viewModel: RootViewModel
16+
@State private var store: StoreOf<RootFeature>
1717
@State private var selectedRoute: Route?
1818
@State private var selectedMainTab = MainTab.home
1919
private let widgetURLTab: (URL) -> MainTab?
@@ -31,12 +31,14 @@ public struct RootView: View {
3131
pushNotificationTodoIdPublisher: AnyPublisher<String, Never>,
3232
clearPushNotificationRoute: @escaping () -> Void
3333
) {
34-
self._viewModel = State(initialValue: RootViewModel(
35-
sessionUseCase: sessionUseCase,
36-
networkConnectivityUseCase: networkConnectivityUseCase,
37-
systemThemeUseCase: systemThemeUseCase,
38-
trackAnalyticsEventUseCase: trackAnalyticsEventUseCase
39-
))
34+
self._store = State(initialValue: Store(initialState: RootFeature.State()) {
35+
RootFeature()
36+
} withDependencies: {
37+
$0.observeAuthSessionUseCase = sessionUseCase
38+
$0.networkConnectivityUseCase = networkConnectivityUseCase
39+
$0.systemThemeUseCase = systemThemeUseCase
40+
$0.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase
41+
})
4042
self.widgetURLTab = widgetURLTab
4143
self.windowEvent = windowEvent
4244
self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher
@@ -46,7 +48,7 @@ public struct RootView: View {
4648
public var body: some View {
4749
ZStack {
4850
Color(UIColor.systemGroupedBackground).ignoresSafeArea()
49-
if let signIn = viewModel.state.signIn {
51+
if let signIn = store.signIn {
5052
if signIn {
5153
MainView(
5254
container: container,
@@ -58,17 +60,17 @@ public struct RootView: View {
5860
}
5961
}
6062
}
61-
.preferredColorScheme(viewModel.state.theme.colorScheme)
62-
.onAppear { viewModel.send(.onAppear) }
63-
.onChange(of: viewModel.state.signIn) { _, value in
63+
.preferredColorScheme(store.theme.colorScheme)
64+
.onAppear { store.send(.view(.onAppear)) }
65+
.onChange(of: store.signIn) { _, value in
6466
guard let value else { return }
6567
if value {
6668
selectedMainTab = .home
6769
}
6870
}
6971
.onOpenURL { url in
7072
guard let mainTab = widgetURLTab(url) else { return }
71-
switch viewModel.state.signIn {
73+
switch store.signIn {
7274
case .some(false):
7375
break
7476
case .some(true):
@@ -77,13 +79,13 @@ public struct RootView: View {
7779
break
7880
}
7981
}
80-
.alert(viewModel.state.alertTitle, isPresented: Binding(
81-
get: { viewModel.state.showAlert },
82-
set: { viewModel.send(.setAlert($0)) }
82+
.alert(store.alertTitle, isPresented: Binding(
83+
get: { store.showAlert },
84+
set: { store.send(.view(.setAlert($0))) }
8385
)) {
8486
Button(String(localized: "common_close"), role: .cancel) { }
8587
} message: {
86-
Text(viewModel.state.alertMessage)
88+
Text(store.alertMessage)
8789
}
8890
.sheet(item: $selectedRoute) { route in
8991
switch route {

0 commit comments

Comments
 (0)