SwiftUI patterns: @Bindings

SwiftUI
19 January 2021

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:

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 view title
  • Both Slider and Stepper use V as their generic type, probably as a reference to "Value"
  • TextField uses T as its generic type, probably as a reference to "arbitrary Type" (this is inconsistent with the views above, FB8972305)

Text: Binding<String>

Used in:

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 with String
  • the text Binding parameter always comes either first or second, right after the view title

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 either Color or CGColor
  • DatePicker (twelve initializers), Binding with Date
  • Picker (three initializers), Binding with a generic Hashable 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 picker title

Non-optional binding, optional type

Used in:

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 either Set<SelectionValue> or SelectionValue? type, where SelectionValue is a generic type conforming to Hashable
  • TabView (one initializer), Binding with SelectionValue as a generic Hashable 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, while Binding<SelectionValue?> is used to indicate the possibility to choose up to one element at any given time
  • List is the only(?) view where the selection: 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 their content to distinguish which view belongs to which element/tag

is...: Binding<Bool>

Used in:

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 with Bool
  • 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 view title

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 type V 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 to Hashable
    • 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)
  • 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!

⭑⭑⭑⭑⭑

Related articles

More SwiftUI articles

Browse all