Задайте универсальному компоненту SwiftUI внешний вид по умолчанию, добавив инициализатор с ограничением по конкретному типу содержимого. Для настройки оставьте отдельный инициализатор со сборщиком. В этом руководстве создаётся повторно используемая панель для окна macOS и объясняется, когда вывод типов из аргументов по умолчанию в Swift 5.7 может упростить интерфейс.

Определите два варианта вызова компонента

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

Предполагаемый интерфейс — DesktopPanel(title: "Workspace") для стандартного представления и DesktopPanel(title: "Workspace") { Text("Three sample items") } для пользовательского содержимого. Эти вызовы создают разные специализации одного универсального компонента.

Параметры обобщённого типа Swift обозначают конкретные типы. Ограничение вроде Content: View требует соответствия протоколу; требование равенства типов, например Content == DefaultPanelContent, задаёт один конкретный тип. В справочнике языка описаны оба вида требований и выбор перегрузки инициализатора на основе ограничений. Параметры и аргументы обобщённых типов

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

Создайте полноценный пример для macOS

Для этого руководства используйте целевой объект приложения SwiftUI для macOS с минимальной версией macOS 11 или новее и компилятор Swift 5.7 или новее. Минимальная версия macOS нужна для используемой ниже структуры App и WindowGroup; Apple описывает WindowGroup как контейнер сцены для иерархии представлений окна. WindowGroup

Замените сгенерированную точку входа приложения этим исходным файлом целиком и удалите созданный файл ContentView.swift, если в нём объявлен ещё один ContentView. В целевом объекте должна быть ровно одна декларация приложения с @main. Все показанные ниже значения — синтетические локальные примеры.

import SwiftUI

struct DefaultPanelContent: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Sample workspace")
                .font(.headline)
            Text("Choose a layout to begin.")
                .foregroundColor(.secondary)
        }
    }
}

struct DesktopPanel<Content: View>: View {
    private let title: String
    private let content: Content

    init(
        title: String,
        @ViewBuilder content: () -> Content
    ) {
        self.title = title
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(title)
                .font(.title2)

            Divider()

            content
        }
        .padding(16)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.08))
        .cornerRadius(10)
        .overlay(
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.gray, lineWidth: 1)
        )
    }
}

extension DesktopPanel where Content == DefaultPanelContent {
    init(title: String) {
        self.init(title: title) {
            DefaultPanelContent()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            DesktopPanel(title: "Default panel")

            DesktopPanel(title: "Custom panel") {
                HStack {
                    Text("Sample items")
                    Spacer()
                    Text("3")
                        .font(.headline)
                }

                Text("Layout: Compact")
                    .foregroundColor(.secondary)
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 300)
    }
}

@main
struct PanelExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Соберите и запустите целевой объект для macOS. В результате должны появиться две панели с одинаковым внешним оформлением: в первой будет стандартный текст рабочей области, во второй — строка со счётчиком и описание макета. В этой среде нет компилятора Swift и SDK Apple, поэтому пример был проверен, но скомпилировать его здесь не удалось.

Рекомендации Apple по созданию пользовательских представлений поддерживают такой способ композиции: объявите тип, соответствующий View, реализуйте его body и объедините встроенные и пользовательские представления в иерархию. Также в документации объясняется, что модификаторы возвращают изменённые представления, поэтому общее оформление должно применяться к содержимому панели снаружи. Объявление пользовательского представления

Почему ограничение инициализатора определяет универсальный тип

Пользовательский инициализатор получает замыкание, возвращающее Content. Вызов content() создаёт значение, сохраняемое в панели. Замыкание используется при инициализации, поэтому эта реализация его не хранит и не требует атрибута @escaping.

Для вызова без сборщика расширение делает init(title:) доступным только при условии, что Content — это DefaultPanelContent. Это требование предоставляет недостающую информацию о типе. Затем реализация делегирует работу инициализатору со сборщиком, чтобы инициализация свойств выполнялась в одном месте.

В результате значение по умолчанию имеет тип DesktopPanel<DefaultPanelContent>. Тип содержимого пользовательской панели выводится из замыкания. Вызывающей стороне не нужно явно указывать потенциально сложный тип.

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

Атрибут сборщика позволяет использовать в примере многострочное пользовательское замыкание. Благодаря ему HStack и следующий за ним Text входят в создаваемое содержимое; он не заменяет требование самого компонента Content: View. Apple описывает, как сборщик обрабатывает несколько выражений и условное содержимое. ViewBuilder

Когда можно заменить перегрузку одним инициализатором со сборщиком по умолчанию?

В Swift 5.7 реализовано предложение SE-0347, позволяющее выводить универсальный тип из конкретного выражения по умолчанию, если аргумент опущен. Для этого компонента оно даёт более краткий вариант. SE-0347: вывод типов из выражений по умолчанию

Для компилятора Swift 5.7 или новее замените инициализатор со сборщиком внутри DesktopPanel следующим фрагментом и удалите расширение с ограничением целиком. Оставьте импорт, сохранённые свойства, DefaultPanelContent, body и код приложения из полного примера.

// Replacement initializer inside DesktopPanel.
init(
    title: String,
    @ViewBuilder content: () -> Content = {
        DefaultPanelContent()
    }
) {
    self.title = title
    self.content = content()
}

Теперь при отсутствии замыкания Content выводится из DefaultPanelContent(). Если передать замыкание, тип выводится из этого аргумента. Это вывод типа из выражения по умолчанию, а не аргумент типа по умолчанию в объявлении компонента.

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

Выбирайте перегрузку, если стандартное содержимое зависит от входных данных инициализатора. Например, стандартный подзаголовок, сформированный на основе title, можно создать внутри init(title:) и передать инициализатору со сборщиком. Выражение аргумента по умолчанию не может захватить переданное title так, словно это замыкание, созданное внутри тела этого инициализатора.

Требование к компоненту Подходящий интерфейс
Фиксированное стандартное содержимое и компилятор Swift 5.7+ Краткий вариант — параметр сборщика со значением по умолчанию.
Стандартное содержимое, формируемое на основе других входных данных инициализатора Используйте инициализатор с ограничением и создавайте значение внутри него.
Несколько именованных стандартных представлений Используйте явные перегруженные инициализаторы с понятными метками и ограничениями.
Явно заданный сохранённый тип стандартной специализации Укажите DesktopPanel<DefaultPanelContent> или введите псевдоним типа.

Не пытайтесь выразить этот интерфейс как struct DesktopPanel<Content: View = DefaultPanelContent>. Текущая грамматика параметров обобщённых типов Swift не предусматривает такой синтаксис типа по умолчанию. Здесь поддерживаются два подхода: инициализация с ограничением и вывод типа из выражения по умолчанию. Параметры и аргументы обобщённых типов

Сохраняйте интерфейс при переходе между версиями сборщика

В актуальной документации Apple различаются Xcode 26 и более ранние версии, с одной стороны, и Xcode 27 и более поздние — с другой. В более новом наборе инструментов ViewBuilder создаёт типонезависимое содержимое; Apple предлагает ContentBuilder — псевдоним типа для ViewBuilder — как обозначение этого унифицированного поведения. Различие определяется набором инструментов сборки, а не только минимальной версией ОС приложения. ViewBuilder, ContentBuilder

В примере сохранён @ViewBuilder для совместимости с более ранними наборами инструментов. При переходе на новое написание в Xcode 27 или новее замените атрибут этого параметра на @ContentBuilder. Сохраните Content: View: панели по-прежнему требуется содержимое, которое можно поместить в иерархию представлений.

Не указывайте специализацию стандартного содержимого как вложенный тип стека и кортежа. В примечании Apple о миграции описан переход от TupleView к TupleContent в сборщиках с несколькими выражениями, который может нарушить ограничения, ссылающиеся на эти автоматически сгенерированные структуры. В примере вместо этого расширение ограничено типом DefaultPanelContent, а внутренний макет скрыт за body: some View. TN3211: устранение несовместимостей исходного кода SwiftUI для State и ContentBuilder

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

Прежде чем менять макет, проверьте контракт

Если компонент не компилируется, сначала выясните, какой части интерфейса не хватает информации о типе. Изменение отступов или оборачивание всего в ещё один стек не устранит отсутствие специализации обобщённого типа.

  1. Вызов без сборщика не может вывести Content. Убедитесь, что существует ограниченный init(title:) или что инициализатор со значением по умолчанию компилируется в Swift 5.7 или новее.
  2. Конкретное значение по умолчанию нельзя присвоить Content. Проверьте, что присваивание выполняется внутри расширения с ограничением. Самого соответствия View недостаточно, чтобы DefaultPanelContent был взаимозаменяем с любым возможным Content.
  3. Несколько пользовательских выражений отклоняются. Проверьте, что у параметра-замыкания есть атрибут сборщика.
  4. Явно заданный тип не принимает пользовательское содержимое. Аннотация DesktopPanel<DefaultPanelContent> намеренно фиксирует тип содержимого. Удалите её, если при вызове должна выводиться другая специализация.
  5. После обновления набора инструментов перестало работать ограничение на основе кортежа. Откажитесь от зависимости от сгенерированной структуры сборщика в пользу именованного стандартного представления или следуйте инструкции Apple по миграции для конкретного ограничения.

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

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

Источники