I have a simple one-to-many relationship. Playlist and Song.
In the same view, upper section prints playlist.name
and playlist.songs
array. Array is expected to update when new songs are added.
Inside Song
‘s init block, surprisingly, the order of the two statements breaks the view updating behavior.
// MARK: - unexpected behavior on switching order
// To reproduce,
// 1. click "add playlist", then click "add song"
// 1. observe the playlist view should update as new songs are added.
// MARK: the upper playlist view are not updated
self.playlist = playlist
self.name = name
// MARK: switching the order, the upper playlist view are correctly updated
// self.name = name
// self.playlist = playlist
It seems to be an bug and I’ve reported it.
I’m looking to understand this better:
- explanations or insights into swift language, macro, etc that how come this could possibly happen.
- any workarounds or what’s the best strategy to achieve the same
behavior without exposing to this uncertainty.
Full example below:
import SwiftData
import SwiftUI
@Model
final class Playlist {
@Attribute(.unique) var name: String
@Relationship(deleteRule: .nullify, inverse: Song.playlist)
var songs: [Song] = []
init(name: String) {
self.name = name
}
}
@Model
final class Song {
var playlist: Playlist?
var name: String
init(
name: String,
playlist: Playlist?
) {
// MARK: - unexpected behavior on switching order
// To reproduce,
// 1. click "add playlist", then click "add song"
// 1. observe the playlist view should update as new songs are added.
// MARK: the upper playlist view are not updated
self.playlist = playlist
self.name = name
// MARK: switching the order, the upper playlist view are correctly updated
// self.name = name
// self.playlist = playlist
}
}
struct MyExampleView: View {
@Environment(.modelContext) private var modelContext
@Query private var playlists: [Playlist]
@Query private var songs: [Song]
var body: some View {
VStack {
List(playlists) { playlist in
Text("(playlist.name) contains: (playlist.songs.description)")
}
Spacer()
Button {
addPlaylist()
} label: {
Text("Add playlist")
.frame(maxWidth: .infinity)
.bold()
}
.background()
Divider()
List(songs) { song in
Text("(song.name) from: (song.playlist?.name)")
}
Spacer()
Button {
addSong()
} label: {
Text("Add song")
.frame(maxWidth: .infinity)
.bold()
}
.background()
}
}
func addPlaylist() {
let newPlaylist = Playlist(
name: "New Playlist (Int.random(in: 1...100).description)"
)
modelContext.insert(newPlaylist)
}
func addSong() {
let newSong = Song(
name: "New Song (Int.random(in: 1 ... 100))",
playlist: playlists.randomElement()
)
modelContext.insert(newSong)
}
}
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(
for: Song.self, Playlist.self,
configurations: config
)
return MyExampleView()
.modelContainer(
container
)
.presentedWindowStyle(.hiddenTitleBar)
.presentedWindowToolbarStyle(.automatic)
}