FRadioPlayer 0.2.1: Migrating from delegates to observers

FRadioPlayer 0.2.1 ships the biggest API change in years: the old delegate callbacks are gone in favor of a simple, flexible observer pattern. This update also adds proper support for file‑based audio (alongside live streams), keeps distribution focused on Swift Package Manager, and refreshes the demo with SwiftUI and XcodeGen.

Release highlights

Here’s the quick tour:

  • Observer-based notifications — swap the single delegate slot for addObserver and removeObserver, enabling multiple listeners inside the same feature.
  • Richer stream handling — duration and current-time tracking now covers non-live streams, complete with seeking helpers.
  • Swift Package Manager first — CocoaPods, Carthage, and Travis CI support were dropped to simplify maintenance around SPM.
  • SwiftUI demo reboot — the sample app targets iOS 15+ and lives behind XcodeGen, so no .xcodeproj files clutter the repo.
  • GitHub Actions CIspm.yml builds/tests the library, while demo.yml generates and compiles the SwiftUI demo.

Why the delegate went away

The delegate model capped you at a single listener per FRadioPlayer instance — not great for modern SwiftUI, where multiple views often observe the same state. In 0.2.1, the player now stores weak references to any number of FRadioPlayerObservers and fans out changes through a lightweight Observation struct.

Here’s what the change looks like at the call site:

// Before 0.2.1
FRadioPlayer.shared.delegate = self

// 0.2.1 and later
FRadioPlayer.shared.addObserver(self)   // conforms to FRadioPlayerObserver
// When updates are no longer required
FRadioPlayer.shared.removeObserver(self)

Under the hood, FRadioPlayer keeps a dictionary keyed by ObjectIdentifier. When state changes (state, playbackState, metadata, artwork, duration, currentTime), the player walks the observer list and triggers the relevant callbacks. Observers are held weakly, so ARC cleans things up once you call removeObserver or the listener goes away.

Interested in the broader design? John Sundell’s deep dive on observation protocols covers the trade-offs that inspired this change.

Implementing the observer protocol

To adopt the new API, conform to FRadioPlayerObserver and implement only the callbacks you care about. Most methods have sensible defaults, so you can keep things small.

final class RadioViewModel: NSObject, FRadioPlayerObserver {
  private let player = FRadioPlayer.shared

  override init() {
    super.init()
    player.addObserver(self)
    player.radioURL = Station.cityPop.url
  }

  func radioPlayer(
    _ player: FRadioPlayer,
    playerStateDidChange state: FRadioPlayer.State
  ) {
    isBuffering = (state == .loading)
  }

  func radioPlayer(
    _ player: FRadioPlayer,
    playTimeDidChange currentTime: TimeInterval,
    duration: TimeInterval
  ) {
    progress = duration > 0 ? currentTime / duration : 0
  }

  deinit {
    player.removeObserver(self)
  }
}

Playback‑time observation now works for both live and file‑based streams thanks to the new duration/currentTime reporting. It’s worth overriding those progress callbacks even if you skipped them before.

Streaming files with seeking

Historically, FRadioPlayer treated everything as live. In 0.2.1 the player distinguishes live vs. file/HTTP sources, tracks duration and currentTime, and adds a simple seek(to:completion:) API. For on‑demand streams, seeking moves to the new position and playback resumes automatically.

player.seek(to: 42) {
  // Update UI once the new position is ready
}

When a file stream finishes, the player resets its hasPlayedToEndTime flag and seeks back to zero so your UI stays in sync.

Packaging & tooling updates

A few housekeeping changes worth calling out:

  • Swift tools 5.5Package.swift now requires Swift 5.5 (Xcode 13+) and focuses dependency management solely on SPM.

  • Demo app revamp — the SwiftUI demo lives in Example/FRadioPlayerDemo/ with Models/, Views/, and Resources/ folders. Generate the project via XcodeGen before opening it:

    cd Example
    xcodegen
    open FRadioPlayerDemo.xcodeproj
    

    EnvironmentObjects propagate the shared player state throughout the views.

  • Continuous integration — GitHub Actions builds both the library and demo. spm.yml runs swift build + swift test; demo.yml installs XcodeGen, generates the demo, and compiles it for the iOS simulator.

Migration checklist

If you’re upgrading an existing app, this is the fastest path:

  1. Remove any player.delegate = self assignments.
  2. Conform to FRadioPlayerObserver and register each listener with addObserver.
  3. Implement the callbacks that are required—especially durationDidChange and playTimeDidChange for progress UI.
  4. For CocoaPods or Carthage distribution targets, migrate consumers to SPM.
  5. Regenerate the demo project with xcodegen if the sample app is part of the workflow.
  6. Upgrade to Xcode 13 (Swift 5.5) or newer to build the package and demo.

0.2.1 is a forward‑looking cleanup that keeps FRadioPlayer lightweight while giving you finer control over playback state. The observer pattern is ready for one‑to‑many experiences — multiple SwiftUI views, widgets, or background services — without the single‑delegate bottleneck.