使用Core Data存儲(chǔ)位置信息
此刻你已經(jīng)擁有了一個(gè)app,可以獲得用戶當(dāng)前的GPS坐標(biāo)。它還有一個(gè)界面可以用來(lái)標(biāo)記位置信息,可以添加一段描述并且選擇一個(gè)分類。稍后,你將添加照片功能進(jìn)去。
接下來(lái)我們要把用戶的位置信息保存到一個(gè)列表里去。
這個(gè)界面完成后會(huì)是下面這個(gè)樣子:
你需要用某種方法把獲取到的位置信息保存下來(lái)。
上一次你保存信息的時(shí)候,你創(chuàng)建了符合NSCoding協(xié)議的數(shù)據(jù)模型對(duì)象,并且使用NSKeyedArchiver將它們保存到.plist文件中。這樣確實(shí)可以達(dá)到很好的效果,但是這節(jié)課,我會(huì)給你介紹一個(gè)新的框架,它可以幫你節(jié)省很多時(shí)間,那就是Core Data。
Core Data是用于iOS應(yīng)用的一個(gè)持久化存儲(chǔ)對(duì)象。如果你之前自己看過(guò)關(guān)于Core Data的文檔的話,你也許發(fā)現(xiàn)它很難,但是實(shí)際上它的原理非常簡(jiǎn)單。
你之前學(xué)到過(guò)假如一個(gè)對(duì)象沒(méi)有其他對(duì)象引用它的時(shí)候,這個(gè)對(duì)象會(huì)被銷毀掉。此外,所有對(duì)象都會(huì)在app被關(guān)閉后銷毀掉。
通過(guò)使用Core Data,你可以指定一些對(duì)象持久化存儲(chǔ),它們會(huì)被保存到數(shù)據(jù)商店中。這樣即使所有引用它們的對(duì)象以及實(shí)例都被銷毀了,它的數(shù)據(jù)還是安全的存儲(chǔ)在Core Data中,并且你可以隨時(shí)把它取回來(lái)。
如果你之前學(xué)習(xí)過(guò)數(shù)據(jù)庫(kù)的話,你會(huì)發(fā)現(xiàn)它們很像,只是Core Data保存的對(duì)象,而數(shù)據(jù)庫(kù)保存的是關(guān)系表。
添加Core Data到app中。
Core Data需要使用數(shù)據(jù)模型。這是一個(gè)特殊的文件用于表述你要持久存儲(chǔ)的對(duì)象。與常規(guī)對(duì)象不同,這些托管對(duì)象會(huì)將數(shù)據(jù)保存起來(lái),除非你本人要求刪除它們。
添加一個(gè)新的文件到工程中,在文件模版處選擇Core Data分節(jié)下的Data Model文件類型:
將文件命名為DataModel。
此時(shí)你可以在工程導(dǎo)航器中看到一個(gè)DataModel.xcdatamodeld文件。
選定這個(gè)文件,就可以打開(kāi)數(shù)據(jù)模型編輯器了:
你用Core Data管理的每一個(gè)對(duì)象,都要先為它創(chuàng)建一個(gè)實(shí)體(entity)。
一個(gè)實(shí)體描述你的對(duì)象將擁有哪些數(shù)據(jù)字段。 從某種意義上來(lái)說(shuō),它的作用與一個(gè)類相同,但是是專門(mén)用于Core Data的數(shù)據(jù)存儲(chǔ)。(如果你以前學(xué)習(xí)過(guò)數(shù)據(jù)庫(kù)的話,你可以把一個(gè)實(shí)體當(dāng)作一個(gè)表)
我們的這個(gè)app僅有一個(gè)實(shí)體,就是位置信息,每個(gè)位置信息包含以下數(shù)據(jù):
1、經(jīng)緯度
2、街道信息
3、時(shí)間
4、用戶添加的描述
5、分類
這些都是Tag Location界面上的內(nèi)容,照片除外。 照片可能會(huì)非常大,它可能需要幾兆字節(jié)的存儲(chǔ)空間。 雖然Core Data存儲(chǔ)可以處理大量的“blobs(不知道啥意思)”數(shù)據(jù),但是最好將照片作為單獨(dú)的文件存儲(chǔ)在應(yīng)用程序的Documents目錄中,后續(xù)我們會(huì)單獨(dú)講解關(guān)于照片存儲(chǔ)的內(nèi)容。
點(diǎn)擊數(shù)據(jù)模型編輯器底部的Add Entity按鈕。這樣就會(huì)在ENTITIES標(biāo)簽下新增一個(gè)實(shí)體。將其命名為L(zhǎng)ocation。在右邊的面板中單擊一下就可以重命名了,見(jiàn)下圖藍(lán)色被選定的那個(gè)記錄:
在實(shí)體的內(nèi)部有三個(gè)分節(jié),Attributes,Relationships以及Fetched Properties。Attributes就是實(shí)體的數(shù)據(jù)字段。
這個(gè)app僅有一個(gè)實(shí)體,但是通常app會(huì)有多個(gè)實(shí)體并且相互關(guān)聯(lián)。通過(guò)Relationships與Fetched Properties你可以告訴Core Data你的對(duì)象間的依賴關(guān)系。
對(duì)于我們這個(gè)app而言,只需要使用Attributes。
點(diǎn)擊編輯器底部的Add Attribute按鈕,或者Attributes分節(jié)下的小加號(hào)按鈕添加新的屬性。給它命名為latitude,并且將類型設(shè)置為Double:
Attributes大體上盒實(shí)例變量很像,因此它也有類型的概念。之前我們介紹過(guò)latitude和longitude都是Double型的,所以它們對(duì)應(yīng)的Attributes也應(yīng)該是Double型的。
??:不要讓這些術(shù)語(yǔ)把你嚇著,你可以這樣想:
entity = object or class
attribute = variable
如果你想知道方法在Core Data中對(duì)應(yīng)什么,我可以告訴你不存在這種概念。Core Data僅用于存儲(chǔ)對(duì)象的數(shù)據(jù)部分。實(shí)體的概念是這樣的:對(duì)象的數(shù)據(jù),對(duì)象間的關(guān)系(如果存在的話)。
稍后,你將創(chuàng)建一個(gè)Swift文件來(lái)定義自己的Location類。 它將與數(shù)據(jù)模型中的Location實(shí)體關(guān)聯(lián)。 但它仍然是一個(gè)普通的類,所以你可以添加你自己的方法。
添加剩余的attributes到Location實(shí)體中:
longitude,類型Double
date,類型Date
locationDescription,類型String
category,類型String
placemark,類型Transformable
現(xiàn)在數(shù)據(jù)模型看起來(lái)應(yīng)該是這個(gè)樣子:
這里提一下locationDescription,你不能將這個(gè)字段命名為description,因?yàn)閐escription是NSObject中的一個(gè)對(duì)象。如果你這樣做了的話,Xcode會(huì)毫不客氣的給出一個(gè)報(bào)錯(cuò)。
placemark的類型是Transformable。Core Data僅支持比較有限的類型,比如String、Double、Date。但是placemark的類型是CLPlacemark,Core Data并不能直接支持這種類型。
幸運(yùn)的是,Core Data有一種處理任意數(shù)據(jù)的規(guī)則。任何遵循NSCoding協(xié)議的類可以被存儲(chǔ)為T(mén)ransformable類型。更加幸運(yùn)的是CLPlacemark正好遵循NSCoding協(xié)議,所以你可以直接存儲(chǔ)它,不用做什么額外的工作。
默認(rèn)情況,實(shí)體的屬性都是可選型,意味著它們可以為nil。在我們的app中只有placemark可能為nil,如果地址解析失敗的話。所以我們可以在Xcode中指明這一點(diǎn)(其實(shí)不做也無(wú)所謂)。
選擇category屬性。在屬性面板中,取消選中Optional選項(xiàng),見(jiàn)下圖:
然后用同樣方法,把除了placemark以外的所有屬性的Optional選項(xiàng)都取消選中。
然后使用command+S保存一下,雖然Xcode支持自動(dòng)保存,但是我們最好還是養(yǎng)成自己隨時(shí)保存的習(xí)慣。
數(shù)據(jù)模型基本處理完了,但是還有一件事我要特別講一下。
點(diǎn)擊選定Location實(shí)體,然后打開(kāi)屬性面板。
你可以看到Class這個(gè)字段中填寫(xiě)的是“NSManagedObject”。當(dāng)你從Core Data中取回這個(gè)實(shí)體時(shí),你會(huì)得到一個(gè)NSManagedObject類的對(duì)象。
這是Core Data管理的所有的對(duì)象的基礎(chǔ)類。常規(guī)對(duì)象從NSObject繼承,但來(lái)自Core Data的對(duì)象是由NSManagedObject擴(kuò)展來(lái)的。
因?yàn)橹苯邮褂肗SManagedObject是有點(diǎn)限制的,所以你可以使用自己的類來(lái)代替。 雖然并不是必須要這樣做,但是這樣確實(shí)會(huì)使Core Data的使用簡(jiǎn)單一些。
現(xiàn)在我們要做的是,從數(shù)據(jù)存儲(chǔ)中取回Location實(shí)體時(shí),直接返回一個(gè)Location的實(shí)例,而不是NSManagedObject。
首先在屬性面板中將Codegen這一欄中的內(nèi)容選擇為Manual/None。
??:從版本8開(kāi)始,Xcode可以從數(shù)據(jù)模型自動(dòng)生成實(shí)體類的源代碼。 Codegen設(shè)置決定了它如何做到這一點(diǎn)。 為了本教程的目的,我們暫時(shí)不使用自動(dòng)代碼生成,這就是你將Codegen設(shè)置為Manual / None的原因。 了解如何制作自己的NSManagedObject子類,而不是完全依靠Xcode是很有用的。
即使你不使用自動(dòng)類生成,Xcode仍然可以提供幫助。
選擇菜單Editor → Create NSManagedObject Subclass。
然后會(huì)彈出一個(gè)窗口讓你選擇實(shí)體和數(shù)據(jù)模型。
選擇DataModel,然后點(diǎn)擊Next。然后選擇Location再點(diǎn)擊Next。
然后選擇保存位置,點(diǎn)擊Create結(jié)束。
現(xiàn)在兩個(gè)新的文件被添加到工程中了。第一個(gè)是Location+CoreDataClass.swift,它里面的內(nèi)容是這樣的:
import Foundation
import CoreData
public class Location: NSManagedObject {
}
和你看到的一樣,Location繼承了NSManagedObject,而不是新建了一個(gè)NSObject。
第二個(gè)文件是Location+CoreDataProperties.swift:
import Foundation
import CoreData
extension Location {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Location> {
return NSFetchRequest<Location>(entityName: "Location")
}
@NSManaged public var latitude: Double
@NSManaged public var longitude: Double
@NSManaged public var date: NSDate?
@NSManaged public var locationDescription: String?
@NSManaged public var category: String?
@NSManaged public var placemark: NSObject?
}
在這個(gè)文件中,Xcode創(chuàng)建了與實(shí)體中的屬性相對(duì)應(yīng)的實(shí)例。這里你沒(méi)有見(jiàn)過(guò)的東西是extension關(guān)鍵字。
通過(guò)extension(擴(kuò)展),你可以將其他功能添加到現(xiàn)有對(duì)象,而無(wú)需更改該對(duì)象的源代碼。 這甚至適用于你實(shí)際上沒(méi)有這些對(duì)象的源代碼。 稍后在本教程中,您將看到一個(gè)如何使用擴(kuò)展向iOS框架中的對(duì)象添加新方法的示例。
在這里,擴(kuò)展名被用于其他目的。 如果稍后更改了Core Data模型,并且想要自動(dòng)更新代碼以匹配這些更改,則可以再次選擇“創(chuàng)建NSManagedObject子類”,而Xcode將只覆蓋“Location + CoreDataProperties.swift”中的內(nèi)容,但不會(huì)添加任何內(nèi)容到Location + CoreDataClass.swift。
因此,如果你打算在之后覆蓋此文件,則更改Location + CoreDataProperties.swift并不是一個(gè)好主意。 不幸的是,Xcode在屬性的類型上有所不滿,所以你不得不對(duì)這個(gè)文件做一些修改。
首先要解決的是placemark變量。 因?yàn)槟銊?chuàng)建了一個(gè)Transformable類型的placemark,所以Xcode并不知道這是什么類型的對(duì)象,所以它選擇了泛型類型NSObject。但是你知道這將是一個(gè)CLPlacemark對(duì)象,所以你可以做些改變來(lái)讓事情變得更容易。
首先在Location+CoreDataProperties.swift中導(dǎo)入CoreLocation框架:
import CoreLocation
然后將placemark屬性修改為:
@NSManaged var placemark: CLPlacemark?
問(wèn)號(hào)不能去掉,因?yàn)閜lacemark是個(gè)可選型。
同時(shí)將date屬性的類型修改為Date:
@NSManaged var date: Date
NSDate是OC中的用法,在Swift中,我們使用Date。并且去掉問(wèn)號(hào),它不再是一個(gè)可選型。
最后,刪除category和locationDescription屬性后面的問(wèn)號(hào)。 之前你告訴Core Data這些屬性不是可選項(xiàng),所以他們不需要這個(gè)問(wèn)號(hào)。
由于這是一個(gè)托管對(duì)象,并且數(shù)據(jù)存在于數(shù)據(jù)存儲(chǔ)區(qū)內(nèi),因此Swift將以特殊方式處理Location的變量。 @NSManaged關(guān)鍵字告訴編譯器這些屬性將在運(yùn)行時(shí)由Core Data解析。 當(dāng)你為這些屬性添加一個(gè)新的值的時(shí)候,Core Data會(huì)把這個(gè)值放到數(shù)據(jù)存儲(chǔ)中以保存,而不是放在一個(gè)普通的實(shí)例變量中。
這樣就結(jié)束了這個(gè)app的數(shù)據(jù)模型的定義。 現(xiàn)在你必須把它連接到一個(gè)數(shù)據(jù)存儲(chǔ)。
數(shù)據(jù)存儲(chǔ)
在iOS系統(tǒng)中,Core Data將其所有數(shù)據(jù)存儲(chǔ)到SQLite數(shù)據(jù)庫(kù)(發(fā)音為“SQL light”)。 如果你不知道SQLite是什么,那也行。 稍后你將看到該數(shù)據(jù)庫(kù),但是你并不需要知道數(shù)據(jù)存儲(chǔ)中為了使用Core Data而做了些了什么。
但是,當(dāng)應(yīng)用程序啟動(dòng)時(shí),你必須初始化這些數(shù)據(jù)存儲(chǔ)。 對(duì)于使用Core Data的app來(lái)說(shuō),這個(gè)代碼是相同的,并且它在app的委托類中。
app委托(app delegate)是獲取與app有關(guān)的通知的對(duì)象。 例如,這是iOS通知應(yīng)用程序啟動(dòng)的地方。
你會(huì)在AppDelegate中做些修改。
打開(kāi)AppDelegate.swift并且導(dǎo)入Core Data框架(在頂部第一行):
import CoreData
在AppDelegate類的內(nèi)部添加以下代碼:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores(completionHandler: {
storeDescription, error in
if let error = error {
fatalError("Could load data store: \(error)")
}
})
return container
}()
這是就是你需要加載之前定義的數(shù)據(jù)模型的代碼,并將其連接到SQLite數(shù)據(jù)存儲(chǔ)。
這里的目標(biāo)是創(chuàng)建一個(gè)所謂的NSManagedObjectContext對(duì)象。 這是你將用于與CoreData交談的對(duì)象。 為了獲得NSManagedObjectContext對(duì)象,應(yīng)用程序需要做幾件事情:
1、從你之前創(chuàng)建的CoreDatamodel中創(chuàng)建一個(gè)NSManagedObjectModel。 該對(duì)象表示運(yùn)行時(shí)的數(shù)據(jù)模型。 你可以從其中得到類型的實(shí)體,這些實(shí)體有什么屬性,等等。 在大多數(shù)app中,你不需要直接使用NSManagedObjectModel對(duì)象。
2、創(chuàng)建一個(gè)NSPersistentStoreCoordinator對(duì)象。該對(duì)象負(fù)責(zé)SQLite數(shù)據(jù)庫(kù)。
3、最后,創(chuàng)建NSManagedObjectContext對(duì)象并將其連接到持久存儲(chǔ)協(xié)調(diào)器。
這些對(duì)象一起被稱為“Core Data stack核心數(shù)據(jù)棧”。
在iOS 9中,你必須手動(dòng)執(zhí)行這些步驟,這可能會(huì)有點(diǎn)混亂。 幸運(yùn)的是,iOS 10中有一個(gè)新的對(duì)象NSPersistentContainer,它負(fù)責(zé)處理所有事情。
這并不意味著你應(yīng)該立即忘記你剛才了解到的NSManagedObjectModel和NSPersistentStoreCoordinator的內(nèi)容,但是它可以幫你避免編寫(xiě)一堆代碼。
剛剛添加的代碼將創(chuàng)建一個(gè)類型為NSPersistentContainer的實(shí)例變量persistentContainer。 為了得到我們之后的NSManagedObjectContext,你可以簡(jiǎn)單的訪問(wèn)一下persistentContainer的viewContext屬性。
為了方便起見(jiàn),添加一個(gè)新屬性來(lái)從持久容器中獲取NSManagedObjectContext:
lazy var managedObjectContext: NSManagedObjectContext = self.persistentContainer.viewContext
現(xiàn)在,我們已經(jīng)做好使用CoreData的準(zhǔn)備工作了。
編譯一下app確保沒(méi)有錯(cuò)誤。 如果你運(yùn)行app,你不會(huì)發(fā)現(xiàn)任何區(qū)別,因?yàn)槟銓?shí)際上還沒(méi)有使用Core Data。