Гайд по созданию простого фоторедактора

Сегодня мы предлагаем читателям подробное руководство по созданию простого фоторедактора на iOS. Для опытных разработчиков задача несложная, но новичкам подобный пошаговый разбор всего процесса, возможно, окажется полезен. Мы отдали предпочтение классической среде разработки для выбранной операционной системы — Xcode version 8.2.1. Разработку будем вести, опять же, на классическом объектно-ориентированном языке программирования Objective-C.

Прежде чем приступать к собственно разработке, предлагаем сначала разбить задачу на подзадачи.

  1. Загрузить фотографию из галереи
  2. Создать коллекцию с фильтрами
  3. Реализовать возможность применять любой из фильтров на выбранную нами фотографию
  4. Сохранить результат в галерею.


Итак, приступим. Открываем Xcode и выбираем шаблон для разработки под iOS во вкладке Application → Single View Application. Назовем наш проект My First Photo Editor. Далее укажем директорию, куда хотим сохранить проект.

446e94447d334efb8ed66fe82d0d4d3e.PNG

ec16259ce06842b98b868a897809a8b6.PNG

Выберем механизм разработки интерфейса программы main.storyboard.

b3fa8e934edb4a0a96369af1c61f32c7.PNG

Нам придется переходить и на другие сцены, которые будут добавляться позже. Для этого перетащим Navigation Controller на storyboard.

41a05e4db7bf45eea4152a1a93f5fe9f.PNG

Удалим Root View Controller Scene — он нам не понадобится. Теперь осталось связать Navigation Controller c исходным View Controller.

e20f4d71b49e47f3b7df5ab9ed80a40b.PNG

cf1fe1bbeca344e5ba56df1d2182c9ed.PNG

Выбираем Navigation Controller, ставим галочку напротив «Is Initial View Controller» (Navigation Controller > Attributes Inspector > View Controller > Is Initial View Controller; на картинках можно увидеть, как это сделать) и связываем Navigation Controller с View Controller.

На View Controller нам понадобится кнопка для открытия окна галереи. Перенесем ее из Object Library.

bd1dcfc5d686473bb2d6a848c9ddec2b.PNG

Создадим событие нажатия на кнопку и добавим его в свойства контроллера.

c354b6e4e00a4cef8a334b3603111a96.PNG

d39bd2dc7315400e91571fe8d6f28f52.PNG

Так как мы будем использовать делегатные методы, добавим в интерфейс контроллера 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. Исправим эту ошибку.

1169c9925a204abca5300c333d4fecef.png

Теперь нужно добавить на storyboard еще один View Controller. Сделаем связь по схеме, представленной на скриншотах.

aac2a6a4e6754f2ea9b9e0296b18b287.PNG

523a549cc943479683bcf7acafa4b53b.PNG

Пропишем Indetifier ключ «toFilters». На иллюстрации показано, как это можно сделать. Выберем связь на Storyboard > Attributes Inspector >Storyboard > identifier > toFilters.

accefffd584f4be688b132c04ebd6b61.PNG

Теперь добавим во 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;
    }
}

12d77267af1a44149838c0248c3902b5.PNG

ab049af00c0f412991aba1b84030192f.PNG

Для нового контроллера нам нужно создать особый класс. Для этого в верхней панели выбираем File→New→File→Cocoa Touch Class→Next. Назовем его FiltersViewController и обязательно выберем в графе Subclass UIViewController. В Indetity inspector прописываем наш класс.

c870c71efe13408eae26f9ef5ecbc28a.PNG

Добавляем на контроллер Scroll View (или какую-нибудь другую View, на ваше усмотрение), в нем будет отображаться изображение с применненым фильтром. Создаем связь с FiltersViewController.

65d738a87bb54deea146f15fde56c870.PNG

401c53929cd74d9bbb972938137b8961.png

Добавим Scroll View в свойства контролера, как делали ранее. Добавим свойства UIImageView и UIimage.

1d066a4e0b2d4f439cb9b72ca603221e.PNG

При этом свойство UIImage нужно добавить в FiltersViewController.h, так как мы планируем использовать его в другом классе.

Теперь создадим коллекцию с фильтрами. Находим в Object Library объект Collection View.

5fb72f67cd29455e84ed12318b23a005.PNG

c04bff28334b4b2f976fd8e144ef8e2d.PNG

93c37ddb9d344dad8f51e0c5402ab4fd.PNG

Создаем новый класс для коллекции. Заходим в xib файл, добавляем UIimageView и Label.
Длаее зададим размеры UIImageView. Самый простой способ это сделать — зажать правой клавишей объект и перетащить его на тот, к которому хотим привязать. Выбираем связи, как показано на скриншоте. Затем переходим в Size inspector этого объекта и изменяем настройки. Выставляем все constraints в 0.

45e648664fdf420a8365e64b5338c629.PNG

Устанавливаем связь между элементами.

552677588d304c7cb3c6b6534a8274af.PNG

Импортируем новый класс в FiltersViewController

927da8227420474c8b0f631eb26d16a5.PNG

и добавляем FiltersViewControllerCell в этот класс вот таким образом:

241db87f0cfe4048b2f6e3f8a26d0b82.PNG

Теперь добавим в свойства FiltersViewController массив NSMutableArray и оператор NSOperationQueue — они нам пригодятся чуть позже.

6a92ae6a64634803be9afda9b4b97ea4.PNG

Жизненный цикл 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]; 

e4b12c83ab5140c488152b898c4ea5ef.PNG

Запустим наш проект. После выбора изображения приложение зависает на некоторое время. Это происходит потому, что на применение фильтров к изображению требуется время. После применения всех фильтров загрузится контроллер с изображением и коллекция с фильтрами.

Чтобы обеспечить отзывчивость интерфейса, добавим асинхронность при выполнении метода.

-(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

deb51410ac0e4313864dde0dbb01261c.PNG

9f0fe86e585e4154b99c1b7ab21a78cb.PNG

добавим кнопку на Navigation Item

7fd7a2ce541841b78d694e991779f634.PNG

и пропишем в действии кнопки:

- (IBAction)saveBtn:(id)sender {
    UIImageWriteToSavedPhotosAlbum(_imageView.image, nil, nil, nil);
}

Запустим приложение, выберем изображение и фильтр. Когда фильтр будет применен, нажмем Save. На симуляторе мы можем использовать комбинацию клавиш cmd+shift+h (это тоже самое что и кнопка «Домой» на iPhone), открыть Галерею — и найти там то самое изображение.

Комментарии (0)

© Habrahabr.ru