Я пытаюсь перенести свою схему SwiftData на новую версию. Раньше у меня была неверсионная схема, которую я воспроизвел в версии 1, затем я внес некоторые изменения в версию 2.
Сначала я объясню модели, которые у меня есть: у меня есть Word и соответствующие Категория.
Код: Выделить всё
Category
Содержит имя и список связанных Word (
Код: Выделить всё
ColorChoice
Код: Выделить всё
@Model
class Category: Codable, Equatable {
enum CodingKeys: CodingKey {
case name, primaryColor, secondaryColor, colorChoiceId, symbol
}
var name: String = "" // not marked as unique due to iCloud requirements with SwiftData, enforced at creation
var colorChoiceId: Int = 0
var symbol: Symbol = Symbol.sunMax
var words: [Word]? = [Word]()
var unwrappedWords: [Word] {
words ?? []
}
var primaryColor: Color {
ColorChoice.choices[colorChoiceId]?.primaryColor ?? .mint
}
var secondaryColor: Color {
ColorChoice.choices[colorChoiceId]?.secondaryColor ?? .blue
}
var tintColor: Color {
ColorChoice.choices[colorChoiceId]?.tintColor ?? .blue
}
init(name: String, colorChoiceId: Int, symbol: Symbol) {
self.name = name
self.colorChoiceId = colorChoiceId
self.symbol = symbol
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.colorChoiceId = try container.decode(Int.self, forKey: .colorChoiceId)
self.symbol = try container.decode(Symbol.self, forKey: .symbol)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.colorChoiceId, forKey: .colorChoiceId)
try container.encode(self.symbol, forKey: .symbol)
}
static func decodeCategories(from json: String) throws -> [Category] {
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode([Category].self, from: data)
}
static func encodeCategories(_ categories: [Category]) throws -> String {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
let encodedCategories = try encoder.encode(categories)
return String(data: encodedCategories, encoding: .utf8)!
}
static func == (lhs: Category, rhs: Category) -> Bool {
lhs.name == rhs.name
}
/// The sort order used for querying the list of categories.
static let sortDescriptors = [SortDescriptor(\Category.name)]
static let example = Category(name: "General", colorChoiceId: 7, symbol: .trayFull)
static let otherExample = Category(name: "Italian words", colorChoiceId: 1, symbol: .sunHorizon)
}
Код: Выделить всё
Word
Вот схема, которую я пытаюсь перенести. В V2 я добавил идентификатор.
Код: Выделить всё
typealias Word = WordSchemaV2.Word
enum WordSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Word.self, Category.self]
}
@Model
class Word: Codable {
enum CodingKeys: CodingKey {
case id, term, learntOn, notes, category, bookmarked
}
var id: UUID = UUID()
let term: String = ""
let learntOn: Date = Date.now
var notes: String = ""
@Relationship(inverse: \Category.words) var category: Category?
var bookmarked: Bool = false
var categoryName: String {
let localizedNoCategory = String(localized: "No category", comment: "The text to display in absence of a user-defined category")
return category?.name ?? localizedNoCategory
}
var categoryIcon: Image {
if let category {
Image(systemName: category.symbol.rawValue)
} else {
Image(.customTraySlash)
}
}
var primaryColor: Color {
category?.primaryColor ?? .mint
}
var secondaryColor: Color {
category?.secondaryColor ?? .blue
}
init(term: String, learntOn: Date, notes: String = "", category: Category? = nil, bookmarked: Bool = false) {
self.id = UUID()
self.term = term
self.learntOn = learntOn
self.notes = notes
self.category = category
self.bookmarked = bookmarked
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UUID.self, forKey: .id)
self.term = try container.decode(String.self, forKey: .term)
let learntOn = try container.decode(String.self, forKey: .learntOn)
self.learntOn = try Date(learntOn, strategy: .iso8601)
self.notes = try container.decode(String.self, forKey: .notes)
self.category = try container.decode(Category?.self, forKey: .category)
self.bookmarked = try container.decode(Bool.self, forKey: .bookmarked)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.term, forKey: .term)
try container.encode(self.learntOn, forKey: .learntOn)
try container.encode(self.notes, forKey: .notes)
try container.encode(self.category, forKey: .category)
try container.encode(self.bookmarked, forKey: .bookmarked)
}
static func decodeWords(from json: String) throws -> [Word] {
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode([Word].self, from: data)
}
static func encodeWords(_ words: [Word]) throws -> String {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
let encodedWords = try encoder.encode(words)
return String(data: encodedWords, encoding: .utf8)!
}
/// The predicate used for querying the list of words.
static func predicate(category: Category?) -> Predicate {
let categoryName = category?.name
return #Predicate { word in
// the predicate does not support comparing two different objects (either Words or Categories);
// also, Predicates do not support external variables, local ones have to be used:
// the working solution is to compare strings and not objects and use a local variable (categoryName)
categoryName == nil || word.category?.name == categoryName
}
}
/// The sort order used for querying the list of words.
static let sortDescriptors = [SortDescriptor(\Word.learntOn, order: .reverse)]
static let example = Word(term: "Swift", learntOn: .now, notes: "A swift testing word.", bookmarked: true)
static let otherExample = Word(term: "Apple", learntOn: .now.addingTimeInterval(-86400), notes: "A fruit or a company?")
}
}
enum WordSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Word.self, Category.self]
}
@Model
class Word: Codable {
enum CodingKeys: CodingKey {
case term, learntOn, notes, category, bookmarked
}
let term: String = ""
let learntOn: Date = Date.now
var notes: String = ""
@Relationship(inverse: \Category.words) var category: Category?
var bookmarked: Bool = false
var categoryName: String {
let localizedNoCategory = String(localized: "No category", comment: "The text to display in absence of a user-defined category")
return category?.name ?? localizedNoCategory
}
var categoryIcon: Image {
if let category {
Image(systemName: category.symbol.rawValue)
} else {
Image(.customTraySlash)
}
}
var primaryColor: Color {
category?.primaryColor ?? .mint
}
var secondaryColor: Color {
category?.secondaryColor ?? .blue
}
init(term: String, learntOn: Date, notes: String = "", category: Category? = nil, bookmarked: Bool = false) {
self.term = term
self.learntOn = learntOn
self.notes = notes
self.category = category
self.bookmarked = bookmarked
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.term = try container.decode(String.self, forKey: .term)
let learntOn = try container.decode(String.self, forKey: .learntOn)
self.learntOn = try Date(learntOn, strategy: .iso8601)
self.notes = try container.decode(String.self, forKey: .notes)
self.category = try container.decode(Category?.self, forKey: .category)
self.bookmarked = try container.decode(Bool.self, forKey: .bookmarked)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.term, forKey: .term)
try container.encode(self.learntOn, forKey: .learntOn)
try container.encode(self.notes, forKey: .notes)
try container.encode(self.category, forKey: .category)
try container.encode(self.bookmarked, forKey: .bookmarked)
}
static func decodeWords(from json: String) throws -> [Word] {
let data = json.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode([Word].self, from: data)
}
static func encodeWords(_ words: [Word]) throws -> String {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
let encodedWords = try encoder.encode(words)
return String(data: encodedWords, encoding: .utf8)!
}
/// The predicate used for querying the list of words.
static func predicate(category: Category?) -> Predicate {
let categoryName = category?.name
return #Predicate { word in
// the predicate does not support comparing two different objects (either Words or Categories);
// also, Predicates do not support external variables, local ones have to be used:
// the working solution is to compare strings and not objects and use a local variable (categoryName)
categoryName == nil || word.category?.name == categoryName
}
}
/// The sort order used for querying the list of words.
static let sortDescriptors = [SortDescriptor(\Word.learntOn, order: .reverse)]
static let example = Word(term: "Swift", learntOn: .now, notes: "A swift testing word.", bookmarked: true)
static let otherExample = Word(term: "Apple", learntOn: .now.addingTimeInterval(-86400), notes: "A fruit or a company?")
}
}
enum WordsMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[WordSchemaV1.self, WordSchemaV2.self]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: WordSchemaV1.self,
toVersion: WordSchemaV2.self,
willMigrate: { modelContext in
let descriptor = FetchDescriptor()
let words = try modelContext.fetch(descriptor)
for word in words {
print("Migrating \(word.term)")
let newWord = WordSchemaV2.Word(term: word.term, learntOn: word.learntOn, notes: word.notes, category: word.category, bookmarked: word.bookmarked)
print(newWord)
modelContext.insert(newWord)
}
try modelContext.save()
},
didMigrate: nil)
static var stages: [MigrationStage] {
[migrateV1toV2]
}
}
При миграции оператор печати выполняется правильно. Я получаю сообщение об ошибке: «Не удалось привести значение типа WordSchemaV1.Word к WordSchemaV2.Word», а затем внутри макроса @Relationship отображается ошибка, поэтому я предполагаю, что список Word s, содержащиеся в категории, почему-то не обновляются и по-прежнему относятся к Word V1.
Поэтому у меня возникает общий вопрос: как вы справляетесь с пользовательской миграцией с помощью обратной отношение? Обратите внимание, что это слово может не быть связано ни с одной категорией.
Подробнее здесь: https://stackoverflow.com/questions/792 ... lationship