Skip to content

Commit eb1df31

Browse files
authored
3.8.3 (#330)
* Убрал забытую локализацию * Поправил отступ снизу экрана * Дорабатываю обработку ошибок сервера * Дефолтный город для фильтра площадок - Москва Если не определили город, в том числе из-за проблем с локализацией, применяем фильтр площадок по Москве * Доработал обновление диалогов * Доработал список прошедших мероприятий Если при открытии приложения не удалось загрузить список прошедших мероприятий, показываем там список ранее сохраненных мероприятий * Доработки экрана мероприятий - Если нет сохраненных прошедших мероприятий, то при отсутствии сети показываем плашку про отсутствие сети вместо пустого экрана на вкладке с прошедшими мероприятиями - Если нет сети, то не делаем запрос - сразу показываем алерт * Обрабатываем отстутсвие сети на экране с картой * Доработки списка диалогов Ошибки загрузки диалогов отображаем не в алерте, а прямо вместо контента экрана * Доработал профиль Загружаем данные профиля при разворачивании приложения по той же логике, что и диалоги - если изменилась фаза приложения, сохраненная в State-свойстве
1 parent 63e09d6 commit eb1df31

File tree

16 files changed

+389
-235
lines changed

16 files changed

+389
-235
lines changed

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@
468468
"$(inherited)",
469469
"@executable_path/Frameworks",
470470
);
471-
MARKETING_VERSION = 3.8.2;
471+
MARKETING_VERSION = 3.8.3;
472472
PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut;
473473
PRODUCT_NAME = WorkoutApp;
474474
RUN_CLANG_STATIC_ANALYZER = YES;
@@ -522,7 +522,7 @@
522522
"$(inherited)",
523523
"@executable_path/Frameworks",
524524
);
525-
MARKETING_VERSION = 3.8.2;
525+
MARKETING_VERSION = 3.8.3;
526526
PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut;
527527
PRODUCT_NAME = WorkoutApp;
528528
RUN_CLANG_STATIC_ANALYZER = YES;

SwiftUI-WorkoutApp/Libraries/SWNetwork/Sources/SWNetwork/APIError.swift

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,45 @@ public enum APIError: Error, LocalizedError, Equatable {
1313
case decodingError
1414
case notConnectedToInternet
1515

16-
init(_ error: ErrorResponse, _ code: Int?) {
17-
if code == 401 {
18-
self = .invalidCredentials
19-
} else if let message = error.message {
20-
self = .customError(code: code ?? 404, message: message)
21-
} else if let array = error.errors {
22-
let message = array.joined(separator: ",\n")
23-
self = .customError(code: code ?? 404, message: message)
24-
} else {
25-
self.init(with: error.realCode)
16+
init(_ error: ErrorResponse, _ statusCode: Int?) {
17+
let serverCode = error.makeRealCode(statusCode: statusCode)
18+
19+
// Приоритет 1: Кастомные сообщения из ErrorResponse
20+
if let message = error.realMessage {
21+
self = .customError(
22+
code: serverCode,
23+
message: message.trimmingCharacters(in: .whitespacesAndNewlines)
24+
)
25+
return
26+
}
27+
28+
// Приоритет 2: Обработка через код (если есть валидный код)
29+
if serverCode != 0 {
30+
self.init(with: serverCode)
31+
return
2632
}
33+
34+
// Приоритет 3: Стандартный код из статуса
35+
if let statusCode {
36+
self.init(with: statusCode)
37+
return
38+
}
39+
40+
// Если все варианты исчерпаны
41+
self = .unknown
2742
}
2843

2944
init(with code: Int?) {
45+
guard let code else {
46+
self = .unknown
47+
return
48+
}
3049
switch code {
31-
case 400: self = .badRequest
50+
case 400, 402, 403, 405 ... 412, 414 ... 499: self = .badRequest
3251
case 401: self = .invalidCredentials
3352
case 404: self = .notFound
3453
case 413: self = .payloadTooLarge
35-
case 500: self = .serverError
54+
case 500 ... 599: self = .serverError
3655
default: self = .unknown
3756
}
3857
}
Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
11
import Foundation
22

33
struct ErrorResponse: Codable {
4-
let errors: [String]?
4+
let errors: [String]
55
let name, message: String?
6-
let code, status: Int?
6+
let code, status: Int
77

8-
var realCode: Int {
9-
if let code, code != 0 {
10-
code
8+
init(from decoder: Decoder) throws {
9+
let container = try decoder.container(keyedBy: CodingKeys.self)
10+
self.errors = try container.decodeIfPresent([String].self, forKey: .errors) ?? []
11+
self.name = try container.decodeIfPresent(String.self, forKey: .name)
12+
self.message = try container.decodeIfPresent(String.self, forKey: .message)
13+
self.code = try container.decodeIfPresent(Int.self, forKey: .code) ?? 0
14+
self.status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0
15+
}
16+
17+
init(
18+
errors: [String] = [],
19+
name: String? = nil,
20+
message: String? = nil,
21+
code: Int = 0,
22+
status: Int = 0
23+
) {
24+
self.errors = errors
25+
self.name = name
26+
self.message = message
27+
self.code = code
28+
self.status = status
29+
}
30+
31+
var realMessage: String? {
32+
if let message {
33+
message
1134
} else {
12-
status ?? 0
35+
errors.isEmpty ? nil : errors.joined(separator: ", ")
1336
}
1437
}
38+
39+
func makeRealCode(statusCode: Int?) -> Int {
40+
let realCode = code != 0 ? code : status
41+
return realCode != 0 ? realCode : (statusCode ?? 0)
42+
}
1543
}

SwiftUI-WorkoutApp/Libraries/SWNetwork/Tests/SWNetworkTests/APIErrorTests.swift

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,96 @@
22
import Testing
33

44
struct APIErrorTests {
5-
@Test
6-
func badRequest() {
7-
let error = APIError(with: 400)
8-
#expect(error == .badRequest)
5+
private static var badRequestCodes: [Int] {
6+
[400, 402, 403] + Array(405 ... 412) + Array(414 ... 499)
97
}
108

11-
@Test
12-
func invalidCredentials() {
13-
let error = APIError(with: 401)
14-
#expect(error == .invalidCredentials)
15-
}
9+
private static let serverErrorCodes = Array(500 ... 599)
1610

17-
@Test
18-
func notFound() {
19-
let error = APIError(with: 404)
20-
#expect(error == .notFound)
11+
@Test(arguments: badRequestCodes)
12+
func badRequest(code: Int) {
13+
let error = APIError(with: code)
14+
#expect(error == .badRequest)
2115
}
2216

23-
@Test
24-
func payloadTooLarge() {
25-
let error = APIError(with: 413)
26-
#expect(error == .payloadTooLarge)
17+
@Test(arguments: serverErrorCodes)
18+
func serverError(code: Int) {
19+
let error = APIError(with: code)
20+
#expect(error == .serverError)
2721
}
2822

2923
@Test
30-
func serverError() {
31-
let error = APIError(with: 500)
32-
#expect(error == .serverError)
24+
func errorInitializationByCode() {
25+
let testCases: [(Int?, APIError)] = [
26+
(401, .invalidCredentials),
27+
(404, .notFound),
28+
(413, .payloadTooLarge),
29+
(nil, .unknown),
30+
(999, .unknown)
31+
]
32+
for (code, expected) in testCases {
33+
let error = APIError(with: code)
34+
#expect(error == expected)
35+
}
3336
}
3437

3538
@Test
3639
func customErrorWithMessage() {
3740
let errorResponse = ErrorResponse(
38-
errors: nil,
39-
name: nil,
4041
message: "Непредвиденная ошибка",
41-
code: nil,
42-
status: nil
42+
code: 0,
43+
status: 0
4344
)
4445
let error = APIError(errorResponse, 123)
4546
#expect(error.errorDescription == "123, Непредвиденная ошибка")
4647
}
4748

4849
@Test
4950
func customErrorWithErrorsArray() {
50-
let errorResponse = ErrorResponse(errors: ["Ошибка 1", "Ошибка 2"], name: nil, message: nil, code: nil, status: nil)
51+
let errorResponse = ErrorResponse(errors: ["Ошибка 1", "Ошибка 2"], code: 0, status: 0)
5152
let error = APIError(errorResponse, nil)
52-
#expect(error.errorDescription == "404, Ошибка 1,\nОшибка 2")
53+
#expect(error.errorDescription == "0, Ошибка 1, Ошибка 2")
5354
}
5455

5556
@Test
5657
func unknownError() {
57-
let errorResponse = ErrorResponse(errors: nil, name: nil, message: nil, code: nil, status: nil)
58+
let errorResponse = ErrorResponse(code: 0, status: 0)
5859
let error = APIError(errorResponse, nil)
5960
#expect(error == .unknown)
6061
}
62+
63+
@Test
64+
func customErrorPriorities() {
65+
// Сообщение имеет приоритет над errors
66+
let case1 = ErrorResponse(
67+
errors: ["Error1", "Error2"],
68+
message: "Priority Message",
69+
code: 400,
70+
status: 0
71+
)
72+
let error1 = APIError(case1, nil)
73+
#expect(error1.errorDescription == "400, Priority Message")
74+
75+
// Errors используются, если message отсутствует
76+
let case2 = ErrorResponse(
77+
errors: ["Error1", "Error2"],
78+
code: 0,
79+
status: 500
80+
)
81+
let error2 = APIError(case2, nil)
82+
#expect(error2.errorDescription == "500, Error1, Error2")
83+
}
84+
85+
@Test
86+
func specialCombinations() {
87+
// Кастомная ошибка с кодом, соответствующим стандартному
88+
let response1 = ErrorResponse(message: "Custom Credentials", code: 401, status: 0)
89+
let error1 = APIError(response1, nil)
90+
#expect(error1 == .customError(code: 401, message: "Custom Credentials"))
91+
92+
// Пустой массив errors
93+
let response2 = ErrorResponse(errors: [], code: 500, status: 0)
94+
let error2 = APIError(response2, nil)
95+
#expect(error2 == .serverError)
96+
}
6197
}

SwiftUI-WorkoutApp/Resources/Localizable.xcstrings

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3013,23 +3013,6 @@
30133013
}
30143014
}
30153015
},
3016-
"Поддержать разработчика" : {
3017-
"extractionState" : "manual",
3018-
"localizations" : {
3019-
"en" : {
3020-
"stringUnit" : {
3021-
"state" : "translated",
3022-
"value" : "Support the developer"
3023-
}
3024-
},
3025-
"ru" : {
3026-
"stringUnit" : {
3027-
"state" : "translated",
3028-
"value" : "Поддержать разработчика"
3029-
}
3030-
}
3031-
}
3032-
},
30333016
"Поделиться приложением" : {
30343017
"extractionState" : "manual",
30353018
"localizations" : {

SwiftUI-WorkoutApp/Screens/Events/EventsListScreen.swift

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import SWUtils
66

77
/// Экран со списком мероприятий
88
struct EventsListScreen: View {
9+
@Environment(\.isNetworkConnected) private var isNetworkConnected
910
@EnvironmentObject private var tabViewModel: TabViewModel
1011
@EnvironmentObject private var defaults: DefaultsService
1112
@State private var futureEvents = [EventResponse]()
@@ -18,6 +19,14 @@ struct EventsListScreen: View {
1819
@State private var showEventCreationSheet = false
1920
@State private var showEventCreationRule = false
2021
private let pastEventStorage = PastEventStorage()
22+
private var currentEventList: [EventResponse] {
23+
switch selectedEventType {
24+
case .future:
25+
futureEvents
26+
case .past:
27+
pastEvents.isEmpty ? pastEventStorage.savedPastEvents : pastEvents
28+
}
29+
}
2130

2231
var body: some View {
2332
NavigationView {
@@ -62,7 +71,7 @@ private extension EventsListScreen {
6271
Icons.Regular.refresh.view
6372
}
6473
.opacity(
65-
showEmptyView && !DeviceOSVersionChecker.iOS16Available ? 1 : 0
74+
!isLoading && !DeviceOSVersionChecker.iOS16Available ? 1 : 0
6675
)
6776
.disabled(isLoading)
6877
}
@@ -78,26 +87,37 @@ private extension EventsListScreen {
7887
.padding(.horizontal)
7988
}
8089

90+
@ViewBuilder
8191
var emptyView: some View {
82-
EmptyContentView(
83-
mode: .events,
84-
action: {
85-
if canAddEvent {
86-
showEventCreationSheet.toggle()
87-
} else {
88-
goToMap()
92+
switch selectedEventType {
93+
case .future:
94+
EmptyContentView(
95+
mode: .events,
96+
action: {
97+
if canAddEvent {
98+
showEventCreationSheet.toggle()
99+
} else {
100+
goToMap()
101+
}
89102
}
90-
}
91-
)
92-
.opacity(showEmptyView ? 1 : 0)
103+
)
104+
.opacity(futureEvents.isEmpty && !isLoading ? 1 : 0)
105+
case .past:
106+
let showView = pastEvents.isEmpty
107+
&& pastEventStorage.savedPastEvents.isEmpty
108+
&& !isLoading
109+
&& !isNetworkConnected
110+
NoConnectionView()
111+
.opacity(showView ? 1 : 0)
112+
}
93113
}
94114

95115
func goToMap() { tabViewModel.selectTab(.map) }
96116

97117
var eventsList: some View {
98118
ScrollView {
99119
LazyVStack(spacing: 12) {
100-
ForEach(selectedEventType == .future ? futureEvents : pastEvents) { event in
120+
ForEach(currentEventList) { event in
101121
Button {
102122
selectedEvent = event
103123
} label: {
@@ -111,7 +131,7 @@ private extension EventsListScreen {
111131
.accessibilityIdentifier("EventViewCell")
112132
}
113133
}
114-
.padding([.top, .horizontal])
134+
.padding()
115135
}
116136
.opacity(isLoading ? 0 : 1)
117137
.sheet(item: $selectedEvent) { event in
@@ -159,11 +179,8 @@ private extension EventsListScreen {
159179
defaults.hasParks && defaults.isAuthorized
160180
}
161181

162-
var showEmptyView: Bool {
163-
selectedEventType == .future && futureEvents.isEmpty && !isLoading
164-
}
165-
166182
func askForEvents(refresh: Bool = false) async {
183+
guard !SWAlert.shared.presentNoConnection(isNetworkConnected) else { return }
167184
let hasFutureEvents = selectedEventType == .future && !futureEvents.isEmpty
168185
let hasPastEvents = selectedEventType == .past && !pastEvents.isEmpty
169186
if isLoading && !refresh

0 commit comments

Comments
 (0)