Класс SwiftData @Model, порядок переключения операторов в блоке инициализации неожиданно нарушит представление

У меня простое отношение «один ко многим». Плейлист и песня.

В том же представлении верхняя часть печатает массивы playlist.name и playlist.songs. Ожидается, что массив будет обновляться при добавлении новых песен.

Удивительно, но внутри блока инициализации Song порядок двух операторов нарушает поведение обновления представления.

    // 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

Кажется, это ошибка, и я сообщил об этом. Я хочу понять это лучше:

  1. объяснения или понимание быстрого языка, макросов и т. д. о том, как это могло произойти.
  2. любые обходные пути или какая лучшая стратегия для достижения того же поведение, не подвергаясь этой неопределенности.

Полный пример ниже:

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)
}

2. Другие возможные обходные пути см. в этом ответе.

Joakim Danielson 12.04.2024 08:30

1. Я попытался отладить это и проверить некоторые свойства, добавленные макросом @Model, но не смог найти ничего полезного. Это очень интересное (и странное) открытие: порядок свойств имеет значение или, скорее, то, что он имеет значение, когда устанавливается связь. Я подозреваю, что ошибка находится где-то глубоко в коде SwiftData, и мы не можем понять, что это такое, поэтому нам просто нужно подождать, пока Apple ее исправит.

Joakim Danielson 12.04.2024 21:46

@JoakimDanielson Спасибо, что рассмотрели этот любопытный случай. (Я не был уверен, насколько воспроизводима эта ошибка.) Я экспериментирую с подходами, на которые вы ссылались в другой теме. Я пытаюсь найти документы о том, как свойства @Query и @Relation взаимодействуют с ними при ручных мутациях. Например. как бы эти ручные мутации сохранялись (или отбрасывались) без этих modelContext.insert() и как разрешаются конфликты.

Weishi Z 13.04.2024 12:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Изменение порядка исправит проблему для одного отношения. Если модель имеет несколько отношений внешнего ключа, она все равно не будет работать на тех, которые не входят в последний.

Самый надежный способ обработки отношений SwiftData, который я нашел, — это всегда сначала создавать сущность, а потом связывать сущности. (и это, возможно, связано с соображениями облачной синхронизации при разработке SwiftData)

В этом случае:

@Model
final class Song {
  var playlist: Playlist? = nil
  var name: String
  init(name: String) {
    self.name = name
  }
}

extension ModelContext {
  func insertSong(
    var name: String
    var playlist: Playlist?
  ) {
    let song = Song(name: name)
    self.insert(song)
    guard let playlist = playlist else { return } 
    // find out if playlists exists (e.g. fetched from modelContext) or newly created (e.g. via Playlist(name: "name")) 
    if let fetched = self.fetchOne(id: playlist.id) {
      // associate song with existing
      song.playlist = fetched 
    } else {
      // insert playlist first and then make associations
      self.insert(playlist)
      song.playlist = playlist
    }
  }

  func fetchOne<T>(id: PersistentIdentifier)
    -> T? where T: PersistentModel
  {
    if let registered: T = registeredModel(for: id) {
      return registered
    }

    var fetchDescriptor = FetchDescriptor<T>(
      predicate: #Predicate {
        $0.persistentModelID == id
      })
    fetchDescriptor.fetchLimit = 1

    do {
      return try fetch(fetchDescriptor).first
    } catch {
      logger.error(
        "Failed to fetchOne for id. Entity name: \(id.entityName) Error: \(error.localizedDescription)"
      )
    }
    return nil
  }
}

Другие вопросы по теме