разработка

WWDC 2015, что посмотреть

WWDC — огромное мероприятие. Что из сессий этого года можно посмотреть всем? Что будет полезно интересующимся мобильной разработкой? Что важно для профессионалов? Какие доклады смотреть сначала, какие после? Попробую ответить на эти вопросы.

Я очень мало занимаюсь играми. Поэтому про эту тему не буду писать, не смотря на то, что на конференции про игры было очень, очень много: Metal, GameplayKit, ReplayKit, улучшения в SceneKit и SpriteKit. Смотрите сами, на сайте целый раздел про это.

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

Если у вас есть добавления или исправления — пишите в комментариях.

Если вы хотите сами решить, что интересно, что нет — видео доступно всем желающим, а Киноут выложен отдельно на сайте Эпл.

Что будет интересно всем

Самый важный доклад вы и так, наверняка, видели. Это открытие, оно же киноут. Там описывается, что будет в новых операционных системах, по-большей части, с точки зрения пользователя, и рассказывается про новый сервис Apple Music.

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

Ещё классно получилось в этом году награждение лучших приложений по версии Эпл. Все победители очень достойные, а кусок, где слепые специалисты Эпл демонстрируют, как они работают с приложениями — вообще космос.

Интересующимся мобильной разработкой

Если вы интересуетесь, что в принципе происходит в мире Эпл-разработки, то хорошо бы посмотреть обзорные доклады. Про нововведения, про вочОС... Просто открываете список видео, выбираете те, что начинаются с «What's new» и фильтруете по интересующим темам. Смотреть можно по-диагонали, в ускоренном виде, только слайды...

Не пропустите при этом следующие обзорные сессии:

  • Platforms State of the Union Тут рассказывается по-верхам, что же нового, но не как в киноуте, для публики, а более технически, для разработчиков.
  • Introducing WatchKit for watchOS 2 Про новую операционную систему, к которой прилагается SDK для разработки нативных приложений.

Практикующим iOS-разработчикам

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

  • Platforms State of the Union. Просто обзор всего того, что есть. Удобно смотреть, чтобы потом определиться с дальнейшим списком.
  • Introducing WatchKit for watchOS 2. Тоже обзорный доклад. Есть еще In-Depth сессии, где рассказано про то же, но подробнее. К ним удобно переходить, если заинтересовало что-либо в этом.
  • Introducing Search APIs. Про поиск, индексирование и глубокое связывание приложений. Казалось бы, ерунда. Но эта ерунда поменяет то, как мы создаем и используем приложения. Очень, очень важная сессия, посмотрите её внимательно.
  • Protocol-Oriented Programming in Swift. А эта штука ломает парадигму программирования. Или даже, пожалуй, не ломает, а дополняет её новым измерением, позволяя более ёмко и коротко выражать мысли в коде. Эта сессия важнее, чем обзорная про Свифт, и, может быть, вообще самая важная на конференции (наряду с Search APIs).
  • What's New in Swift. Свифт, судя по-всему, взлетел. Ему посвящено огромное количество докладов на конференции, на сессиях про оптимизацию рассказывают, как ускорить приложение, переписав некоторые части на Свифте. Вторая версия языка дополнилась обработкой ошибок и другими синтаксическими выражениями, которые сильно упрощают жизнь. Если вы создаете проект, который будет выпускаться осенью, можно даже попробовать создавать его сразу на Свифте 2.
  • iOS Accessibility. Не обходите, пожалуйста, эту тему. Она важная и полезная. Посмотрите Apple Design Awards, сразу станет понятно, зачем.

После просмотра этих докладов, можно идти дальше по темам, интересным вам. Из того, что можно применять прямо сейчас, я бы выделил: Advanced NSOperations, App Extension Best Practices, Cocoa Touch Best Practices, Mysteries of Auto Layout Part 1 и Part 2, Performance on iOS and watchOS, iOS Accessibility, Introducing the New System Fonts, Profiling in Depth и из раздела System Frameworks всё, с чем приходится работать.

На будущее

Часть докладов полезно смотреть «на будущее». Впрочем, если вы начинаете проект, который выйдет только осенью, то может удастся применить всё это сразу.

  • WatchKit In-Depth Part 1 и Part 2. Удивительно, но приложения для часов могут настолько много, что в одну сессию (на самом деле и в две) информация не уместилась.
  • Всё про Свифт 2. Повторюсь, Свифт взлетел, и начиная с осени им, судя по-всему, уже можно будет пользоваться. Пора готовиться. Кстати, обновлены и книжки: The Swift Programming Language (Swift 2 Prerelease) и Using Swift with Cocoa and Objective-C (Swift 2 Prerelease)
  • Все доклады, которые начинаются с «What's New». Их много и я сам буду смотреть их все, чтобы представлять, что будет в iOS 9, El Capitan и watch OS 2.

Интересного просмотра!

Приложение для Apple Watch за час

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

Мы с Ильёй не планировали делать ничего для Ангстрема. Вместо этого сосредоточились на стабилизации того, что есть, написании тестов, и продумывании некоторых моментов, которые оказались менее крутыми, чем мы думали изначально. Но Ангстрем — это «проект выходного дня», на основной работе потребовалось изучить на примерах, что же может Apple Watch. Я решил, что стоит попробовать сделать это на примере Ангстрема.

Задача была — разобраться с базовыми вещами:

  • настройка проекта,
  • взаимодействие приложений на часах и на Айфоне,
  • технические особенности,
  • ограничения,
  • возможно, особенности отправки приложения в стор.

Ангстрем подошел идеально из-за того, что в нём поддерживается диктовка. Собственно, это единственный способ ввода текста на Apple Watch, помимо закодированных заранее строк. Осталось только прикрутить её к часам.

Создание и настройка проекта

В теории всё просто. Добавляем таргет WatchKitApp в проект, их появляется два (WatchKit Extension, WatchKit Application), и должно работать. На деле оказалось, что Xcode не успевает за новыми железками и бета-версия 6.3, которую я тогда использовал, не умеет собирать приложения WatchKit, пришлось немного погуглить, поправить руками файл проекта.

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

  • у вас в проекте будет три info.plist'а. Для Айфон-приложения, для вотч-екстеншна и вотч-приложения. У них должны быть одинаковые версии (обе, как CFBundleVersion так и CFBundleShortVersionString).
  • для вотч-екстеншна и вотч-приложения нужно создать пустые entitlements, так как они сейчас не поддерживают примерно ничего (ни бета-тестирование, ни группы, например). Изначально визард что-то такое сделал, но я подумал, что оно лишнее и удалил. Зря, пришлось восстанавливать.
  • для вотч-екстеншна и вотч-приложения нужно сделать свои провижн-профайлы, как и для других екстеншнов. Очень неудобно получается, но что делать.
  • таргет-версия для приложения и для екстеншнов может различаться (как и для других екстеншнов), например, для приложения 7.0, для вотча должна быть 8.2.

Разобравшись с проектом и запустив приложение, получив черный экран, я пошел разбираться с интерфейсом.

Интерфейс приложений для Apple Watch

 
 

Разрабатывается интерфейс в том же Xcode при помощи привычных сторибоардов. Возможностей, правда, сильно меньше и компоненты совершенно другие, лейаут другой. Я делал только экран самого приложения, глэнсы и нотификации не трогал.

Вот, например, как выглядят настройки компонента «Image», работающего с картинками и покадровой анимацией.

 
 

Сверху все просто, а разделы Position и Size — это как раз новый лейаут. Position определяет, куда этот компонент будет «стремиться». Если все компоненты по-вертикали позиционируются сверху, то получится вот так (они накидываются в скролл-вью сверху вниз):

 
 

А если в поле «позиция» прописать «Bottom», то вот так:

 
 

Можно, например, комбинировать, что-то прибить к верху, что-то к низу.

 
 

Поле «Размер» — отвечает за размер в соответствующем направлении. Есть три типа:

  • чтобы содержимое поместилось (вполне очевидно)
  • относительно контейнера (число рядом задает долю размера контейнера, которую нужно занять)
  • фиксированный размер (в поинтах)

Например, чтобы расставить вьюшки наверх и вниз, чтобы верхняя занимала 0.2 высоты, а нижняя — 0.7, а между ними была дырка, нужно выставить значения как-то так:

Adjustments позволяет «дотюнить» позицию до нужной в случае необходимости.

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

Остальные моменты проще посмотреть в официальном документе.

Код

Почему вообще Apple Watch требует два (а с основным — три) таргета? Всё просто:

  • WatchKit App запускается на часах. Содержит интерфейс, ресурсы для его отображения. Но не умеет ничего кроме получения действий от пользователя и отображения простых лейаутов.
  • WatchKit Extension работает на Айфоне и отвечает за действия интерфейса на часах. Сюда приходят все события от часов, отсюда в интерфейс уходят команды на изменение интерфейса.
  • iPhone App может вызываться из WatchKit Extension и вынужден выполнять всю основную работу.

Для меня было три момента, с которыми нужно было разобраться: - как получить от пользователя ввод (в какой-нибудь форме) и - как сконвертировать ввод пользователя - как вернуть и отобразить результат обратно

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

Расчеты удалось (в отличие от тудей-екстеншна) провести в основном приложении, которое всё уже умеет. Делается это при помощи метода openParentApplication:reply: который как принимает NSDictionary значений, так и возвращает. Единственный важный момент заключается в том, что все значения должны нормально сериализовываться. Строки, числа, NSData — будут переданы отлично. Кастомные классы — только после соответствующей подготовки.

С возвращением (помимо того, что NSAttributedString напрямую передавать нельзя) возникли проблемы. Хотелось сделать красиво, с правильными шрифтами, отступами. Илья нарисовал красивый дизайн... В результате я отказался от вывода результата при помощи NSAttributedString, как хотел изначально. Это бы потребовало добавления нескольких шрифтов на часы и сложной многопроходной логики редеринга результата. Более простым решением оказалось создавать картинки на Айфоне, которые уже передавать.

Это решение оказалось удобным ещё и для тестирования. Я написал небольшую подпрограмму, которая выводит много разных результатов в картинки, чтобы мы с Ильёй смогли проверить красоту. Проверяли, отсматривая вот такие простыни картинок (2 языка, 2 размера экрана часов, больше тысячи картинок для каждого).

Всё остальное оказалось делом техники. Конечно же, пришлось потратить несколько часов на то, чтобы улучшить само распознавание наговоренного человеком текста, но в остальном, приложение не потребовало от меня больших затрат. Основа была сделана, как я и написал в заголовке, за час. Я написал несколько десятков строчек кода, а остальное время разбирался с нюансами, API, HIG и тому подобным.

Отладка приложения в симуляторе

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

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

Сборка приложения

Кроме создания приложения нужно его выложить. За сборку всех приложений у меня отвечает моя собственная система, Bilt, которая умеет готовить приложение для AdHoc или AppStore распространения, выкачивать провижн-профайлы, встраивать Fabric, прописывать правильные версии и дополнительные параметры, упрощающие в дальнейшем анализ ошибок, и вкачивать все это либо в Fabric, либо в iTunesConnect.

Эта система никак не хотела собирать и вкачивать приложение. Добраться до источника проблемы получилось на закрытых Apple-форумах, где выяснилось, что xcodebuild, которым производится сборка, не умеет делать ipa-файлы с поддержкой Apple Watch. И нужно либо руками прописывать там хитрую структуру каталогов, либо не париться и воспользоваться Xcode, в котором кнопка «отправить в AppStore» работает. Поэтому вся подготовительная работа у меня делается Bilt'ом, а потом полученный archive-файл я открываю в Xcode и завершаю отправление приложения в стор. Надеюсь, что xcodebuild допилят и все станет хорошо.

Проблемы со шрифтами

Шрифты — то, с чем пришлось повозиться, потому что хотелось и красиво и «как принято в Apple Watch», то есть, новой гарнитурой, Сан-Франциско.

Выяснилось, что в Сан-Франциско нет русских букв, вместо них подставляется Гельветика. В экранном варианте шрифта нет также и курсива. С некоторыми символами происходят странные трансформации. Так, µ, к примеру, становится курсивной.

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

Результат

Получилась магия. Я, тестируя обычной диктовкой на Айфоне, радуюсь, как ребенок. Говоришь ему «New York», он показывает текущее время в Нью-Йорке.

У меня английский UI на Айфоне, поэтому доступна только ASCII-клавиатура и только английская диктовка. Мне так приятнее. Отстутствие лишних клавиатур также сильно ускоряет ввод, не нужно их постоянно переключать. Запустил, ввел пару букв латиницей и получил результат.

Или говоришь ему «6 feet 2 inches», чтобы узнать, какой рост у Стива Джобса в понятных величинах, а Ангстрем такой: «6,1666666 ft = 1,88 m». Или «UK Pound rate», или «how many cups in a liter», или «twelve hundred in Tokyo», или «3 ångström». И оно понимает!

Магия!

Автотестирование в iOS. Юнит-тесты и UI Automation

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

Что такое автотест?

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

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

А вот четкую последовательность действий можно оформить это в виде небольшой программы:

Нажать на клавиатуре кнопки «1», «м». Проверить, что справа получилось «3,28 ft».

Запуская тесты после изменения приложения, легко проверить, что всё работает, как требуется.

Типы тестов

Для Ангстрема пришлось использовать несколько разных типов тестов:

  • Юнит-тесты, которые проверяют совсем базовые вещи.
  • UI-тесты, имитирующие тыки в Айфон, после чего проверяется правильность результата.
  • Полуавтоматические тесты, которые после протыкивания делают скриншот, проверить который должен буду я сам.
  • Тесты на производительность, которые проверяют, что не затормозился старт приложения, что ввод букв не требует большого времени из-за подгрузки индексов.

Тесты элементарной функциональности (юнит-тесты)

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

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

Юнит-тестами я проверяю:

  • Конвертирование единиц, чтобы метр всегда был равен ста сантиметрам.
  • Приоритет единиц в выдаче, чтобы по латинской «c» — градусы Цельсия, а не скорость света.
  • Форматирование отображения единиц. Чтобы большие/маленькие числа были со степенями, а валюты — с суффиксами, обозначающими размерность. Чтобы тысячи были разделены пробелами, чтобы время показывалось правильно.
  • Список групп единиц в меню, последовательность самих единиц в меню.
  • Автоподбор «правой» единицы по левой, чтобы футы по-умолчанию переводились в метры, а не в парсеки.
  • Диктовка и парсинг строки из буфера обмена.

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

К каждому тесту я создал специальный файл с проверками, чтобы можно было легко их добавлять и исправлять. Вот, например, кусок JSON-файла с тестами диктовки и парсинга буфера обмена (слева — текст, который проверяется, справа — корректный результат):

"$200": "200 $ (USA Dollar)"

"200$": "200 $ (USA Dollar)"

"семь градусов Цельсия": "7 °C (degree Celsius)"

UI-тесты

В 2010 году Эпл показала новое средство тестирования приложений, которое называется UI Automation. К сожалению, тогда «виртуальные тыки» не работали с локализованным интерфейсом и плохо работали с его динамическими изменениями, поэтому использование пришлось отложить. Сейчас проблем с локализацией нет.

Я попробовал UI Automation для:

  • Автоматического создания скриншотов приложения на разных языках и для разных устройств (для Апстора).
  • Автоматического создания видео приложения (для него же).
  • Непосредственно тестирования функциональности.

Скриншоты сделать удалось. Нужно еще автоматически подставлять туда правильную батарейку и все вообще будет классно.

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

Тесты получились почти все. Некоторые, также, как и видео, требуют сложной работы с интерфейсом, поэтому пока не работают.

Чтобы их проще было писать и запускать (пишутся они на JavaScript'е, запускаются из Терминала), я подготовил:

  • Специальные «заголовочные» файлы, которые включаются в каждый js-файл тестов и предоставляют стандартные средства («нажми на кнопку», «сделай свайп», «проверь, что результат такой»). В результате, обычные тесты теперь пишутся в одну строку, даже если они реализуют достаточно сложную логику ввода. Например, так: _testCase(["1m ft", "⌫"], "1 [meter] = 3.28 [foot]", "To").
  • Скрипты для авто-запуска тестов, чтобы можно было запустить тесты в терминале, проанализировать вывод и если, например, нет строк, начинающихся с "Fail:", то сказать, что все тесты пройдены.
  • Само приложение тоже потребовалось доработать, чтобы стало возможным из JavaScript'а «нажимать» на кнопки (об этом в разделе про Accessibility), и чтобы результат тестов можно было проверить.

Вместе с багами в самом UI Automation, было несколько больших проблем.

  • Работа с буфером обмена. Чтобы её сымитировать, пришлось завести ненастоящий и почти невидимый (совсем невидимый не редактируется) UITextField и вставить туда то, что скопировано, после чего проверить вставленное. Кстати, я узнал, что минимально возможный размер UITextField, в который что-то можно ввести — 2х2 поинта. Почему так, интересно?

  • Нажатие на конкретное место в большом контроле. Это пришлось реализовать для цифровой клавиатуры. Она сделана не несколькими кнопками, а одним большим контролом, который обрабатывает нажатия (так можно точнее контролировать внешний вид). Впрочем, для коротких тапов все сравнительно просто. А вот лонг-тап сейчас (благодаря целой группе недоработок в UI Automation) делается только в центре контрола. Так что минус (который получается долгим тапом на точку на цифровой клавиатуре) у меня так и не протестирован.

  • Пожалуй, самая большая проблема заключается в том, как проверить правильность результата тыков и свайпов. С экранных контролов считывать данные плохо, они зависят от языка. Мне же нужно проверить отдельно то, как отображаются единицы (это частично проверяется юнит-тестами и будет проверять человек), и то, как происходит преобразование. Выкрутился я, опять же, фейковым UILable'ом. Поставив несколько таких лейблов (невидимых, за пределами экрана), я проставляю в них специальные тестовые строки, которые точно отражают то, что я вижу на экране. Считать строку из лейбла не составляет труда, после чего можно сравнить её с тестовой.

В результате получилось протестировать почти всё:

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

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

Accessibility

Решение, которое позволило при помощи UI Automation работать с разными языками, называется Accessibility. Изначально оно предназначено для улучшения работы с интерфейсом людей с ограниченными возможностями, но с пятой версии iOS появилась возможность каждой вьюшке назначить accessibilityIdentifier, который позволяет в процессе тестирования найти нужный компонент как-то так:

var keypad = UIATarget.localTarget().frontMostApp().elements()["Keypad"];

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

Как написать свои тесты?

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

  2. Дождитесь, когда устаканится та функциональность, которую хочется оттестировать. Пишете тесты на API, дождитесь, пока он появится на сервере. Впрочем, на API часто есть более чёткое ТЗ, и тесты можно писать заранее, про это смотрите TDD. Проведите традиционное тестирование перед тем, как писать автотесты.

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

  4. Создайте инфраструктуру для тестов. Тесты хороши, когда их много и они маленькие. Если, чтобы создать тест, вам приходится писать даже 5-10 строк, то вы никогда не напишете тысячу тестов, вам просто надоест кодить одно и то же. Сделайте так, чтобы тест создавался одной строкой. Можно вынести конфигурацию в отдельный файл, можно создать макрос или метод.

  5. Иногда полезно сгенерировать тесты. Если вы понимаете, что вот сейчас приложение работает верно, зафиксируйте это состояние, сгенерите тесты прямо самим приложением. В будущем, если что-то поменяется, вы это тут же увидите.

  6. Тесты — это не волшебная палочка, которая «работает». Их нужно поддерживать в рабочем состоянии, они тоже ломаются, в них тоже бывают ошибки. Если вы решили создать тесты, заложите в работу и время на их поддержку и развитие.

Что дальше?

За рамками пока остались полуавтоматические тесты (когда скриншоты смотрятся и контролируются человеком) и тесты на скорость работы интерфейса, с которой начинаются проблемы при добавлении новых единиц. Про это — в следующий раз.

А у вас какой опыт в тестировании приложений для iPhone и iPad?

Круглые пиксели в Айфоне

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

Варианты

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

У такого решения одно достоинство, его можно реализовать на любой версии iOS, любыми средствами. При должной аккуратности, оно очень быстро работает. Из недостатков — уголки и маски закрывают не только часть нужной картинки, но и весь остальной фон. Если на фоне картинка, или градиент, или просто что-то меняющееся (выделение ячейки), то начинаются трудности.

Второе решение — использование свойства слоя cornerRadius. Оно предназначено для создания скругленных углов, но можно довести его до экстремума и поставить радиус скругления в половину размера кнопки (или другой вьюшки). Получается аккуратный такой кружок (если был квадрат). Из минусов можно вспомнить тормоза, если на экране много таких, скругленных, слоёв (это обычно происходит в таблицах при скроллинге). В современных устройствах и версиях iOS, правда, этого можно избежать.

Третье решение также использует параметр слоя, но уже другой. mask позволяет отмаскировать слой любой фигурой. Удобно? Очень. Нужно лишь подобрать правильную маску.

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

Казалось бы, выбирай любое и используй. Так обычно и делают. Но иногда нужно сделать «правильно». В зависимости от того, что значит «правильно», некоторые варианты не подходят совсем.

Круглая кнопка

Round_CornerRadius.png

Если посмотреть на то, что получается в результате скругления кнопки, выяснится, что layer.cornerRadius дает более «равномерное» скругление, чем маскирование эллипсом или скругленным прямоугольником (при помощи маски). Скругленный прямоугольник вообще ведет себя странно, в какой-то момент отказываясь скруглять квадрат, чуть позже превращаясь в круг. Поведение можно посмотреть, если скачать/собрать/запустить приложение-пример по ссылке в конце записи.

На анимации слева показаны оба варианта. Обратите внимание на края. Тот вариант, у которого более острые верх и низ — это маска. Более гладкий — cornerRadius.

Сосисочная кнопка

Sausage_All.png

Изменения в сосисочной кнопке более выражены, поэтому показываю их без анимации. Справа — вариант layer.cornerRadius. Видны «углы» на стыках полукруга и сторон.

Посередине — вариант, который предлагает ОС в UIBezierPath bezierPathWithRoundedRect:cornerRadius: Сгладились эти переходы, кнопка стала более плавной сверху и снизу, но появилась плоская часть слева (радиус скругления по-прежнему половина кнопки, он не одинаковый для всех тестов)

Слева — еще более жесткий вариант. Его мы используем в Ангстреме, но для сильно меньших скруглений. Видно, что тут он черезчур. Если же радиус скругления уменьшать, становится хорошо.

Сложности

На картинках приведены сплошные кнопки. А что, если кнопка должна быть контурная? Для этого, в случае использования layer.cornerRadius, нужно проставить layer.borderColor и layer.borderWidth. Эти параметры не кастомизируются состояниями кнопки, поэтому придется хитрить, переопределяя UIButton. В случае использования маски, я обычно рендерю картинку для разных состояний кнопки. При редеринге получаю контур, потом заливаю его одним цветом, рисую линию по контуру другим и сохраняю полученное изображение в UIImage. Это не только позволяет легко создавать разные кнопки, но и кешировать их, чтобы приложение работало быстрее.

Есть ещё одна проблема, которая почти не видна в приведенных примерах. Если нарисовать, например, белую сосисочную кнопку, скруглив ее при помощи layer.cornerRadius (правый вариант), то по краям сверху и снизу по всей ширине иногда появляются темные полоски. Что это? Наверняка погрешности алгоритма сглаживания. Точно сказать трудно, исходников алгоритма я не видел.

Выводы и программа, чтобы побаловаться

Во-первых, печально, что нет единого решения. Я в будущем планирую использовать для «круглых кнопок» layer.cornerRadius, а для вытянутых сосисок — UIBezierPath. Собственное скругление будет работать для углов с небольшим радиусом.

Если вам интересно самим поисследовать разные варианты, можно взять исходники на ГитХабе: https://github.com/bealex/Test_RoundButton

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

Почему падал Зерулс?

Версия 2.0.2 Зерулса, как можно понять из номера — версия с исправлениями ошибок (как и предыдущая). Самая большая неприятность случилась с одной из предыдущих версий. В ней, если запустить приложение в iOS 6, все падало. 

Источник ошибки оказался в коде, который отвечал за то, чтобы старая версия выглядела как раньше. В новой (для iOS 7+) при этом же, полностью обновлен дизайн, переделан вывод текста на TextKit и добавлены новые анимации.

Почему вообще возникают ошибки в приложениях? Их создает программист. Он пишет код, который ломается. Хочется, конечно, чтобы программист был хороший и писал код без ошибок. Сложно ли этого добиться? Очень.

Чтобы примерно представить себе сложность кода, попробуем её сравнить со стройкой (чуть серьезнее, чем в истории про программистов и дома). Пускай программа содержит несколько тысяч методов, кирпичиков, из которых она состоит. Пусть также квадратный метр стены толщиной «в один кирпич» — это 100 кирпичей. Получается, что из нескольких тысяч кирпичиков-методов можно выстроить, к примеру, четыре стены для одной комнаты. Хороший каменщик справится достаточно быстро. А программист?

Сложность заключается в том, что каждый метод-кирпич — разный. Разный размер, разная форма, разные выступы, разный материал. Нет двух одинаковых кирпичей (один из принципов программирования так и говорит: Don't Repeat Yourself, или DRY). Кирпичеукладка сразу же получается совсем не простая, ведь нужно аккуратно подбирать их друг к другу. При этом, укладывая нижний ряд, следует подумать, что сверху может быть что-то тяжелое.

В середине процесса (или даже в самом конце) укладки-разработки вдруг выясняется, что вот этот кирпичик, поставленный в самом начале строительства, стоит некрасиво. Он должен переливаться разными цветами, а не просто светиться одним. И нужно его аккуратно вынуть и заменить, не сломав соседние и не взорвать всю конструкцию целиком.

Внезапно задача из понятной становится неимоверно, безумно сложной. Нужно держать в голове связи между всеми кирпичами, понимать возможности каждого и способы их замены. Зерулс, конечно, маленький, как одна комната. Но есть и особняки (ядро Линукса), или небольшие города (дистрибутив iOS со всеми приложениями и сервисами). В них количество взаимодействий между отдельными элементами (и даже внутри каждого элемента-подпрограммы) безумное, непредставимо большое.

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

На самом деле, есть способы математически доказать, что программа будет работать корректно. Объем работы, которую нужно для этого проделать, показывает проект ядра операционной системы. Самая интересная там статистика проекта. 8700 строк кода на С + 600 строк на Ассемблере. Проверено из этого 7500 строк. Скрипт проверки — 200 000 строк. Если напечатать его на бумаге формата А4, получится стопка высотой полметра. Потрачено примерно 30 человеко-лет работы.

В ЗеРулсе примерно 17 000 строк кода. В Ангстреме — больше 30 000.

Система стилей Ангстрема

Создание мобильного приложение — это итеративный процесс. Дизайн начинается в Фотошопе, а разработка — в АппКоде. Но когда первый прототип готов, дизайн и программирование объединяются: для практически любого изменения в интерфейсе необходимо изменить код. Для изменения цвета, шрифта или настройки параметров анимаций, дизайнер просит разработчика о помощи. Это взаимодействие замедляет процесс, а некоторые эксперименты из-за него получаются слишком затратными.

Когда мы с Ильёй начинали делать Ангстрем, то сразу решили создать гибкую стилевую систему, чтобы упростить подстройку дизайна в процессе разработки.

Базовые принципы

Стилевые файлы хранятся отдельно от кода, они должны легко читаться, редактироваться в любом редакторе и парситься приложением. Поэтому в качестве формата я выбрал JSON. Plist или CSS нам подошел меньше. Для хранения стилей мы используем DropBox.

Обновление стилей "на лету", в процессе работы приложения, без перекомпиляции, переустановки или рестарта. Если вы трясанете Айфон, стили обновятся с сервера.

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

Код должен легко поддерживаться. Система позволяет не обращаться к стилям по ключам, как, например, [styler boolForKey:@"isTopBarHidden"], так как ошибку в такой строке тяжело искать. Создается объект с соответствующими полями, после чего компилятор его проверяет (style.isTopBarHidden) и разработчик может использовать этот объект для получения стилей.

Архитектура

Три основных объекта системы стилей:

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

После того, как стайлер создаст стилевые объекты из JSON'а, он:

  • Использует стандартный NSArchiver, чтобы сохранить и восстановить кэш стиля в бинарной форме (для быстрой загрузки);
  • Позволяет получать значения стилей максимально быстро и удобно, обращаясь к полям класса (для быстрой работы приложения и удобства).

Пример. Если стиль описан так:

"cursor": {
    "showTime": 0.2,
    "hideTime": 0.2,

    "color": "@colors.cursor.color",

    "period12": 0.4,
    "timingType12": "linear",
}

Стайлер преобразует его в такой класс:

@interface AGRCursorStyle : ASStyleObject 
        <NSCoding, data-preserve-html-node="true" ASStyleObjectApplyable>
    @property (…) CGFloat showTime;
    @property (…) CGFloat hideTime;
    @property (…) UIColor *color;
    @property (…) CGFloat period12;
    @property (…) AGRConfigAnimationType timingType12;
@end

ASStyleObject — это базовый класс всех стилевых объектов.

Работа со стайлером

Сначала нужно попросить стайлер прочитать стили из JSON'а:

ASStyler *styler = [ASStyler sharedInstance];
[styler addStylesFromURL:@"styles.json" 
   toClass:[AGRStyle class] 
   pathForSimulatorGeneratedCache:@"SOME_PATH"];

Префикс для классов, относящихся к стайлеру — "AS". У классов стилей в примерах префикс "AGR", потому что они взяты из проекта "Ангстрем".

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

Конечно же, лучше сделать обновление и кэша и структуры стилевых объектов на лету, в процессе редактирования файла стиля. Но это требует соответствующих плагинов для IDE, а их пока нет.

После инициализации можно получать стили вот так:

ASStyler *styler = [ASStyler sharedInstance];
AGSStyle *mainStyleObject = ((AGSStyle *) styler.styleObject);
AGRCursorStyle *style = mainStyleObject.cursor;

Этот вариант предназначен, чтобы получить значение стиля, использовать его и забыть.

Также можно вытащить значение вот так:

AGRCursorStyle *style = [[AGRCursorStyle alloc] 
   initWithStyleReloadedCallback:
        ^{
             [self styleUpdated];
             [self setNeedsDisplay];
         }];

В этом случае мы не только получаем стиль курсора, но и подписываемся на его изменения. В случае, если стиль обновился, вызовется блок, где уже можно обновить UI, например, с помощью [self setNeedsDisplay], или другого, собственного, метода.

AGRCursorStyle можно написать вручную, но лучше создать все классы стилевых объектов автоматически. Стайлер может делать это каждый раз при запуске симулятора (сохраняя в файлы ProjectStyles.h/m) при помощи следующего кода:

[styler generateStyleClassesForClassPrefix:@"AGR"
        savePath:@"[PATH_TO_CODE]/Styles/"
        needEnumImport:YES];

При этом создадутся классы AGR[ГорбатоеИмяСтиля]Style для каждого правила. Например, по правилу "editor" создастся класс AGREditorStyle, а если внутри него есть правило "toolbar", будет создан класс AGREditorToolbarStyle. Главный класс будет называться AGRStyle.

Формат стилей

Файлы стилей — это обычный JSON. Полное название стилевого правила, если необходимо, составляется из его имени и имени родителей, через точку. То есть, если есть стили:

"editor": {
    "cursor": {
        ...
    }
}

То название стиля курсора будет "editor.cursor".

Стиль может ссылаться на другой при помощи следующего синтаксиса: "@another.name". Ссылка заменяется на конечное значение по ней.

"someStyle1": "value",
"someStyle2": "@someStyle1"

Также есть поддержка включений подфайлов стилей.

"@include.fonts": {
    "inApp": "fontStyles.json",
    "remote": "http://[SERVER]/fontStyles.json"
},

"Remote" — необязательная часть, в случае, если она есть, сначала грузится локальный стиль, и потом, в фоне, подгружается серверный, заменяя своими стилевыми правилами локальные.

Для указания типа стилей, используются префиксы:

  • color для UIColor, Цвета можно указывать шестнадцатеричным значением, почти как в CSS (тремя одинарными шестнадцатеричными цифрами, шестью без альфы или восемью с альфой)
  • point, origin, location, position, center для CGPoint,, указывается массив из двух чисел, x и y
  • size или dimensions для CGSize,, указывается массив, width и height
  • rect, frame, bounds для CGRect,, указывается массив из четырех чисел: x, y, width, weight
  • margin(s), padding(s), border для UIEdgeInsets,, указывается массив из четырех значений: top, left, bottom, right
  • font для UIFont,
  • textAttributes для атрибутов NSAttributedString.

Например:

"margins": [23, 15, 10, 15]
"separatorColor": "@colors.about.separatorColor"
"appBackgroundColor": "#0f0d0a"
"labelRect": [0, 0, 120, 40]

и так далее.

Шрифты и атрибуты для NSAttributedString — это объекты со строго определенными полями. Точки, размеры, прямоугольники, отступы — обычные массивы. Вот, например, описание шрифта:

"font": {
    "name": "HelveticaNeue",
    "size": 13
}

А вот описание атрибутов для NSAttributedString:

"normalTextAttributes": {
    "font": "@primaryFont",
    "lineBreakMode": "NSLineBreakByTruncatingTail",
    "color": "#990202"
}

(можно указывать и другие атрибуты, в примере показаны не все).

Также в стилях можно использовать "функции". Сейчас поддерживаются две:

~color.alpha(Цвет, Прозрачность)
~color.mix(Цвет1, Цвет2, Доля)

Первая изменяет прозрачность цвета, вторая смешивает цвет по формуле:

Результат = Цвет 1*Доля + Цвет 2*(1 - Доля)

Заключение

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

Если у вас есть какие-то пожелания, замечания или вопросы по стайлеру — пишите: alex@lonelybytes.com.

Mastodon