Build a functional custom panel — a property editor that opens from an inspector bar button, edits the selected block, and writes changes back to the scene.

Overview#
Custom panels on iOS are editor sheets whose content you build with native SwiftUI. You present your own view and wire it to the engine directly — there is no panel registration step and no component catalog. This guide builds a property editor: a button in the inspector bar opens a sheet that edits the selected block’s name and opacity.
The example builds on GuideEditorConfiguration, a small helper the iOS examples repository ships as a minimal editor baseline (close, undo, and redo). Substitute your own editor configuration — the inspectorBar builder is exposed on every configuration. The Configuration guide covers how EditorConfiguration and EngineSettings set up the editor as a whole, and the Panel guide covers the sheet mechanism — sheet types, presentation styles, and the built-in sheets — that this guide builds on.
Opening the Panel from the Inspector Bar#
The inspector bar is the surface the editor shows for the current selection, which makes it the natural home for a panel that edits the selected block. Add an inspector bar button whose action opens the sheet. The action receives an InspectorBar.Context, which exposes the engine, the eventHandler, and the current selection. Read the selected block from context.selection.block, then present your content with openSheet(style:content:). The content closure returns any SwiftUI view — here a PropertyPanel (defined below).
builder.inspectorBar { inspectorBar in inspectorBar.items { _ in InspectorBar.Button(id: "open_property_panel") { context in let block = context.selection.block context.eventHandler.send(.openSheet(style: .default(), content: { PropertyPanel( engine: context.engine, block: block, eventHandler: context.eventHandler, ) })) } label: { _ in Label("Properties", systemImage: "slider.horizontal.3") } }}inspectorBar.itemsreplaces the inspector bar’s contents.GuideEditorConfigurationstarts with an empty inspector bar, so this sets the single button; include any other items your app needs in the same closure.- A panel that isn’t tied to a selection — like the built-in sheets — fits a
Dock.Buttoninstead. See Add a New Button for both surfaces. .default()presents a standard resizable sheet. The Panel guide documents the otherSheetStyleoptions.
Building the Panel Content#
Panel content is a regular SwiftUI view. Because the openSheet content: parameter is a @ViewBuilder, panel-local @State lives in a dedicated View — here PropertyPanel. Its initializer seeds the state from the selected block with getName and getOpacity, so the controls open showing the block’s current values. A Form supplies native controls — a TextField for the name and a Slider for the opacity — and navigationTitle gives the sheet its heading.
struct PropertyPanel: View { let engine: Engine let block: DesignBlockID let eventHandler: EditorEventHandler
@State private var name: String @State private var opacity: Float
init(engine: Engine, block: DesignBlockID, eventHandler: EditorEventHandler) { self.engine = engine self.block = block self.eventHandler = eventHandler _name = State(initialValue: (try? engine.block.getName(block)) ?? "") _opacity = State(initialValue: (try? engine.block.getOpacity(block)) ?? 1) }
var body: some View { NavigationStack { Form { HStack { Text("Name") Spacer() TextField("Block name", text: $name) .multilineTextAlignment(.trailing) } VStack(alignment: .leading, spacing: 8) { HStack { Text("Opacity") Spacer() Text("\(Int(opacity * 100))%") .foregroundStyle(.secondary) } Slider(value: $opacity, in: 0 ... 1) } } .navigationTitle("Properties") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { try? engine.block.setName(block, name: name) try? engine.block.setOpacity(block, value: opacity) eventHandler.send(.closeSheet) } } } } }}A non-floating sheet needs List- or NavigationStack-based content, which the Form inside a NavigationStack provides.
Applying Changes and Closing the Panel#
The Done button writes the edited values back to the scene with setName and setOpacity, then dismisses the sheet by sending the closeSheet event. The user can also swipe the sheet down to dismiss it.
Button("Done") { try? engine.block.setName(block, name: name) try? engine.block.setOpacity(block, value: opacity) eventHandler.send(.closeSheet)}API Reference#
| Method | Description |
|---|---|
builder.inspectorBar { inspectorBar in inspectorBar.items { … } } |
Declare the inspector bar’s items in the editor configuration |
InspectorBar.Button(id:action:label:) |
An inspector bar button whose action and label closures receive an InspectorBar.Context |
InspectorBar.Context.selection.block |
The selected block inside the button’s action |
InspectorBar.Context.engine |
Engine access inside the button’s action |
context.eventHandler.send(.openSheet(style:content:)) |
Present custom SwiftUI content as a sheet |
context.eventHandler.send(.closeSheet) |
Dismiss the current sheet |
engine.block.getName(_:) / setName(_:name:) |
Read and write a block’s name |
engine.block.getOpacity(_:) / setOpacity(_:value:) |
Read and write a block’s opacity |
Next Steps#
- Panel — sheet types, presentation styles, and the built-in sheets this guide builds on.
- Add a New Button — full options for dock, canvas menu, inspector bar, and navigation bar buttons.
- Dock — configure the dock area and item order.
- Inspector Bar — the built-in surface for editing properties of the selected block.