一、引言:Core Data,在本地数据持久化中的地位

在 iOS 开发中,本地数据存储几乎是每一个 App 都绕不开的问题。无论是缓存用户信息、离线浏览内容,还是记录用户操作历史,一个合适的数据持久化方案都能大大提升应用的体验和性能。

而说到苹果官方提供的解决方案,Core Data 无疑是最具代表性的工具之一。它不仅仅是一个数据库封装工具,更是一个面向对象的数据模型框架,允许我们以结构化方式描述实体之间的关系,并通过上下文(Context)来完成对象的创建、查询、更新和删除。

很多人可能对 Core Data 保持一定的距离,觉得它“上手复杂”“冗余配置多”,但其实,Core Data 在近年来的演进中已经发生了非常大的变化:

  • NSPersistentContainer 的引入,极大简化了配置步骤;
  • Xcode 支持自动生成模型类,无需手动维护 NSManagedObject 子类;
  • 与 SwiftUI 的集成也越来越顺畅(虽然本文暂不涉及);

在这篇文章中,我们将以两个简单的实体模型为例:PHDramaEntity 和 PHEpisodeEntity,构建一个“一对多”的关系结构,完整演示 Core Data 从模型创建到数据操作的基本用法。

无论你是第一次接触 Core Data,还是想重新认识这个框架,相信这篇文章都能带你快速上手,并掌握它的核心用法。

二、创建 Core Data 模型文件与实体类

在使用 Core Data 之前,我们第一步需要做的,就是创建一个数据模型文件,并在其中定义好我们项目所需的实体(Entity)、属性(Attribute)和关系(Relationship)。

🛠️ 新建 .xcdatamodeld 文件

首先,我们在项目中新增一个 Core Data 模型文件:文件名:AmericanDramaDB。你可以起任何名字,也可以直接使用默认的名称。

你可以通过 Xcode 的「File → New → File… → Data Model」选项来添加这个模型文件。

📦 创建 PHDramaEntity 实体

接下来,我们在模型中创建一个名为 PHDramaEntity 的实体,它表示一部剧,包含剧名、封面、分组信息等字段。同时,它还会关联多个剧集。

实体字段设计如下:

  1. id: Int64
  2. title: String?
  3. coverFileName: String?
  4. packageID: String?
  5. type: Int64
  6. index: Int64
  7. episodeList: To-Many 关系(指向 PHEpisodeEntity)

其中 episodeList 是一对多关系,表示该剧所拥有的所有剧集。Core Data 支持我们在模型中直接配置这种实体间的引用关系。

Xcode 会根据模型自动生成如下代码:

extension PHDramaEntity {@nonobjc public class func fetchRequest() -> NSFetchRequest<PHDramaEntity> {return NSFetchRequest<PHDramaEntity>(entityName: "PHDramaEntity")}@NSManaged public var coverFileName: String?@NSManaged public var id: Int64@NSManaged public var index: Int64@NSManaged public var packageID: String?@NSManaged public var title: String?@NSManaged public var type: Int64@NSManaged public var episodeList: NSSet?
}// MARK: - Generated accessors for episodeList
extension PHDramaEntity {@objc(addEpisodeListObject:)@NSManaged public func addToEpisodeList(_ value: PHEpisodeEntity)@objc(removeEpisodeListObject:)@NSManaged public func removeFromEpisodeList(_ value: PHEpisodeEntity)@objc(addEpisodeList:)@NSManaged public func addToEpisodeList(_ values: NSSet)@objc(removeEpisodeList:)@NSManaged public func removeFromEpisodeList(_ values: NSSet)
}extension PHDramaEntity : Identifiable { }

你到不需要找到代码在哪,编译成功之后就可以直接使用这个实体及其相关的属性和方法。

🎞️ 创建 PHEpisodeEntity 实体

接下来是 PHEpisodeEntity,它代表具体的某一集剧集,字段更丰富一些,还关联了卡片、阅读记录等内容。

字段设计如下:

  1. id: Int64
  2. title: String?
  3. index: Int64(集数顺序)
  4. episodeCoverFileName: String?
  5. packageId: String?
  6. dramaId: Int64
  7. progress: Double
  8. readDate: Date?
  9. type: Int64
  10. cardList: To-Many(指向 PHCardEntity)
  11. readDateList: To-Many(指向 PHReadRecordEntity)
  12. drama: To-One(指向 PHDramaEntity,作为反向引用)

我们可以只考虑PHEpisodeEntity和PHDramaEntity两个实体,其它实体和关系暂且不需要考虑,不影响对Core Data使用的理解。

Xcode 同样生成如下代码:

extension PHEpisodeEntity {@nonobjc public class func fetchRequest() -> NSFetchRequest<PHEpisodeEntity> {return NSFetchRequest<PHEpisodeEntity>(entityName: "PHEpisodeEntity")}@NSManaged public var dramaId: Int64@NSManaged public var episodeCoverFileName: String?@NSManaged public var id: Int64@NSManaged public var index: Int64@NSManaged public var packageId: String?@NSManaged public var progress: Double@NSManaged public var readDate: Date?@NSManaged public var title: String?@NSManaged public var type: Int64@NSManaged public var cardList: NSSet?@NSManaged public var drama: PHDramaEntity?@NSManaged public var readDateList: NSSet?
}// MARK: - Generated accessors for cardList
extension PHEpisodeEntity {@objc(addCardListObject:)@NSManaged public func addToCardList(_ value: PHCardEntity)@objc(removeCardListObject:)@NSManaged public func removeFromCardList(_ value: PHCardEntity)@objc(addCardList:)@NSManaged public func addToCardList(_ values: NSSet)@objc(removeCardList:)@NSManaged public func removeFromCardList(_ values: NSSet)
}// MARK: - Generated accessors for readDateList
extension PHEpisodeEntity {@objc(addReadDateListObject:)@NSManaged public func addToReadDateList(_ value: PHReadRecordEntity)@objc(removeReadDateListObject:)@NSManaged public func removeFromReadDateList(_ value: PHReadRecordEntity)@objc(addReadDateList:)@NSManaged public func addToReadDateList(_ values: NSSet)@objc(removeReadDateList:)@NSManaged public func removeFromReadDateList(_ values: NSSet)
}extension PHEpisodeEntity : Identifiable { }

值得一提的是,drama 是一个 反向关系,它使我们可以在访问剧集时,直接找到它所属的剧,方便非常多。

以上就是使用 Core Data 创建数据模型的全过程。

下一步,我们将配置 Core Data 栈,准备好 NSPersistentContainer 和上下文,真正开始使用 Core Data 的增删改查功能。

三、配置 Core Data 栈:管理上下文与持久容器

完成模型文件的创建后,我们就可以正式初始化 Core Data 的持久化栈了。这一步的核心就是构建 NSPersistentContainer,并拿到 NSManagedObjectContext,用于后续的数据操作。

为此,我们可以创建一个专门的 Core Data 管理类,比如命名为 PHCoreDataManager,采用单例模式进行统一管理。

🧱 创建 Core Data 管理类

import CoreDataclass PHCoreDataManager: NSObject {static let shared = PHCoreDataManager()let container: NSPersistentContainervar context: NSManagedObjectContext {container.viewContext}private override init() {container = NSPersistentContainer(name: "AmericanDramaDB")container.loadPersistentStores { description, error inif let error = error {fatalError("Core Data 加载失败: \(error)")}}}/// 保存上下文func saveContext() {do {try context.save()} catch {print("保存失败: \(error)")}}
}

📌 关键解释

  • NSPersistentContainer(name:) 中的参数要与你的 .xcdatamodeld 文件名一致(不含扩展名),否则会找不到模型。
  • loadPersistentStores 是异步加载持久化存储的过程,建议在其中加上错误处理。
  • container.viewContext 是我们最常使用的上下文,用于主线程读写操作。
  • saveContext() 方法建议封装在这里,方便统一调用,避免遗漏保存。

❗️注意:Core Data 的操作都基于 context,创建、修改、删除对象后都需要调用 save() 才会真正落盘。如果不保存,应用重启后数据会丢失。

四、增删改查:以 PHDramaEntity 为例

有了模型和 Core Data 栈之后,我们终于可以开始使用 Core Data 进行数据操作了。下面我们就以 PHDramaEntity为例,演示最常见的增、删、改、查操作。

1️⃣ 插入数据(Create)

我们先演示如何创建一条新的 PHDramaEntity 数据,并保存到数据库中。

let context = PHCoreDataManager.shared.contextlet drama = PHDramaEntity(context: context)
drama.id = 1001
drama.title = "绝命毒师"
drama.coverFileName = "breaking_bad.jpg"
drama.packageID = "breaking-bad"
drama.type = 1
drama.index = 0PHCoreDataManager.shared.saveContext()

每次创建实体对象时,都需要传入 context,这是 Core Data 的核心机制。

2️⃣ 查询数据(Read)

通过 NSFetchRequest 可以查询所有 PHDramaEntity 数据,按标题排序:

let request: NSFetchRequest<PHDramaEntity> = PHDramaEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)]do {let dramas = try context.fetch(request)for drama in dramas {print("剧名:\(drama.title ?? "未知"),封面:\(drama.coverFileName ?? "无")")}
} catch {print("查询失败:\(error)")
}

你可以通过 NSPredicate 添加条件过滤,比如查找指定 packageID 的剧。

3️⃣ 更新数据(Update)

我们以“更新某个指定 ID 的剧的标题”为例:

let request: NSFetchRequest<PHDramaEntity> = PHDramaEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %d", 1001)do {if let drama = try context.fetch(request).first {drama.title = "绝命毒师(更新后)"PHCoreDataManager.shared.saveContext()print("更新成功")}
} catch {print("更新失败:\(error)")
}

只需要修改对象属性后再 saveContext() 即可完成更新。

4️⃣ 删除数据(Delete)

删除也是非常直接,只需要调用 context.delete(_:):

let request: NSFetchRequest<PHDramaEntity> = PHDramaEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %d", 1001)do {if let drama = try context.fetch(request).first {context.delete(drama)PHCoreDataManager.shared.saveContext()print("删除成功")}
} catch {print("删除失败:\(error)")
}

删除对象后,也一定记得调用 saveContext(),否则不会真正从数据库移除。

五、操作一对多关系:从剧到剧集的增查操作

在前面我们已经定义好了 PHDramaEntity 和 PHEpisodeEntity 的一对多关系:一个剧(Drama)包含多个剧集(Episode),我们通过 episodeList 来描述这种引用。

本节将演示两个核心操作:

  • 如何向 PHDramaEntity 添加多个 PHEpisodeEntity
  • 如何从一个剧中读取它的所有剧集

1️⃣ 添加多个剧集到某个剧

假设我们已经有一个 PHDramaEntity 对象,接下来我们要为它添加两集内容。

let context = PHCoreDataManager.shared.context// 创建剧
let drama = PHDramaEntity(context: context)
drama.id = 2001
drama.title = "纸牌屋"
drama.packageID = "house-of-cards"
drama.index = 1
drama.type = 1// 创建剧集 1
let ep1 = PHEpisodeEntity(context: context)
ep1.id = 1
ep1.title = "第一集"
ep1.index = 1
ep1.drama = drama // 反向关联// 创建剧集 2
let ep2 = PHEpisodeEntity(context: context)
ep2.id = 2
ep2.title = "第二集"
ep2.index = 2
ep2.drama = drama // 同样反向关联// 保存
PHCoreDataManager.shared.saveContext()

✅ 推荐做法:通过设置剧集的 drama 属性来建立反向引用关系,Core Data 会自动同步 episodeList。

当然你也可以反向添加:

drama.addToEpisodeList(ep1)
drama.addToEpisodeList(ep2)

两种方式等效,哪种更符合你的使用习惯都可以。

2️⃣ 从剧中读取所有剧集

Core Data 一对多关系的字段类型通常是 NSSet?,所以我们需要进行类型转换。

if let episodeSet = drama.episodeList as? Set<PHEpisodeEntity> {let sortedEpisodes = episodeSet.sorted { $0.index < $1.index }for episode in sortedEpisodes {print("剧集 \(episode.index):\(episode.title ?? "未知")")}
}

为了更方便使用,你也可以在 PHDramaEntity 中扩展一个 computed property:

extension PHDramaEntity {var sortedEpisodeArray: [PHEpisodeEntity] {let set = episodeList as? Set<PHEpisodeEntity> ?? []return set.sorted { $0.index < $1.index }}
}

这样你就可以直接这样用:

for episode in drama.sortedEpisodeArray {print(episode.title ?? "")
}

六、结语:Core Data,其实没你想的那么复杂

在这篇文章中,我们从零开始,一步步搭建了 Core Data 的使用框架:

  • 创建数据模型文件,并定义实体和一对多关系;
  • 初始化 Core Data 栈,封装 NSPersistentContainer;
  • 演示了如何对实体进行增删改查操作;
  • 展示了一对多关系的建立与遍历方式;

你可以看到,Core Data 的使用并没有传说中那么复杂。随着 NSPersistentContainer 的出现,以及 Xcode 对模型类的自动生成支持,开发者已经可以非常高效地在项目中集成本地数据持久化功能。

当然,本文只是 Core Data 的起点。后续你还可以探索:

  • 如何设置删除规则(Cascade、Nullify 等);
  • 如何使用 NSFetchedResultsController 优雅地驱动 UI;
  • 如何与 SwiftUI 结合,使用 @FetchRequest 实时监听数据变化;
  • 如何进行数据迁移(Model Versioning);

但只要你掌握了本文的内容,Core Data 的世界就已经向你敞开大门。

如果你还没在项目中使用过 Core Data,不妨就从本文的例子开始,动手试一试吧 🙂

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/92630.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92630.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/92630.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java-79 深入浅出 RPC Dubbo 动态路由架构详解:从规则设计到上线系统集成

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; AI炼丹日志-30-新发布【1T 万亿】参数量大模型&#xff01;Kim…

Linux内核中动态内存分配函数解析

在C语言中&#xff0c;动态内存分配通常用于在运行时申请内存。在内核编程中&#xff0c;动态内存分配与用户空间有所不同&#xff0c;因为内核需要更谨慎地处理内存&#xff0c;且不能使用用户空间的库&#xff08;如glibc&#xff09;。下面我们将详细分析Linux内核中动态申请…

Next.js 中配置不同页面布局方案

在 Next.js 应用中&#xff0c;你可以通过多种方式实现某些页面全屏、某些页面带菜单/页眉/页脚的需求。以下是几种实现方案&#xff1a; 方案一&#xff1a;使用多个布局组件 1. 创建不同的布局组件 // app/default-layout.tsx import Header from /components/header; import…

Spring Boot 使用外置 Servlet 容器:从配置到部署全指南

在 Spring Boot 开发中&#xff0c;我们通常使用嵌入式 Servlet 容器&#xff08;如 Tomcat&#xff09;&#xff0c;它能将应用打包成可执行 JAR&#xff0c;简化部署流程。但在某些场景下&#xff08;如需要支持 JSP、复杂的容器定制或企业级部署规范&#xff09;&#xff0c…

借助AI学习开源代码git0.7之九diff-files

借助AI学习开源代码git0.7之九diff-files diff-files.c 是一个用于比较工作目录中的文件和 Git 索引&#xff08;暂存区&#xff09;中文件的工具。 实质上&#xff0c;它是 git diff命令在不指定特定提交时功能的核心实现。 主要功能分析&#xff1a; 1. 核心功能 diff-files …

社区资源媒体管理系统设计与实现

社区资源媒体管理系统设计与实现 1. 系统概述 社区资源媒体管理系统是一个专为社区户外广告打造的高效、专业化平台&#xff0c;旨在实现社区媒体的数字化管理、智能投放和便捷交易。该系统将整合社区各类广告资源&#xff0c;为广告主、物业公司和社区居民提供一站式服务。 1.…

12.1.6 weak_ptr

weak_ptr weak_ptr会指向一个share_ptr&#xff08;使用一个share_ptr来初始化weak_ptr&#xff09;&#xff0c;但并不会增加这个share_ptr的引用计数器&#xff0c;其析构也不会减少share_ptr的引用计数器。 构造函数及使用 #include <iostream> #include <memory&g…

深度分析Java内存模型

Java 内存模型&#xff08;Java Memory Model, JMM&#xff09;是 Java 并发编程的核心基石&#xff0c;它定义了多线程环境下线程如何与主内存&#xff08;Main Memory&#xff09;以及线程的本地内存&#xff08;工作内存&#xff0c;Working Memory&#xff09;交互的规则。…

代码随想录算法训练营第五十二天|图论part3

101. 孤岛的总面积 题目链接&#xff1a;101. 孤岛的总面积 文章讲解&#xff1a;代码随想录 思路&#xff1a; 与岛屿面积差不多&#xff0c;区别是再dfs的时候&#xff0c;如果碰到越界的&#xff0c;需要用一个符号标记这不是孤岛再continue #include <iostream> #i…

前端实现 excel 数据导出,封装方法支持一次导出多个Sheet

一、前言 后台管理项目有时会有需要前端导出excel表格的功能&#xff0c;有时还需要导出多个sheet&#xff0c;并给每个sheet重新命名&#xff0c;下面我们就来实现一下。 二、实现效果图 三、实现步骤 1、 安装 命令行安装 xlsx 和 file-saver npm install xlsx -S npm i…

【Lambda 表达式】返回值为什么是auto

一个例子&#xff1a; int x 10; auto add_x [x](int y) -> int {return x y; }; int result add_x(5); // 结果是 15lambda 是匿名类型&#xff0c;必须用 auto 来接收。&#xff08;必须写auto&#xff0c;不可省略&#xff09;内层 -> auto 是函数的返回类型自动推…

【小董谈前端】【样式】 CSS与样式库:从实现工具到设计思维的跨越

CSS与样式库&#xff1a;从实现工具到设计思维的跨越 一、CSS的本质&#xff1a;样式实现的「施工队」 CSS作为网页样式的描述语言&#xff0c;其核心能力在于&#xff1a; 精确控制元素的尺寸、位置、颜色实现响应式布局和动画效果与HTML/JavaScript协同完成交互体验 但CS…

MTSC2025参会感悟:大模型 + CV 重构全终端 UI 检测技术体系

目录 一、传统 UI 自动化的困局:高成本与低效率的双重枷锁 1.1 根深蒂固的技术痛点 1.2 多维度质量挑战的叠加 二、Page eyes 1.0:纯视觉方案破解 UI 检测困局 2.1 纯视觉检测的核心理念 2.2 页面加载完成的智能判断 2.3 视觉模型驱动的异常检测 2.4 大模型赋能未知异…

使用Claude Code从零到一打造一个现代化的GitHub Star项目管理器

在日常的开发工作中&#xff0c;我们经常会在GitHub上star一些有用的项目库。随着时间的推移&#xff0c;star的项目越来越多&#xff0c;如何有效管理这些项目成为了一个痛点。 今天&#xff0c;分享我使用Claude Code从零构建的一个GitHub Star管理插件。项目背景与需求分析 …

为什么 Linux 启动后还能升级内核?

✅ 为什么 Linux 启动后还能升级内核&#xff1f; 简单结论&#xff1a; 因为 “安装/升级内核 ≠ 当前就使用该内核”&#xff0c;Linux允许你安装多个内核版本&#xff0c;并在下次启动时选择其中一个来加载运行。 &#x1f9e0; 举个现实生活类比 你在穿一件衣服&#xff08…

Go语言实战案例-统计文件中每个字母出现频率

以下是《Go语言100个实战案例》中的 文件与IO操作篇 - 案例19&#xff1a;统计文件中每个字母出现频率 的完整内容。本案例适合用来练习文件读取、字符处理、map统计等基础技能。&#x1f3af; 案例目标读取一个本地文本文件&#xff0c;统计并打印出其中每个英文字母&#xff…

Notepad++工具操作技巧

1、notepad -> ctrlf -> 替换(正则表达式) -> $-a ->每行的行尾加a&#xff1b; 2、notepad -> ctrlf -> 替换(正则表达式) -> ^-a ->每行的行首加a &#xff1b; 3、按住alt切换为列模式 4、删除空行-不包括有空格符号的空行 查找替代 查找目标…

领码课堂 | Java与AI的“硬核“交响曲:当企业级工程思维遇上智能时代

摘要 &#x1f680; 在AI工业化落地的深水区&#xff0c;Java正以其独特的工程化优势成为中流砥柱。本文系统解构Java在AI项目全生命周期中的技术矩阵&#xff0c;通过"三阶性能优化模型"、"微服务化AI部署架构"等原创方法论&#xff0c;结合大模型部署、M…

面经 - 基于Linux的高性能在线OJ平台

真实面试环境中&#xff0c;被问到的相关问题&#xff0c;感兴趣的可以看下1. 这个项目是你独立完成的吗&#xff1f;团队中你的职责是什么&#xff1f;是的&#xff0c;这个项目是我独立完成的&#xff0c;从需求分析、系统设计到项目部署都我做的。重点工作包括&#xff1a;使…

Ubuntu 20.04 上安装 SPDK

以下是在 Ubuntu 20.04 上安装 SPDK (Storage Performance Development Kit) 的完整步骤&#xff1a;1. 系统准备# 更新系统 sudo apt update sudo apt upgrade -y# 安装基础依赖 sudo apt install -y git make gcc g libssl-dev libaio-dev libnuma-dev \pkg-config python3 p…