Задайте универсальному компоненту 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
Именованная оболочка также упрощает обычные изменения. Если добавить третью строку стандартного содержимого, изменится только его реализация, и не придётся переписывать длинный конкретный тип в удобном инициализаторе.
Прежде чем менять макет, проверьте контракт
Если компонент не компилируется, сначала выясните, какой части интерфейса не хватает информации о типе. Изменение отступов или оборачивание всего в ещё один стек не устранит отсутствие специализации обобщённого типа.
- Вызов без сборщика не может вывести
Content. Убедитесь, что существует ограниченныйinit(title:)или что инициализатор со значением по умолчанию компилируется в Swift 5.7 или новее. - Конкретное значение по умолчанию нельзя присвоить
Content. Проверьте, что присваивание выполняется внутри расширения с ограничением. Самого соответствияViewнедостаточно, чтобыDefaultPanelContentбыл взаимозаменяем с любым возможнымContent. - Несколько пользовательских выражений отклоняются. Проверьте, что у параметра-замыкания есть атрибут сборщика.
- Явно заданный тип не принимает пользовательское содержимое. Аннотация
DesktopPanel<DefaultPanelContent>намеренно фиксирует тип содержимого. Удалите её, если при вызове должна выводиться другая специализация. - После обновления набора инструментов перестало работать ограничение на основе кортежа. Откажитесь от зависимости от сгенерированной структуры сборщика в пользу именованного стандартного представления или следуйте инструкции Apple по миграции для конкретного ограничения.
Наконец, не перегружайте сборщик. В этой реализации он вызывается при каждой инициализации панели; это не гарантирует однократное выполнение. Apple отмечает, что SwiftUI может повторно инициализировать представления и советует избегать значительной работы в их инициализаторах. Объявление пользовательского представления
В качестве явной отправной точки используйте инициализатор с ограничением: он показывает стандартную специализацию и позволяет легко выводить тип пользовательского содержимого. Переходите к единственному инициализатору со значением по умолчанию, когда требования к компилятору и ограничения вывода подходят вашему API. Оба подхода позволяют панели macOS отвечать за общее оформление, оставляя вызывающей стороне только выбор нужного содержимого.
Источники
- Параметры и аргументы обобщённых типов — Swift.org
- Вывод типов из выражений по умолчанию — Swift Evolution
- Объявление пользовательского представления — документация Apple для разработчиков
- ViewBuilder — документация Apple для разработчиков
- ContentBuilder — документация Apple для разработчиков
- WindowGroup — документация Apple для разработчиков
- TN3211: устранение несовместимостей исходного кода SwiftUI для State и ContentBuilder — документация Apple для разработчиков