SwiftUI中如何从Core Data获取展示数据?解决越界及存储问题
问题分析与解决方案
核心问题拆解
- 数据仅滚动到底部才保存:
LazyVGrid的onAppear仅在单元格显示到屏幕时触发,未滚动到的单元格不会执行保存逻辑,导致数据不全。 - 数组越界错误:大概率是通过索引匹配API数据与Core Data数据,但两者的顺序/数量不一致引发的。
- Core Data数据展示位置不明:未通过唯一标识关联API数据与Core Data数据,无法在
SearchResultRow中精准获取对应存储内容。
解决方案与代码修改
1. 优化Core Data操作逻辑
先给Item实体添加唯一标识字段id(类型选Integer 64),对应API返回作品的id,避免重复保存。然后扩展PersistenceController,添加批量保存和按id查询的方法:
extension PersistenceController { // 批量保存API返回的所有数据 func saveAllResults(_ results: [ResultItem]) { for result in results { // 先检查数据是否已存在,避免重复存储 let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id == %d", result.id) if let existingItem = try? container.viewContext.fetch(fetchRequest).first { existingItem.title = result.title // 已存在则更新标题 } else { let newItem = Item(context: container.viewContext) newItem.id = Int64(result.id) newItem.title = result.title } } do { try container.viewContext.save() } catch { print("Core Data保存失败:\(error)") } } // 根据作品id查询Core Data中的对应数据 func getItemById(_ id: Int) -> Item? { let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id == %d", id) return try? container.viewContext.fetch(fetchRequest).first } }
2. 修改ViewModel,API返回后批量保存数据
把保存逻辑从单元格移到API请求完成后,一次性保存所有数据:
class HomeViewModel: ObservableObject { @Published var results = [ResultItem]() private let coreDM: PersistenceController // 初始化时传入Core Data单例,避免重复创建实例 init(coreDM: PersistenceController = .shared) { self.coreDM = coreDM } func performSearch() { guard let gUrl = URL(string: "https://api.artic.edu/api/v1/artworks") else { return } Task { do { let (data, _) = try await URLSession.shared.data(from: gUrl) let response = try JSONDecoder().decode(ResponseData.self, from: data) DispatchQueue.main.async { [weak self] in guard let self = self, let results = response.data else { return } self.results = results // API数据返回后直接批量保存到Core Data self.coreDM.saveAllResults(results) } } catch { print("API请求失败:\(error)") } } } }
3. 调整ContentView,传递必要参数
不再在SearchResultRow中创建新的PersistenceController,改用单例,并传递作品id用于查询:
struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)], animation: .default) private var items: FetchedResults<Item> @StateObject private var viewModel: HomeViewModel init() { _viewModel = StateObject(wrappedValue: HomeViewModel(coreDM: .shared)) } var body: some View { NavigationView { ScrollView { LazyVGrid(columns: Array(repeating: .init(.flexible()), count: UIDevice.current.userInterfaceIdiom == .pad ? 4 : 2)) { ForEach(viewModel.results, id: \.id) { result in let resultVM = ResultVM(model: result) NavigationLink(destination: DetailView(data: resultVM.trackName)) { SearchResultRow(resultVM: resultVM, coreDM: .shared, itemId: result.id) } } } } } .onAppear { viewModel.performSearch() } } }
4. 修改SearchResultRow,展示Core Data数据
根据传入的作品id查询Core Data,优先展示存储的数据,没有则用API数据:
struct SearchResultRow: View { let resultVM: ResultVM let coreDM: PersistenceController let itemId: Int // 实时查询对应Core Data数据 private var coreDataItem: Item? { coreDM.getItemById(itemId) } var body: some View { HStack { RoundedRectangle(cornerRadius: 16) .fill(.yellow) .frame(maxWidth: .infinity) .aspectRatio(1, contentMode: .fit) // 优先显示Core Data的数据, fallback到API数据 .overlay(Text(coreDataItem?.title ?? resultVM.trackName)) } .padding() .background(Color.red) } }
关键注意事项
- 始终使用
PersistenceController.shared单例,避免创建多个上下文导致数据不一致 - 用唯一
id匹配API与Core Data数据,彻底解决数组越界问题 - 批量保存逻辑确保所有API数据一次性存入Core Data,无需依赖滚动触发
内容的提问来源于stack exchange,提问作者New iOS Dev
相关产品推荐
相关产品推荐

