Reactivity¶
Assign to a view property and the UI updates. There is no state container, no observer registration and no manual refresh.
import nib
def main(app: nib.App):
status = nib.Text("Idle", foreground_color=nib.Color.SECONDARY)
progress = nib.ProgressView(value=0.0)
def start():
status.content = "Running"
status.foreground_color = nib.Color.BLUE
progress.value = 0.5
app.build(
nib.VStack(
controls=[status, progress, nib.Button("Start", action=start)],
spacing=12,
padding=16,
)
)
nib.run(main)
What happens on assignment¶
- The property setter stores the value and asks the app to render. Setting a property to the value it already has does nothing.
- The render loop thread serialises the tree into flat nodes and compares each node's packed props and modifiers with the previous frame.
- Changed nodes go out as a
patch; a structural change (a child added, removed, reordered or replaced by another type) goes out as a full render. - The runtime applies the patch to the node and SwiftUI re-evaluates only the affected path.
The three assignments in start() above arrive as one frame: callbacks run inside a batch, and changes made outside callbacks coalesce in the render loop.
What is reactive¶
Every constructor argument that has a matching property. That covers content (text.content, slider.value, toggle.is_on, image.src, stack.controls), visible, and every modifier (opacity, background, padding, font, animation, ...). The reference lists the properties of each view.
Two things are deliberately not reactive:
- Private attributes. Writing
view._contentstores a value and nothing else; the runtime never hears about it. - Unknown attributes.
text.colour = "red"is accepted by Python but has no effect; Nib logs a warning the first time so the typo is visible.
Mutating a value in place is invisible too. Assign a new list rather than appending to the old one, or call view.update() (or app.update()) afterwards:
rows.controls = rows.controls + [nib.Text("new row")] # reactive
rows.controls.append(nib.Text("new row")); rows.update() # also fine
Controls stay in sync¶
A TextField, TextEditor, Toggle, Slider, Picker, DisclosureGroup or Map keeps its value up to date as the user interacts with it: reading field.value inside a callback gives what the user typed, with no round trip through the runtime. Assigning the property is a programmatic set and is sent to the runtime. A stale value that Python already knows is never pushed back over what the user is typing.
Threads and long work¶
Callbacks run one at a time on the event thread. Keep them short: a slow callback delays every later event. Do long work on your own thread and set properties from there; property writes are safe from any thread.
import threading, time
def refresh():
status.content = "Loading"
def work():
time.sleep(2) # network, disk, ...
if app.is_running:
status.content = "Done"
threading.Thread(target=work, daemon=True).start()
app.is_running turns False when the app quits or nib run replaces it after a reload; app.on_quit fires in both cases. Threads you start should check one or the other.
Class-based apps¶
With class MyApp(nib.App) the body() method is called on every render and builds fresh view instances, so state cannot live in those instances. Keep it in nib.State attributes: assigning one requests a render.
class Counter(nib.App):
count = nib.State(0)
def body(self) -> nib.View:
return nib.Button(f"Count: {self.count}", action=self.increment)
def increment(self):
self.count += 1
nib.Binding is a small two-way accessor (binding.value) built from a getter and a setter; views take plain values, so wire a binding through on_change callbacks when you need one.
Related¶
- State & Binding:
Statedescriptors for class-based apps - Ref: view handles
- Views and Events: identity, modifiers, events
- Animations:
animationargument