
Всем привет! Меня зовут Женя, я работаю iOS разработчиком в каршеринг-сервисе Ситидрайв, где мы с командой стремимся улучшить пользовательский опыт и сделать наше приложение более интуитивно понятным и функциональным. В этой статье я расскажу, как у нас организована работа с картой: как отображаем автомобили и другие объекты, какие проблемы возникали в процессе разработки, и почему мы выбрали формат данных GeoJSON. Также поделюсь особенностями работы с форматом, которые важно знать любому разработчику и расскажу о некоторых его преимуществах.
Как мы организовали свою работу
У нас есть MapProviderProtocol протокол, определяющий поведение, которое должен уметь делать провайдер картографических данных. По сути, мы оборачиваем методы публичного SDK под внутреннюю реализацию нашего приложения.
protocol MapProviderProtocol: AnyObject {
var view: UIView { get }
func show(wrappedPolygons: inout [PolygonWrapper])
func show(wrappedPolylines: inout [PolylineWrapper])
func show(wrappedCircle: inout CircleWrapper)
func hide(wrappedPolygons: [PolygonWrapper])
func hide(wrappedPolylines: [PolylineWrapper])
func hide(wrappedCircle: CircleWrapper)
func listenCameraPositions()
}Протокол имеет и другие свойства:
Добавление маркеров авто и геолокации пользователя.
Обновление кластеризации.
Получение координат по нажатию на слое с картой.
Перемещение viewport.
Готовый объект мы получаем от фабрики, которая резолвит условие от сервиса конфигурации. Например, мы можем выбрать на сервере карту какого провайдера отображать, если у пользователей неожиданно будут наблюдаться проблемы.
class MapFactory {
func makeMap() -> MapProviderProtocol {
toggleService.isEnabled(.googleMaps)
? GMaps()
: DoubleGisMaps()
}
}Если позже мы захотим добавить работу с OSM (OpenStreetMap) или нативными картами (MapKit), нужно будет написать адаптер, который поддерживает и реализует методы MapProviderProtocol.
Далее управляемый экземпляр карты мы оборачиваем в класс, который соответствует MapProtocol. Этот протокол определяет поведение для объектов, управляющих бизнес данными:
protocol MapProtocol: AnyObject {
var map: MapProviderProtocol { get }
func show()
func removeAllDrawnObjects()
}Так, корневым объектом, который далее мы будем декорировать, является BaseMap класс. Он занимается обновлением и наблюдением за видимой областью карты в момент ее использования.
class BaseMap: MapProtocol {
let map: MapProviderProtocol
init(map: MapProviderProtocol) {
self.map = map
}
func show() {
map.listenCameraPositions()
}
func removeAllDrawnObjects() {}
}Далее накидываем нужные слои, например, ParkingsMap, CarMap, LocationMap и так далее. Каждый из них отвечает за отображение своих собственных данных. Всего у нас есть 14 различных декораторов, которые инкапсулируют логику добавления графики на MapProviderProtocol.
Вот как может выглядеть реализация такого декоратора:
class ActiveCarMap: MapProtocol {
private var selectedCar: MapMarker?
private let car: Car
private let decorator: MapProtocol
let map: MapProviderProtocol
init(decorator: MapProtocol, car: Car) {
self.car = car
self.map = decorator.map
self.decorator = decorator
}
func show() {
decorator.show()
let marker = map.makeMarker(
location: car.location,
icon: car.icon
)
map.add(markers: [marker])
selectedCar = marker
selectedCar?.loadImage(from: car.pinImage)
}
func removeAllDrawnObjects() {
decorator.removeAllDrawnObjects()
if let marker = selectedCarMarker {
map.remove(markers: [marker])
selectedCar = nil
}
}
}Новый слой принимает в инициализаторе MapProtocol объект –– это всегда ранее упомянутый BaseMap, который, как правило, мы передаём во view model при создании модуля. Вот как это выглядит на практике:
class MapViewModel {
let map: MapProviderProtocol
let baseMap: MapProtocol
let mapComposite: MapCompositeProtocol
let car: Car
init(
map: MapProviderProtocol,
baseMap: MapCompositeProtocol,
car: Car
) {
self.map = map
self.baseMap = BaseMap(map: map)
self.mapComposite = mapComposite
self.car = car
setupMap()
}
func setupMap() {
mapComposite.add(map: baseMap)
let locationMap = LocationMap(decoratee: baseMap)
mapComposite.add(map: locationMap)
let activeCarMap = ActiveCarMap(decoratee: baseMap, car: car)
mapComposite.add(map: activeCarMap)
}
}
