CKFetchNotificationChangesOperation returning old notifications - ios

I'm working on a CloudKit-based app that uses CKSubscription notifications to keep track of changes to a public database. Whenever the app receives a push notification I check the notification queue with CKFetchNotificationChangesOperation and mark each notification read after processing it:
__block NSMutableArray *notificationIds = [NSMutableArray new];
CKFetchNotificationChangesOperation *operation = [[CKFetchNotificationChangesOperation alloc] initWithPreviousServerChangeToken:self.serverChangeToken];
operation.notificationChangedBlock = ^(CKNotification *notification) {
[notificationIds addObject:notification.notificationID];
[self processRemoteNotification:notification withCompletionHandler:completionHandler];
};
__weak CKFetchNotificationChangesOperation *operationLocal = operation;
operation.fetchNotificationChangesCompletionBlock = ^(CKServerChangeToken *serverChangeToken, NSError *operationError) {
if (operationError) {
NSLog(#"Unable to fetch queued notifications: %#", operationError);
}
else {
self.serverChangeToken = serverChangeToken;
completionHandler(UIBackgroundFetchResultNewData);
// Mark the processed notifications as read so they're not delivered again if the token gets reset.
CKMarkNotificationsReadOperation *markReadOperation = [[CKMarkNotificationsReadOperation alloc] initWithNotificationIDsToMarkRead:[notificationIds copy]];
[notificationIds removeAllObjects];
markReadOperation.markNotificationsReadCompletionBlock = ^(NSArray *notificationIDsMarkedRead, NSError *operationError) {
if (operationError) {
NSLog(#"Unable to mark notifications read: %#", operationError);
}
else {
NSLog(#"%lu notifications marked read.", (unsigned long)[notificationIDsMarkedRead count]);
}
};
[[CKContainer defaultContainer] addOperation:markReadOperation];
if (operationLocal.moreComing) {
NSLog(#"Fetching more");
[self checkNotificationQueueWithCompletionHandler:completionHandler];
}
}
};
[[CKContainer defaultContainer] addOperation:operation];
As I understand it marking a notification read will keep it from showing up in future queue fetches, even if the server change token is reset to nil. Instead I'm getting a lot of old notifications in every fetch with a non-nil change token when there should only be 1 or 2 new ones. I can detect the old ones from the notificationType flag, but I'm concerned that they're showing up at all. Am I missing a step somewhere?

I know this is a bit old, but I was running into the same issue. I think I figured it out (at least for my case).
In my code, I was doing the same as you: that is, adding all the notificationIDs to an array and using that in my CKMarkNotificationsReadOperation, and was also getting all the notifications returned each time (although, as you noted, with a type of "ReadNotification").
I changed my code so that I was only adding "new" notifications to my array, and not the "ReadNotification" items, and sending those. That fixed it.
It seems that sending a notification back to the server to be marked as read, even if it already has been marked as such, will cause it to be returned again as "ReadNotification."
I hope this helps someone.

The documentation isn't very clear it should say: "Marking a notification as read prevents it from being returned by subsequent fetch operations"...as a query notification type. Further clarification it should say the notifications will instead be returned as read type.
If it wasn't returned at all then other devices that missed the push wouldn't know that something has changed!

Related

Get only new/modified phone contacts from iPhone

I want to import all phone contacts into app first time only and save on server.
Second time wants to imports only new, modified and deleted contacts into an app and sync with contacts saved on the server which should be done according to created/modified date of contacts.
From iOS 9, Apple won't allow to get create/modified date using Contacts Framework
How can I achieve with better approach?
You can save contacts identifier and import date in local storage like SQLite. And when you receive contacts update notification, you can send saved date of respective updated contact and also update date in SQLite with received notification timestamp(date).
You should add observer for change contact list notification like this:
NotificationCenter.default.addObserver(self,selector: #selector(self.addressBookDidChange(_:)), name: NSNotification.Name.CNContactStoreDidChange,object: nil)
And then
#objc func addressBookDidChange(_ notification: Notification){
print(notification.object as Any, notification.userInfo as Any)
//remove observer so far to prevent double method calling when making operations with contacts
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.CNContactStoreDidChange, object: nil)
//processing
//at some point return observer
NotificationCenter.default.addObserver(self,selector: #selector(ContactsListVC.addressBookDidChange(_:)), name: NSNotification.Name.CNContactStoreDidChange,object: nil)
}
There's an undocumented API in CNContactStore - enumeratorForChangeHistoryFetchRequest:error:
I tried to test this method using the following code:
-(void)testChangeHistoryRequest {
CNContactStore *store = [[CNContactStore alloc] init];
[self requestContactsPermissions:store completion:^(BOOL granted) {
if (!granted)
return;
NSData *storeToken = [store currentHistoryToken];
NSLog(#"testChangeHistoryRequest: store token st start - %#", storeToken);
NSError *error;
CNChangeHistoryFetchRequest *req = [[CNChangeHistoryFetchRequest alloc] init];
[req setAdditionalContactKeyDescriptors:#[CNContactGivenNameKey, CNContactFamilyNameKey]];
[req setMutableObjects:YES];
[req setShouldUnifyResults:YES];
[req setStartingToken:storeToken];
CNFetchResult<NSEnumerator<CNChangeHistoryEvent*>*>* res = [store enumeratorForChangeHistoryFetchRequest:req error:&error];
if (res && res.value) {
NSData *token = [res currentHistoryToken];
NSLog(#"token - %#", token);
for (CNChangeHistoryEvent *e in res.value)
NSLog(#"%# - %#", [e class], e);
NSLog(#"token at end - %#", token);
}
}];
}
What I got is that store.currentHistoryToken never changes - it starts and ends with nil value. Also during the iteration, res.currentHistoryToken is always nil.
I also tried to initialize [req setStartingToken:storeToken]; with arbitrary data, but this changed nothing (and didn't even fail).
My guess is that this enumeration is not fully implemented.

How to display an AlertView requesting permission to seed iCloud once and only once in app's iCloud lifetime?

I have one question near the end.
I am working from the belief/experience that seeding iCloud more than once is a bad idea and that if a user can do the wrong thing, he probably will sooner or later.
What I want to do:
A. When the user changes the app preference "Enable iCloud" from NO to YES, display AlertView asking (Yes or No) if the user wishes to seed the cloud with existing non-iCloud Data.
B. Ensure that the app seeds iCloud only once on an iCloud account, refraining to put up the AlertView once seeding is completed the first time.
My Method:
Following Apple's Docs concerning the proper use of NSUbiquitousKeyValueStore, I am using the following method in, - (void)application: dFLWOptions:
- (void)updateKVStoreItems:(NSNotification*)notification {
// Get the list of keys that changed.
NSDictionary* userInfo = [notification userInfo];
NSNumber* reasonForChange = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
NSInteger reason = -1;
// If a reason could not be determined, do not update anything.
if (!reasonForChange)
return;
// Update only for changes from the server.
reason = [reasonForChange integerValue];
if ((reason == NSUbiquitousKeyValueStoreServerChange) ||
(reason == NSUbiquitousKeyValueStoreInitialSyncChange)) { // 0 || 1
// If something is changing externally, get the changes
// and update the corresponding keys locally.
NSArray* changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
// This loop assumes you are using the same key names in both
// the user defaults database and the iCloud key-value store
for (NSString* key in changedKeys) {//Only one key: #"iCloudSeeded" a BOOL
BOOL bValue = [store boolForKey:key];
id value = [store objectForKey:#"iCloudSeeded"];
[userDefaults setObject:value forKey:key];
}
}
}
Include the following code near the top of application: dFLWO:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(updateKVStoreItems:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:store]; // add appDelegate as observer
After loading iCloud Store, then seed it with non-iCloud data ONLY if seeding has never been done
- (BOOL)loadiCloudStore {
if (_iCloudStore) {return YES;} // Don’t load iCloud store if it’s already loaded
NSDictionary *options =
#{
NSMigratePersistentStoresAutomaticallyOption:#YES
,NSInferMappingModelAutomaticallyOption:#YES
,NSPersistentStoreUbiquitousContentNameKey:#"MainStore"
};
NSError *error=nil;
_iCloudStore = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil URL:[self iCloudStoreURL] options:options error:&error];
if (_iCloudStore) {
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded =
[store boolForKey:#"iCloudSeeded"];//If the key was not found, this method returns NO.
if(!iCloudSeeded) // CONTROL IS HERE
[self confirmMergeWithiCloud]; // Accept one USER confirmation for seeding in AlertView ONCE world wide
return YES; // iCloud store loaded.
}
NSLog(#"** FAILED to configure the iCloud Store : %# **", error);
return NO;
}
Once the seeding is completed do the following to prevent any repeat seeding:
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloud];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"]; // NEVER AGAIN
//[store synchronize];
}
}
}
Be sure to get a total iCloud reset before the above process using:
[NSPersistentStoreCoordinator
removeUbiquitousContentAndPersistentStoreAtURL:[_iCloudStore URL]
options:options
error:&error])
This is a very tidy solution to my problem, IMHO, but I can not quite get it done.
MY QUESTION:
How do I respond to the first notification to updateKVStoreItems: above? It is a notification with bad info. I says the value is TRUE, but I have never set it to TRUE. How do I set default values for a key in NSUbiquitousKeyValueStore?
I find that the first notification is of reason : NSUbiquitousKeyValueStoreInitialSyncChange
When that note comes in, bValue is YES. THIS IS MY PROBLEM. It is as if, iCloud/iOS assumes any new BOOL to be TRUE.
I need this value to be NO initially so that I can go ahead and follow the Apple Docs and set
the NSUserDefault to NO. And then Later when the seeding is done, to finally set the value: YES for the key:#"iCloudSeeded"
I find I can not penetrate the meaning of the following from Apple:
NSUbiquitousKeyValueStoreInitialSyncChange
Your attempt to write to key-value storage was discarded because an initial download from iCloud has not yet happened.
That is, before you can first write key-value data, the system must ensure that your app’s local, on-disk cache matches the truth in iCloud.
Initial downloads happen the first time a device is connected to an iCloud account, and when a user switches their primary iCloud account.
I don't quite understand the implications of number 2 below, which I found online:
NSUbiquitousKeyValueStoreInitialSyncChange – slightly more complicated, only happens under these circumstances:
1. You start the app and call synchronize
2. Before iOS has chance to pull down the latest values from iCloud you make some changes.
3. iOS gets the changes from iCloud.
If this problem was with NSUserDefaults and not NSUbiquitousKeyValueStore, I believe I would need to go to registerDefaults.
I am almost there,
How do I do this please!
Thanks for reading, Mark
The code was looking for both
A. NSUbiquitousKeyValueStoreInitialSyncChange and
B. NSUbiquitousKeyValueStoreServerChange
I was unable to figure out what to do with the notifications. I know see that I did not need to do anything with either. My app only needs to read and write, in order to solve the problem I laid out in my question header.
The app gets the current value with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded = [store boolForKey:#"iCloudSeeded"];
The app sets the value in the NSUbiquitousKeyValueStore with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"];
I believe I am correct in saying the following: Writing is done into memory. Very soon thereafter the data is put by the system onto disk.
From there it is taken and put into iCloud and is made available to the other devices running the same app on the same iCloud account. In the application I have described, no observer needs to be added, and
nothing else needs to be done. This is maybe an "unusual" use of NSUbiquitousKeyValueStore.
If you came here looking for a an more "usual" use, say when a user type something into a textview and it later
appears on a view of other devices running the same app, check out a simple demo I came across at :
https://github.com/cgreening/CMGCloudSyncTest
The better functioning (monitoring only) notification handler follows:
- (void)updateKVStoreItems:(NSNotification*)notification {
NSNumber *reason = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
if(!reason) return;
// get the reason code
NSInteger reasonCode = [notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] intValue];
BOOL bValue;
NSUbiquitousKeyValueStore *store;
switch(reasonCode) {
case NSUbiquitousKeyValueStoreServerChange:{ // code 0, monitoring only
store = [NSUbiquitousKeyValueStore defaultStore];
bValue = [store boolForKey:#"iCloudSeeded"];
id value = [store objectForKey:#"iCloudSeeded"];
DLog(#"New value for iCloudSeeded=%d\nNo Action need be take.",bValue);
// For monitoring set in UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:value forKey:#"iCloudSeeded"];
break;
}
case NSUbiquitousKeyValueStoreAccountChange: {// ignore, log
NSLog(#"NSUbiquitousKeyValueStoreAccountChange");
break;
}
case NSUbiquitousKeyValueStoreInitialSyncChange:{ // ignore, log
NSLog(#"NSUbiquitousKeyValueStoreInitialSyncChange");
break;
}
case NSUbiquitousKeyValueStoreQuotaViolationChange:{ // ignore, log
NSLog(#"Run out of space!");
break;
}
}
}
Adding 9/3/14
So sorry but I continued to have trouble using a BOOL, I switched to an NSString and now
all is well.
METHOD TO ENSURE THAT THE "MERGE" BUTTON FOR SEEDING ICOUD IS USED AT MOST ONCE DURING APP LIFETIME
Use NSString and not BOOL in KV_STORE. No need to add observer, except for learning
In Constants.h :
#define SEEDED_ICLOUD_MSG #"Have Seeded iCloud"
#define ICLOUD_SEEDED_KEY #"iCloudSeeded"
Before calling function to seed iCloud with non-iCloud data:
NSUbiquitousKeyValueStore* kvStore = [NSUbiquitousKeyValueStore defaultStore];
NSString* strMergeDataWithiCloudDone =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
NSComparisonResult *result = [strMergeDataWithiCloudDone compare:SEEDED_ICLOUD_MSG];
if(result != NSOrderedSame)
//put up UIAlert asking user if seeding is desired.
If user chooses YES : set Value for Key after the merge is done.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloudwithNoniCloudData];
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
[store setObject:SEEDED_ICLOUD_MSG forKey:ICLOUD_SEEDED_KEY];
}
}
}
Thereafter on all devices, for all time, the code
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
NSString* msg =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
produces: msg == SEEDED_ICLOUD_MESSAGE

Receive data packets via BLE Notifications

When receiving multiple packets via BLE notifications, iOS is only giving me access to the final packet sent. I am using YMSCoreBluetooth to connect to a BLE peripheral with multiple services, each of which has multiple characteristics. I connect to the peripheral, discover the services and discover the characteristics of those services without a problem. My goal is to subscribe to a certain characteristic's notifications and receive via the notifications a series of data packets. My subscription is successful and I can see through use of NSLogs within my code that I am receiving the notifications containing the data. The issue is that when I go to access the data from each notification as it comes in, every notification gives me only the data contained in the last packet sent.
My code for receiving notifications is as follows:
- (void)notifyCharacteristicHandler:(YMSCBCharacteristic *)characteristic error:(NSError *)error
{
if (error) {
NSLog(#"Error: Error in handling notification.\n%#", error);
}
else if ([characteristic.name isEqualToString:#"InterestingChar"]) {
if (self.firstNotify) {
self.mutableData = [[NSMutableData alloc] init];
self.firstNotify = NO;
}
NSData *data = [[NSData alloc] init];
data = characteristic.cbCharacteristic.value;
[self.mutableData appendData:data];
self.notifyCounter++;
NSLog(#"Notify received! Count: %ld \nData =%#",(long)self.notifyCounter,self.mutableData);
}
else NSLog(#"Other notification received");
}
For instance, if I receive 5 notifications with the following data:
1 ababababab
2 bcbcbcbcbc
3 cdcdcdcdcd
4 dedededede
5 efefefefef
My NSLog would print out efefefefef for the first notify data, efefefefef efefefefef for the second, and so on appending the last data value for each subsequent notify.
I am trying to send the notifications as quickly as possible from the peripheral using BLE. The connection interval is between 20ms and 40ms (iOS demands a range of at least 20ms) and three packets are being sent per connection interval.
EDIT:
Paulw11's suggestion worked beautifully. I fixed the issue by amending the YMSCB 'didUpdateValueForCharacteristic' method to obtain the value of the characteristic and pass it along with the pointer to the characteristic itself onto the 'notifyCharacteristicHandler' method. The amended method now looks as follows:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
__weak YMSCBPeripheral *this = self;
NSData *value = characteristic.value;
_YMS_PERFORM_ON_MAIN_THREAD(^{
YMSCBService *btService = [this findService:characteristic.service];
YMSCBCharacteristic *yc = [btService findCharacteristic:characteristic];
if (yc.cbCharacteristic.isNotifying) {
[btService notifyCharacteristicHandler:yc value:value error:error];
} else {
if ([yc.readCallbacks count] > 0) {
[yc executeReadCallback:characteristic.value error:error];
}
}
if ([this.delegate respondsToSelector:#selector(peripheral:didUpdateValueForCharacteristic:error:)]) {
[this.delegate peripheral:peripheral didUpdateValueForCharacteristic:characteristic error:error];
}
});
}
You obviously also need to amend the 'notifyCharacteristicHandler' method to accept the new argument.
Looking at the internal didUpdateValueForCharacteristic delegate method of the YMSCoreBluetooth library, it sends the data to your method using a "perform on main thread" and it doesn't capture the data - it just sends a reference to the characteristic. Also, it performs a "findCharacteristic" on the characteristic by executing a linear search through the array on the main thread even though this could have been done immediately on entering the delegate method on the current thread. Granted this isn't going to be a very big array but it seems that this library hasn't been created with performance in mind.
I suspect that you have a timing problem - by the time your method executes the data in the characteristic has been over written. If you have control over your peripheral, slow it right down for a test to see if the problem goes away.
If it is timing related then you could try a straight Core-Bluetooth implementation, or try a modification to YMSCoreBluetooth so that it captures the data earlier - perhaps if it created a copy of the peripheral at the start of didUpdateValueForCharacteristic and sent that to your method it would work.

How to distinguish between PNMessage sent by current user versus those from others?

I am currently using PubNub to build chat-like functionality into an app. Following the example from the demo project, I added the following to my viewDidLoad to listen for messages received my channel:
[[PNObservationCenter defaultCenter] addMessageReceiveObserver:self withBlock:^(PNMessage *message) {
NSLog(#"message %#", message);
[self DisplayInLog: [NSString stringWithFormat:#"[%#]: %#",message.channel.name, message.message]];
[self showReceivedMessage:message];
}];
My problem is, when the current user sends a message using [PubNub sendMessage:text toChannel:self.currentChannel], the listener picks up on the message (as expected), but I am having trouble distinguishing from the PNMessage that which is sent by the current user and which is sent by someone else and picked up by the receiver. How should I go about approaching this without getting too hacky (for example, comparing the contents of the messages, when they were sent, etc).
Thanks!
#daspianist
You have to add field which will store identifier of the user and on client side check who is sender of the message by retrieving value from dictionary which will be send as message.
For example:
[[PNObservationCenter defaultCenter] addMessageReceiveObserver:self withBlock:^(PNMessage *message) {
NSLog(#"message %#", message);
if (![[message.message valueForKey:#"sender"] isEqualToString:#"Bob"]) {
[self DisplayInLog: [NSString stringWithFormat:#"[%#]: %#",message.channel.name, message.message]];
[self showReceivedMessage:message];
}
}];
[PubNub sendMessage:#{#"message":#"This is actual message which we want to send. It can be any Objective-C type.", #"sender":#"Bob"} toChannel:self.currentChannel];
In example above, it shouldn't process message if it has been sent from "Bob"
SEND MESSAGE IN DICTIONARY
NSDictionary *message = [[NSDictionary alloc] initWithObjectsAndKeys:#"HELLLLOO",#"text",#"Kiran",#"sender", nil];
[PubNub sendMessage:message toChannel:[PNDataManager sharedInstance].currentChannel
withCompletionBlock:^(PNMessageState state, id object) {
}

Can´t receive AppMessage from Pebble on iOS

On the Watch i send an AppMessage like this
DictionaryIterator *iter;
app_message_outbox_begin(&iter);
Tuplet value = TupletInteger(MESSAGE_TYPE, MESSAGETYPE_REFRESH);
dict_write_tuplet(iter, &value);
app_message_outbox_send();
I set the background modes and protocols for my app as described in the tutorial.
In iOS i set the listeners like this:
[PBPebbleCentral defaultCentral].delegate = self;
self.watch = [PBPebbleCentral defaultCentral].lastConnectedWatch;
NSLog(#"Pebble name: %#", _watch.name);
NSLog(#"Pebble serial number: %#", _watch.serialNumber);
[_watch appMessagesAddReceiveUpdateHandler:^BOOL(PBWatch *watch, NSDictionary *update) {
NSLog(#"Update received!");
return YES;
}];
[_watch appMessagesAddReceiveAllUpdatesHandler:^BOOL(PBWatch *watch, NSUUID *uuid, NSDictionary *update) {
NSLog(#"AllUpdate received!");
return YES;
}];
[_watch appMessagesAddAppLifecycleUpdateHandler:^(PBWatch *watch, NSUUID *uuid, PBAppState newAppState) {
NSLog(#"AppLifecycleUpdate received!");
}];
I already did send messages from the phone to the watch. So that way it works. But the listeners for incoming messages on the phone wont get called.
On the clock i get APP_MSG_SEND_TIMEOUT as error code. What did i wrong?
Do you have src/js/pebble-js-app.js in your watch app? I had the same problem and when i removed this generated file it started working.
Check to make sure you are using
appMessagesAddReceiveUpdateHandler:withUUID:
instead of
appMessagesAddReceiveUpdateHandler:
Pay attention to where you put the listener. For example if you are using the WeatherDemo app (provided by Pebble) you should do that after setting the the app UUID.
// Test if the Pebble's firmware supports AppMessages / Weather:
[watch appMessagesGetIsSupported:^(PBWatch *watch, BOOL isAppMessagesSupported) {
if (isAppMessagesSupported) {
...
[_targetWatch appMessagesAddReceiveUpdateHandler:^BOOL(PBWatch *watch, NSDictionary *update) {
NSLog(#"Received message: %#", update);
return YES;
}];
} else {
....
Another thing to pay attention is not to put it under
- (void)pebbleCentral:(PBPebbleCentral*)central watchDidConnect:(PBWatch*)watch isNew:(BOOL)isNew
because this function isn't called if the device is already connected.

Resources