IT-инфраструктура для бизнеса и творчества
Разработка
Denis K

Как я потратил полгода на обновление приложения и потерял 10 тысяч пользователей

Всем привет! Меня зовут Денис Кравцов. Я разработчик приложения Smart Timetable — расписания для школьников, студентов и преподавателей.

Мне как начинающему iOS-разработчику после изучения теории и примеров захотелось создать маленькое приложение для публикации в сторе. Самым простым и очевидным приложением обычно является тудушка (от англ “todo”). Я не стал нарушать эту традицию.

Но тудушка в моём случае — это расписание занятий. Основное отличие расписания от тудушки — регулярность событий. Простое добавление событий в список, с датой или без, не подходит.

Поскольку в iOS-разработку я пришел из веба, то первая версия очень напоминает веб-страницу с формой для ввода данных. Профдеформация налицо :))

Первая версия вышла в далеком 2017 году. Тогда для меня было важно не столько сделать приложение, сколько пройти весь путь от кода до публикации приложения. Ведь все знают, что разработчикам приходится не только писать код, но и заниматься дистрибуцией приложения в App Store.

C 2018-го по март 2019-го было несколько итераций с добавлением уведомлений, четной и нечетной недель, поддержки Apple Watch и прочих мелочей. Несмотря на простоту приложения, за полтора года набралось 50 тысяч пользователей.

Для приложения была выбрана концепция одного из подобных приложений в сторе. Она заключалась в табличном представлении всей недели через UITableView. Это удобно до какой-то степени, но никак не масштабируемо. На этом мой энтузиазм на время затих.

Летом 2019 года появилось желание сделать нечто большее, чем простую таблицу. Была изучена масса комментариев у приложений-конкурентов как в AppStore, так и в GooglePlay. Собрался список фич, которые нужны пользователям, но которых нет среди других приложений или они есть в зачаточном состоянии.

Что получилось собрать:

  • студенты хотели заводить несколько расписаний: для учебы, досуга, спорта; для родителей это также оказалось актуально — чтобы планировать свои дела и быть в курсе расписания детей;
  • для заданий нужны файлы, хотя бы возможность добавить фотографии;
  • таймер оставшегося времени до конца урока;
  • синхронизация между устройствами;
  • нужен простой способ поделиться расписанием;
  • горизонтальная ориентация экрана для просмотра занятий на всю неделю;
  • есть специфические расписания, которые не повторяются от недели к неделе, а имеют собственный период, например четырех-, шести-, восьмидневная неделя.

Специфические расписания мы в команде обозвали кастомной неделей. Как это понимать? Например четырехдневная неделя означает, что первые четыре дня расставлены с понедельника по четверг, а пятница это снова первый день. При этом выходные переносят расписание, следующий понедельник — это второй день.

Кроме выходных расписание также переносят праздничные дни. Расписание кастомной недели вычисляется так: пн(1)-вт(2)-ср(3)-чт(4)-пт(1)-сб-вс-пн(2)-вт(праздник)-ср(3)-... и так далее. Такие расписания, как оказалось, очень популярны в школах США и Канады. Их называют rotation schedule.

На реализацию всех хотелок у меня ушло полгода. Полгода, Карл! Почему так? Этому есть несколько причин:

  • я человек семейный с двумя детьми, у кого есть дети, тот поймёт;
  • работу в офисе никто не отменял, нужно пилить приложение в свободное от работы время, обычно ночью;
  • поначалу не было спринтов и дедлайнов, только планы сделать вот это и это;
  • приложение переписывалось с нуля;
  • нужно не просто сделать, но и хорошо изучить сферу расписаний;
  • трудно вычленить из большого плана что-то маленькое. MVP планировался достаточно фунциональным, поскольку первая версия уже была выпущена ранее.

Сделать как можно больше и сразу хотелось потому, что если делать итерационно, например каждую неделю выпускать по фиче, то это легко повторяется конкурентами. Я же преследовал цель «появиться на свет» достаточно уникальным, с запасом времени на продвижение.

Разработку новой версии я начал в июле 2019 года с планами запуститься сразу после выхода iOS 13 в сентябре. Но реально получилось выпустить приложение только в январе 2020 года. Не зря говорят, что при планировании разработки ПО срок реализации нужно умножать на число пи, так оно и вышло.

Средние сроки на реализацию

  • алгоритмы подсчетов недель, дней кастомной недели — две недели;
  • настройки расписаний — полтора месяца;
  • экран занятия — две недели;
  • экран задания и файлы — месяц;
  • бесконечный свайп, календарь — две недели;
  • виджет Сегодня с таймером — две недели;
  • минимальный продукт для Apple Watch — два дня;
  • минимальный продукт для iPad — неделя;
  • онбординг, возникший как необходимость после бета-тестирования, — две недели;
  • тестирование — на протяжении всего периода разработки с участием десяти бета-тестеров.

Перед запуском

Когда стало понятно, что в сентябре запуск никак не состоится, запуск был перенесен сначала на ноябрь. Потом на декабрь. А потом на 31 января.

До ноября 2019-го задачи заносились в Redmine, а также в чат Telegram. Гораздо веселее дело пошло в конце ноября, когда задачи стали разбиваться на спринты (мы их назвали билдами) и заноситься в Trello. На каждый билд отводилась одна неделя.

Даже если вы единственный разработчик в команде, вам нужен таск-трекер. Это помогает видеть и четко понимать стоящие перед вами задачи.

Начали мы с билда 43, поскольку текущая версия приложения на тот момент была 4.3. И так уж совпало, что билд 50 пришелся на релиз версии 5.0. Теперь номер билда это фактически версия приложения после деления на 10. На момент написания статьи в работе билд 58 для версии 5.8.

От некоторых задач пришлось отказаться и отложить их на «после запуска». Среди таких задач были версия под iPad, виджеты на циферблат Apple Watch, экспорт расписания.

После запуска

Выяснилось, что валидация чека для проверки активности подписки работает как-то не так. Пользователи сразу начали жаловаться на то, что после оплаты Pro-версия всё так же недоступна. На бета-тестерах это отражалось, но в бета-среде не всё можно протестировать. Например, нельзя протестировать отказ от подписки/покупки. Ну раз написано, что должно работать, то в продакшене точно заработает. Ага, конечно. Версия 5.0.1 содержала багфиксы по проверке чека.

Поскольку я переписывал приложение с нуля, с принципиально новой функциональностью, создавать «мост» между старой и новой версиями не планировалось. Это означало, что расписание не будет перенесено. Кому понравится, что его данные после обновления слетят. Мы (разработчик, ASO-шник и тестировщик) морально готовились к этому негативу со стороны пользователей.

Причина такого шага проста: не было гарантии, что миграция пройдет успешно. Старые алгоритмы подсчета четной и нечетной недель работали, так сказать, через раз. Поэтому лучше отсутствие расписания, чем расписание, которое может «обмануть» пользователя. Встречались комментарии в других приложения о том, что человек приходил не на то занятие по причине сбоя в работе приложения. Мы ставили на то, что создать расписание в новой версии гораздо проще и быстрее.

Расписание полностью удалилось при обновлении. Смысл использовать это приложение пропадает, если разработчики будут каждый раз «стирать» сохраненную информацию. Учитывайте это при установке приложения!

by ahifkggj
★☆☆☆☆

Из-за того, что прежнее расписание не было сохранено, негативные оценки и отзывы, поступающие сразу после обновления приложения, повлияли на рейтинг. Отзывы и оценки друзей не помогли :). Средний балл по миру с 4,7 упал до 4,5, по России с 4,8 до 4,6. Через некоторое время ситуация выровнялась и средний балл поднялся до 4,6 и 4,7 соответственно. Отзывы в основном касались резкой смены интерфейса и потери расписаний.

До обновления все прекрасно и корректно работало, но после обновления данные слетели! Все! А они были очень важными и дубликата нет! Это очень расстраивает и злит. 1 звезда на эмоциях

by malinkaelinka
★☆☆☆☆

По сути пользователи устанавливали раньше совсем другое приложение и обновлением были, мягко говоря, недовольны. Написали отзыв, поставили низкую оценку или написали по обратной связи около 80 человек. Остальные молча удалили приложение. Мы потеряли около 20% пользователей, это 10 тысяч в численном представлении.

Через некоторое время страсти поутихли и отзывы стали положительными.

Это приложение реально находка, новое обновление в особенности понравилось. Также хочется отметить службу поддержки, которая мало того, что быстро реагирует и пытается решить вашу проблему, так ещё и мило ведёт с вами беседу! Однозначно 5 звёзд!

by dixset3
★★★★★

Спасибо пользователям за отзывы.

Единственное приложение, которое полностью меня устроило своим функционалом и визуалом. Большое спасибо за старания и труды🙏🏼✨

by ArgonIC
★★★★★

Впереди еще был огромный объем работы: горизонтальная ориентация расписания на iPhone, версия под iPad, приложение под Apple Watch, Web-версия расписания. На момент выхода статьи всё это реализовано, но для запуска 5.0 решено было этого не делать для экономии времени. На доработку всего запланированного после запуска ушло еще два с половиной месяца.

Сложнее всего оказалось сделать версию под iPad и экспорт через PDF. Если с iPad всё более-менее ясно, то PDF — это рисование точек и линий по координатам. Кто занимался подобным в универе, тот в теме. Такое рисование лишено привычных constraint и вертикального выравнивания текста. Нужно вычислять предполагаемую высоту текста и делать отступы внутри контейнера.

Возникшие трудности в разработке новой версии

1. Валидация чека

Не работает так, как это описано в документации Apple, а именно: чек то есть, то его нет, то покупки не сразу появляются. Гуглил на эту тему и похоже, что в iOS 13 что-то изменилось или я что-то делаю не так.

Решение — сохранение данных транзакции и периодическая проверка чека. Валидация чека актуальна, если у вас есть подписки. У меня Pro-версия доступна по подписке $0,99 в месяц или после покупки за $4,99. Для обычных покупок проще и быстрее хранить лишь признак оплаченной транзакции. Но здесь есть нюанс — без проверки чека вы будете предоставлять премиум-доступ даже в том случае, когда пользователь вернул средства от Apple.

2. Лимит на количество уведомлений в очереди центра уведомлений

Я ожидал, что переполнение очереди вызовет ошибку. Но это не так. Можно добавить хоть сотню уведомлений и все они будут добавлены со статусом success. После лимита в 64 записи все последующие будут перезаписывать существующую очередь. Выход: при каждом входе в приложение пересобирать уведомления на ближайшие N событий. А ведь когда расписаний несколько, то и количество уведомлений растёт как на дрожжах.

3. Расписание не содержит дату, а лишь время и день недели

Решение в том, чтобы начинать перебор дней недели от текущей даты с подстановкой времени занятий. Регулярность уведомления isRepeat=true не подходит, так как занятие можно однократно удалить или добавить в произвольный день.

4. Синхронизация данных

Поскольку мобильная разработка подразумевает работу приложения в онлайне и офлайне, вопрос синхронизации не так прост, как кажется. Не использовал iCloud, потому что есть планы кросс-платформенной синхронизации между iOS, Android, вебом. Для каждой модели добавлены поля deleted, ts, чтобы понимать, когда что удалено, изменено, добавлено. На основании этих данных делаю merge данных при появлении сети. Что обновлено позже, то имеет приоритет. Если на любом шаге было удаление, то все остальные изменения игнорируются.

5. Несколько расписаний

Каждое расписание хранит свою конфигурацию настроек. Это несложно. Сложность состоит в понятном представлении настроек, переключении расписания, поддержке уведомлений по всем расписаниям. Переход из уведомления должен переключить интерфейс на нужное расписание, дату и занятие. Часы и виджет показывают расписание, выбранное на телефоне, ибо переключение здесь было бы избыточным.

6. Отказ Firebase в некоторых регионах

Пользователь из Крыма пожаловался на то, что по Wi-Fi не может отправить расписание. Хотя по сотовой связи всё ок. Поддержка Firebase ответила, что есть некий список регионов, который находится под санкциями. Был удивлен тем, что Беларусь по какой-то причине также в этом списке. Полный список можно посмотреть здесь.

Планы

  • Адаптация приложения под правосторонние языки (иврит, арабский).
  • Организация поддержки пользователей.
  • Подача приложения на фичеринг.

Ресурсы

Контакты

(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ { name: 'camera', url: `${cdnUrl}CameraCat`, text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', link: 'https://vc.ru/selectel/306690', num: 3 }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', num: 1 }, { name: 'cloud', url: `${cdnUrl}CloudCat`, text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', link: 'https://vc.ru/dev/294799-maneki-neko', num: 2 } ] let buttonCycle = document.querySelector('.button--cycle') let buttonChoose = document.querySelector('.button--choose') let buttonMobile = document.querySelector('.button--mobile') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) buttonChoose.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) buttonMobile.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { sendEvent(`Promo ${articles[currentArticleNumber].num} Right`, 'Click') if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?3' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } const sendEvent = (label, action = 'Click') => { const value = `SelectelDIY — loc: Footer — ${label} — ${action}`; if (window.dataLayer !== undefined) { window.dataLayer.push({ event: 'data_event', data_description: value, }); } }; function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите научим?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg?3`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
10 комментариев
Популярные
По порядку
Написать комментарий...

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

18

Приложение имеет аудиторию и позиции в сторе. Запуск нового приложения не панацея. Большинство 80% приняли это нормально. В итоге после удалений пришло много новых пользователей. Работаем с ними 🙂

1

Просто в маркетинге есть часть аудитории , которая тяжело принимает любые новинки и «переходы» и пока ей прямо не скажут 100 человек, что там круче или на новом функционале (так делает Гугл всегда, даёт и старую и новую версию, но в новой даёт возможностей больше и сразу об этом говорит- новые инструменты и тд), у тебя тупо выхода нету и ты пилишь на новый интерфейс.

0

Трачу время и деньги на вбивание своих задач в приложение(никогда не понимал зачем это вообще нужно - это еще никому в мире не помогло от безделия, тк это оно и есть, ну да ладно), и так трачу время и деньги на вбивание своих тасков->их все удаляют подчистую без предупреждения -> это норм?? люди бывают очень не довольны даже когда что то что было бесплатным становится платным, но просто брать и удалять чужие данные это просто апогей

5

Вы говорите 80% приняли нормально исходя из того, что только 20% удалили? Скорее всего, 100% приняли плохо и аж 20% пошли на такой шаг, чтобы удалить. Но ничего, у всех бывают ошибки, статья в целом интересная.

1

Удалять пользовательские данные без возможности восстановить их, и как я понял без предупреждения — очень стрёмная идея. Я понимаю все обоснования из статьи — но это не объясняет почему не было никакой попытки смягчить ситуацию. Уж хотя бы можно было обновить "впустую" старую версию на всё ту же старую, и вывести уведомление "пожалуйста заскриньте хотя бы расписание своё, оно удалится скоро, но это одноразово". Я бы после такого обновления оказался среди тех 20%, что приложение удалили.

7

Поддерживаю, но вы верно подметили. Если взять, например, какой-нибудь To-Do и записать туда 100500 задач, а потом разраб к чертям все чистит, то понятное дело люди негодуют. Однако, если бы разработчик понятно и доходчиво объяснил причины сего деяния и незадолго до этого оповестил юзеров хотя б push'ем, то было бы куда легче принять такие изменения.

0

Крутой рассказ! Удачи! В будущем предупреждайте пользователей если стираете их данные!

4

Спасибо 😉

0

После редакторских правок слетела ссылка на список стран под санкциями: https://www.treasury.gov/resource-center/sanctions/Programs/Pages/Programs.aspx

0
Читать все 10 комментариев
ТОП-17 ошибок, которые съедают конверсию лендинга на завтрак
Бизнес — как ребенок: как мамы совмещают свое дело с заботой о детях

Как совмещать бизнес и семью? Ко Дню матери своими историями поделились бизнесвумен, которые работают c ЮKassa и занимаются детьми. Читайте, как им удается сохранять жизненный баланс и добиваться успеха.

Завод по производству идей. Как работают акселераторы, зачем они нужны стартапам и куда идти с идеей прямо сейчас

По данным Startup Genome, 9 из 10 стартапов терпят неудачу. Возможных причин «смерти» много: недостаточно протестированная гипотеза, неподтвержденная юнит-экономика, неверная стратегия или просто неудача в подходе к продажам.

Из науки в IT: как создать свой стартап и стать преподавателем

Как перейти в IT из другой сферы? Как разработать курс, которому нет аналогов? Как студенту получить максимум пользы от занятий? Рассказывает преподаватель OTUS Сергей Окатов, руководитель курсов «Kotlin Backend Developer» и «Kotlin Developer. Basic».

Что Tele2 предлагает клиентам в «черную пятницу»

На главной распродаже года клиентов компании ждут сразу несколько интересных предложений: скидки на смартфоны, пакеты SMS и безлимитный трафик на YouTube, Яндекс.Карты, Яндекс.Навигатор.

Опыт возврата денег за обучение дизайну у Yakovlevv.com. Тварь я дрожащая или право имею?

В данной статье приведен мой личный опыт покупки данных курсов, мои оценочные суждения, а также сухие факты, в виде скриншотов и аудиозаписей из моей личной переписки с владельцем этих самых курсов, на тот момент исполняющего услуги как ИП Яковлев Виталий Борисович ( ОГРНИП: 319784700156839 ), сейчас же, работающего от лица ФОП Торб'як Тетяна…

Как у меня украли 600 тысяч с карты, а «Тинькофф» нарушает федеральный закон

Спойлер: я не вводил никуда код, не переходил по ссылкам и не сообщал данные карты.

МТС не удалила привязанные к номеру персональные данные владельца после перехода номера к другому человеку

Какое-то время я пользовалась телефонным номером МТС, годах в 2015-18х. Номер юзался только для мессенджеров, симку в поездке вынула и куда-то задевала, в итоге номер перешел другому человеку. На звонки номер не отвечал, абонент был не абонент. В 2019 узнавала в салоне, можно ли его выкупить, сказали - увы, уже 2х владельцев сменил. Ну нет и нет.…

Мы сделали бот, который печатает и отправляет ваши фото маме. В 2 клика
Катя со свежими фотками для родителей

Мы запустили Kind Bot — доброго бота, которому в 2 клика можно скинуть свои фотки. Он их напечатает и отправит по почте вашей маме. Или другому близкому человеку.

Новый пролетариат

Очень давно один из классиков написал ёмкую по тогдашним временам фразу «пролетариату нечего терять, кроме своих цепей» Сейчас сложно понять её суть, но тогда она была понятна всем. Рабочий обладал только своими «руками» и это было единственное, чем он владел, абсолютное большинство жило от зарплаты до заплаты в арендованном жилье и не обладала…

Возник по просьбе бразильских банкиров и стал любимым напитком солдат во время Второй мировой: история Nescafe Статьи редакции

В 2021 году Nescafe — крупнейшее подразделение Nestle и бренд, который оценивается больше чем в $20 млрд. По собственным данным компании, в мире каждую секунду выпивают более 5000 чашек напитка.

Дегустация Nescafe National Museum
null