MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift CloudKit云存储与同步

2023-08-072.6k 阅读

Swift CloudKit 简介

CloudKit 是苹果提供的一项云服务,允许开发者在应用程序中轻松实现云存储和数据同步功能。它为 iOS、iPadOS、macOS、watchOS 和 tvOS 应用提供了一个公共数据库,开发者可以利用该数据库来存储和检索数据。在 Swift 中使用 CloudKit,能够让应用的数据在不同设备间无缝同步,极大地提升用户体验。

CloudKit 提供了两种类型的数据库:公共数据库和私有数据库。公共数据库中的数据可供所有应用用户访问,而私有数据库中的数据仅对特定用户可见。这使得开发者可以根据应用需求灵活选择数据存储方式。

准备工作

在开始使用 CloudKit 之前,需要进行一些准备工作。

  1. 启用 CloudKit:在 Xcode 项目设置中,选择“Signing & Capabilities”标签,点击“+ Capability”,然后选择“CloudKit”,Xcode 会自动为项目添加必要的 entitlements。
  2. 配置 CloudKit 仪表板:登录到 Apple Developer 网站,进入 CloudKit 仪表板。在这里可以创建数据库、定义记录类型以及设置访问权限等。

基本数据模型

CloudKit 中的数据以记录(Record)的形式存储。每个记录都属于一种记录类型(Record Type),类似于数据库中的表。记录由字段(Field)组成,字段可以是各种数据类型,如字符串、数字、日期等。 例如,假设我们要创建一个简单的待办事项应用,我们可以定义一个“Todo”记录类型,包含“title”(标题)、“isCompleted”(是否完成)和“dueDate”(截止日期)等字段。

保存数据到 CloudKit

在 Swift 中,使用 CloudKit 保存数据主要涉及 CKRecordCKContainer 这两个类。

import CloudKit

let container = CKContainer.default()
let privateDatabase = container.privateCloudDatabase

// 创建一个新的 CKRecord
let todoRecord = CKRecord(recordType: "Todo")
todoRecord.setValue("Buy groceries", forKey: "title")
todoRecord.setValue(false, forKey: "isCompleted")
todoRecord.setValue(Date(), forKey: "dueDate")

// 保存记录到私有数据库
privateDatabase.save(todoRecord) { (savedRecord, error) in
    if let error = error {
        print("Error saving record: \(error)")
    } else if let savedRecord = savedRecord {
        print("Record saved successfully: \(savedRecord.recordID)")
    }
}

在上述代码中,首先获取默认的 CKContainer 及其私有数据库。然后创建一个新的 CKRecord,设置相应的字段值。最后通过 privateDatabasesave 方法将记录保存到数据库,并在完成闭包中处理保存结果。

从 CloudKit 检索数据

从 CloudKit 检索数据需要使用 CKQueryCKQuery 允许指定查询条件来筛选符合要求的记录。

let query = CKQuery(recordType: "Todo", predicate: NSPredicate(format: "isCompleted == false"))

privateDatabase.perform(query, inZoneWith: nil) { (results, error) in
    if let error = error {
        print("Error performing query: \(error)")
    } else if let results = results {
        for record in results {
            if let title = record.value(forKey: "title") as? String {
                print("Todo: \(title)")
            }
        }
    }
}

在这段代码中,创建了一个 CKQuery,查询“Todo”记录类型中“isCompleted”字段为 false 的记录。然后通过 privateDatabaseperform 方法执行查询,并在完成闭包中处理查询结果。

数据同步

CloudKit 能够自动处理数据在不同设备间的同步。当应用在一台设备上保存或修改数据时,CloudKit 会将这些更改同步到其他设备。 为了更好地处理同步状态,开发者可以监听 CloudKit 的通知。例如,可以监听 CKDatabaseDidChangeExternallyNotification 通知,当数据库外部发生更改(如在其他设备上进行了修改)时,应用可以相应地更新本地数据。

NotificationCenter.default.addObserver(forName: .CKDatabaseDidChangeExternally, object: privateDatabase, queue: .main) { (notification) in
    print("Database changed externally. Refreshing data...")
    // 在这里重新执行查询以更新本地数据
}

处理复杂数据类型

除了基本数据类型,CloudKit 还支持一些复杂数据类型,如 CKAsset(用于存储文件)和 CKReference(用于建立记录之间的关系)。

使用 CKAsset 存储文件

假设我们要在待办事项应用中为每个待办事项添加一个附件(如图片)。

// 假设已经有一个 UIImage
let image = UIImage(named: "exampleImage")
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("exampleImage.jpg")
try? image?.jpegData(compressionQuality: 0.8)?.write(to: fileURL)

let asset = CKAsset(fileURL: fileURL)
todoRecord.setValue(asset, forKey: "attachment")

privateDatabase.save(todoRecord) { (savedRecord, error) in
    if let error = error {
        print("Error saving record with asset: \(error)")
    } else if let savedRecord = savedRecord {
        print("Record with asset saved successfully: \(savedRecord.recordID)")
    }
}

在上述代码中,先将 UIImage 保存到临时文件路径,然后创建一个 CKAsset 并将其关联到 todoRecord 的“attachment”字段,最后保存记录。

使用 CKReference 建立记录关系

假设我们有另一个记录类型“Category”,每个“Todo”记录属于一个“Category”记录。

// 假设已经有一个 categoryRecord
let categoryRecord = CKRecord(recordType: "Category")
categoryRecord.setValue("Shopping", forKey: "name")

privateDatabase.save(categoryRecord) { (savedCategoryRecord, error) in
    if let error = error {
        print("Error saving category record: \(error)")
    } else if let savedCategoryRecord = savedCategoryRecord {
        let reference = CKReference(recordID: savedCategoryRecord.recordID, action: .deleteSelf)
        todoRecord.setValue(reference, forKey: "category")

        privateDatabase.save(todoRecord) { (savedTodoRecord, error) in
            if let error = error {
                print("Error saving todo record with reference: \(error)")
            } else if let savedTodoRecord = savedTodoRecord {
                print("Todo record with reference saved successfully: \(savedTodoRecord.recordID)")
            }
        }
    }
}

在这段代码中,先保存“Category”记录,然后创建一个 CKReference 指向该“Category”记录,并将其关联到“Todo”记录的“category”字段,最后保存“Todo”记录。

权限管理

CloudKit 提供了灵活的权限管理机制。在 CloudKit 仪表板中,可以为不同的记录类型和字段设置公共和私有访问权限。 例如,可以设置“Todo”记录类型的“title”字段在公共数据库中可读,但不可写。这样,所有用户都可以查看待办事项标题,但只有记录所有者可以修改。 对于私有数据库,默认情况下只有记录所有者可以访问和修改数据。然而,开发者可以通过创建共享记录(Shared Record)并设置合适的权限,来实现数据的共享。

// 创建一个共享记录
let sharedTodoRecord = CKRecord(recordType: "Todo")
sharedTodoRecord.setValue("Shared task", forKey: "title")

let share = CKShare(rootRecord: sharedTodoRecord)
share.publicPermission = .readWrite

privateDatabase.save(share) { (savedShare, error) in
    if let error = error {
        print("Error saving share: \(error)")
    } else if let savedShare = savedShare {
        print("Share saved successfully: \(savedShare.shareID)")
    }
}

在上述代码中,创建了一个共享记录,并设置其公共权限为可读可写。然后将共享记录保存到私有数据库。

错误处理

在使用 CloudKit 过程中,可能会遇到各种错误。常见的错误包括网络问题、权限不足、记录不存在等。 在前面的代码示例中,我们已经通过完成闭包中的 error 参数来处理一些基本的错误情况。对于更复杂的错误处理,可以根据 error 的具体类型进行针对性处理。

privateDatabase.save(todoRecord) { (savedRecord, error) in
    if let error = error {
        if let ckError = error as? CKError {
            switch ckError.code {
            case .networkFailure:
                print("Network failure. Please check your connection.")
            case .permissionFailure:
                print("Permission failure. Check your CloudKit permissions.")
            default:
                print("Other CloudKit error: \(ckError)")
            }
        } else {
            print("Unknown error: \(error)")
        }
    } else if let savedRecord = savedRecord {
        print("Record saved successfully: \(savedRecord.recordID)")
    }
}

在这段代码中,先将 error 转换为 CKError 类型,然后根据 CKErrorcode 判断错误类型,并进行相应的处理。

高级主题

批量操作

CloudKit 支持批量操作,以提高数据处理效率。可以使用 CKModifyRecordsOperation 来批量保存或删除记录。

let record1 = CKRecord(recordType: "Todo")
record1.setValue("Task 1", forKey: "title")

let record2 = CKRecord(recordType: "Todo")
record2.setValue("Task 2", forKey: "title")

let recordsToSave = [record1, record2]

let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil)
operation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, error) in
    if let error = error {
        print("Error in batch operation: \(error)")
    } else if let savedRecords = savedRecords {
        for record in savedRecords {
            print("Record saved in batch: \(record.recordID)")
        }
    }
}

privateDatabase.add(operation)

在上述代码中,创建了两个待办事项记录,并使用 CKModifyRecordsOperation 将它们批量保存到数据库。

区域(Zone)

CloudKit 中的区域是一种组织数据的方式。每个数据库可以包含多个区域。默认情况下,所有记录都存储在默认区域。 通过使用区域,可以更好地组织和管理数据,例如按用户或按数据类型进行分区。

let customZone = CKRecordZone(zoneID: CKRecordZone.ID(zoneName: "CustomZone", ownerName: CKCurrentUserDefaultName))

privateDatabase.save(customZone) { (savedZone, error) in
    if let error = error {
        print("Error saving custom zone: \(error)")
    } else if let savedZone = savedZone {
        print("Custom zone saved successfully: \(savedZone.zoneID)")

        let todoRecordInCustomZone = CKRecord(recordType: "Todo", zoneID: savedZone.zoneID)
        todoRecordInCustomZone.setValue("Task in custom zone", forKey: "title")

        privateDatabase.save(todoRecordInCustomZone) { (savedRecord, error) in
            if let error = error {
                print("Error saving record in custom zone: \(error)")
            } else if let savedRecord = savedRecord {
                print("Record saved in custom zone successfully: \(savedRecord.recordID)")
            }
        }
    }
}

在这段代码中,先创建一个自定义区域并保存到私有数据库,然后在该自定义区域中创建并保存一个待办事项记录。

订阅(Subscription)

CloudKit 允许创建订阅,以便在特定条件满足时接收推送通知。例如,可以创建一个订阅,当某个用户的待办事项截止日期临近时发送通知。

let predicate = NSPredicate(format: "dueDate < %@ AND isCompleted == false AND owner == %@", NSDate(timeIntervalSinceNow: 86400), CKRecord.Reference(recordID: CKRecordZone.default().zoneID.ownerID!, action: .none))

let subscription = CKQuerySubscription(recordType: "Todo", predicate: predicate, options: [.firesOnRecordCreation, .firesOnRecordUpdate])

let notificationInfo = CKNotificationInfo()
notificationInfo.alertBody = "A todo is due soon!"
notificationInfo.soundName = "default"

subscription.notificationInfo = notificationInfo

privateDatabase.save(subscription) { (savedSubscription, error) in
    if let error = error {
        print("Error saving subscription: \(error)")
    } else if let savedSubscription = savedSubscription {
        print("Subscription saved successfully: \(savedSubscription.subscriptionID)")
    }
}

在上述代码中,创建了一个 CKQuerySubscription,设置了查询条件和通知信息,当满足条件的待办事项记录创建或更新时,会发送推送通知。

通过以上内容,详细介绍了 Swift 中 CloudKit 的云存储与同步功能,包括基本操作、复杂数据类型处理、权限管理、错误处理以及一些高级主题,希望能帮助开发者更好地利用 CloudKit 为应用添加强大的云存储和同步能力。