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