来自 CloudKit 的推送通知未正确同步

Posted

技术标签:

【中文标题】来自 CloudKit 的推送通知未正确同步【英文标题】:Push Notification from CloudKit doesn't synchronize properly 【发布时间】:2016-05-14 05:37:08 【问题描述】:

我正在尝试使用 iCloudKit 在 ios/Swift 中创建一个简单的聊天。我正在模仿这个例子: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()

这是视图控制器

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()
                
            
        

    

【问题讨论】:

【参考方案1】:

我看到您正在使用推送通知作为重新加载所有数据的信号。 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 您必须知道您必须向它传递一个更改令牌。如果您将其发送为零,您将获得为您的订阅创建的所有通知。操作完成后,它将向您发送一个新的更改令牌。您应该将其保存到您的 userDefaults 中,以便下次开始处理通知时可以使用它。

该查询的代码如下所示:

let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: self.previousChangeToken)
operation.notificationChangedBlock =  notification in
...
operation.fetchNotificationChangesCompletionBlock =  changetoken, error in
...
operation.start()

然后对于该通知,您应该执行与上述初始通知相同的逻辑。并且更改令牌应该被保存。

这种机制的另一个好处是您的记录会一个一个地出现,您可以创建一个漂亮的动画来更新您的 tableview。

【讨论】:

以上是关于来自 CloudKit 的推送通知未正确同步的主要内容,如果未能解决你的问题,请参考以下文章

iOS 未收到来自 CloudKit 的推送通知

未收到CloudKit推送通知

CloudKit:使用通知使更改在多个设备之间保持同步

CloudKit Dashboard 显示推送日志,但是收不到,为啥?

CloudKit 向其他用户推送通知

使用 CloudKit 延迟后触发推送通知