Я понимаю, что на Хабре таких историй уже очень много. Но каждая история уникальна и является маленькой каплей в чаше чьего-либо вдохновения. В этой статье я расскажу, как писал свое приложение для iOS, на какие грабли наступил и что бы я сделал по-другому в своих будущих проектах.

Идея


Основа основ — это идея. Сложно что-либо реализовать без четкой идеи, что должно быть в конце. И такая идея у меня была. Мне всегда были интересны количественные показатели моей жизни. Причем именно тех критериев, которые технике померить просто невозможно. Они субъективны и оцениваются нами по ощущениям. Родилась идея сделать приложение, которое позволило бы оценивать прошедшей день именно по таким субъективным идеям. К тому же, было бы неплохо, если бы оно еще и помогало отмечать прогресс на пути к достижению цели.

Описание идеи


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

Разработка


Не следует думать, что вы можете все. К сожалению, это не так. Я решил и поплатился за это потраченными временем и нервами. Мне казалось, что поскольку я представляю себе конечный продукт, то и рисовать мне его не надо, а сделаю я все по ходу программирования. Отличное заблуждение. В итоге получил ужасный не только UI, но и UX. Хотя работающий прототип создал.



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


Разработка. Перезагрузка


Первый вариант никуда не годился и, более того, у меня была полная уверенность в том, что Apple не должны пускать его в AppStore. Поэтому было решено перезапустить разработку. Первое, что я сделал — это переписал ТЗ. Оно уже не было просто описанием идеи, хотя и до идеала ему было еще далеко. В нем появились описание экранов приложения, переходов между ними.

Я нашел дизайнера для приложение. Про поиски дизайнеров, да и работников, написано достаточно много. Я искал через freelansim.ru, ну и, конечно, со всеми переговорил. Надо отметить, что выбор людей для работы — это всегда субъективное и неподдающееся логическому объяснению решение. Мне даже кажется, что это сродни лотереи.

Мне повезло, что я выбрал человека, который не только прекрасно справился с поставленной задачей, но и сильно помог на этапе тестирования.

Дизайн создавался и потихоньку приложение обретало внешний вид. Тем временем, я решил, что раз уж перезагружать разработку, то делать все максимально по уму. Поскольку из собственного опыта у меня был только один неудачный прототип, я принялся за изучение чужого опыта. Вот что я вынес из прочитанных статей и просмотренных докладов:
  • Используйте CocoaPods — отличный и простой инструмент, помогающий управлять сторонними библиотеками;
  • Структурируйте проект в xCode — я разбил свой проект довольно примитивно, но, в тоже время, довольно понятно.



  • Задумайтесь о статистике еще до начала разработки. Для статистики я использовал проект Yandex.Metrika и Parse. Правда, статистика Parse пошла приятным дополнением. В основном я подключал этот сервис для Push Notification, но теперь планирую использовать его как backend для данных пользователей. Я выписал отдельно все события, которые хочу отслеживать в приложении, и уже по ходу разработки просто добавлял их, не тратя время на решение;
  • Вынесите все, что встречается в проекте более, чем в одном классе, в отдельный файл, которым будет легко управлять и изменять. Сюда попадают различные перечисления, дефайны, константы и прочие данные.

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

Недочеты, откровенные просчеты в организации проекта и интересные ошибки


Это, как мне кажется, самая интересная и полезная часть. Много из того, что я опишу в ней, должно быть понятно и очевидно опытному разработчику, но когда я только начинал, мне не хватило именно этих знаний.

Локализация приложения


На WWDC14 Apple представила новый способ локализации приложений. Можно экспортировать весь проект в .xliff файл и переводить его. Это практически стандарт в мире переводчиков. По крайней мере, на презентации сказали именно так. Я обрадовался: хороший, легкий способ, и все в одном месте. В том месте программы, где нужен будет перевод, вместо обычной строки пишется макрос:
NSLocalizedString(@“string”, @“comment”).
В начале та строка, что требует перевода, а затем комментарий, в основном, для переводчика. На программу он никак не влияет. Но вот беда, от ошибок никто не застрахован, да и есть места, которые повторяются в программе. В общем, если вы где-то ошиблись, то для исправления придется просмотреть весь проект. Но и это еще не все. Дело в том, что строка для перевода в .xliff файле станет ключем, и если вы ее поменяете в программе, а переводчик не изменит файл перевода, то вы получите две строки с одинаковым переводом и разными ключами. Xcode не найдет перевод и место останется не локализованным.

Решением этой проблемы стало комбинирование старого и нового способа локализации. Все строки, необходимые для перевода, выносятся в файл Localizable.strings в виде ключ-значение. И обращаемся уже к ключу. Теперь мы получаем возможность исправлять исходные тексты, не опасаясь за перевод. А переводчик, опять же, получает знакомый и удобный формат для работы.

Странные ошибки


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

typedef enum: NSInteger{
    EDEvaluatePriceHundredPoint,
    EDEvaluatePriceTenPoint,
    EDEvaluatePriceThreePoint
}EDEvaluatePrice;

Сами значения хранятся в CoreData и представлены в виде NSNumber. При выборе я решил делать следующие сравнения:

if (c.priceCriterion == [NSNumber numberWithInteger:EDEvaluatePriceHundredPoint]) {
}

Мне казалось это логичным, тем более это работало у меня на телефоне. Хотя тут сравниваются два разных объекта.
А вот на iPhone 5S и новее это уже не работало и появлялись пустые ячейки. Странная история, которую я до сих пор не понимаю. Но решается просто:

if ([c.priceCriterion integerValue] == EDEvaluatePriceHundredPoint) {
}

Вторая ошибка проявлялась только на iPhone 6 Plus и заключалась она в сломанной верстке приложения. К тому же не на всех его экранах, а только на избранных. Проблема оказалась в представленных в xcode 6 constraint to margin. Причем по умолчанию эта функция включена. Опять же не могу сказать, почему именно такое происходило, мне кажется, что проблема в использовании Containers. Но ведь этот же механизм использован Apple, например, в TabBarController. Да и проблема, опять же, проявлялась не всегда.

P.S. Несмотря на все странности и кучу подводных камней, мне все же понравилось разрабатывать для iPhone. Мое приложение находится в сторе всего пару недель, поэтому говорить о каких бы то ни было результатах еще рано. Позже я опубликую отчет о продвижении приложения.

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


  1. Flanker_4
    29.04.2015 18:52

    Помогу немного

    В этом случае, Вы сравнивали одинаковость указателей.

     (c.priceCriterion == [NSNumber numberWithInteger:EDEvaluatePriceHundredPoint])
    


    Правильно сравнивать объекты по значению нужно так
    [c.priceCriterion isEqual:[NSNumber numberWithInteger:EDEvaluatePriceHundredPoint]]
    


    Еще можно использовать сахар @(EDEvaluatePriceHundredPoint)
    и сравнивать сразу методом для NSNumber (если уверены, что оба объекта этого класса)
    [c.priceCriterion isEqualToNumber:@(EDEvaluatePriceHundredPoint)]
    


    Что касается этого " Проблема оказалась в представленных в xcode 6 constraint to margin"
    Не скажу, нужно смотреть экраны. Может ваш девайс был на iOS7, а айфон 6 на iOS8?


    1. Konstantint Автор
      29.04.2015 23:07

      Точно. Сравнение указателей, вот я болда. Спасибо. Именно поэтому не работало на 64 битных системах.

      По поводу constraint to margin, вот тут мне непонятно зачем их ставить по умолчанию и везде. И проблема проявлялась только на экране iPhone 6 Plus на остальных устройствах только при наличие системной плашки в верху экрана. В остальных случаях и на разных ios все было нормально.


      1. miksayer
        30.04.2015 00:00
        +1

        Есть подозрение, что на 32-битных системах простые константы NSNumber(типа @1, @2 и т.д.) создаются один раз и переиспользуются потом, поэтому сравнение по указателю у вас прокатывало без вопросов. А на 64-битных системах используется оптимизация tagged pointers и константы уже по какой-то причине не переиспользуются. О tagged pointers можно почитать тут.


  1. jaguard
    29.04.2015 23:10

    Жуткий рунглиш на скриншотах, надеюсь, не пошел в продакшн?


    1. Konstantint Автор
      29.04.2015 23:24

      Тот вариант, что на скриншотах не пошел. Хотя из-за откровенно кривой реализации локализации, ошибки в английской версии просочились в продакшн. Буду менять весь подход к локализации, что бы в будущем постараться свести количество ошибок к 0.


  1. i_user
    30.04.2015 09:14

    Хотелось бы вставить свои 5 копеек — очень популярно сейчас говорить «прикрутите статистический модуль в начале разработки».
    1. Крэш-аналитика однозначно нужна на первом этапе. В этом плане от себя могу порекомендовать HockeyApp — у них есть отличный клиент, облегчающий работу с крэшами. И в хороших деплоймент-скриптах навроде fast lane есть интеграция с этим сервисом в отличии от Яндекс.Метрик
    2. Статистика же использования сама по себе абсолютно бесполезна до понимания двух вещей:
    -Статистика до 5-10к пользователей попросту нерепрезентативна
    -Покрывать приложение статистикой «авось пригодится» рано или поздно приводит к тому, что выдача статистики становится бесполезной и ненужной — крайне рекомендую перед тем как включать сбор статистики — написать на отдельной страничке — а что вы, собственно, хотите узнать. Набор гипотез, которые вы хотите проверить — и уже для этих гипотез формируете минимальный набор лог-ивентов, которые позволяют разделить все гипотезы.


    1. Konstantint Автор
      30.04.2015 09:38

      -Покрывать приложение статистикой «авось пригодится» рано или поздно приводит к тому, что выдача статистики становится бесполезной и ненужной — крайне рекомендую перед тем как включать сбор статистики — написать на отдельной страничке — а что вы, собственно, хотите узнать. Набор гипотез, которые вы хотите проверить — и уже для этих гипотез формируете минимальный набор лог-ивентов, которые позволяют разделить все гипотезы.


      Полностью согласен. Я не призываю прописывать событие на все экраны и смотреть, что будет. Избыток данных это тоже не очень хорошо.
      Но всегда полезно знать, какие части приложения пользуются, спросом. Как раз обдумыванием этих ключевых мест я и призываю заниматься с самого начала. А не хвататься за это в самом конце.

      — У яндекса в метре тоже имеются крэш репорты. Этим то она меня и подкупила. Но тут не чего утверждать и рекомендовать не могу, пока сам все не попробую.


  1. ZhukV
    06.05.2015 08:08

    Мне казалось это логичным, тем более это работало у меня на телефоне. Хотя тут сравниваются два разных объекта.
    А вот на iPhone 5S и новее это уже не работало и появлялись пустые ячейки. Странная история, которую я до сих пор не понимаю. Но решается просто:


    Дело в том, что Objective-C — объектный язык, и уже NSNumber, это ни какой не int, не float и другая фигня. В частности, эта скажим некоторая обертка, над цифровым типом данных, с которого Вы можете получить уже любое значение.

    В результате, Ваше сравнение получилось что-то вроде (сравнения числа и объекта):

    int == NSNumber (id)
    


    На самом деле, я считаю это очень верным подходом в объектном языке. Вы не паритесь, какая архитектура (32, 64), ну если Вы не пишете там «большущие» вычисления, а также получаете большое количество helper-ов, с помощью которых Вы сможете многое сделать, не напрягаясь.

    Аналогичным образом работают и NSString и другие обертки над типами.

    В частности, я много видел людей, которые используют базовые типы: int, float, unsigned int… Лично я бы советовал использовать типы объявленые в Objective-C — NSInteger, CGFloat, NSUInteger… При таком подходе Вам пофигу будет, что там потом придумает Apple (ну не прям пофигу, но совместимость наверное таки будет).

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


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

    К примеру:

    + (BOOL)isRetina;
    + (BOOL)isIos8
    + (BOOL)isIos7
    // .....
    


    Вот как один из примеров: github.com/ericjohnson/canabalt-ios/blob/master/flixel-ios/src/Flixel/FlxG.m

    Еще очень хороший подход, это реализация событийной системы. То есть, приложение при каком-то действии отправляет события, а уже подписчики могут делать все что захотят.

    А почему не использовали GoogleAnalytics для статистики? Как по мне, очень даже хороший вариант, да и еще в связке с AdMob-ом.