Core Data with CloudKit(三)—— CloudKit 仪表台

本篇文章中,我们将一起研究CloudKit仪表台。

初识仪表台

使用CloudKit Dashboard需要开发者拥有 Apple Developer Program 账号,访问 https://icloud.developer.apple.com 即可使用。

image-20210808161150623

最近两年苹果对CloudKit 仪表台的布局做过较大的调整,上面的截图是 2021 年中时的样子。

仪表台主要分为三个部分:

  • 数据库(CloudKit Database

数据库Web客户端。涵盖管理SchemaRecordZone、用户权限、容器环境等功能。

  • 遥测(Telemetry

使用直观的可视化效果,深入了解应用程序的服务器端性能以及跨数据库和推送事件的利用率。

  • 日志(Logs

CloudKit 服务器生成实时和历史日志,记录并显示应用程序和服务器之间的交互。

在绝大多数使用Core Data with CloudKit的场景下,我们仅需要使用仪表板中极少数的功能(环境部署),但利用CloudKit Dashboard,我们可以更清楚的了解Core Data数据同步背后运作的一些机制。

健康笔记是我开发的一个iOS app,主要服务于有长期健康管理需求的人士。健康笔记提供了强大的自定义数据类型功能,可以满足记录生活中绝大多数的健康项目数据的需要。你可以为每个家庭成员创建各自的健康数据记录笔记,或者针对某个特定项目、特定时期创建对应的笔记。

推广

数据库(CloudKit Database)

image-20210808163319683

Core Data with CloudKit (一) —— 基础 中已经对CKContainerCKDababaseCKZoneCKSubscriptionCKRecord等基础对象做了简单的说明,本文还将介绍CloudKit的其他一些对象和功能。

环境

CloudKit为你的应用程序网络数据分别提供了开发环境(Develpment)和生产环境(Production)。

  • 开发环境

当你的项目仍处于开发阶段时,所有通过CloudKit产生的数据都只被保存开发环境中,只有开发团队的成员才能访问该环境中的数据。在开发环境中,你可以随时进行Schema结构调整、对Record Type的属性进行删除修改等操作。即使这些操作可能会引起不同版本之间数据冲突都没有问题(可以随时重置开发环境)。非常类似Core Data的应用程序上线前的状态,即使数据无法正常迁移,只需要删除重装 app 即可。通过开发环境,开发者可以在向用户提供CloudKit服务之前对应用程序进行充分的测试。

  • 生产环境

当应用程序完成开发并准备提交应用商店时,需要将开发环境的结构部署到生产环境(Deploy Schema Changes)。Schema一旦部署到生产环境,则意味着开发者不可以像在开发环境中那样随意对Schema进行修改,所有的修改都必须以向前兼容的方式进行。

原因非常简单,一旦应用程序上线,我们无法控制客户端的更新频率,也就是客户端可能存在任何的结构版本,为了能够让低版本的客户端一样可以访问数据,任何对数据模型的更改都需要向下兼容。

App Store上销售的应用程序只能访问生产环境。

即使开发者的开发者账户同个人iCloud账户一致,开发环境和生产环境也是两个不同的沙盒,数据是互不影响的。当使用Xcode调试程序时,应用只能访问开发环境,而通过TestflightApp Store下载的应用则只能访问生产环境。

在开发环境下,点击Deploy Schema Changes将开发环境的Schema部署到生产环境。

image-20210808180259192

部署时,会显示自上次部署后开发环境做出的修改。

即使Schema已经部署到生产环境后,我们仍可继续改动开发环境并部署到生产环境,如果模型无法满足兼容条件,CloudKit仪表台将会禁止你的部署行为。

image-20210808175543219

在容器名称下方会显示Schema是否已经部署到生产环境。上图是尚未部署的状态,下图是已经部署的状态。

image-20210808180421055image-20210808180014216

在做任何操作之前,要首先确认是否处于正确的环境设定中。

鉴于CloudKit的环境部署规则,在采用Core Data with CloudKit的项目中设计Core Data数据模型时一定要特别小心!。我个人的原则是可加、不减、不改。我将在下篇文章详细讨论该如何对Core Data with CloudKit数据模型做版本迁移。

安全角色(Security Roles)

安全角色仅适用于公共数据库。

CloudKit使用基于角色的访问控制(RBAC)来管理权限和控制对公共数据库中数据的访问(私有数据库对于应用程序的用户是唯一的)。通过CloudKit,你可以为一个角色设置权限级别,然后将该角色分配给一个给定的记录类型(Record Type)。

权限包括读、写、创建。读权限只允许读取记录,写权限允许读取和写入记录,而创建权限允许读取和写入记录以及创建新纪录。

CloudKit包含 3 个预设角色,分别为 World(_world)、Authenticated(_icloud)和 Creator(_creator)。World 表示任何人,无论其是否为 iCloud 用户。Authenticated 适用于任何经过验证的 iCloud 用户。Creator 则是作为记录(Record)的创建者。

image-20210808210401070

默认的设置为,任何人都可以读取数据,只有经过验证的 iCloud 用户才可以创建新纪录,记录的创建者可以更新自己的记录。

image-20210809062640040

我们可以创建自定义安全角色,但是不能创建用户记录(User Record),当用户第一次对容器进行身份验证时时系统会为该用户创建用户记录。我们可以查找现有用户并将其分配给任意的自定义的角色。

安全角色是数据模型(Schema)的一部分,每当开发者修改了安全设置后,需要将其部署到生产环境才能在生产环境生效。部署后无法删除安全角色。

大多数Core Data with CloudKit应用场合,直接使用系统的默认配置即可。

索引(Indexes)

CloudKit的索引分为三种类型:

  • 可查询(queryable
  • 可搜索(searchable
  • 可排序(sortable

当我们通过CloudKit创建Recored Type后,可以根据需要为每个字段创建所需的索引(只有NSString支持可搜索)。索引类型选项是独立的,如果你希望该字段既可查询又可排序,则需要分别创建两个索引。

image-20210809064449042

只有为Record TyperecordName创建了queryable索引后,才可以在Records中浏览该 Type 的数据。

image-20210809065509228image-20210809064743215

Core Data with CloudKit会自动为Core Data数据模型的每个属性在CloudKit上创建需要的索引(不包含recordName)。除非你需要在CloudKit仪表台上浏览数据,否则我们不需要对索引做任何添加。

Record Types

Record Type是开发人员为CKRecord指定的类型标识符。你可以直接在代码中创建它,也可以在CloudKit仪表盘上对其进行创建、修改。

image-20210809073043092

基础篇 中曾提到Entity相较Record Type拥有更多的配置信息,但Record Type也有一个Enitity没有的特性——元数据。

image-20210809075124786

CloudKit为每一个Record Type预设了若干元数据字段(即使开发者没有创建任何其他字段),每条数据记录(CKRecord)都会包含这些信息,其中绝大多数都是系统自动设定的。

  • createdTimestamp

CloudKit首次将记录保存到服务器的时间

  • createUserRecordName

_creator的用户记录,该记录保存在Users(系统创建)中,每当用户第一次对容器进行身份验证时时系统会为该用户创建用户记录

  • _etag

版本令牌。每次CloudKit保存记录时,都会将该记录更新为新值。用于比较网络和本地数据的版本

  • modifiedTimestamp

CloudKit 更新记录的最近时间

  • modifiedUserRecordName

最后更新数据的用户记录

  • recordName

记录的唯一 ID。在创建CKRcord时创建,通常会设置为UUID字符串

对于一些特殊类型的Record Type,系统还会增加一些针对性的元数据,比如role,cloud.shared

本文的主题为Core Data with CloudKit,因此让我们来看一下NSPersistentCloudKitContainer是如何将Core Data托管对象的属性转换成CloudKitRecore Type字段的。

image-20210809104558352image-20210809104402659

上图是我们在 同步本地数据库到 iCloud 私有数据库 中模版项目ItemCloudKit对应的Record TypeCloudKit会自动为托管对象实体的每个属性创字段,将属性名称映射到了具有CD_[attribute.name]键名的字段。该字段的类型在Core DataCloudKit之间可能也会有所不同。Record Type名称为CD_[entity]。一切的操作都是由系统自动完成的,我们无需干预。另外,还会为Enitity生成一个CD_entityName的字段,内容为Entity的类映射名。

这些以CD_为前缀的字符串,在数据同步过程中将不断出现在控制台上,了解了它的构成对调试代码有一定帮助。

Record Type部署到生产环境后,字段不可以删除,字段名称也不可以修改。因此一些Core Data中的操作在Core Data with CloudKit中是不允许的。

不要对已经上线的应用程序数据模型的Entity进行更名,也不要对Attribute更名,即使使用 Mapping Model、Renaming ID 都是不行的。在开发阶段如果需要更名的话,可能需要删除 app 重装并重置CloudKit的开发环境。

Zones

每个种类的数据库都有默认Zone,只有私有数据库可以自定义Zone

image-20210809143010363

对于私有数据库中的数据,在创建CKRecord时可以为数据指定Zone

let zone = CKRecordZone(zoneName: "myZone")
let newStudent = CKRecord(recordType: "Student",
                          recordID: CKRecord.ID(recordName: UUID().uuidString,
                                                zoneID: zone.zoneID))

NSPersistentCloudKitContainer在将托管对象转换成CKRecord时,将ZoneID统一设置为com.apple.coredata.cloudkit.zone。必须切换到正确的Zone才能浏览到数据。

image-20210809143648531
  • OWNER RECORD NAME

用户记录,对应Zone_creator

  • CHANGE TOKEN

比对令牌

  • ATOMIC

当 CloudKit 无法更新Zone中的一个或多个记录时,如果值为true则整个操作失败

Records

用于数据记录的浏览、创建、删除、更改、查询。

image-20210809150327144

在浏览数据时,需注意以下几点:

  • 选择正确的环境(开发环境和生产环境的数据完全不同)
  • 选择正确的DatabaseZone
  • 确认需要浏览的Record Type元数据recordName已经添加了queryable索引
  • 如果需要对字段进行排序或过滤,请给该字段创建对应的索引
  • 索引只有在部署后才会在生产环境下起作用

CloudKit仪表台中修改Core Data的镜像数据,客户端会立即收到远程通知并进行更新。不过并不推荐此种做法。

你也可以在代码中获取到Core Data托管对象对应的CKRecord

func getLastUserID(_ object:Item?) -> CKRecord.ID? {
    guard let item = object else {return nil}
    guard let ckRecord = PersistenceController.shared.container.record(for: item.objectID) else {return nil}
    guard let userID = ckRecord.lastModifiedUserRecordID else {
        print("can't get userID")
        return nil
    }
    return userID
}

上面的代码,将获取托管对象记录对应的CKRecord的最后修改用户

Subscriptions

浏览在容器上注册的CKSubscription

CKSubscription 是通过代码创建的,在仪表盘上只可以查看或删除。

比如下面的代码将创建一个CKQuerySubscription

        let predicate = NSPredicate(format: "name = 'bob'")
        let subscription = CKQuerySubscription(recordType: "Student",
                                               predicate: predicate,
                                               options: [.firesOnRecordCreation])
        let info = CKSubscription.NotificationInfo()
        info.alertLocalizationKey = "create a new bob"
        info.soundName = "NewAlert.aiff"
        info.shouldBadge = true
        info.alertBody = "hello world"

        subscription.notificationInfo = info

        publicDB.save(subscription) { subscription, error in
            if let error = error {
                print("error:\(error)")
            }
            guard let subscription = subscription else { return }
            print("save subscription successes:\(subscription)")
        }
image-20210809154503445

NSPersistentCloudKitContainer会为Core Data镜像的私有数据库注册一个CKDatabaseSubscription。当com.apple.coredata.cloudkit.zone数据更新时,会推送远程通知。

image-20210809154946576

Tokens&Keys

设置容器的 API 令牌。

image-20210809152554058

除了可以通过代码和CloudKit仪表台对数据进行操作外,苹果还提供了从网络或其他平台访问iCloud数据的手段。在获取令牌后,开发者还可以通过使用 CloudKit JSCloudKit Web 服务 与数据进行交互。

已有开发者利用以上服务,开发出可在其他平台访问 iCloud 数据的第三方库,比如 DroidNubeKit(在安卓上访问CloudKit)。

对于Core Data的网络镜像数据,除非你的数据模型足够简单,否则不推荐做这种尝试。CloudKit Web服务更适合直接通过Cloudkit创建的数据记录。

Sharing Fallbackd

为低版本操作系统(低于 iOS 10、macOS Sierra)提供数据记录共享回调支持。

遥测(Telemetry)

image-20210809161022705

通过查看 Telemetry 的指标,方便你在开发或更新应用程序时可视化性能。包括请求数量、错误数量、推送数量、服务器延迟以及平均请求大小等等。通过设定范围,仅显示与你相关的数据,帮助你更好地了解应用程序的流量配置及使用趋势。

日志(Logs)

image-20210809162346212

在历史日志中,你可以查看包括时间、客户端平台版本、用户(匿名)、事件、组织、细节等信息。

在提供详尽信息的基础上,CloudKit尽可能地保持用户数据的隐秘性。日志显示每个用户记录的服务器事件,但不暴露任何个人身份信息。仅显示匿名的、特定于容器的CloudKit用户。

AppStoreConnect的分析信息仅来自已同意与 App 开发者共享诊断和使用信息的用户,CloudKit日志信息则来自于你的应用程序中所有使用了CloudKit服务的用户。两者结合使用,可以获得更好的效果。

总结

大多数使用Core Data with CloudKit的场景,开发者基本无需使用CloudKit仪表盘。不过偶尔研究一下仪表盘上的数据,也是一种不错的乐趣。

下一篇文章,我们将聊一下开发Core Data with CloudKit项目经常会碰到的一些情况,比如调试、测试、数据迁移等。

本博客所有文章除特别声明外,均采用CC 4.0许可协议。转载请注明出处和作者。

关注微信公共号肘子的Swift记事本或在推特上关注@fatbobman,永远不会错过新内容! 您的支持和鼓励将为我的博客写作增添更多的动力! 如果您或身边的朋友有健康数据管理的需求,请使用我开发的app【健康笔记】,正是因为它我才创建了这个博客。

关注