Стендап Сьогодні
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
Пости з тегом #СтохастичнийТаймтрекер
29.05.2024
Сповіщення на iOS: по-простому та по правильному
Повернувся до роботи над таймтрекером. Сьогодні вдосконалив, як працюють сповіщення-нагадування (чи не найважливіший компонент всієї системи.)
Взагалі, щоб відкрити застосунок за сповіщенням, робити нічого не потрібно. Окрім запланувати (або отримати) це сповіщення. Цим я й користувався до цього часу. Нагадування про пінг просто відкривали застосунок, а він на момент запуску зʼясовував, чи є актуальний на дану хвилину пінг, та показував в цьому разі форму.
Плюси — рішення тривіальне. Мінуси — якщо відкрити старе сповіщення, то застосунок форму не відкривав. Також нічого не відбувалося, якщо сповіщення приходить, а застосунок вже відкритий. (Хоча за три місяці такого не трапилось жодного разу. Статистично неймовірна подія!) Тобто, можна сказати, що це халтура, але для MVP мене цілком влаштовувало.
А правильне рішення це задекларувати делегат та обробляти відповідні події.
(В iOS обробка зовнішніх подій відбувається через модель делегатів. Кожне джерело подій — наприклад — центр сповіщень — оголошує інтерфейс делегату. Я створюю клас, який реалізує цей інтерфейс, та передаю його системі. Система викликає мій клас, коли це потрібно.)
Подій всього дві, та вони покривають всі мої потреби. Перша — для отримання сповіщень: як в момент запуску, так і коли застосунок вже відкритий. Друга — цікавіша — контролює, чи показувати сповіщення при відкритому застосунку. Так я можу обрати: показати власне інтегроване сповіщення, або просто залишити системне. Чим поки й скористаюсь.
30.01.2025
Покращення алгоритму пропонування для трекера
Завтра випускаю нову збірку трекера. Зміни доторкнулися головної частини — алгоритму пропонування тегів. Частково повернув ту логіку, що була до міграції на SQLite, а також додав нові круті речі.
Для початку, зробив, щоб по змахуванню модала редагування він зберігався. Раніше скасовувався. Скасувати все ще можна — кнопкою — але збереження набагато частіша дія. (Ну та й звісно, якщо нічого не змінювати, то нічого й не збережеться.) Також повернув функцію “скопіювати з минулого”, яка зникла після редизайну.
Але головне — це покращений алгоритм пропонування. До того він спирався тільки на час (тобто пропонувалися теги, що були відмічені раніше в ту саму годину та день тижня.) Бо це просто легше було запрограмувати на SQL. Зараз, якщо вже є обрані теги, то я вибираю з бази суміжні з ними. До речі, хоч через це підбірка тегів постійно змінюється, завдяки анімації наочно зрозуміло, що пішло та що додалося — тому ти не губишся в цих змінах.
Я почав з того, що витягував суміжні теги до кожного обраного, та поєднував за частотою. Але це дає дивні результати; умовно якщо обрати теги desktop, coding
, то в пропозиціях будуть gaming
як суміжний із desktop
та laptop
(!) із coding
.
Тому треба було знаходити теги, суміжні до всіх обраних відразу. Що не так легко, бо тегування зберігаються в табличці tagging
- знайти суміжний до одного тегу це один join
, але якщо до декількох — не плодити ж джойни? Винайшов чудове рішення, що знаходить всі пінги, де зустрічалися всі обрані теги:
SELECT ts, count(*) cnt
FROM tagging
WHERE tagId IN (1,2,3)
GROUP BY ts
HAVING cnt = 3
Залишається результат заджойнити назад до tagging
та вибрати всі теги, що збігаються за часом відмітки.
PS. Після роботи дуже важко перемкнутися на власний проєкт. Голові потрібно відпочити, а коли відпочинеш — вже пізно щось починати. Тому я для себе знайшов, що краще займатися власним проєктом першим чином рано вранці. Тоді до початку роботи встигаю відпочити.
31.01.2025
Ping - сайт
Як і планував, нова версія вже у TestFlight. Також нарешті дістався до створення сторінки для застосунку: ping.leonid.codes. Поки сторінка майже порожня, але на з цим продуктом є стимул її розвивати.
Саму сторінку я скопіював із останньої такої задачі. Взагалі гарно б було натренуватись робити мінісайти для (менших) проєктів, бо інакше вони часто залишаються без всякого маркетингу.
А маркетингу воно знаєте, скільки потрібно? Ще б дорожню карту кудись викласти, та сторінку в соціальних мережах вести, та хоч мінімальний, але набір статей з поясненнями. Та відео використання. В мене традиційно немає на то хисту, а коли і є хист, то немає часу.
Про час: відзначу, що розгортування на Cloudflare Pages зайняло лише пару хвилин. Це з урахуванням того, що мій домен вже під Cloudflare. Бо завдяки цьому він сам робить всі налаштування, достатньо підʼєднати репозиторій та вказати піддомен.
01.02.2025
Робота з iCloud Drive та резервне копіювання
🛟 Сьогодні намагався (та зробив!) виконати ще одне прохання користувачів: додати резервне копіювання бази (таймтрекеру.) Функція очевидна, та ще й врятує мене, якщо раптом випущу збірку, що псує дані. Звісно, як місце для збереження копій я обрав iCloud Drive. Бо він вже доступний для всіх без зайвої авторизації. Отже.
З погляду застосунку, iCloud це майже звичайна файлова система. На iOS найскладніша на моїй памʼяті модель роботи з файлами — доступ порізаний на окремі теки, все відбувається через URL тощо. Але принаймні, коли ти вже знаєш URL контейнера, то далі все як звичайно.
А от процес налаштування заплутаний та погано задокументований. Ось стаття, з якою все нарешті вийшло. Спочатку ми оголошуємо “повсюдний контейнер” - це як скибка сховища користувача, яка буде належати нашому застосунку (та яку він буде бачити). Контейнер існує окремо від застосунку, та навіть декілька застосунків можуть ділити один контейнер.
Потім… потім воно працює з коду (ну, не на симуляторі, якщо тільки не увійти там у свій iCloud). Але в iCloud Drive файлів я не бачив. Було важко зрозуміти — що саме не так. Для iCloud Drive цей повсюдний контейнер потрібно додатково оголосити публічним та призначити йому імʼя - після чого… ні, все ще нічого не відбувається.
З вище вказаної статті збагнув, що щоб файли зʼявилися у Drive, вони повинні бути в піддиректорії Documents
, а не прямо в контейнері. Тоді… ще потрібно було збільшити версію збірки, і ось нарешті тека з файлом зʼявилася і в Files на айфоні, і невдовзі на макбуці теж. 😮💨
Решта - справа техніки; експорт у ZIP в мене давно вже є. Трохи порефакторив, та додав виклик на відкриття застосунку: якщо з попереднього експорту минув день, створюємо ще один. Можна ще потім додати чистку старих файлів, але то менш критично - мій експорт майже за рік займає 277 Кб.
30.04.2025
SwiftUI - рефакторинг та оточення
Сьогодні додав маленьку фічу до свого стохастичного таймтрекера — а саме, можливість передивитися всі пінги, які відмічені конкретним тегом. Що корисно, щоб згадати минуле. Щоправда, я досі не зробив перегляду більше за 50 пінгів, бо для того потрібне ліниве завантаження — тож не дуже далеке минуле.
Фіча звучить зовсім простою. Запит для останніх пінгів вже є. Додаєш до нього фільтр по тегу (до речі, із CTE) та виводиш списком. Але є й проблема.
Натискання на пінг відкриває форму редагування. Така можливість, звісно, потрібна й на новому списку. Ця форма не така проста річ — бо вона є модальним вікном, та на екрані ніколи не повинно бути двох форм. Тому її стан є глобальним.
Цей стан в мене поки зберігався просто в @State var formModel
на структурі застосунку, та передавався глибше через @Binding
. Цього разу я прикинув, через скільки шарів доведеться прокинути @Binding
та вирішив, нарешті, переробити.
У SwiftUI є можливість, схожа на контексти в React - це Оточення, @Environment
. Нею можна передати вглиб структури презентації якесь значення. Але — саме значення, не змінну, редагувати його не можна.
Або… можна? Спочатку я спробував передавати функцію-сетер, setFormModel
. Це спрацювало! Далі пошукав більш ідіоматичне рішення та знайшов, що взагалі-то в @Environment
можна передати Binding(FormModel)
, та тоді його можна буде призначати через formModel.wrappedValue = newFormModel
.
(Binding
, який лежить в основі моделі стану SwiftUI, як раз і є абстракцією над гетером та сетером, яка загортає змінне значення у незмінну обгортку.)
Код став гнучкіше та чистіше. До речі, в останньому SwiftUI цей @Environment
значно спростили, так що якщо давно не бачили — подивіться.
🕜🏷️ Нагадаю, що я пишу Ping - застосунок на iOS для нетрадиційного обліку часу. Якщо цікаво, до бети можна доєднатися тут.
01.10.2025
Автовідповідач для Ping
Після нещодавнього досвіду Shortcuts для експорту нагадувань з Apple Reminders - який, до речі, вже тиждень як зберігає мені щоденні резервні копії — зʼявилася ще одна ідея. (А експорт вже є в опублікованому застосунку).
Нагадаю, що я розробляю стохастичний таймтрекер Ping. Він питає у випадкові моменти — чим я займаюсь? З деякими заняттями це дуже зручно, а з іншими — просто ніяк не зручно. Наприклад, за кермом я очевидно нічого не відмічу. А потім залишаються сірі ділянки часу.
Я шукав різні рішення для автотегінгу. Наприклад, час сну та час тренувань Ping вміє читати з Apple Health. А далі що? Далі починалися різні складні імпорти та інтеграції незрозуміло з чим незрозуміло як. Поки я несподівано не згадав про Shortcuts.
Отже, функція “автовідповідач”. Суть проста — можна в будь-який момент зайти в Ping, та вказати теги “на майбутнє”. Далі застосунок буде проставляти ці теги на кожний пінг, аж поки автовідповідач не буде вимкнений.
Сама по собі така функція не дуже зручна. Зате якщо поєднати її з Shortcuts, то вона перетворюється на потужний механізм автоматизації! Наприклад: зробити скрипт “коли сідаєш в машину -> увімкнути автовідповідач” та “коли виходиш -> вимкнути”. І все! Задача вирішена. Так само можна вмикати за режимом фокуса, за локацією та іншими тригерами.
Обмеженням є те, що все ж тригерами можуть бути тільки системні події, а не такі що відбуваються в інших застосунках. Тож Shortcuts не є повним аналогом IFTTT чи Zapier, а шкода.