You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftData存储JSON解码对象时数组顺序错乱问题排查

问题:SwiftData存储后Lesson和Vocab顺序错乱原因分析

我正在开发一款词汇类App,首次启动时会解码JSON并将结果对象通过SwiftData存储。JSON采用多层数组结构:Unit数组中的每个Unit包含Lesson数组,每个Lesson又包含Vocab数组。通过@Query按id排序后Unit的顺序正确,但Lesson和Vocab对象的顺序出现错乱,请问该问题的原因是什么?

JSON加载函数

func load<T: Decodable>(_  filename: String) -> T {
    let data: Data    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle!")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

解码对象模型

import Foundation
import SwiftData

@Model
final class Unit: Identifiable, Codable {
    var id: Int
    var lessons: [Lesson]
    
    init(id: Int, lessons: [Lesson]) {
        self.id = id
        self.lessons = lessons
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        lessons = try container.decode([Lesson].self, forKey: .lessons)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id.self, forKey: .id)
        try container.encode(lessons.self, forKey: .lessons)
    }
    
    private enum CodingKeys: String, CodingKey {
        case id, lessons
    }
    
    
}

@Model
final class Lesson: Identifiable, Codable {
    var id: Int
    var vocab: [Vocab]
    
    init(id: Int, vocab: [Vocab]) {
        self.id = id
        self.vocab = vocab
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        vocab = try container.decode([Vocab].self, forKey: .vocab)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id.self, forKey: .id)
        try container.encode(vocab.self, forKey: .vocab)
    }
    
    private enum CodingKeys: String, CodingKey {
        case id, vocab
    }
}

@Model
final class Vocab: Identifiable, Codable {
    var id: Int
    var word: String
    var meaning: String
    var url: String
    var practiceData: PracticeData
    
    init(id: Int, word: String, meaning: String, url: String) {
        self.id = id
        self.word = word
        self.meaning = meaning
        self.url = url
        self.practiceData = PracticeData()
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.word = try container.decode(String.self, forKey: .word)
        self.meaning = try container.decode(String.self, forKey: .meaning)
        self.url = try container.decode(String.self, forKey: .url)
        self.practiceData = PracticeData()
    }
    
    private enum CodingKeys: String, CodingKey {
        case id, word, meaning, url, practiceData
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id.self, forKey: .id)
        try container.encode(word.self, forKey: .word)
        try container.encode(meaning.self, forKey: .meaning)
        try container.encode(url.self, forKey: .url)
        try container.encode(practiceData.self, forKey: .practiceData)
    }
}

JSON结构

[
  {
    "id": 1,
    "lessons": [
      {
        "id": 1,
        "vocab": [
          {
            "id": 1,
            "word": xxx,
            "meaning": xxx,
            "url": xxx
          },
          ...
        ]
      },
      ...
    ]
  },
  ...
]

视图代码

@Environment(\.modelContext) private var modelContext
    @Query(sort: \Unit.id) private var units: [Unit]
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach(units) { unit in
                    Text(String(unit.id))
                    UnitRow(unit: unit)
                }
            }
        }
        ...
    }

问题截图

圆形中的数字为lesson.id


原因分析与解决方案

核心原因

  • SwiftData基于Core Data实现,默认情况下,@Model类中的数组属性会被转换为无序的关系集合,而非保留原数组的顺序。这是Core Data一对多/多对多关系的默认特性。
  • 仅对Unit通过@Query(sort: \Unit.id)指定了排序规则,但Lesson和Vocab从SwiftData中取出时未设置排序逻辑,因此会以随机顺序返回,导致显示错乱。

解决方案

  1. 手动排序子对象
    在访问lessons和vocab时,手动按id排序:
// 在UnitRow中使用排序后的Lesson数组
let sortedLessons = unit.lessons.sorted(by: { $0.id < $1.id })
// 在Lesson对应的视图中使用排序后的Vocab数组
let sortedVocab = lesson.vocab.sorted(by: { $0.id < $1.id })
  1. 使用有序关系(iOS 17+/macOS 14+)
    如果系统版本满足要求,可通过@Relationship(ordered: true)修饰数组属性,让SwiftData保留原数组顺序:
@Model
final class Unit: Identifiable, Codable {
    var id: Int
    @Relationship(ordered: true) var lessons: [Lesson]
    
    // 其余代码保持不变
}

@Model
final class Lesson: Identifiable, Codable {
    var id: Int
    @Relationship(ordered: true) var vocab: [Vocab]
    
    // 其余代码保持不变
}

注意:使用有序关系需要重新初始化数据模型,首次启动需重新导入JSON数据。


内容的提问来源于stack exchange,提问作者trsommer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 14:46:04