I've been struggling on this for several days so any would be appreciated. I'm trying to save the players array below and display it in a UITableView. I'd like to save it so I can display the local player's friends. I've tried several different things but something that looks it's working for others is this.
__block NSArray *friends;
- (void) loadPlayerData: (NSArray *) identifiers {
[GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
if (error != nil) {
// Handle the error.
}
if (players != nil) {
friends = players;
NSLog(#"Inside: %#", friends); //Properly shows the array
}
}];
NSLog(#"Outside: %#", friends): //Doesn't work, shows nil
}
But friends is still nil/null afterwards. Am I doing something wrong? Is there any way to save players and use it in a UITableView? Thanks.
***EDIT***
So here's the solution I put together.
typedef void(^CallbackBlock)(id object);
+ (void) retrieveFriends: (CallbackBlock)callback {
GKLocalPlayer *lp = [GKLocalPlayer localPlayer];
if (lp.authenticated) {
[lp loadFriendsWithCompletionHandler:^(NSArray *friends, NSError *error) {
if (friends != nil) {
[self loadPlayerDataWithIdentifiers:friends callback:^(NSArray *playersInfo) {
if (callback) callback(playersInfo);
}];
}
}];
}
}
+ (void) loadPlayerDataWithIdentifiers: (NSArray *) identifiers callback:(CallbackBlock)callback {
[GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
if (error != nil) {
// Handle the error.
}
if (players != nil) {
if (callback) callback(players);
}
}];
}
The only thing is, my UITableView is in another class so I tried doing this and making the two methods above public. info isn't printing out anything. Any ideas?
[GameCenterHelper retrieveFriends:^(NSArray *info) {
NSLog(#"Friends Info: %#", info);
}];
Use callback blocks.
typedef void(^CallbackBlock)(id object);
- (void)loadPlayerDataWithIdentifiers:(NSArray *)identifiers callback:(CallbackBlock)callback {
[GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
if (error != nil) {
// Handle the error.
}
if (callback) callback(players);
}];
}
You imply in your question that this is for a table view. If so, you need to reload your table after the data has been loaded.
[self loadPlayerDataWithIdentifiers:identifiers callback:^(NSArray *players) {
self.players = players;
[self.tableView reloadData];
}];
Crimson Chris is correct.
The other option is use GCD to wait for the response to comeback.
Or change this to synchronous call as you want to get the results immediately.
Related
I wrote some code to add text to the Messages.app input field in my iMessage extension.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"didSelect called");
NSLog(#"%d", 1);
[[self activeConversation] insertText:#"https://google.com" completionHandler:^(NSError * error) {
NSLog(#"Error happened");
NSLog(#"Error: %#", error);
}];
NSLog(#"%d", 2);
}
The strange part is that all of the normal logs are happening. The app will log "didSelect called", "1" and "2". However, the message - the Google url - isn't being inserted, and the error logs aren't being shown. So I don't really have a clue as to what's going wrong. Any idea's what I'm doing wrong?
Solution #1
Send correct reference from MessagesViewController to your view controller.
Check activeConversation value for nil:
if ([self activeConversation] != nil) {
[[self activeConversation] insertText:#"Some text" completionHandler:^(NSError * _Nullable error) {
NSLog(#"error: %#", error.localizedDescription);
}];
} else {
NSLog(#"Conversation is nil");
}
Solution #2
Create Singleton in iMessage extension name space.
In MessagesViewController in - (void)viewDidLoad setup reference to
your MSConversation: [[Conversation shared] activeConversation] = [self activeConversation];
Use [[Conversation shared] activeConversation] insertText: .... ];
for sending messages from any controllers.
This is related to a recent thread Update progress with MRProgress. I converted my cloudkit queries from the convenience API to CKOperations as a result of previous thread (Thanks Edwin!). So while using CKModifyRecordsOperation to save a record, I can see the record's progress via logging in the perRecordProgressBlock, which is great. However, I'm trying to send this progress back to the viewcontroller and I cannot figure out how to do that. I have created a class for all of my CloudKit methods - CKManager. The other problem I'm having is that I'm unsure when to update the progress indicator (using MRProgress framework) in the VC. Do I call it before, during or after the save operations call in the CKManager? Should it be called recursively until the progress == 1.0? Here is the code I have so far...every works fine except for updating/animating the progress indicator (it appears and shows 0% and then disappears when the save operation is completed). Also, I'm using a property (double progress) in my CKManager class and I know that is incorrect, but I wasn't sure how else to do it. And I do not feel that the callback method I've declared/defined in my CKManager class below for this is correct either. Any guidance is appreciated!
CKManager.h
#property (nonatomic, readonly) double progress;
- (void)recordProgressWithCompletionHandler:(void (^)(double progress))completionHandler;
CKManager.m
#property (nonatomic, readwrite) double progress;
- (void)recordProgressWithCompletionHandler:(void (^)(double))completionHandler {
completionHandler(self.progress);
}
- (void)saveRecord:(NSArray *)records withCompletionHandler:(void (^)(NSArray *, NSError *))completionHandler {
NSLog(#"INFO: Entered saveRecord...");
CKModifyRecordsOperation *saveOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:records recordIDsToDelete:nil];
saveOperation.perRecordProgressBlock = ^(CKRecord *record, double progress) {
if (progress <= 1) {
NSLog(#"Save progress is: %f", progress);
self.progress = progress;
}
};
saveOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError *error) {
NSLog(#"Save operation completed!");
completionHandler(#[record], error);
};
[self.publicDatabase addOperation:saveOperation];
}
Viewcontroller.m - this is from the method that takes the photo from the camera and calls the CKManager class to prepare the record and save it to CK as well as display the MRProgress indicator...
if (self.imageDataAddedFromCamera) {
self.hud = [MRProgressOverlayView showOverlayAddedTo:self.myCollectionView animated:YES];
self.hud.mode = MRProgressOverlayViewModeDeterminateCircular;
self.hud.titleLabelText = UPLOADING_MSG;
// prepare the CKRecord and save it
[self.ckManager saveRecord:#[[self.ckManager createCKRecordForImage:self.imageDataAddedFromCamera]] withCompletionHandler:^(NSArray *records, NSError *error) {
if (!error && records) {
NSLog(#"INFO: Size of records array returned: %lu", (unsigned long)[records count]);
CKRecord *record = [records lastObject];
self.imageDataAddedFromCamera.recordID = record.recordID.recordName;
NSLog(#"INFO: Record saved successfully for recordID: %#", self.imageDataAddedFromCamera.recordID);
[self.hud dismiss:YES];
[self.hud removeFromSuperview];
[self.imageLoadManager addCIDForNewUserImage:self.imageDataAddedFromCamera]; // update the model with the new image
// update number of items since array set has increased from new photo taken
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
[self updateUI];
} else {
NSLog(#"Error trying to save the record!");
NSLog(#"ERROR: Error saving record to cloud...%#", error.localizedDescription);
[self.hud dismiss:YES];
[self.hud removeFromSuperview];
[self alertWithTitle:YIKES_TITLE andMessage:ERROR_SAVING_PHOTO_MSG];
}
}];
// where does this call belong?
[self.ckManager recordProgressWithCompletionHandler:^(double progress) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Updating hud display...");
[self.hud setProgress:progress animated:YES];
});
}];
You should include the progress handler in your saveRecord call like this:
- (void)saveRecord:(NSArray *)records withCompletionHandler:(void (^)(NSArray *, NSError *))completionHandler recordProgressHandler:(void (^)(double))progressHandler {
NSLog(#"INFO: Entered saveRecord...");
CKModifyRecordsOperation *saveOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:records recordIDsToDelete:nil];
saveOperation.perRecordProgressBlock = ^(CKRecord *record, double progress) {
if (progress <= 1) {
NSLog(#"Save progress is: %f", progress);
progressHandler(progress)
}
};
saveOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError *error) {
NSLog(#"Save operation completed!");
completionHandler(#[record], error);
};
[self.publicDatabase addOperation:saveOperation];
}
Then you can call that save record like this:
[self.ckManager saveRecord:#[[self.ckManager createCKRecordForImage:self.imageDataAddedFromCamera]] withCompletionHandler:^(NSArray *records, NSError *error) {
if (!error && records) {
NSLog(#"INFO: Size of records array returned: %lu", (unsigned long)[records count]);
CKRecord *record = [records lastObject];
self.imageDataAddedFromCamera.recordID = record.recordID.recordName;
NSLog(#"INFO: Record saved successfully for recordID: %#", self.imageDataAddedFromCamera.recordID);
[self.hud dismiss:YES];
[self.hud removeFromSuperview];
[self.imageLoadManager addCIDForNewUserImage:self.imageDataAddedFromCamera]; // update the model with the new image
// update number of items since array set has increased from new photo taken
self.numberOfItemsInSection = [self.imageLoadManager.imageDataArray count];
[self updateUI];
} else {
NSLog(#"Error trying to save the record!");
NSLog(#"ERROR: Error saving record to cloud...%#", error.localizedDescription);
[self.hud dismiss:YES];
[self.hud removeFromSuperview];
[self alertWithTitle:YIKES_TITLE andMessage:ERROR_SAVING_PHOTO_MSG];
}
}, recordProgressHandler:^(double progress) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Updating hud display...");
[self.hud setProgress:progress animated:YES];
});
}];
So that the code for updating the progress is part of your saveRecord call.
The code above is not tested by me. So I hope I made no typo's
I have already configured all Game Center functions and below code i am using to unlock achievement, which is perfectly working fine.
- (void) unlockAchievementThis:(NSString*)achievementID {
GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:
achievementID];
if (achievement){
achievement.percentComplete = 100;
achievement.showsCompletionBanner = true;
[GKAchievement reportAchievements:#[achievement] withCompletionHandler:^(NSError *error) {
if (error != nil) {
NSLog(#"Error at unlockAchievementThis()");
}
}];
}
}
Now My problem is with incremental achievements. I have another method for few achievements and I want the previous achievement percentage to increase it with a constant.
My game is in cpp and i don't know much ObjC.
I got some code below which i think should help me but i don't know how to use achievementDescriptions to get percentage and add incStep into it and submit it to back game center.
- (void) incrementAchievementThis:(NSString*)achievementID :(NSInteger) incStep
{
NSMutableDictionary *achievementDescriptions = [[NSMutableDictionary alloc] init];
[GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:^(NSArray *descriptions, NSError *error) {
if (error != nil) {
NSLog(#"Error getting achievement descriptions: %#", error);
}
for (GKAchievementDescription *achievementDescription in descriptions) {
[achievementDescriptions setObject:achievementDescription forKey:achievementDescription.identifier];
}
}];
Percentages are stored in GKAchievement percentComplete, so you need to load (and update and report) GKAchievements instead of GKAchievementDescriptions.
GKAchievmenentDescriptions are configured in iTunes Connect and are "read-only" from the point of view of your app.
Finally i got output by below code...
- (void) incrementAchievementThis:(NSString*)achievementID :(NSInteger) incStep
{
[GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error)
{
if (error == nil) {
for (GKAchievement* achievement in achievements) {
if ([achievementID isEqualToString:achievement.identifier]) {
achievement.percentComplete += incStep;
[GKAchievement reportAchievements:#[achievement] withCompletionHandler:^(NSError *error) {
if (error != nil) {
NSLog(#"Error at incrementAchievementThis()");
}
}];
}
}
}
else {
NSLog(#"Error in loading achievements: %#", error);
}
}];
}
Say I have a PFObject which I am editing. At later stage I wish to cancel the changes I have done to the PFObject. How do I revert back to original copy of PFObject?
What I have tried
if (self.request.isDirty) { // self.request is a PFObject
// Reload object
NSLog(#"%#", self.request.requestTitle); // Logs ABC, Original was DEF
[self.request refreshInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error) {
NSLog(#"%#", self.request.requestTitle); // Logs ABC
[self.requestDetailsTableView reloadData];
}
}];
}
I tried fetching the object as well, but same result
if (self.request.isDirty) { // self.request is a PFObject
// Reload object
NSLog(#"%#", self.request.requestTitle); // Logs ABC, Original was DEF
[self.request fetchInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error) {
NSLog(#"%#", self.request.requestTitle); // Logs ABC
[self.requestDetailsTableView reloadData];
}
}];
}
There is now a revert method on PFObject which will reset your object back to the server state as long as you haven't called save: http://parseplatform.org/Parse-SDK-iOS-OSX/api/Classes/PFObject.html#/c:objc(cs)PFObject(im)revert
For example, I use a pushed view controller with a form to edit properties on my PFObject subclass and I have two buttons for Cancel and Save...
#IBAction override func cancelAction() {
self.myObject?.revert()
self.navigationController?.popViewControllerAnimated(true)
}
#IBAction override func saveAction() {
self.myObject?.saveInBackgroundWithBlock({ (succeeded: Bool, error: NSError?) -> Void in
if (succeeded) {
self.navigationController?.popViewControllerAnimated(true)
} else {
// Show error
}
})
}
There isn't currently a way to do this directly - you need to do a little fancy footwork with your calls :)
Try this:
if (self.request.isDirty) { // self.request is a PFObject
// Reload object
NSLog(#"%#", self.request.requestTitle); // Logs ABC, Original was DEF
PFQuery *newRequest = [PFQuery queryWithClassName:#"YourClassName"];
//Add your request parameters here...
[newRequest getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error)
self.request = object;
NSLog(#"%#", self.request.requestTitle); // Should log DEF
[self.requestDetailsTableView reloadData];
}
}];
}
Can you help me how can I get user info.
NSString *name;
[FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error)
{
if (!error)
{
// Success! Include your code to handle the results here
name = [result objectForKey:#"first_name"]; // Error!!! how to get data from this handler
}
else
{
// An error occurred, we need to handle the error
// See: https://developers.facebook.com/docs/ios/errors
}
}];
Code described above - asynchronous? How to make it synchronous? Tell me about the mechanism or tell me where to read. Thank you!
You could read everything about Facebook SDK on this site: https://developers.facebook.com.
They don't provide synchronous API and I don't even know why you might need it. But if you really do you can do some workaround. See implementation:
__block id result = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id theResult, NSError *error) {
result = theResult;
dispatch_semaphore_signal(semaphore); // UPDATE: dispatch_semaphore_wait call was here, which is wrong
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(#"%#", result);// this code will be launched after you've received response from Facebook
Result conforms FBGraphUser protocol. So if you it doesn't contain value for first_name key, user wouldn't have specified it. You could print result in debugger and see what it is.
Seems like you totally mess up. Try this code, but be sure, your understand it:
- (void) SetUserData
{
[FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error)
{
if (!error)
{
self.first_name = [result objectForKey:#"first_name"];
NSLog(#"First name just came from server: first_name: %#", self.first_name);
}
else
{
}
}];
[self furtherProcessMethod];
}
- (void) furtherProcessMethod
{
if (self.first_name == nil)
{
// this is not recursion, do not miss that!!
[self performSelector:#selector(furtherProcessMethod) withObject:nil afterDelay:2.0];
}
else
{
NSLog(#"First name that I can work with first_name: %#", self.first_name);
// here you can handle you first_name, after it came
}
}
So after some time you need to get in log:
First name just came from server: first_name: some name
First name that I can work with first_name: some name
You can use additional method to solve your problem:
1) make name as property: __block NSString *name
2) move you code, that you need to perform, to separate method, like that:
- (void) methodWithComplitionHandler
{
name = nil;
[FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error)
{
if (!error)
{
name = [result objectForKey:#"first_name"];
}
else
{
}
}];
[self futherProcessMethod];
}
- (void) furtherProcessMethod
{
if (self.name == nil)
{
[self performSelector:#selector(furtherProcessMethod) withObject:nil afterDelay:3.0]; // here set appropriate delay
}
else
{
// do some with name;
}
}
As I clarify in previous answer, you need to work with name inside "else" statement. Please, debug for more understanding. So we have:
- (void) SetUserData
{
[FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error)
{
if (!error)
{
self.first_name = [result objectForKey:#"first_name"];
}
else
{
}
}];
[self furtherProcessMethod];
}
- (void) furtherProcessMethod
{
if (self.first_name == nil)
{
[self performSelector:#selector(furtherProcessMethod) withObject:nil afterDelay:30.0]; // here set appropriate delay
}
else
{
NSLog(#"first_name: %#", self.first_name);
}
}