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

Objective-C中的CloudKit云服务集成

2021-05-141.4k 阅读

什么是CloudKit云服务

CloudKit 是苹果提供的一项云服务,允许开发者在自己的应用程序中轻松实现云存储和云同步功能。通过 CloudKit,应用可以在 iCloud 中存储和检索数据,使得用户在不同设备间使用应用时数据能够保持一致。它提供了一种灵活且可扩展的方式来管理应用的数据,无论是小型的个人应用还是大型的企业级应用,都能从中受益。

CloudKit 主要由两部分组成:公共数据库和私有数据库。公共数据库中的数据对所有使用该应用的用户都是可见的,适用于存储如应用配置信息、公共排行榜数据等。而私有数据库则只有当前用户可以访问,用于存储用户特定的数据,比如用户的个人设置、游戏进度等。

在Objective-C项目中集成CloudKit的前期准备

  1. 开启CloudKit功能 在 Xcode 项目设置中,首先要确保 CloudKit 功能已经开启。打开项目导航栏,选择项目文件,然后在 “Capabilities” 标签页中,找到 “CloudKit” 并将其开关打开。Xcode 会自动处理相关的 entitlements 文件配置,确保应用有权限访问 CloudKit 服务。

  2. 配置容器 CloudKit 使用容器来管理数据。默认情况下,Xcode 会为项目创建一个与应用 Bundle ID 相关联的容器。如果需要自定义容器,可以在 Apple Developer 网站的 “CloudKit Dashboard” 中进行设置。在 CloudKit Dashboard 中,可以管理容器的各个方面,包括数据库结构、权限设置等。

基本数据操作 - 创建记录

  1. 定义记录类型 在 CloudKit 中,数据以记录(CKRecord)的形式存储。首先需要定义记录类型,类似于数据库中的表结构。例如,假设我们要创建一个用于存储用户信息的记录类型 “UserProfile”,可以在 CloudKit Dashboard 中进行如下操作:
  • 登录 CloudKit Dashboard,选择对应的容器。
  • 在 “Schema” 标签页中,点击 “Add Record Type”,输入 “UserProfile” 作为记录类型名称。
  • 为 “UserProfile” 记录类型添加字段,比如 “name”(字符串类型)、“age”(数字类型)等。
  1. 使用Objective-C代码创建记录
#import <CloudKit/CloudKit.h>

// 创建一个新的用户记录
CKRecord *userRecord = [[CKRecord alloc] initWithRecordType:@"UserProfile"];
userRecord[@"name"] = @"John Doe";
userRecord[@"age"] = @25;

CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

[privateDatabase saveRecord:userRecord completionHandler:^(CKRecord * _Nullable savedRecord, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error saving record: %@", error);
    } else {
        NSLog(@"Record saved successfully with ID: %@", savedRecord.recordID);
    }
}];

在上述代码中,首先创建了一个 CKRecord 实例,设置了其记录类型为 “UserProfile” 并为字段赋值。然后获取应用的私有数据库,通过 saveRecord:completionHandler: 方法将记录保存到 CloudKit 中。完成处理程序会在保存操作结束后被调用,根据 error 是否为 nil 判断保存是否成功。

基本数据操作 - 查询记录

  1. 简单查询 假设我们要查询所有年龄大于 18 岁的用户记录,可以使用如下代码:
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

CKQuery *query = [[CKQuery alloc] initWithRecordType:@"UserProfile" predicate:[NSPredicate predicateWithFormat:@"age > 18"]];

[privateDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error querying records: %@", error);
    } else {
        for (CKRecord *record in results) {
            NSString *name = record[@"name"];
            NSNumber *age = record[@"age"];
            NSLog(@"User: %@, Age: %@", name, age);
        }
    }
}];

这里创建了一个 CKQuery 对象,指定记录类型为 “UserProfile” 并设置了查询条件谓词 age > 18。然后通过 performQuery:inZoneWithID:completionHandler: 方法执行查询操作,完成处理程序中会返回查询结果数组,遍历数组可以获取每条记录的字段值。

  1. 复杂查询 - 多条件组合 如果要查询年龄大于 18 岁且名字以 “J” 开头的用户记录,可以使用如下代码:
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

NSPredicate *agePredicate = [NSPredicate predicateWithFormat:@"age > 18"];
NSPredicate *namePredicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH 'J'"];
NSPredicate *compoundPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[agePredicate, namePredicate]];

CKQuery *query = [[CKQuery alloc] initWithRecordType:@"UserProfile" predicate:compoundPredicate];

[privateDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error querying records: %@", error);
    } else {
        for (CKRecord *record in results) {
            NSString *name = record[@"name"];
            NSNumber *age = record[@"age"];
            NSLog(@"User: %@, Age: %@", name, age);
        }
    }
}];

这段代码中,先分别创建了年龄和名字的谓词,然后通过 NSCompoundPredicate 将两个谓词组合成一个复合谓词,再用于查询操作。

基本数据操作 - 更新记录

假设我们要更新某个用户的年龄,可以先查询到该用户记录,然后更新其字段值并保存。

CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

// 假设已知要更新记录的recordID
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"yourRecordName"];

[privateDatabase fetchRecordWithID:recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error fetching record: %@", error);
    } else {
        record[@"age"] = @30; // 更新年龄字段
        [privateDatabase saveRecord:record completionHandler:^(CKRecord * _Nullable savedRecord, NSError * _Nullable error) {
            if (error) {
                NSLog(@"Error saving updated record: %@", error);
            } else {
                NSLog(@"Record updated successfully with ID: %@", savedRecord.recordID);
            }
        }];
    }
}];

在代码中,首先通过 fetchRecordWithID:completionHandler: 方法获取到要更新的记录,然后修改其字段值,最后再次调用 saveRecord:completionHandler: 方法保存更新后的记录。

基本数据操作 - 删除记录

删除记录相对简单,只需要知道要删除记录的 CKRecordID 即可。

CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"yourRecordName"];

[privateDatabase deleteRecordWithID:recordID completionHandler:^(CKRecordID * _Nullable deletedRecordID, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error deleting record: %@", error);
    } else {
        NSLog(@"Record deleted successfully with ID: %@", deletedRecordID);
    }
}];

上述代码通过 deleteRecordWithID:completionHandler: 方法删除指定 CKRecordID 的记录,并在完成处理程序中处理删除操作的结果。

处理CloudKit中的关系

  1. 定义关系 假设我们有两个记录类型 “UserProfile” 和 “Post”,一个用户可以有多个帖子,我们可以在 CloudKit Dashboard 中为 “UserProfile” 记录类型添加一个 “posts” 字段,类型选择 “Reference”,指向 “Post” 记录类型。这样就建立了用户和帖子之间的一对多关系。

  2. 在代码中设置关系

// 创建一个新的帖子记录
CKRecord *postRecord = [[CKRecord alloc] initWithRecordType:@"Post"];
postRecord[@"title"] = @"New Post";

// 假设已经有一个用户记录 userRecord
postRecord[@"user"] = [CKRecordReference referenceWithRecordID:userRecord.recordID action:CKRecordReferenceActionDeleteSelf];

CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

[privateDatabase saveRecord:postRecord completionHandler:^(CKRecord * _Nullable savedRecord, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error saving post record: %@", error);
    } else {
        // 将帖子记录的引用添加到用户记录的 posts 字段
        NSMutableArray *posts = [userRecord[@"posts"] mutableCopy];
        if (!posts) {
            posts = [NSMutableArray array];
        }
        [posts addObject:[CKRecordReference referenceWithRecordID:savedRecord.recordID action:CKRecordReferenceActionDeleteSelf]];
        userRecord[@"posts"] = posts;

        [privateDatabase saveRecord:userRecord completionHandler:^(CKRecord * _Nullable savedUserRecord, NSError * _Nullable error) {
            if (error) {
                NSLog(@"Error saving updated user record: %@", error);
            } else {
                NSLog(@"User record updated with new post successfully.");
            }
        }];
    }
}];

在这段代码中,首先创建了一个新的帖子记录,并设置其 “user” 字段为对用户记录的引用。保存帖子记录成功后,将帖子记录的引用添加到用户记录的 “posts” 数组字段中,并再次保存用户记录。

  1. 查询具有关系的记录 要查询某个用户的所有帖子,可以使用如下代码:
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

// 假设已知用户记录的recordID
CKRecordID *userRecordID = [[CKRecordID alloc] initWithRecordName:@"yourUserRecordName"];

CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:@[userRecordID]];
fetchOperation.desiredKeys = @[@"posts"];

fetchOperation.perRecordCompletionBlock = ^(CKRecord * _Nullable record, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error fetching user record: %@", error);
    } else {
        NSArray *postReferences = record[@"posts"];
        NSMutableArray *postRecordIDs = [NSMutableArray array];
        for (CKRecordReference *reference in postReferences) {
            [postRecordIDs addObject:reference.recordID];
        }

        CKFetchRecordsOperation *postFetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:postRecordIDs];
        postFetchOperation.perRecordCompletionBlock = ^(CKRecord * _Nullable postRecord, NSError * _Nullable error) {
            if (error) {
                NSLog(@"Error fetching post record: %@", error);
            } else {
                NSString *title = postRecord[@"title"];
                NSLog(@"Post Title: %@", title);
            }
        };

        [privateDatabase addOperation:postFetchOperation];
    }
};

[privateDatabase addOperation:fetchOperation];

这段代码首先通过 CKFetchRecordsOperation 获取用户记录及其 “posts” 字段(即帖子记录的引用)。然后从引用中提取帖子记录的 CKRecordID,再通过另一个 CKFetchRecordsOperation 获取每个帖子记录的详细信息。

处理CloudKit中的订阅

  1. 创建订阅 订阅允许应用在数据发生变化时收到通知。例如,我们可以创建一个订阅,当某个用户的帖子有新评论时通知应用。
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

// 创建一个查询,用于指定订阅的范围
CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Comment" predicate:[NSPredicate predicateWithFormat:@"post = %@", [CKRecordReference referenceWithRecordID:postRecordID action:CKRecordReferenceActionNone]]];

CKSubscription *subscription = [[CKSubscription alloc] initWithQuery:query subscriptionID:@"NewCommentSubscription" options:CKSubscriptionOptionsFiresOnRecordCreation];

CKNotificationInfo *notificationInfo = [[CKNotificationInfo alloc] init];
notificationInfo.title = @"New Comment";
notificationInfo.body = @"You have a new comment on your post.";
notificationInfo.shouldBadge = YES;
subscription.notificationInfo = notificationInfo;

[privateDatabase saveSubscription:subscription completionHandler:^(CKSubscription * _Nullable savedSubscription, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error saving subscription: %@", error);
    } else {
        NSLog(@"Subscription saved successfully with ID: %@", savedSubscription.subscriptionID);
    }
}];

在上述代码中,首先创建了一个查询,指定了订阅针对的 “Comment” 记录类型且与特定帖子相关。然后创建了一个 CKSubscription,设置了订阅 ID 和触发选项。接着创建了 CKNotificationInfo 来设置通知的标题、内容和是否显示应用图标徽章等信息,并将其关联到订阅。最后通过 saveSubscription:completionHandler: 方法保存订阅。

  1. 处理推送通知 当 CloudKit 触发订阅通知时,应用需要处理推送通知。在 Objective-C 中,首先要在 AppDelegate 中注册推送通知:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 注册推送通知
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    } else {
        UIRemoteNotificationType types = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound;
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types];
    }
    return YES;
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    // 将设备令牌发送到 CloudKit
    CKContainer *container = [CKContainer defaultContainer];
    [container registerDeviceWithToken:deviceToken completionHandler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Error registering device with CloudKit: %@", error);
        } else {
            NSLog(@"Device registered with CloudKit successfully.");
        }
    }];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    // 处理 CloudKit 推送通知
    if ([[userInfo objectForKey:@"ck"] isKindOfClass:[NSDictionary class]]) {
        NSDictionary *ckInfo = [userInfo objectForKey:@"ck"];
        NSString *subscriptionID = [ckInfo objectForKey:@"subscriptionID"];
        if ([subscriptionID isEqualToString:@"NewCommentSubscription"]) {
            // 处理新评论通知逻辑
            NSLog(@"Received new comment notification.");
        }
    }
}

在上述代码中,didFinishLaunchingWithOptions: 方法注册了应用的推送通知设置。didRegisterForRemoteNotificationsWithDeviceToken: 方法将设备令牌发送到 CloudKit 进行注册。didReceiveRemoteNotification: 方法在收到推送通知时,检查是否是 CloudKit 相关的通知,并根据订阅 ID 处理特定的通知逻辑。

管理CloudKit中的权限

  1. 公共数据库权限 在 CloudKit Dashboard 中,可以设置公共数据库的权限。例如,要允许所有用户读取公共数据库中的 “Announcement” 记录类型,可以进行如下操作:
  • 登录 CloudKit Dashboard,选择对应的容器。
  • 在 “Schema” 标签页中,找到 “Announcement” 记录类型。
  • 点击 “Permissions”,设置 “Everyone” 的读取权限为 “Yes”。
  1. 私有数据库权限 私有数据库默认只有当前用户可以访问。但是,可以通过代码为特定用户授予对某些记录的共享权限。例如,假设我们要将某个用户记录共享给另一个用户:
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

// 假设已知要共享记录的recordID
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"yourRecordName"];

[privateDatabase fetchRecordWithID:recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error fetching record: %@", error);
    } else {
        CKShare *share = [[CKShare alloc] initWithRootRecord:record];
        share.publicPermission = CKSharePermissionReadWrite;

        // 假设已知要共享给的用户的userIdentity
        CKUserIdentity *recipientIdentity = [[CKUserIdentity alloc] initWithUserRecordID:recipientUserRecordID];
        [share addParticipantWithUserIdentity:recipientIdentity permission:CKSharePermissionReadWrite];

        [privateDatabase saveShare:share completionHandler:^(CKShare * _Nullable savedShare, NSError * _Nullable error) {
            if (error) {
                NSLog(@"Error saving share: %@", error);
            } else {
                NSLog(@"Share saved successfully with ID: %@", savedShare.shareID);
            }
        }];
    }
}];

在这段代码中,首先获取要共享的记录,然后创建一个 CKShare 对象并设置其根记录为要共享的记录。接着设置公共权限为读写,并添加要共享给的用户的参与者信息及权限。最后通过 saveShare:completionHandler: 方法保存共享设置。

CloudKit集成中的常见问题及解决方法

  1. 网络问题 由于 CloudKit 依赖网络连接,网络不稳定可能导致数据操作失败。在代码中,可以通过检查网络状态并进行适当的重试来解决。例如,可以使用 Reachability 库来检测网络状态:
#import "Reachability.h"

- (void)saveRecordWithRetry:(CKRecord *)record {
    Reachability *reachability = [Reachability reachabilityForInternetConnection];
    NetworkStatus networkStatus = [reachability currentReachabilityStatus];
    if (networkStatus != NotReachable) {
        CKContainer *container = [CKContainer defaultContainer];
        CKDatabase *privateDatabase = [container privateCloudDatabase];

        [privateDatabase saveRecord:record completionHandler:^(CKRecord * _Nullable savedRecord, NSError * _Nullable error) {
            if (error) {
                if ([error code] == CKErrorNetworkUnavailable || [error code] == CKErrorNetworkFailure) {
                    // 网络问题,进行重试
                    [self performSelector:@selector(saveRecordWithRetry:) withObject:record afterDelay:5];
                } else {
                    NSLog(@"Other error saving record: %@", error);
                }
            } else {
                NSLog(@"Record saved successfully with ID: %@", savedRecord.recordID);
            }
        }];
    } else {
        NSLog(@"Network is not available. Cannot save record.");
    }
}

上述代码在保存记录前先检查网络状态,如果网络可用则进行保存操作,若因网络问题保存失败则在 5 秒后重试。

  1. 数据一致性问题 在多设备环境下,可能会出现数据一致性问题。为了确保数据一致性,CloudKit 使用了版本控制。每次记录更新时,其版本号会增加。在读取记录时,可以获取其版本号,并在更新记录时提供当前版本号,以确保更新的是最新版本。
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

// 假设已知要更新记录的recordID
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"yourRecordName"];

[privateDatabase fetchRecordWithID:recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error fetching record: %@", error);
    } else {
        CKRecordID *versionedRecordID = [[CKRecordID alloc] initWithRecordName:record.recordID.recordName zoneID:record.recordID.zoneID];
        versionedRecordID.versionalID = record.versionalID;

        record[@"age"] = @30; // 更新年龄字段

        [privateDatabase saveRecord:record recordID:versionedRecordID completionHandler:^(CKRecord * _Nullable savedRecord, NSError * _Nullable error) {
            if (error) {
                if ([error code] == CKErrorServerRecordChanged) {
                    // 记录已被其他设备更新,重新获取并更新
                    [self performSelector:@selector(updateRecord) withObject:nil afterDelay:0];
                } else {
                    NSLog(@"Error saving updated record: %@", error);
                }
            } else {
                NSLog(@"Record updated successfully with ID: %@", savedRecord.recordID);
            }
        }];
    }
}];

在这段代码中,获取记录时同时获取其版本号,更新记录时使用带版本号的 CKRecordID。如果保存时出现 CKErrorServerRecordChanged 错误,说明记录已被其他设备更新,此时重新获取并更新记录。

  1. 配额问题 CloudKit 为每个应用提供了一定的存储配额。如果应用超出配额,数据操作可能会失败。可以在 CloudKit Dashboard 中查看应用的配额使用情况。为了避免超出配额,可以采取一些策略,比如定期清理不再使用的数据。
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDatabase = [container privateCloudDatabase];

// 查询并删除超过一定时间的记录
NSDate *oneMonthAgo = [NSDate dateWithTimeIntervalSinceNow:-2592000]; // 一个月前
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"creationDate < %@", oneMonthAgo];
CKQuery *query = [[CKQuery alloc] initWithRecordType:@"OldRecordType" predicate:predicate];

[privateDatabase performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
    if (error) {
        NSLog(@"Error querying old records: %@", error);
    } else {
        for (CKRecord *record in results) {
            [privateDatabase deleteRecordWithID:record.recordID completionHandler:^(CKRecordID * _Nullable deletedRecordID, NSError * _Nullable error) {
                if (error) {
                    NSLog(@"Error deleting old record: %@", error);
                } else {
                    NSLog(@"Old record deleted successfully with ID: %@", deletedRecordID);
                }
            }];
        }
    }
}];

上述代码查询并删除了创建时间超过一个月的 “OldRecordType” 记录类型的记录,以释放存储空间,避免超出配额。

通过以上详细的介绍和代码示例,相信开发者能够在 Objective - C 项目中熟练地集成 CloudKit 云服务,充分利用其强大的功能为应用带来更好的用户体验和数据管理能力。无论是简单的数据存储与同步,还是复杂的关系处理、订阅通知以及权限管理,CloudKit 都提供了丰富的接口和灵活的配置方式,使得开发者能够轻松应对各种需求。同时,在集成过程中注意处理常见问题,确保应用的稳定性和数据的一致性。