- PVSM.RU - https://www.pvsm.ru -
Ангстрем, безусловно, если смотреть на выполняемую функцию, велосипед. Сколько способов преобразовать единицы? Много. Можно пользоваться гуглом, можно одним из сотен приложений для iOS или Android.
Но, вместе с тем, ни один способ не решал одну проблему. Как мне получить результат конвертирования, когда я смотрю сериал? Конкретно, Mythbusters [1]. Они там всегда общаются между собой про футы и фунты. Сколько это? Большая ли квартира, 500 ft²? (не очень, как оказалось) Много ли это, 27 psi (угу, дофига)? И, наконец, скажите им, что Фаренгейты — вообще никому не понятны!
С обычными конверторами приходится останавливать видео, выяснять, какая это категория, «psi», потом искать там этот самый «pounds per square inch», вспоминать, какое число нужно ввести, понять, во что её перевести (чтобы осознать масштаб проблемы). Делать это хочется с тем устройством, которое под рукой, желательно без интернета.
И вот эту проблему не решить ни одним конвертером. Я перепробовал, наверное, сотню. Она решается гуглом, но это тоже медленно (запустил браузер, ввел что-то в строке, гугл не понял, или понял не так...).
Так что велосипед ли Ангстрем? Вроде бы нет.
Поглядим теперь на сложности, которые пришлось решить при его разработке. Технические сложности, программерские.
Существенная часть сложностей выросла из великолепного дизайна, который придумал Илья Бирман. Без него Ангстрем бы назывался GeeKonv и выглядел бы как-то так:
Вместо этого получилось приложение, на которое приятно смотреть, и которым удобно пользоваться.
Немного про то, какие вопросы приходилось решать про UI, можно почитать у Ильи в разделе блога про Ангстрем [2], а я продолжу про технику.
Конвертирование единиц — сложная задача. Во-первых, единиц много и сложно не ошибиться со всеми коэффициентами и преобразованиями. Во-вторых, некоторые преобразования нетривиальны. Если преобразование из футов в метры требует деления на один коэффициент и последующего умножения на другой, то, например, чтобы перевести Фаренгейты в Цельсии, нужно постараться немного больше.
Вычисление формул — непростая задача. Их нужно как-то записывать, как-то парсить, как-то подставлять переменные, и так далее. По счастью, в процессе исследований я наткнулся на статью, рассказывающую о побочном свойстве NSExpression
, которое позволяет вычислять некоторые арифметические выражения. Работает оно вот так:
[[NSExpression expressionWithFormat:@"(23-7.5)*40.0/21.0+273.15"] expressionValueWithObject:nil context:nil]
И позволяет вычислять что-то простое (как в примере), или, используя функции, перечисленные в документации [3], чуть более сложное.
Работая с вычислениями через NSNumber
и NSExpression
, нужно не забыть определить поведение NSNumber
в случае неверных результатов. В противном случае вычисления будут ломаться. Делается это при помощи такого кода:
[NSDecimalNumber setDefaultBehavior:Класс, наследующийся от NSDecimalNumberHandler]
Я там возвращаю значения, чтобы ничего не ломалось, и заодно вывожу ошибки в лог.
@implementation CONDecimalNumberHandler
- (NSRoundingMode)roundingMode {
return NSRoundBankers;
}
- (short)scale {
return NSDecimalNoScale;
}
- (NSDecimalNumber *)exceptionDuringOperation:(SEL)operation
error:(NSCalculationError)error
leftOperand:(NSDecimalNumber *)leftOperand
rightOperand:(NSDecimalNumber *)rightOperand {
NSLog(@"Error during parsing number: %@/%@ (%d)", leftOperand, rightOperand, (int) error);
if (error == NSCalculationOverflow || error == NSCalculationUnderflow) {
return [[NSDecimalNumber alloc] initWithString:@"0"];
} else {
return [[NSDecimalNumber alloc] initWithString:@"1"];
}
}
@end
Сами списки единиц тоже хранить непросто. Ведь нужно:
Исходно единицы я храню в виде текстовых файлов (так их проще редактировать), отдельно — базовая информация, отдельно локализация для каждого языка файл. Вот файл для скоростей (бесплатных)
knot kn,kt 0.514444 3 impAdd
# На высоте 11 км из-за падения температуры скорость звука ниже — около 295 м/с или 1062 км/ч.
Mach M 295.0464 2 other
speed of light c 299792458 0.11 siAdd
meter per minute m/min 60 0 siAdd2
centimeter per second cm/s 100 0 siAdd2
^minutes per kilometer min/km FORMULAE(16.666666667/X,16.666666667/X) 0 other
^minutes per mile min/mi FORMULAE(26.805555556/X,26.805555556/X) 0 other
Из них я получаю JSON-файлы, которые изначально и использовались для работы. К сожалению, это оказалось недостаточно гибко и быстро. Поэтому сейчас все данные пакуются в SQLite-базу и читаются оттуда по необходимости. Формат базы повторяет, более-менее, структуру JSON-файла, которая была такой:
[
{
"fml": "",
"abbrs": [
"m/s"
],
"us": "si",
"id": 1,
"tag": "meter per second",
"pri": 3,
"to": 2000002,
"cof": 1,
"names": [
"meter per second"
]
},
...
]
Кроме базы с основными данными ещё нужно дерево поиска. Ангстрем умеет работать в двух режимах:
NSNumberFormatter
умеет парсить строки-как-числа, например, «thirty-four» оно умеет распознавать, как «34». Это суперская фича, которую неимоверно сложно использовать правильно, так как у нас не просто число, а строка, из которой это число нужно выделить. Приходится бежать по словам, используя всё расширяющиеся диапазоны, чтобы попытаться распарсить максимально большое число.Оба этих режима используют дерево единиц. Можно было бы использовать стандартный текстовый поиск, например, из SQLite, но тогда пришлось бы сильно возиться с токенайзерами и настройками, поэтому я решил просто написать свой. Сложность и там и там похожая, но со своим у меня больше возможностей по оптимизации.
Узлы дерева поиска хранятся в отдельных файлах. Вот таких (я взял очень коротенький):
{"p":"наб","u":{"":[[1,13,631]]},"s":{},"f":"ережныечелны"}
Это позволяет не хранить его целиком в памяти, загружая по необходимости. Дерево большое, и это сильно ускоряет запуск, работу на старых устройствах (Ангстрем нормально работает на iPhone 4) и уменьшает нагрузку на память.
Файлы я упаковал DPLPacker'ом,
про который написано в моей статье про iTrace [4]. Их на настоящий момент почти 7500, и без упаковки пришлось бы очень плохо.
Вся информация про единицы в Ангстреме занимает сейчас примерно пять мегабайт, а файл приложения в сторе — 13.2 мегабайта. Сразу видно, приложение — про перевод единиц :)
Оптимизация — вопрос, который далеко не всегда встаёт перед разработчиками приложений. Скорость развития техники позволяет иногда либо просто забить на это, либо сделать «что-то простое», и хватит. Ангстрем же приходится использовать в достаточно экстремальных условиях, например, на Apple Watch, или на стареньком iPhone 4 (пока поддерживается iOS 7). На этих устройствах мало памяти и сравнительно небыстрый процессор. Поэтому оптимизировать приходится всё, и при этом не забывать, что в будущем может быть в 10 раз больше разных единиц (сейчас их примерно 1050).
Главных моментов для оптимизации три:
Ещё до разработки Ангстрема я узнал, что очень удобно, когда есть задача, которая чрезвычайно ограничена по какому-то ресурсу. Например, для работы приложения на Apple Watch, нужно оптимизировать скорость, первая версия часов очень, очень медленная, а парсить приходится естественный текст, это занимает существенное время. Также, в версии 1.8, парсинг происходит сразу на нескольких языках, чтобы можно было продиктовать по-русски, даже если интерфейс по-английски (сам диктейшн не выдает никакой индикации про то, какой язык сейчас используется). Оптимизация под такое, «плохое» устройство, улучшает производительность и для остальных, более современных и быстрых.
Чтобы сделать Today Extension (сейчас он выключен, так как глючит и плохо работает), требовалась жесточайшая оптимизация по памяти. Хотелось, чтобы он умел парсить строку из буфера обмена (а не просто показывать пару строчек), это требовало вполне полноценного приложения. Забавный момент, что Apple говорит про объём памяти, доступный такого рода расширению. «Мало», говорят. Сколько это — мало? «Чем меньше, тем лучше!» Никаких конкретных чисел. Поэтому оптимизация по памяти — до предела.
Это всё доводит требования по оптимизации до экстремума. Приходится применять все известные техники, придумывать новые структуры данных (точнее, использовать хорошо забытые старые), тыкать палочкой в параллельность, внимательнейшим образом смотреть на то, что показывает Instruments, время от времени выкидывать алгоритмы, которые тормозят, в пользу более сложных, но и более эффективных.
Я даже советую иногда, если разрабатывается приложение, взять самое тормозное устройство, которое есть, и запустить, погонять на нём. У меня специально для этого лежит и iPhone 4, и iPod Touch пятого поколения (там то же железо, что и в iPhone 4S). Первый почти неактуален, а вот второй — будет актуален ещё год-полтора (на него встаёт все, включая последнюю на сегодня iOS 9).
Про внешний вид я уже немного писал. Например, про правильное скругление углов [5] можно почитать в моём блоге, там же есть про тестирование UI [6]. Но есть несколько моментов, которые я пока не описывал.
Клавиатура в Ангстреме показывается всегда, кроме случаев, когда она не показывается. Она сдвигается (попробуйте посвайпить вправо от первого экрана), она пропадает (подключите внешнюю клавиатуру или скройте её на Айпаде), и она бывает разная (Айфон/Айпад/Айпад Про). К ней также привязана наша цифровая клавиатура, которая бывает узкая
Бывает высокая
Бывает Айпадная
В версии 1.8 она научилась переключаться, чтобы уметь вводить шестнадцатеричные или римские цифры. Подытоживая, там всё сложно.
Расскажу я про две вещи. Как сдвинуть клавиатуру, и как сделать, чтобы кипад (наша цифровая) работал в Accessibility.
Чтобы сдвинуть клавиатуру, нужно понимать устройство окошек в iOS. Для каждого приложения выделяется своё окно (UIWindow), но если появляются модальные диалоги или клавиатуры, то количество окошек увеличивается. Если использовать что-то вроде Reveal [7], то иерархия видна очень хорошо:
На картинке (от дальних к ближним) слои:
Тут сразу видно (за это я и люблю Reveal), что и как нужно двигать, чтобы работало. В результате главное окно я двигаю, как хочется (у меня над ним полный контроль), свою клавиатуру тоже я, по-крайней мере, могу получить по ссылкам и подвинуть:
_keypadView.transform = CGAffineTransformMakeTranslation(_keypadView.virtualFramePositionX, 0);
Окно клавиатуры же я могу получить перебором всех окон приложения, или просто попробовав получить последнее окно, если оно похоже на клавиатуру:
NSArray *windows = [UIApplication sharedApplication].windows;
if ([NSStringFromClass([windows.lastObject class]) contains:@"Keyboard"]) {
_keyboardWindow = windows.lastObject;
}
Почему я не использую перебор всех окон в приложении каждый раз, кешируя значение? Это достаточно сильно тормозит в iOS 9 (а раньше было нормально). Поэтому приходится оптимизировать (код должен работать 60 кадров в секунду, при интерактивных свайпах). Для ускорения я также проверяю, что frame окна действительно поменялся и обновляю его исключительно в нужной ситуации. И не frame, но только center, так как размеры окна всегда остаются прежними, а смена frame может повести за собой гораздо более серьёзные изменения, чем просто изменение положения вьюхи.
Если вы будете двигать клавиатуру, как я, то будьте готовы к глюкам. Глюки в каждой версии iOS разные, проявляются они в:
UIMenuController
.Также будьте готовы к тому, что клавиатура может пропасть (или появиться другая). Например, при покупке расширенного набора единиц — появляется системная клавиатура для ввода пароля. После завершения процедуры покупки (как бы он ни завершился), нужно вернуть клавиатуру обратно.
В общем, на самом деле, рекомендация простая. Не трогайте клавиатуру без повода, пускай этим система занимается. Иначе вляпаетесь, как я вляпываюсь, граблей там очень много.
Мне очень хотелось, чтобы наш кипад, пусть даже и не выглядящий, как стандартная клавиатура, вёл себя похожим образом. Сначала я попытался найти, как воспроизводить звук нажатой клавиши. Радостный, я узнал про UIInputViewAudioFeedback
, и про метод [[UIDevice currentDevice] playInputClick]
, который делает ровно то, что нужно.
После этого мне потребовалось поддержать accessibility, то есть работу приложения, когда им пользуются пользователи с ограниченными возможностями. Для обычных компонентов интерфейса нет ничего проще. Задаём для компонента несколько пропертей, и всё.
self.isAccessibilityElement = YES;
self.accessibilityLabel = @"Кнопка зелёная";
self.accessibilityHint = @"Нажимайте, если хотите сделать хорошо";
self.accessibilityValue = @"Нажата";
С кипадом всё оказалось сложнее. Рисую я его целиком, чтобы проще было нарисовать фоновый градиент, а в более старых версиях и углы скруглить.
Чтобы поддержать нормально и клавиатурное поведение (клики), и всё остальное, пришлось поверх нарисованной клавиатуры создать прозрачные кнопки-наследники UIButton,
которым правильно прописать значения, и внимательно следить за тем, чтобы они менялись при смене клавиатуры (в последней версии появился ввод римских и шестнадцатеричных чисел).
Если тема Accessibility вам интересна, могу рассказать про неё подробнее. Или можно поглядеть соответствующие сессии с WWDC, они очень хорошие: iOS Accessibility [8] и Apple Watch Accessibility [9]
Accessibility помог мне ещё и при тестировании, о чём я подробнее рассказывал в своём блоге [6].
Может, интересуют какие-то другие особенности или подробности реализации? Спрашивайте!
Автор: bealex
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka-pod-ios/114604
Ссылки в тексте:
[1] Mythbusters: https://en.wikipedia.org/wiki/MythBusters
[2] в разделе блога про Ангстрем: http://ilyabirman.net/meanwhile/tags/angstrom/
[3] перечисленные в документации: http://file:///Users/alex/Library/Developer/Shared/Documentation/DocSets/com.apple.adc.documentation.iOS.docset/Contents/Resources/Documents/documentation/Cocoa/Reference/Foundation/Classes/NSExpression_Class/index.html#//apple_ref/occ/clm/NSExpression/expressionForFunction:arguments:
[4] iTrace: https://habrahabr.ru/post/278683/
[5] правильное скругление углов: http://www.lonelybytes.com/blog-ru/2015/1/29/roundpixels
[6] тестирование UI: http://www.lonelybytes.com/blog-ru/2015/3/22/-ios-ui-automation
[7] Reveal: http://revealapp.com
[8] iOS Accessibility: https://developer.apple.com/videos/play/wwdc2015/201/
[9] Apple Watch Accessibility: https://developer.apple.com/videos/play/wwdc2015/204/
[10] Источник: https://habrahabr.ru/post/278695/
Нажмите здесь для печати.