2016-05-14 67 views
3

我正嘗試使用iCloudKit在IOS/Swift中創建一個簡單的聊天。我在此示例之後建模:Create an App like Twitter: Push Notifications with CloudKit,但將其更改爲聊天而不是Sweets。來自CloudKit的推送通知無法正確同步

該代碼的橫幅和徽章在某種程度上可以很好地工作,並且將數據推送到CloudDashboard的過程非常快速。

但是從雲端套件到設備的同步大部分時間不起作用。有時候,一個設備比其他設備看到的要多,有時更少,只是不太可靠。我正在使用CloudKit中的開發環境。

什麼問題?這是我在的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() 
       } 
      } 
     } 

    } 
} 

回答

3

我看到你正在使用的推送通知作爲信號重新加載的所有數據。 CloudKit確實使用特定謂詞的兌現機制(細節未知)。在你的情況下,你一遍又一遍地執行相同的謂詞。由於這種兌現,你可能會錯過記錄。嘗試在一分鐘左右之後進行手動刷新,然後您會看到突然顯示您的記錄。

您應該以不同方式處理推送通知。當您收到通知時,您還應該查詢通知消息(當有多個通知時,您可能會收到1個推送通知,當您有很多通知時可能會發生這種情況)

但是首先您應該處理當前通知。用支票開始,如果通知使用是一個查詢:

if cloudKitNotification.notificationType == CKNotificationType.Query { 

然後將它轉換爲一個查詢通知使用:

if let queryNotification = cloudNotification as? CKQueryNotification 

獲取的recordId

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。