Ангстрем. Купка складнощів в простій обгортці


Коли потрібно ще один велосипед?
Ангстрем, безумовно, якщо дивитися на виконувану функцію, велосипед. Скільки способів перетворити одиниці? Багато. Можна користуватися гуглом, можна одним з сотень додатків для iOS або Android.

Але, разом з тим, жоден спосіб не вирішував одну проблему. Як мені отримати результат конвертування, коли я дивлюся серіал? Конкретно, Mythbusters. Вони там завжди спілкуються між собою про фути і фунти. Скільки це? Велика квартира, 500 ft2? (не дуже, як виявилося) Багато це, 27 psi (угу, дофіга)? І, нарешті, скажіть їм, що Фаренгейтом — взагалі нікому не зрозумілі!

З звичайними конверторами доводиться зупиняти відео, з'ясовувати, яка це категорія, «psi», потім шукати там цей самий «pounds per square inch», згадувати, яке число потрібно ввести, зрозуміти, у що її перекласти (щоб усвідомити масштаб проблеми). Робити це хочеться з тим пристроєм, який під рукою, бажано без інтернету.

І ось цю проблему не вирішити ні одним конвертером. Я перепробував, мабуть, сотню. Вона вирішується гуглом, але це теж повільно (запустив браузер, ввів що-то в рядку, гугл не зрозумів, чи зрозумів не так...).

Так що велосипед чи Ангстрем? Начебто немає.

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

Коротко про дизайні
Істотна частина складнощів зросла з чудового дизайну, який придумав Ілля Бірман. Без нього Ангстрем би називався GeeKonv і виглядав би як-то так:


Замість цього вийшло додаток, на яке приємно дивитися, і яким зручно користуватися.


Трохи про те, які питання доводилося вирішувати про UI, можна почитати у Іллі у розділі блогу про Ангстрем, а я продовжу про техніку.

Обчислення формул
Конвертування одиниць — складне завдання. По-перше, одиниць багато і важко не помилитися з усіма коефіцієнтами та перетвореннями. По-друге, деякі перетворення нетривіальні. Якщо перетворення з футів на метри вимагає поділу на один коефіцієнт і подальшого множення на інший, то, наприклад, щоб перевести Фаренгейтом в Цельсии, потрібно постаратися трохи більше.

Обчислення формул — непросте завдання. Їх потрібно записувати, як-то парсити, як-то підставляти змінні, і так далі. На щастя, в процесі досліджень я натрапив на статтю, що розповідає про побічну властивості
NSExpression
, що дозволяє обчислювати деякі арифметичні вирази. Працює воно ось так:

[[NSExpression expressionWithFormat:@"(23-7.5)*40.0/21.0+273.15"] expressionValueWithObject:nil context:nil]

І дозволяє обчислювати щось просте (як у прикладі), або, використовуючи функції, перераховані в документації, трохи більш складне.

Працюючи з обчисленнями через
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

Зберігання таблиць з одиницями
Самі списки одиниць теж зберігати непросто. Адже потрібно:

  • Розуміти коефіцієнти або формули для кожній.
  • Зберігати пріоритети, щоб можна було вибрати найбільш правильну одиницю для кожного конкретного випадку.
  • Для цього ж зберігається тип системи вимірювання одиниці (СІ або імперська, наприклад).
  • Переклади назв одиниць робляться для кожної мови і для всіх можливих словоформ (для російської мови, наприклад, п'ять словоформ). Сюди ж відносяться можливі символи для одиниці, синоніми назв (ар або сотка або квадратний декаметр).
  • Деякі одиниці об'єднуються в «кластери», наприклад, 1 метр переведеться у фути, а 10 метрів — в ярди. Друга одиниця вибирається з кластера.
  • Нарешті, самі одиниці потрібно об'єднати в категорії, щоб правильно відображати в меню, та враховувати цю інформацію при форматуванні/перекладі одиниць.
Початково одиниці я зберігаю у вигляді текстових файлів (так їх простіше редагувати), окремо — базова інформація, окремо локалізація для кожної мови файл. Ось файл для швидкостей (безкоштовних)

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 ФОРМУЛИ(16.666666667/X,16.666666667/X) 0 other
^minutes per mile min/mi ФОРМУЛИ(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"
]
},
...
]

Крім бази з основними даними ще потрібно дерево пошуку. Ангстрем вміє працювати в двох режимах:

  • переклад всередині програми, коли є три рядки «число», «одиниця раз», «одиниця два». У цьому випадку потрібно знайти дві одиниці за рядками, і потім обчислити результат. Розпізнавання йде, як звичайно. Бігаємо по дереву з підрядками, у вузлах якого живуть букви, і до кожного з вузлів прив'язаний список одиниць. Добігли до потрібного вузла, отримали список одиниць, відобразили варіанти (або взяли перший, якщо потрібен тільки один).
  • переклад рядка. Наприклад, ми надиктували що на годиннику, або за допомогою стандартної диктування), або скопипастили з іншої програми. В цьому випадку процедура приблизно наступна:
    • Розбиваємо рядок на слова
    • Вичищаємо зовсім вже зайве, готуємо, робимо першу порцію магії (наприклад, second замінюємо, так як система розпізнає це не як «секунду», а як «другий», що зазвичай невірно).

    • Парсим число. Тут застосовується друга порція магії. Справа в тому, що
      NSNumberFormatter
      вміє парсити рядки-як-числа, наприклад, «thirty-four» воно вміє розпізнавати, як «34». Це суперская фіча, яку неймовірно складно правильно використовувати, так як у нас не просто число, а рядок, з якої це число потрібно виділити. Доводиться бігти за словами, використовуючи всі розширюються діапазони, щоб спробувати розпарсити максимально велике число.
    • Залишилися слова пробуємо розпарсити на юніти, або один, або два. Тут магії найбільше, так як говорити користувач може як хоче, в будь-якій послідовності. Доводиться створювати купу евристик, які правильно реагують на ті або інші магічні слова. В основі тут все той же пошук по дереву одиниць, що і в звичайному варіанті.
Обидва ці режими використовують дерево одиниць. Можна було б використовувати стандартний текстовий пошук, наприклад, з SQLite, але тоді довелося б сильно возитися з токенайзерами і налаштуваннями, тому я вирішив просто написати свій. Складність і там і там схожа, але зі своїм у мене більше можливостей оптимізації.

Вузли дерева пошуку зберігаються в окремих файлах. Ось таких (я взяв дуже коротенький):

{"p":"наб","u":{"":[[1,13,631]]},"s":{},"f":"ережныечелны"}

Це дозволяє не зберігати його в пам'яті, завантажуючи по необхідності. Дерево велике, і це сильно прискорює запуск, роботу на старих пристроях (Ангстрем нормально працює на iPhone 4) і зменшує навантаження на пам'ять.

Файли я спакував
DPLPacker'ом,
про який написано в моїй статті про iTrace. Їх на даний момент майже 7500, і без упаковки довелося б дуже погано.

Вся інформація про одиниці в Ангстреме займає зараз приблизно п'ять мегабайт, а файл програми у сторі — 13.2 мегабайта. Відразу видно, додаток — про переклад одиниць :)

Оптимізація
Оптимізація — питання, яке далеко не завжди постає перед розробниками додатків. Швидкість розвитку техніки дозволяє іноді або просто забити на це, або зробити «щось просте», і вистачить. Ангстрем ж доводиться використовувати в досить екстремальних умовах, наприклад, на Apple Watch, або на старенькому iPhone 4 (поки підтримується iOS 7). На цих пристроях мало пам'яті і порівняно нешвидкий процесор. Тому доводиться оптимізувати всі, і при цьому не забувати, що в майбутньому може бути в 10 разів більше різних одиниць (зараз їх приблизно 1050).

Головних моментів для оптимізації три:

  • Старт програми. Для налагодження старту використовуємо Instruments, і викидаємо зі старту все, що можна. Всі оновлення — починаються через пару секунд після старту. Жодних завантажень одиниць, крім тих, що на екрані, ніяких важких ресурсів.
  • Пошук одиниці. Для цього я зробив дерево пошуку, і розбив інформацію по буквах на окремі файли. Ввели букву — завантажили рівне файл про цю букву, нічого більше. У самих файлах інформація зберігається в дуже компактному вигляді (айдишки, прості списки).
  • Використання пам'яті. Прийоми тут схожі на попередній пункт, так як основний споживач пам'яті — як раз база по одиницям. SQLite, разом з фрагментованим файлів деревом пошуку непогано вирішують проблему.
Ще до розробки Ангстрема я дізнався, що дуже зручно, коли є завдання, яка надзвичайно обмежена з якогось ресурсу. Наприклад, для роботи програми на Apple Watch, потрібно оптимізувати швидкість, перша версія годин дуже, дуже повільна, а парсити доводиться природний текст, це займає суттєве час. Також, у версії 1.8, парсинг відбувається відразу на декількох мовах, щоб можна було продиктувати по-російськи, навіть якщо інтерфейс англійською (сам диктейшн не видає ніякої індикації про те, яка мова зараз використовується). Оптимізація під таке «погане» пристрій, покращує продуктивність і для інших, більш сучасних і швидких.

Щоб зробити Today Extension (зараз він вимкнений, так як глючить і погано працює), потрібна була жорстка оптимізація по пам'яті. Хотілося, щоб він умів парсити рядок з буфера обміну (а не просто показувати пару рядків), це потребувало цілком повноцінного додатка. Кумедний момент, що Apple говорить про обсяг пам'яті, доступний такого роду розширення. «Мало», кажуть. Скільки це — мало? «Чим менше, тим краще!» Ніяких конкретних чисел. Тому оптимізація по пам'яті — до межі.

Це все доводить вимоги щодо оптимізації до екстремуму. Доводиться застосовувати всі відомі техніки, придумувати нові структури даних (точніше, використовувати добре забуті старі), тикати паличкою в паралельність, уважно дивитися на те, що показує Instruments, час від часу викидати алгоритми, які гальмують, на користь більш складних, але і більш ефективних.

Я навіть раджу іноді, якщо розробляється додаток, взяти саме гальмівний пристрій, яке є, і запустити, поганяти на ньому. У мене спеціально для цього лежить і iPhone 4 і iPod Touch п'ятого покоління (там те залізо, що і в iPhone 4S). Перший майже неактуальний, а ось другий — буде актуальним ще рік-півтора (на нього постає все, включаючи останню на сьогодні iOS 9).

Техніка. Зовнішній вигляд
Про зовнішній вигляд я вже трохи писав. Наприклад, про правильне скруглення кутів можна почитати в моєму блозі, там є про тестування UI. Але є кілька моментів, які я поки не описував.

Клавіатура

Клавіатура в Ангстреме показується завжди, крім випадків, коли вона не показується. Вона зсувається (спробуйте посвайпить вправо від першого екрана), вона пропадає (підключіть зовнішню клавіатуру або приховування її на Айпад), і вона буває різна (Айфон/Айпад/Айпад Про). До неї також прив'язана наша цифрова клавіатура, яка буває вузька


Буває висока


Буває Айпадная


У версії 1.8 вона навчилася перемикатися, щоб вміти вводити шістнадцяткові або римські цифри. Підсумовуючи, там все складно.

Розповім я про дві речі. Як зрушити клавіатуру, і як зробити, щоб кипад (наша цифрова) працював у Accessibility.

Щоб зрушити клавіатуру, потрібно розуміти пристрій віконець в iOS. Для кожного додатка виділяється своє вікно (UIWindow), але якщо з'являються модальні діалоги або клавіатури, то кількість віконець збільшується. Якщо використовувати щось на зразок Reveal, ієрархія видно дуже добре:


На картинці (від далеких до ближніх) шари:

  • UIScreen
  • головне вікно програми
  • вікно, в яке система засовує кастомний клавіатуру
  • вікно клавіатури
Тут відразу видно (за це я і люблю 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 різні, вони проявляються в:

  • Відсутності клавіатури в модальних діалогах. Наприклад, якщо в Эбауте Ангстрема відкрити створення листа, в діалозі може не бути клавіатури (а може бути). Це було в старих версіях iOS, коли одна і та ж клавіатура використовувалася для всіх вікон. Поїхали одну, виїхали всі інші. Потрібно повертати.
  • Зрушених елементах. У тому ж діалозі створення листа, наприклад, може зсуватися
    UIMenuController
    .
  • У складних випадках роботи з клавіатурою (поїхала, здалася для модального контролера, повернулася) — клавіатура може пропасти. Мабуть, саме вікно клавіатури час від часу пересоздается і стара посилання втрачається.
Також будьте готові до того, що клавіатура може пропасти (або з'явитися інша). Наприклад, при купівлі розширеного набору одиниць — з'являється системна клавіатура для введення пароля. Після завершення процедури купівлі (як би він не завершився), потрібно повернути клавіатуру назад.

Загалом, насправді, рекомендація проста. Не чіпайте клавіатуру без приводу, нехай цим система займається. Інакше вляпаетесь, як я вляпываюсь, граблів там дуже багато.

Accessibility
Мені дуже хотілося, щоб наш кипад, нехай навіть і не виглядає, як стандартна клавіатура, вів себе схожим чином. Спочатку я спробував знайти, як відтворювати звук натиснутою клавіші. Радісний, я дізнався про
UIInputViewAudioFeedback
, і про метод
[[UIDevice currentDevice] playInputClick]
, який робить рівно те, що потрібно.

Після цього мені треба було підтримати accessibility, тобто роботу додатка, коли ним користуються користувачі з обмеженими можливостями. Для звичайних компонентів інтерфейсу немає нічого простіше. Задаємо для компонента кілька пропертей, і все.

self.isAccessibilityElement = YES;
self.accessibilityLabel = @"Кнопка зелена";
self.accessibilityHint = @"Натисніть, якщо хочете зробити добре";
self.accessibilityValue = @"Натиснута";

З кипадом все виявилося складніше. Малюю я його цілком, щоб простіше було намалювати фоновий градієнт, а в більш старих версіях і кути заокруглені.

Щоб підтримати нормально і клавіатурне поведінка (кліки), і все інше, довелося поверх намальованою клавіатури створити прозорі кнопки-спадкоємці
UIButton,
яким правильно прописати значення, і уважно стежити за тим, щоб вони змінювалися при зміні клавіатури (в останній версії з'явився введення римських і шістнадцяткових чисел).

Якщо тема Accessibility вам цікава, можу розповісти про неї докладніше. Або можна подивитися відповідні сесії з WWDC, вони дуже хороші: iOS Accessibility і Apple Watch Accessibility

Accessibility допоміг мені ще й при тестуванні, про що я детальніше розповідав у своєму блозі.

Питання?
Може, цікавлять якісь інші особливості подробиці реалізації? Запитуйте!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.