CloudKit,是苹果最新推出的基于iCloud的一个云端数据存储服务,提供了低成本的云存储并能作为一个后端服务通过用户们的iCloud账号分享其应用数据。
CloudKit主要由两个部分组成:
一个仪表web页面用于管理公开数据的记录类型。
一组API接口用于iCloud和设备之间的数据传递。
CloudKit也具有安全性,为用户的私人数据提供了完整的保护。而开发者不仅只能接入自己的数据库,也不允许查看用户的私有数据。
CloudKit适用于那些在服务端计算量不大,却需要使用大量数据的iOS平*占应用。
这篇教程将带领你通过构建一个叫做BabiFüd的找餐厅应用获取到有关CloudKit开发的实战经验。
提示:这篇教程的实例要求你有一个激活的iOS开发者账户,否则你将没有使用iCloud和CloudKit的权限。
为什么选择CloudKit?
在最开始,你也许会好奇为什么你要选择CloudKit而非Core Data、商业后端服务或者用自己的服务器。
答案有三方面:易操作性、可靠性、成本。
易操作
与其他的后台解决方案不同,CloudKit的设置相当容易。你无需选择、配置、安装服务或者为缩放比率和安全性之类的问题焦虑。
简单的注册成为iOS开发者你就可以拥有使用CloudKit的资格,而不需要为这个附加的功能单独注册或者重新申请账号。作为启动CloudKit功能的一部分,所有必要的设置都不可思议的在服务器上自动完成了。
CloudKit的导入方式和其他的iOS框架一样不需要额外下载运行库并且配置它们,框架本身提供了方便的API接口使一般的操作变得极为简易。
对于用户而言,易操作性也是可体现的。由于CloudKit采用了iCloud的证书认证,一旦用户安装之后(或者通过设置应用)就能直接进入,所以建立一个复杂的登录界面是完全没有必要的。只要用户登录上去,那就能轻松使用你的应用了。
可靠
CloudKit的另一大优势在于,只要用户在开发者和苹果之间更愿意相信苹果,那么他们就不必为自己私密数据的安全感到担忧,因为CloudKit隔离了用户数据与和开发者。
虽然对于开发者来说缺乏数据会有些失望(比如在调试的时候是很需要数据的),不过对于他们仍是有好处的——至少不用再担心安全性,也不用去说服用户信任你。甚至他们丝毫没有意识到CloudKit和iCloud之间的区别,只要他们信任iCloud就意味着他们信任你。
成本
最后,对于每个开发者来说运行一个服务需要相当大的耗费。就连是最便宜的主机服务也不会考虑你应用的价格来定价,而且只要跑上哪怕一个应用,你就得掏钱。而CloudKit允许你免费的使用一定限额的空间来存储公开数据,如果想了解更多你可以看这里。
这些优势使得CloudKit可以简单无脑的服务于Mac或者iOS应用。
关于BabiFüd
本教程的示例程序是一个叫BabiFüd的应用,是一款最新的定位服务式的典型应用。总之,相较于在定位后侧重于餐厅的食物质量、服务效率、价格以及儿童方面的用户定位,这个应用更关注餐厅设施的变化、座位的舒适度和健康饮食。
这款应用包含了四个标签:附近地点的列表(Nearby),显示附近地点的地图(Map),用户生成的记录(Note)和应用设置(Setting)。你也可以通过以下的两张截图来感受以下这个应用大概是什么样子:
这个应用的模型架构是在这些显示视图的内部调用了CloudKit,在这里CloudKit对象称为记录(records),模型中的主要记录类型称作Establishment,代表了你的应用里那些各种各样的地点。通过这种方式你还可以在你的数据库中添加相关的评分和备注信息。
开始
开始教程之前你需要下载一个CloudKit项目。
在你开始编码之前你可以更改你应用的Bundle Identifier和Team,你需要设置一个Team之后苹果才会给你提供CloudKit相关的功能使用权,然后一个唯一的Bundle Identifier会使得整个过程轻松许多。
像下图这样,在Xcode里面打开BabiFud.xcodeproj。选择项目导航里的BabiFud项目,然后选择目标BabiFud之后在Bundle Identifier里填入一个你觉得不会重名的名称,我推荐使用域名反序法,然后加上项目名称,然后选择一个合适的Team。
请留意Bundle Identifier和Team。现在你需要为你的应用设置CloudKit服务,然后创建一个容器来存储你的应用数据。
权限和容器
在你通过应用添加任何数据之前你需要一个容器来存储这些数据记录。所谓的容器其实就是在服务器上为应用数据假想一块存储空间,由共享数据和私密数据组成。创建容器意味着你的应用需要有使用CloudKit的权限。
在目标编辑器里选择Capabilities标签,然后启用iCloud,如下图:
这个时候Xcode可能会提示你登陆你已绑定开发者账号的苹果用户,你只需要照做就是了。最后,在服务选项里面勾上CloudKit选框。
这样你就创建了一个名称形如 iCloud.<你的应用的bundle id="">的默认容器。示例如下图:
如果你在上面这些步骤遇见了什么问题或者错误,这里有一些情况的解决方案:
1.在启用iCloud那一步如果提示了错误或者警告的话,你可以试着点击最Fix Issue按钮,这样可能会需要等一段时间。
2.bundle id和iCloud容器必须一一对应,比如你的bundle identifier写成了“com.<你的域名>.Babifud”,那你的容器名应该是"iCloud.com.<你的域名>.Babifud"。
3.容器的命名必须独一无二,因为这是CloudKit用来接入数据的唯一标识符,所以这也意味着bundle id也必须独一无二。
4.为了确保拥有正常工作的权限,应用id和bundle id必须列在证书、标示符和配置中心的App ID部分。也就是说你需要设置一个Team id以用来验证登录,此外也要列上应用id,还有iCloud 容器的id。
通常来说CloudKit会自动为你完成所有内容,前提是你登入的是一个可用的开发者账号。不过有时候并不是那么及时的同步,你还可以通用一个新的账号,然后改变CloudKit容器id保证对应。另外,为此你可能需要修改info.plist文件或者BabiFud.entitlements文件以确保里面的id也和你设置的bundle id匹配。
关于CloudKit仪表盘
设置了可用权限之后下一步就是配置一下CloudKit,来创建应用数据的记录类型。之后你便可以开始使用CloudKit的仪表盘,如下图,点击CloudKit Dashboard。
提示:你也可以用网页登陆你的仪表盘。
你会看到仪表盘出现了,像这样:
下面是此教程需要的有关于仪表盘的一些概述:
左边栏的SCHEMA代表CloudKit容器的高级类:Record Types, Security Roles, 和Subscription Types。在教程里你只会用到record Types。
一个Record Types用来设置定义一个单独的记录。在面向对象编程里,Record Types就相当于某一对象的类模板。一个记录可以看做一个Record Type类的实例。这是容器的基本数据结构,倒是很想数据库里面的一行数据,包含了一系列键和值。
PUBLIC DATA和PRIVATE DATA 就是你添加查找数据的地方,你需要接入数据库。记住,作为一个开发者你可以阅览所有的共享数据,不过你只能看到你自己的私密数据,User Records记录了一些当前CloudKit使用者的的信息比如名称或者邮件。一个Record Zone(这里是Default Zone)用于给私密数据分组提供数据的逻辑结构。当在进程的其他操作之前允许大量数据数据同时存储时Custom zones支持自动处理。不过那不是我们今天要讨论的范围。
ADMIN栏主要用于管理开发团队的成员权限,如果你的项目有很多人共同开发,你可以规划一下他们各自的权限,这个问题比较艰深而且有些超纲故而在此不谈。
添加Establishment类的记录
选中Record Types,点击左上方+的图标然后你就可以添加一个新的记录类型并设置细节。如下图:
命名你的新记录类为Establishment。
来考虑一下有关应用的设计,每一个你想要记录的Establishment都有很多数据:名称、地点、或者是儿童友好的选项都是有用的。记录类就是用于定义记录中各式各类的数据的。
当你开始定义Name, Attribute Type,和Index的时候你会看到如下这行,这个时候一个Attribute命名模板已经自动创建。
你可以修改这个预设的Attribute Name为你想要的,这里因为要做的是示例中的应用,因此你需要添加下面的Attribute,点击Add Attribute...添加新行。
当你完成之后你会得到如下这样一个属性表单。
点击页面底部的Save以保存你的新纪录类型。
你现在可以添加一些Establishment样本到你数据库里面了。
选择左边导航里的Default Zone栏这里主要存储了你应用上的共享数据,然后在出现在中间的下拉列表里选择Establishment记录类,最后点击右边+的图标。
这样你就创建了一个全新的空Establishment记录。
这时你可以添加一些测试数据了。
下面这个测试样例的数据是完全虚构的,位置数据已经被设定在苹果总部附近以便于在虚拟机上测试。
填写数据如下表:
提示:每一个 CoverPhoto属性里的图片都存储在项目的Supporting Files\Sample Images文件夹里面,你只要拖动这些图片到 CoverPhoto栏里面它们就在记录保存的时候会自动上传。
三个测试数据录入完毕之后会像下面这样;
对于每条数据,这里的值只是代表它在数据库中的样子,在应用的那一端会完全不一样,比如SeatingType和ChangingTable是枚举类型,在这里的这个整数代表的是枚举的数的值。对于HealthyOption和KidsMenu来说这里的值代表了布尔型数据:0代表这家餐厅不满足此项,1则是满足。
让我们在退回Xcode。是时候把这些数据整合进你的应用了。
查询Establishment数据
CKQuery对象被用于从数据库里查询记录。一个CKQuery描述了如何查询一些特定类型或是特定条件的记录。这些条件可以是诸如“记录的Name是M开头的”、“有软垫座位的记录”、“方圆三公里以内的记录”。这一类型在Cocoa里的表达方式则是使用NSPredicate对象,NSPredicate也会判定各对象是否满足规则。在Core Data里适用的很多判定很同样适用于CloudKit。因为判定条件通常会被定义为对某一属性进行比较。
CloudKit仅支持可用的NSPredicate方法中的一部分子集。包括一些数学比较,字符串和集合运算,以及新增的特定距离函数。distanceToLocation:FromLocation方法:NSPredicate为CloudKit添加的一个根据已知坐标计算和半径匹配范围内记录的方法。下面的这类判定都略过了细节,对于其它的查询CKQuery类引用了一个关于使用的方法和如何使用的清单。
提示:CloudKit包括了支持CLLocation类,其中有Core Location框架下包括地理位置的一些对象。这使得在某一地理范围内查询Establishment类变得简单,你不需要写那些痛苦的数学公式。
打开Model\Model.swift文件,包含了服务端的所有调用。
用下面这一段替换掉fetchEstablishments(location:, radiusInMeters:)方法
func fetchEstablishments(location:CLLocation,
radiusInMeters:CLLocationDistance) {
// CloudKit在它自带的距离判定中使用的单位是公里,这里把radiusInMeters转换成公里
let radiusInKilometers = radiusInMeters / 1000.0
// 这一判定Establishment类的条件是它们到当前距离的公里数,这个方法会根据用户当前位置以及设定范围得到范围内所有的Establishment和它们的位置信息
let locationPredicate = NSPredicate(format: "distanceToLocation:fromLocation:(%K,%@) < %f",
"Location",
location,
radiusInKilometers)
// CKQuery 对象的创建需要一个record类型和一个判定条件作为参数,它们将用于查询
let query = CKQuery(recordType: EstablishmentType,
predicate: locationPredicate)
// performQuery(_:, inZoneWithID:, completionHandler:)方法会把你的查询发送到iCloud,返回结果。当传递的inZoneWithID为nil的情况下你只会在default zone也就是共享数据中进行查询,如果你希望连着一起查询私人数据的话,则需要进行一个单独的调用。
publicDB.performQuery(query, inZoneWithID: nil) {
results, error in
if error != nil {
dispatch_async(dispatch_get_main_queue()) {
self.delegate?.errorUpdating(error)
return
}
} else {
self.items.removeAll(keepCapacity: true)
for record in results{
let establishment = Establishment(record: record as CKRecord, database: self.publicDB)
self.items.append(establishment)
}
dispatch_async(dispatch_get_main_queue()) {
self.delegate?.modelUpdated()
return
}
}
}
}
所有的都做完之后你也许会好奇CKDatabase的实例publicDB是从哪儿来的。那么看下Model类最开始的代码:
let container : CKContainer
let publicDB : CKDatabase
let privateDB : CKDatabase init() {
// defaultContainer()代表的是你在iCloud功能栏里制定的那个容器
container = CKContainer.defaultContainer()
// publicCloudDatabase则是你应用上的所有用户共享的数据
publicDB = container.publicCloudDatabase
// privateCloudDatabase仅仅是你个人的私密数据
privateDB = container.privateCloudDatabase
}
这些代码会从共享数据里找出几个当地的Establishment数据来,不过我们还需要在应用里面用一个视图控制器来显示点东西。
设置必要回调函数
你可能会注意到通知中心使用的是我们非常熟悉的委托模式。下面是Model.swift文件最开始的协议部分代码,你可以在你的视图控制器中实现它。
protocol ModelDelegate { func errorUpdating(error: NSError) func modelUpdated() }
打开MasterViewController.swift文件并用下面代码替换掉 modelUpdated()方法:
func modelUpdated() { refreshControl?.endRefreshing() tableView.reloadData() }
这里通常是在新数据可用时执行。在tableView(_: cellForRowAtIndexPath:)中,有关CloudKit对象的所有table view cell的唤醒都被照顾到了,你可以自己随便看一看。
现在来用下面的代码替换掉errorUpdating(error:):
func errorUpdating(error: NSError) {
let message = error.localizedDescription
let alert = UIAlertView(title: "Error Loading Establishments",
message: message, delegate: nil, cancelButtonTitle: "OK")
alert.show()
}
无论查询结果产生任何错误都会调用此方法。这些错误发生的原因可能是网络情况较差,也可能是像用户凭证丢失或出错这样的CloudKit特有问题,还可能是因为你所要找的这条记录根本不存在。
提示:在对待任何远程服务的时候一个好的异常处理是必不可少的,而在这里你仅仅是给用户弹出了一个错误提示而已。
在模拟器上运行一下,你会看到如下这样一个关于附近的Establishment列表:
你可以看到Establishment的名称以及它提供的相关服务,不过你却没有看到任何相关的图片显示,这是为什么?
当你获取你的Establishment记录时候你也会自动的去获取图片,不过,想要在你的App中显示图片的话你还需要做一些必要事项(这在下面会提到,先来看一看一些出错处理)。
常见错误处理
如果你获取的列表显示并不正确,请确保你在Debug\Location\Apple中所设定的位置信息准确无误。如果你更改了位置信息,下拉列表进行强制刷新,而不要等待位置触发器。
如果你是在启用了定位的iPhone或者iPad上调试,而且列表显示仍不正确,那仅仅是因为你这些Establishment的位置离你的当前位置还不够近。这里有两种解决方法:要么把示例数据的位置信息修改到你附近,要么是用模拟器来调试。其实这里还有一个实用性卓越的第三种解决方案——你可以跑到Cupertino去然后在苹果的草坪上散散步。
如果示例数据并未显示适当,或者根本不显示。用CloudKit仪表盘检查一下示例数据,首先确认你是否将它们都添加进了Default Zone之中,然后看一看它们的值是否正确。如果你想重新写入数据的话你可以像下图这样删除掉原来的数据:
有时候调试CloudKit,报错会相当狡猾。比如在写入的时候,CloudKit的报错并不会包含太多信息,如果你想判定错误原因的话你需要看一下错误代码已经你具体要做的数据库操作是什么样的。使用数值化的错误代码,然后再跟CKErrorCode对比来检错。文档中的标题和描述会对错误范围的缩小有所帮助,下面是一些例子:
提示:对于一些简单的错误,你可以在苹果的官方文档中有关CloudKit的章节里CKErrorCode枚举类型中找到。
以下是常见的错误类型,以及处理建议:
.BadContainer 和 .MissingEntitlement
检查一下在iCloud的Entitlements栏目中指定了与CKContainer对象匹配的容器,并且它存在于你的CloudKit仪表盘中。
.NotAuthenticated 和 .PermissionFailure
确保你在Settings.app中输入了正确的iCloud用户凭证,并确保iCloud可用。
.UnknownItem
检查CloudKit仪表盘中的记录类型名称与RecordType字串相匹配。
使用二进制资源
资源就是二进制数据,比如在你的记录中使用的图片。在这个例子中,你的应用资源就是那些将要展示在你附近的列表视图中的Establishment照片。
在这一节你将添加有关资源加载的逻辑,首先这些资源需要在你重获Establishment记录的时候已经下载好了。
打开Model\Establishment.swift文件并且使用下面代码替换掉loadCoverPhoto(completion:)方法:
func loadCoverPhoto(completion:(photo: UIImage!) -> ()) {
//虽然资源数据是在你获取记录的剩余内容时一起下载的,但是你并不想在同时显示这些图片。所以这里把所有的代码都包裹到了dispatch_async块中。
dispatch_async(
dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_BACKGROUND, )){
var image: UIImage!
//资源在CKRecord中作为CKAsset的一个实例来保存,以方便准确的转换
let coverPhoto = self.record.objectForKey("CoverPhoto") as CKAsset!
if let asset = coverPhoto {
//使用资源提供的本地文件URL来加载图片
if let url = asset.fileURL {
let imageData = NSData(contentsOfFile: url.path!)
//使用资源数据来构建一个UIImage实例
image = UIImage(data: imageData)
}
}
//使用获取的image来运行completion回调函数
completion(photo: image)
}
}
构建并运行,Establishment图片应该可以显示了:
下面是有关CloudKit资源我已经给出的两点:
1.资源数据在CloudKit上只能以一个属性的形式存在于记录中,你不能单独的存储它们,删除一条记录也会删除掉相关的所有资源数据。
2.因为资源数据的获取是与记录剩余内容的获取在同时进行的,所以也会在性能上带来一定的负面影响,如果你的App会用到大量的资源,你应该将资源单独存储为另一种形式的记录。
更多
现在这个App可以在表格视图中下载到有关Establishment记录,并加载细节信息和图片了。你可以在最下方下载到一个完整的工程文件,然后对其进行如下扩展:
允许用户上传图片,记录,评论或者投诉。分享一些糟糕的经历会对用户有所帮助。
让用户使用地图来添加新的Establishment记录,这一功能添加到你的Model类后可作为一个关于在共享数据库或者私人数据库中存储记录的示例。
添加过滤盒搜索,Model类可用距离判定构建一个CKQuery,不过判定可以改进得更为复杂。CloudKit也支持文本查找和特征字串。
改善App和数据加载的性能。这个教程只是在一切准备就绪之后使用了便捷可用的方法来调用一些现成的操作。CKDatabase的实例基于NSOperation以提供了比起API执行更多得多的操作方法。在操作中(比如操作刚刚完成)你就可以接受数据而非同时进行。
使用缓存和同步以保证App能够离线使用并在联网之后就能立即更新到最新的内容。