I am working on my first iCloud App. After working for a while the app cannot access a UIManagedDocument any more due to an "UIDocumentStateSavingError". Is there any way to actually find out what error occurred?
This is my code to create the UIManagedDocument:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
iCloudURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
if (iCloudURL == nil) {
dispatch_async(dispatch_get_main_queue(), ^{
[self iCloudNotAvailable];
});
return;
}
iCloudDocumentsURL = [iCloudURL URLByAppendingPathComponent:#"Documents"];
iCloudCoreDataLogFilesURL = [iCloudURL URLByAppendingPathComponent:#"TransactionLogs"];
NSURL *url = [iCloudDocumentsURL URLByAppendingPathComponent:#"CloudDatabase"];
iCloudDatabaseDocument = [[UIManagedDocument alloc] initWithFileURL:url];
NSMutableDictionary *options = [NSMutableDictionary dictionary];
NSString *name = [iCloudDatabaseDocument.fileURL lastPathComponent];
[options setObject:name forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:iCloudCoreDataLogFilesURL forKey:NSPersistentStoreUbiquitousContentURLKey];
iCloudDatabaseDocument.persistentStoreOptions = options;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentContentsChanged:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:iCloudDatabaseDocument.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentStateChanged:) name:UIDocumentStateChangedNotification object:iCloudDatabaseDocument];
if ([[NSFileManager defaultManager] fileExistsAtPath:[iCloudDatabaseDocument.fileURL path]]) {
// This is true, the document exists.
if (iCloudDatabaseDocument.documentState == UIDocumentStateClosed) {
[iCloudDatabaseDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[self documentConnectionIsReady];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self connectionError:iCloudConnectionErrorFailedToOpen];
});
}
}];
} else if (iCloudDatabaseDocument.documentState == UIDocumentStateNormal) {
...
}
} else {
...
}
});
The Document already exists and thus openWithCompletionHandler: is called on the document. This fails and the UIDocumentStateChangedNotification is fired which shows a document states of 5:
UIDocumentStateClosed and
UIDocumentStateSavingError
After this the completion block gets called. What is correct way to proceed from here? Is there any way to find out what went wrong and what kind of error occurred?
I tried to re-open the document in the completion block but the result is the same.
I guess I could solve the problem by just deleting the file and recreate it. But this is obviously not an option once the app will be out in the store. I would like to know what is going wrong and give the user an appropriator way to handle the problem.
I already checked other questions here handling the UIDocumentStateSavingError (there a not a lot of them) but the seem not to be applicable for the problem here.
Any idea how I can find out what the problem is? I cannot belive that the API tells you "Something went wrong during saving but I will not tell you what!"
You can query the documentState in the completion handler. Unfortunately, if you want the exact error, the only way to get it is to subclass and override handleError:userInteractionPermitted:
Maybe something like this would help (typed freehand without compiler)...
#interface MyManagedDocument : UIManagedDocument
- (void)handleError:(NSError *)error
userInteractionPermitted:(BOOL)userInteractionPermitted;
#property (nonatomic, strong) NSError *lastError;
#end
#implementation MyManagedDocument
#synthesize lastError = _lastError;
- (void)handleError:(NSError *)error
userInteractionPermitted:(BOOL)userInteractionPermitted
{
self.lastError = error;
[super handleError:error
userInteractionPermitted:userInteractionPermitted];
}
#end
Then in you can create it like this...
iCloudDatabaseDocument = [[UIManagedDocument alloc] initWithFileURL:url];
and use it in the completion handler like this...
[iCloudDatabaseDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[self documentConnectionIsReady];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self connectionError:iCloudConnectionErrorFailedToOpen
withError:iCloudDatabaseDocument.lastError];
});
}
}];
Based on #JodyHagins excellent snippet, I have made a UIDocument subclass.
#interface SSDocument : UIDocument
- (void)openWithSuccess:(void (^)())successBlock
failureBlock:(void (^)(NSError *error))failureBlock;
#end
#interface SSDocument ()
#property (nonatomic, strong) NSError *lastError;
#end
#implementation SSDocument
- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted {
self.lastError = error;
[super handleError:error userInteractionPermitted:userInteractionPermitted];
}
- (void)clearLastError {
self.lastError = nil;
}
- (void)openWithSuccess:(void (^)())successBlock failureBlock:(void (^)(NSError *error))failureBlock {
NSParameterAssert(successBlock);
NSParameterAssert(failureBlock);
[self clearLastError];
[self openWithCompletionHandler:^(BOOL success) {
if (success) {
successBlock();
} else {
NSError *error = self.lastError;
[self clearLastError];
failureBlock(error);
}
}];
}
#end
Related
I am implementing CoreData stack according to
https://stackoverflow.com/a/24663533 (option A from image) but it works in an unexpected way.
I have rootContext (NSPrivateQueueConcurrencyType), it has 2 children: uiContext (NSMainQueueConcurrencyType) for objects fetching and syncContext (NSPrivateQueueConcurrencyType) for asynchronous data editing.
As I thought, when I save something in syncContext in performBlock (background queue), changes will be propagated to rootContext, but uiContext will not be changed until I observe NSManagedObjectContextDidSaveNotification and merge changes from notification. But changes are reflected immediately after syncContext save.
My first question is: why is uiContext updated without manual merge?
My second question: why is rootContext modified on background (not on main thread) after syncContext save? Some time ago I asked question about "CoreData could not fulfill a fault" problem with MagicalRecord 'CoreData could not fulfill a fault' error with MagicalRecord but I didn't receive answer, so I decided to find solution without external libraries.
It seems, that main thread is reading object properties and the same object is deleted on background whilst operators on main thread still don't return control.
Here is my source code:
#import <CoreData/CoreData.h>
#import "DataLayer.h"
#import "Person.h"
#interface DataLayer ()
#property (nonatomic, strong) NSManagedObjectModel *model;
#property (nonatomic, strong) NSPersistentStoreCoordinator *coordinator;
#property (nonatomic, strong) NSManagedObjectContext *rootContext;
#property (nonatomic, strong) NSManagedObjectContext *uiContext;
#property (nonatomic, strong) NSManagedObjectContext *syncContext;
#end
#implementation DataLayer
+ (void)load
{
[self instance];
}
+ (DataLayer *)instance
{
static DataLayer *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[DataLayer alloc] init];
});
return instance;
}
- (instancetype)init
{
self = [super init];
if (self) {
[self initModel];
[self initCoordinator];
[self initContexts];
[self observeContextSaveNotification];
[self startTesting];
}
return self;
}
- (void)initModel
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"Model" withExtension:#"momd"];
self.model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
}
- (void)initCoordinator
{
NSURL *directory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *storeURL = [directory URLByAppendingPathComponent:#"Model.sqlite"];
NSError *error = nil;
self.coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model];
if (![self.coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
- (void)initContexts
{
self.rootContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.rootContext.persistentStoreCoordinator = self.coordinator;
self.uiContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.uiContext.parentContext = self.rootContext;
self.syncContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.syncContext.parentContext = self.rootContext;
}
- (void)observeContextSaveNotification
{
// [[NSNotificationCenter defaultCenter] addObserver:self
// selector:#selector(onManagedObjectContextDidSaveNotification:)
// name:NSManagedObjectContextDidSaveNotification
// object:nil];
}
- (void)onManagedObjectContextDidSaveNotification:(NSNotification *)notification
{
// NSManagedObjectContext *context = notification.object;
// if (context != self.uiContext) {
// [self.uiContext mergeChangesFromContextDidSaveNotification:notification];
// }
}
- (void)startTesting
{
NSArray *personsBeforeSave = [self fetchEntities:#"Person" fromContext:self.uiContext];
NSLog(#"Before save: %i persons in syncContext", [personsBeforeSave count]); // Before save: 0 persons in syncContext
[self.syncContext performBlock:^{
Person *person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.syncContext];
person.firstName = #"Alexander";
NSError *error = nil;
[self.syncContext save:&error];
if (error) {
NSLog(#"Error during save: %#", error);
}
NSArray *personsAfterSaveFromBackground = [self fetchEntities:#"Person" fromContext:self.rootContext];
NSLog(#"After save from background: %i persons in rootContext", [personsAfterSaveFromBackground count]); // After save from background: 1 persons in rootContext
dispatch_async(dispatch_get_main_queue(), ^{
NSArray *personsAfterSaveFromMain = [self fetchEntities:#"Person" fromContext:self.uiContext];
NSLog(#"After save from main: %i persons in uiContext", [personsAfterSaveFromMain count]); // After save from main: 1 persons in uiContext
});
}];
}
- (NSArray *)fetchEntities:(NSString *)entity fromContext:(NSManagedObjectContext *)context
{
NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entity];
NSArray *result = [context executeFetchRequest:request error:&error];
if (error) {
NSLog(#"Error during fetch %#: %#", entity, error);
return nil;
}
return result;
}
#end
They are not being merged into the UI context. You are manually fetching them.
When you save in the syncContext, the data gets pushed up into the rootContext. The data is NOT merged into the uiContext. However, when you perform the fetch, the fetch pulls data down from the parent context.
You can get the objects in a context with registeredObjects.
I have several CoreDataTableViewControllers that utilize the helper class from Paul Hegarty's course. Everyone of them works except this one, and I cannot see a difference.
When the table first comes up, it is correctly populated and the segue executes properly when a cell is selected. However when I hit the back button, the table displays (null), (null) everywhere.
I have tried every variant of calling [self useDocument] that I can think of, still to no avail. Any thoughts? Thanks in advance.
//
// TeamTableViewController.m
//
#import "TeamTableViewController.h"
#import "iTrackAppDelegate.h"
#import "CoreDataTableViewController.h"
#import "SchoolRecords.h"
#import "ScheduleViewController.h"
#interface TeamTableViewController ()
#property NSInteger toggle;
#end
#implementation TeamTableViewController
#synthesize iTrackContext = _iTrackContext;
#synthesize schoolSelected = _schoolSelected;
-(void) setSchoolSelected:(SchoolRecords *)schoolSelected
{
_schoolSelected = schoolSelected;
}
-(void) setITrackContext:(NSManagedObjectContext *)iTrackContext
{
if(_iTrackContext != iTrackContext){
if (!iTrackContext) {
MyCoreDataHandler* cdh =
[(iTrackAppDelegate *) [[UIApplication sharedApplication] delegate] cdh];
_iTrackContext = cdh.context;
} else {
_iTrackContext = iTrackContext;
}
}
[self useDocument];
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)setupFetchedResultsController // attaches an NSFetchRequest to this UITableViewController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"SchoolRecords"];
// no predicate because we want ALL the Athletes
request.sortDescriptors = [NSArray arrayWithObjects:
[NSSortDescriptor sortDescriptorWithKey:#"schoolName" ascending:YES],
nil];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.iTrackContext
sectionNameKeyPath:nil
cacheName:nil];
__block NSInteger myCount;
int64_t delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self.iTrackContext performBlock:^(void){NSError* requestError = nil;
myCount = [self.iTrackContext countForFetchRequest:request error:&requestError];
NSLog(#"In %# and count of iTrackContext = %lu", NSStringFromClass([self class]),(unsigned long)myCount);
}];
if (!myCount || myCount == 0) {
[self displayAlertBoxWithTitle:#"No Teams" message:#"Have you added athletes yet? \nPlease go to Add Athletes" cancelButton:#"Okay"];
}
});
}
- (void)useDocument
{
if (self.iTrackContext) {
[self setupFetchedResultsController];
} else {
NSString* errorText = #"A problem arose opening the search results database of Athletes.";
[self displayAlertBoxWithTitle:#"File Error" message:errorText cancelButton:#"Okay"];
}
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.iTrackContext) {
MyCoreDataHandler* cdh =
[(iTrackAppDelegate *) [[UIApplication sharedApplication] delegate] cdh];
[self setITrackContext:cdh.context];
} else {
NSLog(#"In %# of %#. Getting ready to call useDocument",NSStringFromSelector(_cmd), self.class);
[self useDocument];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// If divide into sections use line below otherwise return 1.
// return [[self.fetchedResultsController sections] count];
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Do not really need this with only one section, but makes code usable if add sections later.
return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"teamInformation";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
SchoolRecords *schoolResults = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString* titleText = schoolResults.schoolName;
cell.textLabel.text = titleText;
cell.detailTextLabel.text = [NSMutableString stringWithFormat:#"%#, %#", schoolResults.schoolCity, schoolResults.schoolState];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
# pragma navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
[self setSchoolSelected:[self.fetchedResultsController objectAtIndexPath:indexPath]];
// be somewhat generic here (slightly advanced usage)
// we'll segue to ANY view controller that has a photographer #property
if ([segue.identifier isEqualToString:#"scheduleDetailSegue"]) {
// use performSelector:withObject: to send without compiler checking
// (which is acceptable here because we used introspection to be sure this is okay)
NSLog(#"Preparing to passing school with schoolID = %#", self.schoolSelected.schoolID);
[segue.destinationViewController convenienceMethodForSettingSchool:self.schoolSelected];
}
}
- (void) displayAlertBoxWithTitle:(NSString*)title message:(NSString*) myMessage cancelButton:(NSString*) cancelText
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
message:myMessage
delegate:nil
cancelButtonTitle:cancelText
otherButtonTitles:nil];
[alert show];
}
#end
Well, I am not certain what the problem was. I ended up deleting the "offending" TableViewControllers in StoryBoard and recreated them. That did the trick. In retrospect, I wonder if I did not specify the wrong type of segue from my tabViewController. But I deleted it before I thought of that possibility.
I'm having an issue with saving video from a GPUImage videoCamera to the Camera Roll when my app goes into the background. The file is only saved to the camera roll when the app returns to the foreground / is restarted. I'm no doubt making a beginners code error , if anyone can point it out that would be appreciated.
- (void)applicationDidEnterBackground:(UIApplication *)application {
if (isRecording){
[self stopRecording];
};
if (self.isViewLoaded && self.view.window){
[videoCamera stopCameraCapture];
};
runSynchronouslyOnVideoProcessingQueue(^{
glFinish();
});
NSLog(#"applicationDidEnterBackground");
and then
-(void)stopRecording {
[filterBlend removeTarget:movieWriter];
videoCamera.audioEncodingTarget = nil;
[movieWriter finishRecording];
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:#"file.mov"];
ALAssetsLibrary *al = [[ALAssetsLibrary alloc] init];
[al writeVideoAtPathToSavedPhotosAlbum:[NSURL fileURLWithPath:path] completionBlock:^(NSURL *assetURL, NSError *error) {
if (error) {
NSLog(#"Error %#", error);
} else {
NSLog(#"Success");
}
}];
isRecording = NO;
NSLog(#"Stop recording");
It was exactly as Brad pointed out in his, as usual, insightful comment, the -writeVideoAtPathToSavedPhotosAlbum:completionBlock: wasn't completing till after the app returned to the foreground, I solved it by adding
self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Background handler called. Not running background tasks anymore.");
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
}];
and
#property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;
Found this solution at http://www.raywenderlich.com/29948/backgrounding-for-ios
I have got this background thread that does a few things with core data objects. I get the context as follows:
- (id)_managedObjectContextForThread;
{
NSManagedObjectContext * newContext = [[[NSThread currentThread] threadDictionary] valueForKey:#"managedObjectContext"];
if(newContext) return newContext;
newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:[[[NSApplication sharedApplication] delegate] persistentStoreCoordinator]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(_mergeChangesFromManagedObjectContext:)
name:NSManagedObjectContextDidSaveNotification
object:newContext];
[[[NSThread currentThread] threadDictionary] setValue:newContext forKey:#"managedObjectContext"];
return newContext;
}
then I fetch some objects, modify them and save the context:
- (void) saveContext:(NSManagedObjectContext*)context {
NSError *error = nil;
if (![context save:&error]) {
[[NSApplication sharedApplication] presentError:error];
}
}
- (void)_mergeChangesFromManagedObjectContext:(NSNotification*)notification;
{
[[[[NSApplication sharedApplication] delegate] managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
.. later I remove the observer. This works for the main part. But some properties don't get updated when they get merged back. The properties that were nil before get updated. The ones that had a value stay the same.
I tried:
[newContext setMergePolicy:NSOverwriteMergePolicy];
... (and the other merge policies) on the main context but it did not work :P
Thank you for your help.
Note: I have bound the values to a NSTableView. I log them after the merge. The values properties that were nil seem to work fine.
How are you registering both contexts for notifications? You need to do something like this:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundMOC];
[nc addObserver:self
selector:#selector(mainContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:mainMOC];
And implement the callbacks:
// merge changes in background thread if main context changes
- (void)mainContextDidSave:(NSNotification *)notification
{
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[backgroundMOC performSelector:selector onThread:background_thread withObject:notification waitUntilDone:NO];
}
// merge changes in main thread if background context changes
- (void)backgroundContextDidSave:(NSNotification *)notification
{
if ([NSThread isMainThread]) {
[mainMOC mergeChangesFromContextDidSaveNotification:notification];
}
else {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:) withObject:notification waitUntilDone:NO];
}
}
i want to show leaderbord in my own game ....i am using following method for that but noting happen ... i am confuse with rootview controller as my game is developed in cocos2d so there is nothing like dat :(
// Leaderboards
-(void) showLeaderboard
{
if (isGameCenterAvailable == NO)
return;
GKLeaderboardViewController* leaderboardVC = [[[GKLeaderboardViewController alloc] init] autorelease];
if (leaderboardVC != nil)
{
leaderboardVC.leaderboardDelegate = self;
[self presentViewController:leaderboardVC];
}
}
///
-(void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController*)viewController
{
[self dismissModalViewController];
[delegate onLeaderboardViewDismissed];
}
///////
-(UIViewController*) getRootViewController
{
return [UIApplication sharedApplication].keyWindow.rootViewController;
}
///
-(void) presentViewController:(UIViewController*)vc
{
UIViewController* rootVC = [self getRootViewController];
[rootVC presentModalViewController:vc animated:YES];
}
////
-(void) dismissModalViewController
{
UIViewController* rootVC = [self getRootViewController];
[rootVC dismissModalViewControllerAnimated:YES];
}
...
regards
Haseeb
i dont know but it work for me.if anyone can describe the real reason for why this working in this way i will be very glad....i call it through appdelegate
[(myAppDelegate*)[[UIApplication sharedApplication] delegate]gameCenter];
and from appdelegate i call rootviewcontroller method like
-(void)gameCenter
{
[rootViewController gameCenterLeaderboard];
}
and in rootviewcontroller there is a method
-(void)gameCenterLeaderboard
{
GKLeaderboardViewController* leaderboardVC = [[[GKLeaderboardViewController alloc] init] autorelease];
if (leaderboardVC != nil) {
leaderboardVC.leaderboardDelegate = self;
[self presentModalViewController: leaderboardVC animated: YES];
}
}
the following method is also override in rootviewcontroller
- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)leaderboardController
{
[self dismissModalViewControllerAnimated:YES];
}
If you don't have a root UIViewController then I'd recommend creating a new UIViewController set it's view to your openGLView then use that view controller to present the leaderboard as a modal view controller.
UIViewController *leaderboardViewController = [[UIViewController alloc] init];
[leaderboardViewController setView:[[CCDirector sharedDirector] openGLView]];
[leaderboardViewController presentModalViewController:leaderboardVC animated:YES]; //leaderboardVC is your GKLeaderboardViewController