SwiftData @Model class, switching statements order in init block would surprisingly break the view

  Kiến thức lập trình

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:

  1. explanations or insights into swift language, macro, etc that how come this could possibly happen.
  2. 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)
}

LEAVE A COMMENT