Skip to content

Commit 419ac5c

Browse files
authored
Develop 3.10.0 (#351)
* Добавил Makefile и обновил версию ruby * Поднял версию Swift до 6.1.0 * Перенес пакет с картой в проект * Доработка фильтра по городам (#343) * Перенес кнопку поиска города Теперь она общая для обоих режимов таба с площадками * В процессе - Теперь один массив площадок используется и для карты, и для списка площадок - Добавил тесты и поправил отображение вьюхи про отсутствие площадок при запуске приложения * Поправил предупреждение * Доработка смены региона карты (#344) * Меняем регион карты при смене города * В процессе 1. При появлении экрана с картой проверяем город пользователя в профиле - если город указан, и на карте не выбран другой город, то центрируем карту по городу в профиле 2. При отмене фильтра по городу центрируем карту по городу пользователя в профиле * В процессе * Убрал неактуальную настройку выбранного города * Добавил падение в дебаге Если не смогли определить координаты города, будет краш в дебаге * Удаляем дубликаты в подписке * Переиспользуем существующий код * Обновил нейминг * Не прячем карту никогда И обновил проверку на ненастроенный регион * Доработки - при первом появлении экрана с картой настраиваем регион по сохраненному городу - сообщение про доступ к геолокации показываем только при наличии текста ошибки * Доработал вьюху про отсутствие площадок Если фильтр не настроен, но площадки не найдены - показываем только кнопку для смены города * Поправил обновление региона карты * Форматирование * Убрал лишний код * Перенес создание адреса площадки в SWAddress * В процессе - Доработал формирование адреса новой площадки - убираем дубликаты из названия (если регион/город/район совпадают по названию, например) - При движениях пользователя обновляем координаты для новой площадки и пытаемся найти город в справочнике * В процессе - Для CLGeocoder настроил по дефолту локализацию "ru_RU", чтобы не было проблем при определении города - Мелкий рефактор - Добавил тесты * Обновил нейминг и рефактор * Рефактор * Обновил нейминг: ID -> Id * Рефактор * Доработка Для неавторизованного пользователя показываем на карте Москву по умолчанию * Update README.md * Доработки 3.10 * Доработки обновлений карты * Убрал лишнюю смену региона карты * Комментарии * Доработал обновление карты Заменил таск двумя разными модификаторами, теперь выполняем обновление координат пользователя при первом появлении карты и при каждом изменении идентификатора города пользователя в профиле Добавил поддержку маков (для айпада) Добавил поддержку айпада (#350) И обновил скриншоты с ридми
1 parent 6f904f7 commit 419ac5c

File tree

40 files changed

+220
-51
lines changed

40 files changed

+220
-51
lines changed

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,19 @@ make screenshots
161161
6. Если тесты падают с ошибкой при запуске через `fastlane`, нужно убедиться, что при ручном запуске тестов из `Xcode` они успешно проходят во всех локализациях, используемых для создания скриншотов
162162
7. Готовые скриншоты сохраняются в папке [screenshots](./fastlane/screenshots)
163163

164-
| Список площадок | Площадка | Прошедшие мероприятия | Мероприятие | Профиль |
165-
| --- | --- | --- | --- | --- |
166-
| <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-1-sportsGroundsList.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-2-sportsGroundDetails.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-3-pastEvents.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-4-eventDetails.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-5-profile.png"> |
164+
#### iPhone
165+
| Карта с площадками | Список площадок | Площадка | Прошедшие мероприятия | Мероприятие | Профиль |
166+
| --- | --- | --- | --- | --- | --- |
167+
| <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-0-parksMap.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-1-parksList.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-2-parkDetails.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-3-pastEvents.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-4-eventDetails.png"> | <img src="./fastlane/screenshots/ru/iPhone 16 Pro Max-5-profile.png"> |
168+
169+
#### iPad
170+
| Карта с площадками | Список площадок | Площадка | Прошедшие мероприятия | Мероприятие | Профиль |
171+
| --- | --- | --- | --- | --- | --- |
172+
| <img src="./fastlane/screenshots/ru/iPad Pro 13-inch (M4)-0-parksMap.png"> | <img src="./fastlane/screenshots/ru/iPad Pro 13-inch (M4)-1-parksList.png"> | <img src="./fastlane/screenshots/ru/iPad Pro 13-inch (M4)-2-parkDetails.png"> | <img src="./fastlane/screenshots/ru/iPad Pro 13-inch (M4)-3-pastEvents.png"> | <img src="./fastlane/screenshots/ru/iPad Pro 13-inch (M4)-4-eventDetails.png"> | <img src="./fastlane/screenshots/ru/iPad Pro 13-inch (M4)-5-profile.png"> |
167173

168174
#### Модели девайсов, используемые для скриншотов
169-
По состоянию на 2025 год Apple берет за основу скриншоты для диагонали 6.9 (или 6.7) дюймов и масштабирует их под все остальные размеры экранов, то есть можно использовать для скриншотов только один девайс:
175+
По состоянию на 2025 год Apple берет за основу скриншоты для диагонали 6.9 (или 6.7) дюймов для айфона (13 дюймов для айпада) и масштабирует их под все остальные размеры экранов, то есть можно использовать для скриншотов по одному девайсу на платформу:
170176
- iPhone 16 Pro Max
177+
- iPad Pro 13-inch
171178

172-
Список всех существующих девайсов есть [тут](https://www.ios-resolution.com).
179+
Список всех существующих девайсов есть [тут](https://iosref.com/res).

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -471,12 +471,12 @@
471471
RUN_CLANG_STATIC_ANALYZER = YES;
472472
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
473473
SUPPORTS_MACCATALYST = NO;
474-
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
474+
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
475475
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
476476
SWIFT_EMIT_LOC_STRINGS = YES;
477477
SWIFT_STRICT_CONCURRENCY = complete;
478478
SWIFT_VERSION = 6.0;
479-
TARGETED_DEVICE_FAMILY = 1;
479+
TARGETED_DEVICE_FAMILY = "1,2";
480480
};
481481
name = Debug;
482482
};
@@ -521,12 +521,12 @@
521521
RUN_CLANG_STATIC_ANALYZER = YES;
522522
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
523523
SUPPORTS_MACCATALYST = NO;
524-
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
524+
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
525525
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
526526
SWIFT_EMIT_LOC_STRINGS = YES;
527527
SWIFT_STRICT_CONCURRENCY = complete;
528528
SWIFT_VERSION = 6.0;
529-
TARGETED_DEVICE_FAMILY = 1;
529+
TARGETED_DEVICE_FAMILY = "1,2";
530530
};
531531
name = Release;
532532
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
3+
private struct OnFirstAppear: ViewModifier {
4+
@State private var hasAppeared = false
5+
let action: () -> Void
6+
7+
func body(content: Content) -> some View {
8+
content.onAppear {
9+
guard !hasAppeared else { return }
10+
hasAppeared = true
11+
action()
12+
}
13+
}
14+
}
15+
16+
extension View {
17+
/// Выполняет действие только при первом появлении view
18+
func onFirstAppear(_ action: @escaping () -> Void) -> some View {
19+
modifier(OnFirstAppear(action: action))
20+
}
21+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
extension Double {
4+
/// Округляет значение до указанного количества знаков после запятой
5+
func rounded(to places: Int) -> Double {
6+
let divisor = pow(10.0, Double(places))
7+
return (self * divisor).rounded() / divisor
8+
}
9+
}

SwiftUI-WorkoutApp/Libraries/ClusteringMapView/Sources/ClusteringMapView/Internal/RegionOrganizer.swift

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import OSLog
44
struct RegionOrganizer {
55
private let logger = Logger(
66
subsystem: Bundle.main.bundleIdentifier!,
7-
category: "RegionOrganizer"
7+
category: String(describing: RegionOrganizer.self)
88
)
99
let old: MKCoordinateRegion
1010
let new: MKCoordinateRegion
@@ -14,27 +14,18 @@ struct RegionOrganizer {
1414
/// Про тестирование: https://stackoverflow.com/a/51903928/11830041
1515
@MainActor
1616
func updateRegionIfNeeded(for mapView: MKMapView) {
17-
let oldCoordinate = LocationCoordinate(old.center)
18-
let newCoordinate = LocationCoordinate(new.center)
19-
guard newCoordinate.isSpecified, isSpanSpecified else {
17+
logger.debug("Собираемся обновить регион")
18+
let oldCoordinate = LocationCoordinate(old)
19+
let newCoordinate = LocationCoordinate(new)
20+
guard newCoordinate.isSpecified else {
2021
logger.debug("Новый регион не настроен, не обновляем регион")
2122
return
2223
}
23-
guard newCoordinate != oldCoordinate || isSpanDifferent else {
24+
guard newCoordinate != oldCoordinate else {
2425
logger.debug("Новый регион совпадает с предыдущим, не обновляем регион")
2526
return
2627
}
2728
mapView.setRegion(new, animated: true)
28-
}
29-
}
30-
31-
private extension RegionOrganizer {
32-
var isSpanSpecified: Bool {
33-
old.span.latitudeDelta != 0 || old.span.longitudeDelta != 0
34-
}
35-
36-
var isSpanDifferent: Bool {
37-
old.span.latitudeDelta != new.span.latitudeDelta ||
38-
old.span.longitudeDelta != new.span.longitudeDelta
29+
logger.debug("Обновили регион")
3930
}
4031
}

SwiftUI-WorkoutApp/Libraries/ClusteringMapView/Sources/ClusteringMapView/Public/ClusteringMapView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public struct ClusteringMapView: UIViewRepresentable {
5353
view.cameraZoomRange = cameraZoomRange
5454
addTrackingButtonIfNeeded(to: view)
5555
if ClusteringMapView.storedMapView == nil {
56-
// Если не сохранить карту, будут создаваться дубли
56+
// Если не сохранить карту, могут создаваться дубли
5757
ClusteringMapView.storedMapView = view
5858
}
5959
return view

SwiftUI-WorkoutApp/Libraries/ClusteringMapView/Sources/ClusteringMapView/Public/LocationCoordinate.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import CoreLocation
1+
import MapKit
22

33
/// Модель для удобной работы с координатами
44
public struct LocationCoordinate: Sendable {
55
public let lat: Double
66
public let lon: Double
77

8-
public init(_ regionCenter: CLLocationCoordinate2D) {
9-
self.lat = Double(regionCenter.latitude).rounded()
10-
self.lon = Double(regionCenter.longitude).rounded()
8+
public init(_ center: CLLocationCoordinate2D) {
9+
self.lat = center.latitude.rounded(to: 2)
10+
self.lon = center.longitude.rounded(to: 2)
11+
}
12+
13+
public init(_ region: MKCoordinateRegion) {
14+
self.lat = region.center.latitude.rounded(to: 2)
15+
self.lon = region.center.longitude.rounded(to: 2)
1116
}
1217

1318
/// Установлены ли координаты локации
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
@testable import ClusteringMapView
2+
import Foundation
3+
import Testing
4+
5+
struct DoubleRoundingTests {
6+
@Test("Округление положительных чисел")
7+
func roundingPositiveNumbers() {
8+
#expect(3.14159.rounded(to: 2) == 3.14)
9+
#expect(3.14159.rounded(to: 3) == 3.142)
10+
#expect(3.14159.rounded(to: 4) == 3.1416)
11+
#expect(2.5.rounded(to: 0) == 3.0)
12+
}
13+
14+
@Test("Округление отрицательных чисел")
15+
func roundingNegativeNumbers() {
16+
#expect((-3.14159).rounded(to: 2) == -3.14)
17+
#expect((-3.14159).rounded(to: 3) == -3.142)
18+
#expect((-2.5).rounded(to: 0) == -3.0)
19+
#expect((-1.99).rounded(to: 1) == -2.0)
20+
}
21+
22+
@Test("Округление до нуля знаков после запятой")
23+
func roundingToZeroPlaces() {
24+
#expect(3.7.rounded(to: 0) == 4.0)
25+
#expect(3.2.rounded(to: 0) == 3.0)
26+
#expect(3.5.rounded(to: 0) == 4.0)
27+
#expect((-3.7).rounded(to: 0) == -4.0)
28+
}
29+
30+
@Test("Округление до большого количества знаков")
31+
func roundingToManyPlaces() {
32+
let value = 1.123456789
33+
#expect(value.rounded(to: 5) == 1.12346)
34+
#expect(value.rounded(to: 8) == 1.12345679)
35+
#expect(value.rounded(to: 10) == 1.123456789)
36+
}
37+
38+
@Test("Граничные случаи")
39+
func edgeCases() {
40+
#expect(0.0.rounded(to: 2) == 0.0)
41+
#expect(0.0.rounded(to: 0) == 0.0)
42+
#expect(1.0.rounded(to: 3) == 1.0)
43+
#expect((-0.0).rounded(to: 2) == 0.0)
44+
}
45+
46+
@Test("Очень маленькие числа")
47+
func verySmallNumbers() {
48+
#expect(0.000001.rounded(to: 6) == 0.000001)
49+
#expect(0.000001.rounded(to: 5) == 0.0)
50+
#expect(0.0000015.rounded(to: 6) == 0.000002)
51+
}
52+
53+
@Test("Очень большие числа")
54+
func veryLargeNumbers() {
55+
#expect(1234567.89123.rounded(to: 2) == 1234567.89)
56+
#expect(999999.999.rounded(to: 2) == 1000000.0)
57+
#expect(1000000.0.rounded(to: 3) == 1000000.0)
58+
}
59+
60+
@Test("Проверка точности с плавающей запятой", arguments: [
61+
(3.14159, 2, 3.14),
62+
(0.123456789, 4, 0.1235),
63+
(1.234, 2, 1.23),
64+
(2.678, 2, 2.68)
65+
])
66+
func floatingPointPrecision(input: Double, places: Int, expected: Double) {
67+
let result = input.rounded(to: places)
68+
let tolerance = pow(10.0, Double(-places - 1))
69+
#expect(abs(result - expected) < tolerance)
70+
}
71+
72+
@Test("Округление с одним знаком после запятой")
73+
func roundingToOnePlace() {
74+
#expect(3.14.rounded(to: 1) == 3.1)
75+
#expect(3.15.rounded(to: 1) == 3.2)
76+
#expect(3.149.rounded(to: 1) == 3.1)
77+
#expect(3.151.rounded(to: 1) == 3.2)
78+
}
79+
80+
@Test("Округление чисел близких к нулю")
81+
func roundingNearZero() {
82+
#expect(0.001.rounded(to: 2) == 0.0)
83+
#expect(0.005.rounded(to: 2) == 0.01)
84+
#expect((-0.001).rounded(to: 2) == 0.0)
85+
#expect((-0.005).rounded(to: 2) == -0.01)
86+
}
87+
88+
@Test("Идемпотентность округления")
89+
func roundingIdempotency() {
90+
let value = 3.14159
91+
let roundedOnce = value.rounded(to: 2)
92+
let roundedTwice = roundedOnce.rounded(to: 2)
93+
#expect(roundedOnce == roundedTwice)
94+
95+
let roundedThreeTimes = roundedTwice.rounded(to: 2)
96+
#expect(roundedOnce == roundedThreeTimes)
97+
}
98+
}

SwiftUI-WorkoutApp/PreviewContent/PreviewContent.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,13 @@ extension NoParksFoundModel {
163163
isLoading: false
164164
)
165165
}
166+
167+
extension NoParksFoundModel {
168+
static let preview = Self(
169+
isFilterEdited: true,
170+
isFilteredParksEmpty: true,
171+
didParksManagerLoad: true,
172+
isLoading: false
173+
)
174+
}
166175
#endif

SwiftUI-WorkoutApp/Screens/Parks/Map/ParksMapScreen+ViewModel.swift

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,32 @@ extension ParksMapScreen {
4949
updateSelectedCity(selectedCity)
5050
}
5151

52-
func userInfoDidChange(_ info: UserResponse?) {
52+
/// Обрабатывает изменение данных пользователя
53+
///
54+
/// - Вызывается для настройки `userCoordinate` (для региона без выбранного города) и `cityId` (для новой площадки).
55+
/// - При изменении любого `Published`-свойства вьюмодели происходит перерисовка карты,
56+
/// в том числе может обновиться регион - нам это не нужно, поэтому закрываем все обновления
57+
/// свойств вьюмодели явными проверками на отличия от старых значений
58+
/// - Parameter info: Данные профиля пользователя
59+
func userCityDidChange(_ info: UserResponse?) {
5360
guard let countryId = info?.countryId, let cityId = info?.cityId,
54-
let newCoordinate = SWAddress(countryId, cityId).coordinate else {
55-
userCoordinate = (0, 0)
56-
newParkMapModel.cityId = 0
61+
let newCoordinate = SWAddress(countryId, cityId).coordinate
62+
else {
63+
if userCoordinate != (0, 0) {
64+
userCoordinate = (0, 0)
65+
newParkMapModel.cityId = 0
66+
}
5767
return
5868
}
59-
userCoordinate = (newCoordinate.lat, newCoordinate.lon)
60-
// Сохраняем город пользователя для новой площадки на случай,
61-
// если не получится определить город по локации с помощью CLGeocoder
62-
newParkMapModel.cityId = cityId
69+
let newUserCoordinate = (newCoordinate.lat, newCoordinate.lon)
70+
if userCoordinate != newUserCoordinate {
71+
userCoordinate = newUserCoordinate
72+
}
73+
if newParkMapModel.cityId != cityId {
74+
// Сохраняем город пользователя для новой площадки на случай,
75+
// если не получится определить город по локации с помощью CLGeocoder
76+
newParkMapModel.cityId = cityId
77+
}
6378
}
6479

6580
func updateSelectedCity(_ newCity: City?) {
@@ -181,8 +196,13 @@ private extension ParksMapScreen.ViewModel {
181196
func resetMapRegionTo(_ userCoordinate: (Double, Double)) {
182197
guard userCoordinate != (0, 0) else {
183198
ignoreUserLocation = true
184-
updateSelectedCity(.defaultCity)
185-
logger.debug("Регион карты сброшен к координатам Москвы (пользователь не авторизовался)")
199+
if let coordinate2D = City.defaultCity.coordinate2D {
200+
region = .init(
201+
center: coordinate2D,
202+
span: defaultCoordinateSpan
203+
)
204+
logger.debug("Регион карты сброшен к координатам Москвы (пользователь не авторизовался)")
205+
}
186206
return
187207
}
188208
region = .init(

0 commit comments

Comments
 (0)