Как я делал камеру с шифрованием фотографий под iOS

в 19:06, , рубрики: iOS, security, разработка мобильных приложений, разработка под iOS

Одной из главных «мечт» любого программиста, является создание своего личного проекта и его развитие. Аутсорс, фриланс проекты и т.д. — это стандартный набор для поддержки штанов, и от него сложно отказаться ради собственного проекта и предполагаемого получения денег в будущем(опять этот орнитологический выбор — что лучше синица в небе или воробей в руках). И да, все мы помним, что в Appstore уже есть все.

Но я все же решился — толчком послужил общеизвестный случай утечки личных фотографий знаменитостей из iCloud. Тогда в голову мне пришла мысль, что подобные фото есть на телефоне почти у каждого и защита подобных фотографий от чужих глаз — желанная «фича». А это, пожалуй, самое главное, ведь ваш продукт должен решать проблемы, возникающие у людей. Примером обратного, является многообразие различных приложений-копипаст Instargam'а и тысячи приложений-камер с миллионом плюшек вроде ручного управления настройками камеры, «лучших фильтров для ваших селфи» итп. У них тоже есть своя ниша, но чтобы ее занять, нужно немало попотеть, чтобы вылизать этот функционал до блеска. Ведь известно, что самая идеальная камера — нативная, и без киллер-фич конкурировать с ней невозможно. Но вернемся к процессу.
Swift в те времена еще не был достаточно стабилен и изучен мною, чтобы работать вне Playground'a, поэтому писать начал по-старинке на objC, используя ReactiveCocoa. Сейчас я уже создавал бы приложение именно на Swift, т.к несмотря на некоторые неудобства, писать на нем гораздо приятнее.
Вся разработка в ленивом темпе заняла год с лишним. Мотивация — штука сложная.

Итак, Архитектура Грабли:

Я придерживаюсь мнения, что парадигма от Apple, о соблюдении простоты в написании архитектуры приложения — актуальна, и более того, была подтверждена мной на собственном опыте. Чем проще будет архитектура, тем проще вам будет разбираться с тем, что вы «понапишете», и тем меньше глупых ошибок совершите. Бадамц!

Очень легко забыть о том, что пишешь именно MVP версию, прототип. Нужно выкинуть из головы мысли о том, как бы покрасивее да поизысканнее написать код. Все же ваша задача в первую очередь — результат, который можно взять за основу и довести до более-менее идеального состояния.

Не стоит забывать и про дизайн, о котором вы в начале пути имеете весьма схематичное представление, а на момент окончания работы дизайнера…вы понимаете, что это еще одна профессия которую вам надо освоить к следующему проекту. Ну, а если серьезно — вы, конечно, можете подумать, что вам хватит опыта разместить все элементы на экране, ведь правила UX вам хорошо знакомы, как продвинутому пользователю смартфона, после чего останется только вставить на место Asset’ы. Однако, это не так. Всегда стоит прислушиваться к мнению специалистов, которые, возможно, дадут вам пищу для размышлений и укажут на огрехи. В итоге, приложение к моменту релиза изменится до неузнаваемости и именно поэтому сложная архитектура будет вам скорее мешать, т.к. половину изначальных решений нужно будет переделать или просто выкинуть.
Для примера покажу как приложение выглядело в MVP, и как сейчас.

Скриншоты до и после

image image
image image

Reactive Cocoa

Теперь о наболевшем. Мне кажется, что у каждого программиста есть свой порог понимания программирования в целом, а так же восприятия новой информации. Могу сказать, что этот, с позволения сказать, фреймворк, я изучаю уже больше 2х лет и до сих пор возникают вопросы и неприятности. Из-за которых я в итоге перешел на RXSwift, и то не полностью. Удобство, сопряженное с некоторыми потерями, как всегда. Приведу пример:

Пользователь может нажимать на кнопку “сделать фото” довольно быстро, несколько раз в секунду. И каждая фотография с моей стороны должна быть тут же обработана определенным фильтром, конвертирована в jpeg, сверху еще ватермарку налепить, а потом еще и зашифровать. Казалось бы, все просто. Однако существуют и подводные камни. GPUImage обладает очень хорошим быстродействием, но все равно делать фото с фильтром так же быстро, как пользователь тапает на кнопку она не может. А дальше идут синхронные операции, требующие много памяти, т.к.развернуть фотографию на контексте и налепить сверху ватермарку, и шифрование. При этом, если ограничить скорость нажатия на кнопку, допустим, с таймером, это уже фейл UX. Такие проблемы (зависимых операций) решать с помощью сигналов очень удобно. Как-то так, например:

    RACSignal *photoSignal = [[[_cameraView.photoSignal map:^id(NSData *imageData) {
        return [[ARTImageManager.shared watermarkSignal:imageData] map:^id(UIImage *finalImage) {
            if ([GVUserDefaults standardUserDefaults].secureEnabled)
            {
                return [[ARTEncryptionManager shared] encryptImageData:UIImageJPEGRepresentation(finalImage, 1)];
            } else {
                return [[ARTImageManager shared] saveImage:finalImage withGPSMetadata:nil];
            }
            
        }];
    }] flatten:1] switchToLatest];

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

Сигнал можно сделать почти из чего угодно. Взять, хотя-бы жесты:

    [[[self.singleTap rac_gestureSignal] filter:^BOOL(id value) {
        return !self.collectionView.isDragging && !self.collectionView.isDecelerating;
    }] subscribeNext:^(UITapGestureRecognizer *recognizer) {
        @strongify(self)
       // ваш код
    }];

Главным затыком реактивного подхода является ни что иное, как производительность. Это очень легко заметить, если открыть приложение с реактивным UI на phone4s. Впрочем, как я уже говорил, удобство требует жертв.
Для тех, кто использует Swift могу порекомендовать RXSwift, впрочем на моем свифтовом проекте приходится использовать и RAC и RXSwift, т.к в последнем нету поддержки DynamicProperty, который позволяет обзервить ваши проперти и подписываться на их изменения.

Шифрование

Для шифрования я использовал RNCryptor. Я вообще использовал довольно много готовых решений, т.к они невыразимо облегчают работу на ранней стадии разработки. В конечном же итоге пришлось от многих решений отказаться. Ничего необычного, но возможно кому-то будет интересно посмотреть Podfile:

Podfile

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!

pod 'ReactiveCocoa', '~> 4.0.4-alpha-1'
pod 'JRSwizzle'
pod 'GVUserDefaults'
pod 'MBProgressHUD'
pod 'TAlertView'
pod 'UIView+JMFrame'
pod 'GBVersionTracking'
pod 'TLYShyNavBar'
pod 'pop'
pod 'TPKeyboardAvoiding'
pod 'Masonry'
pod 'SnapKit'
pod 'M13ProgressSuite'
pod 'Fabric'
pod 'Crashlytics'

pod 'GPUImage'
pod 'UIImage-Helpers'
pod 'RNCryptor', '~> 3'
pod 'ISO8601DateFormatter'
pod 'DZNEmptyDataSet'
pod 'UICKeyChainStore', '~> 2.0.6'
pod 'MPCoachMarks', '~> 0.0.10'

Долго мучился, чтобы приложение могло шифровать файлы любого размера и количества. Процесс, вроде и быстрый, и довольно очевидным решением было бы просто запихнуть все шифрование в Serial Queue, но ничего не помогало. При шифровке панорам приложение упорно ложилось и упрямилось. В итоге я пришел вот к такому решению:

- (void)encryptPhoto {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block int total = 0;
    int blockSize = 32 * 1024;

    NSArray *docPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *input = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"fileName.rncryptor"];
    NSString *output = [[docPaths objectAtIndex:0] stringByAppendingPathComponent:@"fileName.decrypted.pdf"];

    NSInputStream *cryptedStream = [NSInputStream inputStreamWithFileAtPath:input];
    __block NSOutputStream *decryptedStream = [NSOutputStream outputStreamToFileAtPath:output append:NO];
    __block NSError *decryptionError = nil;

    [cryptedStream open];
    [decryptedStream open];

    RNDecryptor *decryptor = [[RNDecryptor alloc] initWithPassword:@"PASSWORD" handler:^(RNCryptor *cryptor, NSData *data) {
        @autoreleasepool {
            NSLog(@"Decryptor recevied %d bytes", data.length);
            [decryptedStream write:data.bytes maxLength:data.length];
            dispatch_semaphore_signal(semaphore);

            data = nil;
            if (cryptor.isFinished) {
                [decryptedStream close];
                decryptionError = cryptor.error;
                // call my delegate that I'm finished with decrypting
            }
        }
    }];

    while (cryptedStream.hasBytesAvailable) {
        @autoreleasepool {
            uint8_t buf[blockSize];
            NSUInteger bytesRead = [cryptedStream read:buf maxLength:blockSize];
            if (bytesRead > 0) {
                NSData *data = [NSData dataWithBytes:buf length:bytesRead];

                total = total + bytesRead;
                [decryptor addData:data];
                NSLog(@"New bytes to decryptor: %d Total: %d", bytesRead, total);

                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            }
        }
    }

    [cryptedStream close];
    [decryptor finish];

    dispatch_release(semaphore);

}

Таким образом, мы можем зашифровать многомегабайтное фото даже на iPod.

Выводы

Могу с уверенностью сказать, что разработка мобильного приложения — это не действие, а состояние, поэтому его, как и ремонт, нельзя закончить, а можно только прекратить. В связи с этим, в какой-то момент пришлось остановить бесконечный процесс поиска «багов» и сделать долгожданный релиз. Что касается маркетинга, то из за времени релиза (череда зимних праздников) я ограничился рекламой на Facebook и Вконтакте. Знаю, нарушил все правила тотального релиза, и в области маркетинга не смогу служить положительным примером, но приложение волшебным образом попало в топ 10 категории и топ 100 платных по стране. Чудеса на Рождество, почему бы и нет?
Радость, испытываемую в связи с успехом вашего продукта нельзя сравнить ни с чем. Поэтому я предлагаю тем, кто считает, что уже освоил мобильную разработку под iOS или Android попробовать себя, ведь написание своего приложения — это экзамен, который покажет, насколько вы удались как профессионал.
Как бы то ни было, предстоит еще много работы по отлову «багов» и доработке функциональности, да и по рекламе самого приложения еще масса работы, но теперь, видя результат, делать это будет куда приятнее и легче, чем раньше. Мотивация — штука сложная :)

P.S: to be continued, во второй части расскажу про iCloud, багофиксы и про способы и результаты маркетинга.

Автор: Lonkly

Источник

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


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