Skip to content

Commit 74b0145

Browse files
authored
Изменение фото профиля и большой рефактор сетевого слоя / defaults (#268)
* Фича: изменение аватарки профиля (#263) * Починил краш при логировании JSON Был такой краш: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** +[NSJSONSerialization dataWithJSONObject:options:error:]: Invalid top-level type in JSON write' * Добавил Content-Length для запросов с multipart * Рефактор BodyMaker * Убрал лишний модификатор доступа public * Добавил отправку фотографии в запрос `editUser` И доработал конвертацию картинки перед отправкой на сервер * В процессе - Сделал отдельный экран для основного пользователя и отдельный для всех остальных - Поправил обновление аватарки при помощи модификатора `.id` * В процессе - Вынес `defaults` из запроса для авторизации в экран - Добавил анимации на экран профиля при появлении соц.кнопок - Переделываю некоторые свойства `defaults` на вычисляемые * В процессе - Поправил авторизацию - Дорабатываю сетевой слой * Убрал главного пользователя из экрана UserDetails * navigationBar -> topBar Апи обновился уже давно * Мелкий рефактор * Добавил ошибку и текст для 401 в клиенте * Рефактор и мелкие правки - Не показываем кнопку обновления на экране со списком дневников для iOS 16+ - На экране со списком заблокированных пользователей загружаем данные снова при рефреше - Убрал несколько лишних тогглов для отображения алерта с ошибкой - можно опираться только на наличие текста ошибки * MVP: выбор нового аватара * Мелкий рефактор * Локализация для InfoPlist * Рефактор соц.фичей Сохраняем заявки в друзья, друзей и черный список в тех местах приложения, где эти запросы вызываются, а не в сетевом клиенте * Поправил и доработал тесты для SWNetwork (#264) * Рефактор всех алертов в приложении (#265) * Добавил локальный пакет SWAlert * Показываю все алерты через синглтон Дополнительно доработал получение статуса в запросах, где не возвращаются данные: если пришел ответ с ошибкой, то передаем ее наверх, иначе возвращаем `false` * Доработал локализацию в алертах Дополнительно добавил локализацию на случай добавления/удаления пользователя из черного списка * Поднял версию сборки * Доработал SWAlert - Переименовал метод презентации алерта - Добавил комментарии - Убрал `nonisolated` - Применил на экране авторизации * Поправил краш в превью * Поправил авторизацию Возвращаем обычную ошибку 401 вместо `ClientError.forceLogout` * Рефактор DefaultsService (#267) - Вынес из сетевого слоя все упоминания `defaults` кроме токена и логаута - Актуализировал неиспользуемые запросы для регистрации и удаления пользователя - Доработал логику добавления в друзья, обработки заявки на добавление в друзья * Поправил верстку на экране редактирования профиля * Правки и доработки тестов для CI
1 parent e41bebe commit 74b0145

File tree

54 files changed

+1286
-874
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1286
-874
lines changed

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 41 additions & 6 deletions
Large diffs are not rendered by default.

SwiftUI-WorkoutApp/Extensions/Array+.swift

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
extension Date: @retroactive RawRepresentable {
4+
public var rawValue: String {
5+
String(timeIntervalSinceReferenceDate)
6+
}
7+
8+
public init?(rawValue: String) {
9+
guard let interval = Double(rawValue) else { return nil }
10+
self = Date(timeIntervalSinceReferenceDate: interval)
11+
}
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
extension String {
4+
/// Локализованный вариант с использованием `NSLocalizedString`
5+
///
6+
/// Нужен для особых случаев, когда не работает дефолтная локализация
7+
var localized: String {
8+
NSLocalizedString(self, comment: "")
9+
}
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import SWModels
2+
import UIKit.UIImage
3+
4+
extension UIImage {
5+
/// Делает медиафайл из картинки
6+
///
7+
/// Сначала пробуем конвертацию через jpegData для скорости, но в случае проблем обращаемся к `UIGraphicsImageRenderer` для
8+
/// гарантированного результата
9+
func toMediaFile(with key: String = "") -> MediaFile? {
10+
let data = jpegData(compressionQuality: 1) ?? UIGraphicsImageRenderer(size: size).jpegData(withCompressionQuality: 1) { _ in
11+
draw(in: .init(), blendMode: .normal, alpha: 1)
12+
}
13+
return data.isEmpty ? nil : MediaFile(imageData: data, forKey: key)
14+
}
15+
}
16+
17+
extension [UIImage] {
18+
/// Создает список медиафайлов из картинок для отправки на сервер
19+
var toMediaFiles: [MediaFile] {
20+
enumerated().compactMap { index, image in
21+
image.toMediaFile(with: "\(index + 1)")
22+
}
23+
}
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "SWAlert",
8+
platforms: [.iOS(.v15)],
9+
products: [.library(name: "SWAlert", targets: ["SWAlert"])],
10+
targets: [.target(name: "SWAlert")]
11+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SWAlert
2+
3+
Содержит синглтон для отображения алерта с любого экрана поверх активного `UIWindow`.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
@MainActor
5+
public final class SWAlert {
6+
public static let shared = SWAlert()
7+
private var currentAlert: UIViewController?
8+
9+
/// Показывает системный алерт с заданными параметрами
10+
/// - Parameters:
11+
/// - title: Заголовок. Если передать `nil`, то сообщение выделится жирным. Если передать текст или пустую строку, будет без
12+
/// заголовка, и сообщение будет со стандартным шрифтом
13+
/// - message: Текст сообщения
14+
/// - closeButtonTitle: Заголовок кнопки для закрытия алерта
15+
/// - closeButtonStyle: Стиль кнопки для закрытия алерта
16+
/// - closeButtonTintColor: Цвет кнопки для закрытия алерта. Если не настроить явно, то при появлении будет системный (синий) цвет, а
17+
/// при нажатии он изменится на `AccentColor` в проекте
18+
public func presentDefaultUIKit(
19+
title: String? = "",
20+
message: String,
21+
closeButtonTitle: String = "Ok",
22+
closeButtonStyle: UIAlertAction.Style = .default,
23+
closeButtonTintColor: UIColor? = .systemGreen
24+
) {
25+
guard currentAlert == nil, let topMostViewController else { return }
26+
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
27+
alert.view.tintColor = closeButtonTintColor
28+
alert.addAction(
29+
.init(
30+
title: closeButtonTitle,
31+
style: closeButtonStyle,
32+
handler: { [weak self] _ in
33+
self?.dismiss()
34+
}
35+
)
36+
)
37+
currentAlert = alert
38+
topMostViewController.present(alert, animated: true)
39+
}
40+
41+
private func dismiss() {
42+
currentAlert?.dismiss(animated: true)
43+
currentAlert = nil
44+
}
45+
46+
private var topMostViewController: UIViewController? {
47+
UIApplication.shared.firstKeyWindow?.rootViewController?.topMostViewController
48+
}
49+
}
50+
51+
private extension UIApplication {
52+
var firstKeyWindow: UIWindow? {
53+
connectedScenes
54+
.filter { $0.activationState == .foregroundActive }
55+
.compactMap { $0 as? UIWindowScene }
56+
.first?.windows
57+
.first(where: \.isKeyWindow)
58+
}
59+
}
60+
61+
private extension UIViewController {
62+
var topMostViewController: UIViewController {
63+
if let presented = presentedViewController {
64+
return presented.topMostViewController
65+
}
66+
if let navigation = self as? UINavigationController {
67+
return navigation.visibleViewController?.topMostViewController ?? navigation
68+
}
69+
if let tab = self as? UITabBarController {
70+
return tab.selectedViewController?.topMostViewController ?? tab
71+
}
72+
return self
73+
}
74+
}
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/// Используется во всех запросах, где нужна авторизация
22
public struct AuthData: Codable {
3-
public let login, password: String
4-
5-
public var base64Encoded: String? {
6-
(login + ":" + password).data(using: .utf8)?.base64EncodedString()
7-
}
3+
/// Используем для генерации токена
4+
///
5+
/// Например, при смене логина нужно сгенерировать новый токен, чтобы не выбросило из аккаунта - вот тут и используем этот пароль
6+
public let password: String
7+
/// Отправляем на сервер
8+
public let token: String?
89

910
public init(login: String, password: String) {
10-
self.login = login
11+
self.token = (login + ":" + password).data(using: .utf8)?.base64EncodedString()
1112
self.password = password
1213
}
1314
}

0 commit comments

Comments
 (0)