Scenario = I have an app that allows users to message other users. I also have a button that will display that amount of new (unread) messages that the user has. The amount will be displayed on a badge icon on the tab bar at the bottom.
What I've Been Doing = This queries every 5 seconds if a new message has been posted for the current user.
- (void)queryForMessagesBadgeNumber:(NSTimer *)timer
{
NSString *currentUserID = [PFUser currentUser][#"userID"];
PFQuery *badgeQuery = [PFQuery queryWithClassName:#"Message"];
[badgeQuery whereKey:#"receiverID" equalTo:currentUserID];
[badgeQuery whereKey:#"wasRead" equalTo:[NSNumber numberWithBool:NO]];
[badgeQuery countObjectsInBackgroundWithBlock:^(int number, NSError *error)
{
if (number == 0)
{
NSLog(#"No unread messages");
}
else
{
[[self.tabBar.items objectAtIndex:2] setBadgeValue:[NSString stringWithFormat:#"%d",number]];
}
}];
}
Issues = This is very taxing on the "Requests Per Second" count. Parse allows for 30req/sec on freemium and just with my one phone I am using half of that with this query.
Question = Does anyone know of a more efficient way I can achieve what I am doing here?
Use Push notifications.
When a user sends a message to another user, you could get the app to send the recepient a push notification. You can choose whether to display a banner or not, or simply update the badge.
Guide is located here - Parse Push guide
Parse push is also free to a certain amount of notifications, but a much better way than polling the DB every x amount of seconds
Related
I've implemented Game Center features like achievements and leaderboards, and now I'm working on the challenges. I was under the impression that I didn't have to add any additional code - if I had achievements or leaderboards, players would be able to send challenges to their friends. But now, in iOS10, you no longer have the ability to add players as friends - the challenges are handled through iMessages. The problem is - I don't see that feature anywhere in the GKViewController screen. If you select an achievement/leaaderboard score, you can tap on 'Challenge Friends', but it only suggests the players you already have in your friends list rather than in your contact list. Apple has also deprecated GKChallengesViewController, so I'm not sure where to look on how to do this.
Does anyone know how to add the iMessage Challenges feature to Game Center in iOS 10?
Update: I have seen that this feature lives within the GKMatchmakerViewController, but that seems to be for multiplayer type things. I'm still not sure how to use this to just send challenges.
From the Apple Docs:
Issuing a challenge does not display a user interface to the player issuing the challenge; this is code you need to implement yourself.
There are also a few examples on how to issue challenges and how to find players you can invite, such as:
- (void) challengePlayersToCompleteAchievement: (GKAchievement*) achievement
{
[achievement selectChallengeablePlayers:[GKLocalPlayer localPlayer].friends withCompletionHandler:^(NSArray *challengeablePlayerI, NSError *error) {
if (challengeablePlayers)
{
[self presentChallengeWithPreselectedPlayers: challengeablePlayers];
}
}];
}
...or:
- (void) challengeLesserMortalsForScore: (int64_t) playerScore inLeaderboard: (NSString*) leaderboard
{
GKLeaderboard *query = [[GKLeaderboard alloc] init];
query.leaderboardIdentifier = leaderboard;
query.playerScope = GKLeaderboardPlayerScopeFriendsOnly;
query.range = NSMakeRange(1,100);
[query loadScoresWithCompletionHandler:^(NSArray *scores, NSError *error) {
NSPredicate *filter = [NSPredicate predicateWithFormat:#"value < %qi",playerScore];
NSArray *lesserScores = [scores filteredArrayUsingPredicate:filter];
[self presentChallengeWithPreselectedScores: lesserScores];
}];
}
By the looks of it you still can only invite players that are already part of game center, i.e. no arbitrary "contacts" from the contact list (which makes sense), but this is only an assumption.
I downloaded TigerText Demo app from here and I opened it in xcode but its a huge app I just need conversation part or that single viewcontorller no need of login because I will pass that hard coded like this.
[[TTKit sharedInstance] loginWithUserId:#"username" password:#"password"
success:^(TTUser *user) {
// Handle login.
} failure:^(NSError *error) {
// Handle failure.
}];
After that there will be only one view where users will see there msgs. no login and not organization views required.
To display messages in 'ConversationViewController' you will first need a TTRosterEntry object (An object that represents a conversation), if you would like to load all TTRosterEntry objects regardless of their organization you can use this api
NSFetchedResultsController *frc = [[TTKit sharedInstance] rosterFetchControllerForAllOrganizationsWithDelegate:self];
NSArray *rostersForAllOrganizations = [frc fetchedObjects];
Once you select a roster you can load 'ConversationViewController'
ConversationViewController *conversation = [GetAppDelegate.storyboard instantiateViewControllerWithIdentifier:#"ConversationViewController"];
conversation.rosterEntry = rosterEntry;
[self.navigationController pushViewController:conversation animated:YES];
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!
I've got a JSON object containing 200,000 items. I need to iterate through these objects, and determine if they exist or not and perform the relevant action (insert / update / delete). The shell for this is shown below. Granted, it's not actually saving anything yet. It was more to see how long this way would take. This action takes about 8 minutes to process on an iPhone 4, which seems insane, considering there isn't even any changes occurring yet.
Is there a more efficient way to be handling this?
Any advice or pointers would be greatly appreciated.
- (void) progressiveInsert
{
prodAdd = 0;
prodUpdate = 0;
prodDelete = 0;
dispatch_queue_t backgroundDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_async(backgroundDispatchQueue,
^{
_productDBCount = 0;
NSLog(#"Background Queue");
NSLog(#"Number of products in jsonArray: %lu", (unsigned long)[_products count]);
NSManagedObjectContext *backgroundThreadContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundThreadContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
[backgroundThreadContext setUndoManager:nil];
[fetchRequest setPredicate:predicate];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Products" inManagedObjectContext:_managedObjectContext]];
[fetchRequest setIncludesSubentities:NO]; //Omit subentities. Default is YES (i.e. include subentities)
[fetchRequest setFetchLimit:1];
[_products enumerateObjectsUsingBlock:^(id product, NSUInteger idx, BOOL *stop) {
predicate = [NSPredicate predicateWithFormat:#"code == %#", [product valueForKey:#"product_code"]];
[fetchRequest setPredicate:predicate];
NSError *err;
NSArray *fetchedObjects = [_managedObjectContext executeFetchRequest:fetchRequest error:&err];
if (fetchedObjects == nil) {
if ([[product valueForKey:#"delete"] isEqualToNumber:[NSNumber numberWithBool:TRUE]]){
prodDelete += 1;
} else {
prodAdd += 1;
}
} else {
if ([[product valueForKey:#"delete"] isEqualToNumber:[NSNumber numberWithBool:TRUE]]){
prodDelete += 1;
} else {
prodUpdate += 1;
}
}
dispatch_sync(dispatch_get_main_queue(), ^
{
self.productDBCount += 1;
float progress = ((float)self.productDBCount / (float)self.totalCount);
_downloadProgress.progress = progress;
if (_productDBCount == _totalCount){
NSLog(#"Finished processing");
_endProcessing = [NSDate date];
[_btn.titleLabel setText:#"Finish"];
NSLog(#"Processing time: %f", [_endProcessing timeIntervalSinceDate:_startProcessing]);
NSLog(#"Update: %i // Add: %i // Delete: %i", prodUpdate, prodAdd, prodDelete);
[self completeUpdateProcess];
}
});
}];
});
}
Have a look at
Implementing Find-or-Create Efficiently in the "Core Data Programming Guide".
(Update: This chapter does not exist anymore in the current Core Data Programming Guide. An archived version can be found at
http://web.archive.org/web/20150908024050/https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html.)
One of the key ideas is not to execute one fetch request per product, but execute a
"bulk fetch" with a predicate like
[NSPredicate predicateWithFormat:#"code IN %#", productCodes]
where productCodes is an array of "many" product codes from your JSON data.
Of course you have to find the optimal "batch size".
With that many objects, I think you need to start being very clever about your data and system and to look for other ways to trim your items prior to fetching 200K JSON objects. You say your using Core Data and are on an iPhone, but you don't specify if this is a client/server application (hitting a web server from the phone). I will try to keep my suggestions general.
Really, you should think outside of your current JSON and more about other data/meta-data that can provides hints about what you really need to fetch prior to merge/update. It sounds like you're synchronizing two databases (phone & remote) and using JSON as your means of transfer.
Can you timestamp your data? If you know the last time you updated your phone DB, you need only pull the data changed after that time.
Can you send your data in sections/partitions? Groupings of 1000-10000 might be much more manageable.
Can you partition your data into sections more or less relevant to the user/app? In this way, items that the user touches first are updated first.
If your data is geographic, can you send data close to region of interest first?
If your data is products, can you send data that the user has looked at more recently first?
If your data is hierarchical, can you mark root nodes as changed (or again timestamp) and only update sub-trees that have changed?
I would be hesitant in any system, whether networked or even local DB, to attempt to merge updates from a 200K list of items unless it were a very simple list (like a numeric merge sort). It's a tremendous waste of time and network resources, and it won't make your customers very happy.
Don't work on individual items, batch them. Currently you make lots of fetch requests to the context and these take time (use the Core Data Instruments tool to take a look). If you set the batch size for your processing to 100 initially, then fetch that group of ids and then locally check for existence in the fetch results array.
I use a query FindObjectsInBackgroundWithBlock (assync) to get a value from webservice... I want that value on the appdelegate before the app loads. I'm trying do it but he still load the app before check the webservices to get the value. How can I handle it? I already tried with a NSTimer..
I called this method in -(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions :
-(void)checkUserVersion{
PFQuery *query = [PFQuery queryWithClassName:#"sqliteversion"]; //1
// NSNumber *n=databaseVersion;
[query whereKey:#"user_version" equalTo:[NSNumber numberWithInt:[databaseVersion integerValue]]];//2
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {//4
if (!error && [objects count]>0) {
for (PFObject *object in objects) {
valor1=[[object objectForKey:#"Value"]intValue];
user_version=[NSNumber numberWithInt:valor1];
}
}
}];
}
You may want to evaluate why it's necessary to have this information from Parse.com before launch. Also, it can mean many things to say "before launch", and there are many solutions that don't involve doing "everything" before the first view is attached to the window and shown.
That said...run the PFQuery fetch in the foreground. Doing this will block execution of the rest of your code, though. Instead of findObjectsInBackground just use the findObjects method of PFQuery. I would advise against doing this though, and instead try to find out a way to wait for the query to return while the app is already loaded. Maybe you could suspend user interaction but display a loading indicator or something?
You can't run any code in your app before it finishes launching. If the value is critical for your app to begin working you'll have to show a "waiting for data" message.