Привет, Хабр. Меня зовут Александр Пиманов, я ведущий iOS-разработчик в МТС Диджитал. Сегодня расскажу о простой (как мне сначала показалось) задаче — нужно было свалидировать UI в зависимости от того, есть в файловой системе файл с логами или нет. Я быстро с ней справился, но тут возникла проблема: на моем устройстве валидация работала, а на некоторых чужих — нет. Чтобы понять причину, я перепробовал, кажется, все. Как мне удалось найти проблему и как мы ее решали, рассказываю в статье.

Какая была задача

Итак, задача: свалидировать UI в зависимости от того, есть у нас в файловой системе файл с логами или нет. Задача на 10 минут, подумал я тогда.

Я написал actor и метод в нем, который ходит в файловую систему и проверяет два параметра:

  1. Само наличие файла по определенному «хвосту», то есть конечному имени файла.

  2. «Пустоту» файла. То есть метод проверял, что нужный мне файл создан и его размер ≠ 0.  

Выбрал actor по вполне логичной причине: чтобы не было гонки данных. Иначе мы могли одновременно и писать, и читать по одному и тому же рутовому пути. И у меня все заработало! UI — кнопки отправки и удаления логов — валидировался в обе стороны.

Метод во ViewModel выглядел так:

Я отправил фичу на тестирование и забыл про нее. Как оказалось, зря.

Что-то пошло не так

Через некоторое время Q&A возвращает мне эту задачу и говорит, что на одном из устройств у него не работает валидация. Я проверил у себя — все работает. Стал искать ошибки в своей реализации. На следующий день написал еще один коллега-разработчик: у него тоже не работает. Проверил на других своих устройствах — работает.

После этого мы командой собрали тест-группу и выяснили, что примерно из 15 тест-моделей на четырех рандомных устройствах (разные модели iPhone и версии iOS) валидация не работает. Сказать, что я был удивлен, — ничего не сказать.

Я начал копать. Кнопка отправки логов была невалидна, поэтому посмотреть их на устройствах, где не работала фича, я не мог.

Как я все перепробовал и нашел причину

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

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

  2. Проблема в самом UI. То есть с его реактивным обновлением.

  3. Проблема с гонкой данных.

Чтобы проверить эти гипотезы, я делал delay между созданием файла и записью в него. Не работало. Я переписывал метод actor’а по получению файла и проверку его размера, писал кастомные Combine Subjects — в общем, изощрялся как мог. Но на некоторых девайсах валидация по-прежнему не работала. При этом в файловой директории создавался файл с логами, но он был пустым. Казалось, я перепробовал все.

Еще через несколько дней я отчаялся и просто захардкодил изначальную валидацию кнопки, чтобы после scratch install она была валидна. Потом запустил симулятор. Кнопка валидна, файл создан, и он не пустой — отличное решение (нет). Тогда я убрал хардкод и запустил симулятор снова. И тут произошло чудо!

Валидация стала работать как надо в обе стороны без последующего хардкода! То есть когда мы создавали файл и сразу писали в него, кнопка отправки логов валидировалась — как и кнопка удаления логов, когда размер файла 0.

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

Причина такого поведения — некорректная работа апдейта файловой системы после создания файла и мгновенной записи в него. И вот каким способом я решил эту проблему.

В didFinishLaunchingWithOptions создаем пустой файл (абсолютно любой) и через пару секунд удаляем его. Это нужно, чтобы принудительно вызвать апдейт файловой системы и подготовить ее к работе.

Метод по созданию и удалению пустого файла в AppLogsService:

Потом — вызов. На такие нишевые кейсы я всегда оставляю комментарии:

На запомнить

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

Вот такое решение у меня получилось — с одной стороны, костыльное, а с другой — вполне юзабельное. Надеюсь, эта статья поможет сэкономить кучу вашего времени на ресерч и решение проблемы. Пишите в комментах, сталкивались ли вы с подобным, и если да, то как решали. Можете написать про похожие кринж-проблемы в целом — будет интересно почитать!

Комментарии (4)


  1. Bardakan
    18.06.2024 20:33
    +1

    почему вы в одном куске кода используете Task, а в другом - DispatchQueue?

    Можете написать про похожие кринж-проблемы в целом — будет интересно почитать!

    В iOS объект по умолчанию создается и уничтожается в одном и том же потоке. В одном из приложений объект создавался в фоновом потоке (и это никак нельзя было изменить), а приложение ожидало уничтожения этого объекта в главном потоке. Пришлось в deinit создать retain cycle и убирать циклическую ссылку при наступлении главного потока.


  1. AleksandrPimanov Автор
    18.06.2024 20:33

    Привет! По поводу вопроса - да все очень просто) просто те файлы/модули, которые еще не переехали на swift structured concurrency, там сохраняем GCD API. Стараемся постепенно переезжать на новую API, потому небольшая мешанина. Да- не есть хорошо, но мы с этим боремся)


  1. AleksandrPimanov Автор
    18.06.2024 20:33

    Интересный кейс с деинитом. Не было проблем с другими экранами нав стэка?


    1. Bardakan
      18.06.2024 20:33
      +1

      Со SwiftUI не проверял, с UIKit работало нормально