Connect with us

Разработка

Лучшие практики SwiftUI из агентского навыка Xcode 27

Агентский навык SwiftUI от Apple, поставляемый с Xcode 27, дает отличное представление о лучших практиках SwiftUI. Навык компактен и следует лучшим практикам, что, безусловно, приводит к улучшению кода SwiftUI в целом.

Опубликовано

/

     
     

Xcode 27, представленный на WWDC 2026, впервые включает в себя агентские навыки разработки, в том числе на SwiftUI, от Apple. Эти навыки отлично подходят для агентской разработки в Xcode или при использовании навыков из Xcode 27 в Claude, Codex и Cursor — любой IDE для ИИ.

Хотя мы можем использовать эти навыки и не оглядываться назад, гораздо интереснее углубиться в анализ того, что Apple считает достаточно важным для включения в навык. Навыки должны быть компактными и оптимизированными для использования токенов, поэтому в них будут включены только необходимые части. В этой статье мы углубимся в лучшие практики SwiftUI, рассмотрев навык SwiftUI Specialist Agent, входящий в состав Xcode 27.

Рассмотрим структуру навыка

Прежде чем перейти к советам, полезно посмотреть, как навык выглядит на диске. Навык — это не что иное, как папка с Markdown файлами, причем единственный файл SKILL.md служит точкой входа:

swiftui-specialist/
├── SKILL.md
└── references/
    ├── structure.md
    ├── dataflow.md
    ├── environment.md
    ├── modifiers.md
    ├── foreach.md
    ├── animations.md
    ├── localization.md
    ├── soft-deprecation.md
    └── soft-deprecated-apis.md

Файл SKILL.md объясняет агенту, для чего предназначен этот навык и в каких случаях следует обращаться к каждому справочному материалу. Всё содержимое каталога references/ — это дополнительный контекст, который загружается только тогда, когда он действительно нужен для выполнения задачи.

  • structure.md
    Когда и как выносить части представления в отдельные типы View, а не оформлять их в виде вычисляемых свойств, и почему такое разделение влияет на производительность.
  • dataflow.md
    Как передавать и хранить данные с помощью @State, @Binding и @Observable, чтобы обновлялись только те представления, которых действительно коснулись изменения.
  • environment.md
    Проблемы с производительностью при использовании @Environment, например хранение замыканий или применение нестабильных значений по умолчанию.
  • modifiers.md
    Почему популярный условный модификатор представления .if нарушает идентичность представления и чем его следует заменить.
  • foreach.md
    Правила идентификации элементов в ForEach, List и Table, включая распространённые ошибки, связанные с индексами и временными идентификаторами.
  • animations.md
    Использование макроса @Animatable и случаи, когда свойство animatableData следует реализовывать самостоятельно.
  • localization.md
    Работа с LocalizedStringKey, каталогами строк, #bundle и форматированием с учётом языкового стандарта и региона.
  • soft-deprecation.md
    Как агент должен работать с программными интерфейсами, которые считаются устаревающими, но ещё не объявлены устаревшими официально, не переписывая при этом код, которого пользователь не просил касаться.
  • soft-deprecated-apis.md
    Список всех устаревающих программных интерфейсов SwiftUI с возможностью поиска и указанием современных замен.

Проще говоря, каждый справочный файл посвящён отдельной главе рекомендаций по разработке на SwiftUI.

Что находится в SKILL.md

Разделение между SKILL.md и справочными файлами важно из-за того, как агенты загружают навыки. В начале сеанса агент читает только краткое описание из служебного заголовка каждого навыка. Полное содержимое SKILL.md загружается после того, как задача пользователя была сопоставлена с этим навыком. И только после этого агент подключает конкретные справочные файлы, необходимые для выполнения задачи.

Такой подход позволяет экономно расходовать объём контекста. Именно поэтому файл SKILL.md остаётся небольшим и содержит ссылки на более объёмные материалы, а не включает их содержимое целиком.

Таким образом, после активации навыка SKILL.md становится его постоянно используемой частью. В нём содержатся:

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

Иными словами, SKILL.md выполняет роль маршрутизатора. Он не обучает SwiftUI напрямую, а определяет, какой справочный файл должен ответить на текущий вопрос, чтобы решение соответствовало рекомендациям по разработке на SwiftUI.

Компактный файл SKILL.md

За последние несколько месяцев я создал несколько навыков для агентов:

Но особенно интересно то, что файл SKILL.md, подготовленный Apple, получился действительно компактным. Он сосредоточен исключительно на том, чтобы направить агента к подходящему справочному файлу:

Лучшие практики SwiftUI из агентского навыка Xcode 27

Очевидно, что агентские навыки, поставляемые вместе с Xcode 27, выполнены на высоком уровне и соответствуют общим рекомендациям по созданию таких навыков.

Лучшие практики из справочных файлов

Именно в справочных файлах содержатся практические знания о SwiftUI. Каждый из них загружается по мере необходимости: когда вы просите агента написать ForEach, он загружает foreach.md; когда вы начинаете работать с окружением, он обращается к environment.md. Поскольку все файлы никогда не загружаются одновременно, каждый из них посвящён одной конкретной области.

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

Файлы structure.md и dataflow.md посвящены созданию чётких границ вокруг данных, которые считывает каждое представление. В environment.md и foreach.md рассказывается, как избегать значений, которые выглядят стабильными, но на самом деле таковыми не являются. Файлы modifiers.md и animations.md объясняют, как сохранять идентичность представлений, чтобы SwiftUI мог правильно анимировать и повторно использовать их.

И снова хорошо заметны чёткое разграничение обязанностей и разделение областей ответственности. Рассмотрим наиболее интересные моменты, которые мне удалось обнаружить при более подробном изучении некоторых файлов навыков для агентов из Xcode 27.

Оформляйте разделы как отдельные представления, а не как вычисляемые свойства

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

Когда содержимое свойства body становится слишком большим, разработчики обычно разбивают его на вычисляемые свойства, вроде private var header: some View. Однако в навыке рекомендуется выносить каждый такой раздел в отдельный тип, соответствующий протоколу View.

// AVOID: a computed property is inlined into the parent's body.
// Toggling `isExpanded` re-evaluates header, details, AND footer.
struct ProfileView: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            header
            details
            footer
        }
    }

    private var header: some View { /* ... */ }
}

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

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

// PREFER: each section is its own type with the exact data it reads.
// Toggling `isExpanded` only re-evaluates ProfileDetails.
struct ProfileView: View {
    @State private var isExpanded = false
    let user: User

    var body: some View {
        VStack {
            ProfileHeader(name: user.name)
            ProfileDetails(bio: user.bio, isExpanded: isExpanded)
        }
    }
}

На том же принципе основано правило из dataflow.md: передавайте представлениям только те данные, которые они действительно используют. SwiftUI сравнивает поля входных значений значимых типов по отдельности. Поэтому представление, принимающее целую структуру User, будет обновляться при любом изменении этой структуры — даже если изменилось поле, которое представление вообще не отображает.

Не используйте модификатор представления .if

Если вы давно работаете со SwiftUI, то наверняка встречали или сами писали собственный модификатор .if, который применяет преобразование только при выполнении определённого условия.

В рекомендациях прямо сказано: такой модификатор писать не следует.

// AVOID: this destroys structural identity every time `condition` flips.
extension View {
    @ViewBuilder
    func `if`<Content: View>(_ condition: Bool,
                             transform: (Self) -> Content) -> some View {
        if condition { transform(self) } else { self }
    }
}

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

Из-за этого сбрасываются все значения @State во вложенной иерархии, а плавная анимация превращается в резкую замену одного представления другим.

Решение простое: используйте тернарный оператор внутри аргумента модификатора.

// PREFER: identity is preserved and the change animates smoothly.
Text("Hello")
    .foregroundStyle(isHighlighted ? .red : .primary)

Делайте типы свойств в @Observable сравнимыми

Эту деталь легко упустить. При использовании макроса @Observable создаётся сеттер, который не запускает обновление представлений, если новое значение совпадает с текущим. Однако такая проверка работает только в том случае, если тип свойства соответствует протоколу Equatable.

// AVOID: DeliveryStatus isn't Equatable, so every assignment notifies
// observing views — even when the value didn't actually change.
enum DeliveryStatus {
    case placed, preparing, shipped, delivered
}

Если тип не соответствует протоколу Equatable, повторная запись того же значения всё равно приводит к обновлению представления.

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

// PREFER: Equatable lets the generated setter short-circuit redundant
// invalidations when the same status is set again.
enum DeliveryStatus: Equatable {
    case placed, preparing, shipped, delivered
}

То же самое относится и к коллекциям: массив соответствует протоколу Equatable только тогда, когда ему соответствует тип его элементов. Поэтому один несравнимый тип элемента лишает весь массив этой оптимизации. Это одна из тех рекомендаций по разработке на SwiftUI, о которых хотелось бы получать предупреждение непосредственно в Xcode: «Вы используете @Observable со свойством, тип которого не соответствует протоколу Equatable«.

Не выполняйте тяжёлую работу в инициализаторе представления

Инициализатор представления вызывается гораздо чаще, чем ожидают многие разработчики. Каждый раз, когда родительское представление повторно вычисляет свойство body, дочернее представление создаётся заново.

Внутри List, LazyVStack или родительского представления с анимацией это может происходить много раз в секунду.

// AVOID: decoding JSON and allocating a formatter on every init.
struct WeatherCard: View {
    let summary: WeatherSummary
    let formattedDate: String

    init(rawJSON: Data, date: Date) {
        self.summary = try! JSONDecoder().decode(WeatherSummary.self, from: rawJSON)
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        self.formattedDate = formatter.string(from: date)
    }
}

Рассматривайте инициализатор как операцию с постоянной вычислительной сложностью, которая лишь копирует входные значения в хранимые свойства. Разбор данных следует выполнять на уровне модели. Для отображения дат используйте встроенные средства форматирования SwiftUI, чтобы не создавать форматированные строки заново при каждом вычислении представления.

// PREFER: inputs are already prepared, and Text formats lazily.
struct WeatherCard: View {
    let summary: WeatherSummary
    let date: Date

    var body: some View {
        VStack {
            Text(summary.headline)
            Text(date, format: .dateTime.day().month().year())
        }
    }
}

Задавайте каждому ForEach стабильную идентичность

Идентичность — это основа, благодаря которой SwiftUI может сохранять состояние, анимировать перемещение элементов и повторно использовать вложенные иерархии представлений при обновлениях.

Самая распространённая ошибка — использовать в качестве идентификаторов индексы коллекции. В справочном файле, посвящённом ForEach, на это указано напрямую:

// AVOID: an index describes a position, not an element. Reordering or
// inserting makes every later index point to a different element.
ForEach(items.indices, id: \.self) { index in
    ItemRow(item: items[index])
}

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

// PREFER: identity comes from the element itself, not its position.
ForEach(items) { item in
    ItemRow(item: item)
}

В справочном файле также не рекомендуется формировать идентификатор на основе изменяемого свойства. Например, если id вычисляется из заголовка, который пользователь может редактировать, идентичность строки изменится прямо во время редактирования. В результате поле потеряет фокус, а изменение будет анимировано как удаление старой строки и добавление новой, а не как обычное обновление. Когда читаешь это объяснение, всё кажется очевидным, но я и сам определённо допускал такую ошибку раньше.

Внутри List отдавайте предпочтение строкам с одним представлением

Это более продвинутая рекомендация, но она объясняет реальную причину резкого падения производительности. List должен заранее знать идентификаторы всех строк. Он может быстро подготовить их по шаблону, если каждая строка создаёт одно корневое представление. Но как только содержимое строки начинает ветвиться и возвращать разные top-level структуры, этот быстрый путь перестаёт работать.

// AVOID: the top-level switch makes each row's structural identity
// depend on which case ran, so SwiftUI evaluates every row's body.
struct ItemRow: View {
    var item: Item

    var body: some View {
        switch item.kind {
        case .plain:       Text(item.title)
        case .highlighted: Text(item.title).bold()
        case .disabled:    Text(item.title).foregroundStyle(.secondary)
        }
    }
}

Если обернуть ветвящееся содержимое в контейнер с одним корневым представлением, строка снова станет «однокорневой». Подойдёт любой контейнер: VStack, HStack, ZStack или собственная обертка. Главное — свести несколько возможных корневых представлений к одному. Обратите внимание, что конструкция switch обёрнута в VStack:

// PREFER: one top-level view regardless of which case runs.
struct ItemRow: View {
    var item: Item

    var body: some View {
        VStack {
            switch item.kind {
            case .plain:       Text(item.title)
            case .highlighted: Text(item.title).bold()
            case .disabled:    Text(item.title).foregroundStyle(.secondary)
            }
        }
    }
}

Условие if без ветви else на верхнем уровне приводит к той же проблеме, поскольку в зависимости от условия оно создаёт либо одно представление, либо ни одного. Если некоторые элементы вообще не должны становиться строками, отфильтруйте коллекцию до её передачи в ForEach, а не возвращайте пустую строку.

Чего не хватает в навыке для работы со SwiftUI?

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

Я решил сравнить его со своим навыком для работы со SwiftUI — самым популярным навыком для SwiftUI по данным skills.sh. Мне действительно было интересно увидеть различия.

  • Доступность
    Ничего не сказано о подписях для VoiceOver, динамическом размере текста, признаках элементов и группировке, хотя всё это необходимо для выпуска полноценного приложения.
  • Навигация и модальные окна
    Apple отмечает NavigationView как устаревающий программный интерфейс, но не предлагает рекомендаций по использованию NavigationStack, NavigationSplitView, модальных окон и инспектора.
  • Компоновка
    Не рассматриваются размеры, выравнивание и альтернативы GeometryReader.
  • Прокрутка и фокус
    Отсутствуют рекомендации по программному управлению прокруткой, применению ScrollViewReader и работе с @FocusState.
  • «Жидкое стекло» и поддержка iOS 26 и новее
    Новейшие программные интерфейсы оформления полностью пропущены.
  • macOS
    Не рассматриваются сцены, оформление окон, MenuBarExtra и взаимодействие с AppKit. В целом навык выглядит ориентированным прежде всего на iOS.
  • Swift Charts
    Ничего не сказано о метках, осях, выборе данных и специальных возможностях диаграмм.
  • Изображения
    Не рассматриваются AsyncImage, уменьшение разрешения изображений и кэширование.
  • Предварительный просмотр
    Нет рекомендаций по использованию #Preview, @Previewable и автономных мок-данных.
  • Средства анализа производительности
    Навык учит создавать эффективные представления, но ничего не говорит о записи и анализе трассировок в Instruments, когда приложение уже работает медленно.
  • Анимации
    Файл об анимациях охватывает только макрос @Animatable. Переходы, поэтапные и ключевые анимации в нём не рассматриваются.

Справедливости ради, у этого сравнения есть и обратная сторона. Apple глубже рассматривает локализацию и отслеживание устаревающих программных интерфейсов. Поэтому навык нельзя назвать поверхностным во всех областях — он просто очень точно сосредоточен на темах, которые Apple считает наиболее важными для агента. Он определённо охватывает рекомендации по разработке на SwiftUI. Вопрос лишь в том, охватывает ли он их полностью.

Компактные или подробные навыки для агентов

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

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

Отдельные справочные файлы выполняют свою задачу и сводят использование контекста к минимуму. В конечном счёте без специализированного навыка со знаниями вы потратите больше контекста на исправление некорректных приёмов написания кода.

Лучшее из двух подходов: навык SwiftUI Expert 4.0.0

Очевидно, что наилучшим решением было бы объединить оба навыка в один, чтобы получить наиболее полные рекомендации по правильной разработке на SwiftUI.

Именно поэтому я только что выпустил версию 4.0.0 своего навыка SwiftUI Expert. Установить её можно напрямую с помощью следующей команды:

npx skills add https://github.com/avdlee/swiftui-agent-skill --skill swiftui-expert-skill

Если вам нужны дополнительные варианты установки, я рекомендую ознакомиться с репозиторием с открытым исходным кодом AvdLee/SwiftUI-Agent-Skill.

Заключение

Агентский навык SwiftUI от Apple, поставляемый с Xcode 27, дает отличное представление о лучших практиках SwiftUI. Навык компактен и следует лучшим практикам, что, безусловно, приводит к улучшению кода SwiftUI в целом. Хотя навык отличный, он не так полон, как я надеялся. Именно поэтому я выпустил следующую версию своего популярного навыка SwiftUI Expert.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: