存儲(chǔ)位置信息
你已經(jīng)成功初始化Core Data泵额,并將NSManagedObjectContext傳遞給Tag Location界面移怯。 現(xiàn)在鄙早,當(dāng)用戶按下“Done”按鈕時(shí)撬腾,將新的Location對(duì)象放入數(shù)據(jù)存儲(chǔ)區(qū)。
打開(kāi)LocationDetailsViewController.swift装黑,添加一個(gè)新的實(shí)例變量:
var date = Date()
你添加這個(gè)變量是因?yàn)槟阈枰獙?dāng)前日期存儲(chǔ)到新的Location對(duì)象中副瀑,因此你需要一個(gè)Date對(duì)象。
在viewDidLoad()中曹体,將設(shè)置dateLabel的文本那一行語(yǔ)句修改為:
dateLabel.text = format(date: date)
這時(shí)新的這個(gè)變量就被用起來(lái)了俗扇。
將done方法修改為下面這個(gè)樣子:
@IBAction func done() {
let hudView = HudView.hud(inView: navigationController!.view,
animated: true)
hudView.text = "Tagged"
// 1
let location = Location(context: managedObjectContext)
// 2
location.locationDescription = descriptionTextView.text
location.category = categoryName
location.latitude = coordinate.latitude
location.longitude = coordinate.longitude
location.date = date
location.placemark = placemark
// 3
do {
try managedObjectContext.save()
afterDelay(0.6) {
self.dismiss(animated: true, completion: nil)
}
} catch {
fatalError("Error: \(error)")
}
}
它的工作原理是這樣的硝烂。
1箕别、首先,你創(chuàng)建了一個(gè)Location的實(shí)例滞谢。因?yàn)檫@是一個(gè)被管理的對(duì)象串稀,你必須使用它的init(context :)方法。 你不能只寫(xiě)Location()狮杨,這樣的話managedObjectContext將不知道新的對(duì)象是啥母截。
2、一旦創(chuàng)建了Location對(duì)象的實(shí)例橄教,你就可以像使用其他對(duì)象一樣使用它清寇。 在這里,你將其屬性設(shè)置為用戶在屏幕上輸入的內(nèi)容护蝶。
3华烟、現(xiàn)在,你已經(jīng)有了一個(gè)新的Location對(duì)象持灰,其屬性全部被填充盔夜,但如果你在此時(shí)查看數(shù)據(jù)存儲(chǔ)區(qū),那么你仍然不會(huì)在其中看到任何對(duì)象堤魁。 直到你使用save()保存后上下文才會(huì)發(fā)生喂链。
保存功能會(huì)將添加到上下文的任何對(duì)象或其內(nèi)容已更改的任何托管對(duì)象永久寫(xiě)入數(shù)據(jù)存儲(chǔ)區(qū)。 這就是為什么他們把上下文稱為“便箋簿(scratchpad)”妥泉。 它的變化會(huì)一直持續(xù)下去椭微,直到你保存它們。
save()方法可能由于各種原因而失敗盲链,因此你需要捕獲潛在的錯(cuò)誤蝇率。 這是使用Swift的do-try-catch功能完成的检诗。
do-try-catch
有時(shí)來(lái)自iOS SDK的操作可能會(huì)失敗并返回某種類型的錯(cuò)誤。 你已經(jīng)見(jiàn)過(guò)用于描述這種錯(cuò)誤的對(duì)象(NSError)瓢剿。 但是使用Error可能很麻煩逢慌,因?yàn)樗鼘?shí)際上是從Objective-C中借用的東西。
Swift有一個(gè)更好的方法來(lái)處理錯(cuò)誤间狂。 任何可能失敗的方法必須在其前面有try關(guān)鍵字攻泼。 用try關(guān)鍵字的方法調(diào)用必須在do-catch塊內(nèi)。
保存托管對(duì)象上下文是一個(gè)可能會(huì)失敗的操作鉴象。 這就是為什么你這樣寫(xiě)代碼:
do {
try managedObjectContext.save()
// code that runs when the “try” succeeds . . .
} catch {
// code that runs when the “try” fails . . .
}
如果出現(xiàn)錯(cuò)誤忙菠,并且方法失敗 - 或者像我們所說(shuō)的那樣“拋出錯(cuò)誤”,應(yīng)用程序?qū)⑻^(guò)do部分的中所有代碼纺弊,并立即跳轉(zhuǎn)到catch部分來(lái)處理錯(cuò)誤牛欢。
如果你用使用過(guò)支持異常處理的語(yǔ)言,這可能看起來(lái)很熟悉淆游。 任何可能引發(fā)錯(cuò)誤的方法調(diào)用都必須寫(xiě)為try methodName()傍睹。 這會(huì)很容易找出哪些方法調(diào)用引發(fā)了錯(cuò)誤。
在本教程的后面犹菱,你將看到更多的do-try-catch示例拾稳。 這是Swift中非常重要的課題,因?yàn)闆](méi)有人喜歡錯(cuò)誤腊脱。 他們必須被抓追玫谩!
運(yùn)行app陕凹,獲得一個(gè)位置信息悍抑,并且添加一段描述后點(diǎn)擊Done按鈕。
如果一切正常杜耙,Core Data會(huì)在調(diào)試區(qū)域打印下列信息:
CoreData: sql: BEGIN EXCLUSIVE ...
CoreData: sql: INSERT INTO ZLOCATION(Z_PK, Z_ENT, Z_OPT, ZCATEGORY,
ZDATE, ZLATITUDE, ZLOCATIONDESCRIPTION, ZLONGITUDE, ZPLACEMARK) VALUES(?,
?, ?, ?, ?, ?, ?, ?, ?)
CoreData: sql: COMMIT
...
CoreData: annotation: sql execution time: 0.0001s
這些是Core Data執(zhí)行存儲(chǔ)新的Location對(duì)象到數(shù)據(jù)庫(kù)中所執(zhí)行的SQL語(yǔ)句搜骡。
??:Xcode8中不會(huì)打印這些信息。
打開(kāi)Liya軟件泥技,刷新ZLOCATION表(點(diǎn)擊Go按鈕)浆兰,這時(shí)你可以看到表中出現(xiàn)了新的一行。
如果你在表中沒(méi)有看到任何行珊豹,請(qǐng)先按Xcode中的“Stop”按鈕退出應(yīng)用程序簸呈。 也可以嘗試關(guān)閉Liya窗口并打開(kāi)到數(shù)據(jù)庫(kù)的新連接。
如你所見(jiàn)店茶,此表中的列包含Location對(duì)象的屬性值蜕便。 唯一不可讀的列是ZPLACEMARK。 其內(nèi)容已被編碼為二進(jìn)制“blob”數(shù)據(jù)贩幻。 這是因?yàn)樗且粋€(gè)轉(zhuǎn)換后的屬性轿腺,NSCoding協(xié)議已經(jīng)把它的字段轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)塊两嘴。
如果你沒(méi)有Liya或者你是一個(gè)命令行的癮君子,那么還有另外一種方法來(lái)檢查數(shù)據(jù)庫(kù)的內(nèi)容族壳。 你可以使用終端應(yīng)用程序和sqlite3工具憔辫,但是不推薦這樣做:
處理Core Data錯(cuò)誤
將上下文的內(nèi)容保存到數(shù)據(jù)存儲(chǔ)區(qū),你使用了如下代碼:
do {
try managedObjectContext.save() ...
} catch {
fatalError("Error: \(error)")
}
如果保存有什么問(wèn)題怎么辦仿荆? 在這種情況下贰您,try跳轉(zhuǎn)到catch部分,并調(diào)用fatalError()函數(shù)拢操。 這會(huì)立即中斷掉app锦亦,并將用戶返回到iPhone的界面。 這對(duì)用戶來(lái)說(shuō)是非常粗暴的令境,因此不推薦這樣做杠园。
好消息是,如果你試圖保存無(wú)效的內(nèi)容舔庶,Core Data只會(huì)給出一個(gè)錯(cuò)誤抛蚁。 換句話說(shuō),此時(shí)你的app存在BUG栖茉。
當(dāng)然篮绿,在開(kāi)發(fā)過(guò)程中你可以解決掉所有的bug,所以用戶將永遠(yuǎn)不會(huì)遇到任何bug吕漂,對(duì)嗎? 可悲的是尘应,你永遠(yuǎn)不會(huì)趕上你所有的bug惶凝。 有些bug總是會(huì)設(shè)法溜過(guò)去。
壞消息是當(dāng)Core Data崩潰時(shí)犬钢,你無(wú)法進(jìn)行過(guò)多的干預(yù)苍鲜。 有些地方出現(xiàn)了可怕的bug,比如現(xiàn)在你被無(wú)效的數(shù)據(jù)困住了玷犹。 如果app被允許繼續(xù)運(yùn)行混滔,事情可能只會(huì)變得更糟,因?yàn)槟悴恢繿pp處于什么狀態(tài)歹颓。最后的事就是用戶的數(shù)據(jù)會(huì)被破壞掉坯屿。
不要用fatalError()讓app崩潰,而是首先告訴用戶這個(gè)問(wèn)題巍扛,至少他們知道發(fā)生了什么领跛。 崩潰仍然是不可避免的,但現(xiàn)在你的用戶會(huì)知道為什么應(yīng)用程序突然停止工作撤奸。
在本節(jié)中吠昭,你將添加一個(gè)彈出警報(bào)來(lái)處理這種情況喊括。 當(dāng)然,這些bug只會(huì)在開(kāi)發(fā)過(guò)程中發(fā)生矢棚,但是如果他們確實(shí)發(fā)生在實(shí)際的用戶身上郑什,那么你至少應(yīng)該試著用一點(diǎn)點(diǎn)比較和諧的手段來(lái)處理它。
我們通過(guò)一些方法來(lái)制造點(diǎn)麻煩蒲肋,看看會(huì)發(fā)生什么蹦误。
打開(kāi)數(shù)據(jù)模型文件(DataModel.xcdatamodeld ),然后選擇placemark屬性肉津。然后取消選擇Optional選項(xiàng)强胰。
這就是說(shuō)location.placemark不再是一個(gè)可選型了。這是Core Data強(qiáng)制執(zhí)行的一個(gè)約束條件妹沙。 當(dāng)你嘗試保存為nil的location時(shí)偶洋,Core Data就會(huì)崩潰掉。 所以我們可以利用這一點(diǎn)來(lái)進(jìn)行測(cè)試距糖。
運(yùn)行app玄窝。看看效果悍引。恩脂。。
你剛剛通過(guò)更改placemark屬性來(lái)變更 了數(shù)據(jù)模型趣斤。 在啟動(dòng)應(yīng)用程序時(shí)俩块,NSPersistentContainer會(huì)注意到這一點(diǎn),并嘗試將SQLite數(shù)據(jù)庫(kù)執(zhí)行所謂的“遷移”到更新后的數(shù)據(jù)模型上浓领。
遷移可能成功玉凯,也可能不成功,這依賴于數(shù)據(jù)存儲(chǔ)中當(dāng)前的內(nèi)容联贩。 如果之前的placemark為nil漫仆,則遷移到新數(shù)據(jù)模型將失敗。 畢竟泪幌,新的數(shù)據(jù)模型不允許為nil的placemark盲厌。
此時(shí),你應(yīng)該可以在調(diào)試區(qū)域看到如下信息:
reason=Cannot migrate store in-place: Validation error missing attribute
values on mandatory destination attribute, . . . {entity=Location,
attribute=placemark, . . .}
DataModel.sqlite文件對(duì)于已更改的數(shù)據(jù)模型已經(jīng)無(wú)法適應(yīng)祸泪,并且Core Data不能自動(dòng)解決這個(gè)問(wèn)題吗浩。
有兩種方法可以解決這個(gè)問(wèn)題:1)簡(jiǎn)單地刪掉Library目錄下的DataModel.sqlite文件; 2)從模擬器中刪除整個(gè)app。
刪除DataModel.sqlite文件浴滴,以及-shm和-wal文件拓萌,然后再次運(yùn)行app。
我們的重點(diǎn)并不是觀看app如何崩潰升略,而是一定要謹(jǐn)記數(shù)據(jù)模型變更后微王,你需要拋棄之前的數(shù)據(jù)庫(kù)文件屡限,否則Core Data無(wú)法正常初始化。
??:如果NSPersistentContainer遷移失敗炕倘,并不會(huì)全部丟失钧大。 Core Data允許你在使用新數(shù)據(jù)模型時(shí)向你的app發(fā)布更新并自己執(zhí)行遷移。 比起讓app崩潰罩旋,這種機(jī)制允許你將用戶現(xiàn)有數(shù)據(jù)存儲(chǔ)的內(nèi)容轉(zhuǎn)換為新模型啊央。 但是,在開(kāi)發(fā)時(shí)還是直接刪除掉原有的文件比較簡(jiǎn)單涨醋。
看這樣一個(gè)場(chǎng)景瓜饥,點(diǎn)擊Get My Location按鈕,然后點(diǎn)擊Tag Location浴骂。 如果你足夠快的話乓土,地址解析就會(huì)失敗,Tag Location界面將會(huì)顯示:“No Address Found”溯警。 僅當(dāng)placemark為nil時(shí)才會(huì)顯示這條信息趣苏。
可以通過(guò)注釋掉CurrentLocationViewController.swift中的locationManager(didUpdateLocations)中的self.placemark = p.last!這一行來(lái)偽造地址解析過(guò)快的現(xiàn)象 梯轻。 這會(huì)使得看起來(lái)沒(méi)有地址被搜索到食磕,并且placemark的值為 nil。
點(diǎn)擊Done按鈕來(lái)保存新的location對(duì)象喳挑。
app將會(huì)調(diào)用fatalError()并且崩潰掉彬伦。
你會(huì)在調(diào)試區(qū)域看到如下信息:
The operation couldn’t be completed . . . NSValidationErrorKey=placemark
意思是placemark屬性沒(méi)有值。因?yàn)槟惆阉O(shè)置為非可選型了蟀悦,Core Data無(wú)法接受placemark的值為nil媚朦。
當(dāng)然,剛剛看到的只是在Xcode中運(yùn)行應(yīng)用程序時(shí)才會(huì)發(fā)生日戈。 當(dāng)它崩潰時(shí),調(diào)試器接管app并指向錯(cuò)誤行孙乖。 但這不是用戶所看到的浙炼。
點(diǎn)擊Stop按鈕關(guān)閉app。現(xiàn)在點(diǎn)擊模擬器中的app圖標(biāo)唯袄,在Xcode之外啟動(dòng)app弯屈。 重復(fù)相同的操作,使app崩潰恋拷。 app將停止運(yùn)作资厉,并從屏幕上消失。你作為用戶什么都看不到蔬顾。
想象一下宴偿,剛剛為你的應(yīng)用付了99美分(或更多)的用戶就會(huì)遇到這種情況湘捎。 他們會(huì)非常困惑,“剛剛發(fā)生了什么窄刘?窥妇!”此時(shí)你并沒(méi)有什么給用戶解釋的機(jī)會(huì),只能接受退款給他們娩践。
發(fā)生這種情況時(shí)最好顯示一個(gè)提示活翩。 在用戶確認(rèn)之后,app依然會(huì)崩潰翻伺,但至少用戶知道原因材泄。 (提示消息應(yīng)該讓用戶聯(lián)系你,由你來(lái)解釋一下怎么回事吨岭,這樣你至少有機(jī)會(huì)在下一個(gè)版本中修復(fù)這個(gè)問(wèn)題拉宗。)
打開(kāi)Functions.swift,并且添加以下代碼:
let MyManagedObjectContextSaveDidFailNotification = Notification.Name(
rawValue: "MyManagedObjectContextSaveDidFailNotification")
func fatalCoreDataError(_ error: Error) {
print("*** Fatal error: \(error)")
NotificationCenter.default.post(
name: MyManagedObjectContextSaveDidFailNotification, object: nil)
}
這里定義了一個(gè)新的全局函數(shù)來(lái)處理致命的Core Data錯(cuò)誤未妹。
打開(kāi)LocationDetailsViewController.swift在done()方法中簿废,替換掉處理錯(cuò)誤的代碼。
do {
try managedObjectContext.save() ...
} catch {
fatalCoreDataError(error)
}
這里使用 fatalCoreDataError(error)替換掉了fatalError()络它。那么這個(gè)新的方法到底做了些什么事呢族檬?
它首先使用print()將錯(cuò)誤消息輸出到調(diào)試區(qū)域,因?yàn)橛涗涍@樣的錯(cuò)誤總是有用的化戳。 在這之后单料,執(zhí)行以下操作:
NotificationCenter.default.post(name: MyManagedObjectContextSaveDidFailNotification, object: nil)
我一直在使用術(shù)語(yǔ)“通知(notification)”來(lái)表示任何通用事件或消息正在交付,但iOS SDK也有一個(gè)名為NotificationCenter的對(duì)象(不要和你的手機(jī)上的通知中心混淆)点楼。
上面的代碼使用NotificationCenter發(fā)布通知扫尖。 你的app中的任何對(duì)象都可以訂閱此類通知,如果發(fā)生這些通知掠廓,NotificationCenter將在這些偵聽(tīng)器對(duì)象上調(diào)用某個(gè)方法换怖。
使用這個(gè)官方的通知系統(tǒng)是使你的對(duì)象可以相互溝通的另一種方式。 它的方便之處在于蟀瞧,發(fā)送通知的對(duì)象和接收通知的對(duì)象不需要彼此知道任何事情沉颂。 發(fā)件人只是發(fā)送通知,但并不在乎發(fā)生了什么事情悦污。 如果有人在聽(tīng)铸屉,太好了。 如果沒(méi)有切端,那么也無(wú)所謂彻坛。
UIKit定義了許多可以訂閱的標(biāo)準(zhǔn)通知。 例如,有一個(gè)通知讓你知道昌屉,當(dāng)用戶點(diǎn)擊Home按鈕時(shí)钙蒙,app即將被暫停。
你也可以定義自己的通知怠益,這就是你剛才所做的仪搔。 新的通知被稱為MyManagedObjectContextSaveDidFailNotification。
思路是蜻牢,app中有一個(gè)地方監(jiān)聽(tīng)這個(gè)通知烤咧,彈出一個(gè)警報(bào)視圖,并終止app抢呆。 使用NotificationCenter的好處是你的Core Data代碼部分不需要關(guān)心這些煮嫌。
無(wú)論何時(shí)保存發(fā)生錯(cuò)誤,無(wú)論app中的哪一點(diǎn)抱虐,fatalCoreDataError()函數(shù)都會(huì)發(fā)出此通知昌阿,因?yàn)樗嘈牌渌麑?duì)象正在偵聽(tīng)通知并處理錯(cuò)誤。
那么誰(shuí)來(lái)處理這個(gè)錯(cuò)誤恳邀? app delegate是一個(gè)很好的地方懦冰。 這是app中頂級(jí)的一個(gè)對(duì)象,你總是保證這個(gè)對(duì)象是存在的谣沸。
在AppDelegate.swift中添加以下方法:
func listenForFatalCoreDataNotifications() {
// 1
NotificationCenter.default.addObserver(
forName: MyManagedObjectContextSaveDidFailNotification,
object: nil, queue: OperationQueue.main, using: { notification in
// 2
let alert = UIAlertController(
title: "Internal Error",
message:
"There was a fatal error in the app and it cannot continue.\n\n"
+ "Press OK to terminate the app. Sorry for the inconvenience.",
preferredStyle: .alert)
// 3
let action = UIAlertAction(title: "OK", style: .default) { _ in
let exception = NSException(
name: NSExceptionName.internalInconsistencyException,
reason: "Fatal Core Data error", userInfo: nil)
exception.raise()
}
alert.addAction(action)
// 4
self.viewControllerForShowingAlert().present(alert, animated: true,completion:nil)
})
}
// 5
func viewControllerForShowingAlert() -> UIViewController {
let rootViewController = self.window!.rootViewController!
if let presentedViewController =
rootViewController.presentedViewController {
return presentedViewController
} else {
return rootViewController
}
}
我們一步步的來(lái)看一下:
1刷钢、告訴通知中心,每當(dāng)發(fā)布MyManagedObjectContextSaveDidFailNotification時(shí)乳附,都要通知你内地。
2、創(chuàng)建一個(gè)UIAlertController用于展示錯(cuò)誤消息赋除。
3阱缓、為警報(bào)彈窗的OK按鈕添加一個(gè)操作。處理按鈕按下的代碼
還是閉包(閉包這些東西無(wú)處不在>倥)荆针。 閉包創(chuàng)建一個(gè)NSException對(duì)象來(lái)終止應(yīng)用程序,而不是使用fatalError()颁糟。這樣的好處是祭犯,它提供了更多的信息到日志。
4滚停、最后,展現(xiàn)這個(gè)警報(bào)彈窗粥惧。
5键畴、為了通過(guò)present(animated,completion)方法來(lái)展示這個(gè)警報(bào)彈窗,你需要一個(gè)當(dāng)前可見(jiàn)的視圖控制器,所以這個(gè)方法就自己找了一個(gè)起惕。 不幸的是涡贱,你不能簡(jiǎn)單地使用窗口的rootViewController(在這個(gè)app中rootViewController就是tab bar controller),因?yàn)楫?dāng)Location Dteails界面打開(kāi)時(shí)惹想,它是處于隱藏狀態(tài)的问词。
剩下的就是調(diào)用一個(gè)新的listenForFatalCoreDataNotifications()方法,以便通知處理程序向NotificationCenter注冊(cè)嘀粱。
將以下內(nèi)容添加到application(didFinishLaunchingWithOptions)中激挪,就在return true語(yǔ)句之前:
listenForFatalCoreDataNotifications()
再次運(yùn)行app,并嘗試在獲得街道地址之前標(biāo)記位置锋叨。 現(xiàn)在垄分,app還是會(huì)崩潰,但是至少它告訴用戶發(fā)生了什么事情:
我應(yīng)該再?gòu)?qiáng)調(diào)一次娃磺,你必須對(duì)你的app進(jìn)行完善的測(cè)試薄湿,以確保你沒(méi)有給Core Data任何未驗(yàn)證過(guò)的對(duì)象。 你需要不惜一切代價(jià)避免這些存儲(chǔ)錯(cuò)誤偷卧!
理想情況下豺瘤,用戶不應(yīng)該看到該警報(bào)彈窗,但它必須存在听诸,因?yàn)闆](méi)有任何保證你的app不會(huì)有錯(cuò)誤坐求。
??:你可以合理地使用managedObjectContext.save()讓Core Data驗(yàn)證用戶輸入。 并不是說(shuō)每當(dāng)存儲(chǔ)失敗蛇更,你都要使app崩潰掉瞻赶,只有發(fā)生哪些不可預(yù)期的錯(cuò)誤時(shí)才會(huì)這樣做。
除了可選型之外派任,還有更多的驗(yàn)證設(shè)置可以放在實(shí)體的屬性上砸逊。如果你允許你的用戶輸入這些實(shí)體屬性,那么最好用save()去驗(yàn)證一下掌逛。如果save()方法拋出了一個(gè)錯(cuò)誤师逸,那么用戶的任何輸入都是無(wú)效的,你必須妥善處理這種情況豆混。
打開(kāi)數(shù)據(jù)模型文件篓像,重新將placemark屬性設(shè)置為可選型。
運(yùn)行app皿伺,看看是否一切正常员辩。