TL;DR: SwiftUI делает настройку представлений лёгкой — пока вы не создадите собственные повторно используемые компоненты. Так мы создали макрос для решения этой проблемы. Попробуйте с SPM: https://github.com/grindrllc/view-configurable.
Мы все через это проходили.
Вы создаёте удобное и простое представление для своей команды. Возможно, что-то вроде этого:
public struct GrindrButton: some View {
private let title: String
private let onAction: () -> Void
public init(title: String, onAction: @escaping () -> Void) {
self.title = title
self.onAction = onAction
}
public var body: some View { ... }
}
Всё идёт отлично, и люди осваивают этот замечательный новый компонент. И тут появляются запросы:
- Можно ли изменить цвет текста кнопки?
- Можно ли изменить цвет фона кнопки?
- Продукт хочет, чтобы радиус скругления углов был 5, а не 8
- Эй, шрифт должен быть жирным. Спасибо. Подготовьте его к завтрашнему дню
- … и так далее.
Теперь этот симпатичный компонент начинает выглядеть вот так:
public struct GrindrButton: some View {
private let title: String
private let backgroundColor: Color
private let textColor: Color
private let font: Font
private let cornerRadius: CGFloat
private let depressAnimation: Animation = .default
private let onAction: () -> Void
public init(title: String,
backgroundColor: Color = .yellow,
textColor: Color = .black,
font: Font = .body,
cornerRadius: CGFloat = 5,
depressAnimation: Animation = .default
// ... more configurations
onAction: @escaping () -> Void) {
self.title = title
self.backgroundColor = backgroundColor
self.textColor = textColor
self.font = font
/// ...etc
}
public var body: some View { ... }
}
Теперь всем, кто использует GrindrButton, нужно:
- Убедиться, что все параметры инициализатора в порядке;
- Беспокоиться о том, какие из этих параметров им действительно нужны;
- Когда они открывают этот файл, он оказывается длиннющим!
Нативные компоненты SwiftUI невероятно гибкие. Почему же у них нет такой проблемы?
В Grindr мы разрабатываем наши компоненты на SwiftUI, и это было постоянной проблемой. Чтобы решить её, мы задались вопросом: «Почему компоненты Apple настолько понятнее и проще в использовании?» Например, Apple не заставляет вас вводить шрифт, цвет, размер и т.д. в инициализатор. Это бы выглядело так:
var body: some View {
Text("Hello World", foregroundColor: .black, font: .body, style: .italic, ...)
}
Вместо этого они выбрали другой подход. На высоком уровне компоненты SwiftUI разделяют required (данные) и optional (настройки) параметры. Рассмотрим несколько примеров.
Текст:
- Обязательно: Строка
- Настройки: шрифт, цвет, начертание и т. д.
// A string (ie - "Hello") is required
Text("Hello")
// Everything below is an optional customization
.font(.body)
.fontWeight(.bold)
.foregroundStyle(.green)
Текстовое поле:
- Обязательно: Строка, Binding<String>
- Настройки: стиль, цвет и т.д.
// "Hello" and $text are required
TextField("Hello", text: $text)
// Everything below is an optional customization
.textFieldStyle(.roundedBorder)
.foregroundStyle(Color.blue)
// ...etc
Почему у нас не может быть так же?
Затем мы спросили себя: «Почему у нас не может быть так же?» Оказалось, что может! Нам просто приходится писать кучу шаблонного кода каждый раз, когда мы хотим добавить ещё одну настройку. Вот как выглядело наше первоначальное решение:
public struct GrindrButton: View {
private let title: String
private let onAction: () -> Void
// customizations
private var textColor: Color = .blue
private var font: Font = .body
public init(title: String, onAction: @escaping () -> Void) {
self.title = title
self.onAction = onAction
}
var body: some View { // ... }
}
public extension GrindrButton {
func textColor(_ color: Color) -> Self {
var mutableSelf = self
mutableSelf.textColor = color
return mutableSelf
}
func titleFont(_ font: Font) -> Self {
var mutableSelf = self
mutableSelf.font = font
return mutableSelf
}
}
Внезапно у нас появился синтаксис, похожий на SwiftUI, для наших пользовательских компонентов! Пользователи нашей кнопки могли писать код, который выглядит так:
var body: some View {
GrindrButton(title: "Click Me", onAction: {})
.textColor(.blue)
.titleFont(.callout)
}
В чём подвох?
Всякий раз, когда мы хотели добавить настройку, нам нужно было добавить приватную переменную и создать новую функцию расширения со странным синтаксисом mutableSelf. Мы задались вопросом: можно ли это автоматизировать?
Макросы спешат на помощь .
Мы написали макрос @ViewConfigurable для автоматизации этого процесса. Вот как он работает:
@ViewConfigurable // Step 1 - apply macro to view
public struct GrindrButton: some View {
// required
private let title: String
private let onAction: () -> Void
// configurable
private var config = ViewConfiguration() // Step 2 - add this var
public struct ViewConfiguration { // Step 3 - declare your ViewConfiguration struct
var titleColor: Color = .black
var buttonBackgroundColor: Color = .yellow
var titleFont: Font = .body
// ..etc
}
public init(title: String, onAction: @escaping () -> Void) { ... }
public var body: some View {
// Step 4 - Use the "config" var for customizations
Button(action: onAction) {
Text(title)
.font(config.titleFont)
.foregroundStyle(config.titleColor)
}
.background(config.buttonBackgroundColor)
}
}
Макрос автоматически генерирует эти функции расширения на основе имён переменных, которые он видит в ViewConfiguration. Он выполняет следующие простые шаги:
- Создаёт расширение вашего типа представления с соответствующей областью действия (публичное, внутреннее, приватное)
- Проверяет структуру
ViewConfiguration - Для каждой переменной в структуре (например,
var titleColor: Color) - Создаёт функцию в расширении, используя имя переменной. Например,
var titleColor: Colorсоздаст функциюfunc titleColor(_ value: Color) -> Self
Таким образом, для компонента выше потребитель может использовать его следующим образом:
GrindrButton("Click Me", onAction: {})
.titleColor(.blue)
.buttonBackgroundColor(.red)
.titleFont(.callout)
// or they could just stick with the defaults
GrindrButton("Click Me", onAction: {})
Как это работает?
Это новый подход к созданию компонентов, но он очень помог нам как команде! Лаконичные представления облегчают чтение и анализ. Кроме того, небольшие инициализаторы значительно упрощают добавление компонентов в представление, после чего вы можете настраивать его так же, как любое другое представление SwiftUI.
Как это использовать?
Мы создали публичный репозиторий для этого проекта. Вставьте эту ссылку в SPM, чтобы начать использовать макрос @ViewConfigurable!

