Skip to content

Commit 4e2af63

Browse files
committed
Merge branch 'feature/forum' into develop
2 parents 05fc84b + 48b0531 commit 4e2af63

File tree

19 files changed

+1079
-68
lines changed

19 files changed

+1079
-68
lines changed

Packages/Package.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let package = Package(
1212
.library(name: "AppFeature", targets: ["AppFeature"]),
1313
.library(name: "ArticlesListFeature", targets: ["ArticlesListFeature"]),
1414
.library(name: "ArticleFeature", targets: ["ArticleFeature"]),
15+
.library(name: "ForumFeature", targets: ["ForumFeature"]),
1516
.library(name: "MenuFeature", targets: ["MenuFeature"]),
1617
.library(name: "AuthFeature", targets: ["AuthFeature"]),
1718
.library(name: "ProfileFeature", targets: ["ProfileFeature"]),
@@ -52,6 +53,7 @@ let package = Package(
5253
dependencies: [
5354
"ArticlesListFeature",
5455
"ArticleFeature",
56+
"ForumFeature",
5557
"MenuFeature",
5658
"AuthFeature",
5759
"ProfileFeature",
@@ -93,6 +95,19 @@ let package = Package(
9395
.product(name: "YouTubePlayerKit", package: "YouTubePlayerKit")
9496
]
9597
),
98+
.target(
99+
name: "ForumFeature",
100+
dependencies: [
101+
"Models",
102+
"SharedUI",
103+
"APIClient",
104+
"CacheClient",
105+
"AnalyticsClient",
106+
"ParsingClient",
107+
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
108+
.product(name: "NukeUI", package: "nuke")
109+
]
110+
),
96111
.target(
97112
name: "MenuFeature",
98113
dependencies: [

Packages/Sources/AppFeature/AppFeature.swift

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import SwiftUI
99
import ComposableArchitecture
1010
import ArticlesListFeature
1111
import ArticleFeature
12+
import ForumFeature
1213
import MenuFeature
1314
import AuthFeature
1415
import ProfileFeature
@@ -24,9 +25,18 @@ public struct AppFeature: Sendable {
2425
// MARK: - Path
2526

2627
@Reducer(state: .equatable)
27-
public enum Path {
28+
public enum ArticlesPath {
2829
case article(ArticleFeature)
29-
case menu(MenuFeature)
30+
case profile(ProfileFeature)
31+
}
32+
33+
@Reducer(state: .equatable)
34+
public enum ForumPath {
35+
36+
}
37+
38+
@Reducer(state: .equatable)
39+
public enum MenuPath {
3040
case auth(AuthFeature)
3141
case profile(ProfileFeature)
3242
case settings(SettingsFeature)
@@ -36,9 +46,15 @@ public struct AppFeature: Sendable {
3646

3747
@ObservableState
3848
public struct State: Equatable {
39-
public var path: StackState<Path.State>
4049
public var appDelegate: AppDelegateFeature.State
50+
51+
public var articlesPath: StackState<ArticlesPath.State>
52+
public var forumPath: StackState<ForumPath.State>
53+
public var menuPath: StackState<MenuPath.State>
54+
4155
public var articlesList: ArticlesListFeature.State
56+
public var forum: ForumFeature.State
57+
public var menu: MenuFeature.State
4258

4359
public var showToast: Bool
4460
public var toast: ToastInfo
@@ -50,15 +66,26 @@ public struct AppFeature: Sendable {
5066
}
5167

5268
public init(
53-
path: StackState<Path.State> = StackState(),
5469
appDelegate: AppDelegateFeature.State = AppDelegateFeature.State(),
70+
articlesPath: StackState<ArticlesPath.State> = StackState(),
71+
forumPath: StackState<ForumPath.State> = StackState(),
72+
menuPath: StackState<MenuPath.State> = StackState(),
5573
articlesList: ArticlesListFeature.State = ArticlesListFeature.State(),
74+
forum: ForumFeature.State = ForumFeature.State(),
75+
menu: MenuFeature.State = MenuFeature.State(),
5676
showToast: Bool = false,
5777
toast: ToastInfo = ToastInfo(screen: .articlesList, message: "")
5878
) {
59-
self.path = path
6079
self.appDelegate = appDelegate
80+
81+
self.articlesPath = articlesPath
82+
self.forumPath = forumPath
83+
self.menuPath = menuPath
84+
6185
self.articlesList = articlesList
86+
self.forum = forum
87+
self.menu = menu
88+
6289
self.showToast = showToast
6390
self.toast = toast
6491
}
@@ -68,9 +95,16 @@ public struct AppFeature: Sendable {
6895

6996
public enum Action: BindableAction {
7097
case appDelegate(AppDelegateFeature.Action)
71-
case path(StackActionOf<Path>)
98+
99+
case articlesPath(StackActionOf<ArticlesPath>)
100+
case forumPath(StackActionOf<ForumPath>)
101+
case menuPath(StackActionOf<MenuPath>)
102+
72103
case articlesList(ArticlesListFeature.Action)
73-
case binding(BindingAction<State>) // TODO: Do I need it?
104+
case forum(ForumFeature.Action)
105+
case menu(MenuFeature.Action)
106+
107+
case binding(BindingAction<State>) // For Toast
74108
case deeplink(URL)
75109
case scenePhaseDidChange(from: ScenePhase, to: ScenePhase)
76110
}
@@ -92,6 +126,14 @@ public struct AppFeature: Sendable {
92126
ArticlesListFeature()
93127
}
94128

129+
Scope(state: \.forum, action: \.forum) {
130+
ForumFeature()
131+
}
132+
133+
Scope(state: \.menu, action: \.menu) {
134+
MenuFeature()
135+
}
136+
95137
Reduce { state, action in
96138
switch action {
97139

@@ -123,7 +165,7 @@ public struct AppFeature: Sendable {
123165
let id = Int(match!.output.1)!
124166

125167
let articlePreview = ArticlePreview.outerDeeplink(id: id, imageUrl: imageUrl, title: title)
126-
state.path.append(.article(ArticleFeature.State(articlePreview: articlePreview)))
168+
state.articlesPath.append(.article(ArticleFeature.State(articlePreview: articlePreview)))
127169

128170
default: // For new deeplink usage cases
129171
break
@@ -141,14 +183,25 @@ public struct AppFeature: Sendable {
141183
return .none
142184
}
143185

144-
// MARK: - ArticlesList
186+
// MARK: - Default
187+
188+
case .articlesList, .forum, .menu:
189+
return .none
145190

146-
case .articlesList(.menuTapped):
147-
state.path.append(.menu(MenuFeature.State()))
191+
case .articlesPath, .forumPath, .menuPath:
148192
return .none
193+
}
194+
}
195+
196+
// MARK: - Article Path
197+
198+
Reduce { state, action in
199+
switch action {
200+
201+
// MARK: - Articles List
149202

150203
case let .articlesList(.articleTapped(articlePreview)):
151-
state.path.append(.article(ArticleFeature.State(articlePreview: articlePreview)))
204+
state.articlesPath.append(.article(ArticleFeature.State(articlePreview: articlePreview)))
152205
return .none
153206

154207
case let .articlesList(.cellMenuOpened(_, action)):
@@ -164,9 +217,9 @@ public struct AppFeature: Sendable {
164217
case .articlesList:
165218
return .none
166219

167-
// MARK: - Article
220+
// MARK: Article
168221

169-
case let .path(.element(id: _, action: .article(.menuActionTapped(action)))):
222+
case let .articlesPath(.element(id: _, action: .article(.menuActionTapped(action)))):
170223
switch action {
171224
case .copyLink, .report:
172225
state.toast = ToastInfo(screen: .article, message: action.rawValue)
@@ -176,44 +229,62 @@ public struct AppFeature: Sendable {
176229
state.showToast = true
177230
return .none
178231

179-
case let .path(.element(id: _, action: .article(.delegate(.handleDeeplink(id))))):
232+
case let .articlesPath(.element(id: _, action: .article(.delegate(.handleDeeplink(id))))):
180233
let articlePreview = ArticlePreview.innerDeeplink(id: id)
181-
state.path.append(.article(ArticleFeature.State(articlePreview: articlePreview)))
234+
state.articlesPath.append(.article(ArticleFeature.State(articlePreview: articlePreview)))
182235
return .none
183236

184-
case let .path(.element(id: _, action: .article(.delegate(.commentHeaderTapped(id))))):
185-
state.path.append(.profile(ProfileFeature.State(userId: id)))
237+
case let .articlesPath(.element(id: _, action: .article(.delegate(.commentHeaderTapped(id))))):
238+
state.articlesPath.append(.profile(ProfileFeature.State(userId: id)))
239+
return .none
240+
241+
default:
242+
return .none
243+
}
244+
}
245+
.forEach(\.articlesPath, action: \.articlesPath)
246+
247+
// MARK: - Forum Path
248+
249+
Reduce { state, action in
250+
switch action {
251+
default:
186252
return .none
253+
}
254+
}
255+
.forEach(\.forumPath, action: \.forumPath)
256+
257+
// MARK: - Menu Path
258+
259+
Reduce { state, action in
260+
switch action {
187261

188-
// MARK: - Menu
262+
// MARK: Menu
189263

190-
case .path(.element(id: _, action: .menu(.delegate(.openAuth)))):
191-
state.path.append(.auth(AuthFeature.State()))
264+
case .menu(.delegate(.openAuth)):
265+
state.menuPath.append(.auth(AuthFeature.State()))
192266
return .none
193267

194-
case let .path(.element(id: _, action: .menu(.delegate(.openProfile(id: id))))):
195-
state.path.append(.profile(ProfileFeature.State(userId: id)))
268+
case let .menu(.delegate(.openProfile(id: id))):
269+
state.menuPath.append(.profile(ProfileFeature.State(userId: id)))
196270
return .none
197271

198-
case .path(.element(id: _, action: .menu(.settingsTapped))):
199-
state.path.append(.settings(SettingsFeature.State()))
272+
case .menu(.settingsTapped):
273+
state.menuPath.append(.settings(SettingsFeature.State()))
200274
return .none
201275

202-
// MARK: - Auth
276+
// MARK: Auth
203277

204-
case let .path(.element(id: id, action: .auth(.delegate(.loginSuccess(userId: userId))))):
278+
case let .menuPath(.element(id: id, action: .auth(.delegate(.loginSuccess(userId: userId))))):
205279
// TODO: How to make seamless animation?
206-
state.path.pop(from: id)
207-
state.path.append(.profile(ProfileFeature.State(userId: userId)))
280+
state.menuPath.pop(from: id)
281+
state.menuPath.append(.profile(ProfileFeature.State(userId: userId)))
208282
return .none
209283

210-
// MARK: - Default
211-
212-
case .path:
284+
default:
213285
return .none
214286
}
215-
216287
}
217-
.forEach(\.path, action: \.path)
288+
.forEach(\.menuPath, action: \.menuPath)
218289
}
219290
}

Packages/Sources/AppFeature/AppView.swift

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import SwiftUI
99
import ComposableArchitecture
1010
import ArticlesListFeature
1111
import ArticleFeature
12+
import ForumFeature
1213
import MenuFeature
1314
import AuthFeature
1415
import ProfileFeature
@@ -25,31 +26,90 @@ public struct AppView: View {
2526

2627
public var body: some View {
2728
WithPerceptionTracking {
28-
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
29-
ArticlesListScreen(store: store.scope(state: \.articlesList, action: \.articlesList))
30-
} destination: { store in
31-
switch store.case {
32-
case let .article(store):
33-
ArticleScreen(store: store)
34-
35-
case let .menu(store):
36-
MenuScreen(store: store)
37-
38-
case let .auth(store):
39-
AuthScreen(store: store)
40-
41-
case let .profile(store):
42-
ProfileScreen(store: store)
43-
44-
case let .settings(store):
45-
SettingsScreen(store: store)
46-
}
29+
TabView {
30+
ArticlesListTab()
31+
32+
ForumTab()
33+
34+
MenuTab()
4735
}
4836
.toast(isPresenting: $store.showToast) {
4937
AlertToast(displayMode: .hud, type: .regular, title: store.toast.message, bundle: store.localizationBundle)
5038
}
5139
}
5240
}
41+
42+
// MARK: - Articles List Tab
43+
44+
@ViewBuilder
45+
private func ArticlesListTab() -> some View {
46+
NavigationStack(path: $store.scope(state: \.articlesPath, action: \.articlesPath)) {
47+
ArticlesListScreen(store: store.scope(state: \.articlesList, action: \.articlesList))
48+
} destination: { store in
49+
switch store.case {
50+
case let .article(store):
51+
ArticleScreen(store: store)
52+
53+
case let .profile(store):
54+
ProfileScreen(store: store)
55+
}
56+
}
57+
.tabItem {
58+
Label {
59+
Text("Articles", bundle: .module)
60+
} icon: {
61+
Image(systemSymbol: .newspaperFill)
62+
}
63+
}
64+
}
65+
66+
// MARK: - Forum Tab
67+
68+
@ViewBuilder
69+
private func ForumTab() -> some View {
70+
NavigationStack(path: $store.scope(state: \.forumPath, action: \.forumPath)) {
71+
ForumScreen(store: store.scope(state: \.forum, action: \.forum))
72+
} destination: { store in
73+
switch store.case {
74+
default:
75+
return EmptyView()
76+
}
77+
}
78+
.tabItem {
79+
Label {
80+
Text("Forum", bundle: .module)
81+
} icon: {
82+
Image(systemSymbol: .bubbleLeftAndBubbleRightFill)
83+
}
84+
}
85+
}
86+
87+
// MARK: - Menu Tab
88+
89+
@ViewBuilder
90+
private func MenuTab() -> some View {
91+
NavigationStack(path: $store.scope(state: \.menuPath, action: \.menuPath)) {
92+
MenuScreen(store: store.scope(state: \.menu, action: \.menu))
93+
} destination: { store in
94+
switch store.case {
95+
case let .auth(store):
96+
AuthScreen(store: store)
97+
98+
case let .profile(store):
99+
ProfileScreen(store: store)
100+
101+
case let .settings(store):
102+
SettingsScreen(store: store)
103+
}
104+
}
105+
.tabItem {
106+
Label {
107+
Text("Menu", bundle: .module)
108+
} icon: {
109+
Image(systemSymbol: .line3Horizontal)
110+
}
111+
}
112+
}
53113
}
54114

55115
#Preview {

0 commit comments

Comments
 (0)