Skip to content

Commit fed77df

Browse files
committed
Рефактор
1 parent 31c2f6f commit fed77df

File tree

2 files changed

+136
-122
lines changed

2 files changed

+136
-122
lines changed

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

Lines changed: 134 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,80 @@ extension ParksMapScreen {
1111
subsystem: Bundle.main.bundleIdentifier!,
1212
category: "ParksMapScreenViewModel"
1313
)
14-
/// `true` - таймер отслеживания локации актиуен, `false` - неактивен
14+
/// Подписки, которые должны работать всегда, пока существует вьюмодель
15+
private var persistentCancellables = Set<AnyCancellable>()
16+
17+
override init() {
18+
super.init()
19+
manager.delegate = self
20+
manager.requestWhenInUseAuthorization()
21+
setupUserCityCoordinateObserver()
22+
setupUserLocationObserver()
23+
setupAppLifecycleObservers()
24+
setupRegionChangeObserver()
25+
updateSelectedCity(selectedCity)
26+
}
27+
28+
// MARK: - Трекинг локации
29+
/// Менеджер локации
30+
private let manager = CLLocationManager()
31+
/// `true` - таймер отслеживания локации активен, `false` - неактивен
1532
///
1633
/// Местоположение пользователя отслеживается каждые 10 секунд по таймеру,
1734
/// чтобы снизить нагрузку на аккумулятор
1835
private var isLocationTrackingActive = false
1936
private let locationTrackingInterval: TimeInterval = 10
37+
private var locationTrackingCancellable: AnyCancellable?
2038
/// Крайняя локация пользователя, которую мы определили и сохранили в этом сеансе
2139
@Published private var lastUserLocation: CLLocation?
40+
/// Влияет на доступность кнопки отслеживания локации на карте
41+
@Published private(set) var ignoreUserLocation = false
42+
/// Сообщение об ошибке, связанное с локацией
43+
@Published private(set) var locationErrorMessage = ""
44+
/// Включает или выключает определение локации пользователя
45+
func setLocationTracking(_ active: Bool) {
46+
guard isLocationTrackingActive != active else { return }
47+
isLocationTrackingActive = active
48+
if active {
49+
startPeriodicLocationUpdates()
50+
} else {
51+
stopPeriodicLocationUpdates()
52+
}
53+
}
54+
55+
// MARK: - Геокодирование
2256
/// Запускаем геокодирование не чаще раза в минуту
2357
private let geocodingInterval: TimeInterval = 60
2458
/// Последняя локация, для которой выполнялось геокодирование
2559
private var lastGeocodedLocation: CLLocation?
26-
private let defaultCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
27-
/// Подписки, которые должны работать всегда, пока существует вьюмодель
28-
private var persistentCancellables = Set<AnyCancellable>()
2960
private var geocodingCancellable: AnyCancellable?
30-
private var locationTrackingCancellable: AnyCancellable?
31-
/// Менеджер локации
32-
private let manager = CLLocationManager()
61+
62+
// MARK: - Регион карты
63+
@Published private(set) var region = MKCoordinateRegion()
64+
/// Предыдущее значение региона для отслеживания изменений
65+
private var previousRegion: MKCoordinateRegion?
66+
private let defaultCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
67+
3368
/// Нужно ли обновлять регион карты
3469
///
3570
/// Используется для выборочного обновления региона
3671
/// внутри `ClusteringMapView` в методе `updateUIView`
3772
@Published private(set) var shouldUpdateRegion = false
38-
@Published private(set) var locationErrorMessage = ""
39-
/// Модель с данными для создания новой площадки
40-
@Published private(set) var newParkMapModel = NewParkMapModel.empty
41-
/// Можно ли создавать новую площадку
42-
var canCreateNewPark: Bool {
43-
locationErrorMessage.isEmpty && !newParkMapModel.isEmpty
73+
/// Сбрасывает флаг обновления региона
74+
func resetRegionUpdateFlag() {
75+
if shouldUpdateRegion {
76+
logger.debug("Сбрасываем флаг обновления региона")
77+
shouldUpdateRegion = false
78+
}
4479
}
4580

81+
// MARK: - Город пользователя и выбранный город
82+
/// Координаты города в профиле авторизованного пользователя
83+
@Published private var userCityCoordinate = LocationCoordinate.empty
84+
4685
/// Город для фильтра списка площадок
4786
@AppStorage("selectedCityFilter") private(set) var selectedCity: City?
87+
4888
var cityFilterButtonTitle: String {
4989
if let selectedCity {
5090
selectedCity.name
@@ -54,31 +94,10 @@ extension ParksMapScreen {
5494
}
5595

5696
var canClearCityFilter: Bool { selectedCity != nil }
57-
@Published private(set) var region = MKCoordinateRegion()
58-
/// Предыдущее значение региона для отслеживания изменений
59-
private var previousRegion: MKCoordinateRegion?
60-
/// Влияет на доступность кнопки отслеживания локации на карте
61-
@Published private(set) var ignoreUserLocation = false
62-
/// Координаты города в профиле авторизованного пользователя
63-
@Published private var userCityCoordinate = LocationCoordinate.empty
64-
65-
override init() {
66-
super.init()
67-
manager.delegate = self
68-
manager.requestWhenInUseAuthorization()
69-
setupUserCityCoordinateObserver()
70-
setupUserLocationObserver()
71-
setupAppLifecycleObservers()
72-
setupRegionChangeObserver()
73-
updateSelectedCity(selectedCity)
74-
}
7597

7698
/// Обрабатывает изменение данных пользователя
7799
///
78100
/// - Вызывается для настройки `userCoordinate` (для региона без выбранного города) и `cityId` (для новой площадки).
79-
/// - При изменении любого `Published`-свойства вьюмодели происходит перерисовка карты,
80-
/// в том числе может обновиться регион - нам это не нужно, поэтому закрываем все обновления
81-
/// свойств вьюмодели явными проверками на отличия от старых значений
82101
/// - Parameter info: Данные профиля пользователя
83102
func userCityDidChange(_ info: UserResponse?) {
84103
guard let countryId = info?.countryId, let cityId = info?.cityId,
@@ -109,29 +128,18 @@ extension ParksMapScreen {
109128
}
110129
}
111130

112-
/// Включает или выключает вьюмодель для работы
113-
func setActive(_ active: Bool) {
114-
guard isLocationTrackingActive != active else {
115-
return
116-
}
117-
isLocationTrackingActive = active
118-
if active {
119-
startPeriodicLocationUpdates()
120-
} else {
121-
stopPeriodicLocationUpdates()
122-
}
123-
}
131+
// MARK: - Новая площадка
132+
/// Модель с данными для создания новой площадки
133+
@Published private(set) var newParkMapModel = NewParkMapModel.empty
124134

125-
/// Сбрасывает флаг обновления региона
126-
func resetRegionUpdateFlag() {
127-
if shouldUpdateRegion {
128-
logger.debug("Сбрасываем флаг обновления региона")
129-
shouldUpdateRegion = false
130-
}
135+
/// Можно ли создавать новую площадку
136+
var canCreateNewPark: Bool {
137+
locationErrorMessage.isEmpty && !newParkMapModel.isEmpty
131138
}
132139
}
133140
}
134141

142+
// MARK: - CLLocationManagerDelegate
135143
extension ParksMapScreen.ViewModel: CLLocationManagerDelegate {
136144
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
137145
guard let location = locations.last else { return }
@@ -177,8 +185,9 @@ extension ParksMapScreen.ViewModel: CLLocationManagerDelegate {
177185
}
178186
}
179187

188+
// MARK: - Подписки Combine
180189
private extension ParksMapScreen.ViewModel {
181-
/// Настраивает отслеживание изменений региона для управления флагом
190+
/// Отслеживаем изменения региона для управления флагом `shouldUpdateRegion`
182191
func setupRegionChangeObserver() {
183192
$region
184193
.dropFirst()
@@ -196,8 +205,8 @@ private extension ParksMapScreen.ViewModel {
196205
.store(in: &persistentCancellables)
197206
}
198207

208+
/// Реагируем на изменение `userCityCoordinates`, если город не выбран
199209
func setupUserCityCoordinateObserver() {
200-
// Реагируем на изменение `userCityCoordinates`, если город не выбран
201210
$userCityCoordinate
202211
.dropFirst()
203212
.removeDuplicates()
@@ -212,8 +221,7 @@ private extension ParksMapScreen.ViewModel {
212221
.store(in: &persistentCancellables)
213222
}
214223

215-
/// Подписываемся на события сворачивания/разворачивания приложения,
216-
/// чтобы включать и выключать отслеживание локации
224+
/// Подписываемся на события сворачивания/разворачивания приложения, чтобы включать и выключать отслеживание локации
217225
func setupAppLifecycleObservers() {
218226
let center = NotificationCenter.default
219227
center.publisher(for: UIApplication.didEnterBackgroundNotification)
@@ -229,66 +237,29 @@ private extension ParksMapScreen.ViewModel {
229237
.store(in: &persistentCancellables)
230238
}
231239

240+
func setupUserLocationObserver() {
241+
$lastUserLocation
242+
.compactMap(\.self)
243+
.receive(on: DispatchQueue.main)
244+
.sink { [weak self] _ in
245+
self?.startGeocodingTimer()
246+
}
247+
.store(in: &persistentCancellables)
248+
}
249+
}
250+
251+
// MARK: - Работа с регионом
252+
private extension ParksMapScreen.ViewModel {
232253
func setupDefaultLocation(permissionDenied: Bool) {
233254
locationErrorMessage = permissionDenied
234255
? Strings.Alert.locationPermissionDenied
235256
: Strings.Alert.needLocationPermission
236257
resetMapRegionTo(userCityCoordinate)
237258
}
238259

239-
func startPeriodicLocationUpdates() {
240-
guard isLocationTrackingActive else { return }
241-
stopPeriodicLocationUpdates()
242-
logger.debug("Запустили таймер для отслеживание локации")
243-
locationTrackingCancellable = Timer.publish(
244-
every: locationTrackingInterval,
245-
on: .main,
246-
in: .common
247-
)
248-
.autoconnect()
249-
.sink { [weak self] _ in
250-
guard let self, isLocationTrackingActive else { return }
251-
logger.debug("Запрашиваем новую локацию")
252-
manager.requestLocation()
253-
}
254-
}
255-
256-
func stopPeriodicLocationUpdates() {
257-
if locationTrackingCancellable != nil {
258-
logger.debug("Остановили отслеживание локации")
259-
locationTrackingCancellable?.cancel()
260-
locationTrackingCancellable = nil
261-
}
262-
}
263-
264-
/// Обновляет адрес для новой площадки, если нужно
265-
///
266-
/// - Новый адрес должен отличаться от старого
267-
/// - Адрес включает все доступные данные, полученные из `placemark`
268-
/// - Parameter placemark: Точка на карте
269-
func updateAddressIfNeeded(placemark: CLPlacemark) {
270-
let fullAddress = SWAddress.makeAddress(for: placemark)
271-
if let fullAddress, fullAddress != newParkMapModel.address {
272-
newParkMapModel.address = fullAddress
273-
logger.debug("Адрес для площадки: \(fullAddress)")
274-
}
275-
}
276-
277-
/// Обновляет идентификатор города для новой площадки, если нужно
278-
///
279-
/// Новый идентификатор должен отличаться от старого
280-
/// - Parameter placemark: Точка на карте
281-
func updateCityIfNeeded(placemark: CLPlacemark) {
282-
if let cityId = SWAddress.makeCityId(with: placemark.locality),
283-
cityId != newParkMapModel.cityId {
284-
newParkMapModel.cityId = cityId
285-
logger.debug("Идентификатор города для площадки: \(cityId)")
286-
}
287-
}
288-
289260
/// Сбрасывает регион карты на точку пользователя из профиля
290261
///
291-
/// Предварительно вычисляем широту и долготу при помощи `SWAddress` на основе справочника стран/городов.
262+
/// Предварительно вычисляем широту и долготу при помощи `SWAddress` на основе справочника стран/городов.
292263
/// - Parameter userCoordinate: Широта и долгота по данным профиля
293264
func resetMapRegionTo(_ userCoordinate: LocationCoordinate) {
294265
guard userCoordinate.isSpecified else {
@@ -314,17 +285,36 @@ private extension ParksMapScreen.ViewModel {
314285
}
315286
}
316287

288+
// MARK: - Трекинг локации
317289
private extension ParksMapScreen.ViewModel {
318-
func setupUserLocationObserver() {
319-
$lastUserLocation
320-
.compactMap(\.self)
321-
.receive(on: DispatchQueue.main)
322-
.sink { [weak self] _ in
323-
self?.startGeocodingTimer()
324-
}
325-
.store(in: &persistentCancellables)
290+
func startPeriodicLocationUpdates() {
291+
guard isLocationTrackingActive else { return }
292+
stopPeriodicLocationUpdates()
293+
logger.debug("Запустили таймер для отслеживание локации")
294+
locationTrackingCancellable = Timer.publish(
295+
every: locationTrackingInterval,
296+
on: .main,
297+
in: .common
298+
)
299+
.autoconnect()
300+
.sink { [weak self] _ in
301+
guard let self, isLocationTrackingActive else { return }
302+
logger.debug("Запрашиваем новую локацию")
303+
manager.requestLocation()
304+
}
326305
}
327306

307+
func stopPeriodicLocationUpdates() {
308+
if locationTrackingCancellable != nil {
309+
logger.debug("Остановили отслеживание локации")
310+
locationTrackingCancellable?.cancel()
311+
locationTrackingCancellable = nil
312+
}
313+
}
314+
}
315+
316+
// MARK: - Геокодирование
317+
private extension ParksMapScreen.ViewModel {
328318
/// Запускает таймер геокодирования при получении новой локации
329319
func startGeocodingTimer() {
330320
stopGeocodingTimer()
@@ -354,24 +344,23 @@ private extension ParksMapScreen.ViewModel {
354344

355345
/// Выполняет геокодирование, если есть актуальная локация и это необходимо
356346
func performGeocodingIfNeeded() {
357-
guard let location = lastUserLocation else {
347+
guard let lastUserLocation else {
358348
logger.debug("Нет локации для геокодирования")
359349
return
360350
}
361-
let distanceFromLastGeocode = lastGeocodedLocation?.distance(from: location) ?? 1000
351+
let distanceFromLastGeocode = lastGeocodedLocation?.distance(from: lastUserLocation) ?? 1000
362352
let isCityEmpty = newParkMapModel.cityId == 0
363353
let isAddressEmpty = newParkMapModel.address.isEmpty
364354
let shouldUpdateAddress = isCityEmpty || isAddressEmpty
365355
let movedSignificantly = distanceFromLastGeocode > 50
366-
367356
guard shouldUpdateAddress || movedSignificantly else {
368357
logger.debug("Геокодирование не требуется")
369358
return
370359
}
371360
logger.debug("Запускаем CLGeocoder... (нужен адрес: \(shouldUpdateAddress), далеко прошли: \(movedSignificantly))")
372-
lastGeocodedLocation = location
361+
lastGeocodedLocation = lastUserLocation
373362
CLGeocoder().reverseGeocodeLocation(
374-
location,
363+
lastUserLocation,
375364
preferredLocale: .init(identifier: "ru_RU")
376365
) { [weak self] places, error in
377366
guard let self, let target = places?.first else { return }
@@ -385,4 +374,29 @@ private extension ParksMapScreen.ViewModel {
385374
updateAddressIfNeeded(placemark: target)
386375
}
387376
}
377+
378+
/// Обновляет адрес для новой площадки, если нужно
379+
///
380+
/// - Новый адрес должен отличаться от старого
381+
/// - Адрес включает все доступные данные, полученные из `placemark`
382+
/// - Parameter placemark: Точка на карте
383+
func updateAddressIfNeeded(placemark: CLPlacemark) {
384+
let fullAddress = SWAddress.makeAddress(for: placemark)
385+
if let fullAddress, fullAddress != newParkMapModel.address {
386+
newParkMapModel.address = fullAddress
387+
logger.debug("Адрес для площадки: \(fullAddress)")
388+
}
389+
}
390+
391+
/// Обновляет идентификатор города для новой площадки, если нужно
392+
///
393+
/// Новый идентификатор должен отличаться от старого
394+
/// - Parameter placemark: Точка на карте
395+
func updateCityIfNeeded(placemark: CLPlacemark) {
396+
if let cityId = SWAddress.makeCityId(with: placemark.locality),
397+
cityId != newParkMapModel.cityId {
398+
newParkMapModel.cityId = cityId
399+
logger.debug("Идентификатор города для площадки: \(cityId)")
400+
}
401+
}
388402
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ struct ParksMapScreen: View {
4545
}
4646
.task { await askForParks() }
4747
.onAppear {
48-
viewModel.setActive(true)
48+
viewModel.setLocationTracking(true)
4949
}
5050
.onDisappear {
51-
viewModel.setActive(false)
51+
viewModel.setLocationTracking(false)
5252
}
5353
.sheet(item: $sheetItem) { makeContentView(for: $0) }
5454
.toolbar {

0 commit comments

Comments
 (0)