@@ -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
135143extension 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
180189private 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: - Трекинг локации
317289private 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}
0 commit comments