SwiftUI patterns: @Bindings
Every time we create a new view, we're defining a new API: regardless of whether those views will be used just by us or by several people around the world, it's very important to keep all definitions consistent.
More importantly, our views signature should follow SwiftUI's own definitions and patterns:
doing so ensures that using our views feels natural, as anyone familiar with SwiftUI will automatically be familiar with our definitions.
In order to achieve this, we need to analyze and understand SwiftUI's APIs: let's start by exploring SwiftUI's use of @Binding in views initializers!
This article contains quite a bit of definitions and examples, if you'd rather have a TL:DR; check out the "Main takeaways" chapter at the bottom.
Value: Binding<V>
Used in:
Slider
(six initializers),V
must conform toBinaryFloatingPoint
Stepper
(six initializers),V
must conform toStrideable
TextField
(two initializers)
Examples:
// MARK: Slider
// Definition
extension Slider {
public init<V>(
value: Binding<V>,
in bounds: ClosedRange<V> = 0...1,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint
}
// Use
struct ContentView: View {
@State var value: Float = 0
var body: some View {
Slider(value: $value)
}
}
// MARK: Stepper
// Definition
extension Stepper {
public init<V: Strideable>(
_ titleKey: LocalizedStringKey,
value: Binding<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
)
}
// Use
struct ContentView: View {
@State var value: Float = 0
var body: some View {
Stepper("Stepper title", value: $value)
}
}
// MARK: TextField
// Definition
extension TextField {
public init<S: StringProtocol, T>(
_ title: S,
value: Binding<T>,
formatter: Formatter,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}
)
}
// Use
struct ContentView: View {
@State private var nameComponents = PersonNameComponents()
var body: some View {
TextField(
"Person name:",
value: $nameComponents,
formatter: PersonNameComponentsFormatter()
)
}
}
Notes:
- the
value
Binding
is always associated with generic types - the
value
Binding
may (or not) require a protocol conformance to its associated generic type - the
value
Binding
parameter always comes either first or second, right after the viewtitle
- Both
Slider
andStepper
useV
as their generic type, probably as a reference to "Value" TextField
usesT
as its generic type, probably as a reference to "arbitrary Type" (this is inconsistent with the views above, FB8972305)
Text: Binding<String>
Used in:
SecureField
(two initializers)TextEditor
(one initializer)TextField
(one initializer)
Examples:
// MARK: SecureField
// Definition
extension SecureField {
public init(
_ titleKey: LocalizedStringKey,
text: Binding<String>,
onCommit: @escaping () -> Void = {}
)
}
// Use
struct ContentView: View {
@State var password = ""
var body: some View {
TextField("Password:", text: $password)
}
}
// MARK: TextEditor
// Definition
extension TextEditor {
public init(text: Binding<String>)
}
// Use
struct ContentView: View {
@State var text = ""
var body: some View {
TextEditor(text: $text)
}
}
// MARK: TextField
// Definition
extension TextField {
public init(
_ titleKey: LocalizedStringKey,
text: Binding<String>,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}
)
}
// Use
struct ContentView: View {
@State var name = ""
var body: some View {
TextField("Name:", text: $name)
}
}
Notes:
- the
text
Binding
parameter is always associated withString
- the
text
Binding
parameter always comes either first or second, right after the viewtitle
Selection: Binding
The selection: Binding<...>
pattern is the most common pattern found in SwiftUI, for this reason the section has been further split in multiple chapters.
Non-optional binding, non-optional type
Used in:
ColorPicker
(six initializers),Binding
with eitherColor
orCGColor
DatePicker
(twelve initializers),Binding
withDate
Picker
(three initializers),Binding
with a genericHashable
SelectionValue
Examples:
// MARK: ColorPicker
// Definition
extension ColorPicker {
public init(
_ titleKey: LocalizedStringKey,
selection: Binding<Color>,
supportsOpacity: Bool = true
)
}
// Use
struct ContentView: View {
@State var color: Color = .yellow
var body: some View {
ColorPicker("Choose color:", selection: $color)
}
}
// MARK: DatePicker
// Definition
extension DatePicker {
public init(
_ titleKey: LocalizedStringKey,
selection: Binding<Date>,
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date]
)
}
// Use
struct ContentView: View {
@State var date = Date()
var body: some View {
DatePicker("Your birthday:", selection: $date)
}
}
// MARK: Picker
// Definition
extension Picker {
public init(
selection: Binding<SelectionValue: Hashable>,
label: Label,
@ViewBuilder content: () -> Content
)
}
// Use
struct ContentView: View {
enum PizzaTopping: String, CaseIterable, Identifiable {
case 🍍, 🍄, 🫒, 🐓
var id: String { rawValue }
}
@State var pizzaTopping: PizzaTopping = .🍍
var body: some View {
Picker(
selection: $pizzaTopping,
label: Text("Best pizza topping:")
) {
ForEach(PizzaTopping.allCases) { flavor in
Text(flavor.rawValue)
}
}
}
}
Notes:
- all non-optional
selection
Binding
with non-optional types are used exclusively by (all) SwiftUI pickers - the
selection
Binding
parameter always comes either first or second, right after the pickertitle
Non-optional binding, optional type
Used in:
NavigationLink
(three initializers),Binding
with a genericHashable
V?
type
Examples:
// Definition
extension NavigationLink {
public init<V: Hashable>(
destination: Destination,
tag: V,
selection: Binding<V?>,
@ViewBuilder label: () -> Label
)
}
// Use
struct ContentView: View {
enum ScreenNavigation: Hashable {
case a, b
}
@State var showingNavigation: ScreenNavigation? = nil
var body: some View {
NavigationView {
NavigationLink(
destination: Text("Screen A"),
tag: .a,
selection: $showingNavigation,
label: { Text("Go to Screen A") }
)
}
}
}
Notes:
As this is used only in one view, and as we covered in Hashable SwiftUI bindings and The future of SwiftUI navigation (?), this can be considered an exception instead of a pattern.
Optional binding
Used in:
List
(twelve initializers),Binding
with eitherSet<SelectionValue>
orSelectionValue?
type, whereSelectionValue
is a generic type conforming toHashable
TabView
(one initializer),Binding
withSelectionValue
as a genericHashable
type
Examples:
// MARK: List
// Definition
extension List {
public init(
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder content: () -> Content
)
}
// Use
struct ContentView: View {
enum MyListElement: Hashable {
case a, b, c
}
@State var selectedElements: Set<MyListElement> = []
var body: some View {
List(selection: $selectedElements) {
Text("Element a").tag(MyListElement.a)
Text("Element b").tag(MyListElement.b)
Text("Element c").tag(MyListElement.c)
}
.environment(\.editMode, .constant(EditMode.active))
.onReceive(selectedElements.publisher, perform: { _ in
print("Selected elements: \(selectedElements)")
})
}
}
// MARK: TabView
// Definition
extension TabView {
public init(
selection: Binding<SelectionValue>?,
@ViewBuilder content: () -> Content
)
}
// Use
struct ContentView: View {
enum MyTab: Hashable {
case home, news, settings
}
@State var selectedTab: MyTab = .home
var body: some View {
TabView(selection: $selectedTab) {
Text("Home View").tabItem { Label("Home", systemImage: "house") }.tag(MyTab.home)
Text("News View").tabItem { Label("News", systemImage: "newspaper") }.tag(MyTab.news)
Text("Settings View").tabItem { Label("Settings", systemImage: "gear") }.tag(MyTab.settings)
}
}
}
Notes:
- all these views work with or without passing a binding (refer to Adding optional @Bindings to SwiftUI views for more details)
- the
SelectionValue
naming is back, used to indicate something that can be selected/picked - similar to all pickers,
TabView
requires a value to be selected at any given time - in
List
case,Binding<Set<SelectionValue>>
is used to show the possibility to have multiple selections at the same time, whileBinding<SelectionValue?>
is used to indicate the possibility to choose up to one element at any given time List
is the only(?) view where theselection: Binding<..>?
parameter can be found at the 2nd, 3rd and 4th position (depending on the initializer)- all these views work with
Hashable
tags (explicitly or implicitly), used in theircontent
to distinguish which view belongs to which element/tag
is...: Binding<Bool>
Used in:
NavigationLink
(four initializer),isActive: Binding<Bool>
DisclosureGroup
(tree initializers),isExpanded: Binding<Bool>
Toggle
(tree initializers),isOn: Binding<Bool>
Examples:
// MARK: NavigationLink
// Definition
extension NavigationLink {
public init(
destination: Destination,
isActive: Binding<Bool>,
@ViewBuilder label: () -> Label
)
}
// Use
struct ContentView: View {
@State var showingDetails = false
var body: some View {
NavigationView {
NavigationLink(
destination: Text("Detail Screen"),
isActive: $showingDetails,
label: {
Text("See more details")
}
)
}
}
}
// MARK: DisclosureGroup
// Definition
extension DisclosureGroup {
public init(
_ titleKey: LocalizedStringKey,
isExpanded: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content
)
}
// Use
struct ContentView: View {
@State var showingDetails = false
var body: some View {
DisclosureGroup("See more details", isExpanded: $showingDetails) {
Text("More details here")
}
}
}
// MARK: Toggle
// Definition
extension Toggle {
public init(isOn: Binding<Bool>, @ViewBuilder label: () -> Label)
}
// Use
struct ContentView: View {
@State var isOn = false
var body: some View {
Toggle(isOn: $isOn) {
Text("Add extra topping:")
}
}
}
Notes:
- the
is...
Binding
parameter is always associated withBool
- the
is...
Binding
parameter always represents straightforward yes/no states - there's not a single repetition, each view defines its own word (
isActive
/isExpanded
/isOn
) - the
is...
Binding
parameter always comes either first or second, right after the viewtitle
Main takeaways
In this article we've explored all SwiftUI views that accept a @Binding
parameter, here are some of the most important takeaways:
- use
value: Binding<V>
for generic bindings, require the generic typeV
to conform to protocols if needed - use
text: Binding<String>
for bindings associated with text - use
selection: Binding<...>
when zero, one, or more elements can be selected/picked- when binding to generic types, use
SelectionValue
as the type name - nearly all
selection: Binding<..>
require the associated type to conform toHashable
- when a value must always be selected at any given time (like in all SwiftUI pickers), associate the binding with a non-optional type,
selection: Binding<Color>
for example - when zero or up to one element can be picked, associate the binding with an optional type,
selection: Binding<Color?>
for example - when zero or more elements can be picked, associate the binding with a set,
selection: Binding<Set<SelectionValue>>
for example - when the view can manage the selection state by itself but also expose such selection externally, offer an optional binding,
selection: Binding<SelectionValue>?
for example (refer to Adding optional @Bindings to SwiftUI views for guidance on how to do this)
- when binding to generic types, use
- use
is...: Binding<Bool>
for simple yes/no states (isActive
,isExpanded
,isOn
etc) - no view accepts more than one
@Binding
- if the view has/accepts a
title
, make sure its the first parameter of the initializer - most bindings are the first parameter of a view (or second if the view has a
title
) - if the view accepts optional closures (e.g.
onCommit
,onEditingChanged
), provide a default implementation and put such parameters at the end of the initializer - if your case doesn't match any of the above, create your own pattern and be consistent with it
Conclusions
SwiftUI is a young framework, there's no doubt that new patters will emerge as it grows, and old patterns will sunset as it evolves.
When expanding SwiftUI with our own definitions, we should be careful and try to follow and respect the patterns that the framework gives us as best as we can.
What other patterns have you spotted while using SwiftUI? Do you like/use any in particular? What patterns would you like me to cover next? Please let me know!
Thank you for reading and stay tuned for more articles!