UIPageViewController throws SIGABRT on orientation change - ios

I have the following code to display a magazine type app. When the app is rotated it runs this code. I made sure that it is only run when rotated to supported orientations. When this function returns, the app fails with a SIGABRT. There is no other indication as to why.
I know it's this function because when I remove it the program does not fail.
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
//If portrait mode, change to single page view
if(UIInterfaceOrientationIsPortrait(orientation)){
UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSArray *viewControllers = [NSArray arrayWithObject:currentViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
self.pageViewController.doubleSided = NO;
return UIPageViewControllerSpineLocationMin;
//If landscape mode, change to double page view
}else{
//Get current view
UIViewController *currentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
//Create an array to store, views
NSArray *viewControllers = nil;
NSUInteger currentIndex = self.currentPage;
//Conditional to check if needs page before or after
if(currentIndex == 1 || currentIndex %2 == 1){
UIViewController *nextViewController = [self pageViewController:self.pageViewController viewControllerAfterViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:currentViewController,nextViewController, nil];
}else{
UIViewController *previousViewController = [self pageViewController:self.pageViewController viewControllerBeforeViewController:currentViewController];
viewControllers = [NSArray arrayWithObjects:previousViewController, currentViewController, nil];
}
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
return UIPageViewControllerSpineLocationMid;
}
//return UIPageViewControllerSpineLocationMid;
}

Alas, borrrden is probably right. One of your IBOutlets is probably missing from your XIB. Make sure ALL of your IBs are connected properly, and if the problem continues, say so.

Well you didn't provide the output from the console, which would be nice. Giving the code a quick look I would guess that one of your controllers (next or previous) is nil, and since you can't insert nil into an NSArray (except as the last object) it is throwing an error.
EDIT Well, my guess was wrong. The error message is saying that the UIViewControllers you are giving to it do not support the orientation that the page controller needs. This is because you have a method called shouldRotateToInterfaceOrientation: in your child UIViewControllers, and they are returning no for (in this case) left landscape.

I was getting the same error
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'All provided view controllers ((
"<ContentViewController: 0x6a7eac0>",
"<ContentViewController: 0x6d89f10>"
)) must support the pending interface orientation (UIInterfaceOrientationLandscapeLeft)'
Adding the following to my PageModel class, where the page layout is designed worked for me:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}

Related

Adding accessibilityIdentifier to UITabBarButton

I'm displaying a UITabBar in my app, and am trying to assign accessibilityIdentifiers to the buttons. To accomplish this, I use the following lines in each of my view controller instantiations:
viewController.tabBarItem.accessibilityIdentifier = #"ViewControllerID";
These viewControllers all get added to the UITabBar like so:
NSMutableArray *tabBarItems = [NSMutableArray array];
for (NSInteger i=0; i<_viewControllers.count; i++) {
UIViewController *viewController = [_viewControllers objectAtIndex:i];
[viewController setCommonTabBarController:self];
[self addChildViewController:viewController];
if (i == 0) {
viewController.view.frame = self.currentTabView.bounds;
[self.currentTabView addSubview:viewController.view];
[self addConstraintsToSubView:viewController.view];
_selectedViewController = viewController;
_selectedIndex = 0;
}
[viewController didMoveToParentViewController:self];
[tabBarItems addObject:[viewController tabBarItem]];
}
[self.tabBar setItems:tabBarItems animated:animated];
So, what I think should be happening here, is we are grabbing the tabBarItem that has the accessibilityIdentifier set correctly (when I set breakpoints, the accessibilityIdentifier of each view controller is what I expect.) Then, when it is actually displayed, there is no accessibilityIdentifier.
Things I've noticed:
iOS is using UITabBarButton instead of UITabBarItem. I think that has something to do with it. When I print out the items array of the tab bar, each of the items has the correct accessibilityIdentifier, however, none of the UITabBarButton objects has the accessibilityIdentifier of the associated tab bar item.
Does anyone know why the accessibility identifier isn't, for lack of a better word, "carrying through" to the UITabBarButton object that iOS uses?
I ran into the same problem trying to add accessibility identifiers to UITabbar items for UITesting, I was able to do it using this workaround which is not perfectly sane but it works:
[self.tabBar setItems:tabBarItems animated:animated];
NSArray *identifiers = #[#"itemIdentifier1", #"itemIdentifier2", #"itemIdentifier3"];
int index = 0;
for (UIControl *control in controller.tabBar.subviews)
{
if ([control isKindOfClass:UIControl.class] && index < identifiers.count)
{
//This is actually the UITabBarButton
control.accessibilityIdentifier = identifiers[index];
index++;
}
}
The key is to assign the identifiers to the UITabbar subviews after the tabs have been added, which means the UITabBarButton objects have been created and ready to have an identifier set.

Correct way to handle application launching with quick actions

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.

UISplitviewController and different UIKeyCommands depending on master, detail or both being on screen

I want to include some UIKeyCommands in my app. My app consists of one UISplitViewController that forces the master to be always visible on iPad full screen. On smaller screen it works like it normally would.
Now, I've implemented some UIKeyCommands in the MasterViewController and some in the DetailViewController. However, the app will only show those in DetailViewController. So I put all of them in the RootSplitViewController, but that will show all of them, even when the MasterViewController is hidden in iOS 9's splitview.
What I want though, is for it to show all when the app is fullscreen on iPad and thus the MasterViewController is forced on screen together with the DetailViewController. And when the view is small (ie 50-50) and the MasterViewController is hidden, I want it to only show those of the window that's on screen.
Any ideas on how to achieve this?
In the end I managed to do this - although in a not-so-pretty way.
The UIKeyCommands are added to the RootSplitViewController.
- (NSArray *)keyCommands {
if (self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
return #[
[UIKeyCommand keyCommandWithInput:#"r" modifierFlags:UIKeyModifierCommand action:#selector(changeRestaurant:) discoverabilityTitle:#"Change restaurant"],
[UIKeyCommand keyCommandWithInput:#"t" modifierFlags:UIKeyModifierCommand action:#selector(changeTable:) discoverabilityTitle:#"Change table"]
];
} else {
if (self.masterIsVisible == YES) {
return #[
[UIKeyCommand keyCommandWithInput:#"t" modifierFlags:UIKeyModifierCommand action:#selector(changeRestaurant:) discoverabilityTitle:#"Change restaurant"]
];
} else {
return #[
[UIKeyCommand keyCommandWithInput:#"t" modifierFlags:UIKeyModifierCommand action:#selector(changeTable:) discoverabilityTitle:#"Change table"]
];
}
}
}
Those methods call the actual methods in the specific UIViewController.
- (void)changeRestaurant:(id)sender {
UINavigationController *nav = (UINavigationController *)[self.viewControllers objectAtIndex:0];
RestaurantController *master = [nav.viewControllers objectAtIndex:0];
[master changeRestaurant];
}
- (void)changeTable:(id)sender {
UINavigationController *nav = (UINavigationController *)[self.viewControllers objectAtIndex:1];
TableController *detail = [nav.viewControllers objectAtIndex:0];
[detail changeTable:sender];
}
In order for this to work I added a BOOL to the UISplitViewController.
#interface RootSplitViewController : UISplitViewController
#property (nonatomic) BOOL masterIsVisible;
#end
Which is then called in the MasterViewController.
- (void)viewDidDisappear:(BOOL)animated {
RootSplitViewController *rootView = (RootSplitViewController *)self.splitViewController;
rootView.masterIsVisible = NO;
}
- (void)viewDidAppear:(BOOL)animated {
RootSplitViewController *rootView = (RootSplitViewController *)self.splitViewController;
rootView.masterIsVisible = YES;
}
I know this might not be the pretties method, but it works. If anyone knows a better way to do it, I'd love to hear your feedback.

popToRootViewController crashes when tableView is still scrolling

When I give a good swipe to my tableView and press the "Back" button before the tableView ended it's scrolling, my app crashes. I've tried the following:
- (void) closeViewController
{
[self killScroll];
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)killScroll
{
CGPoint offset = sellersTableView.contentOffset;
[sellersTableView setContentOffset:offset animated:NO];
}
That didn't work, same crash. I don't see why, the error I'm getting is the following:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
So that means that the tableView is still requesting a cell when everything is already being deallocated. Makes no sense.
Then I tried this:
- (void) closeViewController
{
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
sellersTableView = nil;
}
Gives me the same error. Any ideas?
Update:
My delegate methods
creation
if (textField == addSellerTextField) {
sellersTableView = [[UITableView alloc] initWithFrame:CGRectMake(addSellerTextField.frame.origin.x + addSellerTextField.frame.size.width + 10, addSellerTextField.frame.origin.y - [self heightForTableView] + 35, 200, [self heightForTableView])];
sellersTableView.delegate = self;
sellersTableView.dataSource = self;
sellersTableView.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.05];
sellersTableView.separatorColor = [[UIColor grayColor] colorWithAlphaComponent:0.15];
sellersTableView.rowHeight = 44;
sellersTableView.layer.opacity = 0;
[self.companyView addSubview:sellersTableView];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 1;} completion:nil];
}
cellForRowAtIndexPath
if (tableView == sellersTableView) {
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.backgroundColor = [UIColor clearColor];
if ([sellersArray count] > 0) {
cell.textLabel.text = [sellersArray objectAtIndex:indexPath.row];
} else {
UILabel *noSellersYetLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, sellersTableView.frame.size.width, [self heightForTableView])];
noSellersYetLabel.text = #"no sellers yet";
noSellersYetLabel.textAlignment = NSTextAlignmentCenter;
noSellersYetLabel.textColor = [UIColor grayColor];
[cell addSubview:noSellersYetLabel];
sellersTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
}
removing
- (void) textFieldDidEndEditing:(UITextField *)textField
{
if (textField == addSellerTextField) {
[self updateSellers:textField];
}
}
- (void)updateSellers:(UITextField *)textField
{
[textField resignFirstResponder];
[self hideSellersTableView];
}
- (void)hideSellersTableView
{
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 0;} completion:nil];
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[sellersTableView removeFromSuperview];
sellersTableView = nil;
}
Solution
So apparently putting the dataSource = nil and delegate = nil into textFieldDidEndEditing fixed the problem. Thanks everybody for the answers!
It's strange behaviour of UITableView. The easiest way to resolve this issue just set the dataSource and delegate property of UITAbleView to nil before you make a call of function popToRootViewControllerAnimated. Furthermore you can use more common solution and add the code that set the properties to nil into the -dealloc method. In addition you no need the -killScroll method.
After a short research I have realized what the problem is. This unusual behaviour appeared in iOS 7. The scroll view retained by its superview may send message to delegate after the delegate is released. It happens due to -removeFromSuperview implementation UIScrollView triggers -setContentOffset: and, eventually, send message to delegate.
Just add following lines at the beginning of dealloc method:
sellersTableView.delegate = nil;
sellersTableView.dataSource = nil;
No need to use hacks like your killScroll method.
Also, I can't see why you want to call both popToRootViewController and dismissViewController.
If you dismiss a view controller which is embedded in a navigation controller, navigation controller itself as well as all contained view controllers will be released.
In your case you'll have just weird animation.
setContentOffset method won't help you, try to set
sellersTableView.dataSource = nil;
somewhere in your viewWillDisappear method.
This is not a good practice of course.
Change you closeViewController like below and see if works
(void) closeViewController
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
I don't think that setting the tableView (or it's delegate) to nil is the issue. You should be able to perform both dismissViewControllerAnimated or popToRootViewController individually without having to modify the tableView in this way.
So the issue is most likely due to calling both of these methods at the same time (and with animated = YES), and in doing so asking your viewController setup to do something unnatural.
Looks like upon tapping a "close" button you are both popping to a rootViewController of a UINavigationController, as well as dismissing a modal viewController.
In doing so, you're dismissing a modal viewController which is likely presented by the topViewController of the navigationController (so top vc is holding a reference to modal vc). AND you're trying to kill the top vc via the popToRootViewController method call. And you're doing both of these things using animated = YES, which means they take some time to complete, and you can't be sure when each finishes (ie you can't be sure when dealloc will be called).
Depending on your needs you could do one of several things.
Consider adding a delegate property to your modal vc. Dismiss the modal vc, and in the completionBlock of the modal vc tell its delegate that it's finished dismissing. At that point call popToRootViewController (because at this point you can be sure that the modal is gone and scrolling wasn't interrupted).
If it's your navController that's been presented modally, then do this in the opposite order. Notifying the delegate that the pop operation has completed, and do the modal dismissal then.

Odd number of pages on UIPageViewController

I've implemented a UIPageViewController in my iPad app. However, when the iPad is in portrait you can see all the pages, but when the iPad is in landscape you can’t see the last one, and if you are in the last page in portrait and change to landscape the app crashes with the following error:
Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘The >number of provided view controllers (1) doesn’t match the number required (2) for the >requested spine location (UIPageViewControllerSpineLocationMid)’
Because it needs 2 pages and there is only one.
What can I do when the “book” has an odd number of pages (7, for example) to avoid the previous exception?
The way I fixed it is like this.
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
DSBookletPageViewController *currentVC = (DSBookletPageViewController *)viewController;
NSUInteger currentIndex = [currentVC index];
if(currentIndex >= self.modelArray.count-1) {
if (currentIndex %2 == 0 && !isPortrait) {
//return an empty one
DSBookletPageViewController *newVC = [[DSBookletPageViewController alloc] init];
[newVC setIndex:currentIndex+1];
return newVC;
} else {
return nil;
}
}
currentIndex ++;
UIImage *currentPage = [self.modelArray objectAtIndex:currentIndex];
DSBookletPageViewController *newVC = [[DSBookletPageViewController alloc] initWithImage:currentPage andOrientation:isPortrait];
[newVC setIndex:currentIndex];
return newVC;
}
I basically check if I'm at the end of the array, or beyond it since I'm adding another page to the UIPageViewController outside of my Model array. If I'm there, and the current index is even and we're not in portrait orientation, then I add the page, if not, then I return nil.
I hope that helps.
Take a look at my answer here. Basically in your case you have to set the UIPageViewControllerDataSource which provides the content for all pages that are not shown at the time when the view appears for the first time.

Resources