Миграция модели SwiftData с обратной зависимостьюIOS

Программируем под IOS
Ответить Пред. темаСлед. тема
Anonymous
 Миграция модели SwiftData с обратной зависимостью

Сообщение Anonymous »

У меня есть вопрос, на который я не могу найти ответ.
Я пытаюсь перенести свою схему 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)
}
модель
Вот схема, которую я пытаюсь перенести. В 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
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «IOS»