useObservable

Scripting provides a reactive state system formed by Observable<T> and the useObservable<T> hook. This system drives UI updates, interacts with the animation engine, and aligns closely with SwiftUI’s binding model—enabling future APIs such as List(selection:), NavigationStack(path:), TextField(text:), and more.


1. Observable<T>

Observable<T> is a reactive container that holds a mutable value. Whenever the value changes, any UI components that read this value are automatically re-rendered.

1.1 Class Definition

1class Observable<T> {
2  constructor(initialValue: T);
3  value: T;
4  setValue(value: T): void;
5  subscribe(callback: (value: T, oldValue: T) => void): void;
6  unsubscribe(callback: (value: T, oldValue: T) => void): void;
7  dispose(): void;
8}

1.2 Property & Method Details

value

The current value stored inside the observable.

setValue(newValue)

Updates the value and triggers UI re-rendering.

1observable.setValue(newValue);

T may be any type: primitives, arrays, objects, or class instances.

subscribe / unsubscribe

Allows external listeners to respond to value changes. Most components do not need to use these manually.

dispose

Releases internal subscriptions. Typically only needed when manually managing observables outside the component system.


2. useObservable<T>

useObservable<T> creates component-local reactive state and provides an Observable<T> instance whose value persists across re-renders.

2.1 Function Signature

1declare function useObservable<T>(): Observable<T | undefined>;
2declare function useObservable<T>(value: T): Observable<T>;
3declare function useObservable<T>(initializer: () => T): Observable<T>;

2.2 Initialization Modes

1. Without initial value

Value defaults to undefined.

1const data = useObservable<string>();

2. With initial value

1const count = useObservable(0);

3. Lazy initialization

The initializer is executed only on the first render.

1const user = useObservable(() => createDefaultUser());

3. Using Observable in UI Components

Reading .value inside JSX automatically establishes dependency tracking.

1<Text>{name.value}</Text>

Updating the state triggers re-render:

1<Button title="Change" action={() => name.setValue("Updated")} />

This behavior is similar to React’s useState, but aligned with SwiftUI’s reactive identity-based rendering.


4. Integration with Animation

Observable values participate directly in Scripting’s animation system.

There are two main animation mechanisms:


4.1 Explicit animations: withAnimation

1withAnimation(() => {
2  size.setValue(size.value + 20);
3});

Any view that depends on size.value will animate its change.


4.2 Implicit animations: the animation modifier

Views can animate whenever a specific dependency changes.

Correct syntax:

1animation={{
2  animation: Animation.spring({ duration: 0.3 }),
3  value: size.value
4}}

This mirrors SwiftUI’s .animation(animation, value: value) API.

Example:

1<Rectangle
2  frame={{
3    width: size.value,
4    height: size.value,
5  }}
6  animation={{
7    animation: Animation.easeIn(0.25),
8    value: size.value,
9  }}
10/>

5. Forward Compatibility with SwiftUI-Style Binding APIs

Observable is the foundation for future SwiftUI-style binding APIs. Upcoming components will accept Observable<T> directly, matching SwiftUI’s $binding behavior.

5.1 List(selection:)

1const selection = useObservable<string | undefined>(undefined)
2
3<List selection={selection}>
4  ...
5</List>

5.2 NavigationStack(path:)

1const path = useObservable<string[]>([])
2
3<NavigationStack path={path}>
4  ...
5</NavigationStack>

This allows fully type-safe and reactive navigation, mirroring SwiftUI’s native patterns.


6. ForEach: Recommended Data Binding Pattern

Scripting provides a SwiftUI-aligned ForEach API:

1<ForEach data={items} builder={(item, index) => <Text>{item.name}</Text>} />

Where each item must satisfy:

1T extends { id: string }
  • Enables insertion/removal animations
  • Avoids index-based rendering issues
  • Improves performance for large lists

Example:

1const items = useObservable([
2  { id: "1", name: "Apple" },
3  { id: "2", name: "Banana" }
4])
5
6<ForEach
7  data={items}
8  editActions="all"
9  builder={(item) => <Text>{item.name}</Text>}
10/>

7. Complete Example

1export function Demo() {
2  const visible = useObservable(true);
3  const size = useObservable(100);
4
5  return (
6    <VStack spacing={20}>
7      {visible.value && (
8        <Rectangle
9          frame={{
10            width: size.value,
11            height: size.value,
12          }}
13          background="blue"
14          animation={{
15            animation: Animation.spring({ duration: 0.4, bounce: 0.3 }),
16            value: size.value,
17          }}
18          transition={Transition.opacity()}
19        />
20      )}
21
22      <Button
23        title="Toggle Visible"
24        action={() => {
25          withAnimation(() => {
26            visible.setValue(!visible.value);
27          });
28        }}
29      />
30
31      <Button
32        title="Resize"
33        action={() => {
34          withAnimation(Animation.easeOut(0.25), () => {
35            size.setValue(size.value === 100 ? 160 : 100);
36          });
37        }}
38      />
39    </VStack>
40  );
41}

8. Summary

  • Observable<T> is the core reactive state container in Scripting.
  • useObservable creates component-local observable state.
  • Any change to .value automatically re-renders dependent UI.
  • Observable integrates directly with animations (explicit and implicit).
  • It is the foundation for SwiftUI-style binding APIs such as List(selection:) and NavigationStack(path:).
  • ForEach works best with data: Observable<Array<T>> for identity-based diffing and smooth animations.