众所周知,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 文件。这是整个工程的预编译头文件,其中有两行代码值得注意
1
2
|
#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声明前添加下面一行代码
1
|
@objc(Beer)
|
它带来的好处是可以使 Beer Model 可以兼容Objective-C 的runtime 和 Core Data。
下一步,打开 BeerDetails.swift 执行和上一步相似的步骤,在class声明前添加以下代码
1
|
@objc(BeerDetails)
|
接下来,该初始化 Core Data Stack 了。打开 Appdelegate.Swift 找到application(_:didFinishLaunchingWithOptions:) 方法,在return语句前添加下面的一行代码:
1
|
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 文件并为类添加以下属性:
1
|
var
cuurentBeer: Beer!
|
这会使得 Beer 实体会被展示并持有。
接下来在 viewDidLoad() 方法尾端添加以下代码:
1
2
3
4
5
6
7
8
9
|
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():方法末尾添加以下代码
1
2
3
4
5
6
7
8
9
|
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() 方法中添加以下代码。
1
2
3
4
5
|
let cbName: String? = currentBeer.name
if
let bName = cbName {
beerNameTextField.text = bName
}
|
如果 currentBeer 的 name 属性有值,就将值填入对应的textfield中。
相似的为了填充beer详情页面,在viewDidLoad()方法末端添加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 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方法末端添加以下代码
1
2
3
4
5
|
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(): 方法中添加如下代码.
1
|
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 语句下添加如下代码
1
|
currentBeer.name = textField.text
|
当用户输入完毕的时候会把当前的beer的名字赋值为textfiled的文字内容。
接着,找到 textViewDidEndEditing(_:) 方法,在方法的结尾处添加如下代码:
1
2
3
|
if
textView.text !=
""
{
currentBeer.beerDetails.note = textView.text
}
|
这句代码的作用和上面那句相似,在用户在textView输入结束时候把用户输入的内容保存到beerDetail的note属性中。
最后,找到updateRating()方法并添加以下代码:
1
|
currentBeer.beerDetails.rating = ratingControl().rating
|
除了这些代码之外,在ImagePickerControllerDelegate中添加一些代码。 找到imagePickerController(_:didFinishPickingMediaWithInfo:)方法,在调用该方法之前,添加以下代码
1
2
3
4
5
6
7
8
9
|
// 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类已经创建,用以下代码替代原来的类声明:
1
|
class func saveImageToDisk(image: UIImage, andToBeer beer:?Beer)?-> Bool?{
|
这句代码将参数 beer 由 AnyObject 类型转换成了Beer类型,这有一定的安全性。
在 } else {之前添加如下代码:
1
|
beer.beerDetails.image = pathName
|
这句代码保存了图片的路径而不是图片的二进制数据。
现在,我们已经可以保存并展示数据,是时候看一下列表页面了。 打开 BeerListViewController.swift 并且在类中添加以下属性:
1
|
var
beers: [Beer]!
|
接下来,找到fetchAllBeers()方法,并在方法末尾添加以下代码:
1
2
3
4
5
|
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语句:
1
|
return
beers.count
|
这样我们就正确的返回了查询所得数据条数。
接下来,找到configureCell(_:atIndex:) 并在方法头部添加以下方法:
1
2
|
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 按钮时会触发这个方法,在方法的头部添加一下代码。
1
|
currentBeer.deleteEntity()
|
当详情页面加载之后,viewDidLoad()创建一个新的 Beer 对象,即使这个对象并未进行任何编辑。当用户点击Cancel 的时候,这个对象需要从数据源中删除。MagicalRecord中的 deleteEntity()方法将会完成这个任务。
找到方法的来源
有些开发者可能会对找到方法的来源感到好奇
-
高亮方法的名称
-
右键点击方法名称
-
选择Jump to Definition
编译运行,再次点击我们开始的"取消"操作,这回就不会再有空行在列表页出现了。
删除一条记录
现在我们应该整理一下导航栏了,并且将列表中空项目删除。这首先就要求要为tableView开启可编辑模式。
打开 BeerListViewController.swift,并找到tableView(_:commitEditingStyle:forRowAtIndexPath:)方法,在 if 语句中添加如下几行代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
//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
-
向方法中添加以下方法
1
|
NSManagedObjectContext.defaultContext().saveToPersistendStoreAndWait()
|
编译运行。向左滑动列表中的空行,并点击Delete按钮。
以上工作我们避免了列表中出现空的条目,可以再试试多删除几条。
编辑一条记录
截至目前,一个beer可以被添加、取消或者是删除,接下来我们实现一下编辑功能。点击列表行展示新的beer名称。发生了什么?App跳转到了一个空的详细页面。难道有人把beer喝掉了?事实上,该列表视图需要将beer对象发送给详细页面,接下来我们就来修复它。
打开?BeerListViewController.swift 找到prepareForSegue(_:sender:),它指出了列表在什么地方将要跳转到详细页。
在if segue.identifier == "editBeer" {:后添加以下代码:
1
2
3
4
|
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()添加以下代码:
1
2
3
4
5
6
|
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使用提供能多思路。