Experimenting with MusicKit for Swift - Repeat Mode

In MusicKit for Swift, Apple provides us with a MusicPlayer object that our app uses to play music. It has three kinds of enumerations -

  • PlaybackStatus - The music player playback status modes.
  • RepeatMode - The repeat modes for the music player.
  • ShuffleMode - The shuffle modes for the music player.

In this post, we’ll explore RepeatMode, and how we can implement it in our app.

RepeatMode is an enum that contains three cases -

  • all - Repeating the currently playing collection.
  • none - In disabled state, and playback stops after the current item.
  • one - Repeating the currently playing entry.

The MusicPlayer also exposes a State class, which is an ObservableObject. We observe the properties of the music player such as playbackStatus, playbackRate, repeatMode and shuffleMode.

If you take the example of Apple Music, there’s a repeat mode button that you can iterate through to repeat all the songs or a particular one. To create something similar in my app, Musadora, I extended MusicPlayer.RepeatMode to conform to CaseIterable. As I’m extending the enum outside the framework, it prevents automatic synthesis of ‘allCases’, a requirement of protocol CaseIterable. To fix that, I manually add a static variable to the extension -

public static var allCases: [MusicPlayer.RepeatMode] = [.none, .all, .one]

Now, I add a mutating function next() for circulation iteration of the options. It sets to the first element if it is the last one. Otherwise, increments to the next one.

mutating func next() {
    let allCases = Self.allCases
    let currentIndex = allCases.index(after: allCases.firstIndex(of: self)!)

    self = allCases[currentIndex == allCases.endIndex ? allCases.startIndex : currentIndex]
}

The extension looks like -

extension MusicPlayer.RepeatMode: CaseIterable {
    public static var allCases: [MusicPlayer.RepeatMode] = [.none, .all, .one]

    mutating func next() {
        let allCases = Self.allCases
        let currentIndex = allCases.index(after: allCases.firstIndex(of: self)!)

        self = allCases[currentIndex == allCases.endIndex ? allCases.startIndex : currentIndex]
    }
}

In the NowPlayingPrimaryControlsView , I observe the value of state -

@ObservedObject private var state = ApplicationMusicPlayer.shared.state

When the user taps on the repeat mode button, the next() method of the enum is called and based on the response, the image is changed.

Button(action: { state.repeatMode?.next() }) {
    Image(systemName: state.repeatMode == .one ? "repeat.1" : "repeat")
        .font(.title3)
        .padding(12)
        .background(state.repeatMode == MusicPlayer.RepeatMode.none ? nil : Circle().opacity(0.1))
}

Here’s how it looks -

My Replay 2015 playlist on Apple Music

Based on the value of the repeatMode, the MusicPlayer decides if the song is repeated or not.

You can similarly create the shuffle button as well!

Button(action: { state.shuffleMode?.next() }) {
    Image(systemName: "shuffle")
        .font(.title3)
        .padding(12)
        .background(state.shuffleMode == .songs ? Circle().opacity(0.1) : nil)
}