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