2016-05-14 9 views
3

Ich versuche, einen einfachen Chat in IOS/Swift mit iCloudKit zu erstellen. Ich modelliere nach diesem Beispiel: Create an App like Twitter: Push Notifications with CloudKit, aber ich ändere es, um ein Chat anstelle von Süßigkeiten zu werden.Push-Benachrichtigung von CloudKit synchronisiert nicht richtig

Das Banner und das Logo des Codes funktionieren in gewissem Maße gut und das Übertragen von Daten auf CloudDashboard ist in Ordnung und schnell.

Die Synchronisation vom cloudKit zu den Geräten funktioniert jedoch meistens nicht. Manchmal sieht ein Gerät mehr als das andere, manchmal weniger, nur nicht zu zuverlässig. Ich verwende die DEVELOPMENT-Umgebung in CloudKit.

Was ist das Problem? Hier ist mein Code der implementierten Methoden in AppDelegate und der 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() 
} 

und dies ist der 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() 
       } 
      } 
     } 

    } 
} 

Antwort

3

Ich sehe, dass Sie die Push-Benachrichtigung als Signal verwenden alle Daten neu zu laden. CloudKit verwendet einen Einzahlungsmechanismus (Details dazu sind unbekannt) für ein bestimmtes Prädikat. In Ihrem Fall führen Sie dasselbe Prädikat immer wieder aus. Wegen dieser Einlösung könnten Sie Aufzeichnungen verpassen. Versuchen Sie eine manuelle Aktualisierung nach einer Minute oder so und Sie werden sehen, dass dann plötzlich Ihre Aufzeichnungen erscheinen.

Sie sollten Push-Benachrichtigungen anders behandeln. Wenn Sie eine Benachrichtigung erhalten, sollten Sie auch die Benachrichtigungsnachrichten abfragen (Sie könnten eine Push-Benachrichtigung erhalten, wenn mehrere Benachrichtigungen vorhanden sind.)

Aber zuerst sollten Sie die aktuelle Benachrichtigung behandeln. Beginnen Sie mit einer Überprüfung, ob die Benachrichtigung für eine Abfrage ist mit:

if cloudKitNotification.notificationType == CKNotificationType.Query { 

warf sie dann auf eine Abfrage-Benachrichtigung über:

if let queryNotification = cloudNotification as? CKQueryNotification 

Holen Sie sich das recordID

if let recordID = queryNotification.recordID { 

dann je nachdem, was Ist passiert, ändern Sie Ihre lokalen (in App) Daten. Sie können dies überprüfen mit:

if queryNotification.queryNotificationReason == .RecordCreated 

Natürlich könnte es auch sein. RecordDeleted oder .RecordUpdated

Wenn es eine .RecordCreated oder .RecordUpdated ist, sollten Sie diesen Rekord holen mit der recordID

Dann, wenn das verarbeitet wird, müssen Sie andere nicht verarbeitet Benachrichtigungen holen. Dafür müssen Sie eine CKFetchNotificationChangesOperation erstellen. Sie müssen beachten, dass Sie ein Änderungs-Token übergeben müssen. Wenn Sie eine Null senden, erhalten Sie alle Benachrichtigungen, die jemals für Ihre Abonnements erstellt wurden. Wenn die Vorgänge abgeschlossen sind, erhalten Sie ein neues Änderungs-Token. Sie sollten dies in Ihren userDefaults speichern, damit Sie das nächste Mal beim Bearbeiten von Benachrichtigungen verwenden können.

Der Code für diese Abfrage wird wie etwas aussehen:

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

Dann für diese Meldung sollten Sie die gleiche Logik wie oben für die erste Anmeldung auszuführen. Und das Changetoken sollte gespeichert werden.

Ein weiterer Vorteil dieses Mechanismus besteht darin, dass Ihre Datensätze eins nach dem anderen eintreffen und Sie eine nette Animation erstellen können, die Ihre Tabellenansicht aktualisiert.