什麼是 Core Data ? 在 iOS(OSX) 應用程式中,要儲存資料可以使用資料庫或檔案,以及現在要介紹的 Core Data,所以 Core Data 的用途就是儲存資料。Core Data 是在 OSX 10.4 及 iOS 3.0 之後開始使用,它可以將物件序列化後儲存在 XML、binary(二位元檔)或 SQLite 資料庫。
Core Data 是一個儲存資料的框架,它的底層本質上還是使用 SQLite 資料庫,它提供簡單易用的方式讓你儲存資料,而不用撰寫複雜的 SQL 語法。如果你的專案有使用 Core Data,可以在該 App 的 Document 目錄中找到 sqlite 檔案。關於效能的問題,到底直接使用 SQLite 好,還是使用 Core Data 好,可以參考這篇文章 iOS Data Storage: Core Data vs. SQLite。簡單來說,Core Data 是以空間換取時間,意思是比較佔記憶體空間,但速度比較快;另外一個好處是,使用 Core Data 的程式碼比較簡單易讀,不會有複雜的 SQL 語法,官方的說法是,會減少 50~70% 的程式碼。
Managed Object Model(託管物件模型)
大部份的 Core Data 的功能,依賴你所建立的綱要(schema)去描述應用程式的實體(Entity),包含屬性及關聯等等。Core Data 使用一個被稱為 Managed object model 的綱要(schema),它是一個 NSManagedObjectModel 物件的實體。簡單的說,一個 Managed object model 會對應到資料儲存(persistent store)的一組紀錄,這裡的 persistent store 相當於資料庫;而 Managed object model 即一組紀錄,相當於資料表(table)。
由於 Core Data 並不把自己當成關聯式資料庫(雖然它的底層是使用 SQLite,但這只是它的儲存格式之一),它把這些傳統關聯式資料庫的概念抽象化,所以這邊在說明時會在抽象的定義中,以關聯式資料庫的概念來類比。
因此:
- NSManagedObjectModel 相當於所有 table 的集合
- NSPersistentStoreCoordinator 相當於 database(SQLite)
在 Core Data 中:
會有一個 Model(=資料庫),
包含至少一個 Entity(=資料表),
這個 Entity 會有屬性及關聯(=欄位)。
範例 - 使用 Core Data 建立基本的 CRUD 功能
註:本範例使用 XCode 7.0 及 Swift 2.0開啟 XCode ,建立一個 Single View Application 專案: 專案名稱為 CoreDataDemo,記得勾選 Core Data 選項: 當你勾選 Core Data 時,在 AppDelegate.swift 中會被加入 Core Data 相關的程式碼。你會在其中找到像是 "SingleViewCoreData.sqlite" 這段字串,這就是你的 SQLite 資料庫的名稱。
只要在建立專案時有勾選 Core Data,XCode 就會自動產生一個 *.xcdatamodeld 檔案,選擇它會開啟 model (即database)的管理界面: 目前這個資料庫是空的,讓我們來加第一個實體(Entity,即table)。點選下方的 Add Entity,在左邊欄的會出現新的 Entity 項目,你可以點選它來改名,或在右邊欄中改名。我們將名稱改為 "Product": 接著新增這個 Product entity 的屬性(Attirbutes),點選 Attributes 欄位下方的 "+" (或下方的 Add Attribute 也可以)即可新增。加入 name 及 price 兩個屬性,並且選擇它的 Type: 再來要讓程式碼中可以使用這個 Entity,我們要建立一個 Product 類別,並且繼承 NSManagedObject。XCode 可以幫我們自動產生,在選單列中選擇 Editor > Create NSManagedObject Subclass...: 選擇要產生的 Data Model 及 Entity 之後,將要產生的檔案選擇建立在專案中。
XCode 會產生兩個檔案:
Product.swift:
import Foundation
import CoreData
class Product: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
}
Product+CoreDataProperties.swift:
import Foundation
import CoreData
extension Product {
@NSManaged var name: String?
@NSManaged var price: NSNumber?
}
Product+CoreDataProperties.swift 用來存放 Entity 的屬性,Product.swift 就專心在該 Entity 要提供的方法。你會注意到這個 Product 類別是繼承 NSManagedObject。註:如果不知道為什麼這裡會有兩個檔案,可以研究一下 Swift 的 extension 功能,它和 Objective-C 的 Category 是同樣用途。
NSManagedObject 類別的屬性是 @NSManaged,表示它是受管理的,因此我們不必再去寫 getter/setter 或檢查資料型態是否符合等等的工作。
新增
前置工作都完成後,就可以開始寫程式了,首先,我們要新增一筆資料,打開 ViewController.swift,加入新增資料的程式碼:let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
override func viewDidLoad() {
super.viewDidLoad()
self.addProduct("iPhone 6s 16GB", price: 24500)
}
func addProduct(name:String, price:Int) {
let product = NSEntityDescription.insertNewObjectForEntityForName("Product", inManagedObjectContext: self.moc) as! Product
product.name = name
product.price = price
do {
try self.moc.save()
}catch{
fatalError("Failure to save context: \(error)")
}
}
註:記得先 import CoreData,否則可能會出現找不到類別的錯誤。XCode 在 AppDelegate 中幫我們建立了 managedObjectContext 這個參考到 NSManagedObjectContext 類別的變數,任何對資料表的操作都必須透過它,所以第一步就是先取得它。
addProduct() 方法用來新增產品,要新增資料到資料表,必須透過 NSEntityDescription 類別的 insertNewObjectForEntityForName() 方法,第一個參數是 Entity 的名稱,第二個參數則是 NSManagedObjectContext,這個方法會回傳我們要求的 Entity,接著只要對該 Entity 的屬性給值即可。
動作到此為止,資料表中已經有一筆資料了,可是目前的動作都僅存在於記憶體中,如果這時把程式關閉,這些資料就會消失。因此,最後記得要呼叫 NSManagedObjectContext 的 save() 方法真正的把資料儲存下來。
查詢
來看看剛才新增的產品是否有新增成功。加入顯示全部產品的方法:func showProducts() {
let request = NSFetchRequest(entityName: "Product")
do {
let results = try moc.executeFetchRequest(request) as! [Product]
for result in results {
print("Product Name: \(result.name!), Price: \(result.price!)")
}
}catch{
fatalError("Failed to fetch data: \(error)")
}
}
然後在原本新增產品的後面加入此方法:
self.addProduct("iPhone 6s 16GB", price: 24500)
self.showProducts()
我執行了 3 次後,結果如下:
刪除
因為每次重新執行程式時,就會新增一次重覆的資料,我們並不想這樣,所以在每次重新執行時,就把資料表清空,把裡面的資料全部刪除。func cleanUpProducts() {
let request = NSFetchRequest(entityName: "Product")
do {
let results = try moc.executeFetchRequest(request) as! [Product]
for result in results {
moc.deleteObject(result)
}
do {
try moc.save()
}catch{
fatalError("Failure to save context: \(error)")
}
}catch{
fatalError("Failed to fetch data: \(error)")
}
}
刪除的動作是,先如同查詢的動作一樣,把資料表中的資料全部取出,然後使用 NSManagedObjectContext 物件的 deleteObject() 方法來一一刪除。現在 viewDidLoad() 裡是這樣:
self.cleanUpProducts()
self.addProduct("iPhone 6s 16GB", price: 24500)
self.showProducts()
更新
更新的動作,就是先找到該筆資料,修改它的屬性值,然後儲存就完成了。這裡假設要新增三筆資料,但有一筆的價格打錯了,於是把它更新。現在的 viewDidLoad():self.cleanUpProducts()
self.addProduct("iPhone 6s 16GB", price: 24500)
self.addProduct("iPhone 6s 64GB", price: 28500)
self.addProduct("iPhone 6s 128GB", price: 22500)
self.updateProductPrice()
self.showProducts()
我們要找到 22500 這筆資料,把它改成 32500,新增一個方法:
func updateProductPrice(){
let request = NSFetchRequest(entityName: "Product")
request.predicate = NSPredicate(format: "name == %@", "iPhone 6s 128GB")
do{
let results = try moc.executeFetchRequest(request) as! [Product]
if (results.count > 0){
let product = results[0]
product.price = 32500
try self.moc.save()
}
} catch {
fatalError("Failed to update data: \(error)")
}
}
在原本的查詢中,指定 request 的 predicate 屬性來過濾資料,找到後,只要將新的值指定給它,然後儲存,這樣就完成更新了。
NSPredicate 的設定方法和 SQL 語法很像,詳細內容可以參考這裡。以上是最基本的 Core Data 的 CRUD 操作。
您好,如果是在同一個專案裡的View Controller,是不是可以直接導入Core Data,使用NSFetchResultsControllerDelegate,取得最新的資料庫資料?
回覆刪除這樣的話要如何使用已儲存的資料? 直接 var 一個變數 =product.price ?
同一個專案裡的檔案都可以用,
回覆刪除NSFetchResultsControllerDelegate 我查API上的說明,
它是用在當資料有「新增、刪除、移動或更新」時會觸發取得結果,
這我不熟,要再查資料。
要取資料可以參考這篇 http://blog.tonycube.com/2015/10/swift-core-data-2.html
我寫了兩個例子,一個是取全部的資料 getAll(),一個是以名稱查詢 findByName(),
用類似的寫法就能取得資料,取回來的是 Product 物件,假設你存在變數 p,
那就是 p.price
多謝您的解說,對取得資料有更進一步的認識!!!
刪除可是我發現我提問的問題似乎沒有講到重點,我目前是用CoreData儲圖片檔(NSData),而我要實做的東西是,將我新增到CoreData後的圖片能夠放到pickerView的陣列裡頭,
EX: var imageArray = [NSData],imageArray = [UIImage(data: restaurant.image)],然而卻被Xcode說,沒有image這member,但我明明已宣告在Restaurant.swift這檔案裡頭,不知道我是否忽略掉哪個重要細節?
如果你的 xxx.xcdatamodeld 檔案在執行 App 過後又有新增 Attributes 的話,再次執行時,Xcode 會出錯,解決方法是,必須先把前一版的 App 除除,然後再執行就會正常。
刪除我猜你的問題可能出在這裡。
---
另外幾個重點:
1.Attribute 的 Type 必須是 Binary Data
2.在 Product+CoreDataProperties 檔案加入該 Attribute,一樣注意 Type 是 NSData?
3.儲存圖片大概像這樣
product.thumbnail = UIImagePNGRepresentation(UIImage(imageLiteral: "ball.png"))
記得要先 import UIKit。
4.取出圖片
if let img = product.thumbnail {
imgPic.image = UIImage(data: img)
}
imgPic 是 ImageView。
Tony你好,不好意思又來煩你了QQ,
刪除我這個畫面是只有使用UIPickerView而已,圖片已經儲存在Core Data裡(NSData)
單純的想要將圖片檔案置入旋轉的選單而已~!
我後來 直接宣告
var restaurants:Restaurant!
var imageArray = [NSData]()
func viewDidLoad(){
imageArray[restaurants.image!]
}
func pickerView(pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusingView view: UIView?) -> UIView {
let pickerImage = UIImageView()
if component == 0 {
_ = imageArray[(Int)(dataArray1[row])]
}
return pickerImage
}
這樣子是沒有bug的,只是當我轉到這個畫面時,就會直接閃退
看看 log 有顯示什麼錯誤訊息,
刪除不過,
imageArray[restaurants.image!]
和
_ = imageArray[(Int)(dataArray1[row])]
這兩行怪怪的
我跟你卡在一樣的地方...
刪除但是我想要把UITableView的資料,顯示在UIPickerView上可以選取...
但我沒辦法讀取...
TableView已經儲存資料了也可以顯示...我的語法如下..
People 是 core data 的類別...
imageArray = [Person().name!]
因為我寫好的PickerView 使用的是 imageArray這個陣列
所以我想說用帶入的方法 但是出現
CoreData: error: Failed to call designated initializer on NSManagedObject class 'Porject222.Person'
求解...大師...
我也試過用轉型的方法
let tkt = Person().name
let lol = tkt! as String
print("\([lol])")
lol最後變成String沒錯,但是連Print 都印不出值來...如何從
class Person : NSManagedObject 裡面取值放進陣列...?
錯誤訊息告訴你「Failed to call designated initializer on NSManagedObject class 'Porject222.Person'」
刪除呼叫 NSManagedObject 的特定初始化方法失敗,你要呼叫
init(entity:insertIntoManagedObjectContext:)
這個方法來建立 Person 物件。
參考資料:
http://stackoverflow.com/questions/33301250/resolving-failed-to-call-designated-initializer-on-nsmanagedobject-class
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/CoreDataFramework/Classes/NSManagedObject_Class/index.html#//apple_ref/doc/uid/TP30001171-SW7
感謝參考資料及分享...問題已解決
刪除連樓主的問題也可以解決。。關鍵在於初始化 -> 呼叫 -> 轉型(取值)以及“部分語法“需要修改,,感謝Tony 大
謝謝你的文章
回覆刪除我覺得非常實用!!!!