I am wondering how I can prevent a crash when trying to close UIDoc twice. I tried to make sure in my code that you (theoretically) can not close a UIDocument twice. However, it still happens sometimes and I don't know why. If it does, the app crashes:
2012-07-06 15:24:34.470 Meernotes[11620:707] ... doc state:Normal
2012-07-06 15:24:34.472 Meernotes[11620:707] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'closeWithCompletionHandler called while document is already closing'
*** First throw call stack:
(0x3720e88f 0x34f13259 0x3720e789 0x3720e7ab 0x312681d1 0xd19db 0x96f7f 0x9593f 0xacb8f 0x30f0cd23 0x37a7f933 0x371e2a33 0x371e2699 0x371e126f 0x371644a5 0x3716436d 0x33923439 0x30f10cd5 0x94fdd 0x94f78)
terminate called throwing an exception(lldb)
I tried to prevent the crash as follows, but it has no effect whatsoever (i.e. it will still crash):
-(void)closeDoc {
UIDocumentState state = _selectedDocument.documentState;
NSMutableArray * states = [NSMutableArray array];
if (state == 0) {
[states addObject:#"Normal"];
}
if (state & UIDocumentStateClosed) {
[states addObject:#"Closed"];
}
if (state & UIDocumentStateInConflict) {
[states addObject:#"In conflict"];
}
if (state & UIDocumentStateSavingError) {
[states addObject:#"Saving error"];
}
if (state & UIDocumentStateEditingDisabled) {
[states addObject:#"Editing disabled"];
}
NSLog(#"... doc state: %#", [states componentsJoinedByString:#", "]);
if (_selectedDocument.documentState & UIDocumentStateClosed) return;
[_selectedDocument closeWithCompletionHandler:^(BOOL success) {
NSLog(#"Closed document.");
// Check status
if (!success) {
NSLog(#"Failed to close %#", _selectedDocument.fileURL);
} else {
_selectedDocument = nil;
}
}];
}
It looks like UIDocument doesn't store the closing state, only normal and closed, so you'll have to do it yourself.
Add this to your class variables:
BOOL _documentClosing;
And add its use in your closeDoc method:
-(void)closeDoc {
if (_docClosing || (_selectedDocument.documentState & UIDocumentClosed) != 0)
return;
_docClosing = YES;
[_selectedDocument closeWithCompletionHandler:^(BOOL success) {
NSLog(#"Closed document.");
// Check status
if (!success) {
NSLog(#"Failed to close %#", _selectedDocument.fileURL);
} else {
_selectedDocument = nil;
_docClosing = NO;
}
}];
}
It's very important to know that each UIDocument object can only be opened and closed once. I had a lot of odd problems with UIDocuments before realizing this, both in the cloud and with local files. When you close a document set its pointer to nil, so you can't close it again. If you need to access the same file again later, create a new UIDocument with the same fileURL.
The error message you show above is one that appears when you try to re-use a document.
Related
NSDocument continues to be a software maintenance nightmare.
Anyone else having a problem where they want certain blocking dialogs to be handled SYNCHRONOUSLY?
BEGIN EDIT: I may have found a solution that allows me to wait synchronously
Can anyone verify that this would be an "Apple approved" solution?
static BOOL sWaitingForDidSaveModally = NO;
BOOL gWaitingForDidSaveCallback = NO; // NSDocument dialog calls didSave: when done
...
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
if ( gWaitingForDidSaveCallback )
{
// first, dispatch any other potential alerts synchronously
while ( gWaitingForDidSaveCallback && [NSApp modalWindow] )
[NSApp runModalForWindow: [NSApp modalWindow]];
if ( gWaitingForDidSaveCallback )
{
sWaitingForDidSaveModally = YES;
[NSApp runModalForWindow: [NSApp mbWindow]]; // mbWindow is our big (singleton) window
sWaitingForDidSaveModally = NO;
}
}
...
- (void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
[self recordLastSaveURL];
gWaitingForDidSaveCallback = NO;
if ( sWaitingForDidSaveModally )
[NSApp stopModal];
}
END EDIT
I have to support Snow Leopard/Lion/ML
App termination is an ugly process.
When the user decides to quit, and the document has changes that need saving, I call this:
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
I really really really want this call to be synchronous, but in latest Lion, this hangs my app:
while ( gWaitingForDidSaveCallback )
{
// didSave: callback clears sWaitingForDidSaveCallback
// do my own synchronous wait for now
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0.05]];
}
My best guess for the hang is that the mouseDown: of a window close button
is confusing the NSDocument.
So now, I have to return, and pepper my apps main loop with unmaintainable state machine logic to prevent user from executing various dangerous hotkeys.
Ok, so I grin and bear it, and run into yet another roadblock!
In previous OS versions/SDKs, [NSApp modalWindow] would return a window when it
was in this state. Now it doesn't! Grrrrr...
NSDocument has no API to test when it is in this state!
So, now there is no mechanism to globally check this state!
I have to add yet another state variable to my state machine.
Anyone have a cleaner solution for this problem that works in all OS versions and all present (and future) SDKs?
The better way is to save unsaved documents in chain. It is very easy:
// Catch application terminate event
-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
for (NSInteger i = 0; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:i];
if ([doc isDocumentEdited])
{
// Save first unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:i + 1])]; // Next document
return NSTerminateLater; // Wait until last document in chain will be saved
}
}
return NSTerminateNow; // All documents are saved or there are no open documents. Terminate.
}
...
// Document saving finished
-(void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
if (didSave) // Save button pressed
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
NSInteger nextIndex = [(__bridge NSNumber *)contextInfo integerValue];
for (NSInteger i = nextIndex; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:nextIndex];
if ([doc isDocumentEdited])
{
// Save next unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:nextIndex + 1])]; // Next document
return;
}
}
[NSApp replyToApplicationShouldTerminate:YES]; // All documents saved. Terminate.
}
else [NSApp replyToApplicationShouldTerminate:NO]; // Saving canceled. Terminate canceled.
}
Maybe this answer is too late to be useful but... In one of my apps I implemented -(IBAction)terminate:(id)sender in my NSApplication derived class which would conditionally call [super terminate] to actually close the application only if all open documents were cleanly saved. I may have found some of this in the Apple docs or other examples.
The terminate override will go through each document and either close it (because it's saved), or call the document's canCloseDocumentWithDelegate method in the NSDocument derived class passing 'self' and 'terminate' as the didSaveSelector. Since the terminate method falls through and does nothing except make the document present an NSAlert, the alert in the document class will callback and re-run the terminate routine if the user clicks YES or NO. If all documents are clean, the app will terminate since [super terminate] will get called. If any more dirty documents exist, the process repeats.
For example:
#interface MyApplication : NSApplication
#end
#implementation MyApplication
- (IBAction)terminate:(id)sender
{
//Loop through and find any unsaved document to warn the user about.
//Close any saved documents along the way.
NSDocument *docWarn = NULL;
NSArray *documents = [[NSDocumentController sharedDocumentController] documents];
for(int i = 0; i < [documents count]; i++)
{
NSDocument *doc = [documents objectAtIndex:i];
if([doc isDocumentEdited])
{
if(docWarn == NULL || [[doc windowForSheet] isKeyWindow])
docWarn = doc;
}
else
{
//close any document that doesn't need saving. this will
//also close anything that was dirty that the user answered
//NO to on the previous call to this routine which triggered
//a save prompt.
[doc close];
}
}
if(docWarn != NULL)
{
[[docWarn windowForSheet] orderFront:self];
[[docWarn windowForSheet] becomeFirstResponder];
[docWarn canCloseDocumentWithDelegate:self shouldCloseSelector:#selector(terminate:) contextInfo:NULL];
}
else
{
[super terminate:sender];
}
}
#end
Later in the document derived class:
typedef struct {
void * delegate;
SEL shouldCloseSelector;
void *contextInfo;
} CanCloseAlertContext;
#interface MyDocument : NSDocument
#end
#implementation MyDocument
- (void)canCloseDocumentWithDelegate:(id)inDelegate shouldCloseSelector:(SEL)inShouldCloseSelector contextInfo:(void *)inContextInfo
{
// This method may or may not have to actually present the alert sheet.
if (![self isDocumentEdited])
{
// There's nothing to do. Tell the delegate to continue with the close.
if (inShouldCloseSelector)
{
void (*callback)(id, SEL, NSDocument *, BOOL, void *) = (void (*)(id, SEL, NSDocument *, BOOL, void *))objc_msgSend;
(callback)(inDelegate, inShouldCloseSelector, self, YES, inContextInfo);
}
}
else
{
NSWindow *documentWindow = [self windowForSheet];
// Create a record of the context in which the panel is being
// shown, so we can finish up when it's dismissed.
CanCloseAlertContext *closeAlertContext = malloc(sizeof(CanCloseAlertContext));
closeAlertContext->delegate = (__bridge void *)inDelegate;
closeAlertContext->shouldCloseSelector = inShouldCloseSelector;
closeAlertContext->contextInfo = inContextInfo;
// Present a "save changes?" alert as a document-modal sheet.
[documentWindow makeKeyAndOrderFront:nil];
NSBeginAlertSheet(#"Would you like to save your changes?", #"Yes", #"Cancel", #"No", documentWindow, self,
#selector(canCloseAlertSheet:didEndAndReturn:withContextInfo:), NULL, closeAlertContext, #"%");
}
}
- (void)canCloseAlertSheet:(NSWindow *)inAlertSheet didEndAndReturn:(int)inReturnCode withContextInfo:(void *)inContextInfo
{
CanCloseAlertContext *canCloseAlertContext = inContextInfo;
void (*callback)(id, SEL, NSDocument *, BOOL, void* ) = (void (*)(id, SEL, NSDocument *, BOOL, void* ))objc_msgSend;
if (inAlertSheet) [inAlertSheet orderOut:self];
// The user's dismissed our "save changes?" alert sheet. What happens next depends on how the dismissal was done.
if (inReturnCode==NSAlertAlternateReturn)
{
//Cancel - do nothing.
}
else if (inReturnCode==NSAlertDefaultReturn)
{
//Yes - save the current document
[self saveDocumentWithDelegate:(__bridge id)canCloseAlertContext->delegate
didSaveSelector:canCloseAlertContext->shouldCloseSelector contextInfo:canCloseAlertContext->contextInfo];
}
else
{
// No - just clear the dirty flag and post a message to
// re-call the shouldCloseSelector. This should be
// the app:terminate routine.
[self clearDirtyFlag];
if (canCloseAlertContext->shouldCloseSelector)
{
(callback)((__bridge id)canCloseAlertContext->delegate,
canCloseAlertContext->shouldCloseSelector, self, YES, canCloseAlertContext->contextInfo);
}
}
// Free up the memory that was allocated in -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:.
free(canCloseAlertContext);
}
#end
And that should do it - No loops... no waiting...
I'm using what seems to be a simple invocation of the NSFileVersion class method removeOtherVersionsOfItemAtURL: inside a coordinated writing block for some iCloud conflict resolution.
When my devices go into 'spaz mode', which is a technical term for repeatedly opening and closing the application on a few devices, an EXC_BAD_ACCESS exception is thrown internally. Code snippet:
- (void)compareVersionChanges:(NSFileVersion *)version {
if (![DataLoader iCloudPreferenceEnabled]) {
NSLog(#"Ignoring iCloud changes (version comparison) based on user preference");
return;
}
NSLog(#"compareVersionChanges");
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^(void) {
NSError *readError = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:(id)self];
[coordinator coordinateReadingItemAtURL:[version URL] options:0 error:&readError byAccessor:^(NSURL *newURL) {
DataContext *loadedContext = nil;
NSData *data = [NSData dataWithContentsOfURL:newURL];
NSError *e = nil;
loadedContext = [self convertXmlDataToContext:data error:&e];
if (e) {
NSLog(#"Done loading, error: %#", e);
[[DataLoader applicationDelegate] displayError:e];
loadedContext = nil;
}
if (!loadedContext) {
return;
}
id appDelegate = [DataLoader applicationDelegate];
DataContext *inMemoryContext = nil;
if (appDelegate != nil && [appDelegate respondsToSelector:#selector(context)]) {
inMemoryContext = [appDelegate performSelector:#selector(context)];
}
if (inMemoryContext) {
NSLog(#"Performing iCloud context synchronizating...");
DataContextSynchronizer *synchronizer = [[DataContextSynchronizer alloc] init];
ChangeSet *changes = [synchronizer compareLocalContext:inMemoryContext andRemoteContext:loadedContext];
if ([[changes changes] count] > 0) {
[SelectionManager disable];
#synchronized(appDelegate) {
NSLog(#"Applying synchronization changes...");
[synchronizer applyChangeSet:changes toDataContext:inMemoryContext];
NSLog(#"Synchronization changes applied");
}
[SelectionManager enable];
if ([appDelegate respondsToSelector:#selector(setSkipRefreshSave:)]) {
[appDelegate performSelector:#selector(setSkipRefreshSave:) withObject:[NSNumber numberWithBool:YES]];
}
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^(void) {
[SelectionManager notifyListeners];
});
if ([appDelegate respondsToSelector:#selector(setSkipRefreshSave:)]) {
[appDelegate performSelector:#selector(setSkipRefreshSave:) withObject:[NSNumber numberWithBool:NO]];
}
[self save:[[DataLoader applicationDelegate] context]];
} else {
NSLog(#"No sync changes applicable.");
}
NSError *coordinateWriteRemoveError = nil;
[coordinator coordinateWritingItemAtURL:newURL options:NSFileCoordinatorWritingForDeleting error:&coordinateWriteRemoveError byAccessor:^(NSURL *theURL) {
theURL = [theURL copy];
NSError *removeOtherVersionsError = nil;
[NSFileVersion removeOtherVersionsOfItemAtURL:theURL error:&removeOtherVersionsError];
if (removeOtherVersionsError) {
NSLog(#"Error removing other versions: %#", removeOtherVersionsError);
}
}];
if (coordinateWriteRemoveError) {
NSLog(#"Error occurred coordinating write for deletion of other file versions: %#", coordinateWriteRemoveError);
}
}
}];
if (readError) {
NSLog(#"Done loading (outside block) error: %#", readError);
}
});
}
I thought a little syntax highlighting might make this easier to examine:
Link to image of code snippet and failure stack in Xcode
The error actually occurs on line 1404, and as you can see from the below screenshot, it's deep in Apple code territory.
Link to image of debugger
Before submitting a radar, I thought I'd check here to see if there's something I'm doing wrong? The extra [... copy] on line 1402 was just a quick check to make sure I'm not losing the reference to the block-provided argument, and will be removed.
Edit: An important note! I'm using ARC.
Edit 2: I've noticed that when calling:
[NSFileVersion otherVersionsOfItemAtURL:theURL]
The return value is nil, which indicates (via the documentation):
...or nil if there is no such file. The array does not contain the version object returned by the currentVersionOfItemAtURL: method.
So by checking the return value of this method before I make the call to removeOtherVersionsOfItemAtURL:, it has alleviated the issue. But I still find it strange that an EXC_BAD_ACCESS is thrown, rather than that method handling it properly.
I've noticed that when calling:
[NSFileVersion otherVersionsOfItemAtURL:theURL]
immediately prior to the call to removeOtherVersionsOfItemAtURL:, the return value is nil, which indicates (via the documentation):
Returns: An array of file version objects or nil if there is no such
file. The array does not contain the version object returned by the
currentVersionOfItemAtURL: method.
So by checking the return value of this method before I make the call to removeOtherVersionsOfItemAtURL:, it has alleviated the issue. But I still find it strange that an EXC_BAD_ACCESS is thrown by removeOtherVersionsOfItemAtURL:, rather than that method simply returning NO, or simply populating the provided NSError object.
I'll be filing a Radar and will update here when I hear back.
I'm trying to add Beeblex's new In App Purchase verification to my app, however i'm struggling passing a return value from within a block.
Here's the code I have now, and as you can see I set a BOOL value, then within the verification block I set the BOOL and return it at the end. However the return at the end is called before the block finishes, so what I need is to return the BOOL from within the block?
- (BOOL)verifyTransaction:(SKPaymentTransaction *)transaction
{
if (![BBXIAPTransaction canValidateTransactions]) {
return YES; // There is no connectivity to reach the server
}
BOOL __block toReturn = YES;
BBXIAPTransaction *bbxTransaction = [[BBXIAPTransaction alloc] initWithTransaction:transaction];
bbxTransaction.useSandbox = YES;
[bbxTransaction validateWithCompletionBlock:^(NSError *error) {
if (bbxTransaction.transactionVerified) {
if (bbxTransaction.transactionIsDuplicate) {
// The transaction is valid, but duplicate - it has already been sent to Beeblex in the past.
NSLog(#"Transaction is a duplicate!");
[FlurryAnalytics logEvent:#"Transaction duplicate!"];
toReturn = NO;
} else {
// The transaction has been successfully validated and is unique
NSLog(#"Transaction valid data:%#",bbxTransaction.validatedTransactionData);
[FlurryAnalytics logEvent:#"Transaction verified"];
toReturn = YES;
}
} else {
// Check whether this is a validation error, or if something went wrong with Beeblex
if (bbxTransaction.hasConfigurationError || bbxTransaction.hasServerError || bbxTransaction.hasClientError) {
// The error was not caused by a problem with the data, but is most likely due to some transient networking issues
NSLog(#"Transaction error caused by network, not data");
[FlurryAnalytics logEvent:#"Transaction network error"];
toReturn = YES;
} else {
// The transaction supplied to the validation service was not valid according to Apple
NSLog(#"Transaction not valid according to Apple");
[FlurryAnalytics logEvent:#"Transaction invalid!!"];
toReturn = NO;
}
}
}];
NSLog(#"toReturn: %#",toReturn ? #"Yes" : #"No");
return toReturn;
}
If I simply put return = NO; inside the block, I get compiler warnings of Incompatible block pointer types, and control may reach end of non-void block.
Beeblex developer here. The code inside -validateWithCompletionBlock: execute asynchronously (in the background, it needs to talk to our servers, so there's no point blocking your app completely while we wait for the Internet to do its thing).
Therefore, you need to rethink your approach to validating your receipts. Right now you, general workflow is:
Complete purchase
Call Beeblex
Wait for response
Check boolean value
But #3 returns right away, so this will never work. You should consider doing something like this:
Complete purchase
Show a “Please wait…” view, or something similar that advises the user that you're unlocking whatever they've purchased.
Call Beeblex
Inside the block, determine whether the validation succeeded or not, and then act to unlock the content from there.
Sit idle until called by the block
Here's a quick-and-dirty example. I didn't compile it, so it probably has a few bugs, but it should give you the idea behind the intended usage pattern.
- (void) showWaitView {
// Display a wait view
}
- (void) hideWaitView {
// Hide the wait view
}
- (void) completeValidationWithValidateReceiptData:(NSDictionary *) receipt isRecoverableError(BOOL) isRecoverableError {
[self hideWaitView];
if (receipt) {
// Unlock the content, tell the user
} else {
if (isRecoverableError) {
// Probably a network error of some kind. Tell user they need to be connected,
// and ask them to do it again.
} else {
// Keep the content locked, tell the user something went wrong
}
}
}
- (void) validateReceipt:(SKPaymentTransaction *) transaction {
if (![BBXIAPTransaction canValidateTransactions]) {
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:YES];
return;
}
BBXIAPTransaction *bbxTransaction = [[BBXIAPTransaction alloc] initWithTransaction:transaction];
bbxTransaction.useSandbox = YES;
[bbxTransaction validateWithCompletionBlock:^(NSError *error) {
if (bbxTransaction.transactionVerified) {
if (bbxTransaction.transactionIsDuplicate) {
// The transaction is valid, but duplicate - it has already been sent to Beeblex in the past.
[FlurryAnalytics logEvent:#"Transaction duplicate!"];
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:NO];
} else {
// The transaction has been successfully validated and is unique
[FlurryAnalytics logEvent:#"Transaction verified"];
[self completeValidationWithValidateReceiptData:bbxTransaction.validatedTransactionData isRecoverableError:NO];
}
} else {
// Check whether this is a validation error, or if something went wrong with Beeblex
if (bbxTransaction.hasConfigurationError || bbxTransaction.hasServerError || bbxTransaction.hasClientError) {
// The error was not caused by a problem with the data, but is most likely due to some transient networking issues
[FlurryAnalytics logEvent:#"Transaction network error"];
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:YES];
} else {
// The transaction supplied to the validation service was not valid according to Apple
[FlurryAnalytics logEvent:#"Transaction invalid!!"];
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:NO];
}
}
}];
}
<3 blocks
- (void)verifyTransaction:(SKPaymentTransaction *)transaction completionHandler:(void (^)(BOOL flag))completionHandler
{
if (![BBXIAPTransaction canValidateTransactions]) {
completionHandler(YES); // There is no connectivity to reach the server
}
BBXIAPTransaction *bbxTransaction = [[BBXIAPTransaction alloc] initWithTransaction:transaction];
bbxTransaction.useSandbox = YES;
[bbxTransaction validateWithCompletionBlock:^(NSError *error) {
if (bbxTransaction.transactionVerified) {
if (bbxTransaction.transactionIsDuplicate) {
// The transaction is valid, but duplicate - it has already been sent to Beeblex in the past.
NSLog(#"Transaction is a duplicate!");
[FlurryAnalytics logEvent:#"Transaction duplicate!"];
completionHandler(NO);
} else {
// The transaction has been successfully validated and is unique
NSLog(#"Transaction valid data:%#",bbxTransaction.validatedTransactionData);
[FlurryAnalytics logEvent:#"Transaction verified"];
completionHandler(YES);
}
} else {
// Check whether this is a validation error, or if something went wrong with Beeblex
if (bbxTransaction.hasConfigurationError || bbxTransaction.hasServerError || bbxTransaction.hasClientError) {
// The error was not caused by a problem with the data, but is most likely due to some transient networking issues
NSLog(#"Transaction error caused by network, not data");
[FlurryAnalytics logEvent:#"Transaction network error"];
completionHandler(YES);
} else {
// The transaction supplied to the validation service was not valid according to Apple
NSLog(#"Transaction not valid according to Apple");
[FlurryAnalytics logEvent:#"Transaction invalid!!"];
completionHandler(NO);
}
}
}];
}
Then use
[instance verifyTransaction:transaction completionHandler:^(BOOL flag) {
if (flag) {
// YOUR CODE HERE
}
}];
instead of
if ([instance verifyTransaction:transaction]) {
// YOUR CODE HERE
}
Having a heck of a time with this one.
I've got a super-simple Cocoa app containing one WebView, a WebScripting API defined in the page, and a single NSObject defined on that API. When I turn on the debugger tools (in the embedded WebView), I can see the API on the JavaScript window object, and I can see my "api" property defined on that -- but when I call the API's "get" method, the arguments aren't being serialized -- when the Obj-C method gets called, the arguments are missing. See below, which hopefully illustrates:
I've combed through the docs, I've (apparently) set the appropriate methods to expose everything that needs to be exposed, and I can see the method being called. There has to be something stupid I'm missing, but as a relative newbie to this environment, I'm not seeing it.
Thanks in advance for your help!
Have you set WebKitDeveloperExtras to YES in your default user defaults when you send -[NSUserDefaults registerDefaults:]?
Depending on what version of Xcode you're using you could be getting a known error. If you're using LLDB on anything but the most recent version, it might not be giving you the right variables in the debugger. The solution has been to use GDB instead of LLDB until Apple fixes the problem. But I think they fixed the problem in the latest version. I'd change the debugger to use GDB and see if you're getting the right variables in Xcode. (Product-> Edit Scheme...-> Run -> Debugger). I came across this problem in iOS, though, so I don't know its applicability to OSX. Worth a try anyway.
I originally came across the problem here: https://stackoverflow.com/a/9485349/1147934
I process javascript in the main thread of my app from a local file stored in the apps directory. I check for beginning and ending tokens for the js functions I am executing and whether the function contains a variable.
Hopefully this can give you some good ideas for your issue. You could also do alerts in the js to see if the values post correctly as you run the app (I am sure you thought of that already, but it's worth mentioning.) Happy coding! I hope this helps!
in the .h file define:
NSMutableString *processedCommand;
NSArray *commandArguments;
In the .m file:
// tokens
#define kOpenToken #"<%%"
#define kCloseToken #"%%>"
// this will throw
-(void)executeJScriptCommand:(NSString *)aCommand {
[self performSelectorOnMainThread:#selector(executeThisCommand:) withObject:aCommand waitUntilDone:YES];
}
// this will throw
-(NSString *)executeCommand:(NSString *)command {
NSString *aCommand = [[[command stringByReplacingOccurrencesOfString:kOpenToken withString:#""]
stringByReplacingOccurrencesOfString:kCloseToken withString:#""]
stringByTrimmingLeadingAndTrailingWhitespaces];
if ([aCommand hasPrefix:#"="])
{
// variable. get value
[self getVariableFromCommand:aCommand];
}
else {
[self executeThisCommand:aCommand];
}
NSString *returnValue = [NSString stringWithString:processedCommand];
self.processedCommand = nil;
self.commandArguments = nil;
return returnValue;
}
-(void)executeThisCommand:(NSString *)aCommand {
BOOL hasError = NO;
// clear result
self.processedCommand = nil;
self.commandArguments = nil;
BOOL isFromJS = NO;
NSString *function = nil;
NSMutableArray *commandParts = nil;
#try {
// first, break the command into its parts and extract the function that needs to be called, and the (optional) arguments
commandParts = [[NSMutableArray alloc] initWithArray:[aCommand componentsSeparatedByString:#":"]];
if ([[[commandParts objectAtIndex:0] lowercaseString] isEqualToString:#"js-call"]) {
isFromJS = YES;
[commandParts removeObjectAtIndex:0];
}
// get our function, arguments
function = [[commandParts objectAtIndex:0] retain];
[commandParts removeObjectAtIndex:0];
if ([commandParts count] > 0){
if (isFromJS == YES) {
NSString *arguments = [[commandParts objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
if ([arguments length] > 0) {
self.commandArguments = [arguments JSONValue];
}
}
else {
self.commandArguments = [NSArray arrayWithArray:commandParts];
}
}
// build invoke
SEL sel = NSSelectorFromString(function);
if ([self respondsToSelector:sel]) {
[self performSelectorOnMainThread:sel withObject:nil waitUntilDone:YES];
// using invocation causes a SIGABORT because the try/catch block was not catching the exception.
// using perform selector fixed the problem (i.e., the try/catch block now correctly catches the exception, as expected)
}
else {
[appDelegate buildNewExceptionWithName:#"" andMessage:[NSString stringWithFormat:#"Object does not respond to selector %#", function]];
}
}
#catch (NSException * e) {
hasError = YES;
[self updateErrorMessage:[NSString stringWithFormat:#"Error processing command %#: %#", aCommand, [e reason]]];
}
#finally {
[function release];
[commandParts release];
}
if (hasError == YES) {
[appDelegate buildNewExceptionWithName:#"executeThisCommand" andMessage:self.errorMessage];
}
}
// this can return nil
-(NSString *)getQueryStringValue:(NSString *)name {
NSString *returnValue = nil;
if (queryString != nil) {
returnValue = [queryString objectForKey:[name lowercaseString]];
}
return returnValue;
}
Ok, lets start with the code. I am looping through a returned array of dictionaries and creating (or updating) objects based on them. In this method I'm trying find or create a new entity. And then if the object is supposed to be deleted, I'd like to do so and not waste time updating it with the new information.
- (void)updateOrCreateObjectWith:(NSDictionary*)dictionary {
RKManagedObjectStore *objectStore = ((MyAppDelegate*)[[UIApplication sharedApplication] delegate]).objectStore;
id updateObject = (NSManagedObject*)[objectStore findOrCreateInstanceOfEntity:[resource entity] withPrimaryKeyAttribute:#"myID" andValue:[dictionary objectForKey:#"id"]];
[updateObject setMyID:[dictionary objectForKey:#"id"]];
// if marked for deletion, delete it now
if ([[dictionary objectForKey:#"deleted_at"] isKindOfClass:[NSString class]]) {
if ([updateObject isNew]){
NSError *error = nil;
[objectStore.managedObjectContext save:&error];
if (error) {
NSLog(#"error saving before delete: %#",error);
return;
}
// [objectStore.managedObjectContext deleteObject:updateObject];
// [objectStore.managedObjectCache delete:updateObject];
}
else {
[objectStore.managedObjectContext deleteObject:updateObject];
}
return;
}
[updateObject updateWith:dictionary];
}
The part to be aware of is the deleted_at section with the (1) save section, (2) delete object from context, and (3) delete object from cache. I have tried a few combinations of those three but I don't get the desired results.
If I delete it from the cache (just #3):
The objects get saved but they have no attributes.
If I delete it from the managed context (just #2) I get:
NSUnderlyingException=Cannot update object that was never inserted.
Since it was never inserted, I thought I'd save it and then delete it (#1 and #2), but then I get:
*** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xed27810 <x-coredata://4EE6AD5A-CC34-460A-A97A-0909454126A4/User/p166>''
So what is the proper way to remove a "new" object from NSMangedObjectContext?
It's easier to get the RKManagedObjectStore instance with [[RKObjectManager sharedManager] objectStore] (assuming you want the shared one, which it seems like you do since you're calling your app delegate).
Check for the deleted_at key before you ever create an NSManagedObject. This code assumes you're converting to type Resource, which is a subclass of NSManagedObject. It's untested but should give you an idea of what you should be doing.
- (void)updateOrCreateObjectWith:(NSDictionary*)dictionary {
RKManagedObjectStore *objectStore = [[RKObjectManager sharedManager] objectStore];
//get a reference to the object
Resource *resource = [Resource findFirstByAttribute:#"myID" withValue:[dictionary objectForKey:#"id"]];
//see if "deleted_at" exists in dictionary
if ([[dictionary objectForKey:#"deleted_at"] isKindOfClass:[NSString class]])
{
//check to see if object exists in the context
if(resource)
{
//if it exists, delete it
[objectStore.managedObjectContext deleteObject:resource];
}
} else {
//no "deleted at", so create the object
if (!resource) {
//resource is nil (it doesn't exist in the context), so we need to create it
resource = [Resource object];
}
[resource updateWith:dictionary];
}
NSError *error = nil;
[objectStore.managedObjectContext save:&error];
if (error) {
NSLog(#"error saving before delete: %#",error);
}
}
You want to avoid creating managed objects unless necessary. The best strategy is to follow this pseudo code:
NSManagedObject *existingObject = ...; // fetch the object
if (existingObject) {
if (deleted) {
[self.managedObjectContext deleteObject: existingObject];
}
} else {
if (!deleted) {
// create the object, insert it into the MOC, set the object properties
}
}