Designing custom SwiftUI view APIs like Apple
Declarative SwiftUI is great. Once you get your hands on it, there’s no going back. Public APIs are well defined and composable, clear and concise. They have some quirks, but in general, they are rock solid.
We often face the challenge of implementing custom UI controls.
How can we design these controls to feel as though they were created by Apple?
Let’s have a look at a custom control example from the Bear notes application.

This is a list of tags. A single tag item can have the following properties:
- tag icon
- tag name
- selected/unselected state
The first idea for the implementation could be injecting parameters via a constructor:
struct TagItem: View {
var icon: IconImage
var title: String
var isSelected: Bool
}
// Usage
TagItem(icon: .manga, title: "manga", isSelected: false)
TagItem(icon: .hash, title: "matcha", isSelected: true)
At first glance, the public API looks decent. It’s easy for a client to know what they can configure just by looking at the constructor. On the flip side, because there can be a lot of parameters to customize a control, it can be overwhelming and hard to find what they are looking for.
Max Roche wrote a great article about a better way to build SwiftUI components. He makes the point that a UI component can be divided into mandatory and optional parts.
For instance, a Text component is meaningless without text. If no text is provided, it would simply render a blank space. In contrast, if a text color or font isn’t set, nothing happens because these parameters are optional. The Text component comes with defaults for customizable properties.
Text("Hello, world!") // mandatory
.foregroundStyle(.orange) // optional
We can apply this concept to our TagItem component. A tag requires a title, and the rest of the parameters are optional.
struct TagItem: View {
var icon: IconImage = .hash
var title: String
var isSelected: Bool = false
}
// Usage
TagItem(icon: .manga, title: "manga")
TagItem(title: "matcha", isSelected: true)
Although the API is improved, there’s still room for enhancement. Optional parameters can be styled using dedicated modifiers:
struct TagItem: View {
private var icon: IconImage = .hash
private var title: String
private var isSelected: Bool = false
init(_ title: String) {
self.title = title
}
func icon(_ value: IconImage) -> Self {
var copy = self
copy.icon = value
return copy
}
func isSelected(_ value: IconImage) -> Self {
var copy = self
copy.isSelected = value
return copy
}
}
// Usage
TagItem("manga")
.icon(.manga)
TagItem("matcha")
.isSelected(true)
The API now feels more aligned with SwiftUI, but the work isn’t done yet. We still need to write boilerplate code for each custom property. We can solve it by introducing a custom macro:
@Customizable private var icon: IconImage = .hash
// Macro expands to
func icon(_ value: IconImage) -> Self {
var copy = self
copy.icon = value
return copy
}
It’s uncommon to see optional values in modifiers, so by default, the @Customizable macro converts optional values to non-optional. If we want to retain optionals, we can add additional options to the macro:
@Customizable(preserveOptional: true) private var subtitle: String?
// Macro expands to
func subtitle(_ value: String?) -> Self {
var copy = self
copy.subtitle = value
return copy
}
The @Customizable macro is going to help with many custom properties, but there will be cases where custom modifier does better job. For example, if we want to add accessory view to our TagItem, a manually written modifier would be ideal for this task:
TagItem("matcha")
.accessoryView {
AccessoryView()
}
To summarize, here is how the final implementation for TagItem might look:
struct TagItem: View {
@Customizable private var icon: IconImage
@Customizable private var title: String
@Customizable private var isSelected: Bool
init(_ title: String) { ... }
func accessoryView<Content>(
@ViewBuilder content: () -> Content
) -> some View where Content : View { ... }
}
Transitioning from parameter injection via a constructor to using modifiers for styling optional properties simplifies API usage, improves code readability, and aligns with Apple’s design principles.
A big thanks to Max Roche for writing his article, which was a big inspiration for this one!