Use sheet(item:onDismiss:content:) with one optional selected item, and build the sheet from the item its closure receives. This gives a Mac app one value that determines both whether to present a sheet and what it displays.

This tutorial implements a small desktop task: click a local sample item and inspect its details immediately. It also provides a diagnostic sequence for a sheet that opens blank, shows the previous selection, or displays stale text inside an AppKit view.

Why use an item-based sheet?

SwiftUI offers two relevant presentation methods. The Boolean overload presents when its binding becomes true. The item overload accepts an optional identifiable value and supplies that value to the content closure. Choose the Boolean form for a fixed panel; choose the item form when the panel’s contents depend on a selection. See Apple’s documentation for Boolean-based sheets and item-based sheets.

A design with separate selection and presentation properties permits combinations such as a requested presentation with no selection. Combining them into one optional value removes that combination from your own state model: nil represents no requested item, and a populated value represents the item to inspect. This is a design deduction from the two APIs, rather than a claim about an undocumented SwiftUI scheduling defect.

If an existing implementation assigns a selection and then toggles a Boolean, those two statements alone do not establish why its first presentation fails. Inspect the content’s data path before attributing the symptom to timing. The useful question is whether the sheet renders its supplied input, reads another property, or displays a separately stored copy.

Build a complete macOS example

Create a macOS app project in Xcode with the SwiftUI interface. Use a deployment target of macOS 12 or later for this example. The item-based sheet API dates to macOS 10.15, while the environment dismissal action used below requires macOS 12. These availability boundaries are documented by Apple for item-based sheets and the dismissal environment value.

Replace the generated app declaration and content view with this complete program. Keep only one app entry point in the target. All labels and descriptions below are synthetic local sample data.

import SwiftUI

struct SampleItem: Identifiable {
    let id: String
    let title: String
    let detail: String
}

@main
struct FirstSheetDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ItemListView()
        }
    }
}

struct ItemListView: View {
    private let items = [
        SampleItem(
            id: "tile-a",
            title: "Amber Tile",
            detail: "A square sample with an amber finish."
        ),
        SampleItem(
            id: "tile-b",
            title: "Blue Tile",
            detail: "A rectangular sample with a blue finish."
        )
    ]

    @State private var presentedItem: SampleItem? = nil

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

            ForEach(items) { item in
                Button("Inspect \(item.title)") {
                    presentedItem = item
                }
            }
        }
        .padding(24)
        .frame(minWidth: 360, minHeight: 200)
        .sheet(item: $presentedItem) { item in
            ItemDetailView(item: item)
        }
    }
}

struct ItemDetailView: View {
    let item: SampleItem
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text(item.title)
                .font(.title)

            Text(item.detail)

            Text("Item ID: \(item.id)")
                .font(.caption)

            HStack {
                Spacer()
                Button("Done") {
                    dismiss()
                }
            }
        }
        .padding(24)
        .frame(width: 380, height: 220)
    }
}

The button assigns the clicked value to presentedItem. The presentation closure then passes its own item parameter into ItemDetailView. Notice that the detail view has a plain input property, let item, rather than a second selected-item state property. Apple recommends keeping a single source of truth and sharing read-only values through properties, using bindings when a child needs write access. See State.

The dismissal action belongs inside ItemDetailView, where it reads the sheet’s environment. Apple explicitly warns that reading it in the presenting view and invoking that action from sheet content uses the presenting view’s environment instead. Keep the declaration beside the sheet’s own content, as shown. See dismiss.

The program has been inspected for consistent inputs and references; it has not been compiled or run here because a Swift toolchain is unavailable in this environment. Run the following checks in Xcode on your target macOS version.

Verify the first presentation with a deliberate click sequence

Start each first-presentation check with a fresh launch. Testing only after opening another item can conceal the original symptom. The following expectations come from the sample’s explicit labels and data assignments; they are acceptance criteria, not reported test results.

Action Expected sheet content What it checks
Launch and click Inspect Blue Tile first Blue Tile, its rectangular description, and tile-b First presentation uses the clicked item
Click Done, then inspect Amber Tile Amber Tile, its square description, and tile-a A subsequent selection reaches the content
Dismiss and inspect Amber Tile again The same amber details Reopening an item remains consistent
Relaunch and inspect Amber Tile first Amber details immediately Correctness does not depend on which sample opens first

Showing both a title and an ID makes this check more useful than checking a title alone. If your real interface allows duplicate display names, use two synthetic items with the same title and different IDs and descriptions during development. That exposes accidental selection by display text.

Diagnose stale content from the button to the final view

Work through these checks in order. Each step narrows the location of the mismatch, so you can change the responsible layer without adding unrelated presentation workarounds.

  1. Check the action’s input. Set a breakpoint in the clicked button’s action and inspect item.id. Confirm that the action receives the intended record.
  2. Check the presentation input. Inspect the item parameter inside the sheet closure. Pass that parameter into the detail view rather than looking up a separate selection again.
  3. Check the final displayed property. Confirm that the title and description read from the detail view’s input. Look for a cached property or local draft containing an earlier value.
  4. Check any AppKit bridge. If ordinary SwiftUI text is correct but wrapped text is stale, inspect the representable’s update method.

Keep identity stable

The item-based API requires Identifiable. Swift defines that protocol around stable identity and leaves its duration and scope to the conforming type and its consumer. The sample uses stored string IDs that distinguish the two records. See Identifiable.

For a real list, choose an identifier that continues to identify the same record across the updates your interface supports. Avoid deriving identity from a title that can change or be shared by multiple items. Also avoid a computed id that creates a fresh UUID every time it is read; that would not provide stable identity.

Separate display input from an editable draft

An inspector can display item.title directly. An editor may intentionally create a local draft, but a draft introduces another value whose lifetime must be defined. Decide when it is created, whether later model changes should replace it, and what happens when the user finishes or cancels.

Apple explains that SwiftUI preserves state storage for a view’s identity and initializes it at the beginning of that identity’s lifetime. Recreating a view value does not inherently create fresh state. Consequently, initializing local state from an input is not a general mechanism for keeping that state synchronized with later inputs. See Demystify SwiftUI.

For this read-only task, keep the plain input property. If you extend it into editing, choose an explicit draft-and-commit design or a binding to the intended model value. Avoid adding a changing view identity solely to refresh text: changing identity replaces associated state, which can discard an unfinished draft. That consequence follows from Apple’s documented state-lifetime model.

What changes when the sheet contains an AppKit text view?

A sheet containing an NSTextView still needs the same selected-item data path. It also needs a correctly implemented bridge. Apple’s NSViewRepresentable protocol separates initial creation in makeNSView(context:) from subsequent configuration in updateNSView(_:context:). A wrapper that assigns its text only during creation has no implementation for applying later text inputs. See NSViewRepresentable.

For a read-only wrapper, initialize the native view with the supplied text and have its update method bring the displayed text into agreement with the current input when necessary. Apple documents that SwiftUI calls the update method for changes affecting the corresponding AppKit view. See updateNSView(_:context:).

An editable wrapper needs an additional policy: do not blindly replace its text on every update and overwrite in-progress edits. Use a coordinator to communicate native delegate or target-action changes back to SwiftUI, as Apple describes in the representable documentation. Let SwiftUI control the represented view’s frame and bounds; Apple warns against directly setting those layout properties.

TextKit addresses text storage, layout, and rendering. Apple documents both the modern NSTextLayoutManager and legacy NSLayoutManager paths exposed by NSTextView. Those facilities do not supply the sheet’s selected record. Establish that the bridge receives the correct item text before investigating text layout, and check availability separately before introducing additional TextKit APIs. See TextKit.

Choose the presentation model that matches the task

Use the Boolean overload when the content is fixed and visibility is the only changing input. Use the item overload for this inspector task. Apple documents that changing the supplied item can dismiss and replace an existing sheet, so do not treat item replacement as a promise of uninterrupted editing within the same presentation. See sheet(item:onDismiss:content:).

For correct first presentation, make the selected record easy to trace: one assignment in the button action, one item parameter in the sheet closure, and one input in the detail view. Verify both sample items from a fresh launch. If the result still differs, the diagnostic sequence identifies whether the mismatch enters through selection, retained state, or the native-view update path.

Sources