I'm trying to incorporate GKGameSession into my Game Center game. I've tried several combinations of the following code: running the commands asynchronously, chaining them in the completion handlers, etc. Every time I see the same result: I can use saveData just fine until I've called getShareURLWithCompletionHandler. After that, any attempt to saveData throws an error.
Here's the simplest version of code that exhibits the problem:
CKContainer *defaultContainer = [CKContainer defaultContainer];
[GKGameSession createSessionInContainer:defaultContainer.containerIdentifier
withTitle:#"temp title"
maxConnectedPlayers:4
completionHandler:^(GKGameSession * _Nullable session, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}];
In most cases, the saveData call simply crashes:
malloc: *** error for object 0x32df14: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
But sometimes it throws an error:
GKGameSessionErrorDomain:GKGameSessionErrorUnknown
I've tried different kinds of data being saved. I've tried making the calls sequential by chaining all the calls in completion handlers. I've tried doing the URL fetch and data save inside and outside of the creationSession completion handler.
Is there something I'm doing wrong here?
I see the same, but with a more useful error:
The requested operation could not be completed because the session has been updated on the server, causing a conflict.
The save documentation says,
It is up to the developer to decide how to handle save conflicts.
Here though, retrying the save fails every time, forever. So yeah, that's the same state you're in.
However, when the player joining the game enters the URL on their device, their GKGameSessionEventListener's didAddPlayer: is called, and then if they save... they get the same conflict error, but if they then retry the save... it works!
The player creating the link is locked out of saving or updating game state, until joining players have updated the data. When the other player saves, the original player gets a call to session:player:didSave: on the GKGameSessionEventListener.
At that point the original player can then save as expected.
You should put one block inside other. Because blocks may be completed in any order.
I have working code like this:
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
} else {
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}
}];
Related
There are multiple places within my app that I display a list of objects returned from the server. I'm trying to re-write some code so it only has to be modified in one place, rather than three, for updates.
The basic gist looks like this:
PFQuery *query = [PFQuery queryWithClassName:"MyClass"];
//query constraints
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if( !error ) //Do stuff with objects;
else [self displayError:error];
}];
I have subclassed PFObject for this class, so I have a class called MyClass. I was trying to put a class method on this class that returns an NSArray * containing the results of a query. However, the following function throws some errors:
+(NSArray *)getListOfMyClass {
PFQuery *query = [PFQuery queryWithClassName:"MyClass"];
//query constraints
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if( !error ) return objects;
else [self displayError:error];
}];
}
This error appears at the end of the function:
Control may reach end of non-void block
This error appears at return objects;: Incompatible block pointer types sending 'NSArray * _Nullable (^)(NSArray * _Nullable __strong, NSError * _Nullable __strong)' to parameter of type 'PFQueryArrayResultBlock _Nullable' (aka 'void (^)(NSArray<PFGenericObject> * _Nullable __strong, NSError * _Nullable __strong)')
So, I know this issue is due to the asynchronous behavior of this query. I need to call this query from a few different places, so was hoping I could call the single function, but now I'm lost as to how make this efficient and not very mucked up. I'm sorta leaning towards just doing it in line three times, as that's what I was doing already anyway, but I'm trying to learn better practices.
Is there a way I can force this function to wait to return until the query has succeeded? Or would it be OK to pass in sender, and then trigger a function on the sender that passes in the results? I'm gonna be working on the latter to see if it works, but even that seems so roundabout. I'm wondering if there's something obvious I'm missing.
Thanks for your help!
Note - I would prefer Objective C answers, but can work with a Swift answer if need be.
According to PFQuery.h :
typedef void (^PFQueryArrayResultBlock)(NSArray<PFGenericObject> *_Nullable objects, NSError * _Nullable error);
The findObjectsInBackgroundWithBlock is a void block which returns nothing, so obviously the return statement gonna cause error.
If you want to retrieve the array, you can use a completion handler, like:
+(void)getListOfMyClassWithCompletionBlock:(void (^)(NSError *error, NSArray *objectArray))block {
PFQuery *query = [PFQuery queryWithClassName:"MyClass"];
//query constraints
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
if( !error ) block(nil, objects);
else block(error, nil); [self displayError:error];
}];
}
And somewhere else (other classes) in your code:
[YourClass getListOfMyClassWithCompletionBlock:^(NSError *error, NSArray *objectArray) {
if (!error) //update your UI with objectArray
}];
I am trying to wait until my rest service call back receive, but after
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
line, everythings stop and i never receive call back. My code is below.
__block NSString *strTaggedText=#"";
dispatch_semaphore_t sem;
sem = dispatch_semaphore_create(0);
[[manager2 dataTaskWithRequest:req completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if (!error) {
NSLog(#"Reply JSON: %#", responseObject);
dictionary=[responseObject objectForKey:#"****"];
strTaggedText=[dictionary objectForKey:#"Text"];
NSLog(#"strText = %#",strTaggedText);
} else
{
NSLog(#"Error: %#, %#, %#", error, response, responseObject);
}
dispatch_semaphore_signal(sem);
}] resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
return strTaggedText;
You've stopped the main runloop, which means no events are getting processed and the app will stop responding.
You are calling an asynchronous method and the main thread is expected to return immediately, after perhaps showing a busy indicator.
Processing is meant to continue from within the block, which will be called in a background thread. You can then push the data to whereever it's expected to go and cancel the busy indicator. The next thing can then be triggered, perhaps on the main thread, but that's up to you; certainly any UI-interaction must be done on the main thread only.
This is asynchronous programming, but a full discussion is beyond the scope of a stackoverflow answer, in my opinion.
The reason that this isn't working at all is that you wrote
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
on the main thread, and the main thread stops right there until a _signal comes - which won't arrive because your manager2 will never reach the code where it resumes the data task.
My goal is to be able to save a record using CloudKit when my app gets backgrounded. I first fetch the record I'm trying to modify, modify it, then save it. I'm making this call from my app delegate's applicationDidEnterBackground:
CKDatabase *db = [[CKContainer defaultContainer] privateCloudDatabase];
CKRecordID *recordId = [[CKRecordID alloc] initWithRecordName:#"record_a"];
[db fetchRecordWithID:recordId completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
// Doesn't get here until I foreground the app
if (!record) // Doesn't exist yet so create it
{
record = [[CKRecord alloc] initWithRecordType:#"SaveData" recordID:recordId];
}
NSURL *fileURL = [NSURL fileURLWithPath:#"path/to/file"];
CKAsset *saveAsset = [[CKAsset alloc] initWithFileURL:fileURL];
record[#"saveAsset"] = saveAsset;
[db saveRecord:record completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable saveError) {
// Doesn't get here until I foreground
}];
}];
The completion block for the fetchRecordWithID call doesn't get fired until I foreground the app again, meaning the saveRecord: call doesn't get called until then either.
I've tried using the CKOperation version of fetch and save with modified qualityOfService and queuePriority properties to no avail. Also tried setting the longLived property, but that didn't work either. I've scoured the docs and couldn't find anything that should stop the block from being called when the app is backgrounded.
I have other code that can call through their completion blocks when in the background, so not sure why this would be limited. Any ideas?
I'm trying to start a new GKGameSession and when I use createSession, all I can get so far is nil. Here's my code:
GKGameSession.createSession(inContainer: "test", withTitle: "MyGame", maxConnectedPlayers: 8)
{ (newGameSession, error) in
self.gameSession = newGameSession
print("\(newGameSession)")
newGameSession?.getShareURL(completionHandler: { (url, error) in
print("url: \(url) error: \(error)")
})
}
The only thing it prints is "nil". Any help is appreciated.
If you using the emulator, I suggest using a device instead. GKGameSessions do not play well with the emulator, because they depend on push notifications and an iCloud account that is logged in.
newGameSession is optional. So it seems like something has gone wrong when creating a new session.
I would say newGameSession is likely nil, in that case error will hopefully contain some useful information.
Try replacing print("\(newGameSession)") with print(newGameSession, error) to see what the error var has to say, or set a breakpoint if you know how to do that.
Try to use your app's iCloud container name instead of "test". The container name will be in the format iCloud.com.yourcompany.appname if you have selected the default container option. To ensure your app has an iCloud container you need to enable it in your app's Capabilities.
I'm a jumping in a little late, but I'd triple check the container name you're using to create the session.
I enabled all three options in xCode: key-value storage, iCloud documents and CloudKit.
I don't use iCloud drive.
The following code successfully creates a new session each time I send invites. It prints the new session, then iterates through all existing sessions for that container ID.
-(void)turnBasedMatchmakerViewController:(GKTurnBasedMatchmakerViewController *)viewController didFindMatch:(GKTurnBasedMatch *)match
{
[self dismissViewControllerAnimated:YES completion:nil];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *iCloudContainerName = [#"iCloud." stringByAppendingString: bundleIdentifier];
[GKGameSession createSessionInContainer:iCloudContainerName
withTitle:#"test"
maxConnectedPlayers:4
completionHandler:^(GKGameSession * _Nullable session, NSError * _Nullable error)
{
NSLog(#"(1) Session: %#, Error: %#", session.identifier, [error description]);
[GKGameSession loadSessionsInContainer:iCloudContainerName
completionHandler:^(NSArray<GKGameSession *> * _Nullable sessions, NSError * _Nullable error)
{
for (GKGameSession *session in sessions)
{
NSLog(#"(2) Session: %#, Error: %#", session.identifier, [error description]);
}
NSLog(#"-----");
}];
}];
}
I'm trying to use the new Apple Music APIs from 9.3 to add a song to a playlist created by my app, without adding it to the user's library.
Consider the productID 316654632, it's the song Lisztomania by Phoenix, in the US iTunes Store.
Using the following code, I can play the song
MPMusicPlayerController *musicPlayer = [MPMusicPlayerController systemMusicPlayer];
[musicPlayer setQueueWithStoreIDs:#[#"316654632"]];
[musicPlayer play];
Using the following code, I can add the song to my Apple Music library
[[MPMediaLibrary defaultMediaLibrary] addItemWithProductID:#"316654632" completionHandler:^(NSArray<__kindof MPMediaEntity *> * _Nonnull entities, NSError * _Nullable error) {
NSLog(#"%#", error);
}];
Error is nil, and I can see the song in my library.
But trying the same with a playlist doesn't work.
[[MPMediaLibrary defaultMediaLibrary] getPlaylistWithUUID:uuid creationMetadata:[[MPMediaPlaylistCreationMetadata alloc] initWithName:#"Test Playlist"] completionHandler:^(MPMediaPlaylist * _Nullable playlist, NSError * _Nullable error) {
NSLog(#"%#", error);
if (!error) {
[playlist addItemWithProductID:#"316654632" completionHandler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
}];
The playlist is created, I can see it in Music.app, but when I try to add the same product ID I played & added to my library to the playlist, I get an error
Error Domain=MPErrorDomain Code=4 "The requested id could not be found" UserInfo={NSLocalizedDescription=The requested id could not be found}
But how could it not be found if I successfully added the same item to my library?
UPDATE
Good news! Apple has fixed rdar://26408683 on 10.2.1!
In my playlist conversion app (mixlib), the only solution I have found to reliably add some tracks to a newly created playlist is to wait.
In my tests, waiting five seconds seems to be enough.
[[MPMediaLibrary defaultMediaLibrary] getPlaylistWithUUID:uuid creationMetadata:[[MPMediaPlaylistCreationMetadata alloc] initWithName:#"Test Playlist"] completionHandler:^(MPMediaPlaylist * _Nullable playlist, NSError * _Nullable error) {
if (!error) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 /*seconds*/ * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)), ^() {
[playlist addItemWithProductID:#"316654632" completionHandler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
}];
I suspect it is a server/network related issue because it sometimes works without waiting. The "requested id" which is not found may be the playlist id, not the track id.
When it starts to work for a playlist, then it will always work. So you don't need to wait before adding each additional track, but only before adding the first one.