Предисловие
Привет, друзья, в начале сразу обозначу цель статьи: экономия вашего времени если вам нужно обновлять или встраивать Яндекс-карты в мобильный клиент на iOS, плюс желание поделиться опытом.
Встроили мы как-то раз в приложение Яндексмапкит (октябрь 2017 года примерно) вместо эпловых карт (ничего личного — только бизнес). Через месяца 3 в один прекрасный зимний день Андройд версия карт вышла из строя на дня 2 из-за ключей, карта просто превратилась в тыкву) На что в чатике от руководства проскочило: “на андройде карты сломали, починить не знают как” В то время, как iOS-клиента это не коснулось. Бедолаги на андройде… на этот раз ребята были не причем ведь. В те дни упали многие прилаги: почта России, Утконос, может помните, ребята?
Это я к тому, что когда ваше приложение завязано на сторонние сервисы, то неплохо бы иметь план “Б” на этот случай, например переключить на предыдущий вариант реализации карт от Apple, а не заменять один на другой…
Еще через месяца 3, где-то в марте от Яндекса прилетело письмо, что наконец-то обновили они sdk, (прошло совсем немного времени, года 4-5 с предшествующего обновления):
"-Обновляйтесь, через год старый отключим", вкратце. До этого просто была старая версия 1.0
Ну, мы, конечно, после такого предупреждения, тянуть не стали и сразу начали переход…через 3 месяца)) в августе.
Этап “комментирования” !(отключения функционала)
Вы скажите, ха…че там обновляться, pod обновил, пару мест поправил да и всё. Так вот нет, ребята, новое API карт абсолютно не совместимо со старым, и более того, как выяснилось позже, там даже отсутствует много жизненно необходимого, что есть из коробки в старой версии и других карточных библиотеках!
Итак, mapkit 3.0 (пока писал статью вышла версия 3.1), ссылка на документацию.
А зачем помимо предупреждения от яндекса? А тем временем на бета-версии Xcode 10 проект со старым китом тупо не собирается, так как используется либа С++ где-то внутри, которая деприкейтед в новой версии. Нужно в подспеке переименовывать ее, чет там обновлять и т.д., короче я не стал этим заниматься, так как в итоге все равно нужно обновляться
1) Обновляем sdk, вместо версии 1.0 сразу 3.0, естественно API поменялось, но чтоб настолько….
Итак старый YMKAnnotation Protocol просто отсутствует
В свифте это выглядит как форсанврапнутый стриг(: что не гуд, да и обращаться потом таская “!” знак такое себе… Пример реализации протокола:
Что ж, напишем заново) свой, только вместо метода coordinate() сделаем свойства, тут все просто, метод избыточен; title!() заменяем на нефорсанврапнутый title(), с другой стороны можно сделать свойство, ну да много, где в проекте менять придется, поэтому просто восклицательный знак пока убрал.
В нашем проекте заменить нужно было в 3х контроллерах пару раз, еще в одном импортировать CoreLocation, так как теперь он не импортируется ни в одном из хедеров мапкита от Яндекса.
Чтоб не ренеймить по всему проекту привычный YMKMapCoordinate (старый кит) сделал typealias для YMKPoint (новый кит)
2) Также давайте объявим несколько свойств, которые нам понадобятся в дальнейшем для работы с картой, в основном это геттеры:
3) Первое, что нужно создать — это YMKMapView (с ним все ок, такой объект по-прежнему доступен). Ранее я его инициализировал сходу, теперь так делать нельзя, будет краш, так как сначала нужно ключ поставить! Текущий ключ не подойдет и нужно просить новый. Добавляем в AppDelegate, согласно документации. Только после установки ключа, можем создать YMKMapView и сконфигурировать необходимым нам образом в методе setupMap()
Что здесь происходит, детально разберем позднее по ходу необходимости в соответствующих настройках
4) Что дальше?, а дальше у нас была начальная локация CLLocation, но теперь для использования нужно добавить фреймворк CoreLocation вручную, или… или заменить на YMKPoint из мапкита от Яндекса
5) Дальше у нас центрировалась карта по этой координате очень простым методом, но теперь простого нету
Зато есть чуть сложнее и чуть поглубже, у объекта map ) … mapView.mapWindow.map!.move. Тут мы узнает о существовании такого объекта как YMKCameraPosition.
6) Далее комментим конфиг карты, так как такого api/свойств уже нету. Сейчас мы это просто опускаем, чтобы хоть минимально запуститься.
Комментим добавление аннотаций, (обращу внимание, что это стандартный функционал), также комментим показ ближайших пунктов (это уже какой-то наш функционал).
И весь YMKMapViewDelegate тоже комментим, который я в новом фрэймворке не нашел и схожий аналог тоже.
Реализацию опустил, только сами методы:
— нужно ли отображать локацию пользователя,
— какие вьюхи использовать для пинов,
— реакция на нажатие на аннотацию,
— какие каллауты для аннотации,
— реакция на нажатие на каллаут, т е все чем обычно пользуемся. В конце был какой-то наш метод, который также использовал определенное АПИ карт.
ВСЁ, c MapVC разобрались — это основной класс, где использовался мапкит
7) Немного закомментим кастомный каллаут, больше он YMKCalloutView не наследует, нет такого больше в новом ките.
Ура, теперь проект собрался, я смог все запустить и… увидеть тетрадь в клеточку, потому что после того, как он заведется, нужно дать время на “прогрев”)), но об этом я не знал и подумал, что что-то не так, хотя предположил, что нужно время для активации ключа. Оказывается, предположение было верным. Подождать нужно примерно часик (может что-то и поменялось теперь).
С очередной попыткой модификации старого апи на новый лад карта отобразилась.
Этап второй — “поиск” (как реализовать старый функционал по-новому)
Начнем восстанавливать утраченный функционал. Итак, нужно отобразить юзерлокацию, но по-старому просто изменив свойство и настроив делегат это сделать нельзя.
Теперь это делается через слой, смотри метод setupMap() пункт 3.
Пример подсмотрим в демо (качаем с гитхаба яндекса), благо, что он там есть. Кстати, обратить нужно внимание на setAnchorWith. Позже скажу почему, связано с зумом. Окей локация работает.
2) Что дальше, ну конечно, аннотации. По-старому добавить нельзя, смотрим в демо опять. Там есть класс — MapObjectsViewController. В новой версии, чтобы добавить пины на карту, делегат не нужен, для этого необходимо обращаться к свойству mapObjects, вызывать на объекте метод addPlacemark, ну и передать туда координату (есть еще другие перегрузки) Пример:
Итерируемся по коллекции аннотаций (например, после получения с сервера) и добавляем по одной на карту. Метод, кстати, возвращает “placeMark” (метку места), которую и с помощью которой можно сделать доп настройку, например, изменить порядок отображения через zIndex свойство.
Тут, правда, я упустил момент, что перед этим я полез искать делегат и благополучно его не нашел и ни одного делегата вообще (на самом деле они просто по-другому стали называться, теперь это слушатели). Я же знаю из предыдущего опыта с эпловским китом, да и старым китом яндекса, что аннотации переиспользуются, все как с ячейками, но в демо есть только addPlacemark. На вопрос к лиду Яндекс-карт (тут помогло небольшое личное знакомство) “- Как оптимизировать использование памяти, переиспользовать объекты?” Ответ: “А зачем, итак нормально работает”… ну вроде да, работает.
Примечание: 1) Важно отметить, что Яндекс.Карты используют мапкит, а не разрабатывают его. Это делает команда мапкита (ник Николая на хабре — likhogrud@).
2) Объяснение почему объекты не переиспользуются:
В старом ките annotationView были вьюхами, их создавал пользователь, и конечно вьюхи нужно переиспользовать, потому что их создание недешево. В новом ките плейсмарки создаются мапкитом прямо в open GL. И возможно они там и реиспользуются, но это неточно. В любом случае это намного эффективнее, чем создание вьюх.
3) Из новенького, кстати, есть возможность модификации иконки аннотации для пользователя. Реализовано так: нужно добавить слушателя ( аналог делегата), реализовать соответствующий протокол — 1 из 3-х методов, 2-ва просто оставить пустыми.
Заодно перезагрузим placeмark-и с нашими иконками.
Также, обращу ваше внимание на свойство анкор. Нажав на карте кнопку локации юзера, камера перемещает фокус в центр локации. Но вот беда, повторное нажатие действия не производит. Чё? Комментим метод анкер и все работает.
4) Теперь нужно показать каллаут, соответственно отработать нажатие. Есть несколько методов в интерфейсах, правильный — это YMKMapObjectTapListener. Tам есть 1 важный интересный метод, с которым пришлось еще намучиться позже, он возвращаем true, чтоб дальше не интерироваться, если найден подписчик. Обращаю внимание, нужно подписаться сначала, подписываться будет mapObjects (линия 149).
Итак нажатия отрабатывает. Ура. Еще, правда, были попытки отображения пинов только в видимой зоне, это лишнее, показывайте все сразу, поэтому оставим (как раз потому что не тормозит)
5) Потом захотелось для удобства сделать кнопки zoom in/out. Немного копипаста и правок по аналогии с кнопкой локации, и готово.
Дальше, так как знаем о камере, используя метод move и соответственно текущий зум +- 1 или 0.5, сколько вам нужно. Тут все нормально.
6) Переходим к основному функционалу — коллаут (это такой прямоугольник с доп инфой, с треугольничком внизу). Тут выясняется, что API нет («опьяняет летом» — распознал мою речь Яндекс, когда я читал пометки с листочка, чтоб не набирать вручную эту статью).
Как, ребята? 100 500 приложений юзают коллаут.
Хорошо пишем в “тех поддержку” (Коле) как это сделать в ручную, узнаю. Какие у вас варианты?
Вот такие вот костыли получается на ровном, казалось месте.
На самом деле не костыли, конечно, а просто отсутствие считаю базового функционала
7) Ладно, давайте для начала добавим тестовый каллаут, в качестве такового используем красный квадрат. Итак, по нажатию на наш пин вызывается метод делегата/ слушателя, куда и передается точка нажатия и объект. Не ожидая подвоха, берем “point” в качестве точки куда нужно добавить коллаут. (Внимание, все ли правильно сделано: “Нажали, взяли точку, привязали”? Ок 80% ответили правильно, 20% — нет)
И вызовем в теле метода вспомогательный метод showCallout:
строка 544
Внутри создадим тестовую вью красного цвета сорок на сорок, конвертнем в картинку, объявим константу х со значением 0.5, будет использоваться для положения треугольника коллаута в середине поинта. От идеи менять его положение, в дальнейшем отказался, предпочел сдвигать камеру, чтобы выбранный каллаут отображался посредине экрана телефона
Далее объявляем “нажимаемую” область tappableArea, есть такое свойство у стиля иконки для пина. О неплохо, можно ограничить зону нажатие, так и сделаем. Зона варьируется от 0.0 до 1.1. нам нужна нижняя часть, где якобы кнопка, которую трансформировали в картинку до этого (помним). Окей, значит зона (0,0.5 — 1.1) так как кнопка внизу находиться.
Ограничение зоны работает, но есть нюанс, да такой, что все сводит на нет. Если под ненажимаемой областью другой пин, то нажатие сработает на нем. Смысл этой области? Сделали б флаг что ли, чтобы нажатие не проходило. Ладно…
550 строка
создадим стиль для иконки, можно сразу в первом параметре указать позицию анкора, я для примера сделал ниже на 557 строке. Позиция у равна 1.05, чтобы поднять треугольник над пином опять же по вертикали
559 строка
создаем наш кастомный коллаут определенного размера,
конфигурируем необходимые нам поля с помощью информации из выбранной аннотации selectedAnnotation, в частности тайтл и сабтайтл, надпись на кнопке у этого коллаута. Тут вы сами что хотите, то и можете делать. Выбранную аннотацию определяем ранее в делегате. Но на карту пока добавляем созданный ранее красный квадрат
Далее в коллекцию mapObjects добавляем пин, метод вернет нам добавленный плэйсмарк, сохраним его в переменную,
По нажатию на сам коллаут открывается детальный контроллер, так вот нюанс, если под попапом другой пин и в него попали, сработает делегат опять, поэтому тут нужно изменить порядок в иерархии через zIndex. Настроим видимость, и сместим наш коллаут в центр на 564 строке
Нюансы: Переменная плэйсмарк является указателем на каллаут.
Сначала у нас его нет, после нажатия на пин, он появляется, после нажатия на следующий пин, нам нужно удалить наш первый коллаут и добавить новый. Поэтому, если переменная placemark != nil, нужно из коллекции mapObjects удалить старый коллаут)
Также в случае тапа на карту нужно скрыть коллаут, поэтому в методе делегата присваиваем placemark, срабатывает обзервер, коллаут удаляется, плюс очищаем выбранную аннотацию
Для этого мы и подписывались ранее на YMKMapInputListener
Как делать вьюху не описываю), но если будет много проблем(с треугольником возможно) с этим, то пишите, добавлю и этот этап
Магическая 20-ка, добавленная к высоте, нужна для места под треугольник внизу, который тож придется рисовать
Еще мы хотим, чтобы popup всплывал (был привязан) к пину в определенном месте (слева, справа, в середине), для этого есть свойство анкор. Определял так:
Видимую область карты делим на 3 зоны по вертикали и определяем в какой находимся, в зависимости от этого, меняем положение привязки. В примере кода проверяем находимся ли в левой части, по аналогии делаем для середины, если не слева и не в середине, соответственно точка справа находиться
Вспомогательная функция для проверки, попадает ли точка в регион:
Старт. Работает. Но здесь есть нюанс, казалось бы, пробуем зумить, раз, два, три и коллаут улетает от пина. Чё? Как?
7) Начинаем debug, координаты совпадают
Потом были попытки понять что происходит и как это работает, очередное возвращение в демо, еще более внимательный поиск отличий…
Замечаю, что координата передаются напрямую, а не та, что по тапу! Но ведь я же дебажил, видно что координаты те же, то есть круг и квадрат имеют одну и ту же координату.
Вот поэтому в методе сразу не обратился к объекту, а передавал point, что неверно.
А нужно, кастить объект, брать свойство (везде все по-разному называется, то coordinate, то point, тут вот теперь geometry) это такой креатив или как)? 493 строчка
Так как нам надо обработать два варианта нажатия: первый на пин, второй на коллаут, и не обрабатывать нажатия, если повторно нажали на один и тот же пин, то первое, что делаем — это находим пин на который мы нажали в коллекции пинов, сравнивая координаты 495 строка, иначе возвращаем true, тем самым говоря, что нажатие мы обработали и дальше по иерархии не надо идти
Второе: это определяем нажали мы на пин или на коллаут, сравним также координаты меток 499 строка. Проверка на равенство:
Далее если это коллаут и мы хотим реагировать на нажатие на кнопку(или имитировать, так как теперь это картинка), а не на всю область, то нужно ручками произвести некоторые расчеты:)
- Конвертируем мировую координату в экранную 501 строка
Считаем сами: конвертим координаты карты в экранные, анкор ведь знаем где расположен, потом добавлением ширины и высоты вьюхи, получаем угловые точки, но почему то они не совпадают и я вручную умножал на три, в моем случае для 10го айфона)). Как потом выяснилось, я забыл и не учел количество пикселей на точку. Которых может быть у нас 1х, (одна точка 1 пиксель), 2х, 3х одна точка это три пикселя. - Вычислим высоту кнопки — высота коллаута + высота треугольника, умноженная на масштаб, информация о scale (строка 498). Далее делим все это на два, так как высота кнопки составляет половину от высоты коллаута
- Потом вычисляем координаты углов, исходя из того, что анкор (х: 0.5, у: 1), учитывая масштаб и зону треугольника))
- Затем эти экранные координаты конвертим в мировые
- Создаем видимую область на их основе, представляет собой зону кнопки
- И проверяем, попали мы при нажатии в зону кнопки или нет. Если попали, то проверяем тип аннотации, в зависимости от типа вызываем какой-то наш метод: переходим на детальный экран или выбираем этот магазин для доставки в случаи со StorePoint
В противном случае — это нажатие на пин и нам нужно добавить коллаут, что собственно и делали выше уже.
Вот и все, на этом первичное знакомство с новым китом закончилось.
П.С. Пока решил и стал писать статью, вышло обновление 3.1, где из вышеупомянутых проблем решены и реализованы:
Добавлено
Для Android появились arm64 и x86 сборки.
На карту можно добавить любой объект View.
Появилась веломаршрутизация.
Добавлены аннотации nullable для Android.
Изменено
MapKit разбит на части:
MapKit — только карта;
MapKit Directions — автомобильная маршрутизация;
MapKit Transport — пешеходная маршрутизация, маршрутизация с использованием общественного транспорта и велосипедная маршрутизация;
MapKit Search — поиск и геокодирование;
MapKit Places — панорамы.
Для iOS аннотации nullable стали более строгими.
Исправлено
Исправлен ряд ошибок.
Улучшена производительность.
tech.yandex.ru/maps/doc/mapkit/3.x/concepts/versions-docpage
Пишите свои комментарии, вопросы.