Skip to content

Commit 3456377

Browse files
authored
Бейдж с количеством непрочитанных сообщений в таббаре (#276)
* MVP * Сделал вьюмодель для экрана с диалогами * Доработки + доработал цвета в таббаре и навбаре
1 parent 6f12933 commit 3456377

File tree

6 files changed

+154
-81
lines changed

6 files changed

+154
-81
lines changed

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
67627750283A3A54009C203F /* JournalsListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6762774F283A3A54009C203F /* JournalsListScreen.swift */; };
3737
67627755283A4C77009C203F /* JournalEntriesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67627754283A4C77009C203F /* JournalEntriesScreen.swift */; };
3838
6762775B283A87AD009C203F /* JournalCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6762775A283A87AD009C203F /* JournalCell.swift */; };
39+
6764D6382D52009F00699007 /* DialogsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6764D6372D52009F00699007 /* DialogsViewModel.swift */; };
3940
6765B2562D451771006164AB /* UIImage+toMediaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6765B2552D451771006164AB /* UIImage+toMediaFile.swift */; };
4041
6765B2582D4544C8006164AB /* MainUserProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6765B2572D4544C8006164AB /* MainUserProfileScreen.swift */; };
4142
6765B25B2D455D5C006164AB /* ProfileViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6765B25A2D455D5C006164AB /* ProfileViews.swift */; };
@@ -123,6 +124,7 @@
123124
6762774F283A3A54009C203F /* JournalsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalsListScreen.swift; sourceTree = "<group>"; };
124125
67627754283A4C77009C203F /* JournalEntriesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalEntriesScreen.swift; sourceTree = "<group>"; };
125126
6762775A283A87AD009C203F /* JournalCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JournalCell.swift; sourceTree = "<group>"; };
127+
6764D6372D52009F00699007 /* DialogsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogsViewModel.swift; sourceTree = "<group>"; };
126128
6765B2552D451771006164AB /* UIImage+toMediaFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+toMediaFile.swift"; sourceTree = "<group>"; };
127129
6765B2572D4544C8006164AB /* MainUserProfileScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserProfileScreen.swift; sourceTree = "<group>"; };
128130
6765B25A2D455D5C006164AB /* ProfileViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViews.swift; sourceTree = "<group>"; };
@@ -227,6 +229,7 @@
227229
67419AD5282E8E7C004F5339 /* Messages */ = {
228230
isa = PBXGroup;
229231
children = (
232+
6764D6372D52009F00699007 /* DialogsViewModel.swift */,
230233
67D916802838E2460098D3CB /* DialogsListScreen.swift */,
231234
67D916852838F0DD0098D3CB /* DialogScreen.swift */,
232235
);
@@ -646,6 +649,7 @@
646649
6798AA84280C0F7D00DB76F1 /* EditProfileScreen.swift in Sources */,
647650
6798AA73280B43FE00DB76F1 /* LoginScreen.swift in Sources */,
648651
67D9169628396C1E0098D3CB /* SendMessageScreen.swift in Sources */,
652+
6764D6382D52009F00699007 /* DialogsViewModel.swift in Sources */,
649653
6747575928128603002F0A24 /* ParkDetailScreen.swift in Sources */,
650654
675EC6572815433600C2E229 /* UsersListScreen.swift in Sources */,
651655
675EC65F2815532800C2E229 /* EventFormScreen.swift in Sources */,
@@ -852,7 +856,7 @@
852856
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
853857
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
854858
CODE_SIGN_STYLE = Automatic;
855-
CURRENT_PROJECT_VERSION = 7;
859+
CURRENT_PROJECT_VERSION = 8;
856860
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
857861
DEVELOPMENT_TEAM = CR68PP2Z3F;
858862
ENABLE_PREVIEWS = YES;
@@ -903,7 +907,7 @@
903907
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
904908
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
905909
CODE_SIGN_STYLE = Automatic;
906-
CURRENT_PROJECT_VERSION = 7;
910+
CURRENT_PROJECT_VERSION = 8;
907911
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
908912
DEVELOPMENT_TEAM = CR68PP2Z3F;
909913
ENABLE_PREVIEWS = YES;

SwiftUI-WorkoutApp/Libraries/SWNetworkClient/Sources/SWNetworkClient/SWClient.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public struct SWClient: Sendable {
6262
/// - Parameters:
6363
/// - userID: `id` пользователя
6464
/// - Returns: вся информация о пользователе
65-
@discardableResult
6665
public func getUserByID(_ userID: Int) async throws -> UserResponse {
6766
let endpoint = Endpoint.getUser(id: userID)
6867
return try await makeResult(for: endpoint)

SwiftUI-WorkoutApp/Screens/Messages/DialogsListScreen.swift

Lines changed: 21 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import SWUtils
88
struct DialogsListScreen: View {
99
@Environment(\.isNetworkConnected) private var isNetworkConnected
1010
@EnvironmentObject private var defaults: DefaultsService
11-
@State private var dialogs = [DialogResponse]()
11+
@EnvironmentObject private var viewModel: DialogsViewModel
1212
@State private var selectedDialog: DialogResponse?
13-
@State private var isLoading = false
1413
@State private var indexToDelete: Int?
1514
@State private var openFriendList = false
1615
@State private var showDeleteConfirmation = false
@@ -34,22 +33,22 @@ struct DialogsListScreen: View {
3433
.navigationTitle("Сообщения")
3534
}
3635
.navigationViewStyle(.stack)
37-
.task { await askForDialogs() }
36+
.onChange(of: defaults.isAuthorized, perform: viewModel.clearDialogsOnLogout)
37+
.task(id: defaults.isAuthorized) { await askForDialogs() }
3838
}
3939
}
4040

4141
private extension DialogsListScreen {
4242
var authorizedContentView: some View {
4343
dialogList
4444
.overlay { emptyContentView }
45-
.loadingOverlay(if: isLoading)
45+
.loadingOverlay(if: viewModel.isLoading)
4646
.background(Color.swBackground)
4747
.confirmationDialog(
4848
.init(Constants.Alert.deleteDialog),
4949
isPresented: $showDeleteConfirmation,
5050
titleVisibility: .visible
5151
) { deleteDialogButton }
52-
.refreshable { await askForDialogs(refresh: true) }
5352
.toolbar {
5453
ToolbarItem(placement: .topBarLeading) {
5554
refreshButton
@@ -58,9 +57,6 @@ private extension DialogsListScreen {
5857
friendListButton
5958
}
6059
}
61-
.onDisappear {
62-
[refreshTask, deleteDialogTask].forEach { $0?.cancel() }
63-
}
6460
}
6561

6662
var refreshButton: some View {
@@ -71,8 +67,8 @@ private extension DialogsListScreen {
7167
} label: {
7268
Icons.Regular.refresh.view
7369
}
74-
.opacity(showEmptyView || !DeviceOSVersionChecker.iOS16Available ? 1 : 0)
75-
.disabled(isLoading)
70+
.opacity(viewModel.showEmptyView ? 1 : 0)
71+
.disabled(viewModel.isLoading || !isNetworkConnected)
7672
}
7773

7874
var friendListButton: some View {
@@ -86,28 +82,24 @@ private extension DialogsListScreen {
8682
Icons.Regular.plus.view
8783
.symbolVariant(.circle)
8884
}
89-
.opacity(hasFriends || !dialogs.isEmpty ? 1 : 0)
85+
.opacity(hasFriends || viewModel.hasDialogs ? 1 : 0)
9086
.disabled(!isNetworkConnected)
9187
}
9288

9389
var emptyContentView: some View {
9490
EmptyContentView(
9591
mode: .dialogs,
96-
action: emptyViewAction
92+
action: { openFriendList.toggle() }
9793
)
98-
.opacity(showEmptyView ? 1 : 0)
99-
}
100-
101-
var showEmptyView: Bool {
102-
dialogs.isEmpty && !isLoading
94+
.opacity(viewModel.showEmptyView ? 1 : 0)
10395
}
10496

10597
@ViewBuilder
10698
var dialogList: some View {
10799
ZStack {
108100
Color.swBackground
109101
List {
110-
ForEach(dialogs) { model in
102+
ForEach(viewModel.dialogs) { model in
111103
dialogListItem(model)
112104
.listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16))
113105
.listRowBackground(Color.swBackground)
@@ -116,9 +108,10 @@ private extension DialogsListScreen {
116108
.onDelete { initiateDeletion(at: $0) }
117109
}
118110
.listStyle(.plain)
119-
.opacity(dialogs.isEmpty ? 0 : 1)
111+
.opacity(viewModel.hasDialogs ? 1 : 0)
112+
.refreshable { await askForDialogs(refresh: true) }
120113
}
121-
.animation(.default, value: dialogs.count)
114+
.animation(.default, value: viewModel.dialogs.count)
122115
.background(
123116
NavigationLink(
124117
destination: lazyDestination,
@@ -130,7 +123,12 @@ private extension DialogsListScreen {
130123
@ViewBuilder
131124
var lazyDestination: some View {
132125
if let selectedDialog {
133-
DialogScreen(dialog: selectedDialog) { markAsRead($0) }
126+
DialogScreen(
127+
dialog: selectedDialog,
128+
markedAsReadClbk: { dialog in
129+
viewModel.markAsRead(dialog, defaults: defaults)
130+
}
131+
)
134132
}
135133
}
136134

@@ -162,39 +160,12 @@ private extension DialogsListScreen {
162160
defaults.hasFriends
163161
}
164162

165-
func emptyViewAction() {
166-
openFriendList.toggle()
167-
}
168-
169-
func markAsRead(_ dialog: DialogResponse) {
170-
dialogs = dialogs.map { item in
171-
if item.id == dialog.id {
172-
var updatedDialog = dialog
173-
updatedDialog.unreadMessagesCount = 0
174-
return updatedDialog
175-
} else {
176-
return item
177-
}
178-
}
179-
guard dialog.unreadMessagesCount > 0,
180-
defaults.unreadMessagesCount >= dialog.unreadMessagesCount
181-
else { return }
182-
let newValue = defaults.unreadMessagesCount - dialog.unreadMessagesCount
183-
defaults.saveUnreadMessagesCount(newValue)
184-
}
185-
186163
func askForDialogs(refresh: Bool = false) async {
187-
guard defaults.isAuthorized else { return }
188-
if isLoading || (!dialogs.isEmpty && !refresh) { return }
189-
if !refresh { isLoading = true }
190164
do {
191-
dialogs = try await client.getDialogs()
192-
let unreadMessagesCount = dialogs.map(\.unreadMessagesCount).reduce(0, +)
193-
defaults.saveUnreadMessagesCount(unreadMessagesCount)
165+
try await viewModel.askForDialogs(refresh: refresh, defaults: defaults)
194166
} catch {
195167
SWAlert.shared.presentDefaultUIKit(message: error.localizedDescription)
196168
}
197-
isLoading = false
198169
}
199170

200171
func initiateDeletion(at indexSet: IndexSet) {
@@ -204,17 +175,11 @@ private extension DialogsListScreen {
204175

205176
func deleteAction(at index: Int?) {
206177
deleteDialogTask = Task {
207-
guard let index, !isLoading else { return }
208-
isLoading = true
209178
do {
210-
let dialogID = dialogs[index].id
211-
if try await client.deleteDialog(dialogID) {
212-
dialogs.remove(at: index)
213-
}
179+
try await viewModel.deleteDialog(at: index, defaults: defaults)
214180
} catch {
215181
SWAlert.shared.presentDefaultUIKit(message: error.localizedDescription)
216182
}
217-
isLoading = false
218183
}
219184
}
220185
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
import SWModels
3+
import SWNetworkClient
4+
import SWUtils
5+
6+
final class DialogsViewModel: ObservableObject {
7+
@Published private(set) var dialogs = [DialogResponse]()
8+
@Published private(set) var isLoading = false
9+
var hasDialogs: Bool { !dialogs.isEmpty }
10+
var showEmptyView: Bool { !hasDialogs && !isLoading }
11+
12+
@MainActor
13+
func askForDialogs(
14+
refresh: Bool = false,
15+
defaults: DefaultsService
16+
) async throws {
17+
guard defaults.isAuthorized else { return }
18+
if isLoading || (!dialogs.isEmpty && !refresh) { return }
19+
if !refresh || dialogs.isEmpty { isLoading = true }
20+
dialogs = try await SWClient(with: defaults).getDialogs()
21+
let unreadMessagesCount = dialogs.map(\.unreadMessagesCount).reduce(0, +)
22+
defaults.saveUnreadMessagesCount(unreadMessagesCount)
23+
isLoading = false
24+
}
25+
26+
@MainActor
27+
func deleteDialog(at index: Int?, defaults: DefaultsService) async throws {
28+
guard let index, !isLoading else { return }
29+
isLoading = true
30+
let dialogID = dialogs[index].id
31+
if try await SWClient(with: defaults).deleteDialog(dialogID) {
32+
dialogs.remove(at: index)
33+
}
34+
isLoading = false
35+
}
36+
37+
@MainActor
38+
func markAsRead(_ dialog: DialogResponse, defaults: DefaultsService) {
39+
dialogs = dialogs.map { item in
40+
if item.id == dialog.id {
41+
var updatedDialog = dialog
42+
updatedDialog.unreadMessagesCount = 0
43+
return updatedDialog
44+
} else {
45+
return item
46+
}
47+
}
48+
guard dialog.unreadMessagesCount > 0,
49+
defaults.unreadMessagesCount >= dialog.unreadMessagesCount
50+
else { return }
51+
let newValue = defaults.unreadMessagesCount - dialog.unreadMessagesCount
52+
defaults.saveUnreadMessagesCount(newValue)
53+
}
54+
55+
@MainActor
56+
func clearDialogsOnLogout(isAuthorized: Bool) {
57+
guard !isAuthorized else { return }
58+
dialogs.removeAll()
59+
}
60+
}

SwiftUI-WorkoutApp/Screens/Root/RootScreen.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,37 @@ import SwiftUI
44
struct RootScreen: View {
55
@Environment(\.userFlags) private var userFlags
66
@Binding var selectedTab: TabViewModel.Tab
7+
let unreadCount: Int
78

89
var body: some View {
910
TabView(selection: $selectedTab) {
1011
ForEach(TabViewModel.Tab.allCases, id: \.rawValue) { tab in
1112
tab.screen
1213
.tabItem { tab.tabItemLabel }
1314
.tag(tab)
15+
.badge(tab == .messages ? unreadCount : 0)
1416
}
1517
}
1618
.navigationViewStyle(.stack)
1719
}
1820
}
1921

2022
#if DEBUG
21-
#Preview {
22-
RootScreen(selectedTab: .constant(.map))
23-
.environmentObject(ParksManager())
24-
.environmentObject(DefaultsService())
23+
#Preview("Есть бейдж для чатов") {
24+
RootScreen(
25+
selectedTab: .constant(.map),
26+
unreadCount: 1
27+
)
28+
.environmentObject(ParksManager())
29+
.environmentObject(DefaultsService())
30+
}
31+
32+
#Preview("Нет бейджа") {
33+
RootScreen(
34+
selectedTab: .constant(.map),
35+
unreadCount: 0
36+
)
37+
.environmentObject(ParksManager())
38+
.environmentObject(DefaultsService())
2539
}
2640
#endif

0 commit comments

Comments
 (0)