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

в 12:41, , рубрики: iOS, vkonakte, Программирование, разработка под iOS, метки: ,

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

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

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

      Во-первых, очень грамотно, что с самого начала в ленте видно не ровно 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. Этот способ работает быстро, плавно и без косяков.

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

Код, выполняющий синхронизацию скроллов миниатюр с главным скроллом (это методы делегата 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. Ну и напоследок, большое спасибо Вконтакте за организацию и проведение подобных конкурсов. Ведь они мотивируют/стимулируют программистов начинать разрабатывать под новые для них перспективные платформы (мне почему-то кажется что среди участников много новичков по отношению к платформе). Очень порадовали входные данные для конкурса — все изображения были как на подбор. Ни одного лишнего пикселя нигде не торчало…

Автор: k06a

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js