Reading time: 2 min

This recipe shows how to add radio buttons in SwiftUI. Each radio button will have a customizable appearance and will be a part of a single radio button group.

The end result looks like this:

preview

First, we'll define a RadioButton view. It has two UI components to it - button and label, both of which are customizable. The button part should change appearance based on if the radio button is selected or not. Also, each radio button has a unique tag associated with it that tells it apart from other radio buttons in a group:

struct RadioButton<Tag, CircleButton, Label>: View
where Tag : Hashable, CircleButton : View, Label : View {
  let tag: Tag
  @Binding var selection: Tag?
  @ViewBuilder let button: (Bool) -> CircleButton
  @ViewBuilder let label: () -> Label

  var body: some View {
    Button {
      selection = tag
    } label: {
      HStack {
        button(selection == tag)
        label()
      }
    }
    .buttonStyle(.plain)
  }
}

Next, a RadioButtonGroup renders a collection of radio buttons, all of whom share the same appearance and the same selection state - only one can be selected at the time. We'll also support two orientation modes - horizontal and vertical, respectively.

struct RadioButtonGroup<Tag, CircleButton, Label>: View 
where Tag : Hashable, CircleButton : View, Label : View {
  @Binding var selection: Tag?
  let orientation: Orientation
  let tags: [Tag]
  @ViewBuilder let button: (Bool) -> CircleButton
  @ViewBuilder let label: (Tag) -> Label

  var body: some View {
    if orientation == .horizonal {
      HStack(alignment: .top) {
        radioButtons
      }
    } else {
      VStack(alignment: .leading) {
        radioButtons
      }
    }
  }

  @ViewBuilder private var radioButtons: some View {
    ForEach(tags, id: \.self) { tag in
      RadioButton(tag: tag, selection: $selection, button: button) {
        label(tag)
      }
    }
  }

  enum Orientation {
    case horizonal, vertical
  }
}

If you're using SwiftUI 4 (iOS 16, macOS 12.4), you can simplify the above code by utilizing AnyLayout:

struct RadioButtonGroup<Tag, CircleButton, Label>: View 
where Tag : Hashable, CircleButton : View, Label : View {
  @Binding var selection: Tag?
  let orientation: Orientation
  let tags: [Tag]
  @ViewBuilder let button: (Bool) -> CircleButton
  @ViewBuilder let label: (Tag) -> Label

  var body: some View {
    ((orientation == .horizonal)
     ? AnyLayout(HStackLayout(alignment: .top))
     : AnyLayout(VStackLayout(alignment: .leading))) {
      ForEach(tags, id: \.self) { tag in
        RadioButton(tag: tag, selection: $selection, button: button) {
          label(tag)
        }
      }
    }
  }

  enum Orientation {
    case horizonal, vertical
  }
}

Lastly, here's some sample code to showcase RadioButtonGroup in action:

struct RadioButtonTest: View {
  @State private var selection: Option?

  var body: some View {
    VStack(alignment: .leading, spacing: 20) {
      RadioButtonGroup(selection: $selection,
                       orientation: .vertical,
                       tags: Option.allCases,
                       button: { isSelected in
        ZStack {
          Circle()
            .foregroundColor(.blue)
            .frame(width: 32, height: 32)
          if isSelected {
            Circle()
              .foregroundColor(Color.white)
              .frame(width: 16, height: 16)
          }
        }
      },
                       label: { tag in
        Text("\(tag.description)")
      })

      Text("The selection is: \(selection?.description ?? "None")")
        .frame(maxWidth: .infinity)
    }
    .padding(20)
  }

  enum Option: CaseIterable, CustomStringConvertible {
    case first, second, third

    var description: String {
      switch self {
      case .first:
        return "First"
      case .second:
        return "Second"
      case .third:
        return "Third"
      }
    }
  }
}

Next Post Previous Post