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),Vmust conform toBinaryFloatingPointStepper(six initializers),Vmust conform toStrideableTextField(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
valueBindingis always associated with generic types - the
valueBindingmay (or not) require a protocol conformance to its associated generic type - the
valueBindingparameter always comes either first or second, right after the viewtitle - Both
SliderandStepperuseVas their generic type, probably as a reference to "Value" TextFieldusesTas 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
textBindingparameter is always associated withString - the
textBindingparameter 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),Bindingwith eitherColororCGColorDatePicker(twelve initializers),BindingwithDatePicker(three initializers),Bindingwith a genericHashableSelectionValue
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
selectionBindingwith non-optional types are used exclusively by (all) SwiftUI pickers - the
selectionBindingparameter always comes either first or second, right after the pickertitle
Non-optional binding, optional type
Used in:
NavigationLink(three initializers),Bindingwith a genericHashableV?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),Bindingwith eitherSet<SelectionValue>orSelectionValue?type, whereSelectionValueis a generic type conforming toHashableTabView(one initializer),BindingwithSelectionValueas a genericHashabletype
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
SelectionValuenaming is back, used to indicate something that can be selected/picked - similar to all pickers,
TabViewrequires a value to be selected at any given time - in
Listcase,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 Listis 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
Hashabletags (explicitly or implicitly), used in theircontentto 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...Bindingparameter is always associated withBool - the
is...Bindingparameter always represents straightforward yes/no states - there's not a single repetition, each view defines its own word (
isActive/isExpanded/isOn) - the
is...Bindingparameter 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 typeVto 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
SelectionValueas 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,isOnetc) - 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!