@ViewConfigurable - A better way to build SwiftUI components

Max Roche
&
October 7, 2025
7
min. read
Table of Contents
TABLA DE CONTENIDOS
ÍNDICE DE CONTEÚDO

TL;DR: SwiftUI makes customizing views feel effortless — until you build your own reusable components. Here’s how we built a macro to fix that. Try it out with SPM: https://github.com/grindrllc/view-configurable

We’ve all been there before.

You create a nice simple View for your team to use. Perhaps something like this:

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

Everything is going great and people are adopting this awesome new component 🎉. Then, along come some requests:

  • “Can I change the text color of the button?”
  • “Can I change the button background color?”
  • “Product wants this to have a corner radius of 5, not 8”
  • “Hey font needs to be bold. Thx. Have it ready by tomorrow”
  • … and on and on

Now that nice pretty component is starting to look like this

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

Everyone who uses `GrindrButton` now needs to:

  1. Make sure all initializer params are in order
  2. Worry about which of these params they actually need
  3. When they click into this file, it’s a mile long!

Native SwiftUI components are super flexible. Why don’t they have this issue?

At Grindr, we are building out our components in SwiftUI and this was a constant pain-point. To solve it, we asked ourselves “Why are Apple’s components so much cleaner and easier to use?” For example, Apple does not make you hand in the font, color, size, etc into the initializer. That would look like this:

var body: some View {  Text("Hello World", foregroundColor: .black, font: .body, style: .italic, ...)}

Instead, they took a different approach. At a high level, SwiftUI components have a separation between what is required (data) and what is optional (customizations). Let’s look at a few examples:

Text:

  • Required: String
  • Customizations: font, color, font-weight, …etc

// A string (ie - "Hello") is requiredText("Hello")   // Everything below is an optional customization  .font(.body)  .fontWeight(.bold)  .foregroundStyle(.green)

TextField:

  • Required: String, Binding<String>
  • Customizations: style, color, …etc

// "Hello" and $text are requiredTextField("Hello", text: $text)  // Everything below is an optional customization  .textFieldStyle(.roundedBorder)  .foregroundStyle(Color.blue)  // ...etc

Why can’t we have this?

Then we asked ourselves, “why can’t we have this?” Turns out, we can! We just have to write a ton of boilerplate code whenever we want to add another customization. Here is what our initial solution looked like:

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

All of a sudden, we had a SwiftUI-like syntax for our custom components! Consumers of our button could write code that looked like this

var body: some View {  GrindrButton(title: "Click Me", onAction: {})    .textColor(.blue)    .titleFont(.callout)}

What’s the catch?

Well whenever we wanted to add a customization, we need to add a private variable and create a new extension function with the weird mutableSelf syntax. We wondered — can this be automated?

Macros to the rescue 🦸

We wrote a macro called @ViewConfigurable to automate this. Here is how it works:

@ViewConfigurable // Step 1 - apply macro to viewpublic 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)  }}

The macro will automatically generate those extension functions based variable names it sees in ViewConfiguration. It follows these simple steps:

  1. Creates an extension of your view type with the proper scope (public, internal, private)
  2. Inspects the ViewConfiguration struct
  3. For each variable in the structure (i.e. var titleColor: Color)
  4. Creates a function in the extension, using the variable name. For example, var titleColor: Color would create func titleColor(_ value: Color) -> Self

So, for the component above, a consumer could use it like this:

GrindrButton("Click Me", onAction: {})  .titleColor(.blue)  .buttonBackgroundColor(.red)  .titleFont(.callout)// or they could just stick with the defaults 👇GrindrButton("Click Me", onAction: {})

How has it worked so far?

It’s a new way of thinking about component building, but it’s been very helpful for us as a team! Having concise views makes it easier to read and reason about. Also, having small initializers makes it far easier to add components to your view — and then you can customize it just like you would any other SwiftUI view.

How can I use this?

We created a public git repo for this project: https://github.com/grindrllc/view-configurable . Paste this link into SPM to start using the @ViewConfigurable macro!

Share this article
Comparte este artículo
Compartilhe este artigo

Find & Meet Yours

Get 0 feet away from the queer world around you.
Thank you! Your phone number has been received!
Oops! Something went wrong while submitting the form.
We’ll text you a link to download the app for free.
Table of Contents
TABLA DE CONTENIDOS
ÍNDICE DE CONTEÚDO
Share this article
Comparte este artículo
Compartilhe este artigo
“A great way to meet up and make new friends.”
- Google Play Store review
Thank you! Your phone number has been received!
Oops! Something went wrong while submitting the form.
We’ll text you a link to download the app for free.
“A great way to meet up and make new friends.”
- Google Play Store review
Discover, navigate, and get zero feet away from the queer world around you.
Descubre, navega y acércate al mundo queer que te rodea.
Descubra, navegue e fique a zero metros de distância do mundo queer à sua volta.
Already have an account? Login
¿Ya tienes una cuenta? Inicia sesión
Já tem uma conta? Faça login

Browse bigger, chat faster.

Find friends, dates, hookups, and more

Featured articles

Artículos destacados

Artigos em Destaque

Related articles

Artículos relacionados

Artigos Relacionados

No items found.

Find & Meet Yours

Encuentra y conoce a los tuyos

Encontre o Seu Match Perfeito

4.6 · 259.4k Raiting
4.6 · 259.4k valoraciones
4.6 · 259.4k mil avaliações