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

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


  1. creker
    16.01.2016 22:25

    Метод encryptPhoto конечно жесть. Выделение буфера на каждой итерации, копиравание при создании NSData. При чем копирование с использованием dataWithBytes, который засунет объект в autorelease pool. Не говоря о том, что все решение выглядит мало читаемо как-то из-за колбеков и семафоров. Хотя, казалось бы, примитивнейшая задача — прочитал из одного потока, расшифровал, записал в другой. Готовые решения это конечно хорошо, но не тогда, когда они так уродуют простейший код.

    Буфер надо было объявить за циклом в виде NSMutableData. Это позволит и читать в него и передавать его декриптору, раз тут все равно все синхронно происходит. Уйдут лишние копирования и не нужен будет autoreleasepool в цикле и скорее всего в декрипторе тоже.


    1. Lonkly
      16.01.2016 22:32

      Иными словами, написать вот так:

      __block NSMutableData *data = [NSMutableData dataWithLength:blockSize];
      __block RNDecryptor *decryptor = nil;
      
      
      dispatch_block_t readStreamBlock = ^{
      
          [data setLength:blockSize];
      
          NSInteger bytesRead = [inputStream read:[data mutableBytes] maxLength:blockSize];
          if (bytesRead < 0) {
              // Throw an error
          }
          else if (bytesRead == 0) {
              [decryptor finish];
          }
          else {
      
              [data setLength:bytesRead];
              [decryptor addData:data];
          }
      };
      
      decryptor = [[RNDecryptor alloc] initWithPassword:@"blah" handler:^(RNCryptor *cryptor, NSData *data) {
      
              [decryptedStream write:data.bytes maxLength:data.length];
              _percentStatus = (CGFloat)[[decryptedStream propertyForKey:NSStreamFileCurrentOffsetKey] intValue] / (CGFloat)_inputFileSize;
              if (cryptor.isFinished)
              {
                  [decryptedStream close];
                  [self decryptFinish];
              }
              else
              {
                  dispatch_async(cryptor.responseQueue, ^{
                      readStreamBlock();
                  });
                  [self decryptStatusChange];
              }
      
      }];
      
      
      // Read the first block to kick things off
      
      decryptor.responseQueue = self.cryptorQueue;
      [self decryptStart];
      dispatch_async(decryptor.cryptorQueue, ^{
          readStreamBlock();
      });
      


      Этот шмат я в апдейте уже исправил, хотел увидеть Ваши комментарии к коду, возможно размышления…

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


      1. creker
        16.01.2016 22:44

        Ох. Все равно жутко. Все время что-то диспатчится в очередь — пустая трата времени. Все таки советую выкинуть этот RNDecryptor и найти что-то проще. Слишком он уродует код, да и внутри у него вижу, что копирования лишние с использованием пула тоже происходят. Это простейшая задача, которая решается одним циклом в фоновом потоке без всяких колбеков и проблем с памятью. Хоть там терабайты данных будут.


        1. Lonkly
          16.01.2016 22:52

          Вот видите, решил использовать RNCryptor потому, что прочитал много хороших отзывов о нем, меня не особо заботил вопрос красоты и вообще проблем с памятью на начальном этапе, а когда столкнулся с проблемами, уже было не с руки его выкидывать, стремился к релизу и был озабочен UX приложения. Спасибо Вам за вброс, я последую вашему совету и подумаю с недели над качественным решением.
          Как я говорил, проблем при создании своего проекта такая гора, что для начала нужно прийти хоть к какому-то решению, а позже браться за заделывание брешей :)


      1. creker
        16.01.2016 22:55

        Я сам делал практически тоже самое — съемка фото и видео с шифрованием. При чем видео шифровалось прямо во время записи. Все отлично бегало еще на 4s, потому что я сразу решил делать все как можно конкретнее — небольшая обертка над fopen и CCCryptor. Больше ничего не нужно для этой задачи. Очевидно, что память и лишние ее выделения тут основной враг, поэтому нужно быть осторожным с абстракциями. Особенно в objc с его пулами, которые могут подвести в неожиданный момент.


  1. egormerkushev
    16.01.2016 23:14
    +1

    А есть возможность зашифровать фотографию и сохранить её при этом в обычную Галерею в виде реальной но другой фото? Как бы стеганография?


    1. Lonkly
      16.01.2016 23:19

      Хехе, удивили :) Даже в голову не приходило. Често говоря, тут надо еще поразмыслить, как на это отреагируют пользователи, и особенно Apple :D


      1. egormerkushev
        16.01.2016 23:41

        Учитывая, как цензоры Apple «досконально» проверяют приложения — нормально отреагирует.
        Еще вот хотел спросить, раз шифруете — регистрировали приложение в США (что-то там про экспорт)?


        1. Lonkly
          16.01.2016 23:50
          +1

          Стеганография это есть гуд, но у меня еще выше крыши насущных вопросов, вроде secure iCloud storage, и, например, секьюрного шаринга.

          Вот на счет этого есть много обсуждений. Простой ответ — нет, ничего регистрировать не нужно, например, если используете стандартный AES-256, т.к Apple сам его использует, и в iPhone Settings это, кстати, указано.
          Если же вы используете какое-то свое шифрование, то нужно убедиться, что оно совпадает с правилами. Тысячи их, прочитав я сперва испугался, что меня могут завернуть на ревью, но все обошлось.


          1. vsb
            17.01.2016 11:36

            По вашей же ссылке написано:

            «How do I know if I can follow the Exporter Registration and Reporting (ERN) process?

            If your app uses, accesses, implements or incorporates industry standard encryption algorithms for purposes other than those listed as exemptions under question 2, you need to submit for an ERN authorization. Examples of standard encryption are: AES, SSL, https. This authorization requires that you submit an annual report to two U.S. Government agencies with information about your app every January. „

            Ваше приложение не подходит под исключения, поэтому в теории вам надо получать это разрешение. Впрочем правило довольно странное на практике (если ваше приложение делает хоть один HTTPS-запрос, вам надо получать разрешение) и вряд ли оно соблюдается хотя бы большинством разработчиков.