+(AppStore *) Timera: архитектура приложения и особенности разработки. Часть 2

от автора

Для тех, кто не читал первую часть, коротко: в ней было описано решение задачи по созданию слоев и эффектов, для совмещения старой и новой фотографии. Подробно об этом можно прочитать здесь.

В этой части наш iOS разработчик – heximal – расскажет о том, как реализовывалась гео-позиционно-картографическая функциональность. Так как у него read only то этот пост опять публикую я (будет классно, если кто-нибудь даст ему инвайт). С его слов все записано верно.

— «Изначально timera проектировалась, как средство создания временнЫх туннелей. Согласно многим научным теориям пространство и время имеют непосредственную связь, поэтому местоположение для таймеры является очень важным аспектом. Первая реализация таймеры подразумевала поиск старых фотографий исключительно на карте: пользователь открывал в приложении экран с картой, находил старые фотографии вокруг себя, выбирал понравившуюся, и запускал процесс таймераграфии. Ради этого был разработан веб-сервис, возвращающий все старые фотографии в регионе, который определялся видимой областью на экране карты (minlat,maxlat,minlng,maxlng). На этапе тестирования стало ясно, что данный процесс необходимо оптимизировать, поскольку количество пинов в определенной области может достичь такого количества, что будет непонятное месиво, и в итоге выбрать какой-то объект на карте станет очень сложно.

image

Устранять эту проблему было решено с помощью кластеризации. Кластеры – это пины на карте, связанные с группой, нежели с каким-то определенным объектом. Визуально – это иконка с числом, отражающим количество сгруппированых объектов.
Здесь следует упомянуть, что в качестве картографического сервиса мы избралиGoogle Maps. Почему мы предпочли их нативным Apple картам? Скорее всего, это решение ближайшем будущем будет пересмотрено. Просто на момент проектирования еще было свежы воспоминания о фиаско Apple-карт, а так же мой личный опыт использования обоих фреймворков.
Возвращаемся к кластеризации. Изначально рассматривалась возможность реализации кластеров локально. Выяснилось, что Google Maps имеет возможность делать это буквально в одну строчку кода, однако это свойство было доступно только на Андроиде. Мы уже начали локальную реализацию кластеров на iOS, как в коллективное сознание взбрела здравая мысль: усовершенствовать веб-сервис, таким образом, чтобы он возвращал кластеры. Серверное решение имеет большое преимущество в плане оптимизации: во-первых, уменьшение количества передаваемых объектов (читай, уменьшение трафика), и во-вторых снижения стоимости сохранения данных в Core Data. Модель Core Data так же пришлось модифицировать – добавилась новая сущность MapCluster, который обладает такими атрибутами, как latitude, longitude, zoom, count, objectId
где
latitude, longitude – координаты кластера
zoom – уровень зума, которую выставляет пользователь
count – количество объектов, привязанных к кластеру.
objectId – кластер может быть привязан к конкретному объекту, таким образом, его нужно отображать в виде настоящего кликабельного пина.

Далее дело техники: если пользователь меняет местоположение карты или уровень зума, то сначала делается запрос в локальном хранилище для выбранной области, и кластеры из полученной коллекции наносятся на карту, а также следом отправляется запрос на сервер с теми же параметрами. Если со связью все хорошо, и сервер возвращает ответ, локальные кластеры из базы удаляются, и на их место заливаются новые – так происходит обновление.
Уверен, что много всего интересного о реализации веб-сервиса могли бы расказать наши сервер-сайд разработчики. Могу лишь сказать, что ради этого так же создавались новые сущности БД для агрегации объектов в кластеры, которые заполняются задачей, запускаемой по расписанию.

Еще пару интересных моментов хотелось бы рассказать об iOS реализации кластеров. Для отрисовки кластеров пришлось создавать небольшой класс, который возвращает изображение в виде кружка с числом, ведь методу setIcon класса GMSMarker из GoogleMaps.framework нужен именно UIImage, в виде которого он отобразит соответствуюший пин.
В итоге созданный класс представляет собой наследника UIView, который содержит вложенные элементы, формирующие изображение кластера, а UIImage из этого всего получается следующим методом:

-(UIImage *) renderedClusterImage { UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale); [self.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage *capturedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return capturedImage; }

Также хотелось бы рассказать об одной особенности GoogleMaps фреймворка, с которой мне пришлось изрядно повозиться. Речь идет о способе создания кастомного представления InfoWindow (окошко с описанием, которое появляется, когда пользователь тыкает по пину).

Для отображения кастомного окна информации гугл-карты вызывают метод делегата

- (UIView *)mapView:(GMSMapView *)mapView markerInfoWindow:(GMSMarker *)marker;

как видно, этот метод должен вернуть объект UIView. На этой вьюхе можно располагать разные компоненты (UILabel, UIImageView etc), и все это в итоге будет отображено рядом с выбранным пином. Вроде все понятно и не вызывает подозрений. Однако в нашем случае возникла необходимость перерисовки окна в связи с тем, что картинка превью на момент открытия InfoWindow может не быть загружена с сервера. В таком случае запускается процесс загрузки изображения, по завершению которого нужно перерисовать InfoWindow. И тут возник нюанс. Я думал, достаточно будет сохранить указатель на UIView, который мы возвращаем в методе делегата, а потом через свойства поменять изображение вложенному UIImageView. Оказалось, GoogleMaps растеризует (переводит в UIImage) отданный ему UIView, возможно из соображений оптимизации, поэтому все попытки перерисовать его задуманным способом оказались тщетны.

В результате пришлось изобретать хак. Заключался он в следующем: при тапе на пин показывается пустое InfoWindow, если данных еще нет, запускается процесс загрузки, и далее происходит следующее:

- (UIView *)mapView:(GMSMapView *)mapView markerInfoWindow:(GMSMarker *)marker { TMMapImagePreview *view = [[TMMapImagePreview alloc] initWithFrame:CGRectMake(0, 0, mapView.frame.size.width * 0.7, 64)]; id image = marker.userData; NSData *imgData = (((MapCluster *)image).image).imageThumbnailData; if (imgData) view.imgView.image = [UIImage imageWithData:imgData]; else { NSString * url = (((MapCluster *)image).image).imageThumbnailURL; if (url) { [[ImageCache sharedInstance] downloadDataAtURL:[NSURL URLWithString:url] completionHandler:^(NSData *data) { (((MapCluster *)image).image).imageThumbnailData = data; [marker setSnippet:@""]; [mapView_ setSelectedMarker:marker]; }]; } } return view; }

здесь TMMapImagePreview – это класс-наследник UIView, в нем формируется лэйаут InfoWindow. Вся магия принудительной перерисовки заключена в compeltion-блоке метода downloadDataAtURL синглтона ImageCache, который как не трудно догадаться, занимается скачиванием и кэшированием графического контента.

Будет здорово вы скачаете приложение, проверите его и дадите взвешенную критику и комментарии. Тем более, мы выпустили обновление с момента написания первой части поста. Нужен фидбек. Спасибо!

ссылка на оригинал статьи http://habrahabr.ru/company/timera_inc/blog/215665/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *