I got the following method in a singleton/shared instance and would like update the user with the progress of fetching emails.
- (void)getAllImapEmailsForMailbox:(NSString *)mailbox completionBlock:(void (^)(BOOL success, NSString *errorString, NSArray *emails))block
First I'm unlocking the API with a key, then I'm connecting to their IMAP server, then I'm logging in with their details, then I select a particular mailbox, then I loop through whole mailbox to download the messages.
So I would like to know how would I get these updates from a singleton method to my view controller displaying these messages in a UIAlertView for example.
eg. Connecting.. Logging in.. Selecting mailbox.. Downloading mail 1 of 69..
I'm currently only doing 1 message saying Downloading Emails, but it takes too long and don't want the user to think the app is hanging and not doing anything. This is what I'm doing:
UIAlertView *loadingView = [[UIAlertView alloc] initWithTitle:#"Downloading Emails..." message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:nil];
[loadingView show];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self getAllEmailsForMailbox:#"Inbox"];
dispatch_async(dispatch_get_main_queue(), ^{
[loadingView dismissWithClickedButtonIndex:-1 animated:YES];
});
});
Thanks!
Add another block param called something like statusBlock. Give the block a string param that will contain a status message. How you get the status to send this block depends on the details of how you do the listed steps, but at an outline level...
- (void)getAllImapEmailsForMailbox:(NSString *)mailbox
statusBlock:(void (^)(NSString *)statusBlock
completionBlock:(void (^)(BOOL success, NSString *errorString, NSArray *emails))block {
statusBlock(#"connecting");
// do connecting stuff
NSInteger numberOfMessagesToFetch = // find this out however you do now
statusBlock([NSString stringWithFormat:#"fetching %d messages", numberOfMessagesToFetch]);
// fetch mail, and so on
On the caller side:
[mailSingleton getAllImapEmailsForMailbox:#"mailbox"
statusBlock:^(NSString *message) { // update UI with message }
completionBlock: ... { // update UI now that you're complete }];
Also , looking at your edit, is it possible to hide the asynch stuff in this method? Would be a lot friendlier for the caller, who could just pass the blocks and assume the asynch.
Related
I am using CNContacts to fetch phonebook contacts in my iOS device.
When I have a small number of contacts in my phone (say, 50) the contacts are fetched easily.
However, when I have a lot of contacts (say 500-700) it hangs/waits for a long time to fetch these contacts from iOS phonebook into my App's array.
Previously I used https://github.com/Alterplay/APAddressBook which was a fast library for the previous Apple framework, but now I am using the latest Apple framework.
My code to fetch contacts is ....
#pragma mark - Get All Contacts....
-(void)getAllContactsWithNewFrameworkOfApple {
NSMutableArray *_contactsArray = [[NSMutableArray alloc]init];
CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
if (status == CNAuthorizationStatusDenied || status == CNAuthorizationStatusDenied) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:#"This app previously was refused permissions to contacts; Please go to settings and grant permission to this app so it can use contacts" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:nil]];
// [self presentViewController:alert animated:TRUE completion:nil];
return;
}
CNContactStore *store = [[CNContactStore alloc] init];
[store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
// make sure the user granted us access
if (!granted) {
dispatch_async(dispatch_get_main_queue(), ^{
// user didn't grant access;
// so, again, tell user here why app needs permissions in order to do it's job;
// this is dispatched to the main queue because this request could be running on background thread
});
return;
}
NSError *fetchError;
CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:#[CNContactIdentifierKey,CNContactGivenNameKey,CNContactFamilyNameKey,CNContactEmailAddressesKey,CNContactPhoneNumbersKey]];
BOOL success = [store enumerateContactsWithFetchRequest:request error:&fetchError usingBlock:^(CNContact *contact, BOOL *stop) {
[_contactsArray addObject:contact];
NSLog(#"I am %#", _contactsArray);
}];
if (!success) {
NSLog(#"error = %#", fetchError);
}else {
NSLog(#"End with Success %#", _contactsArray);
[self finalUsage:_contactsArray];
}
}];
}
I was trying to fetch contacts in batches of 20, reloading the table each time. However here it can't reload within the dispatch thread, or simply crashes.
How do I improve this code to fetch contacts quickly?
Thanks.
As Toro mentioned, the requestAccessForEntityType: completionHandler: method is not running on the main (UI) thread. From the documentation you can see it mentions that:
Users are able to grant or deny access to contact data on a
per-application basis. Request access to contact data by calling
requestAccessForEntityType:completionHandler: method. This will not
block your application while the user is being asked for permission.
The user will only be prompted the first time access is requested; any
subsequent CNContactStore calls will use the existing permissions. The
completion handler is called on an arbitrary queue. It is recommended
that you use CNContactStore instance methods in this completion
handler instead of the UI main thread. This method is optional when
CNContactStore is used in the background thread. If this method is not
used, CNContactStore may block your application while the user is
asked for access permission.
So any updates you want to do in the UI, you'll have to do on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
//Update UI on the Main thread, like reloading a UITableView
});
I have started use Parse (which I downloaded using Cocoapods) in a practice iOS application and I having a little bit of trouble understanding certain concepts.
I have written this code so far:
- (IBAction)saveUserButtonClicked:(id)sender {
PFObject *loginCredentials = [PFObject objectWithClassName:#"LoginCredentials"];
loginCredentials[#"name"] = self.usernameTextField.text;
loginCredentials[#"password"] = self.passwordTextField.text;
[loginCredentials saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if(error.code == kPFErrorConnectionFailed){
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Error" message:#"Please check you connection" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alertView show];
}else if(succeeded && !error){
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Save" message:#"Your object saved" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alertView show];
}else if(error){
NSLog(#"%#", error);
}
}];
}
My main question I have is what is the purpose of using saveInBackGroundWithBlock. Could I do the same logic by doing:
[loginCredentials saveInBackground];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Save" message:#"Your object saved" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alertView show];
Is the block only helpful if we want to have access to the succeeded and error variables?
in saveInBackgroundWithBlock the save operation is executed in a background thread not in the main thread(thread used by UI ), once it ends executing it calls back the block to execute it. Not using the main thread for this save-operation makes the user-interface responsive while executing the save in another thread.
you can make the save operation in the main thread by using save method and then show alert based on success, but you will completely block the user interface and its not a best practice unless its necessarily to continue on the app.
bool succeeded = [loginCredentials save];
Synchronous calls will execute code on the main thread (UI thread), which will prevent the UI from responding to user events. This is generally considered bad practice because it gives the user the impression that your app has frozen when in fact one thread can't do two things at once. Therefore, you typically use asynchronous calls (blocks) to execute complex code on background threads, freeing the UI to continue performing some operation so the user isn't confused or just left hanging waiting on the operation to finish.
Looking at the PFObject spec, the – saveInBackground and – saveInBackgroundWithBlock: methods are the asynchronous options. In this case, the callback block is just reporting whether or not the operation was successful, and includes an NSError object for reporting in case it failed.
That all being said, I think you can save the object using your code, but you will not get the opportunity to respond to any errors that arise in the process. Meaning, you are making an assumption that your object saved successfully when you are creating and presenting the alert view.
I'm basing this opinion on the saveInBackground method implementation, which is just returning a BFTask object:
- (BFTask *)saveInBackground {
return [self.taskQueue enqueue:^BFTask *(BFTask *toAwait) {
return [self saveAsync:toAwait];
}];
}
Here's the object documentation on the two methods from Parse:
– saveInBackground
Saves the PFObject asynchronously.
(BFTask PF_GENERIC ( NSNumber *) *)saveInBackground
Return Value
The task that encapsulates the work being done.
Declared In PFObject.h
– saveInBackgroundWithBlock:
Saves the PFObject asynchronously and executes the given callback block.
(void)saveInBackgroundWithBlock:(PF_NULLABLE PFBooleanResultBlock)block
Parameters
block
The block to execute.
It should have the following argument signature: ^(BOOL succeeded, NSError *error).
Declared In PFObject.h
If you aren't interested in handling the errors, and your objects are sufficiently simple to save, have you considered just using one of the synchronous save methods, like - (BOOL)save or - (BOOL)save:(NSError **)error? You could immediately respond to the BOOL value that is returned, in similar fashion to the Parse example you posted above (inside the completion block).
I've implemented the following exception handler using:
NSSetUncaughtExceptionHandler(&ExceptionHandler)
The handler then makes a call to handleException using the following:
[[AppContext alloc] init] performSelectorOnMainThread:#selector(handleException:) withObject:exception waitUntilDone:YES];
Which has been implemented as such:
NSString *message = [NSString stringWithFormat:MSG_UNHANDLED_EXCEPTION_FORMAT, exception.reason, [exception callStackSymbols]];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:TITLE_UNHANDLED_EXCEPTION message:message delegate:self cancelButtonTitle:nil otherButtonTitles:nil];
[alert show];
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!_quit) {
for (NSString *mode in (__bridge NSArray *)allModes) {
CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
This works well except for the following scenario. I still get the UIAlertView to show but the user interaction is no longer available (user can't scroll message or tap OK).
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
// do long work on another thread...
dispatch_async(dispatch_get_main_queue(), ^(){
// updating the UI so get back on main thread...
// exception occurs in this block (example)...
#throw [NSException exceptionWithName:#"DispatchAsyncToMainThreadException" reason:#"Example" userInfo:nil];
});
Note: I am not actually throwing an exception in my production code. This is purely to show where the unhandled exception is being generated.
What am I missing that should be accounted for when the exception is being raised from within a block that has been dispatched back to the main queue.
The documentation for NSSetUncaughtExceptionHandler states that you should be doing last-minute error logging before the application terminates. You should not expect any UI to work at this point and should only be logging somewhere to the file system the details of the error. When the user opens the application again you can let them know something went wrong and possible offer recovery/reporting options.
Discussion Sets the top-level error-handling function where you can perform last-minute logging before the program terminates.
I have an app where the user can authenticate with Instapaper. They need an Instapaper subscription to be able to do this, however, so if they try to log in with an account that isn't subscribed to Instapaper, I want to display an error to them.
But when they try to log in, AFNetworking sees it as successful, then displays this error to the console:
Error: Error Domain=AFNetworkingErrorDomain Code=-1011 "Expected
status code in (200-299), got 400" UserInfo=0x8374840
{NSLocalizedRecoverySuggestion=[{"error_code": 1041, "message":
"Subscription account required", "type": "error"}],
AFNetworkingOperationFailingURLRequestErrorKey=https://www.instapaper.com/api/1/bookmarks/list>,
NSErrorFailingURLKey=https://www.instapaper.com/api/1/bookmarks/list,
NSLocalizedDescription=Expected status code in (200-299), got 400,
AFNetworkingOperationFailingURLResponseErrorKey=}
All I'm using is AFXAuthClient which is a modification of AFNetworking. I subclassed it to create a custom Instapaper API client that looks like this:
#import "AFInstapaperClient.h"
#import "AFJSONRequestOperation.h"
#implementation AFInstapaperClient
+ (AFInstapaperClient *)sharedClient {
static AFInstapaperClient *sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedClient = [[AFInstapaperClient alloc] initWithBaseURL:[NSURL URLWithString:#"https://www.instapaper.com/"]
key:#"..."
secret:#"..."];
});
return sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
if (self = [super initWithBaseURL:url]) {
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setDefaultHeader:#"Accept" value:#"application/json"];
}
return self;
}
#end
And when they log in, the following code is executed:
- (IBAction)doneButtonPressed:(UIBarButtonItem *)sender {
[[AFInstapaperClient sharedClient] authorizeUsingXAuthWithAccessTokenPath:#"/api/1/oauth/access_token"
accessMethod:#"POST"
username:self.loginBox.text
password:self.passwordBox.text
success:^(AFXAuthToken *accessToken) {
// Save the token information into the Keychain
[UICKeyChainStore setString:accessToken.key forKey:#"InstapaperKey"];
[UICKeyChainStore setString:accessToken.secret forKey:#"InstapaperSecret"];
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"Login Successful"
message:#"Your articles are being downloaded now and will appear in your queue."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles: nil];
[alert show];
[[NSUserDefaults standardUserDefaults] setObject:#"YES" forKey:#"IsLoggedInToInstapaper"];
[self dismissViewControllerAnimated:YES completion:nil];
}
failure:^(NSError *error) {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"Login Failed."
message:#"Are you connected to the internet? Instapaper may also be down. Try again later."
delegate:nil
cancelButtonTitle:#"Okay"
otherButtonTitles: nil];
[alert show];
}];
}
But the code never goes into the failure block. How could I modify my code so that it would allow me to tell them they need an Instapaper subscription account?
Based on your situation, I don't think you will ever trigger the failure block because your request isn't failing. You are getting a response from the web service. In my experience the failure block only executes if you fail to get a response because of something like network availability or something like it.
Therefore, you need to handle the account error in the success block. One way you could do it is to read the status code that is returned in the response. If the status code is 400 like your console is showing then alert the user.
You can follow the method used here "https://stackoverflow.com/q/8469492/2670912"
It seems with this implementation, as WeekendCodeWarrior said, it will deem it successful even though they won't be able to make further requests. The code that spat out the error was actually an NSLog that I did (whoops, didn't realize it was my code outputting that) after making a request as I assumed all was fine.
My solution was just to make a request to the API in that success block, check the result of that request (which does have a response object returned) and then act accordingly on the response object.
I am getting a EXEC_BAD_ACCESS Error when I attempt to run this code, and the user has not allowed access to the calendar. Does requestAccessToEntityType run on a separate thread, if thats the case how do I access the main thread to display the UIAlertView?
EKEventStore *store = [[EKEventStore alloc] init];
if ([store respondsToSelector:#selector(requestAccessToEntityType:completion:)])
{
[store requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError *error)
{
if ( granted )
{
[self readEvents];
}
else
{
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"Denied Access To Calendar"
message:#"Access was denied to the calendar, please go into settings and allow this app access to the calendar!"
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil,
nil];
[alert show];
}
}];
}
According to the docs for requestAccessToEntityType
When the user taps to grant or deny access, the completion handler
will be called on an arbitrary queue.
So, yes, it could be on a different thread than the UI one. You can only put up alerts from the main GUI thread.
Look into performSelectorOnMainThread. More information here: Perform UI Changes on main thread using dispatch_async or performSelectorOnMainThread?
Reason why your app is crashing because you are trying to deal with your GUI elements i.e UIAlertView in background thread, you need to run it on the main thread or try to use dispatch queues
Using Dispatch Queues
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
//show your UIAlertView here... or any GUI stuff
});
OR you can show the GUI elements on the main thread like this
[alertView performSelectorOnMainThread:#selector(show) withObject:nil waitUntilDone:YES];
You can have more detail about using GUI elements on Threads on this link