Приемы разработки под iOS, использованные мной в конкурсе Pictograph

от автора

      Недавно прошли три тура конкурса Вконтакте по созданию фотоприложения для платформы iOS. Ссылка на конкурс: http://vk.com/photo_contest. В процессе разработки приложения первого тура я нашел несколько интересных решений некоторых проблем. Этими решениями я и хотел поделиться с общественностью. Матерым разработчикам под iOS я врядли открою что-то новое, не думаю что статья подойдет также новичкам. Предполагаю, что статья будет интересна разработчикам под iOS со стажем 2-5 приложений.


Прокручиваемая по горизонтали лента с
последними фотографиями из памяти устройства

      Во-первых, очень грамотно, что с самого начала в ленте видно не ровно 4 или не ровно 5 снимков, а 4 и 1/3. Это дает пользователю моментально понять, что список фотографий прокручивается по горизонтали.

Возникает несколько вопросов:

  • Сколько фотографий загружать в эту ленту?
  • Как организовать динамическую подгрузку фотографий, чтобы они все не висели в оперативной памяти?

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

@implementation ALAssetsLibrary (Extension) - (void)latestAssetsAndCall:(void (^)(NSMutableArray *))callback {     __block NSMutableArray * assets = [NSMutableArray arrayWithCapacity:5000];     [self enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {         if (group == nil)         {             callback(assets);             return;         }                  ALAssetsGroupType groupType = [[group valueForProperty:ALAssetsGroupPropertyType] intValue];         int insertIndex = (groupType == ALAssetsGroupSavedPhotos) ? 0 : assets.count;         [group enumerateAssetsUsingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {             if (result != nil)                 [assets insertObject:result atIndex:insertIndex];         }];     } failureBlock:^(NSError *error) {         if (error)             NSLog(@"%@", error);     }]; } @end

      Что касается второго вопроса, я не нашел ничего лучше, чем использовать UITableView, ведь он просто создан для прокрутки длинных списков, динамической подгрузки контента и повторного использования похожих ячеек таблицы. Единственное, что — таблицу необходимо повернуть на 90° против часовой стрелки. Учитывая, что трансформация осуществляется относительно центра объекта, располагаем центр UITableView в предполагаемом месте центра ленты и выполняем:

self.tableView.transform = CGAffineTransformMakeRotation(-M_PI_2);

При создании ячеек таблицы, необходимо выполнить обратную трансформацию — вращение на 90° по часовой стрелке:

- (UItableViewCell *)tableView:(UITableView *)tableView           cellForRowAtIndexPath:(NSIndexPath *)indexPath {     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BottomRollCell"];     if (cell == nil)      {         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault                                       reuseIdentifier:@"BottomRollCell"];         cell.contentView.transform = CGAffineTransformMakeRotation(M_PI_2);         // тут создание элементов ячейки     }     // тут заполнение элементов ячейки конкретным контентом }

      Что мы имеем в итоге? Таблица по мере прокрутки запрашивает у нас содержимое своих ячеек. Имея массив ALAsset-ов, получаем thumbnail-ы изображений и заполняем ими ячейки таблицы. Таблица прокручивается очень плавно, лагов с подгрузкой фотографий не замечено. По поводу времени получения всех фотографий — получение 2500 фотографий занимает менее 1 секунды времени, но для запуска приложения это критично. Делаем анимацию выпадения таблицы справа налево по факту получения ALAsset-ов. Получается очень мило и задержка в полсекунды практически не заметна. Тем более что запрос не всех ассетов, а через задание множества индексов прироста скорости не дает, это меня даже несколько обескуражило. Таким образом оптимизация с быстренькой предзагрузкой первых фотографий — не покатила.

Анимация открытия и закрытия фотографий

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

- (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath;

      Пришлось потратить немного манны, чтобы определить точные координаты картинки с учетом её поворота и т.д. После получения координат миниатюрного изображения в главном виде, поверх миниатюры я располагал уменьшенное полноразмерное изображение и анимированно менял размер и позицию изображения. В идеале, я хотел бы чтобы изображение выпрыгивало из ленты с нелинейной траекторией, но на это времени так и не осталось…

      Для того чтобы отслеживать изменения в фотографиях устройства, необходимо подписаться на событие ALAssetsLibraryChangedNotification, по которому необходимо перезагружать массив ALAsset-ов. Чтобы лента не начала обновляться при сохранении фотографии самим приложением — необходимо использовать внутренний флаг для отмены перерисовки ленты при следующем обновлении и добавлять вручную новый ALAsset в начало массива ассетов.
      Сохранение у меня осуществляется в самую левую позицию, я сдвигаю таблицу вправо на ширину одного изображения, осуществляю анимацию улета изображения в ленту, двигаю таблицу обратно без анимации и вручную вызываю reloadData.

      Для того чтобы анимации открытия и закрытия изображений выполнялись плавно и максимально быстро, пришлось сделать одну интересную вещь. Если открыть фотографию, изменить её масштаб и нажать кнопку отмены — фотография улетит в ленту именно в том виде в котором мы её оставили после масштабирования. Фотография будет оставаться там в этом виде до тех пор, пока она не будет скрыта за границей экрана и не перезагружена вновь. Для достижения этого эффекта я использовал NSMutableDictionary с URL-ами ассетов в качестве ключей и NSValue, содержащий CGRect, в качестве значений. К сожалению, я забыл снять это свойство в видео-обзоре, но это была одна из самых интересных проблем для меня.

Плавное масштабирование и позиционирование фотографии с применением эффектов

      Очень хотелось сделать масштабирование и позиционирование фотографии с одновременным применением выбранного эффекта и в предпросмотрах тоже все двигать синхронно и накладывать эффект. Вобщем, если попытаться так сделать — тормозить эта радость будет безбожно. Было найдено интересное решение, применить выбранный эффект к основной фотографии и взять фото уменьшенное в пять раз (точнее 320.0/56.0) и применить остальные эффекты к нему, а в процессе масштабирования и позиционирования синхронизировать скроллы миниатюр с главным UIScrollView. Этот способ работает быстро, плавно и без косяков.

Код, выполняющий синхронизацию скроллов миниатюр с главным скроллом (это методы делегата UIScrollViewDelegate):

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {     for (UITableViewCell * cell in [self.filtersTable visibleCells])     {         UIScrollView * filterScrollView = (UIScrollView *)[cell.contentView viewWithTag:125];         filterScrollView.contentOffset = CGPointMake(scrollView.contentOffset.x*56/320,                                                      scrollView.contentOffset.y*56/320);     } }  - (void)scrollViewDidZoom:(UIScrollView *)scrollView {     for (UITableViewCell * cell in [self.filtersTable visibleCells])     {         UIScrollView * filterScrollView = (UIScrollView *)[cell.contentView viewWithTag:125];         filterScrollView.zoomScale = self.scrollView.zoomScale;         filterScrollView.contentOffset = CGPointMake(scrollView.contentOffset.x*56/320,                                                      scrollView.contentOffset.y*56/320);     } }

Сохранение результата

      Так как для нас самое главное скорость работы приложения и качество снимков в целом условиями конкурса не регламентированы, я решил сохранять снимки размером 640х640 (на ретине). И проще всего это сделать через рендеринг главного вида в контексте изображения со смещением вверх:

- (UIImage *)renderImageForSaving {     UIGraphicsBeginImageContextWithOptions(self.scrollView.bounds.size, YES, 0.0);     CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 0, -self.scrollView.frame.origin.y);     [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];     UIImage * image = UIGraphicsGetImageFromCurrentImageContext();     UIGraphicsEndImageContext();          return image; }

      Это быстро и без дополнительных проблем с надписями и т.д. Да, разрешение можно было бы сохранять и побольше — но это уже совсем другая проблема, требующая времени и терпения)

Видео с моей непослушной собакой, демонстрирующее работу приложения:

Думаю ссылку дать можно (ок?). Приложение бесплатное и без рекламы, соответственно.
Ссылка на приложение: https://itunes.apple.com/app/pictography/id570470169

P.S. Ну и напоследок, большое спасибо Вконтакте за организацию и проведение подобных конкурсов. Ведь они мотивируют/стимулируют программистов начинать разрабатывать под новые для них перспективные платформы (мне почему-то кажется что среди участников много новичков по отношению к платформе). Очень порадовали входные данные для конкурса — все изображения были как на подбор. Ни одного лишнего пикселя нигде не торчало…

ссылка на оригинал статьи http://habrahabr.ru/post/154209/


Комментарии

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

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