#76 Core Data 的修煉-1:從簡單的任務開始 , 存取單一欄位資料表(新增、刪除資料)。

Core Data 的目的是讓我們輕鬆使用資料庫,但它的高級抽象設計可能會讓人望而卻步。因此,我們將從一個簡單的任務開始,只用最少的程式碼來完成一個基本的資料存取任務,順利跨進 Core Data 的世界。

APP操作 GIF:

  • Add String 新增 Entity 資料
  • 在Label 裡顯示 Entity 資料
  • Delete All 刪除 Entity 裡所有資料

螢幕截圖:

實驗目標

  • 了解 Core Data 的基本工作原理及設定。
  • 探討如何使用 Core Data 進行基本的數據操作,包括插入、查詢和刪除。
  • 通過程式範例來展示如何在 iOS 應用中使用 Core Data 進行數據持久化。

建立專案

建立新專案時,在 Storage 選項中勾選 Core Data。

專案自動增加的地方

AppDelegate.swift

  • 新增 NSPersistentContainer 變數 persistentContainer
  • 使用 persistentContainersaveContext() 方法中進行資料儲存。

.xcdatamodeld

  • 新增 .xcdatamodeld 檔案並可以在這裡設置資料模型。

程式重點

簡化 AppDelegate

將預設的 Core Data 相關代碼移至 CoreDataStack 類別,並在 SceneDelegate中引用該類別。

import CoreData

class CoreDataStack {
static let shared = CoreDataStack()

// MARK: - Core Data stack
let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "phw23_2_coredata")
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()

var context: NSManagedObjectContext {
return persistentContainer.viewContext
}

// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}

private init() {}
}

修改 SceneDelegate.swift

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
func sceneDidEnterBackground(_ scene: UIScene) {
//(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
CoreDataStack.shared.saveContext()
}
}

建立 Entity

.xcdatamodeld 中新增一個名為 "SimpleEntity" 的 Entity,並添加一個 "simple_value" 屬性(只能使用小寫、底線和數字)。

儲存資料

在 ViewController 建立一個新增資料的函數 ,程式很簡單,就是輸入一個字串,儲起來。

 func insertSimpleValue(_ value: String) {
let context = CoreDataStack.shared.context
let entity = NSEntityDescription.entity(forEntityName: "SimpleEntity", in: context)!
let simpleObject = NSManagedObject(entity: entity, insertInto: context)

simpleObject.setValue(value, forKey: "simple_value")

do {
try context.save()
} catch {
print("Failed to save value: \(error)")
}
}

然後在 viewDidLoad() 方法中呼叫該函數。

    override func viewDidLoad() {
super.viewDidLoad()
insertSimpleValue("Jason:\(Int.random(in: 0..<100))")
}

就這樣,資料就儲存好了。

讀取資料

接下來我們讀取資料出來看看。

建立抓資料的函數,回傳值是不包含 nil 值的字串陣列。

    func fetchSimpleValues() -> [String]? {
let context = CoreDataStack.shared.context
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "SimpleEntity")

do {
let results = try context.fetch(fetchRequest)
return results.map { $0.value(forKey: "simple_value") as? String }.compactMap { $0 }
} catch {
print("Failed to fetch values: \(error)")
return nil
}
}

為何使用 compactMap

compactMap 會對數組中的每個元素應用一個閉包,並返回一個過濾掉 nil 值的新數組。如果 values 包含 nil,直接將它們拼接成字符串會出現問題,甚至導致應用崩潰。所以使用 compactMap 來確保只有非 nil 的值被處理和顯示。

ViewController 中,我們可以使用 fetchSimpleValues 來更新顯示的資料:

class ViewController: UIViewController {

@IBOutlet weak var stringLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
insertSimpleValue("Jason:\(Int.random(in: 0..<100))")
updateStringLabel()
}

func updateStringLabel() {
if let values = fetchSimpleValues() {
stringLabel.text = values.joined(separator: "\n")
}
}
...
}

這樣,我們就可以在 viewDidLoad() 方法中呼叫 insertSimpleValueupdateStringLabel 來顯示插入的資料。

執行看看

增加和刪除資料

那我們再添加兩個按鈕,一個用於新增資料,一個用於刪除所有資料。

刪除資料的函數

首先,建立一個刪除資料的函數:

    func deleteAllData() {
let context = CoreDataStack.shared.context
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "SimpleEntity")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

do {
try context.execute(batchDeleteRequest)
try context.save() // 保存上下文
} catch {
print("Failed to delete all data: \(error)")
}
}

然後,實現按鈕的動作方法。新增資料 insertSimpleValue 剛才已經寫好了,直接呼叫就行。

    @IBAction func AddString(_ sender: Any) {
insertSimpleValue("Jason:\(Int.random(in: 0..<100))")
updateStringLabel()
}

@IBAction func deleteAll(_ sender: Any) {
deleteAllData()
updateStringLabel()
}

結語

就這麼簡單,通過使用 Core Data,我們輕鬆地建立了一個簡單的資料表。透過 Core Data 提供的方便的工具,我們能夠輕鬆地存取和管理這個資料表。這個筆記實驗了如何使用 Core Data 在應用程式中插入、查詢和刪除資料,實現了數據持久化。希望這個簡單的範例能夠幫助我們更好地理解如何利用 Core Data 來建立和管理你的應用程式中的資料。

深入研究

接下來,我們來看一下剛才的程式碼,詳細了解 Core Data 的工作原理。

Core Data Stack

這段程式碼建立了一個 NSPersistentContainer 實例,它管理 Core Data 的持久性存儲:

// MARK: - Core Data stack
let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "phw23_2_coredata")
container.loadPersistentStores {…}
return container
}()

當你想要在應用程式中使用 Core Data 來保存資料時,你需要一個容器來管理這些資料。這個容器就像是一個盒子,可以幫助你管理所有的 Core Data 相關事務,比如保存、讀取、刪除資料等。
這段程式碼的目的就是創建這個盒子,也就是 NSPersistentContainer 實例。當你第一次使用這個盒子時(也就是第一次訪問 persistentContainer 屬性),程式碼會自動幫你初始化這個盒子,並且準備好使用。這樣,你就可以方便地進行 Core Data 相關的操作,比如存儲和獲取資料。

儲存上下文(Context)

在 Core Data 中,上下文(Context)是管理對象生命週期、讀取和寫入數據的地方。它是你在應用程式中與 Core Data 進行交互的主要接口之一。

// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}

Context 充當了數據的暫存區域,它存儲了從持久存儲區(通常是 SQLite 數據庫)中檢索到的數據的副本。當你從持久存儲區中讀取數據時,Core Data 會將數據加載到 Context 中,使你可以在應用程式中對其進行操作。同樣地,當你對 Context 中的對象進行修改或創建新對象時,這些修改和新對象也會暫時存在於 Context 中。

Context還負責管理對象的生命週期。當你創建一個新對象並將其添加到 Context 中時,Context 會負責追蹤這個對象,直到你將其保存到持久存儲區中。同樣地,當你從持久存儲區中刪除一個對象時,Context 也會負責處理這個操作,並確保對象從 Context 中移除。

Entity 的自動生成程式碼

Codegan 設定

Xcode 預設會自動幫我們產生 NSManagedObject 的類別,使得我們的代碼更加簡潔和方便。上面的程式實驗,其實還沒有用到,這裡我們來研究自動產生的程式碼怎麼使用。

例如,SimpleEntity 的自動生成代碼如下:

//  phw23_2_coredata+CoreDataModel.swift
import Foundation
import CoreData
//  SimpleEntity+CoreDataClass.swift
import Foundation
import CoreData

@objc(SimpleEntity)
public class SimpleEntity: NSManagedObject {
}
//  SimpleEntity+CoreDataProperties.swift
import Foundation
import CoreData

extension SimpleEntity {

@nonobjc public class func fetchRequest() -> NSFetchRequest<SimpleEntity> {
return NSFetchRequest<SimpleEntity>(entityName: "SimpleEntity")
}

@NSManaged public var simple_value: String?
}

extension SimpleEntity : Identifiable {
}

這樣,我們的 insertSimpleValuefetchSimpleValues 函數可以進一步簡化:

Attribute

他幫我們寫好了欄位的變數,這真的很方便。

所以原本的新增程式

    func insertSimpleValue(_ value: String) {
let context = CoreDataStack.shared.context
let entity = NSEntityDescription.entity(forEntityName: "SimpleEntity", in: context)!
let simpleObject = NSManagedObject(entity: entity, insertInto: context)

simpleObject.setValue(value, forKey: "simple_value")

do {
try context.save()
} catch {
print("Failed to save value: \(error)")
}
}

可以改寫為

    func insertSimpleValue(_ value: String) {
let context = CoreDataStack.shared.persistentContainer.viewContext
let simpleEntity = SimpleEntity(context: context)
simpleEntity.simple_value = value

CoreDataStack.shared.saveContext()
}

fetchRequest

他幫我們寫好了 SimpleEntity.fetchRequest()

所以原本的讀取程式

    func fetchSimpleValues() -> [String]? {
let context = CoreDataStack.shared.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "SimpleEntity")

do {
let results = try context.fetch(fetchRequest)
return results.map { $0.value(forKey: "simple_value") as? String }.compactMap { $0 }
} catch {
print("Failed to fetch values: \(error)")
return nil
}
}

可以改寫為

    func fetchSimpleValues() -> [String]? {
let context = CoreDataStack.shared.persistentContainer.viewContext
let fetchRequest = SimpleEntity.fetchRequest()

do {
let results = try context.fetch(fetchRequest)
return results.map { $0.simple_value ?? "" }
} catch {
print("Failed to fetch values: \(error)")
return nil
}
}

參考:

Swift Arcade Youtube : Getting Started With CoreDataCore Data Codegen ExplainedGetting Started With NSFetchedResultsController

--

--