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 ControlGroupControlGroup 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.