Стендап Сьогодні
📢
Канал в 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 для нетрадиційного обліку часу. Якщо цікаво, до бети можна доєднатися тут.