Exploring Reactivity in Swift: A Comparison with JavaScript Frameworks

2 min read

A developer's exploration of Swift's reactive programming capabilities, comparing them to popular JavaScript libraries like MobX and SolidJS. This post delves into the challenges of managing state changes and batching updates in Swift's Key-Value Observing (KVO) system.

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