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

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

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

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

Итак, приступим. Открываем 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<UIImagePickerControllerDelegate,UINavigationControllerDelegate>

Пропишем в действии кнопки следующие строки:

 - (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 <UIKit/UIKit.h>

@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), открыть Галерею – и найти там то самое изображение.
Productivity Inside
Для старательного нет ничего невозможного

Comments 11

    0
    А чего не Swift? :)
      –3
      Шшш, не спугните людей! :3 Не очень люблю iOS и фильтры, но за использование Objective-C жирный, как чёрная дыра в центре Млечного Пути, плюс. Лично я использую его и никуда не собираюсь переходить по целому ряду причин:

      • Более 30 лет развития, много специалистов с большим опытом, у которых можно спросить совет, сотни тысяч писем в списке рассылки с разбором многих проблем.
      • Полная совместимость с C и почти полная с C++ без всяких биндингов.
      • Прямой доступ к Objc Run Time Library и прочие низкоуровневые фичи.
      • Огромное количество уже написанного кода, в том числе свободного.
      • Просто не люблю Swift.
      +3
      Жизненный цикл UIViewController начинается с метода loadView.

      -loadView используется для создания view «вручную», в вашем случае лучше бы подошел -viewDidLoad, но это меньшее, к чему можно придраться =)
        0
        viewDidUnload? Серьёзно? :)
        А ещё у вас часть пробелов пропало между классами и методами.
          0
          а у меня наоборот все )
          Свифт — люблю, к ОбжС отношусь с громадным уважением и считаю, что многие вещи в нем реализованы удивительно хорошо! Но после Свифта на ОбжС писать расхотелось совсем.
          Все сказанное — ИМХО
            0
            Я с огромным уважением отношусь к гению Chris Lattner, и иногда меня посещает мысль, что swift был его неудачной шуткой, которую все восприняли всерьёз. ;-)
            0

            Свитч на 12 кейсов, серьёзно? Не проще в массив запихнуть все объекты? Они и не создавались бы каждый раз, и код аккуратней был бы, а то просто стыд какой то. По поводу свифта, и неудачной штуки, имхо только ленивые не переходят на него, ибо после его использования, не просто использования как ранее objc, а именно на полную используя дженерики, екстеншны протоколов, и прочие плюшки, уж точно не захочется вернуться обратно на objc. Сам 3 года писал на objc, последние два на Свифте.

              0
              А C++ либу из него можно напрямую вызвать?
                0
                Я либу на с++ подключал в жизни только один раз когда использовал Box2D;
                Для таких случаев есть бридж на ObjC, и потом через .mm.
                Но писать на ObjC ради того что можно подключать с++ либу это выглядит крайне странно, потому что это крайне
                редкий кейс впринципе.
                Не дай Бог выпустят в 4 версии соместимовсть с с++, тогда что аргументы кончатся?
                  0
                  Вы, видимо, пишете скринсейверы для iOS, если бы вы писали в Mac OS X визуальный редактор для численных рассчётов с использованием кроссплатформенных C / C++ либ с сотнями функций, вы были бы иного мнения. Бридж — не нативная поддержка.
                  Но писать на ObjC ради того что можно подключать с++ либу это выглядит крайне странно, потому что это крайне редкий.
                  Что, что простите? Вы, серьёзно? Вообще говоря, многие крупные (и не очень) проекты пишутся по следующей схеме: низкоуровневые функции — C, модель — С++, контроллеры и интерфейс — Objective-C. И городить переходники для сотен вызовов — вот это странно. Собственно, целевая аудитория у Swift — прогеры игр (причём не блещущих физикой и графикой, либо пищущих движки с нуля) и небольших iOS приложений. Apple уже не знает, чем ещё привлечь людей, скоро вообще на радость школьникам на HTML перейдут.
                    0
                    Под мак возможно имеет смысл, под iOS врядли. Web аппы большие, и игры на основе еппловских движков нормально зайдут на свифте. Я последнее время собственно и занимался веб аппами. Там тот с++ вообще не упал. Опять таки, для каждой задачи своя технология подходит, но говорить что swift это плохо потому что с++ нельзя подключить, это странно. Нужен с++ вперед. Давайте еще с++ для веб приложений использовать, почему бы и нет, быстрый же. Зачем нужен тот питон или nodeJS, на котором это можно в 10 раз быстрее собрать. Какая задача такие и технологии. Если вам нужен с++ то юзайте ObjC на здоровье. Если как в моем случае с головой хватает swift, ну так в чем проблема. Нужен будет с++, то же буду ObjC юзать. Все ведь от задачи зависит. Странно обсирать технологию если ее не использовал на полную.

            Only users with full accounts can post comments. Log in, please.