У меня простое отношение «один ко многим». Плейлист и песня.
В том же представлении верхняя часть печатает массивы 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
Кажется, это ошибка, и я сообщил об этом. Я хочу понять это лучше:
Полный пример ниже:
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)
}
1. Я попытался отладить это и проверить некоторые свойства, добавленные макросом @Model, но не смог найти ничего полезного. Это очень интересное (и странное) открытие: порядок свойств имеет значение или, скорее, то, что он имеет значение, когда устанавливается связь. Я подозреваю, что ошибка находится где-то глубоко в коде SwiftData, и мы не можем понять, что это такое, поэтому нам просто нужно подождать, пока Apple ее исправит.
@JoakimDanielson Спасибо, что рассмотрели этот любопытный случай. (Я не был уверен, насколько воспроизводима эта ошибка.) Я экспериментирую с подходами, на которые вы ссылались в другой теме. Я пытаюсь найти документы о том, как свойства @Query
и @Relation
взаимодействуют с ними при ручных мутациях. Например. как бы эти ручные мутации сохранялись (или отбрасывались) без этих modelContext.insert()
и как разрешаются конфликты.
Изменение порядка исправит проблему для одного отношения. Если модель имеет несколько отношений внешнего ключа, она все равно не будет работать на тех, которые не входят в последний.
Самый надежный способ обработки отношений 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
}
}
2. Другие возможные обходные пути см. в этом ответе.