Important update: I found out that most part of my question was based on a false premise (see my answer below). Notifications actually got to the receiver, they just got there too fast. (Although, it still doesn't explain why the behavior with breakpoint and without it was different.)
I'm developing the app that calculates the hashes of files given to it. The calculation takes place in SHHashComputer. It's an abstract class (well, intended to be abstract, as there are no abstract classes in Objective C) that takes the file path and creates an NSInvocationOperation. It, in turn, calls the method (void)computeAndSendHash, which uses the file path saved in the object to compute hash and sends it as notification. The actual computing takes place in (NSString*)computeHash method that child classes need to override.
Here's SHHashComputer.m:
- (NSString*)computeHash {
return [NSString stringWithFormat:#"unimplemented hash for file %#", self.path];
}
- (void)computeAndSendHash {
NSString *result = [self computeHash];
NSString *notificationName = [NSString stringWithFormat:#"%#%#",
gotResultNotification,
self.hashType];
[[NSNotificationCenter defaultCenter] postNotificationName:notificationName
object:result];
self.operation = nil;
}
And here's SHMD5Computer.m (the child class of SHHashComputer):
- (NSString*)computeHash {
return #"MD5 test"; // it actually doesn't matter what it returns
}
I won't bother you with the receivers of notification. Let's just say that as long as I comment out the computeHash method in SHMD5Computer.m everything works just fine: the notification with text "unimplemented ..." is received & displayed in GUI. But if I don't — then it gets really interesting.
If I don't set up any breakpoints, the notification just never comes. However, if I set up a breakpoint at the declaration of computeHash in SHMD5Computer.h and then step over until the line 'self.operation = nil', and continue execution at that point, the notification gets to destination. If I don't stop there, the debugger suddenly switches to the state as if it isn't debugging anything, and the app freezes.
I don't think that 'WTF' is a good form for a question here, so let me put it this way: am I missing something? Are there errors in my code? What can cause this type of behavior in xcode? How can I fix this?
(If you'll want to get all my code to reproduce it, I'll gladly give it to you.)
More experiments:
If I continute execution exactly after stopping at breakpoint, the application encounters EXC_BAD_ACCESS error in the code that receives the notification, at the last line:
id newResult = [newResultNotification object];
if (newResult == nil)
[NSException raise:#"No object"
format:#"Expected object with notification!"];
else if (![newResult isKindOfClass:[NSString class]])
[NSException raise:#"Not NSString"
format:#"Expected NSString object!"];
else
self.result = (NSString*) newResult;
[self.textField setStringValue:self.result];
When I tried to reproduce the previous experiment, something even stranger happenned. In my debug setup, I have two hash computer objects: one SHMD5HashComputer (which we're talking about), and one SHHashComputer (which, of course, produces the "unimpemented" hash). In all previous experiments, as long as app didn't crash, the notification form SHHashComputer always successfully arrived. But in this case, both notifications didn't arrive, and the app didn't crash. (All the steps are exactly the same as in previous one).
As Josh Caswell pointer out in the comments, I wasn't using the notifications correctly. I should've sent the object itself as notification object, as described in documentation. I fixed that, and I'm getting exactly the same results. (Which means that I fixed it correctly, because sometimes the notifications work correctly, and also that it wasn't the problem).
More updates:
The notification that I'm sending should arrive at SHHashResultViewController. That's how I create it and register for notification:
- (id)initWithHashType:(NSString *)hashType {
self = [self initWithNibName:#"SHHashResultView" bundle:[NSBundle mainBundle]];
if (self) {
[self setHashType:hashType];
}
return self;
}
- (void)setHashType:(NSString *)hashType {
[self.label setStringValue:[NSString stringWithFormat:#"%#:", hashType]];
_hashType = hashType;
NSString *notificationName = [NSString stringWithFormat:#"%#%#",
gotResultNotification,
_hashType];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(gotResult:)
name:notificationName
object:nil];
}
Actually, the question was based on a false premise. I thought that notification never came through because I never saw the information displayed in the GUI; however, my error was in the code of controllers (not published there) which made possible the situation in which the GUI first got results of hash calculation and only after that got information about a new input — which resulted in deleting all the text and activating progress animation.
Related
I try to subscribe to a signal with throttle, but it never executes.
I have a UISearchController (Attention: UISearchController from iOS8, not the older UISearchDisplayController, which works quiet better and has thousands of working tutorials and examples in the web) and want to make API-Requests while the user is typing.
To let the traffic being low, i don't want to start API-Requests with each key the user is pressing, but when the user stops for a while, say 500 ms after the last keypress.
Since we're unable to reference the TextField in the SearchBar of the UISearchController, we use the delegates from UISearchController:
To get the latest typed text of the Textfield in the Searchbar, I use this:
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *searchText = searchController.searchBar.text;
// strip out all the leading and trailing spaces
NSString *strippedString = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if([strippedString isEqualToString:self.currentFilter]) {
return;
}
self.currentFilter = strippedString;
}
The property currentFilter keeps the current search string.
Also, i have a RACObserve on the currentFilter-Property to react on every change which is made to this property:
[RACObserve(self, currentFilter) subscribeNext:^(NSString* x) {
NSLog(#"Current Filter: %#", x);
// do api calls and everything else
}];
Now i want to throttle this signal. But when i implement the call to throttle, nothing happens. SubscribeNext will never be called:
[[RACObserve(self, currentFilter) throttle:500] subscribeNext:^(NSString* x) {
NSLog(#"%#", x); // will never be called
}];
How to achieve to throttle inputs in a searchbar? And what is wrong here?
UPDATE
i found a workaround besides ReactiveCocoa thanks to #malcomhall. I moved the code within the updateSearchResultsForSearchController-delegate method into a separate method and schedule it with performSelector and cancel this scheduler with cancelPreviousPerformRequestsWithTarget.
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(useTextSearchQuery) object:nil];
[self performSelector:#selector(useTextSearchQuery) withObject:nil afterDelay:1.0];
}
Anyway, i want still understand how "throttle" from ReactiveCocoa is working and why not in this case :)
-throttle: accepts an NSTimeInterval, which is a floating-point specification of seconds, not milliseconds.
Given the code in the question, I expect you would see results after 500 seconds have elapsed.
I see a crash in Crashlytics than happens to my users sometimes. The crash happens when presenting UIActivityViewController in the last line of the following code:
NSData* snapShot = ... ;
UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:[NSArray arrayWithObjects:activityTextsProvider, snapShot ,nil] applicationActivities:[NSArray arrayWithObjects:customActivityA, customActivityB, customActivityC, nullptr]];
activityViewController.excludedActivityTypes = [NSArray arrayWithObjects:UIActivityTypePrint, UIActivityTypeAssignToContact, UIActivityTypeMail, UIActivityTypeCopyToPasteboard, nil];
activityViewController.popoverPresentationController.sourceView = self.myButton;
activityViewController.popoverPresentationController.sourceRect = self.myButton.bounds;
activityViewController.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError)
{
...
};
[self presentViewController:activityViewController animated:YES completion:nil];
I perform this in the main thread and unable to reproduce this crash locally. What could be the reason of this crash?
Edit: I changed nullptr to nil and the issue still happened. I managed to reproduce the issue: the crash happens only if before opening the activity controller i showed a UIMenuController. When creating UIActivityViewController it is not nil, but when presenting the controller i see the crash in the presentViewController line and the activity controller there is shown as nil
József addressed the use of nullptr in comments, and Fogh is spot-on that the actual crash log is important (please edit your question and post the full crash log), but I'd like to point out something else.
You're assuming your call to initialize activityViewController is succeeding. You should code defensively (by assuming everything that can fail probably will fail and testing for this at runtime). Wrap the rest of the configuration and presentation inside an if (activityViewController != nil) {} condition (you should probably have an else with proper error handling/reporting too) so you're properly detecting an all-out initialization failure for multiple reasons (like a misplaced nib, missing resource, etc.).
In your case, I think it's likely the initialization is failing because your class is doing something with a faulty array, as József's nullptr catch suggests. Perhaps you're using one or more pre-C++11 c libraries / compiling with a non-C11/gnu11 "C Language Dialect" build setting and nullptr is not equivalent to nil, leading to strange results in a supposed-to-be-nil-terminated array?
Note: If that turns out to be the case, I'll happily take an upvote but would rather József post his comment as an answer so you can give him proper credit. (Feel free to edit this request out of my answer if/when that happens.)
I was under the impression that when using bindings (been following this tutorial despite being outdated. http://cocoadevcentral.com/articles/000085.php - You can use it to see what I'm doing) the Persistent Store would automagically save the changes you make. In fact, though it was hours ago and I wouldn't be surprised if I'm now going mad, I got it working, and when I made a change it would persist on rebuilding the app.
However, the test app I've built following the tutorial no longer saves and despite showing the changes I make within the app, they disappear once I re-run the app. I've been checking the Core Data debug menu and nothing happens when I press the "+" button which is set up to the "Add" method of my NSArrayController. I know it's accessing my data model too as my textField for the Title (again, see the tutorial so you know what I'm referring to) adopts the default text I put in the DataModel section. The only thing missing therefore is the actual saving.
So my real question is, based on the tutorial, what part of the bindings actually makes the managedObjectContext save? Is there a flag or something that isn't checked?
I don't know if it's important or not, but there were differences between the tutorial and my project, mainly that the NSArrayControllers are bound to "App Delegate"with a Model Key Path of "self.managedObjectContext". Also, I removed all the relationships in an attempt to whittle down the issue.
Any help would be greatly appreciated.
Regards,
Mike
UPDATE: Here are some pictures that show the bindings.
How I set up the NSArrayController:
Here is how is how my Data Model Looks:
Lastly, this is how I set up the TextFields to update the NSArrayControllers:
I hope this helps to get a an ideas as to the set up.
Thanks,
Mike
Could you check to make sure you've copied all the Core Data boiler-plate code from the source code of the tutorial you mentioned.
Specifically this part in the App Delegate:
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
NSError *error;
NSManagedObjectContext *context;
int reply = NSTerminateNow;
context = [self managedObjectContext];
if (context != nil) {
if ([context commitEditing]) {
if (![context save:&error]) {
// This default error handling implementation should be changed to make sure the error presented includes application specific error recovery. For now, simply display 2 panels.
BOOL errorResult = [[NSApplication sharedApplication] presentError:error];
if (errorResult == YES) { // Then the error was handled
reply = NSTerminateCancel;
} else {
// Error handling wasn't implemented. Fall back to displaying a "quit anyway" panel.
int alertReturn = NSRunAlertPanel(nil, #"Could not save changes while quitting. Quit anyway?" , #"Quit anyway", #"Cancel", nil);
if (alertReturn == NSAlertAlternateReturn) {
reply = NSTerminateCancel;
}
}
}
} else {
reply = NSTerminateCancel;
}
}
return reply;
}
If it's there, changes will be saved when the app is terminated normally. Pressing the 'stop' button in Xcode will terminate the app immediately, without going through the method mentioned above.
My guess is that you are not going mad, but first exited the app properly and have been pressing the 'stop' button later ;).
I am totally stuck on this one. My basic problem is that my:
- (NSFetchedResultsController *)fetchedResultsController
Method is crashing when it tries to read my Core Core entity, as the managedObjectContext / UIManagedDocument is nil. At the moment I think it is because my UIManagedDocument is not open / ready. So for the last 3 hours I have been trying to make it so my delegate method is not fired until the document is open.
This is the code that I am using to get the document:
if (!self.document) {
[[CATManagedDocumentHandler sharedDocumentHandler] performWithDocument:^(UIManagedDocument *document) {
self.document = document;
}];
}
This works fine at any other place in my app, but it seems as the opening process is just not quick enough for the delegate methods in my tableView.
Links I have looked at so far:
http://omegadelta.net/2011/05/10/how-to-wait-for-ios-methods-with-completion-blocks-to-finish/
Executing Code Block In Place Of #selector
About calling of dispatch_queue_t and dispatch_sync
Grand Central Dispatch (GCD) vs. performSelector - need a better explanation
GCD to perform task in main thread
iOS - how to be notified when a thread (using GCD) ends it's job
I have tried: Blocking main thread until I get NSNotification (set up in CATManagedDocumentHandler) & Blocking main thread until I get a block call back.
Neither of these work. My app just freezes. Am I think about this wrongly? How can I get the delegate method to wait until my document is open / ready? Or is there a different approach I should be taking with this?
Thanks
Carl.
ok, when your app first starts, I would suggest checking whether 1. your database exists (if it doesn't, you alloc init it) and 2. if the document doesn't exist (on disk), is closed or is open.
here's how you could do this:
looks like you are using a singleton for your database, so when your first view controller comes up, check whether the Managed Document has been alloc inited, so in ViewWillAppear:
if (!dataBase)
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Your DB Name"];
self.dataBase=[[UIManagedDocument alloc]initWithFileUrl:url];
}
so now if the database (UIManagedDocument) has been alloc inited, it still does not mean that the actual database file has been created on disk. You have to check for it as follows (and you could do this in a setter for your database or in ViewDidLoad, but don't do it in another thread because it won't work)
so you are checking for 3 cases here: does not exist
if (![[NSFileManager defaultManager]fileExistsAtPath:[yourManagedDocumentFromSingleton.fileUrl path])
{
[[NSFileManager defaultManager]fileExistsAtPath:[yourManagedDocumentFromSingleton saveToUrl:[[NSFileManager defaultManager]fileExistsAtPath:[yourManagedDocumentFromSingleton.fileUrl forSaveOperation:UIDocumentSaveForCreating completionHandler:
^(BOOL success){ now you can use your uidocument and its managed object context}];
}
if file does exist but is closed:
else if (yourManagedDocumentFromSingleton.documentState==UIDocumentStateClosed)
{
[yourManagedDocumentFromSingleton openWithCompletionHandler:
^(BOOL success) {use your document here}];
}
finally if the document is already open, just use it:
else if (yourManagedDocumentFromSingleton.documentState=UIDocumentStateNormal)
{
//do whatever with the document
}
one important thing to mention is that UIDocument is not thread safe. So it must be used and checked in the same thread that it was created (presumably main thread here). Otherwise it will not work.
I don't know the exact structure of your view controller or singleton but if you follow these steps it will work.
PS. Also make sure that once your doc is up and running and you're adding items to it or removing, save it after each operation so your NSFetchedResultsController gets updated. CoreData does have autosave but I found that I had to manually save for things to work properly. You can save with (from previous method):
forSaveOperation:UIDocumentSaveForOverwriting
My app is using an NSFetchedResultsController tied to a Core Data store and it has worked well so far, but I am now trying to make the update code asynchronous and I am having issues. I have created an NSOperation sub-class to do my updates in and am successfully adding this new object to an NSOperationQueue. The updates code is executing as I expect it to and I have verified this through debug logs and by examining the SQLite store after it runs.
The problem is that after my background operation completes, the new (or updated) items do not appear in my UITableView. Based on my limited understanding, I believe that I need to notify the main managedObjectContext that changes have occurred so that they may be merged in. My notification is firing, nut no new items appear in the tableview. If I stop the app and restart it, the objects appear in the tableview, leading me to believe that they are being inserted to the core data store successfully but are not being merged into the managedObjectContext being used on the main thread.
I have included a sample of my operation's init, main and notification methods. Am I missing something important or maybe going about this in the wrong way? Any help would be greatly appreciated.
- (id)initWithDelegate:(AppDelegate *)theDelegate
{
if (!(self = [super init])) return nil;
delegate = theDelegate;
return self;
}
- (void)main
{
[self setUpdateContext:[self managedObjectContext]];
NSManagedObjectContext *mainMOC = [self newContextToMainStore];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:updateContext];
[self setMainContext:mainMOC];
// Create/update objects with mainContext.
NSError *error = nil;
if (![[self mainContext] save:&error]) {
DLog(#"Error saving event to CoreData store");
}
DLog(#"Core Data context saved");
}
- (void)contextDidSave:(NSNotification*)notification
{
DLog(#"Notification fired.");
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[[delegate managedObjectContext] performSelectorOnMainThread:selector
withObject:notification
waitUntilDone:YES];
}
While debugging, I examined the notification object that is being sent in contextDidSave: and it seems to contain all of the items that were added (excerpt below). This continues to make me think that the inserts/updates are happening correctly but somehow the merge is not being fired.
NSConcreteNotification 0x6b7b0b0 {name = NSManagingContextDidSaveChangesNotification; object = <NSManagedObjectContext: 0x5e8ab30>; userInfo = {
inserted = "{(\n <GCTeam: 0x6b77290> (entity: GCTeam; id: 0xdc5ea10 <x-coredata://F4091BAE-4B47-4F3A-A008-B6A35D7AB196/GCTeam/p1> ; data: {\n changed =
The method that receives your notification must indeed notify your context, you can try something like this, which is what I am doing in my application:
- (void)updateTable:(NSNotification *)saveNotification
{
if (fetchedResultsController == nil)
{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
//Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
else
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
// Reload your table view data
[self.tableView reloadData];
}
}
Hope that helps.
Depending on the specifics of what you are doing, you may be going about this the wrong way.
For most cases, you can simply assign a delegate using NSFetchedResultsControllerDelegate. You provide an implementation for one of the methods specified in "respondingToChanges" depending on your needs, and then send the tableView a reloadData message.
The answer turned out to be unrelated to the posted code which ended up working as I expected. For reasons that I am still not entirely sure of, it had something to do with the first launch of the app. When I attempted to run my update operation on launches after the Core Data store was created, it worked as expected. I solved the problem by pre-loading a version of the sqlite database in the app so that it did not need to create an empty store on first launch. I wish I understood why this solved the problem, but I was planning on doing this either way. I am leaving this here in the hope that someone else may find it useful and not lose as much time as I did on this.
I've been running into a similar problem in the simulator. I was kicking off an update process when transitioning from the root table to the selected folder. The update process would update CoreData from a web server, save, then merge, but the data didn't show up. If I browsed back and forth a couple times it would show up eventually, and once it worked like clockwork (but I was never able to get that perfect run repeated). This gave me the idea that maybe it's a thread/event timing issue in the simulator, where the table is refreshing too fast or notifications just aren't being queued right or something along those lines. I decided to try running in Instruments to see if I could pinpoint the problem (all CoreData, CPU Monitor, Leaks, Allocations, Thread States, Dispatch, and a couple others). Every time I've done a "first run" with a blank slate since then it has worked perfectly. Maybe Instruments is slowing it down just enough?
Ultimately I need to test on the device to get an accurate test, and if the problem persists I will try your solution in the accepted answer (to create a base sql-lite db to load from).