本文由CocoaChina譯者lynulzy(論壇ID)翻譯
作者:Ed Sasena
原文:Getting Started with MagicalRecord
(轉載請保持內容和所有鏈接的完整性)
眾所周知,Core Data 是蘋果內置關於用戶數據持久化的解決方案,無論在 iOS 平台還是 OS X 平台,它都是通用的。盡管蘋果一直秉持著最大化的使編程簡單的宗旨,但是 Core Data 上手對於有經驗的開發者也不是一件易事。
即便是你知道如何使用 Core Data ,很簡單的日常任務,在使用了 Core Data 之後會變得笨重而繁瑣。 好消息是,MagicalPanda開源了一個針對 Core Data 的三方庫 -- MagicalRecord。MagicalRecord 提供了便利的方法來創建那些使用Core Data 所必須的代碼,諸如對 Core Data 的設置、查詢、更新。它的設計靈感來源於德高望重的 Active Record 設計模式。
本教程將讓你快速上手 MagicalRecord: 你將創建一個應用來追蹤你最喜愛的beer(或者其他類似的飲料),它將具備以下功能:
添加beer
評價beer
評論某款beer
為beer拍照
學習本教程要求你要有一定的Core Data使用基礎。這裡有一些 Core Data 資料供參考。
開始吧
下載起始工程,並且雙擊 BeerTracker.xcworkspace 打開工程。
運行程序,你會發現存在幾個關於 MagicalRecord 的警告,這些警告可以暫時忽略。
這個應用由一個帶導航欄按鈕的基本的導航控制器、列表、搜索框和一個控制排序的種類的分段控制器構成。點擊“+”按鈕顯示一個添加beer信息的頁面,但是目前這個程序並不能保存添加的數據。
大致浏覽一下整個工程,展開 Beertracker 下的 BeerTracker 文件夾。
浏覽一下你會發現, Core Data 模型存在但是卻並未發現使用它的代碼。通過本系列的教程,你將為這個工程添加一些功能,並借助於MagicalRecord 實現不添加Core Data 的框架代碼而使用Core Data 模型。
探索MagicalRecord
在工程的導航標簽頁下,打開 Pods target 下的 Pods 文件夾,如圖
在 Shorthand 文件夾下,打開 NSManagedObjectModel+MagicalRecord.h. 文件。在這個文件夾下的方法名都被冠以 MR_. 這正是因為 MagicalRecord 是擴展了 Cord Data 類,這個命名空間能夠保證不會和已經存在的方法名出現沖突。
再次打開 Shordhand 文件夾,打開其下的 Support Files 文件夾下的 Pods-BeerTracker-MagicalRecord-prefix.pch 文件。這是整個工程的預編譯頭文件,其中有兩行代碼值得注意
#define MR_SHORTHAND 0 #import “CoreData+MagicalRecord.h”
正是這兩行代碼可以讓整個工程正確的使用 MagicalRecord:
MR_SHORTHAND 會告訴 MagicalRecord 這個工程將不會使用前綴MR_。對於有興趣研究這是怎麼實現的童鞋,可以在 MagicalRecord+ShorthandSupport.m 中找到答案。
導入 CoreData+MagicalRecord.h 使得可以使用MagicalRecord 中任何API。
實體類: Beer Model
這時候就可以使用數據模型來追蹤你最愛的beer了。為了使MagicalRecord中的數據模型類可以被 Objective-C代碼訪問,需要在Beer.swift 中的class聲明前添加下面一行代碼
@objc(Beer)
它帶來的好處是可以使 Beer Model 可以兼容Objective-C 的runtime 和 Core Data。
下一步,打開 BeerDetails.swift 執行和上一步相似的步驟,在class聲明前添加以下代碼
@objc(BeerDetails)
接下來,該初始化 Core Data Stack 了。打開 Appdelegate.Swift 找到 application(_:didFinishLaunchingWithOptions:) 方法,在return語句前添加下面的一行代碼:
MagicalRecord.setupCoreDataStackWithStoreNamed("Beer")
如果你之前使用 Core Data 開發過應用,應該知道配置這個stack需要多少代碼,但是使用 MagicalRecord,所有工作只需要一行代碼而已!
MagicalRecord 提供了一些方法來配置 Core Data Stack,選用哪種方法取決於:
後台存儲類型
是否支持自動遷移
Core Data Model是否和項目名稱匹配
MagicalRecord 中與之相應的便捷初始化方法如下:
setupCoreDataStackWithInMemoryStore
setupAutoMigratingCoreDataStack
setupCoreDataStack
如果數據模型和工程的名稱一致(比如:模型文件名為: BeerTracker.xcdatamodeld,而工程的名字為 BeerTracker,這即是一致的。),這時你就可以使用上面列出的便捷方法。而在本教程中,模型文件名是和工程名不同的,所以應該通過下面兩個方法指明存儲名稱:setupCoreDataStackWithStoreNamed(_:)或者
setupCoreDataStackWithAutoMigratingSqliteStoreNamed(_:).
一旦初始化了數據模型,Core Data 需要一些附加的代碼來處理model發生的任何細微的變化。 有關自動遷移的初始化方法中,MagicalRecord將會處理由老的數據模型向新的數據模型遷移的工作,當然,前提是能夠滿足遷移的條件。
編譯並運行。應用的外觀應該沒有任何變化,所有的變化僅僅是集中在後台的數據存儲中。
構造 Beer 實體
截至目前,數據模型和 Core Data Stack 已經創建並初始化,現在就可以向列表中添加 beer 了。 打開 BeerDetailViewController.swift 文件並為類添加以下屬性:
var cuurentBeer: Beer!
這會使得 Beer 實體會被展示並持有。
接下來在 viewDidLoad() 方法尾端添加以下代碼:
if let beer = currentBeer { // A beer exists. EDIT Mode. } else { // A beer does NOT exist. ADD Mode. currentBeer = Beer.createEntity() as! Beer currentBeer.name = "" }
BeerDetailViewController的加載是由BeerListViewController:中兩個條件觸發:
用戶選擇了列表中的一項 - 編輯模式
用戶點擊了 + 按鈕 - 新增模式
這段代碼用來檢查一個Beer對象是否被設置,這一定是在編輯模式下,如果沒有設置,會創建並添加一個新的Beer對象。
接下來,做一些和實體詳情有關的工作,在viewDidLoad():方法末尾添加以下代碼
let details: BeerDetails? = currentBeer.beerDetails if let bDetails = details { // Beer Details exist. EDIT Mode. } else { // Beer Details do NOT exist. Either ADD Mode or EDIT Mode with a beer that has no details. currentBeer.beerDetails = BeerDetails.createEntity() as! BeerDetails }
這段代碼作用是訪問beer detail屬性,也會做一些像之前那樣的檢查然後進入編輯模式或者添加模式。編譯並運行保證這裡沒有編譯錯誤。在外表上看來應用並沒有發生太多的改變,保持耐心,好的代碼就像美酒的釀制,需要一些時間。
構造用戶界面
如果已經存在的beer對象被編輯了,那麼在UI上的體現就是頁面信息由這個beer的詳情信息填充,此外,頁面還需要保留一些空白以添加新的beer。
在 BeerDetailViewController.swift 的 viewDidLoad() 方法中添加以下代碼。
let cbName: String? = currentBeer.name if let bName = cbName { beerNameTextField.text = bName }
如果 currentBeer 的 name 屬性有值,就將值填入對應的textfield中。
相似的為了填充beer詳情頁面,在viewDidLoad()方法末端添加以下代碼
// Note if let bdNote = details?.note { beerNotesView.text = bdNote } // Rating let theRatingControl = ratingControl() cellNameRatingImage.addSubview(theRatingControl) if let bdRating = details?.rating { theRatingControl.rating = Int(bdRating) } else { // Need this for ADD Mode. theRatingControl.rating = 0 } // Image if let beerImagePath = details?.image { let beerImage = UIImage(contentsOfFile: beerImagePath) if let bImage = beerImage { showImage(bImage) } }
這段代碼完成了BeerDetails屬性note、rating以及image的填充。
為了將頁面的標題和頁面內容相符,在viewDidLoad方法末端添加以下代碼
if currentBeer.name == "" { title = "New Beer" } else { title = currentBeer.name }
現在數據可以展示了,但是需要添加一些要展示的數據,接下來我們將完成這項任務。
添加一條數據
當用戶點擊列表頁面的 "+" 按鈕時,prepareForSegue(_:sender:)方法被調用,來准備向 BeerDetailViewController 頁面過渡。在 BeerListViewController.swift 中找到prepareForSegue(_:sender:)方法,注意 if 分支下的有 addBeer 標識的分支。這段代碼的作用之一是創建了一個可以在點擊"Done"導航按鈕的時候執行addNewBeer方法。
打開 BeerDetailViewController.swift 找到 addNewBeer() 方法。這個方法將從導航控制器中彈出 BeerDetailViewController,這將會觸發 viewWillDisappear 方法,反過來又會調用 saveContext() 方法。
在 BeerDetailViewController.swift 的 saveContext(): 方法中添加如下代碼.
NSManagedObjectContext.defaultContext().saveToPersistendStoreAndWait()
這句代碼通過調用MagicalRecord的方法保存了這個Beer對象。無論是在viewDidLoad()方法中新建一個對象,或者是在 BeerDetailViewController 的編輯模式下,這句代碼都適用。
不要小看了這一句代碼,在這一句代碼後面包含了許多操作。在 Appdelegate.swift中,我們通過MagicalRecord設置了Core Data棧。創建棧的同時會創建一個managementObjectContext,這個對象對於整個應用全局可用。當創建 Beer 實例和 BeerDetails 實例時,它們就會默認的被插入這個defaultContext,而 MagicalRecord 允許任何的 managedObjectContext 通過 saveToPersistentStoreAndWait(_:)方法來存儲。
接下來,根據用戶的數據入內容為 Beer 和 BeerDetail 的屬性賦值。 打開 BeerDetailViewController.swift 找到textFieldDidEndEditing(_:)。在 if 語句下添加如下代碼
currentBeer.name = textField.text
當用戶輸入完畢的時候會把當前的beer的名字賦值為textfiled的文字內容。
接著,找到 textViewDidEndEditing(_:) 方法,在方法的結尾處添加如下代碼:
if textView.text != "" { currentBeer.beerDetails.note = textView.text }
這句代碼的作用和上面那句相似,在用戶在textView輸入結束時候把用戶輸入的內容保存到beerDetail的note屬性中。
最後,找到updateRating()方法並添加以下代碼:
currentBeer.beerDetails.rating = ratingControl().rating
除了這些代碼之外,在ImagePickerControllerDelegate中添加一些代碼。 找到imagePickerController(_:didFinishPickingMediaWithInfo:)方法,在調用該方法之前,添加以下代碼
// 1 if let imageToDelete = currentBeer.beerDetails.image { ImageSaver.deleteImageAtPath(imageToDelete) } // 2 if ImageSaver.saveImageToDisk(image!, andToBeer: currentBeer) { showImage(image!) }
這段代碼做了以下兩件事:
1. 在用戶對圖片移動或者批量操作之前的清除工作。
2. 在UI上展示圖片,並將圖片存至磁盤。
在上面的代碼中我們使用到了saveImageToDisk(_:andToBeer:)方法,我們需要完善這個方法,在 ImageSaver.swift 文件中找到saveImageToDisk(_:andToBeer:)方法。
由於Beer類已經創建,用以下代碼替代原來的類聲明:
class func saveImageToDisk(image: UIImage, andToBeer beer:?Beer)?-> Bool?{
這句代碼將參數 beer 由 AnyObject 類型轉換成了Beer類型,這有一定的安全性。
在 } else {之前添加如下代碼:
beer.beerDetails.image = pathName
這句代碼保存了圖片的路徑而不是圖片的二進制數據。
現在,我們已經可以保存並展示數據,是時候看一下列表頁面了。 打開 BeerListViewController.swift 並且在類中添加以下屬性:
var beers: [Beer]!
接下來,找到fetchAllBeers()方法,並在方法末尾添加以下代碼:
let sortKey = NSUserDefaults.standardUserDefaults().objectForKey(wbSortKey) as? String let ascending = (sortKey == sortKeyRating) ? false : true // Fetch records from Entity Beer using a MagicalRecord method. beers = Beer.findAllSortedBy(sortKey, ascending: ascending) as! [Beer]
這段代碼調用了MagicalRecord的 findAllSortedBy(_:ascending:)方法,結果是beers對象會保存從數據源中取處的所有Beer對象。
findAllSortedBy(_:ascending:)方法是MagicalRecord中執行一個CoreData篩選的幾種方法之一,更多的信息請參考 NSManagedObject+MagicalFinders.m 文件。
現在beers已經存儲了一些記錄,我們可以在列表中展示這些數據了。仍然是在BeerListViewController.swift,找到tableView(_:numberOfRowsInSection:),用以下內容代替?return語句:
return beers.count
這樣我們就正確的返回了查詢所得數據條數。
接下來,找到configureCell(_:atIndex:) 並在方法頭部添加以下方法:
let currentBeer = beers[indexPath.row] cell.textLabel?.text = currentBeer.name
現在,列表中就已經添加了name屬性。
編譯運行,現在試著添加一個beer
點擊導航欄的 + 按鈕
當頁面過渡到詳情頁面,在BeerName輸入框中為這個新的beer對象添加名稱
點擊導航欄的 Done 按鈕
當頁面返回的時候會發現,這條記錄在列表中顯示了!?
異常修復
如果程序不能像上面那樣正常運行,試著用Xcode菜單中的Product\Clean選項來清除對象,或者試著把應用從模擬器中刪除:
長按App icon直到所有icon晃動
點擊icon左上角的刪除按鈕
點擊警示框的確認刪除按鈕
從iOS模擬器中菜單中選擇Hardware\Home
這種方法在之後還可能會被用到,比如帶右箭頭的空行在修復了tableView:numberOfRowsInSection中的返回行數之後還會出現的時候就可以使用。
取消一條記錄
說起空行,我們試想一下,當用戶開始添加一條beer記錄的時候,之後又不想加了怎麼辦? 打開我們的應用,
點擊導航欄上的 + 按鈕
當頁面跳轉到詳情頁面的時候點擊 Cancel 按鈕
這樣操作之後,頁面會返回到列表頁,但是這時候會有一個空的 beer 出現在列表中,一個帶有指示標識的右箭頭但並沒有名稱的 beer 出現在列表中。
這時候就需要使用 MagicalRecord 來拯救這種情況了!
打開 BeerDetailViewController.swift,找到 cancelAdd()方法,當用戶點擊了 Cancel 按鈕時會觸發這個方法,在方法的頭部添加一下代碼。
currentBeer.deleteEntity()
當詳情頁面加載之後,viewDidLoad()創建一個新的 Beer 對象,即使這個對象並未進行任何編輯。當用戶點擊 Cancel 的時候,這個對象需要從數據源中刪除。MagicalRecord中的 deleteEntity()方法將會完成這個任務。
找到方法的來源
有些開發者可能會對找到方法的來源感到好奇
高亮方法的名稱
右鍵點擊方法名稱
選擇Jump to Definition
編譯運行,再次點擊我們開始的"取消"操作,這回就不會再有空行在列表頁出現了。
刪除一條記錄
現在我們應該整理一下導航欄了,並且將列表中空項目刪除。這首先就要求要為tableView開啟可編輯模式。
打開 BeerListViewController.swift,並找到tableView(_:commitEditingStyle:forRowAtIndexPath:)方法,在 if 語句中添加如下幾行代碼:
//1 beers.removeAtIndex(indexPath.row).deleteEntity() saveContext() //2 let indexPaths = [indexPath] //3 tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic) //4 tableView.reloadData()
檢查一下上述代碼:
從beers數組中刪除了Beer對象並且調用了MagicalRecord的deleteEntity方法來把數據從數據倉庫中刪除。調用saveContext使MagicalRecord可以具體的實現將數據從數據存儲中刪除。
生成一個臨時數組,保存將要被刪除的行的indexPath,而這個indexPath能夠唯一確定一條數據
通知 table view 將臨時數組中的保存的指定行刪除
重新加載 table view 使我們上述的改變體現出來。
注意 saveContext()這句代碼。保存變更必須要使用這句代碼,之前我們已經有過一次這樣的操作,解決方案如下:
在 BeerListViewController.swift 中
找到saveContext
向方法中添加以下方法
NSManagedObjectContext.defaultContext().saveToPersistendStoreAndWait()
編譯運行。向左滑動列表中的空行,並點擊Delete按鈕。
以上工作我們避免了列表中出現空的條目,可以再試試多刪除幾條。
編輯一條記錄
截至目前,一個beer可以被添加、取消或者是刪除,接下來我們實現一下編輯功能。點擊列表行展示新的beer名稱。發生了什麼?App跳轉到了一個空的詳細頁面。難道有人把beer喝掉了?事實上,該列表視圖需要將beer對象發送給詳細頁面,接下來我們就來修復它。
打開?BeerListViewController.swift 找到prepareForSegue(_:sender:),它指出了列表在什麼地方將要跳轉到詳細頁。
在if segue.identifier == "editBeer" {:後添加以下代碼:
let indexPath = tableView.indexPathForSelectedRow() let beerSelected = beers[indexPath!.row] controller!.currentBeer = beerSelected controller!.currentBeer.beerDetails = beerSelected.beerDetails
試著編輯beer的名稱並點擊Done,App會跳轉到list頁面並展示beer的名稱。
收尾
除此之外,我們的應用能做的還有很多,例如:執行查詢操作以及預先填入一些數據作為開始。
搜索
現在在應用中有多款beer出現,我們測試一下應用的搜索功能。我們起始的應用實際上已經包含了搜索框。將列表滑動到最上方就可以看到。當然我們還需要添加一些代碼才能實現搜索功能。
之前,我們使用MagicalRecord的方法來取得所有的數據。現在我們的需求是只取出符合需求的數據。
針對這個需求,我們需要使用NSPredicate。如何實現呢?邏輯應該是在BeerListViewController.swift 的performSearch()。
解決方案
在tableView.reloadData()之前,為performSearch()添加以下代碼:
let searchText = searchBar.text let filterCriteria = NSPredicate(format: "name contains[c] %@", searchText) beers = Beer.findAllSortedBy(sortKeyName, ascending: true, withPredicate: filterCriteria, inContext: NSManagedObjectContext.defaultContext()) as? [Beer]
其他的搜索方法詳情參考MagicalRecord的頭文件。
運行我們的應用,找到搜索框,查看列表中的beer,搜索其中的一項,看下能否正常工作?
Demo數據
如果能夠給用戶一些提示數據來教會用戶如何追蹤自己喜愛的beer將會帶來良好的用戶體驗。在最終版本的 AppDelegate.swift 中,這裡有兩種先澤來預加載示例數據: 1. 為了只預加載beer數據一次,無論應用運行多少次,都把沒有評論的beer放在第一部分。
2. 為了強制的預加載數據,忽略數據是否已經被加載過的情況,沒有被評論的數據放在第二部分。這種方案應對當一個或者更多的預加載數據被刪除的時候,用戶希望有一個新的數據填充在這裡是極好的。
Magical調試器
當應用啟動的時候,MagicalRecord會打印出在Core Data Stack創建的時候的四條記錄。這些記錄了在棧創建過程和defaultContext的創建,而這個 defaultContext 正是我們之前使用的 saveContext。
同樣的,當我們變更或者添加新的beer對象的時候,在點擊Done按鈕之後,MagicalRecord會打印執行了保存和報告日志的操作,例如
defaultContext保存到主線程?
標記為1的上下文環境的變更?
標記為1的保存同步?
對象被插入到上下文中?
保存結束的時候
MagicalRecord將會報告那些因為沒有更改但卻沒有保存的操作,試著這樣操作一下:
編譯運行應用
打開調試控制台
選擇列表中一個當前存在的條目
進入到詳情頁面的時候返回列表頁面並點擊Wender Beer選項
這時候打印的日志是這樣的:
“NO CHANGES IN ** BACKGROUND SAVING (ROOT) ** CONTEXT – NOT SAVING”
離開詳情頁面會使viewWillDisappear(_:)調用saveContext(),這項操作會使defaultContext保存。由於我們沒有對該條目作出任何改變,MagicalRecord識別出沒有必要執行保存操作,所以會跳過這個過程。我們沒有必要考慮是否有保存上下文環境的必要,調用保存的方法,讓MagicalRecord幫我們判定吧。
注意MagicalRecord的日志,通常會有很重要的信息能夠幫助你。
結語
你可以在此下載最終的項目,如果被卡在某個步驟的時候可以參考一下。
希望這篇教程能講明白MagicalRecord的易用性,對於減少樣板代碼非常有用。該教程的基本原則可幫你開發任何種類的應用程序以幫助用戶跟蹤他們喜歡的照片。筆記以及評分等。
如果你想進一步開發BeerTracker項目,以下是一些建議
為BeerListViewController添加“no beers created yet”信息--在MagicalRecord中找到並使用hasAtLeastOneEntity方法。
添加信息以說明有多少款beer匹配搜索結果,可使用countOfEntitiesWithPredicate方法。
實現數據庫的恢復功能,可使用MagicalRecord的truncateAll方法。
在Pods/MagicalRecord/Shorthand下打開Pods target的MagicalRecordShorthand.h,浏覽方法的名稱,它們大多數是自說明的。這個頭文件可以為MagicalRecord使用提供能多思路。
關於該教程有任何問題,歡迎加入討論。