Skip to content

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 ControlGroup renders its children side-by-side in a compact, segmented style inside a Menu
  • When to use ControlGroup versus regular Button items in a menu
  • How ControlGroup can contain nested Menu views 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 ​

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 ​

  1. Menu("My Menu") { ... } — The outer menu trigger. Everything inside the trailing closure becomes a menu item. This is the same Menu from lesson #68 — the only addition here is ControlGroup as one of the menu items.

  2. ControlGroup { ... } — Inside a Menu, 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.

  3. Button("Uno") and Button("Dos") — Two side-by-side buttons inside the ControlGroup. In the rendered menu, these appear as two equal-width segments in a compact row, not as two separate lines.

  4. Menu("How are you?") inside ControlGroup — ControlGroup supports 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.

  5. Button("Two") below ControlGroup — A regular menu item. Comparing it visually with the ControlGroup row illustrates the difference: Button("Two") spans the full menu width, while the ControlGroup row is a compact row of segments at the top.

  6. #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 look
ControlGroup 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 ​

  • ControlGroup inside a Menu renders 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 Picker if you need that behavior
  • ControlGroup is context-adaptive: the same code renders as a compact row in menus and as a grouped cluster in toolbars

Last updated: June 27, 2026

Released under the MIT License.