在 iOS 应用中使用 Core Data 保存数据
本文通过描述编写一个待办清单应用的过程, 阐述在 iOS 应用中, 如何分离数据层与视图层. 如何使用 Core Data 保存数据. 如何保存两个数据结构之间的关系.
前提条件
示例代码
创建 xcdatamodeld 文件
在 Model 文件夹下创建一个新的文件, 类型是 "Data Model".
创建 Persistence.swift
在 Data/Local 文件夹下创建 Persistence.swift 文件, 里面定义访问 Core Data 的逻辑.
swift
//
// Persistence.swift
// demo5-todo-list
//
// Created by arno_solo on 3/20/25.
//
import CoreData
struct PersistenceController {
static let shared = PersistenceController(dbName: "demo5_todo_list", appGroupsId: nil)
static let preview: PersistenceController = {
let result = PersistenceController(dbName: "demo5_todo_list", appGroupsId: nil, inMemory: true)
return result
}()
let container: NSPersistentContainer
init(dbName: String, appGroupsId: String? = nil, inMemory: Bool = false) {
container = NSPersistentContainer(name: dbName)
if let appGroupsId,
let appGroupsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupsId) {
// Specify the URL for the persistent store
let storeURL = appGroupsURL.appendingPathComponent("\(dbName).sqlite")
// Set up the persistent store with the shared URL
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print(error.localizedDescription)
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}定义数据类型
在 Model 文件夹定义 TodoModel, 然后在 xcdatamodeld 文件中定义一个对应的 TodoEntity.
swift
import Foundation
struct TodoModel {
let todoId: String
let createdAt: Date
let updatedAt: Date
let title: String
let completedAt: Date?
let tags: [TagModel]
let location: LocationModel?
}定义 DAO
定义一个 TodoDAO 负责持久化 TodoModel.
swift
import CoreData
class TodoDAO {
enum CustomError: Error, LocalizedError {
case notFound
}
static let shared = TodoDAO(container: PersistenceController.shared.container)
private let container: NSPersistentContainer
init(container: NSPersistentContainer) {
self.container = container
}
func createOne(todo: TodoModel) async throws {
try await container.performBackgroundTask { ctx in
let entity = Self.findEntity(todoId: todo.todoId, ctx: ctx) ?? TodoEntity(context: ctx)
Self.modifyEntity(entity: entity, todo: todo)
try ctx.save()
}
}
func updateOne(todo: TodoModel) async throws {
try await container.performBackgroundTask { ctx in
guard let entity = Self.findEntity(todoId: todo.todoId, ctx: ctx) else {
throw CustomError.notFound
}
Self.modifyEntity(entity: entity, todo: todo)
entity.removeFromTags(entity.tags ?? [])
for tag in todo.tags {
if let tagEntity = TagDAO.findEntity(tagId: tag.tagId, ctx: ctx) {
entity.addToTags(tagEntity)
}
}
if let id = todo.location?.id {
entity.location = LocationDAO.findEntity(id: id, ctx: ctx)
} else {
entity.location = nil
}
try ctx.save()
}
}
func deleteOne(todoId: String) async throws {
return try await container.performBackgroundTask { ctx in
guard let entity = Self.findEntity(todoId: todoId, ctx: ctx) else {
throw CustomError.notFound
}
ctx.delete(entity)
try ctx.save()
}
}
func findOne(todoId: String) async -> TodoModel? {
return await container.performBackgroundTask { ctx in
guard let entity = Self.findEntity(todoId: todoId, ctx: ctx) else { return nil }
return Self.entityToModel(entity: entity, ctx: ctx)
}
}
// page: Start from 0
func findMany(searchInput: String?, tagId: String?, page: Int?, pageSize: Int?) async throws -> [TodoModel] {
let req = TodoEntity.fetchRequest()
// Search
var predicates: [NSPredicate] = []
if let searchInput, !searchInput.isEmpty {
// c: Case insensitive
// d: Diacritic insensitive
let predicate = NSPredicate(format: "title CONTAINS[cd] %@", searchInput)
predicates.append(predicate)
}
if let tagId {
let predicate = NSPredicate(format: "ANY tags.tagId == %@", tagId)
predicates.append(predicate)
}
// Pagination
if let page, let pageSize {
req.fetchLimit = pageSize
req.fetchOffset = page * pageSize
}
// Sort
req.sortDescriptors = [NSSortDescriptor(key: "updatedAt", ascending: false)]
return try await container.performBackgroundTask { ctx in
let entities = try ctx.fetch(req)
return entities.compactMap { Self.entityToModel(entity: $0, ctx: ctx) }
}
}
static func findEntity(todoId: String, ctx: NSManagedObjectContext) -> TodoEntity? {
do {
let req = TodoEntity.fetchRequest()
req.predicate = NSPredicate(format: "todoId = %@", todoId)
let entities = try ctx.fetch(req)
return entities.isEmpty ? nil : entities[0]
} catch {
print(error.localizedDescription)
return nil
}
}
static func modifyEntity(entity: TodoEntity, todo: TodoModel) {
entity.todoId = todo.todoId
entity.createdAt = todo.createdAt
entity.updatedAt = todo.updatedAt
entity.title = todo.title
entity.completedAt = todo.completedAt
}
static func entityToModel(entity: TodoEntity, ctx: NSManagedObjectContext) -> TodoModel? {
guard let todoId = entity.todoId,
let createdAt = entity.createdAt,
let updatedAt = entity.updatedAt,
let title = entity.title
else { return nil }
let tagEntities = entity.tags?.allObjects as? [TagEntity]
let tags: [TagModel] = (tagEntities ?? []).compactMap { entity in
TagDAO.entityToModel(entity: entity, ctx: ctx)
}
var location: LocationModel? = nil
if let locationEntity = entity.location {
location = LocationDAO.entityToModel(entity: locationEntity, ctx: ctx)
}
return TodoModel(
todoId: todoId,
createdAt: createdAt,
updatedAt: updatedAt,
title: title,
completedAt: entity.completedAt,
tags: tags,
location: location
)
}
}关系
修改 .xcdatamodeld 文件
多对多
- 在 TodoEntity > Relationships 界面中增加
tags字段, Destination 选择TagEntity. 打开右侧栏 > Relationship, 选择 Type 为To Many - 在 TagEntity > Relationships 界面中增加
todos字段, Destination 选择TodoEntity. 打开右侧栏 > Relationship, 选择 Type 为To Many - 选择 TagEntity > Relationships > Inverse 为
todos
一对多
- 在 TodoEntity > Relationships 界面中增加
location字段, Destination 选择LocationEntity - 在 LocationEntity > Relationships 界面中增加
todos字段, Destination 选择TodoEntity. 打开右侧栏 > Relationship, 选择 Type 为To Many
写入
swift
// Models/TodoModel.swift
class TodoDAO {
func updateOne(todo: TodoModel) async throws {
try await container.performBackgroundTask { ctx in
guard let entity = Self.findEntity(todoId: todo.todoId, ctx: ctx) else {
throw CustomError.notFound
}
Self.modifyEntity(entity: entity, todo: todo)
// 多对多
entity.removeFromTags(entity.tags ?? [])
for tag in todo.tags {
if let tagEntity = TagDAO.findEntity(tagId: tag.tagId, ctx: ctx) {
entity.addToTags(tagEntity)
}
}
// 一对多
if let id = todo.location?.id {
entity.location = LocationDAO.findEntity(id: id, ctx: ctx)
} else {
entity.location = nil
}
try ctx.save()
}
}
}读取
swift
// Models/TodoModel.swift
class TodoDAO {
static func entityToModel(entity: TodoEntity, ctx: NSManagedObjectContext) -> TodoModel? {
guard let todoId = entity.todoId,
let createdAt = entity.createdAt,
let updatedAt = entity.updatedAt,
let title = entity.title
else { return nil }
let tagEntities = entity.tags?.allObjects as? [TagEntity]
let tags: [TagModel] = (tagEntities ?? []).compactMap { entity in
TagDAO.entityToModel(entity: entity, ctx: ctx)
}
var location: LocationModel? = nil
if let locationEntity = entity.location {
location = LocationDAO.entityToModel(entity: locationEntity, ctx: ctx)
}
return TodoModel(
todoId: todoId,
createdAt: createdAt,
updatedAt: updatedAt,
title: title,
completedAt: entity.completedAt,
tags: tags,
location: location
)
}
}分离数据层与视图层
我个人的偏好是,
- 一种数据类型定义一个
DAO(Data access object).DAO负责读写本地的 sqlite 数据库. - 一项后端服务定义一个
Service.Service负责与远程数据库进行通讯. - 一种数据类型定义一个
Repository.Repository负责协调数据从本地数据库还是远程数据库获取. - 在视图中尽量通过调用
Repository而不是DAO或Service来读写数据.
下面是一个文件结构的示例:
yml
Data/
Local/
TodoDAO.swift
TagDAO.swift
Network/
FirestoreService.swift
TodoRepository.swift
TagRepository.swift下面是 TodoRepository 的代码, 目前只有读写本地数据库的代码, 如果以后有服务器请求的代码, 也可以往这个类里写.
swift
import Foundation
class TodoRepository {
static let shared = TodoRepository(
todoDAO: TodoDAO.shared,
tagDAO: TagDAO.shared
)
private var todoDAO: TodoDAO
private var tagDAO: TagDAO
init(todoDAO: TodoDAO, tagDAO: TagDAO) {
self.todoDAO = todoDAO
self.tagDAO = tagDAO
}
func createTodo(todo: TodoModel) async throws {
try await todoDAO.createOne(todo: todo)
try await todoDAO.updateOne(todo: todo)
}
func updateTodo(todo: TodoModel) async throws {
try await todoDAO.updateOne(todo: todo)
}
func deleteTodo(todo: TodoModel) async throws {
try await todoDAO.deleteOne(todoId: todo.todoId)
}
func findTodos(searchText: String?, tagId: String?, page: Int?, pageSize: Int?) async throws -> [TodoModel] {
return try await todoDAO.findMany(searchInput: searchText, tagId: tagId, page: page, pageSize: pageSize)
}
}参考资料