Pull to refresh
0

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

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

В этой части наш 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, который как не трудно догадаться, занимается скачиванием и кэшированием графического контента.

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

Articles

Information

Website
timera.com
Registered
Founded
Employees
2–10 employees