I'm writing an iOS application that populates an array using the data retrieved from the server and displays it in a picker view.
Everything goes smoothly the first time the view is displayed; However, when switching to another view which uses a camera to scan stuff and switch back using a slide-out menu (built using SWRevealViewController), it fails to populate the array. When we access it on the UI thread after the background task has finished to retrieve the records, an NSRangeException is thrown with the error index 0 beyond bounds for empty array.
I'm pretty sure the background task is being run and the data is being retrieved successfully as I log every single request to the server.
I believe it might be an issue with concurrency and the background thread not updating the variable.
As far as we have tested, this issue is only present on iOS 6, and does not happen, or at least has not yet, on iOS 7.
This is the code used to retrieve and set the array:
dispatch_queue_t queue = dispatch_queue_create("com.aaa.bbb", NULL);
dispatch_async(queue, ^{
_events = [_wi getEvents:_auth_token];
dispatch_async(dispatch_get_main_queue(), ^{
// code to be executed on the main thread when background task is finished
[_mPicker reloadComponent:0];
// Set the default on first row
[self pickerView:_mPicker didSelectRow:0 inComponent:0];
[pDialog dismissWithClickedButtonIndex:0 animated:YES];
});
});
This is the prepareforSegue method in my SidebarViewController that is responsible for switching between views when an item is selected from the slide-out menu.
- (void) prepareForSegue: (UIStoryboardSegue *) segue sender: (id) sender
{
// Set the title of navigation bar by using the menu items
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
UINavigationController *destViewController = (UINavigationController*)segue.destinationViewController;
destViewController.title = [[_menuItems objectAtIndex:indexPath.row] capitalizedString];
if ( [segue isKindOfClass: [SWRevealViewControllerSegue class]] ) {
SWRevealViewControllerSegue *swSegue = (SWRevealViewControllerSegue*) segue;
swSegue.performBlock = ^(SWRevealViewControllerSegue* rvc_segue, UIViewController* svc, UIViewController* dvc) {
UINavigationController* navController = (UINavigationController*)self.revealViewController.frontViewController;
[navController setViewControllers: #[dvc] animated: NO ];
[self.revealViewController setFrontViewPosition: FrontViewPositionLeft animated: YES];
};
}
}
The views are linked together from the storyboard for switching.
The error occurs when I try to retrieve a specific entry from my events array in the pickerView:didSelectRow:inComponent: method :
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
// Set the current id
_currentId = [NSString stringWithFormat:#"%d", row];
_currentName = [[_events objectAtIndex:row] objectForKey:#"event_name"];
-----------^
...
Here's the list of running threads and the call stack.
From my experience in Android, I think that this might have to do something with me not finishing the background task properly the first time, or somehow the code that is supposed to be run after the background task, is run alongside it the second time.
I would appreciate any suggestions that might help me with this issue!
Thanks
The variable _events is accessed from different threads without any synchronization mechanism.
Try this:
dispatch_queue_t queue = dispatch_queue_create("com.aaa.bbb", NULL);
dispatch_async(queue, ^{
NSArray* events = [_wi getEvents:_auth_token];
dispatch_async(dispatch_get_main_queue(), ^{
_events = [events copy];
// code to be executed on the main thread when background task is finished
[_mPicker reloadComponent:0];
// Set the default on first row
[self pickerView:_mPicker didSelectRow:0 inComponent:0];
[pDialog dismissWithClickedButtonIndex:0 animated:YES];
});
});
Also _events should be an NSArray, not NSMutableArray, to avoid such problems.
Related
I am having a hard time figuring out how to get my quick actions working when I launch my app with a quick action.
My quick actions work, however, if the app was in the background and re-launched with the quick action.
When I try to launch the app straight from the quick action, the app opens as if it was launched by simply tapping the app icon (i.e. it does nothing).
Here is some code from my App Delegate.
In didFinishLaunchingWithOptions:
UIApplicationShortcutItem *shortcut = launchOptions[UIApplicationLaunchOptionsShortcutItemKey];
if(shortcut != nil){
performShortcutDelegate = NO;
[self performQuickAction: shortcut fromLaunch:YES];
}
The method called:
-(BOOL) performQuickAction: (UIApplicationShortcutItem *)shortcutItem fromLaunch:(BOOL)launchedFromInactive {
NSMutableArray *meetings = [self.fetchedResultController.fetchedObjects mutableCopy];
[meetings removeObjectAtIndex:0];
unsigned long count = meetings.count;
BOOL quickActionHandled = NO;
if(count > 0){
MainViewController *mainVC = (MainViewController *)self.window.rootViewController;
if(launchedFromInactive){
mainVC.shortcut = shortcutItem;
}
else{
UINavigationController *childNav;
MeetingViewController *meetingVC;
for(int i = 0; i < mainVC.childViewControllers.count; i++){
if([mainVC.childViewControllers[i] isKindOfClass: [UINavigationController class]]){
childNav = mainVC.childViewControllers[i];
meetingVC = childNav.childViewControllers[0];
break;
}
}
self.shortcutDelegate = meetingVC;
if ([shortcutItem.type isEqual: #"Meeting"]){
NSNumber *index = [shortcutItem.userInfo objectForKey:#"Index"];
[self.shortcutDelegate switchToCorrectPageWithIndex: index launchedFromInactive:NO];
quickActionHandled = YES;
}
}
}
The only action that needs to be performed is that my page view controller (which is embedded inside the meetingVC) should switch to a certain page with respect to the shortcut chosen.
Any ideas on what causes the shortcut to not do anything when using it to launch as opposed to re-opening the app from the background??
I came to realize I was trying to call my methods on a view controller that was not in memory yet. This was causing bizarre behavior in my app. I did have the correct approach to getting access to the view controller and then it dawned on me the possibility of trying to execute the code using GCD.
__block MeetingViewController *safeSelf = self;
contentVC = [self initializeContentViewController: self.didLaunchFromInactive withPageIndex: intIndex];
NSArray *viewControllers = #[contentVC];
dispatch_async(dispatch_get_main_queue(), ^{
[safeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
});
The above worked like magic, and the shortcuts are leading to the correct page. Using a similar approach to mine hopefully yields the desired results for anyone else who wanted to get their shortcuts working by launching the app.
So i am showing a model controller on top of a view controller. And i have texts in the model controller, but somehow the texts are not visible. I tried everything but somehow labels are not visible. But of you stay on the page for like 30 -40 sec the text shows up. Also this model controller is called from main view controller after a successful service(REST) call. If i call the model without making the service call then labels are visible in simulator/iPad both. But if i call it after service call inside success block then labels are not visible. I tried adding the text programmatically but still same issue. I tried debugging using Color blended layers, but the label is not at all visible in the view somehow. :(
[self.serviceManager getCustDetails:account successBlock:^(NSDictionary * successDict) {
[self hideLoadingAnimation];
NSDictionary *custData = [[successDict objectForKey:#"txnData"] objectForKey:#"custData"];
self.showCurrYear = [iraContribData objectForKey:#"showCurrYear"];
if ([self.showCurrYear isEqual: #"true"]){
[self performSegueWithIdentifier:#"CSegue" sender:self];
}
} failureBlock:^(NSDictionary * failureDict) {
[self hideLoadingAnimation];
NSLog(#"Failiure Dict %#",failureDict);
}];
And this prepareForSegue method, -
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"CSegue"]) {
CustViewController *cVC = segue.destinationViewController;
cVC.delegate = self;
[cVC setModalPresentationStyle:UIModalPresentationFormSheet];
cVC.preferredContentSize = CGSizeMake(800,750);
}
}
Below is my screen in storyboard, but in simulator the label is not visible, only continue and close button is visible.
Please help!, any suggestions are most welcome. Thanks!
It is possible that the delay is due to a user interface update not made on the main thread.
Try to make sure that your code is executed on the main thread using dispatch_async like this :
[self.serviceManager getCustDetails:account successBlock:^(NSDictionary * successDict) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingAnimation];
NSDictionary *custData = [[successDict objectForKey:#"txnData"] objectForKey:#"custData"];
self.showCurrYear = [iraContribData objectForKey:#"showCurrYear"];
if ([self.showCurrYear isEqualToString:#"true"]){
[self performSegueWithIdentifier:#"CSegue" sender:self];
}
});
} failureBlock:^(NSDictionary * failureDict) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingAnimation];
NSLog(#"Failiure Dict %#",failureDict);
});
}];
Maybe this is purely simulator related. I have not tried it on an actual device yet.
I'm on the latest greatest MacBook with a 1TB flash drive, and 95% free processor, and less than full memory consumption.
I have a UIPopoverController with 4 items in it, sized to those items.
There's nothing complicated or multi-threaded or long running in any way associated in the UIPopoverController in question.
I've set the appear and dismiss animation at 0, yet when I tap on an item in the list, there seems to be an random indeterminate delay between 0 and .4 seconds in the popover disappearing. Of course the 0 is expected, but the times when it's nearly a half second is very noticeably longer and disconcerting.
Any idea what may be causing that?
Code that shows the popover...
-(IBAction)theLandImpsButtonPressed:(UIButton *)sender
{
iRpNameValuePopover *thePopoverContent = [[iRpNameValuePopover alloc] init];
thePopoverContent.theTableValues = [self getLandImpsChoicesList];
impsLandPopover = [[UIPopoverController alloc] initWithContentViewController:thePopoverContent];
thePopoverContent.thePopoverController = impsLandPopover;
impsLandPopover.popoverContentSize = [iRpUIHelper sizeForPopoverThatHasTitle:NO andListContent:thePopoverContent.theTableValues];
impsLandPopover.delegate = self;
[impsLandPopover presentPopoverFromRect:self.theLandImpsButton.bounds inView:self.theLandImpsButton permittedArrowDirections:UIPopoverArrowDirectionAny animated:NO];
}
Code that dismisses the popover...
BTW, there is no evaluation time incurred here [self userChoiceIsValid] because it simply returns YES right now.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
_theChosenNameValueItem = [self.theTableValues objectAtIndex:indexPath.row];
[self acceptUserChoiceAndClose];
}
// This contentViewController is encapsulated INSIDE a UIPopoverViewController, and this class cannot itself
// close the popover which contains it, hence the need for the reference to the popover controller
// It is the popover's delegate... the one that created the popover, that is able to close it.
-(void)acceptUserChoiceAndClose
{
_theUserChoseAValue = NO; // Start by assuming they didn't chose a valid value.
if ([self userChoiceIsValid])
{
// Set variable that indicates the user chose a value which can be saved to core data, and/or presented on screen.
_theUserChoseAValue = YES;
// Close the popover.
[_thePopoverController dismissPopoverAnimated:NO];
// Notify the class that presented the popover that the popover has been dismissed.
// It will still be available to the dismissal method where code can retrieve the user's choice, and set the popover to nil.
if (_thePopoverController.delegate && [_thePopoverController.delegate respondsToSelector:#selector(popoverControllerDidDismissPopover:)])
{
[_thePopoverController.delegate popoverControllerDidDismissPopover:_thePopoverController];
}
}
else
{
[self showValidationFailureMessageToUser];
}
}
Dismissing the viewController in main thread will solve the issue.
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
I would check it out in the profiler and see what the time is being spent on.
There's a good tutorial here.
UIPopoverPresentationController *popOverView;
//////
Dismiss it...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
[popOverView.presentedViewController dismissViewControllerAnimated:NO completion:nil];
popOverView = nil;
});
});
I can't seem to add a badge on a TabBarItem
Tried a lot of options (that's why the code is splitted into variables).
Thought it had something to do with the treading so the update is back on the main thread, still nothing.
The code:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger badge_count = 0;
badge_count = getDataFromServer();
if (snacks_count > 0)
{
MainTabBarViewController *c = [self.storyboard instantiateViewControllerWithIdentifier:#"tabBarController"];
UINavigationController *nav = [c.viewControllers objectAtIndex:1];
dispatch_async(dispatch_get_main_queue(), ^{
nav.tabBarItem.badgeValue = [NSString stringWithFormat:#"%ld", (long)badge_count];
});
}
});
Please pay attention to the method: :instantiateViewControllerWithIdentifier" description:
Instantiates and returns the view controller with the specified identifier.
You use this method to create view controller objects that you want to manipulate and present programmatically in your application. Before you can use this method to retrieve a view controller, you must explicitly tag it with an appropriate identifier string in Interface Builder.
Namely, you create another instance.
You should change to
UINavigationController *nav = [self.viewControllers objectAtIndex:1];
in order to change the UINavigationController that you refer to
Apparently the original problem was that I didn't do it from the main thread. Probably made a wrong code with all the playing around trying to debug.
The working code:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
badge_count = getDataFromServer();
if (snacks_count > 0)
{
NSInteger badge_count = 0;
badge_count = getDataFromServer();
if (badge_count > 0)
{
dispatch_async(dispatch_get_main_queue(), ^{
[(UIViewController *)[self.tabBarController.viewControllers objectAtIndex:1] tabBarItem].badgeValue = [NSString stringWithFormat:#"%ld", (long)badge_count];
});
}
});
In my app I frequently get the warning
CoreAnimation: warning, deleted thread with uncommitted CATransaction; set CA_DEBUG_TRANSACTIONS=1 in environment to log backtraces.
when I'm transitioning from a View Controller with a MKMapView to a TableViewController. Every once and a while this also throws the following exception:
Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x198e81f0> was mutated while being enumerated.'
My issue is that I never actually call anything from the CoreAnimation library directly in my code so the solution offered here wouldn't work for me.
I can't show you the exact code I'm using for confidentiality reasons, but here is the gist of what my code looks like for the transition:
MKMapView Side
UITabBarController *tbc = mapViewController.tabBarController;
UINavigationController *nav = [[tbc viewControllers]objectAtIndex:1];
ViewController *anotherController = [[nav viewControllers]objectAtIndex:0]; // not the final destination
[nav popToRootViewControllerAnimated:NO]; // pops to root without animation
[anotherController setSelectedEvent:event]; // sets which event to select before the segue
[tbc setSelectedIndex:1]; // changes the selected tab
[anotherController performSegueWithIdentifier:#"identifier" sender:nil]; // segues to the TableView
Segue from anotherController to tableViewController
if ([[segue identifier]isEqualToString:#"identifier"]) {
TableViewController *vc = segue.destinationViewController;
[vc setEvent:selectedEvent];
[vc.navigationItem setTitle:#"Title"];
[vc performSelectorInBackground:#selector(searchForAndScrollToEvent:) withObject:_selectedEvent];
}
Final Destination
-(void)searchForAndScrollToEvent:(Events*)event {
scroll_to_event_waiting_for_table_to_load = YES;
while (scroll_to_event_waiting_for_table_to_load) {
usleep(1000);
}
NSLog(#"segue completed");
EventListVirtualTable* virtualTable = [[EventListVirtualTable alloc]initWithViewController:self];
NSIndexPath *indexPath = [virtualTable indexPathOfEvent:event];
if (/*indexPath is bad*/) {
NSLog(#"NOT scrolling to: row %i section %i",indexPath.row,indexPath.section);
return;
}
[self performSelectorOnMainThread:#selector(scrollToAtIndexPath:) withObject:indexPath waitUntilDone:YES];
while (![[_tableView indexPathsForVisibleRows] containsObject:indexPath]) {
usleep(100);
}
[self performSelectorOnMainThread:#selector(selectCellAtIndexPath:) withObject:indexPath waitUntilDone:YES];
usleep(delay);
[self performSelectorOnMainThread:#selector(decselectCellAtIndexPath:) withObject:indexPath waitUntilDone:YES];
}
-(void)scrollToAtIndexPath:(NSIndexPath*)indexPath {
if(_dataController == nil){return;}
if (_tableView.numberOfSections == 0 || [self numberOfRowsInSection:indexPath.section] == 0 ) {
return;
}
[_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
NSLog(#"scrolling to: row %i section %i",indexPath.row,indexPath.section);
}
Thanks everyone for taking the time to offer any responses or useful hints!
Everybody knows not to modify UIView objects from another thread. I was activating UIActivityIndicator from another thread. That was causing the exceptions. I'm still getting the warnings, but the warnings happen now when I cancel threads that I've called.