To build interactive SwiftUI previews, give each preview its own local state, pass an observable view model into the editor, and expose editable properties through bindings. This walkthrough builds a small macOS label editor using synthetic example text.
Choose where preview state should live
The task is to preview a desktop editor where someone changes a label’s title and detail text, then toggles whether the detail appears. The editor should respond immediately, and each preview should start with an explicit example configuration.
Use the smallest input that supports the interaction. Apple recommends passing views only the data they need because complex dependencies make previews harder to construct. A static label can take a string; an editor needs a writable connection to its state. Adding previews to your interface files.
| What the view needs | Appropriate input | Where example state belongs |
|---|---|---|
| Display fixed text | An immutable string | Directly in the preview |
| Edit one value owned by its parent | A @Binding property |
Local @State in the preview host |
| Edit several related observable properties | An observable model exposed through @Bindable |
A model instance stored in the preview host’s @State |
| Read a model supplied through the environment | An environment dependency | Explicit injection at the preview root |
For this example, the view model holds two strings, a visibility setting, and a computed display title. Each preview owns a separate instance. This makes the starting conditions visible beside the preview declaration and prevents the example from depending on a shared mutable singleton.
Prepare a macOS SwiftUI target
Use Xcode 16 or later for the inline @Previewable syntax shown here. Apple introduced that macro in Xcode 16. Set this example’s deployment target to macOS 14 or later, where SwiftUI supports Observation; Apple’s current Previewable() reference also lists macOS 14 availability. Xcode 16 Release Notes, Managing model data in your app, Previewable().
Add a Swift file named LabelEditor.swift to an existing macOS SwiftUI app target. The next two code blocks together form its complete preview example. Keep the app’s existing entry point; this file defines the model, editor, and previews.
The code uses @State spelling across supported toolchains. Apple’s current documentation distinguishes its implementation: Xcode 26 and earlier use the State property wrapper, while Xcode 27 and later use the State() macro. Keep that distinction in mind when inspecting generated code or reasoning about initialization costs. State, State().
Build the observable model and editable view
Apply @Observable to the model so SwiftUI can track properties read by the interface. Use @Bindable in the editor to create the property bindings required by its text fields and toggle. Apple documents this combination for editing observable models. Managing model data in your app, Bindable.
import Foundation
import Observation
import SwiftUI
@MainActor
@Observable
final class LabelViewModel {
var title: String
var detail: String
var showsDetail: Bool
init(
title: String,
detail: String,
showsDetail: Bool = true
) {
self.title = title
self.detail = detail
self.showsDetail = showsDetail
}
var displayedTitle: String {
let trimmed = title.trimmingCharacters(
in: .whitespacesAndNewlines
)
return trimmed.isEmpty ? "Untitled label" : trimmed
}
}
@MainActor
struct LabelEditor: View {
@Bindable var model: LabelViewModel
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text("Title")
TextField("Label title", text: $model.title)
}
VStack(alignment: .leading, spacing: 6) {
Text("Detail")
TextField("Label detail", text: $model.detail)
}
Toggle("Show detail", isOn: $model.showsDetail)
Divider()
Text("Rendered label")
.font(.caption)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 8) {
Text(model.displayedTitle)
.font(.title2)
.fixedSize(horizontal: false, vertical: true)
if model.showsDetail {
Text(model.detail)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(
.quaternary,
in: RoundedRectangle(cornerRadius: 10)
)
}
.textFieldStyle(.roundedBorder)
.padding(20)
}
}
The model deliberately accepts all example values through its initializer. Its initializer only assigns local properties. The fallback calculation changes what the rendered label displays without overwriting the title someone entered: a whitespace-only title remains editable while the rendered result reads Untitled label.
The editor receives its model from the caller. Its @Bindable declaration provides bindings such as $model.title; state ownership will sit in the preview host. If this view only displayed the model’s values, a plain model reference would be sufficient for observation. Bindable storage is useful here because the controls require writable property bindings. Bindable.
Give each preview independent example state
Append the following declarations to the same file. Each creates its own model with explicit synthetic values. The widths are illustrative layout checks, not recommended minimum window sizes.
#Preview("Standard label") {
@Previewable @State var model = LabelViewModel(
title: "Sample tile",
detail: "Blue square, medium size"
)
LabelEditor(model: model)
.frame(width: 420)
.preferredColorScheme(.light)
}
#Preview("Long label in a narrow editor") {
@Previewable @State var model = LabelViewModel(
title: "Sample collection with a longer descriptive label",
detail: "Round tiles arranged in three columns"
)
LabelEditor(model: model)
.frame(width: 300)
.preferredColorScheme(.dark)
}
#Preview("Blank title and hidden detail") {
@Previewable @State var model = LabelViewModel(
title: " ",
detail: "An optional detail line",
showsDetail: false
)
LabelEditor(model: model)
.frame(width: 420)
}
@Previewable makes the local state declaration part of an embedded SwiftUI view generated by #Preview. Apple documents that tagged declarations become properties on that generated view. Put the declaration at the root of the preview closure; using @Previewable outside a preview body is an error. Previewable().
The resulting ownership chain is explicit: the generated preview host stores the model in @State, LabelEditor receives the model reference, and @Bindable exposes its editable properties. SwiftUI supports storing an Observation model in state and passing that reference to child views. State.
Treat the initializer values as starting fixtures. An interactive preview can change them while it runs. When reproducing a particular layout, return to a fresh preview instance before comparing its initial appearance.
Check behavior with a small scenario matrix
Open LabelEditor.swift, choose Show Canvas, and select a named preview from the canvas tabs. If the canvas is paused, use Restart. Enable Live mode to exercise text entry and control logic. These controls are described in Apple’s current canvas guide. Interacting with previews in the canvas.
Use the following checks to separate state problems from layout problems. The expected results follow from the example code; they are not reported test results.
| Scenario | Action | Expected result |
|---|---|---|
| Standard label | Replace the title with Amber circle |
The rendered title changes immediately |
| Detail visibility | Turn Show detail off, then on |
The detail disappears and returns with its text retained |
| Blank title | Remove every title character | The rendered title becomes Untitled label |
| Whitespace title | Enter only spaces | The field keeps those spaces; the rendered title uses the fallback |
| Narrow editor | Inspect the long-title preview | The rendered title wraps within the card |
| Separate fixtures | Edit one preview, then inspect a fresh different preview | The other preview begins with its own declared model values |
If text entry works but the rendered output stays unchanged, inspect the connection between the field binding and the property the output reads. Here, both paths lead to model.title, directly or through displayedTitle. If toggling visibility clears the detail, look for code that assigns an empty value; this example only changes showsDetail.
No Swift compiler or macOS preview runtime was available in the research environment, so these examples were inspected against the documented APIs rather than compiled or exercised. Build the target and complete the interaction checks in Xcode before adapting the editor.
Keep shared setup separate from editable fixtures
For a model this small, local construction keeps each preview easy to understand. If several previews eventually need expensive reusable setup, Apple provides PreviewModifier: its shared context is cached and reused across participating previews. The API requires macOS 15 or later. PreviewModifier.
That documented sharing behavior leads to a practical design choice: keep independently editable fixture models local, and reserve shared context for setup you intentionally want participating previews to reuse. A shared mutable model would also share its edits, which can make separate scenarios harder to compare.
If an existing view retrieves its model by type from the environment, inject the required instance at the preview root. Apple documents that a missing nonoptional model of the requested type causes an exception. A preview therefore needs to supply the dependencies its chosen root actually reads. Managing model data in your app.
Preserve the AppKit and TextKit boundaries
The editor above uses SwiftUI controls. If your existing macOS editor contains an AppKit text view, use NSViewRepresentable to integrate that view into the SwiftUI hierarchy. Create and configure it through makeNSView, then apply incoming SwiftUI state through updateNSView. Forward AppKit delegate changes through a coordinator when edits must travel back to SwiftUI. Let SwiftUI control the represented view’s frame and bounds. NSViewRepresentable.
Apple also supports direct previews of AppKit views and view controllers. However, @Previewable is a SwiftUI-only facility and does not apply to direct AppKit previews. To use the local-state pattern in this article with AppKit content, preview a SwiftUI host containing the representable. Adding previews to your interface files.
Preserve your text engine choice when constructing an NSTextView. Apple documents NSTextView(usingTextLayoutManager: true) for choosing TextKit 2 at initialization. Its TextKit guidance also explains that accessing the TextKit 1 layoutManager can trigger a one-way fallback for that text-view instance. Use the appropriate TextKit 2 APIs when that is your intended engine; a preview does not remove those boundaries. What’s new in TextKit and text views.
Diagnose preview failures at the ownership boundary
Start with the first build diagnostic. Swift macros generate code at compile time, so an error highlighted at a macro call can involve generated declarations. Xcode’s Editor > Expand Macro command lets you inspect that code. Applying Macros.
Then check the state mechanism. An existing Combine-based ObservableObject needs its corresponding ownership pattern: Apple warns that storing such an object in State does not track changes to its published properties and directs owners to StateObject. The example here uses @Observable, so its state ownership follows the Observation path. State.
Keep the finished preview file centered on explicit starting values and visible interactions. Once the standard, long-title, and blank-title scenarios behave as expected, reuse the same editor with a model owned by your app’s appropriate parent view. The preview fixtures remain a compact way to reproduce those interface conditions.
Sources
- Adding previews to your interface files — Apple
- Xcode 16 Release Notes — Apple
- Managing model data in your app — Apple
- Previewable() — Apple
- Bindable — Apple
- State — Apple
- State() — Apple
- Interacting with previews in the canvas — Apple
- PreviewModifier — Apple
- NSViewRepresentable — Apple
- What’s new in TextKit and text views — Apple
- Applying Macros — Apple