Playground
Examples/PlaygroundNook is a live customization playground. A controls window
beside the running nook changes its appearance, theme, size and shape, type and
motion, top bar, companion surfaces, rim glow, scroll edge fade, and hover
behavior, and the real nook - not a mock - updates as you go. When it looks right,
copy the result as Swift that sets only what you changed, or save it as a JSON
preset to share or open later.
swift run PlaygroundNookThe window
Section titled “The window”The playground opens a regular, resizable window next to the nook. The nook starts as the stock chrome, so everything you see change is something you changed.
- The sidebar lists the pages. A dot marks a page with changes, and right-clicking a page resets it.
- At the top right, Keep Open holds the nook open while you work in the window, Expand opens or collapses the nook, and Code shows or hides the code.
- The code on the right is the Swift snippet or the JSON preset for what you have now, updated as you change it, with a button to copy it.
Each page groups its settings into cards, with one control column so every control lines up. Settings most people leave alone - content insets, metrics, extra theme colors, the chrome’s labels - are folded into a row until you open it; a dot on the row means something inside has changed. Hover a label or an info icon to see what it does. A changed value shows a reset arrow beside its label, a changed card shows Reset, and applying a preset or resetting a page can be undone from the confirmation at the bottom of the window. The window follows the Mac’s light or dark appearance.
The controls are in a window rather than in the nook itself: the nook changes shape as you edit it, collapses when the pointer leaves, and has no room for text fields and color pickers. While the window is open the playground is a regular app with a Dock icon and a menu bar, so Command-Tab reaches it and text fields get the usual editing shortcuts. Closing the window returns it to a menu-bar-only app; the Controls button in the nook brings the window back.
Two launch options help when you try presets:
swift run PlaygroundNook --preset my-preset.json # open a preset at launchswift run PlaygroundNook --expand # open the nook at launchWhat you can change
Section titled “What you can change”| Page | Changes | Lands on |
|---|---|---|
| Appearance | layout, palette, material, backdrop strength, accent, haptics | AppState.appearancePreferences |
| Theme | font design; accent, label, fill, stroke, and inactive icon colors | NookConfiguration.theme |
| Panel | expanded width, corner radii, content insets, a few metrics | expandedWidth, style, metrics |
| Type and Motion | four typography roles, the Settings and banner springs | typography, motion |
| Top Bar | top bar and Settings switches, width, leading title and icon, notch clearance, labels | topBar, labels |
| Companions | the size, look, and entrance every companion shares; add, duplicate, remove, and reorder companions; the selected one’s items, placement, style, and details | companionSize, companionStyle, companionPresence, addCompanion(...) |
| Effects | rim style, scroll edge fade edges and length | rimGlow, scrollEdgeFade |
| Behavior | hover keeps visible, hover haptics | chromeBehavior.hoverBehavior |
Some controls exist only to show a setting off. Preview on the Top Bar page posts
a status banner, and on the Type and Motion page switches between home and Settings.
The Effects page lights the rim and fills the nook with a list and a row of chips that
scroll. Header beside notch on the Top Bar page moves the nook’s own header into the
band beside the notch with nookNotchAccessories(leading:trailing:); with the top bar
off, it shows what clearing the notch
does. The theme colors start as Live - the palette the chrome resolves from the
user’s preferences - until you pick a color.
Composing companions
Section titled “Composing companions”A companion holds a list of items, each one of:
- a button - an SF Symbol, a name, a glyph color, a fill (none, subtle, a color, or the nook’s material) with an optional fade, a size, and an action (post a status, toggle the rim glow, keep open, Settings, or collapse);
- a label - an SF Symbol and some text;
- the nook’s own lock or gear.
A group of controls is one companion with several items, laid out in a row or a
column; a control that stands apart is a companion of its own. Add starts a
companion from a template - an action pill, a round button, the nook controls, a
status chip, or nothing - and its Content tab adds, reorders, and edits the items.
The Every Companion card sets the size, entrance, fade, edge, shadow, and hover
every companion shares, and each companion’s Style tab can set its own, with
Default following the card. A button sized Surface is as tall as the
companion and draws its own surface, so a companion holding only those draws none
around them and takes the .plain style.
How a change reaches the nook
Section titled “How a change reaches the nook”The playground is a NookModule whose makeConfiguration() builds a
NookConfiguration from its current settings. After each change it calls
reloadActiveConfiguration() on the coordinator, which builds the configuration
again and applies it to the running chrome. A slider drag changes the settings
many times a second, so the playground applies them once per main-actor turn,
inside withAnimation.
Two things take a different path, as they would in any app. The appearance
preferences belong to the user, so the playground writes them to AppState with
replaceAppearancePreferences(_:), as the built-in Settings screen does. Hover
behavior belongs to the whole host rather than to a module, so it goes through
replaceChromeBehavior(_:). Nothing needs a relaunch.
Doing the same in your app
Section titled “Doing the same in your app”Any module whose configuration depends on state that changes at runtime - its own
preferences, a feature flag, a live preview - can use the same seam. Build the
configuration from that state in makeConfiguration(), keep the coordinator from
onReady, and reload when the state changes:
@MainActorfinal class DashboardModule: NookModule { nonisolated static let moduleDescriptor = NookModuleDescriptor( id: "com.example.dashboard", displayName: "Dashboard" )
let descriptor = DashboardModule.moduleDescriptor private let preferences = DashboardPreferences() // your own state private weak var coordinator: AppCoordinator?
func makeConfiguration() -> NookConfiguration { var configuration = NookConfiguration() configuration.setHome { DashboardHome() } configuration.expandedWidth = preferences.isWide ? 640 : nil if preferences.showsTimer { configuration.addCompanion(id: "timer", visibility: .both, shape: .circle) { TimerButton() } } configuration.onReady = { [weak self] coordinator in self?.coordinator = coordinator } return configuration }
func setShowsTimer(_ showsTimer: Bool) { preferences.showsTimer = showsTimer withAnimation(.snappy) { coordinator?.reloadActiveConfiguration() } }}A reload applies everything a module switch applies - the home and compact
content, theme, top bar, labels, metrics, motion, typography, width, lifecycle
hooks, file-drop handler, companions, rim glow, and scroll edge fade - and leaves
Settings when the new configuration turns Settings off. It also applies style
and transitions, which the chrome otherwise reads only at launch. It does not
call onActivate() or onReady again, and it leaves the nook in whatever state it
was in.
A few things stay as they are:
- The process-wide values a single-module configuration carries -
preferenceDefaults,chromeBehavior,branding, andshowsMenuBarExtra- are read once at launch. UsereplaceChromeBehavior(_:)to change chrome behavior. NookApp.main(configuration)wraps one fixed configuration, so a reload hands the same value back. Register a module, or a configuration closure withhost.register(_:configuration:), to build a new one each time.- A module switch keeps the style and transitions the chrome already has.
At the NookSurface level, Nook.style and Nook.hoverBehavior are settable, so a
host driving a Nook directly can restyle it in place too.
Exporting Swift
Section titled “Exporting Swift”The Swift export targets the single-module NookConfiguration path and sets only
the values that differ from the framework’s defaults, so an untouched playground
exports an empty configuration. Copy it from the code card, or from the Presets page:
import NookAppimport SwiftUI
var configuration = NookConfiguration()configuration.setHome { MyHomeView() } // your home view
configuration.expandedWidth = 420configuration.style = NookConfiguration.defaultStyleconfiguration.style?.bottomCornerRadius = 30
configuration.topBar.showsKeepOpenButton = falseconfiguration.topBar.showsSettingsButton = false
// How every companion looks and appears, unless it says otherwise.configuration.companionStyle = .faded
configuration.addCompanion(id: "controls", anchor: .trailing, hidesInSettings: false) { ControlsCompanion()}
NookApp.main(configuration)
// MARK: - Companion content
struct ControlsCompanion: View { var body: some View { VStack(spacing: 2) { NookKeepOpenButton() NookSettingsButton() } }}- Each companion’s items are written out as a view of its own, named after the
companion: glyph buttons with
.nookGlyph, labels, and the framework’s lock and gear. A button’s action is a comment to replace with yours, except the ones the framework runs - keep open, Settings, and collapse - which callNookChromeActions. A companion with no items is hidden in the playground, so it is left out, with a comment saying so. - The home view appears as a placeholder -
MyHomeView, and with a header beside the notchHeaderTitleandHeaderButtons- for you to replace with yours. - With Header beside notch on, the home view is wrapped in a small view type that
applies
nookNotchAccessories(leading:trailing:), sincesetHome(_:)takes aSendableview. - Changed appearance preferences become
preferenceDefaults, a launch seed: they set the first run, and a choice the user later makes in Settings wins. Keep Open is left out, since the playground uses it only to hold the nook open. - A changed shape starts from
NookConfiguration.defaultStyle, so only the values you moved appear. - A changed rim glow style comes with a reminder that the rim only shows while some
content lights it with
nookRimGlow(_:).
In a multi-module host, the configuration lines go in a module’s
makeConfiguration(), and preferenceDefaults and chromeBehavior go on
NookHostConfiguration instead.
JSON presets
Section titled “JSON presets”A preset is the playground’s settings plus the appearance preferences they were
tuned against. The Presets page copies, pastes, saves, and opens them, and offers a
few built-in starting points; applying one can be undone. The playground writes every
key, sorted, so presets diff cleanly under version control, and writes colors as
#RRGGBB or #RRGGBBAA. A preset written by hand can be much shorter:
{ "appearance" : { "chromePalette" : "dark", "presentation" : "floating", "surfaceStyle" : "liquidGlass" }, "format" : "opennook.playground-preset", "settings" : { "companionDefaults" : { "fade" : 0.35, "hover" : "lift" }, "companions" : [ { "alignment" : "end", "id" : "status", "items" : [ { "symbol" : "sparkles", "title" : "3 new", "type" : "label" } ] }, { "gap" : 16, "id" : "leave", "items" : [ { "action" : "collapse", "fill" : "color", "fillColor" : "#FF3B30", "size" : "surface", "symbol" : "phone.down.fill", "tint" : "#FFFFFF", "title" : "Leave", "type" : "button" } ], "backdrop" : "none", "outline" : "circle" } ], "panel" : { "expandedWidth" : 440 }, "theme" : { "accent" : "#FA5C85", "fontDesign" : "rounded" } }, "version" : 1}Presets are read leniently:
- A missing key takes its default, so a hand-written preset can hold only the values it cares about, and a preset from an earlier playground still opens.
- An unknown key is ignored.
- A value of the wrong type, or a choice the playground does not know, is an
error that names the JSON path of the value - for example
settings.panel.expandedWidth should be a number. - A preset whose
versionis newer than the playground reads is refused rather than misread. - Companion ids are made unique and non-empty, and negative lengths become zero,
since
addCompaniontraps on a duplicate id. Fades are kept between 0 and 1, and a companion keeps at most twelve items. - A companion from an earlier playground names its content with
kind(actions,button,controls, orchip) rather than listingitems, and opens with the items that kind stood for. The playground writesitemsfrom then on.
Keep Open is never part of a preset, and neither are the demo switches - the lit rim, the scrolling demo, the banner message - which the playground remembers separately.
Under swift run the playground is an unbundled, unsandboxed binary, so the save
and open panels cannot enter the folders macOS protects (Desktop, Documents,
Downloads). Use another folder, or copy and paste the JSON.
Where the playground keeps its state
Section titled “Where the playground keeps its state”- The settings and demo switches live in the module’s own defaults suite,
context.defaults(opennook.module.com.opennook.example.playground), like any module’s state. - The appearance preferences live where the framework keeps them for any host.
- The window’s own choices, such as which folded rows are open and whether the code shows, live in the app’s standard defaults.
- Reset beside Everything on the Presets page returns the settings and the appearance to the defaults.
How the example is built
Section titled “How the example is built”The playground is bigger than the other examples, so it spans several files:
Examples/PlaygroundNook/Core- thePlaygroundNookCoretarget: the settings model (PlaygroundSettings), presets and their JSON coder (PlaygroundPreset,PlaygroundPresetCoder), the Swift exporter (PlaygroundSwiftExporter), and the store. The exporters are pure, andTests/PlaygroundNookTestscovers them, including a check that every default still matches the framework’s.Examples/PlaygroundNook/App- the module, the model that applies changes, the controls window, and the views shown in the nook.
See also
Section titled “See also”- Chrome customization - the configuration seams the playground drives.
- Companion surfaces and Rim glow and edge fade - the features behind the Companions and Effects pages.
- Layout and content insets - notch clearance and views beside the notch, behind the Top Bar page’s Notch card.
- Multiple modules - modules, contexts, and the switch lifecycle a reload borrows from.