Гайд по созданию простого фоторедактора
Прежде чем приступать к собственно разработке, предлагаем сначала разбить задачу на подзадачи.
- Загрузить фотографию из галереи
- Создать коллекцию с фильтрами
- Реализовать возможность применять любой из фильтров на выбранную нами фотографию
- Сохранить результат в галерею.
Итак, приступим. Открываем Xcode и выбираем шаблон для разработки под iOS во вкладке Application → Single View Application. Назовем наш проект My First Photo Editor. Далее укажем директорию, куда хотим сохранить проект.
Выберем механизм разработки интерфейса программы main.storyboard.
Нам придется переходить и на другие сцены, которые будут добавляться позже. Для этого перетащим Navigation Controller на storyboard.
Удалим Root View Controller Scene — он нам не понадобится. Теперь осталось связать Navigation Controller c исходным View Controller.
Выбираем Navigation Controller, ставим галочку напротив «Is Initial View Controller» (Navigation Controller > Attributes Inspector > View Controller > Is Initial View Controller; на картинках можно увидеть, как это сделать) и связываем Navigation Controller с View Controller.
На View Controller нам понадобится кнопка для открытия окна галереи. Перенесем ее из Object Library.
Создадим событие нажатия на кнопку и добавим его в свойства контроллера.
Так как мы будем использовать делегатные методы, добавим в интерфейс контроллера ViewConroller.h протоколы делагатов UIImagePickerControllerDelegate, UINavigationControllerDelegate
Получается:
@interface ViewController : UIViewController
Пропишем в действии кнопки следующие строки:
- (IBAction)btnOpen_pressed:(id)sender {
UIImagePickerController *picker = [[UIImagePickerControlleralloc] init];
picker.delegate = self;
[pickersetSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
[selfpresentViewController:picker animated:YEScompletion:nil];// вызов окна галереи
}
Запускаем проект и видим на симуляторе нашу кнопку. Попробуем нажать и… получим краш. Почему? Мы забыли добавить свойство в info.plist NSPhotoLibraryUsageDescription. Исправим эту ошибку.
Теперь нужно добавить на storyboard еще один View Controller. Сделаем связь по схеме, представленной на скриншотах.
Пропишем Indetifier ключ «toFilters». На иллюстрации показано, как это можно сделать. Выберем связь на Storyboard > Attributes Inspector >Storyboard > identifier > toFilters.
Теперь добавим во ViewController.m методы выбора фото и перехода на новый контроллер.
// делагатный метод, который вызывается после выбора изображения
-(void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingImage:(UIImage *)image
editingInfo:(NSDictionary *)editingInfo
{
[pickerdismissViewControllerAnimated:YEScompletion:nil];
self.btnOpenPhoto.enabled = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.selectedImage = image;
dispatch_async(dispatch_get_main_queue(), ^{
[selfperformSegueWithIdentifier:kFiltersSegueIdsender:self];
self.btnOpenPhoto.enabled = YES;
});
});
}
// делагатный метод, который вызывается при отмене выбора изображения
-(void) imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[pickerdismissViewControllerAnimated:YEScompletion:nil];
}
#pragma mark - segue
// Этот метод будет вызван у контроллера, из которого был начат переход
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{//kFiltersSegueId – это константа типа String с идентификатором перехода «toFilters»
if ([segue.identifierisEqualToString:kFiltersSegueId])
{
FiltersViewController *destinationController = (FiltersViewController *)segue.destinationViewController;
destinationController.image = self.selectedImage;
}
}
Для нового контроллера нам нужно создать особый класс. Для этого в верхней панели выбираем File→New→File→Cocoa Touch Class→Next. Назовем его FiltersViewController и обязательно выберем в графе Subclass UIViewController. В Indetity inspector прописываем наш класс.
Добавляем на контроллер Scroll View (или какую-нибудь другую View, на ваше усмотрение), в нем будет отображаться изображение с применненым фильтром. Создаем связь с FiltersViewController.
Добавим Scroll View в свойства контролера, как делали ранее. Добавим свойства UIImageView и UIimage.
При этом свойство UIImage нужно добавить в FiltersViewController.h, так как мы планируем использовать его в другом классе.
Теперь создадим коллекцию с фильтрами. Находим в Object Library объект Collection View.
Создаем новый класс для коллекции. Заходим в xib файл, добавляем UIimageView и Label.
Длаее зададим размеры UIImageView. Самый простой способ это сделать — зажать правой клавишей объект и перетащить его на тот, к которому хотим привязать. Выбираем связи, как показано на скриншоте. Затем переходим в Size inspector этого объекта и изменяем настройки. Выставляем все constraints в 0.
Устанавливаем связь между элементами.
Импортируем новый класс в FiltersViewController
и добавляем FiltersViewControllerCell в этот класс вот таким образом:
Теперь добавим в свойства FiltersViewController массив NSMutableArray и оператор NSOperationQueue — они нам пригодятся чуть позже.
Жизненный цикл UIViewController начинается с метода loadView. А значит, в этом методе нам нужно передать картинку, которую мы выбрали на прошлой сцене, и добавить превью в коллекцию.
В результате loadView будет выглядеть так:
- (void)loadView{
[super loadView];
self.automaticallyAdjustsScrollViewInsets = NO;
[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotificationobject:[UIDevice currentDevice]];
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
_imageView = [[UIImageView alloc] init];
[_scrollView addSubview:_imageView];
_imageView.image = _image;
_imageView.frame = CGRectMake(0, 0, _image.size.width, _image.size.height);
_filteringQueue = [NSOperationQueue new];
_filteringQueue.maxConcurrentOperationCount = 1;
_filteringQueue.qualityOfService = NSQualityOfServiceUserInitiated;
[self.filtersCollection registerNib:[UINib nibWithNibName:@"FiltersCollectionViewCell" bundle:nil] forCellWithReuseIdentifier:@"filtersCollectionCell»];//Регистрируем xib файл для коллекции.
// Do any additional setup after loading the view.
}
Чтобы не перезагружать контроллер, создадим класс Extension и добавим в него методы, которые будут выполнять функцию наложения фильтра на изображение:
#import "Extension.h"
@implementation CIImage (Extension)
-(CIImage *)applyFilter:(long)i
{
CIFilter *filter;
switch (i) {
case 0:
return [self copy];
break;
case 1:
filter = [CIFilter filterWithName:@"CISepiaTone"];
break;
case 2:
filter = [CIFilter filterWithName:@"CIColorMonochrome"];
break;
case 3:
filter = [CIFilter filterWithName:@"CIPhotoEffectMono"];
break;
case 4:
filter = [CIFilter filterWithName:@"CIPhotoEffectInstant"];
break;
case 5:
filter = [CIFilter filterWithName:@"CIHueAdjust"];
[filter setDefaults];
[filter setValue: [NSNumber numberWithFloat: M_PI] forKey: kCIInputAngleKey];
break;
case 6:
filter = [CIFilter filterWithName:@"CIHueAdjust"];
[filter setDefaults];
[filter setValue: [NSNumber numberWithFloat: M_PI_2] forKey: kCIInputAngleKey];
break;
case 7:
filter = [CIFilter filterWithName:@"CIColorInvert"];
break;
case 8:
filter = [CIFilter filterWithName:@"CIFalseColor"];
break;
case 9:
filter = [CIFilter filterWithName:@"CIPhotoEffectTonal"];
break;
case 10:
filter= [CIFilter filterWithName:@"CIPhotoEffectTransfer"];
break;
case 11:
filter= [CIFilter filterWithName:@"CIPhotoEffectProcess"];
break;
case 12:
filter= [CIFilter filterWithName:@"CIPhotoEffectChrome"];
break;
case 13:
filter = [CIFilter filterWithName:@"CIGaussianBlur"];
[filter setDefaults];
[filter setValue: [NSNumber numberWithFloat:(self.extent.size.width + self.extent.size.height)/30.] forKey:@"inputRadius"];
default:
break;
}
[filter setValue:self forKey:kCIInputImageKey];
CIImage *result = [filter valueForKey:kCIOutputImageKey];
return result;
}
@end
@implementation UIImage (Extension)
+(NSArray *)filterNames
{
static NSArray *names;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
names = @[@"Original",@"Sepia", @"Old Photo", @"Mono", @"Instant", @"Shift", @"Hue", @"Invert", @"Falce", @"Tonal", @"Transfer", @"Process", @"Chrome"];
});
return names;
}
- (UIImage *)applyFilter:(long)i
{
CIImage *ciImage = [[CIImage alloc] initWithImage: self];
CIImage *result = [ciImage applyFilter:i];
CGRect extent1 = [result extent];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage1 = [context createCGImage:result fromRect:extent1];
UIImage *img = [UIImage imageWithCGImage:cgImage1];
CGImageRelease(cgImage1);
return img;
}
+ (instancetype)imageWithCIImageImproved:(CIImage *)img
{
CGRect extent = [img extent];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef cgImage = [context createCGImage:img fromRect:extent];
UIImage *image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return image;
}
@end
далее пропишем интерфейс вызовов в Extension.h:
#import
@interface CIImage (Extension)
-(CIImage *)applyFilter:(long)i;
@end
@interface UIImage (Extension)
@property (class, readonly) NSArray * filterNames;
+ (NSArray *)filterNames;
- (UIImage *)applyFilter:(long)i;
+ (instancetype)imageWithCIImageImproved:(CIImage *)img;
@end
Добавляем следующие методы для коллекции и для Scroll View в класс FiltersViewController.m:
#pragma mark - Collection
// Возвращает количество ячеек для секции, обязательный метод
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
returnself.filterPreviews.count;
}
// Возвращает размер ячейки
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat sz = collectionView.frame.size.height - 6;
returnCGSizeMake(sz, sz);
}
//Обязательный метод, в реализации которого мы возвращаем созданный и сконфигурированный объект UICollectionViewCell
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
long index = [indexPath item];
FiltersCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"filtersCollectionCell"forIndexPath:indexPath];
UIImageView *ivPreview = cell.imageFilter;
ivPreview.image = self.filterPreviews[index];
ivPreview.clipsToBounds = YES;
ivPreview.layer.cornerRadius = 3.;
UILabel *labelName = cell.nameFilter;
labelName.text = [UIImagefilterNames][index];
return cell;
}
// метод, который вызывается, если ячейка была успешно выбрана
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[self.filteringQueuecancelAllOperations];
__blockUIImage *filtered = nil;
NSBlockOperation *operation1 = [NSBlockOperationblockOperationWithBlock:^{
filtered = [self.imageapplyFilter:[indexPath item]];
}];
NSBlockOperation *operation2 = [NSBlockOperationblockOperationWithBlock:^{
[CATransactionbegin];
dispatch_sync(dispatch_get_main_queue(), ^{
if (filtered) {
_imageView.image = filtered;
}
});
}];
[operation2addDependency:operation1];
[self.filteringQueueaddOperation:operation1];
[self.filteringQueueaddOperation:operation2];
}
#pragma mark - Scroll View
// метод возвращает объект UIView, используемый при масштабировании содержимого
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
returnself.imageView;
}
// делегатный метод, вызываемый во время изменения масштаба содержимого
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
CGSize sz = scrollView.contentSize;
float xInsets = MAX(0, scrollView.frame.size.width/2.-sz.width/2.);
float yInsets = MAX(0, scrollView.frame.size.height/2.-sz.height/2.);
[scrollViewsetContentInset:UIEdgeInsetsMake(yInsets,xInsets,yInsets,xInsets)];
}
// метод, который вызывается при смене ориентации
- (void) orientationChanged:(NSNotification *)note
{
[self.viewlayoutSubviews];
[selfupdateScaleInScrollView:self.scrollView];
}
// метод, который корректно выводит содережимое ScrollView после изменения масштаба
-(void)updateScaleInScrollView:(UIScrollView *)scrollView
{
UIImage *image = _image;
float minScale = sizeFit(image.size,scrollView.frame.size).width/image.size.width;
scrollView.maximumZoomScale = MAX(1,minScale);
scrollView.minimumZoomScale = minScale;
scrollView.zoomScale = minScale;
if (scrollView.zoomScale> scrollView.maximumZoomScale)
scrollView.zoomScale = scrollView.maximumZoomScale;
elseif (scrollView.zoomScale< scrollView.minimumZoomScale)
scrollView.zoomScale = scrollView.minimumZoomScale;
[selfscrollViewDidZoom:scrollView];
}
#pragma mark - other
// далее приведен набор методов для масштабирования
CGSizesizeFill(CGSize size, CGSize sizeToFill)
{
CGSize newSize;
if (size.width / sizeToFill.width< size.height / sizeToFill.height)
newSize = CGSizeMake(sizeToFill.width, sizeToFill.width*size.height/size.width);
else
newSize = CGSizeMake(sizeToFill.height*size.width/size.height, sizeToFill.height);
return newSize;
}
CGSizesizeFit(CGSize size, CGSize sizeToFit)
{
float w = size.width/sizeToFit.width;
float h = size.height/sizeToFit.height;
return w > h ? CGSizeMake(sizeToFit.width, size.height/w) : CGSizeMake(size.width/h, sizeToFit.height);
}
CGRectframeFill(CGSize size, CGSize sizeToFill)
{
CGSize szFill = sizeFill(size, sizeToFill);
CGPoint pntFill = CGPointMake(sizeToFill.width/2.-szFill.width/2., sizeToFill.height/2.-szFill.height/2.);
returnCGRectMake(pntFill.x, pntFill.y, szFill.width, szFill.height);
}
Следующий метод жизненного цикла ViewController — это viewDidLoad. viewDidLoad является хорошим местом для продолжения инициализации контроллера. Нам он не понадобится по причине того, что инициализация всех элементов дизайна уже определена. Однако размеры view не заданы, поэтому добавим обработку ScrollView в следующим этапе жизненного цикла viewWillAppear:
-(void)viewWillAppear:(BOOL)animated
{
[superviewWillAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^{
[selfupdateScaleInScrollView:self.scrollView];
});
}
Создадим метод для вывода превью фильтров:
-(void)makeFilterPreviews
{
self.filterPreviews = [[NSMutableArrayalloc] init];
float size = 90 * [UIScreenmainScreen].scale;
CGRect cropRect = frameFill(self.image.size,CGSizeMake(size, size));
UIGraphicsBeginImageContext(CGSizeMake(size, size));
[self.imagedrawInRect:cropRect];
UIImage *cropped = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CIImage *ciimg = [[CIImagealloc] initWithImage:cropped];
for (int i = 0; i < [UIImagefilterNames].count; i++)
{
CIImage *ciFiltered = [ciimg applyFilter:i];
UIImage *filtered = [UIImageimageWithCIImageImproved:ciFiltered];
[self.filterPreviewsaddObject:filtered];
}
}
и пропишем вызов этого метода в loadView
[self makeFilterPreviews];
Запустим наш проект. После выбора изображения приложение зависает на некоторое время. Это происходит потому, что на применение фильтров к изображению требуется время. После применения всех фильтров загрузится контроллер с изображением и коллекция с фильтрами.
Чтобы обеспечить отзывчивость интерфейса, добавим асинхронность при выполнении метода.
-(void)makeFilterPreviews
{
self.filterPreviews = [[NSMutableArrayalloc] init];
//Заполним массив пустыми элементами для предварительного отображения в коллекцию ->
for (long i = 0; i < [UIImagefilterNames].count; i++)
{
[self.filterPreviews addObject:[[UIImage alloc]init]];
}
float size = 90 * [UIScreenmainScreen].scale;
CGRect cropRect = frameFill(self.image.size,CGSizeMake(size, size));
UIGraphicsBeginImageContext(CGSizeMake(size, size));
[self.imagedrawInRect:cropRect];
UIImage *cropped = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CIImage *ciimg = [[CIImagealloc] initWithImage:cropped];
//создадим очередь для добавления превью изображения с примененным фильтром
NSOperationQueue *queue = [[NSOperationQueuealloc]init];
queue.maxConcurrentOperationCount = 2;
for (int i = 0; i < [UIImagefilterNames].count; i++)
{
[queueaddOperationWithBlock:^{
CIImage *ciFiltered = [ciimg applyFilter:i];
UIImage *filtered = [UIImageimageWithCIImageImproved:ciFiltered];
@synchronized (self.filterPreviews) {
[self.filterPreviewsreplaceObjectAtIndex:i withObject:filtered];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.filtersCollectionreloadData];
});
}
}];
}
}
@synchronized (self.filterPreviews) блокирует редактирование массива filterPreviews для всех элементов, кроме текущего.
Чтобы корректно обновить коллекцию, нужно это делать в потоке dispatch_get_main_queue ().
Оставшиеся методы жизненного цикла ViewController:
- viewDidAppear:(BOOL)animated — вызывается после отрисовки всех элементов интерфейса
- viewWillDisappear:(BOOL)animated — вызывается перед удалением всех элементов интерфейса
- viewDidDisappear:(BOOL)animated — вызвается после удаления
- viewDidUnload — вызывается, когда view выгружен из памяти
нами не использовались.
Теперь создадим кнопку Save.
Добавим Navigation Item
добавим кнопку на Navigation Item
и пропишем в действии кнопки:
- (IBAction)saveBtn:(id)sender {
UIImageWriteToSavedPhotosAlbum(_imageView.image, nil, nil, nil);
}
Запустим приложение, выберем изображение и фильтр. Когда фильтр будет применен, нажмем Save. На симуляторе мы можем использовать комбинацию клавиш cmd+shift+h (это тоже самое что и кнопка «Домой» на iPhone), открыть Галерею — и найти там то самое изображение.