For a small row of labels in a Mac app, use ViewThatFits to choose the longest complete prefix that fits, with its hidden count included. This tutorial builds that row without truncating labels or updating state during layout.

The concrete task is a resizable macOS window containing six synthetic labels. Preserve their order, show only complete labels, and display a badge such as 3 more for the omitted suffix. The solution assumes enough horizontal space for the count-only badge and enough vertical space for one row.

Choose complete row alternatives

Apple documents ViewThatFits as selecting the first child whose ideal size fits the proposed size on the constrained axes. It is available on macOS 13 and later. Use .horizontal here because the task concerns row width; the surrounding interface must provide the row’s height. ViewThatFits

For six labels, supply seven alternatives in this order: all six labels without a badge, five labels plus 1 more, four plus 2 more, and so on, ending with only 6 more. Each alternative is a complete row. Its badge participates in the fit decision, so adding the count cannot unexpectedly displace the last visible label.

This implements a prefix policy: a long first label can cause every label to be omitted, even if a later short label would fit. That is intentional. If your task instead permits skipping individual labels, define that different selection policy before implementing it.

Apply fixedSize() to each candidate row so its labels and badge retain their ideal dimensions. Apple explains that this modifier can make a view exceed its parent’s bounds; it does not independently guarantee a fit. Here, the enclosing selection container performs the fit decision. fixedSize()

Build the complete macOS example

Create a macOS SwiftUI app in Xcode and set its deployment target to macOS 13 or later. Replace the generated app declaration and content view with the following single-file program. Keep exactly one app declaration marked @main in the target.

The example uses explicit identifiers rather than label text as identity. Apple’s ForEach documentation requires identified elements or an explicit identifier key path; distinct identifiers also allow two items to share the same displayed title. ForEach

import SwiftUI

struct DemoItem: Identifiable {
    let id: Int
    let title: String
}

struct ItemChip: View {
    let title: String

    var body: some View {
        Text(title)
            .lineLimit(1)
            .padding(.horizontal, 10)
            .padding(.vertical, 6)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(Color.blue.opacity(0.12))
            )
    }
}

struct PrefixCandidate: View {
    let items: [DemoItem]
    let visibleCount: Int

    var body: some View {
        HStack(spacing: 8) {
            ForEach(items.prefix(visibleCount)) { item in
                ItemChip(title: item.title)
            }

            if visibleCount < items.count {
                Text("\(items.count - visibleCount) more")
                    .lineLimit(1)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.15))
                    )
            }
        }
        .fixedSize()
    }
}

struct WholeItemRow: View {
    let items: [DemoItem]

    var body: some View {
        if items.isEmpty {
            Text("No items")
                .fixedSize()
        } else {
            ViewThatFits(in: .horizontal) {
                ForEach(
                    Array((0...items.count).reversed()),
                    id: \.self
                ) { visibleCount in
                    PrefixCandidate(
                        items: items,
                        visibleCount: visibleCount
                    )
                }
            }
        }
    }
}

struct ContentView: View {
    let items: [DemoItem] = [
        DemoItem(id: 1, title: "Sketch"),
        DemoItem(id: 2, title: "Color samples"),
        DemoItem(id: 3, title: "Shapes"),
        DemoItem(id: 4, title: "Paper textures"),
        DemoItem(id: 5, title: "Layout notes"),
        DemoItem(id: 6, title: "Preview")
    ]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Sample labels")
                .font(.headline)

            WholeItemRow(items: items)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("Resize the window to change how many labels fit.")
                .foregroundStyle(.secondary)
        }
        .font(.system(size: 14))
        .padding(24)
        .frame(minWidth: 320, idealWidth: 680, minHeight: 160)
    }
}

@main
struct WholeItemsDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

The candidate identifiers are the possible visible counts, descending from the item count to zero. Within each candidate, item identifiers identify the labels. Each iteration creates one PrefixCandidate, which contains one horizontal row.

The outer flexible frame lets the row receive the available horizontal space while keeping the selected content aligned to the leading edge. Keep fixedSize() on the candidate inside ViewThatFits. Applying it to the entire selection container would change how that container responds to its parent’s width proposal.

This is a complete source example, but it was not compiled or run here because a Swift toolchain was unavailable. The illustrative arithmetic in the next section was checked separately; actual text widths must be verified in the Mac app.

Include the badge when calculating what fits

The useful decision rule is: select the greatest visible count whose entire candidate fits. The hidden count is always the total item count minus that candidate’s visible count. There is no separate estimate to synchronize.

Consider an illustrative four-item row with widths of 72, 96, 64, and 88 points, measured including item padding. Assume 8-point spacing and a 60-point badge for every nonzero hidden count. These are invented calculation inputs, not measurements of the sample app.

When all four items are visible, their required width is 72 + 96 + 64 + 88 + 3 × 8 = 344 points. With three visible items, the badge replaces the omitted suffix, so the width becomes 72 + 96 + 64 + 3 × 8 + 60 = 316 points. Three gaps are needed because the candidate contains three items and one badge.

Available width Visible items Hidden items Selected candidate width
344 points 4 0 344 points
343 points 3 1 316 points
300 points 2 2 244 points
220 points 1 3 140 points
60 points 0 4 60 points

In the program, each candidate contains its actual badge text. It therefore accommodates differences between badge strings without relying on the illustration’s constant-width assumption. Candidate widths need not change by a uniform amount; descending visible count expresses the selection preference.

The count-only candidate establishes the minimum usable width. If even that badge cannot fit, showing the full count within the available width is impossible under these rules. Provide sufficient minimum width, allow a different arrangement, or move the count elsewhere. Do not treat clipping as a successful whole-item result.

The sample supplies a minimum content width and a modest explicit font for its short synthetic badge. If you change the wording, font, padding, or surrounding layout, verify the count-only case again. A minimum width chosen for this example is not a universal badge measurement.

Keep layout selection separate from view lifecycle

The hidden count belongs to each candidate’s immutable inputs. The program does not write a binding from a measurement method, schedule an asynchronous count update, or infer visibility from appearance callbacks. Choosing the row also chooses its matching badge.

Keep the repeated candidate content simple. Repeating a label across alternatives does not establish one shared identity for that label across all alternatives. Apple explains that state storage is associated with view identity and that changing identity can replace state. Demystify SwiftUI

If you later replace the labels with editable controls, keep their shared values in a common ancestor or model and pass the appropriate inputs or bindings into each presentation. Apple recommends placing shared state in the least common ancestor of the views that need it. Do not depend on candidate-local state surviving a change of presentation. Managing user interface state

This solution displays the hidden count inside the selected row. It does not expose the selected count to a sibling view or application model. Apple’s documented ViewThatFits initializer accepts constrained axes and candidate content; it provides no selected-count binding. Treat exporting that value as an additional design requirement. init(in:content:)

Know when a custom layout needs a different design

A custom Layout is appropriate when you need specialized geometry. Apple defines its core responsibilities as reporting the composite size and assigning subview positions through layout proxies. The proxies expose measurement and placement operations rather than direct access to the underlying views. Layout

Do not implement omission simply by stopping placement after the last item that fits. Apple’s LayoutSubview documentation instructs you to place each subview once. Skipping placement is not a documented hiding mechanism, and a layout proxy does not provide a general content-rewriting operation for turning another subview into a new count label. LayoutSubview

If you build a custom layout, preserve agreement between measurement and placement, including spacing and the size proposal used. Apple explicitly requires those computations to be consistent. That contract still applies when the layout probes several possible arrangements. placeSubviews(in:proposal:subviews:cache:)

The candidate approach is deliberately bounded. For six items it describes seven candidate rows and 21 item appearances across those rows. In general, the number of item appearances grows as n × (n + 1) / 2. This is a structural count, not a performance benchmark or a claim that every candidate renders on every update. Use it for a small label collection; profile before extending it to a large collection or expensive content.

Verify the row at its boundaries

Run the app and resize its window slowly. Check the following conditions before adapting the component:

  • At a wide width, every label appears and there is no 0 more badge.
  • As the window narrows, labels disappear from the end without partial labels or ellipses.
  • The visible item count plus the badge’s hidden count always equals six.
  • With one exceptionally long first label, the count-only candidate remains available.
  • With an empty array, the row displays No items.
  • With duplicate titles and distinct identifiers, both items remain represented in the count.
  • After changing fonts or badge wording, the count-only candidate still fits the smallest supported width.

For a small, ordered row, complete alternatives make the fit rule easy to inspect: each candidate includes its labels, spacing, and exact hidden count. Keep the minimum-width condition explicit and the repeated content lightweight, and the row can adapt to window resizing without partial items or a separate count-update cycle.

Sources