Стендап Сьогодні 📢 Канал в Telegram @stendap_sogodni

🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!

Пости з тегом #SwiftUI

04.02.2024

Стохастичний тайм-трекінг на SwiftUI

Сьогодні наполовину для розваги, наполовину для діла спробував зробити на SwiftUI реалізацію одного особливого тайм-трекера. Особливий він тим, що, замість ручного ведення журналу чи автоматичного збору даних, трекер питає тебе, чим зараз займаєшся. А щоб це не було передбачуваним — інтервал запитів буде стохастичним, тобто випадковим. Інколи через пʼять хвилин, інколи через дві години. Випадковість моменту робить такий трекер неупередженим (якщо завжди заносити саме те, що робиш зараз.) На довгому проміжку часу статистика дає правдивий розподіл часу.

Це все не я придумав — йдеться про TagTime - зроблений чимало років тому розробниками сервісу Beeminder. TagTime має вигляд купки скриптів на Perl та, відповідно, здатний працювати тільки на десктопі.

А в мене ідея зробити застосунок для Apple Watch та скористатись повсюдністю годинника. У WatchOS є така цікава можливість. як long look для повідомлень — фактично цілий інтерактивний екран, доступний безпосередньо при перегляді сповіщення.

До того ж використання бази даних CloudKit та універсальності SwiftUI дозволяє легко зробити застосунок, в який можна заносити з десктопу, з телефону чи з годинника — де зручніше. А інтеграція Apple Health може автоматично логувати сон та тренування. Причому виглядає так, ніби все це не потребує багато зусиль, динамічного програмування і так далі. Ну, подивимось.


30.11.2024

Сховище даних для таймтрекера

Я зараз готуюся до серйозної роботи над застосунком для таймтрекінгу, та напевно почати доведеться з перероблення моделі та сховища даних. Бо SwiftData/CoreData мене все ж бісить. Вона, мабуть, гарна для простого випадку, коли все що потрібно — це показувати екран списку та екран запису. Тоді SwiftData інтегрується безпосередньо у SwiftUI. Але як тільки зʼявляється складніша логіка — як-от агрегація, розрізи, особливо із застосуванням великого обсягу даних — то вона починає тріщати. Тому дивлюся на альтернативи.

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

Логічною серединою мені бачиться SQLite; зокрема, всіляку статистику можна обчислювати за допомогою SQL, а в памʼять завантажувати тільки те, що буде видно. У Swift є декілька бібліотек для SQLite, я поки схиляюся до Lighter, бо вона надає типізовані записи, але в іншому не перетягує абстракціями.

До речі, в базі CoreData взагалі немає агрегацій. Бо це обʼєктна база; хочеш агрегацій — будь ласка, ходи по графу асоціацій. Наприклад, така тривіальна в SQL задача, як “покажи найбільші 10 тегів за кількістю використань”, тут вимагає зберігання кількості в атрибут-кеш, інакше буде повільно та витратно. І це ще проста задача; а як щодо “покажи карту використань тегу за годинами та днем тижня”? В такому важкому на статистику продукті з SQL піде веселіше.


09.12.2024

Дев-адвент 9: кольори та навігація дерева

🌳 Продовжив роботу над деревною мапою. Вже бачу, як цей компонент можна пристосувати, наприклад, для аналізу конкретного тегу. Також, поки зовсім без контролю вкладеності все ж незручно, бо за моїм алгоритмом вкладеність залежить від розміру тегів в даному тижні, а значить, від тижня до тижня вона схильна змінюватись непередбачувано. Думаю, буде в тегу шкала “догори /вище / середнє / нижче / донизу”, яка впливатиме на вибір вкладеності.

🎨 Щоб там не було… Додав таки кольори. Чесно скажу, спочатку я хотів обирати колір з емодзі. Знайшов декілька бібліотек, які вміють це робити (хоч і на JavaScript). Внутрішній програміст так і рвався реалізувати такий нівроку розумний алгоритм. Проте я розглянув приклади в тестері, та зрозумів, що кольори з емодзі не будуть достатньо різноманітними. Тобто це цікавий спосіб додати краплю гармонічного кольору, але ніяк не для розрізнення тегів у звіті.

🎲 Тому повернувся до випадкових кольорів (звісно, з можливістю редагування.) Формула гармонічних випадкових кольорів в мене вже є. Виходить теж не ідеально, зате зусиль встократ менше.

🪜 А навігація за деревом працює так: за тицем в мапу спочатку знаходжу, в який регіон він відбувся (бо мапа — це Canvas.) Якщо це тег з вкладеністю, то зберігаю його в масив “шлях” та переобчислюю мапу, але вже для вкладених тегів. Без анімацій поки грубувато, але працює.

🖋️ Наступним кроком буде, певно, доповнення мапи текстовою інформацією: фактичними цифрами, для початку.


18.12.2024

Дев-адвент 18: життєвий цикл у SwiftUI

😵‍💫 Розбирався з ситуацією, що після зміни налаштувань не оновлюються запити, які ті налаштування зчитують. (Не з бази зчитують, а зі змінних.) Для того довелося все ж вивчити життєвий цикл компонент у SwiftUI. Знайшов прегарну книжку Thinking in SwiftUI (це справжній “посібник, який забули покласти”!) та з її допомогою вдалося второпати.

(Примітка. Я звик до реактивного UI в моделі React, особливо — з хуками. Все ще вважаю, що цей API, разом з лінтером для Rules of Hooks, найкращий у своєму класі. Там в мене ніколи не стає питання, що саме буде виконано при рендері, коли будуть викликані хуки, та який стан є тимчасовим, а який — сталим. У SwiftUI не все так гарно.)

😶‍🌫️ У SwiftUI компоненти є нащадками структури View. Недосвідченому у Swift програмісту може здатись, що наявність структури робить компонент сталою сутністю, бо це “щось на кшталт класу”. Але така інтуїція неправильна, бо навпаки, структура у Swift - це “тип-значення”, а відповідно, нащадки View не здатні зберігати стан.

🏭 Натомість кожен View є “фабрикою” для компонент. Центром її є атрибут body - він аналогічний до функціональної компоненти в React. А все, що оголошено поза body, є допоміжною логікою. Головним чином, ця логіка встановлює привʼязку до стану. Як і в React, стан зберігається… “десь там”, а ми до нього можемо звернутися — обгорткою @State тощо. Важливо памʼятати, що весь зміст View - це тільки аргументи до функції рендеру body, та вони не є сталими.

Ось такий цікавий патерн Swift - структура є фактично замиканням навколо функції та її параметрів. А тільки що відкрив вікіпедію та побачив, що таке використання буквально називається замиканням! А я-то думав, що замиканням може бути тільки функція.

💣 А тепер, підступ. Під час виконання конструктора View ще не має доступу до реального стану! Бо це ж тільки фабрика будується, а не компонент. Тому в конструкторі можна задати тільки початковий стан @State, або, наприклад @Query з GRDB. Це зовсім не очевидно, бо конструктор дійсно викликається на кожну перемальовку, та в ньому можна присвоїти значення для змінної @State, але працювати воно не буде. Та ані компілятор, ані лінтер не попереджають.

🪨 В моєму випадку з запитами знайшовся режим Query(constant:in:). В ньому, як я це розумію, замість стану, який можна змінювати, запит стає “константою” - а це значить, парадоксально, що коли його параметри в конструкторі змінюються, то й результат теж — а не тільки початковий стан. Таке.


19.12.2024

Дев-адвент 19: дерево, тепер з анімацією

Сьогодні замотався з роботою, хотілося зробити щось веселе — а що може бути веселіше анімації? Занурився в анімаційні можливості SwiftUI та… щось навіть вийшло.

Як можна побачити зверху, я придумав ось такий спосіб навігації деревом: обираєш тег, він збільшується — але зберігає пропорції. Так легше відстежити звʼязок даних, ніж коли кожний вкладений тег розтягується (як тут) на весь екран. Тепер достатньо один раз викласти дерево, а вкладені рівні малювати суто через лінійні перетворення. Далі почалася сувора 2D-графіка без нормальних векторних типів…

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

Найскладнішою частиною було правильно виконати перетворення, щоб тег зʼявлявся посередині екрану. Я обчислюю та запамʼятовую для кожного рівня зсув та масштаб відносно базової викладки (яку отримав ще на самому початку.) Їх потрібно врахувати й для малювання, й під час обробки натискань.

Це все вже нормально працює, а ось анімацію я тільки почав робити. Для неї є гарний компонент TimelineView - він перемальовує власний зміст за розкладом, хоч кожного кадру. Залишалося тільки досипати до моєї векторної алгебри ще й обчислення проміжного значення між “маленьким” та збільшеним розміром тегу. А ще проміжного значення прозорості фону. А ще лінійний перехід виглядає абсолютно ненатурально, та треба робити easing

Ото сиджу тепер та думаю, марно це все було. Треба було кожний шар робити окремим Canvas, позиціювати та масштабувати їх засобами SwiftUI, та анімувати їх теж через SwiftUI, як я колись робив пейджинатор. Тоді брудної роботи було б значно менше.


20.12.2024

Дев-адвент 20: анімація з Transition

Так, дійсно, анімацію принципово легше зробити через вбудовані засоби. А саме .animation - для змін стану, та .transition - для появи та зникнення елементів. В моєму випадку, оскільки нові шари мапи саме зʼявляються, то тут потрібний .transition.

Порівняно з React+CSS, мені подобається, що видалений елемент автоматично зберігається аж доки не відпрацює перехід. Просто додаєш/прибираєш елемент з промальовки, та сам запускається перехід. Навіть помітив, що якщо на кінець переходу він не зникає з екрана через прозорість або позицію, то некрасиво зависає ще на пару секунд.

Для конфігурації Transition задається “активний (чомусь) стан”, а насправді стан “неіснування”. От в моєму випадку тег зʼявляється зі своєї позиції в попередньому батьку, тому код переходу виглядає так:

.transition(
    .offset(pathElement.offsetOfOriginalPosition)
    .combined(with: .scale(pathElement.scaleOfOriginalPosition))
    .combined(with: .opacity))

З хорошого — що в основі це працює чудово, навіть без всіх особливих налаштувань. З поганого — що все ж доводиться обчислювати відносне позиціювання та розміри, бо нутрощі мапи я малюю все ще через Canvas (а кожен рівень навігації є новим Canvas, з власними вкладеними тегами.) Ну й ще можна полірувати параметри анімації… нескінченно.


24.12.2024

Дев-адвент 24: форма додавання проби

Освіжив форму додавання проби — ліворуч те як вона виглядала вже багато місяців, праворуч перша ітерація покращень.

(Взагалі, за досвідом, ця форма з усіма тегами ледве влазить в екран телефону. Особливо якщо відкрити клавіатуру — тому в мене зараз пошук тегів відкривається окремим модалом. Наступним кроком хочу його прибрати.)

PS. Найважче в реалізації дизайнів на SwiftUI - це знати інструментарій. Мабуть, так само як і з CSS. Бо коли не знаєш, наприклад, що можна отримати системний колір третинного фону як .background.tertiary, або як все ж працює викладка елементів (зовсім не як в CSS!), то виходитиме складно та незграбно.


25.12.2024

Дев-адвент 25: пошук тегів з форми

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

Головна проблема цієї форми — їй дуже тісно. На телефоні мало місця. Особливо коли відкриєш клавіатуру — чого на відео не видно, бо то на емуляторі. Тому в мене пошук був окремим екраном, де йому вже була воля.

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

На те спробував поки що заміняти блок “ймовірних тегів” результатами пошуку. (А ще — ховати — з анімацією! - попередній та наступний теги, бо вони тут зайві.) Та обмежити результати за кількістю. Та показувати теги “потоком”, а не по одному на рядок. Наче непогано — хоча сам UX пошукового рядка ще треба доробити — щоб було легко очистити.

Наступне, що тут треба переробити — це логіку обирання отих “ймовірних тегів”; хочу агрегувати дані про суміжність тегів, щоб знайти “ідеальне поєднання” між наступними, попередніми, та спільними тегами до тих тегів, що вже відомі.


26.12.2024

Дев-адвент 26: цілісність дизайну та покращення пошуку

Нарешті взявся за остаточне впорядкування дизайну (ну, наскільки ці слова можна застосувати до цього MVP.) Якщо тег — це сірий округлий прямокутник, то таким він повинен бути всюди: тоді ясно, що це тег. Так само проби — більші округлі прямокутники з кольоровою крапкою: пропущено / заповнено / заповнено, але пізніше. Сюди можна витратити необмежену кількість часу, тому поки мета почати.

🎨 Відчуваю, що треба було б зробити палітру стилів, а не робити зміни там-сям, проте цим займуся пізніше. (У SwiftUI “палітра стилів” може існувати у вигляді набору модифікаторів.)

🏔️ Плюс чим далі, тим більше покращення UX тягнуть за собою додаткові покращення. Наприклад: є в мене автотегінг з Apple Health. Раніше було важко побачити його роботу в реальному часі, і це було… нормально. А тепер на головній є блок “незаповнені проби”, в якому видно, як стрибають спочатку порожні, а потім автозаповнені сном проби. Щоб було зрозуміло, тепер доведеться додати блок “автотегінг” з його результатами. І це гарно! Тільки роботи більшає.

🫥 На відео зверху видно новий UX додавання тегу. Просто до кінця списку результатів додаю пунктиром тег, який можна створити та відразу й додати. Також результати пошуку тепер спочатку показують теги зі збігом на початку (бо зазвичай саме так ми їх вводимо), а потім вже решту збігів.

🎅 Якщо все буде добре, планую до 1 січня все ж випуститися хоча б в TestFlight.


30.12.2024

Дев-адвент 30: TipKit

Сьогодні накидав більше порад з TipKit та продовжую бути ним вражений. До речі, ось гарне викладення всіх можливостей, а я поки поділюся власними думками.

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

Відразу хочеться розставити ці поради просто на кожному кроці. Принаймні для онбордінгу це навіть має сенс — бо замість традиційних слайдів (які мені робити та підтримувати буде важко) я вкраплю інструкції прямо в інтерфейс. Та як окрема перевага - інтерфейс не буде надто порожнім.

Наприклад: замість типового слайду “дозволь нам надсилати сповіщення” показую пораду на кшталт: “тут ти бачиш крайню пробу, але вона вже минула… а щоб побачити наступну вчасно, увімкни сповіщення”. Або “ось 10 порожніх проб за ніч… якщо дозволити доступ до Apple Health, ми їх автоматично протегаємо.” Органічно та зрозуміло.