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