How to use ControlGroup in SwiftUI | SwiftUI Bootcamp #75
Inside a Menu, buttons normally stack vertically in a list. ControlGroup breaks that pattern by visually grouping a small set of controls side-by-side in a compact segmented row — the same style you see in Notes for text formatting buttons. It's the right choice when two or three tightly related actions belong together visually.
What You'll Learn
- How
ControlGrouprenders its children side-by-side in a compact, segmented style inside aMenu - When to use
ControlGroupversus regularButtonitems in a menu - How
ControlGroupcan contain nestedMenuviews for submenus within the grouped row
Mental Model
Think of a text editor's formatting menu. There are standalone options like "Insert Image" listed one per line. But Bold, Italic, and Underline are shown together in a small segmented control — three buttons huddled in a row because they're closely related formatting tools. You'd tap one of the three to apply it.
ControlGroup recreates that "related controls in a row" pattern inside any SwiftUI Menu. It signals to users that these controls belong together — they're variations of the same concept — without needing separate section headers or labels.
Detailed Explanation
ControlGroup was introduced in iOS 15 and adapts its appearance based on context. Inside a Menu, it renders as a compact, horizontally grouped set of buttons — similar to a SegmentedPickerStyle. In a toolbar, it renders differently (as a grouped toolbar item cluster). This adaptive rendering is what makes ControlGroup worth learning: the same SwiftUI code produces platform-appropriate grouping in all contexts.
The children of ControlGroup can be Button, nested Menu, or Picker views. When the number of controls exceeds roughly four, the grouping can become cramped — three items is the sweet spot. The commented-out "Tres" button in the sample shows where you'd add a third button; uncommenting it creates a three-button row.
ControlGroup does not enforce mutual exclusivity — all buttons remain independently tappable. If you need selection (only one active), use a Picker with a segmented style instead.
Outside a Menu or toolbar, ControlGroup still renders but may appear as a plain VStack or HStack of controls depending on the context. The visual benefit is most pronounced inside menus and toolbars.
Code Structure
ControlGroupMenuBootcamp.swift shows a Menu with three items: a ControlGroup containing two buttons and a nested menu, a standalone button, and a nested menu. This arrangement demonstrates how ControlGroup contrasts with regular menu items — the grouped row looks distinctly compact compared to the full-width button items below it.
Complete Code
ControlGroupMenuBootcamp.swift
import SwiftUI
struct ControlGroupMenuBootcamp: View {
var body: some View {
Menu("My Menu") { // Top-level menu trigger
ControlGroup { // Renders these controls side-by-side in a compact row
Button("Uno") {
// First grouped action — e.g., bold, increase font, retweet
}
Button("Dos") {
// Second grouped action — e.g., italic, decrease font, like
}
// Button("Tres") {
// // Third grouped action — uncomment to see a three-button row
// }
Menu("How are you?") { // Nested menu can live inside ControlGroup too
Button("Good") {
// Action for "Good"
}
Button("Bad") {
// Action for "Bad"
}
}
}
Button("Two") { // Regular full-width menu item — no grouping
// Standalone action
}
Menu("Three") { // Regular submenu — displayed full-width like a normal menu item
Button("Hi") {
// Action for "Hi"
}
Button("Hello") {
// Action for "Hello"
}
}
}
}
}
#Preview {
ControlGroupMenuBootcamp()
}Code Walkthrough
Menu("My Menu") { ... }— The outer menu trigger. Everything inside the trailing closure becomes a menu item. This is the sameMenufrom lesson #68 — the only addition here isControlGroupas one of the menu items.ControlGroup { ... }— Inside aMenu, this renders the enclosed buttons in a compact segmented row instead of as separate full-width items. This is purely a visual grouping — each button still functions independently.Button("Uno")andButton("Dos")— Two side-by-side buttons inside theControlGroup. In the rendered menu, these appear as two equal-width segments in a compact row, not as two separate lines.Menu("How are you?")insideControlGroup—ControlGroupsupports nested menus as children. The "How are you?" item appears in the compact row alongside the buttons, with a chevron indicating it expands to a submenu. This is a more advanced pattern — use it sparingly to avoid confusion.Button("Two")belowControlGroup— A regular menu item. Comparing it visually with theControlGrouprow illustrates the difference:Button("Two")spans the full menu width, while theControlGrouprow is a compact row of segments at the top.#Preview { ... }— Modern Swift macro for previews. Run this in the simulator to see the live menu render — Xcode's canvas may not render the menu dropdown in static preview; you need to tap the trigger.
Common Mistakes
Mistake: Using ControlGroup with more than three or four buttons
More than three buttons in a ControlGroup row can become too small to tap accurately, especially on iPhone. Limit ControlGroup to 2–3 tightly related actions. If you need more, use separate Button items with a Divider() for visual separation.
Mistake: Expecting ControlGroup to enforce single selection like Picker
All buttons in a ControlGroup are independently tappable — none becomes "selected" or inactive. If you need to track which one is active (e.g., bold is currently on/off), use a Picker with a .segmented or .menu style instead.
Mistake: Using ControlGroup outside Menu or toolbar expecting the same compact lookControlGroup adapts its rendering to its container. Inside a Menu or toolbar, it looks like a compact segmented row. In other contexts (a plain VStack), it may just render as a vertical stack of buttons. The visual payoff requires using it in the right context.
Key Takeaways
ControlGroupinside aMenurenders its child controls as a compact segmented row — use it for 2–3 tightly related actions that belong together visually- It does not enforce selection or mutually exclusive states — use
Pickerif you need that behavior ControlGroupis context-adaptive: the same code renders as a compact row in menus and as a grouped cluster in toolbars
Last updated: June 27, 2026