来自 CloudKit 的推送通知未正确同步
Push Notification from CloudKit doesn't synchronize properly
我正在尝试在 IOS/Swift 中与我 CloudKit 创建一个简单的聊天。我在这个例子之后建模:Create an App like Twitter: Push Notifications with CloudKit 但将其更改为聊天而不是 Sweets。
代码的横幅和徽章在某种程度上运行良好,将数据推送到 CloudDashboard 既好又快。
但是从 cloudKit 到设备的同步大部分时间都不起作用。有时一个设备比另一个设备看到更多,有时更少,只是不太可靠。我在 CloudKit 中使用 DEVELOPMENT 环境。
有什么问题?这是我在 appDelegate 和 viewController:
中实现方法的代码
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let notificationSettings = UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)
UIApplication.sharedApplication().registerForRemoteNotifications()
return true
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
let cloudKitNotification = CKNotification(fromRemoteNotificationDictionary: userInfo as! [String:NSObject])
if cloudKitNotification.notificationType == CKNotificationType.Query {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().postNotificationName("performReload", object: nil)
})
}
}
func resetBadge () {
let badgeReset = CKModifyBadgeOperation(badgeValue: 0)
badgeReset.modifyBadgeCompletionBlock = { (error) -> Void in
if error == nil {
UIApplication.sharedApplication().applicationIconBadgeNumber = 0
}
}
CKContainer.defaultContainer().addOperation(badgeReset)
}
func applicationWillResignActive(application: UIApplication) {
}
func applicationDidEnterBackground(application: UIApplication) {
resetBadge()
}
func applicationWillEnterForeground(application: UIApplication) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().postNotificationName("performReload", object: nil)
})
}
func applicationDidBecomeActive(application: UIApplication) {
resetBadge()
}
这是 viewController
import UIKit
import CloudKit
class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
@IBOutlet weak var dockViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var messageTextField: UITextField!
@IBOutlet weak var sendButton: UIButton!
@IBOutlet weak var messageTableView: UITableView!
var chatMessagesArray = [CKRecord]()
var messagesArray: [String] = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.messageTableView.delegate = self
self.messageTableView.dataSource = self
// set self as the delegate for the textfield
self.messageTextField.delegate = self
// add a tap gesture recognizer to the tableview
let tapGesture:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ChatViewController.tableViewTapped))
self.messageTableView.addGestureRecognizer(tapGesture)
setupCloudKitSubscription()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChatViewController.retrieveMessages), name: "performReload", object: nil)
})
// retrieve messages form iCloud
self.retrieveMessages()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func sendButtonTapped(sender: UIButton) {
// Call the end editing method for the text field
self.messageTextField.endEditing(true)
// Disable the send button and textfield
self.messageTextField.enabled = false
self.sendButton.enabled = false
// create a cloud object
//var newMessageObject
// set the text key to the text of the messageTextField
// save the object
if messageTextField.text != "" {
let newChat = CKRecord(recordType: "Chat")
newChat["content"] = messageTextField.text
newChat["user1"] = "john"
newChat["user2"] = "mark"
let publicData = CKContainer.defaultContainer().publicCloudDatabase
//TODO investigate if we want to do public or private
publicData.saveRecord(newChat, completionHandler: { (record:CKRecord?, error:NSError?) in
if error == nil {
dispatch_async(dispatch_get_main_queue(), {() -> Void in
print("chat saved")
self.retrieveMessages()
})
}
})
}
dispatch_async(dispatch_get_main_queue()) {
// Enable the send button and textfield
self.messageTextField.enabled = true
self.sendButton.enabled = true
self.messageTextField.text = ""
}
}
func retrieveMessages() {
print("inside retrieve messages")
// create a new cloud query
let publicData = CKContainer.defaultContainer().publicCloudDatabase
// TODO: we should use this
let predicate = NSPredicate(format: "user1 in %@ AND user2 in %@", ["john", "mark"], ["john", "mark"])
let query = CKQuery(recordType: "Chat", predicate: predicate)
//let query = CKQuery(recordType: "Chat", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
query.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: true)]
publicData.performQuery(query, inZoneWithID: nil) { (results: [CKRecord]?, error:NSError?) in
if let chats = results {
dispatch_async(dispatch_get_main_queue(), {() -> Void in
self.chatMessagesArray = chats
print("count is: \(self.chatMessagesArray.count)")
self.messageTableView.reloadData()
})
}
}
}
func tableViewTapped () {
// Force the textfied to end editing
self.messageTextField.endEditing(true)
}
// MARK: TextField Delegate Methods
func textFieldDidBeginEditing(textField: UITextField) {
// perform an animation to grow the dockview
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5, animations: {
self.dockViewHeightConstraint.constant = 350
self.view.layoutIfNeeded()
}, completion: nil)
}
func textFieldDidEndEditing(textField: UITextField) {
// perform an animation to grow the dockview
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5, animations: {
self.dockViewHeightConstraint.constant = 60
self.view.layoutIfNeeded()
}, completion: nil)
}
// MARK: TableView Delegate Methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Create a table cell
let cell = self.messageTableView.dequeueReusableCellWithIdentifier("MessageCell")! as UITableViewCell
// customize the cell
let chat = self.chatMessagesArray[indexPath.row]
if let chatContent = chat["content"] as? String {
let dateFormat = NSDateFormatter()
dateFormat.dateFormat = "MM/dd/yyyy"
let dateString = dateFormat.stringFromDate(chat.creationDate!)
cell.textLabel?.text = chatContent
//cell.detailTextLabel?.text = dateString
}
//cell.textLabel?.text = self.messagesArray[indexPath.row]
// return the cell
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//print(tableView.frame.size)
//print("count: \(self.chatMessagesArray.count)")
return self.chatMessagesArray.count
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
// MARK: Push Notifications
func setupCloudKitSubscription() {
let userDefaults = NSUserDefaults.standardUserDefaults()
print("the value of the bool is: ")
print(userDefaults.boolForKey("subscribed"))
print("print is above")
if userDefaults.boolForKey("subscribed") == false { // TODO: maybe here we do multiple types of subscriptions
let predicate = NSPredicate(format: "user1 in %@ AND user2 in %@", ["john", "mark"], ["john", "mark"])
//let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let subscription = CKSubscription(recordType: "Chat", predicate: predicate, options: CKSubscriptionOptions.FiresOnRecordCreation)
let notificationInfo = CKNotificationInfo()
notificationInfo.alertLocalizationKey = "New Chat"
notificationInfo.shouldBadge = true
subscription.notificationInfo = notificationInfo
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveSubscription(subscription) { (subscription: CKSubscription?, error: NSError?) in
if error != nil {
print(error?.localizedDescription)
} else {
userDefaults.setBool(true, forKey: "subscribed")
userDefaults.synchronize()
}
}
}
}
}
我看到您正在使用推送通知作为重新加载所有数据的信号。 CloudKit 确实为特定谓词使用了兑现机制(细节未知)。在您的情况下,您一遍又一遍地执行相同的谓词。由于这种兑现,您可能会错过记录。尝试在一分钟左右后手动刷新,然后您会发现您的记录会突然出现。
您应该以不同的方式处理推送通知。当您收到通知时,您还应该查询通知消息(如果有多个通知,您可能会收到 1 个推送通知。当您有很多通知时,可能会发生这种情况)
但首先你应该处理当前的通知。首先检查通知是否针对使用以下查询的查询:
if cloudKitNotification.notificationType == CKNotificationType.Query {
然后使用以下方法将其转换为查询通知:
if let queryNotification = cloudNotification as? CKQueryNotification
获取记录ID
if let recordID = queryNotification.recordID {
然后根据发生的情况更改您的本地(在应用程序中)数据。您可以检查使用:
if queryNotification.queryNotificationReason == .RecordCreated
当然也可以。 RecordDeleted 或 .RecordUpdated
如果它是 .RecordCreated
或 .RecordUpdated
,您应该使用 recordID
获取该记录
然后在处理完后,您必须获取其他未处理的通知。为此你必须创建一个 CKFetchNotificationChangesOperation
你必须知道你必须向它传递一个更改令牌。如果您发送 nil ,您将收到为您的订阅创建的所有通知。操作完成后,它会向您发送一个新的更改令牌。您应该将其保存到 userDefaults 中,以便下次开始处理通知时可以使用它。
该查询的代码类似于:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: self.previousChangeToken)
operation.notificationChangedBlock = { notification in
...
operation.fetchNotificationChangesCompletionBlock = { changetoken, error in
...
operation.start()
然后对于该通知,您应该执行与上述初始通知相同的逻辑。并且应该保存 changetoken。
此机制的另一个好处是您的记录会一个接一个地出现,您可以创建一个漂亮的动画来更新您的表格视图。
我正在尝试在 IOS/Swift 中与我 CloudKit 创建一个简单的聊天。我在这个例子之后建模:Create an App like Twitter: Push Notifications with CloudKit 但将其更改为聊天而不是 Sweets。
代码的横幅和徽章在某种程度上运行良好,将数据推送到 CloudDashboard 既好又快。
但是从 cloudKit 到设备的同步大部分时间都不起作用。有时一个设备比另一个设备看到更多,有时更少,只是不太可靠。我在 CloudKit 中使用 DEVELOPMENT 环境。
有什么问题?这是我在 appDelegate 和 viewController:
中实现方法的代码func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let notificationSettings = UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)
UIApplication.sharedApplication().registerForRemoteNotifications()
return true
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
let cloudKitNotification = CKNotification(fromRemoteNotificationDictionary: userInfo as! [String:NSObject])
if cloudKitNotification.notificationType == CKNotificationType.Query {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().postNotificationName("performReload", object: nil)
})
}
}
func resetBadge () {
let badgeReset = CKModifyBadgeOperation(badgeValue: 0)
badgeReset.modifyBadgeCompletionBlock = { (error) -> Void in
if error == nil {
UIApplication.sharedApplication().applicationIconBadgeNumber = 0
}
}
CKContainer.defaultContainer().addOperation(badgeReset)
}
func applicationWillResignActive(application: UIApplication) {
}
func applicationDidEnterBackground(application: UIApplication) {
resetBadge()
}
func applicationWillEnterForeground(application: UIApplication) {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().postNotificationName("performReload", object: nil)
})
}
func applicationDidBecomeActive(application: UIApplication) {
resetBadge()
}
这是 viewController
import UIKit
import CloudKit
class ChatViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
@IBOutlet weak var dockViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var messageTextField: UITextField!
@IBOutlet weak var sendButton: UIButton!
@IBOutlet weak var messageTableView: UITableView!
var chatMessagesArray = [CKRecord]()
var messagesArray: [String] = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.messageTableView.delegate = self
self.messageTableView.dataSource = self
// set self as the delegate for the textfield
self.messageTextField.delegate = self
// add a tap gesture recognizer to the tableview
let tapGesture:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ChatViewController.tableViewTapped))
self.messageTableView.addGestureRecognizer(tapGesture)
setupCloudKitSubscription()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChatViewController.retrieveMessages), name: "performReload", object: nil)
})
// retrieve messages form iCloud
self.retrieveMessages()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func sendButtonTapped(sender: UIButton) {
// Call the end editing method for the text field
self.messageTextField.endEditing(true)
// Disable the send button and textfield
self.messageTextField.enabled = false
self.sendButton.enabled = false
// create a cloud object
//var newMessageObject
// set the text key to the text of the messageTextField
// save the object
if messageTextField.text != "" {
let newChat = CKRecord(recordType: "Chat")
newChat["content"] = messageTextField.text
newChat["user1"] = "john"
newChat["user2"] = "mark"
let publicData = CKContainer.defaultContainer().publicCloudDatabase
//TODO investigate if we want to do public or private
publicData.saveRecord(newChat, completionHandler: { (record:CKRecord?, error:NSError?) in
if error == nil {
dispatch_async(dispatch_get_main_queue(), {() -> Void in
print("chat saved")
self.retrieveMessages()
})
}
})
}
dispatch_async(dispatch_get_main_queue()) {
// Enable the send button and textfield
self.messageTextField.enabled = true
self.sendButton.enabled = true
self.messageTextField.text = ""
}
}
func retrieveMessages() {
print("inside retrieve messages")
// create a new cloud query
let publicData = CKContainer.defaultContainer().publicCloudDatabase
// TODO: we should use this
let predicate = NSPredicate(format: "user1 in %@ AND user2 in %@", ["john", "mark"], ["john", "mark"])
let query = CKQuery(recordType: "Chat", predicate: predicate)
//let query = CKQuery(recordType: "Chat", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
query.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: true)]
publicData.performQuery(query, inZoneWithID: nil) { (results: [CKRecord]?, error:NSError?) in
if let chats = results {
dispatch_async(dispatch_get_main_queue(), {() -> Void in
self.chatMessagesArray = chats
print("count is: \(self.chatMessagesArray.count)")
self.messageTableView.reloadData()
})
}
}
}
func tableViewTapped () {
// Force the textfied to end editing
self.messageTextField.endEditing(true)
}
// MARK: TextField Delegate Methods
func textFieldDidBeginEditing(textField: UITextField) {
// perform an animation to grow the dockview
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5, animations: {
self.dockViewHeightConstraint.constant = 350
self.view.layoutIfNeeded()
}, completion: nil)
}
func textFieldDidEndEditing(textField: UITextField) {
// perform an animation to grow the dockview
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5, animations: {
self.dockViewHeightConstraint.constant = 60
self.view.layoutIfNeeded()
}, completion: nil)
}
// MARK: TableView Delegate Methods
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Create a table cell
let cell = self.messageTableView.dequeueReusableCellWithIdentifier("MessageCell")! as UITableViewCell
// customize the cell
let chat = self.chatMessagesArray[indexPath.row]
if let chatContent = chat["content"] as? String {
let dateFormat = NSDateFormatter()
dateFormat.dateFormat = "MM/dd/yyyy"
let dateString = dateFormat.stringFromDate(chat.creationDate!)
cell.textLabel?.text = chatContent
//cell.detailTextLabel?.text = dateString
}
//cell.textLabel?.text = self.messagesArray[indexPath.row]
// return the cell
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//print(tableView.frame.size)
//print("count: \(self.chatMessagesArray.count)")
return self.chatMessagesArray.count
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
// MARK: Push Notifications
func setupCloudKitSubscription() {
let userDefaults = NSUserDefaults.standardUserDefaults()
print("the value of the bool is: ")
print(userDefaults.boolForKey("subscribed"))
print("print is above")
if userDefaults.boolForKey("subscribed") == false { // TODO: maybe here we do multiple types of subscriptions
let predicate = NSPredicate(format: "user1 in %@ AND user2 in %@", ["john", "mark"], ["john", "mark"])
//let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let subscription = CKSubscription(recordType: "Chat", predicate: predicate, options: CKSubscriptionOptions.FiresOnRecordCreation)
let notificationInfo = CKNotificationInfo()
notificationInfo.alertLocalizationKey = "New Chat"
notificationInfo.shouldBadge = true
subscription.notificationInfo = notificationInfo
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveSubscription(subscription) { (subscription: CKSubscription?, error: NSError?) in
if error != nil {
print(error?.localizedDescription)
} else {
userDefaults.setBool(true, forKey: "subscribed")
userDefaults.synchronize()
}
}
}
}
}
我看到您正在使用推送通知作为重新加载所有数据的信号。 CloudKit 确实为特定谓词使用了兑现机制(细节未知)。在您的情况下,您一遍又一遍地执行相同的谓词。由于这种兑现,您可能会错过记录。尝试在一分钟左右后手动刷新,然后您会发现您的记录会突然出现。
您应该以不同的方式处理推送通知。当您收到通知时,您还应该查询通知消息(如果有多个通知,您可能会收到 1 个推送通知。当您有很多通知时,可能会发生这种情况)
但首先你应该处理当前的通知。首先检查通知是否针对使用以下查询的查询:
if cloudKitNotification.notificationType == CKNotificationType.Query {
然后使用以下方法将其转换为查询通知:
if let queryNotification = cloudNotification as? CKQueryNotification
获取记录ID
if let recordID = queryNotification.recordID {
然后根据发生的情况更改您的本地(在应用程序中)数据。您可以检查使用:
if queryNotification.queryNotificationReason == .RecordCreated
当然也可以。 RecordDeleted 或 .RecordUpdated
如果它是 .RecordCreated
或 .RecordUpdated
,您应该使用 recordID
然后在处理完后,您必须获取其他未处理的通知。为此你必须创建一个 CKFetchNotificationChangesOperation
你必须知道你必须向它传递一个更改令牌。如果您发送 nil ,您将收到为您的订阅创建的所有通知。操作完成后,它会向您发送一个新的更改令牌。您应该将其保存到 userDefaults 中,以便下次开始处理通知时可以使用它。
该查询的代码类似于:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: self.previousChangeToken)
operation.notificationChangedBlock = { notification in
...
operation.fetchNotificationChangesCompletionBlock = { changetoken, error in
...
operation.start()
然后对于该通知,您应该执行与上述初始通知相同的逻辑。并且应该保存 changetoken。
此机制的另一个好处是您的记录会一个接一个地出现,您可以创建一个漂亮的动画来更新您的表格视图。