Own shared observable state in your SwiftUI App, then pass the same instance into both scenes. Keep each window’s draft inside its root view. This lets a menu bar control update every window while each window retains its own unfinished text.

This tutorial builds that arrangement for a small Mac label editor: multiple windows show one shared label, each window has an independent draft, and a menu bar panel can edit the shared label or open another window. All example text belongs to the app and uses synthetic labels.

Decide which changes should reach every window

Start with the expected behavior of each value. A shared label should change everywhere when edited. An unfinished draft should change only in the window where someone types. These requirements determine where the values belong.

Apple documents that each window created by a WindowGroup has independent storage for state instantiated inside its view hierarchy. By contrast, Apple’s model-data guidance demonstrates owning an observable reference in the top-level App and passing it into a view hierarchy. Combining those patterns gives this ownership plan. WindowGroup, Managing model data in your app.

Value Owner in this example Intended behavior
Shared label App, using @State with an @Observable model Every window and the menu bar panel edit the same value
Unfinished draft Each window’s root view, using @State Typing stays within that window
Menu bar controls Receive the app-owned model Changes reach all consumers of that instance
Window-opening action Read from the menu bar view’s environment Opens a window from the identified group

The distinction is the instance’s owner. Giving two separately created objects the same class name does not make their contents shared. Likewise, placing a reference to one app-owned object inside several windows does not create independent copies of that object. This is why the example creates its shared model once at the app level.

Set the macOS baseline before adding the scenes

Use a macOS SwiftUI app project with a deployment target of macOS 14 or later and an Xcode toolchain that supports Observation, such as Xcode 15 or later. SwiftUI’s Observation integration starts at macOS 14. MenuBarExtra and its window style are available from macOS 13, so Observation sets the higher baseline for this implementation. Managing model data in your app, MenuBarExtra, window.

The menu bar panel uses .menuBarExtraStyle(.window) because it contains text fields and a custom layout. Apple describes this style as a popover-like window with controls styled similarly to those in a normal window. It is a separate scene from the editor’s WindowGroup. window.

The example also explicitly isolates its model and UI types to @MainActor. Apple defines the main actor as having an executor equivalent to the main dispatch queue. Here, all mutations originate from UI controls; no background work is required. MainActor.

Build the shared model and independent window drafts

Replace the generated app entry point with the following complete single-file example. Keep only one @main declaration in the target. The sample includes its imports, model, views, and initial text. It was inspected against the documented APIs but could not be compiled in this environment because a Swift toolchain was unavailable.

import SwiftUI
import Observation

@MainActor
@Observable
final class LabelStore {
    var sharedLabel = "Sample Alpha"
}

@main
@MainActor
struct LabelScopeApp: App {
    @State private var store = LabelStore()

    var body: some Scene {
        WindowGroup("Label Editor", id: "label-editor") {
            EditorWindow()
                .environment(store)
        }

        MenuBarExtra("Label Controls", systemImage: "square.grid.2x2") {
            LabelMenuPanel()
                .environment(store)
        }
        .menuBarExtraStyle(.window)
    }
}

@MainActor
struct EditorWindow: View {
    @Environment(LabelStore.self) private var store
    @State private var draft = "Sample Draft"

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            SharedLabelControls()

            Divider()

            TextField("This window's draft", text: $draft)
                .textFieldStyle(.roundedBorder)

            Button("Use Draft as Shared Label") {
                store.sharedLabel = draft
            }

            Text("Publishing the draft keeps its local text intact.")
                .font(.caption)
        }
        .padding(20)
        .frame(minWidth: 420, minHeight: 220)
    }
}

@MainActor
struct SharedLabelControls: View {
    @Environment(LabelStore.self) private var store

    var body: some View {
        @Bindable var editableStore = store

        VStack(alignment: .leading, spacing: 8) {
            Text("Shared label: \(store.sharedLabel)")

            TextField("Shared label", text: $editableStore.sharedLabel)
                .textFieldStyle(.roundedBorder)
        }
    }
}

@MainActor
struct LabelMenuPanel: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            SharedLabelControls()

            Button("Open Another Editor") {
                openWindow(id: "label-editor")
            }
        }
        .padding(16)
        .frame(width: 320)
    }
}

There are two explicit environment injections: one on the window content and one on the menu bar content. Both receive store. Do not expect an environment modifier attached to a window’s content to provide a dependency to a sibling scene. Apple documents environment sharing through a view hierarchy; the two injections make each hierarchy’s dependency explicit. Managing model data in your app.

Use @Bindable where a control needs a binding

SharedLabelControls reads the model through @Environment(LabelStore.self). Inside its body, the local @Bindable variable supplies the projected binding required by TextField. It refers to the existing observable object and does not create another store. Apple documents this exact environment-to-binding pattern. Bindable.

The draft field instead uses $draft, because its value is already owned by the window view’s @State. The button copies the current draft into the shared label. That is an explicit action: later changes to the shared label do not rewrite either window’s draft. These are the sample’s chosen editing semantics.

Match the window group identifier exactly

The menu bar button obtains openWindow from its own environment and calls it with "label-editor", matching the group declaration. Apple documents this identifier-based approach for creating a window from a group. The button’s purpose is to open another editor, so this example does not promise to locate and reuse a particular existing window. WindowGroup.

Verify the scope with a two-window sequence

The following sequence is an expected-behavior check for the sample. It distinguishes shared model identity from independent draft storage without needing additional instrumentation.

  1. Launch the app. The first editor shows the shared label Sample Alpha and draft Sample Draft.
  2. Open the menu bar panel and choose Open Another Editor.
  3. Change the first window’s draft to Draft One. Leave the second window’s draft unchanged.
  4. In the menu bar panel, change the shared label to Sample Beta. Both editor windows should display that shared label, while retaining their different draft values.
  5. In the first window, choose Use Draft as Shared Label. The shared label becomes Draft One in both windows and the menu bar panel. The second window’s draft remains Sample Draft.

This sequence checks three distinct paths: local typing, a menu bar edit, and an explicit local-to-shared copy. If local typing changes the other window’s draft, inspect whether the draft was moved into the app-owned store. If menu bar edits fail to reach the windows, inspect whether another LabelStore() was created in the menu bar hierarchy.

Symptom First thing to inspect
Window and menu bar labels diverge Separate model initializations instead of one shared instance
Every draft changes together Draft storage placed in a shared model
A view fails when reading its model Missing .environment(store) in that hierarchy
An editor unexpectedly resets Changes to the owning view’s identity or replacement of its model
Opening another editor fails Mismatch between the declared and requested scene identifiers

The missing-environment case has a documented consequence: reading a nonoptional observable object by type assumes that an ancestor supplied it; Apple says an absent object causes an exception. Supply the dependency in previews and alternate entry points as well. Managing model data in your app.

The sample stores everything in memory and contains no save or restore implementation. Closing a window should therefore be treated as discarding its unfinished draft, and quitting should be treated as discarding the shared label. If drafts must survive those actions, define a separate persistence requirement before extending this example.

Keep focused commands separate from shared menu bar controls

A shared label needs no active-window selection: every consumer receives the same model. An action that edits the currently focused window’s draft has a different requirement because it must identify a particular window.

For standard application menu commands, Apple documents publishing a window-owned model with focusedSceneValue and consuming it through @FocusedValue in a Commands implementation. Its example disables the command when no focused model exists. That is a supported path for actions whose target follows the active scene. Building and customizing the menu bar with SwiftUI.

Do not infer from that command example that opening a MenuBarExtra gives it a dependable reference to the previously active editor. This article’s panel deliberately operates on app-wide state. If the panel must target an individual editor, add an explicit target-selection design, such as a list of app-owned editor identifiers, rather than assuming which window should receive the action.

Add AppKit or TextKit only when the editor requires them

The label editor needs only SwiftUI controls. If its requirements grow to include a custom AppKit text view, NSViewRepresentable is Apple’s supported bridge for creating, updating, and tearing down an NSView in a SwiftUI hierarchy. Apple also describes using a coordinator to forward delegate and target-action messages into SwiftUI. NSViewRepresentable.

Preserve the ownership plan when adding that bridge: keep each editor’s view instance and editing state associated with its window, and pass shared content through the app model only where sharing is intended. A coordinator should forward edits through a binding or model action. As an implementation precaution, compare incoming content before replacing the text view’s contents so unrelated shared updates do not repeatedly reset an ongoing edit.

For a TextKit 2 implementation, Apple identifies textLayoutManager as the modern layout-engine access point. Its TextKit session explains that accessing the legacy layoutManager on a TextKit 2 text view can trigger compatibility mode, switching that view to TextKit 1 without an automatic switch back. Avoid using that legacy property merely to inspect a text view. TextKit, What’s new in TextKit and text views.

If supporting macOS 13 is required, retain the same ownership arrangement but use the older observation path: ObservableObject with @Published, an owning @StateObject, and .environmentObject with receiving @EnvironmentObject properties. Apple’s migration guide explains how those mechanisms correspond to the newer Observation approach. Changing observation systems should preserve the intended sharing boundaries. Migrating from the Observable Object protocol to the Observable macro.

Choose ownership by the behavior someone expects from the interface: one app-owned instance for the shared label, one window-owned value for each unfinished draft, and an explicit action connecting them. Keep the two-window sequence as a regression check whenever the menu bar panel or editor grows.

Sources