Site icon AppTractor

Лучший способ создания компонентов SwiftUI: @ViewConfigurable

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 { ... }

}

Всё идёт отлично, и люди осваивают этот замечательный новый компонент. И тут появляются запросы:

Теперь этот симпатичный компонент начинает выглядеть вот так:

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, нужно:

  1. Убедиться, что все параметры инициализатора в порядке;
  2. Беспокоиться о том, какие из этих параметров им действительно нужны;
  3. Когда они открывают этот файл, он оказывается длиннющим!

Нативные компоненты 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)

Текстовое поле:

// "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. Он выполняет следующие простые шаги:

  1. Создаёт расширение вашего типа представления с соответствующей областью действия (публичное, внутреннее, приватное)
  2. Проверяет структуру ViewConfiguration
  3. Для каждой переменной в структуре (например, var titleColor: Color)
  4. Создаёт функцию в расширении, используя имя переменной. Например, 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!

Источник

Exit mobile version