Создание приложения с помощью ИИ


MP3copy с открытым меню настроек: список из 9 языков, оптимизация имён файлов, обложки, эффект дыма и всегда тёмная тема, рядом окно «О программе»

Проблема

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

Proof of concept

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

Оставалось автоматизировать. Я описал логику Claude Code и получил скрипт copy-alphabetical.sh. Поначалу он работал не очень надёжно (порядок файлов иногда сбивался), и я придумал читать только что записанный файл – это должно сбрасывать кэш. А чтобы чтение файла не проходило зря, можно считать контрольную сумму и сравнивать её с оригиналом – так появилась дополнительная проверка записи.

Затем с помощью Automator я встроил скрипт в систему – правый клик по группе музыкальных файлов отправлял их прямиком на mp3-плеер. Неделю я пользовался этим скриптом, всё работало идеально, но мне очень не хватало удобства и дополнительных функций (например, автоматического переименования файлов и проверки свободного места).

От скрипта к приложению MP3copy

Мне давно собирался написать полноценное приложение для macOS на SwiftUI (раньше я писал только простые консольные программы для расчётов). Я хотел, чтобы файлы можно было перетаскивать в окно, чтобы была очередь копирования, пауза, оценка свободного места на устройстве и отсев скрытых файлов. Благодаря Claude Code первая рабочая версия приложения была готова всего за час:

Ранняя версия приложения, тогда называвшегося AlphaCopy: зона для перетаскивания файлов и выбор папки назначения

Аппетит приходит во время еды, и мне захотелось добавить управление музыкальной коллекцией: переименование файлов по единому шаблону, исправление плейлистов, встраивание обложек в музыкальные файлы – всё то, что раньше приходилось делать руками. Коллекция может состоять из совершенно по-разному оформленных файлов: что-то скачано из интернета, что-то рипнуто с CD. Например, файл может называться номер-группа-альбом-песня.mp3 и лежать в папке группа/альбом-год. Это неудобно: самое важное (название песни) стоит в конце и не помещается на маленьком экране mp3-плеера.

Структура группа/год-альбом/номер-песня.mp3 гораздо удобнее – MP3copy приводит к этому формату все имена файлов (если, конечно, в настройках включить соответствующую опцию). После этого нужно исправить имена файлов в плейлисте, если он есть (m3u, pls, cue). Кроме того, в плейлистах часто встречаются ссылки на wav-файлы, хотя сама музыка закодирована в mp3 или flac – это тоже нужно проверять.

Окно «О программе» MP3copy поверх пустого главного экрана: иконка приложения, версия 1.0 и ссылка на документацию

Обложка обычно лежит отдельным файлом в папке альбома, но картинок может быть несколько. Нужно выбрать одну и убедиться, что это не скан для печати, а маленькая картинка. Скачивать обложки из онлайн-баз я не хочу – на этом этапе приложение должно быть полностью независимо от Интернета. Да и, честно говоря, для половины музыки, которую я слушаю, общедоступных обложек нет – это трекерная музыка и музыка из видеоигр (обложки для таких альбомов и сборников я рисую сам).

Для всех этих операций с файлами я составил свод правил и скормил его Claude. Важный момент: изменения вносятся именно в файлы на диске назначения и не должны затрагивать оригиналы. Подводных камней в реализации оказалось очень много, и работа заняла неделю.

Отдельная интересная история – определить, поместятся файлы или нет. Недостаточно просто спросить у системы, сколько свободного места на диске назначения: файлы займут не ровно сумму своих размеров. Дело в том, что размер файла округляется до размера кластера, а размер кластера бывает разным на разных дисках и файловых системах (FAT, exFAT, FAT32, NTFS). Кроме того, место тратится и на запись о файле в каталоге, размер этой записи тоже зависит от системы. Всё это нужно учитывать, чтобы рассчитать нужное место с точностью до кластера.

Особенно я горжусь дизайном прогресс-бара: он одновременно показывает, сколько скопировано, сколько осталось и сколько не поместится. Причём показывает не количество файлов, а их объём в мегабайтах. Когда в списке выделен файл, на прогресс-баре хорошо видно его положение – можно сразу понять, поместится ли файл, и примерно оценить его размер.

Напоследок я добавил приятные мелочи – вращающийся диск с обложкой, эффект дыма (всё это было задумано с самого начала, но было отложено на заключительную фазу), переключение тем, локализацию на 9 языков и поведение при перетаскивании файлов на иконку приложения. Звучит просто (код ведь пишет ИИ, а не я), но багов и просадок производительности было множество, так что работа заняла ещё неделю.

Плюс неделя тестирования на разных компьютерах и внешних устройствах. Только на этом этапе я понял, что приложение полезно не только для музыкальных плееров, но и для флеш-картриджей Nintendo Game Boy и других ретро-консолей. Однако, менять название было уже поздно :) Так и осталось MP3copy.

Mp3-плеер SanDisk Sansa Clip с наушниками Game Boy Color со списком файлов флеш-картриджа на экране

Тяжела и неказиста жизнь простого программиста

Перетаскивание и диалоги выбора файлов можно протестировать только руками. Поэтому после каждой сборки Claude Code запускал приложение, управлял мышью и клавиатурой через AppleScript/System Events, делал скриншоты и сравнивал их с задуманным. Работать за компьютером в это время невозможно – если на скриншот попадёт Telegram или Slack, Claude тут же прекращает работу со словами: «Я не уполномочен читать личную переписку».

А теперь представьте: вы что-то рисуете в Figma, и вдруг открывается собранное приложение, сами собой нажимаются горячие клавиши и начинает двигаться курсор мыши. В этот момент вы нажимаете свои клавиши или уводите курсор куда вам нужно. Естественно, Claude не может провести эксперимент и требует вашего внимания: «Тест не прошёл. Пожалуйста, запустите тест вручную».

Мужчина за офисным столом пьёт кофе среди десятков пустых стаканчиков, на мониторе работает Claude Code

В общем, для такой работы нужен либо второй компьютер с отдельным монитором, либо новый быстрый компьютер с большим диском, чтобы вести всю работу в виртуальной машине. Рекламные лозунги вроде «займитесь своими делами, пока ИИ делает вашу работу» и «всего за $20 в месяц» – сильное преувеличение.

Отвергнутые идеи

Отвергнутый вариант: кнопки паузы и остановки копирования рядом под устройством Отвергнутый вариант: кнопка паузы копирования под списком файлов
  • Глобальная кнопка паузы. Она нужна редко, но привлекала столько же внимания, сколько важная кнопка «стоп». Перенеся паузу в список файлов, я неожиданно открыл новый сценарий: можно запланировать остановку на конкретном файле. Это очень удобно, когда часть треков альбома не помещается, и вы решили пока не копировать альбом целиком. Обожаю такие находки: если что-то визуально не вписывается в интерфейс, значит, не продумана логика (программы или поведения пользователя). Нет смысла слепо держаться исходного плана и впихивать элемент любой ценой – лучше сначала упростить интерфейс, а потом зайти с другой стороны.
  • Плавная анимация списка файлов при прокрутке с клавиатуры оказалась плохой идеей – курсор мгновенно перескакивал на следующую строку, и только потом прокручивалось содержимое окна. Движение выглядело дёрганым. А если делать движение курсора таким же плавным, как прокрутку окна, при быстрой прокрутке сложно понять (и вовремя остановиться), какой файл сейчас выбран. Так что анимацию пришлось убрать и вернуть дискретное движение. Микроанимации – это хорошо, но не здесь. Без них взаимодействие стало чище, отзывчивей и понятнее.
  • Идея оптимизировать запись, пропуская файлы, которые уже есть на диске назначения, не оправдала всех ожиданий. Изначально я рассуждал так: чтение с флеш-накопителя гораздо быстрее записи и не изнашивает носитель. Значит, если на диске назначения найден файл с тем же именем и размером, что и копируемый, можно сравнить их контрольные суммы и при полном совпадении не перезаписывать файл. Эксперименты в реальных условиях на реальных данных показали, что прирост производительности действительно заметен. Но оказалось, что это подрывает главную идею приложения – сохранять правильный порядок файлов на диске. Поэтому было принято гибридное решение: внутри одной папки не перезаписывать файлы, которые идут в строгом алфавитном порядке начиная с самого первого, но как только алфавитный порядок нарушается на каком-либо файле, начиная с этого места все файлы перезаписывать без проверки на совпадение. Это правило действует и для папок и применяется рекурсивно. Важно было внятно описать логику и предусмотреть все пограничные случаи. В итоге такая оптимизация записи не даёт существенного выигрыша в скорости, но хотя бы немного бережёт ресурс флеш-памяти.

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

ИИ ошибается

  • При сравнении скопированного файла с оригиналом программа часто показывала ошибку записи. Оказалось, что Claude брал имена файлов не из того списка. Дело в том, что имена исходного и записанного файлов могут отличаться из-за переименования – в таких случаях оригинал не находился, и подпрограмма сравнения завершалась с ошибкой. Напомню, вы читаете главу «Ошибки ИИ», а не «Ошибки программиста», так что здесь нет моей вины – я чётко описал, что нужно вести список переименованных файлов и сверять с ним, но Claude Code всё равно запутался. Разобраться с таким багом было непросто.
  • Запустив Мониторинг системы, я заметил необъяснимо высокое потребление оперативной памяти (4 гигабайта при копировании пары тысяч файлов). Расследование выявило причину: при проверке файла запускалась процедура расчёта контрольной суммы SHA-256 для каждого блока (файл состоит из множества блоков), но Claude написал процедуру так, что массив контрольных сумм накапливался и не очищался после завершения. В результате возникала утечка памяти. Процедуру пришлось переписать, и потребление памяти упало до 50 мегабайт.
  • Прокрутка списка в левой панели тормозила анимацию диска в правой панели. Пришлось повозиться с профайлером Xcode и объяснить Claude, как разнести отрисовку по разным независимым процессам.
  • Текст справки, написанный Claude, пришлось дважды отправлять на доработку: цитирование интерфейсных текстов не совпадало с реальным интерфейсом (бессмертная классика: «нажмите Далее», когда в реальности кнопка называется «Продолжить» – ИИ научился повторять ошибки человека). В справке встречался и технический жаргон (например, «resource fork» я заменил на «файл-спутник»: скрытый файл macOS с дополнительной информацией).

Было ещё множество других ошибок, все и не вспомнить. Без опыта программирования их невозможно заметить. Даже объяснив ошибку ИИ, нельзя рассчитывать на успех: ИИ часто предлагает нелепые обходные пути, которые только усугубляют проблему. Обо всём нужно думать самому.

Баги SwiftUI

Значительная часть трудностей была связана не с логикой, а с SDK – Claude то и дело натыкался на поведение, не соответствующее документации:

  • Стандартный способ измерить размер view с помощью GeometryReader не работал: он всегда возвращал ноль, хотя GeometryReader на самом деле отрисовывался. Отладка заняла целую сессию – цветные маркеры на экране, отладочный текст и логирование в файл. Исправили переходом на .onGeometryChange.
  • В WindowGroup при первом запуске не определялся реальный размер окна – оно всё равно открывалось шире заданного. Решение: использовать отдельный модификатор .defaultSize(width:height:) на сцене, а не на view с содержимым.
  • Много внимания я уделил accessibility, в частности навигации с клавиатуры. Битва за правильное отображение элементов в фокусе превратилась в настоящую эпопею. Я пробовал и системный фокус, и собственный перехват – ничто не работало как следует. Обёртка .focusable/.focused в ScrollViewReader ломала навигацию по клавише Tab в списке файлов. Пришлось вынести состояние фокуса во внешний VStack. И всё равно всё ломалось при добавлении каждой новой функции. Даже не помню, как я справился – это было как страшный сон.
  • Интерфейс берёт системную тему (светлая/тёмная), но я добавил опцию «всегда тёмная». При отключении этой опции заголовок окна возвращался к системной теме, а содержимое оставалось тёмным. Обойти баг удалось, управляя NSApp.appearance напрямую вместо модификатора SwiftUI. Были и другие расхождения между документацией и реальным поведением SDK.

Результат

MP3copy скомпилирован как универсальное приложение для macOS, переведён на 9 языков и опубликован в App Store со всеми сопутствующими материалами. Приложение работает и приносит пользу людям. Оно занимает всего 7 мегабайт, половина из которых – документация с картинками.

За всё время я открывал Figma лишь дважды: чтобы нарисовать иконку приложения и чтобы примерить разные положения кнопки паузы в интерфейсе.

3 недели работы в Claude Code ушли не столько на дизайн, сколько на реализацию: баги в бета-версии SDK macOS 26.5, неточности в документации, ошибки ИИ, тестирование, оптимизация. Теперь я готов браться за более сложные проекты!

Страница MP3copy в Mac App Store