Skip to content

Commit eebbb3b

Browse files
authored
Доработал отображение отсутствия подключения (#317)
* Основное - Обновил экран со списком записей в дневнике - Обновил экран списка друзей основного пользователя - Обновил экран с черным списком - Обновил экран со списком дневников - Обновил экран с диалогами * Дополнительно - При успешной отправке сообщения с экрана чужого профиля дополнительно обновляем список диалогов - При авторизации в случае ошибки отсутствия интернета показываем алерт (не подставляем эту ошибку под текстфилды) - При успешной авторизации запрашиваем разрешения на уведомления для отображения бейджа на иконке приложения - Небольшой рефактор
1 parent fc4d9b3 commit eebbb3b

31 files changed

+903
-426
lines changed

SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/DialogResponse.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import SWUtils
33

44
/// Модель с информацией о диалоге
5-
public struct DialogResponse: Codable, Identifiable, Sendable {
5+
public struct DialogResponse: Codable, Identifiable, Sendable, Equatable {
66
public let id: Int
77
public let anotherUserImageStringURL: String?
88
public let anotherUserName: String?
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
/// Тип ошибки для экранов
4+
public enum ErrorKind: Equatable {
5+
/// Нет подключения к сети
6+
case notConnected
7+
/// Другая ошибка
8+
case common(message: String)
9+
}

SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/Journal/JournalEntryResponse.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import SWUtils
33

44
/// Модель с информацией о записи в дневнике
5-
public struct JournalEntryResponse: Codable, Identifiable, Sendable {
5+
public struct JournalEntryResponse: Equatable, Codable, Identifiable, Sendable {
66
public let id: Int
77
public let journalID, authorID: Int?
88
public let authorName, message, createDate, modifyDate, authorImage: String?
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import Foundation
22

3-
enum ClientError: Error, LocalizedError {
3+
public enum ClientError: Error, LocalizedError {
44
case forceLogout
5+
case noConnection
56

6-
var errorDescription: String? {
7+
public var errorDescription: String? {
78
switch self {
89
case .forceLogout: "Для корректной работы приложения нужен повторный вход"
10+
case .noConnection: "Проверьте подключение и повторите попытку"
911
}
1012
}
1113
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public struct SWClient: Sendable {
9393
/// - current: текущий пароль
9494
/// - new: новый пароль
9595
/// - Returns: `true` в случае успеха, `false` при ошибках
96+
@discardableResult
9697
public func changePassword(current: String, new: String) async throws -> Bool {
9798
let endpoint = Endpoint.changePassword(currentPass: current, newPass: new)
9899
return try await makeStatus(for: endpoint)
@@ -134,6 +135,7 @@ public struct SWClient: Sendable {
134135
/// - userID: `id` инициатора заявки
135136
/// - accept: `true` - одобрить заявку, `false` - отклонить
136137
/// - Returns: `true` в случае успеха, `false` при ошибках
138+
@discardableResult
137139
public func respondToFriendRequest(from userID: Int, accept: Bool) async throws -> Bool {
138140
let endpoint: Endpoint = accept
139141
? .acceptFriendRequest(from: userID)
@@ -146,6 +148,7 @@ public struct SWClient: Sendable {
146148
/// - userID: `id` пользователя, к которому применяется действие
147149
/// - option: вид действия - отправить заявку на добавление в друзья или удалить из списка друзей
148150
/// - Returns: `true` в случае успеха, `false` при ошибках
151+
@discardableResult
149152
public func friendAction(userID: Int, option: FriendAction) async throws -> Bool {
150153
let endpoint: Endpoint = option == .sendFriendRequest
151154
? .sendFriendRequest(to: userID)
@@ -160,6 +163,7 @@ public struct SWClient: Sendable {
160163
/// - user: Пользователь, к которому применяется действие
161164
/// - option: вид действия - добавить/убрать из черного списка
162165
/// - Returns: `true` в случае успеха, `false` при ошибках
166+
@discardableResult
163167
public func blacklistAction(user: UserResponse, option: BlacklistOption) async throws -> Bool {
164168
let endpoint: Endpoint = option == .add
165169
? .addToBlacklist(user.id)
@@ -276,6 +280,7 @@ public struct SWClient: Sendable {
276280
/// - option: тип записи
277281
/// - entryID: `id` записи
278282
/// - Returns: `true` в случае успеха, `false` при ошибках
283+
@discardableResult
279284
public func deleteEntry(from option: TextEntryOption, entryID: Int) async throws -> Bool {
280285
let endpoint: Endpoint = switch option {
281286
case let .park(id):
@@ -382,6 +387,7 @@ public struct SWClient: Sendable {
382387
/// - message: отправляемое сообщение
383388
/// - userID: `id` получателя сообщения
384389
/// - Returns: `true` в случае успеха, `false` при ошибках
390+
@discardableResult
385391
public func sendMessage(_ message: String, to userID: Int) async throws -> Bool {
386392
let endpoint = Endpoint.sendMessageTo(message, userID)
387393
return try await makeStatus(for: endpoint)
@@ -433,6 +439,7 @@ public struct SWClient: Sendable {
433439
/// - viewAccess: доступ на просмотр
434440
/// - commentAccess: доступ на комментирование
435441
/// - Returns: `true` в случае успеха, `false` при ошибках
442+
@discardableResult
436443
public func editJournalSettings(
437444
with journalID: Int,
438445
title: String,
@@ -457,6 +464,7 @@ public struct SWClient: Sendable {
457464
/// - title: название дневника
458465
/// - mainUserID: `id` главного пользователя
459466
/// - Returns: Создает новый дневник для пользователя
467+
@discardableResult
460468
public func createJournal(with title: String, for mainUserID: Int?) async throws -> Bool {
461469
guard let mainUserID else {
462470
throw APIError.invalidUserID
@@ -480,6 +488,7 @@ public struct SWClient: Sendable {
480488
/// - journalID: `id` дневника для удаления
481489
/// - mainUserID: `id` владельца дневника (главного пользователя)
482490
/// - Returns: `true` в случае успеха, `false` при ошибках
491+
@discardableResult
483492
public func deleteJournal(with journalID: Int, for mainUserID: Int?) async throws -> Bool {
484493
guard let mainUserID else {
485494
throw APIError.invalidUserID
@@ -515,6 +524,8 @@ private extension SWClient {
515524
} catch APIError.invalidCredentials {
516525
await defaults.triggerLogout()
517526
throw ClientError.forceLogout
527+
} catch APIError.notConnectedToInternet {
528+
throw ClientError.noConnection
518529
} catch {
519530
throw error
520531
}
@@ -530,6 +541,8 @@ private extension SWClient {
530541
} catch APIError.invalidCredentials {
531542
await defaults.triggerLogout()
532543
throw ClientError.forceLogout
544+
} catch APIError.notConnectedToInternet {
545+
throw ClientError.noConnection
533546
} catch {
534547
throw error
535548
}

SwiftUI-WorkoutApp/Libraries/SWUtils/Sources/SWUtils/NetworkStatus.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ private actor NetworkMonitorActor {
6262
}
6363
monitor.start(queue: .global(qos: .background))
6464
await withTaskCancellationHandler {
65-
monitor.cancel()
66-
continuation.finish()
65+
while !Task.isCancelled {
66+
await Task.yield()
67+
}
6768
} onCancel: {
6869
monitor.cancel()
6970
continuation.finish()

SwiftUI-WorkoutApp/Resources/Localizable.xcstrings

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,16 @@
16411641
}
16421642
}
16431643
},
1644+
"Загрузка..." : {
1645+
"localizations" : {
1646+
"en" : {
1647+
"stringUnit" : {
1648+
"state" : "translated",
1649+
"value" : "Loading..."
1650+
}
1651+
}
1652+
}
1653+
},
16441654
"Закрыть" : {
16451655
"localizations" : {
16461656
"en" : {
@@ -2389,6 +2399,12 @@
23892399
"state" : "translated",
23902400
"value" : "Failed to decode server response"
23912401
}
2402+
},
2403+
"ru" : {
2404+
"stringUnit" : {
2405+
"state" : "translated",
2406+
"value" : "Не удалось декодировать ответ"
2407+
}
23922408
}
23932409
}
23942410
},
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import SWDesignSystem
2+
import SwiftUI
3+
import SWModels
4+
5+
struct CommonErrorView: View {
6+
let errorKind: ErrorKind
7+
8+
var body: some View {
9+
ContainerRelativeView {
10+
if case let .common(message) = errorKind {
11+
VStack(spacing: 16) {
12+
Text("Ошибка").bold()
13+
Text(.init(message))
14+
.multilineTextAlignment(.center)
15+
}
16+
.foregroundStyle(Color.swMainText)
17+
.frame(maxWidth: .infinity, maxHeight: .infinity)
18+
} else {
19+
NoConnectionView()
20+
}
21+
}
22+
}
23+
}
24+
25+
#if DEBUG
26+
#Preview("Общая ошибка") {
27+
CommonErrorView(errorKind: .common(message: "Неизвестная ошибка"))
28+
}
29+
30+
#Preview("Нет подключения") {
31+
CommonErrorView(errorKind: .notConnected)
32+
}
33+
#endif
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import SwiftUI
2+
3+
/// Для iOS 17+ располагает вьюху внутри `ZStack` с нужным выравниванием относительно контейнера
4+
struct ContainerRelativeView<Content: View>: View {
5+
let content: Content
6+
let axes: Axis.Set
7+
let alignment: Alignment
8+
9+
init(
10+
@ViewBuilder content: () -> Content,
11+
_ axes: Axis.Set = [.vertical],
12+
alignment: Alignment = .center
13+
) {
14+
self.content = content()
15+
self.axes = axes
16+
self.alignment = alignment
17+
}
18+
19+
var body: some View {
20+
if #available(iOS 17.0, *) {
21+
ZStack {
22+
Spacer().containerRelativeFrame(axes, alignment: alignment)
23+
content
24+
}
25+
} else {
26+
content
27+
}
28+
}
29+
}
30+
31+
#if DEBUG
32+
#Preview {
33+
ScrollView {
34+
ContainerRelativeView {
35+
VStack {
36+
ForEach(0 ..< 3, id: \.self) {
37+
Text("element #\($0)")
38+
}
39+
}
40+
}
41+
}
42+
}
43+
#endif

SwiftUI-WorkoutApp/Screens/Common/EmptyContentView.swift

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,11 @@ struct EmptyContentView: View {
1616
if userFlags.isAuthorized {
1717
Button(actionButtonTitle, action: action)
1818
.buttonStyle(SWButtonStyle(mode: .filled, size: .large))
19+
hintTextIfAvailable
1920
}
2021
} else {
21-
Icons.Regular.noSignal.imageView
22-
.resizable()
23-
.scaledToFit()
24-
.frame(width: 50, height: 50)
25-
.foregroundStyle(.accent)
26-
titleText("Нет соединения с сетью")
22+
NoConnectionView()
2723
}
28-
hintTextIfAvailable
2924
}
3025
.frame(maxWidth: .infinity, maxHeight: .infinity)
3126
.padding()
@@ -88,19 +83,22 @@ private extension EmptyContentView.Mode {
8883

8984
#if DEBUG
9085
#Preview {
91-
VStack {
92-
ForEach(EmptyContentView.Mode.allCases, id: \.self) { mode in
86+
ScrollView {
87+
VStack {
88+
ForEach(EmptyContentView.Mode.allCases, id: \.self) { mode in
89+
EmptyContentView(
90+
mode: mode,
91+
action: {}
92+
)
93+
Divider()
94+
}
95+
.environment(\.isNetworkConnected, true)
9396
EmptyContentView(
94-
mode: mode,
97+
mode: .dialogs,
9598
action: {}
9699
)
97-
Divider()
98100
}
99-
.environment(\.isNetworkConnected, true)
100-
EmptyContentView(
101-
mode: .dialogs,
102-
action: {}
103-
)
104101
}
102+
.environment(\.userFlags, .init(isAuthorized: true, hasParks: false, hasFriends: true))
105103
}
106104
#endif

0 commit comments

Comments
 (0)