About
Blog
Projects
Contact

Reactivity in Swift?

I mostly work with JavaScript, but I've become in charge of some Swift code recently.

I also realized today that Swift has some built in mechanisms for observing changing properties. That is pretty cool, kind of like the Swift equivalent of MobX or SolidJS?

class MyObjectToObserve: NSObject {
  var observation: NSKeyValueObservation?

  @objc dynamic var tracks: [Int] = []

  override init() {
    super.init()
    observation = observe(
      \.tracks,
      options: .new
    ) { [self] _, _ in
      let next = tracks.first
      switch next {
      case .none:
        print("next track is empty")
      case let .some(next):
        print("next track is \(next)")
      }
    }
  }

  func setTracks() {
    tracks.removeAll()
    tracks.append(1)
    tracks.append(2)
    tracks.append(3)
  }
}

Basically it observes a list of (media) tracks, and emits some update events when the next track changes.

However, I am running into a bug because the Swift implementation emits a change event for every intermediate step. Is there a good way to perform multiple modifications, similar to a batch (SolidJS) or action (MobX)?

Here is what happens when I call setTracks():

> swift run
next track is empty
next track is 1
next track is 1
next track is 1

SolidJS

Here is the same thing in Solid:

import { batch, createEffect, createSignal, on } from 'solid-js';

const [tracks, setTracks] = createSignal([]);

class MyObjectToObserve {
  constructor() {
    createEffect(
      on(
        tracks,
        (tracks) => {
          const next = tracks[0];

          if (typeof next === 'number') {
            console.log(`next track is ${next}`);
          } else {
            console.log('next track is empty');
          }
        },
        {
          defer: true,
        }
      )
    );
  }

  setTracks() {
    batch(() => {
      setTracks([]);
      setTracks((tracks) => [...tracks, 1]);
      setTracks((tracks) => [...tracks, 2]);
      setTracks((tracks) => [...tracks, 3]);
    });
  }
}

The output of the SolidJS program is:

$ node --conditions=browser solid.mjs
next track is 1

MobX

Or in MobX:

import { action, makeObservable, reaction } from 'mobx';

class MyObjectToObserve {
  tracks = [];

  constructor() {
    makeObservable(this, {
      tracks: true,
      setTracks: action,
    });

    reaction(
      () => this.tracks,
      (tracks) => {
        const next = tracks[0];
        if (typeof next === 'number') {
          console.log(`next track is ${next}`);
        } else {
          console.log('next track is empty');
        }
      }
    );
  }

  setTracks() {
    this.tracks = [];
    this.tracks = [...this.tracks, 1];
    this.tracks = [...this.tracks, 2];
    this.tracks = [...this.tracks, 3];
  }
}
$ node mobx.mjs
next track is 1