Problem Overview

While developing an iOS music player app, I encountered an issue where adding a song to a new playlist inadvertently removed it from its original playlist. This occurred due to incorrect management of bi-directional relationships between songs and playlists.

The relationship setup:

  • MusicModel represents a song and has a playlists property linking to the playlists it belongs to.
  • PlaylistModel represents a playlist and has a songs property linking to its songs.

Original Code

Adding Songs to a Playlist

private func addSelectedSongs() {
    let songsToAdd = allSongs.filter { selectedSongs.contains($0.id) }
    for song in songsToAdd {
        playlist.songs.append(song)
        song.playlists.append(playlist)
    }
    playlist.updatedAt = Date()
    try? modelContext.save()
}

Removing Songs from a Playlist

private func removeSong(_ song: MusicModel) {
    playlist.songs.removeAll { $0.id == song.id }
    song.playlists.removeAll { $0.id == playlist.id }
    playlist.updatedAt = Date()
    try? modelContext.save()
}

Issue with the Original Code

  • Direct Modification of Relationships: Adding a playlist to a song (song.playlists.append(playlist)) unintentionally overwrote existing playlist associations.

Updated Code

Updated addSelectedSongs

private func addSelectedSongs() {
    let songsToAdd = allSongs.filter { selectedSongs.contains($0.id) }
    for song in songsToAdd {
        // Ensure the song is not already in the playlist
        if !playlist.songs.contains(where: { $0.id == song.id }) {
            playlist.songs.append(song)
        }

        // Ensure the playlist is not already in the song
        if !song.playlists.contains(where: { $0.id == playlist.id }) {
            song.playlists.append(playlist)
        }
    }
    playlist.updatedAt = Date()
    try? modelContext.save()
}

Updated removeSong

private func removeSong(_ song: MusicModel) {
    // Remove the song from the current playlist
    playlist.songs.removeAll { $0.id == song.id }

    // Remove the current playlist from the song
    song.playlists.removeAll { $0.id == playlist.id }

    playlist.updatedAt = Date()
    try? modelContext.save()
}

Idea to fix the issue

  • Preserves Existing Associations:
    • The updated addSelectedSongs method checks if the song or playlist already exists in the relationship before appending. This prevents overwriting existing associations.
  • Scoped Removal:
    • The updated removeSong method only removes the current playlist from the song’s playlists, ensuring other playlist associations remain intact.
  • Ensures Data Integrity:
    • The changes ensure that a song can belong to multiple playlists simultaneously without any unintended side effects.