Xcode Page Based Application Interface Rotation Issue - objective-c

Start a new page based application project in Xcode
Run the project and turn some pages
Rotate the simulator or device
=> The page view conroller switches back to the first page (january)
How can I prevent step 4. ?
EDIT:
This happens only the first time you rotate after the app started in simulator/device.
I use most recent Xcode 4.5 with iOS 6.0 Simulator and iOS 6 on my testing device.
The same thing happens when I download some other sample code from blogs / etc. Maybe an iOS 6 bug?
EDIT2:
I found out that the first page view that is passed to the UIPageViewController is not dealloced until first rotation. This really looks like a bug to me.

(UPDATE FROM 2014: This seems to have been fixed in iOS7, if you start again from a new Page View application template.)
I've experienced this bug as well. It seems to kick in any time after the main view reappears. My app has several full-screen modals in it, and after those go away the same behaviour occurs.
This happens in XCode 4.5.1 and iOS6 - I 'fixed' this by re-downloading XCode 4.4 and reverting my app back to iOS5.1. Obviously not a great long-term solution. I filed this in Radar and got a note back that it was already logged.
FWIW I noticed that iBooks had this same bug in it right after iOS6 came out, but they seem to have fixed it in a recent update.

Here's how I managed to fix this problem in my app. I'm afraid it's kind of a hacky solution, but it's a quirky bug.
Context: My app is a diary (it's called Remembary) and each page is a different day's diary entry. I have a singleton class called "AppContext" that keeps track of various app-level values, such as the currently showing diary entry object, the current date, and the like. Each day's dataViewController also keeps track of its own diary entry.
The trickiest part was finding a context where I could catch that the app was showing the wrong page. It turns out that this is in [RootViewController viewDidLayoutSubviews], so I added the following to that method:
// get the currently displaying page
DataViewController *currentPage = self.pageViewController.viewControllers[0];
// check if we're showing the wrong page
if ([currentPage myEntry] != [AppContext getCurrentEntry]) {
// jump to the proper page (the delay is needed to ensure that the rotation has fully completed)
[self performSelector:#selector(forceJumpToDate:)
withObject:[AppContext getCurrentEntryDate]
afterDelay:0.5];
}
Here's the forceJumpToDate function, which basically gets a new page based on the current date and tells the pageViewController to jump to it without animating:
- (void) forceJumpToDate:(NSDate *)targetDate {
DataViewController *targetPage = [self.modelController viewControllerForDate:targetDate
storyboard:self.storyboard];
NSArray *viewControllers = [NSArray arrayWithObject:targetPage];
[self.pageViewController setViewControllers:viewControllers
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
}
The user might notice a brief hiccup on the screen as the new page is forced into place, but this only happens if they would otherwise be getting the wrong page, so it's still an improvement.
This was seriously interfering with my ability to upgrade my app to iOS6, so I'm glad I finally figured it out.

Here is my solution:
// RootViewController.m
#import "RootViewController.h"
#import "ModelController.h"
#import "DataViewController.h"
#interface RootViewController ()
#property (readonly, strong, nonatomic) ModelController *modelController;
//added
#property (strong, nonatomic) DataViewController *currentViewController;
#end
#implementation RootViewController
#synthesize modelController = _modelController;
//added
#synthesize currentViewController = _currentViewController;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// Configure the page view controller and add it as a child view controller.
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
self.pageViewController.delegate = self;
DataViewController *startingViewController = [self.modelController viewControllerAtIndex:0 storyboard:self.storyboard];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];
self.pageViewController.dataSource = self.modelController;
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
// Set the page view controller's bounds using an inset rect so that self's view is visible around the edges of the pages.
CGRect pageViewRect = self.view.bounds;
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
pageViewRect = CGRectInset(pageViewRect, 40.0, 40.0);
}
self.pageViewController.view.frame = pageViewRect;
[self.pageViewController didMoveToParentViewController:self];
// Add the page view controller's gesture recognizers to the book view controller's view so that the gestures are started more easily.
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
//added
self.currentViewController = self.pageViewController.viewControllers[0];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (ModelController *)modelController
{
// Return the model controller object, creating it if necessary.
// In more complex implementations, the model controller may be passed to the view controller.
if (!_modelController) {
_modelController = [[ModelController alloc] init];
}
return _modelController;
}
#pragma mark - UIPageViewController delegate methods
/*
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
}
*/
//added
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
self.currentViewController = self.pageViewController.viewControllers[0];
}
- (DataViewController *)currentViewController
{
if (!_currentViewController) _currentViewController = [[DataViewController alloc] init];
return _currentViewController;
}
- (UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
if (UIInterfaceOrientationIsPortrait(orientation) || ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)) {
// In portrait orientation or on iPhone: Set the spine position to "min" and the page view controller's view controllers array to contain just one view controller. Setting the spine position to 'UIPageViewControllerSpineLocationMid' in landscape orientation sets the doubleSided property to YES, so set it to NO here.
//deleted: UIViewController *currentViewController = self.pageViewController.viewControllers[0];
//changed to self.currentViewController
NSArray *viewControllers = #[self.currentViewController];
[self.pageViewController setViewControllers:viewControllers
direction:UIPageViewControllerNavigationDirectionForward
animated:YES
completion:NULL];
self.pageViewController.doubleSided = NO;
return UIPageViewControllerSpineLocationMin;
}
// In landscape orientation: Set set the spine location to "mid" and the page view controller's view controllers array to contain two view controllers. If the current page is even, set it to contain the current and next view controllers; if it is odd, set the array to contain the previous and current view controllers.
// deleted: DataViewController *currentViewController = self.pageViewController.viewControllers[0];
//deleted: NSArray *viewControllers = nil;
//added
NSArray *viewControllers = #[self.currentViewController];
//changed currentViewController to self.currentViewController
NSUInteger indexOfCurrentViewController = [self.modelController indexOfViewController:self.currentViewController];
if (indexOfCurrentViewController == 0 || indexOfCurrentViewController % 2 == 0) {
UIViewController *nextViewController = [self.modelController pageViewController:self.pageViewController viewControllerAfterViewController:self.currentViewController];
viewControllers = #[self.currentViewController, nextViewController];
} else {
UIViewController *previousViewController = [self.modelController pageViewController:self.pageViewController viewControllerBeforeViewController:self.currentViewController];
viewControllers = #[previousViewController, self.currentViewController];
}
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
return UIPageViewControllerSpineLocationMid;
}
#end

What is it you want to prevent? Do you want to prevent rotation? If that is what you want, modify the shouldAutorotateToInterfaceOrientation return value in the RootViewController.m implementation file.
When I did this, the App was able to keep the same page (month) even after rotating the device. I used the simulator and tried on both iPhone and iPad. On the iPad, in landscape mode, it showed two months at a time, but then when rotated back to portrait, still kept the first of the two months that was displayed. This was when I incremented to June. I used the default project without changing a line of code.

Today I found out that in my app I could just use the following to remove the bug (but I have no clue why).
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
...
self.pageViewController.view.hidden = YES;
}
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
self.pageViewController.view.hidden = NO;
}

Related

iOS7 Autoresize When Switching Views Programmatically

I have googled for a few hours with no luck, so I'm coming to you guys to save me here!
Apparently, I can't find the right information on how exactly to go about doing this (or the best way). I have an app that supports portrait and landscape (no support for upside down though). However, the portrait and landscape views are COMPLETELY different, so I'll need to use two views to represent each. Am I correct in assuming I need 3 viewcontrollers in my storyboard (the main one, and then one for portrait and one for landscape? I was going to use just two but I didn't see how to if I start with the portrait, and then need to load landscape, I would have to delete portrait, which is where my code is?
My viewcontroller has the correct constraints in place to keep the label top center, but when replacing or swapping the views programmatically, it seems the auto-resize doesn't get called. I finally fixed this by resetting the frames on the subviews, but now when the device is flipped upside down, the portrait label is forever shifted to the right. So I'd just like to know the proper way to do this, as I'm sure this can't be it.
As far as code, I have one obj-c viewcontroller class with the following modified methods...
#interface AMBViewController ()
#property (strong, nonatomic) UIViewController *portraitViewController;
#property (strong, nonatomic) UIViewController *landscapeViewController;
#end
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
UIApplication *app = [UIApplication sharedApplication];
UIInterfaceOrientation currentOrientation = app.statusBarOrientation;
[self doLayoutForOrientation:currentOrientation];
}
-(void) willAnimateRotationToInterfaceOrientation:
(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[self doLayoutForOrientation:toInterfaceOrientation];
}
-(void) doLayoutForOrientation:(UIInterfaceOrientation)orientation {
if (UIInterfaceOrientationIsPortrait(orientation)) {
self.portraitViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"Portrait"];
if (self.landscapeViewController != nil ) {
[self.landscapeViewController.view removeFromSuperview];
self.landscapeViewController = nil;
}
self.portraitViewController.view.frame = self.view.bounds;
[self.view insertSubview:self.portraitViewController.view atIndex:0];
} else {
self.landscapeViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"Landscape"];
if (self.portraitViewController != nil ) {
[self.portraitViewController.view removeFromSuperview];
self.portraitViewController = nil;
}
self.landscapeViewController.view.frame = self.view.bounds;
[self.view insertSubview:self.landscapeViewController.view atIndex:0];
}
}
Just to be clear on my storyboard, I have one blank root controller (subclass AMBViewController) and two other view controllers "Landscape" and "Portrait"
I might also mention that the label only gets off in portrait view IF you rotate the device in a full circle (4 right or 4 left rotations). If you go right right (now it's upside down) but then left left, it's still fine. It's only when the screen flips from Right/Left Landscape to Left/Right Landscape that it messes up. Really weird, I know I must be omitting something important.
Any help is greatly appreciated. Thanks!
Solution: After finding a guide located on Apple's Dev Site (finally) I was able to come up with a solution using segues and a modal window. First view controller is portrait, second view controller is landscape, connected by a modal segue. The first view controller has the following modified methods:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
_isShowingLandscapeView = NO;
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
-(void) orientationChanged:(NSNotification *)notification {
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
if (UIInterfaceOrientationIsPortrait(orientation) && _isShowingLandscapeView
&& orientation != UIInterfaceOrientationPortraitUpsideDown) {
[self dismissViewControllerAnimated:NO completion:nil];
_isShowingLandscapeView = NO;
} else if (UIInterfaceOrientationIsLandscape(orientation) && !_isShowingLandscapeView ) {
[self performSegueWithIdentifier:#"ShowLandscape" sender:self];
_isShowingLandscapeView = YES;
}
}
Thanks to all who might have looked into this!

extending UIImagePicker controller doesn't help to prevent rotation in io6

My application is set in info.plist to support only portrait mode.
However, the UIImagePickerController, rotates when the user rotates the screen to landscape.
Since in io6 the method shouldAutoRotate is not being called, I tried to extend it like this:
#interface NonRotatingUIImagePickerController : UIImagePickerController
#end
#implementation NonRotatingUIImagePickerController
-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationMaskPortrait;
}
#end
But it doesn't help. Any idea why?
And in the log I see the above methods being called. The UIImagePickerController at first is displayed in portrait and when the user rotates - it rotates as well instead of staying portrait.
I set the image picker in the view like this:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (!self.imagePickerController) {
self.imagePickerController = [[NonRotatingUIImagePickerController alloc] init];
self.imagePickerController.delegate = self;
}
return self;
}
- (void)viewDidAppear:(BOOL)animated{
self.imagePickerController.showsCameraControls = NO;
CGRect imagePickerControllerFrame = CGRectMake(0, topBar.frame.size.height, self.view.frame.size.width, self.view.frame.size.height - topBar.frame.size.height - bottomBar.frame.size.height);
self.imagePickerController.view.frame = imagePickerControllerFrame;
self.imagePickerController.allowsEditing = YES;
self.imagePickerController.view.clipsToBounds = YES;
self.imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera
[self.view.window addSubview:self.imagePickerController.view];
}
self.imagePickerController.view.frame = imagePickerControllerFrame;
// ...
[self.view.window addSubview:self.imagePickerController.view];
Well, that's all totally illegitimate. Apple makes this very clear in the docs:
This class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified
There is only one correct way to use an image picker controller that uses UIImagePickerControllerSourceTypeCamera - as a fullscreen presented view controller:
BOOL ok = [UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypeCamera];
if (!ok) {
NSLog(#"no camera");
return;
}
NSArray* arr = [UIImagePickerController availableMediaTypesForSourceType:
UIImagePickerControllerSourceTypeCamera];
if ([arr indexOfObject:(NSString*)kUTTypeImage] == NSNotFound) {
NSLog(#"no stills");
return;
}
UIImagePickerController* picker = [UIImagePickerController new];
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.mediaTypes = #[(NSString*)kUTTypeImage];
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
If you want to present a live picture-taking interface inside your own interface, use AVFoundation and the camera capture API that it gives you.
Downloadable working example here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch30p816cameraCaptureWithAVFoundation/p683cameraCaptureWithAVFoundation/ViewController.m
Perhaps you'll consider this answer unhelpful; but I'll just paste a snippet from Apple's documentation:
Important: The UIImagePickerController class supports portrait mode only. This class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified, with one exception. You can assign a custom view to the cameraOverlayView property and use that view to present additional information or manage the interactions between the camera interface and your code.
UIImagePickerController Doc Link
Sorry to be a kill-joy. You should look for a replacement class. Quickie search shows there are a bunch.

Really Stuck on Screen Rotation iOS 6, I Need a xib locked to portrait

Having read every method of handling screen rotation in IOS 6 on here, Apple documents, and fierce Googling, after days and days I am still stuck on this when using xibs.
What im trying to achieve is have a few iPad only xibs, in landscape only, rest of app is portrait.
If I was using storyboards I understand how to subclass the nav controller to override the rotation but I am NOT, im using xibs and would appreciate any input how this works with xibs
In my app delegate:
ICHomeController *homeController = [[ICHomeController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:homeController];
navController.navigationBarHidden = YES;
navController.navigationBar.barStyle = UIBarStyleBlackOpaque;
navController.navigationBar.tintColor = [UIColor grayColor];
self.navigationController = navController;
[self.window setRootViewController:self.navigationController];
[self.window makeKeyAndVisible];
return YES;
In addition to trying what seems like every solution on here Ive tried the obvious in my vew controllers, but as I understand in iOS 6 it doesn't get called.
// Older versions of iOS (deprecated)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
}
- (BOOL)shouldAutorotate {
return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskLandscape;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeLeft;
}
I've been battling with this for days too with a more complex navigation structure.
The basic rule seems to be that the ROOT controller, whether it be a UINavigationController, UITabBarController or just a simple UIViewController, takes control of the rotation. So as you PUSH UIViewControllers onto the stack, the very first one still controls the rotation.
If you use a modal view, then this model view will be considered the ROOT of the stack until it is dismissed. So if you show a modal UIViewController then push views on top of it, the UIViewController that was presented as the modal view is in control of the rotation.
Just make sure for modal views to use presentViewController:animated:completion.
The easiest way to get certain views to be in a different rotation, is present it modally.
If this can't be avoided, then go right back to your first view or nag controller and set the rotations.
Add some NSLog(#"%s", __FUNCTION__); statements to your rotation methods to see which ones are being called.
In your code above, only the first method is the old iOS5 (Now deprecated) method. The other 3 are the new iOS 6 methods.
Using your example code above, you would need to subclass the UINavigationController. To do this create a new class as a UINavigationController subclass with a different name eg. MyNavigationController and add the rotation methods to it. Then in your code above instead of using
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:homeController];
You would use:
MyNavigationController *navController = [[MyNavigationController alloc] initWithRootViewController:homeController];
Also, make sure you have allowed orientations selected here:
I had the same problem. Here's what worked for me in 5 and 6.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || toInterfaceOrientation == UIInterfaceOrientationLandscapeRight);
}
- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0)
{
return NO;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) {
return UIInterfaceOrientationLandscapeLeft;
}
In terms of the target summary, only portrait mode is "supported".

How to set size for popover? [duplicate]

I have a UIPopoverController hosting a UINavigationController, which contains a small hierarchy of view controllers.
I followed the docs and for each view controller, I set the view's popover-context size like so:
[self setContentSizeForViewInPopover:CGSizeMake(320, 500)];
(size different for each controller)
This works as expected as I navigate forward in the hierarchy-- the popover automatically animates size changes to correspond to the pushed controller.
However, when I navigate "Back" through the view stack via the navigation bar's Back button, the popover doesn't change size-- it remains as large as the deepest view reached. This seems broken to me; I'd expect the popover to respect the sizes that are set up as it pops through the view stack.
Am I missing something?
Thanks.
I was struggling with the same issue. None of the above solutions worked for me pretty nicely, that is why I decided to do a little investigation and find out how this works.
This is what I discovered:
When you set the contentSizeForViewInPopover in your view controller it won't be changed by the popover itself - even though popover size may change while navigating to different controller.
When the size of the popover will change while navigating to different controller, while going back, the size of the popover does not restore
Changing size of the popover in viewWillAppear gives very strange animation (when let's say you popController inside the popover) - I'd not recommend it
For me setting the hardcoded size inside the controller would not work at all - my controllers have to be sometimes big sometimes small - controller that will present them have the idea about the size though
A solution for all that pain is as follows:
You have to reset the size of currentSetSizeForPopover in viewDidAppear. But you have to be careful, when you will set the same size as was already set in field currentSetSizeForPopover then the popover will not change the size. For this to happen, you can firstly set the fake size (which will be different than one which was set before) followed by setting the proper size. This solution will work even if your controller is nested inside the navigation controller and popover will change its size accordingly when you will navigate back between the controllers.
You could easily create category on UIViewController with the following helper method that would do the trick with setting the size:
- (void) forcePopoverSize {
CGSize currentSetSizeForPopover = self.contentSizeForViewInPopover;
CGSize fakeMomentarySize = CGSizeMake(currentSetSizeForPopover.width - 1.0f, currentSetSizeForPopover.height - 1.0f);
self.contentSizeForViewInPopover = fakeMomentarySize;
self.contentSizeForViewInPopover = currentSetSizeForPopover;
}
Then just invoke it in -viewDidAppear of desired controller.
Here's how I solved it for iOS 7 and 8:
In iOS 8, iOS is silently wrapping the view you want in the popover into the presentedViewController of the presentingViewController view controller. There's a 2014 WWDC video explaining what's new with the popovercontroller where they touch on this.
Anyways, for view controllers presented on the navigation controller stack that all want their own sizing, these view controllers need (under iOS 8) to call this code to dynamically set the preferredContentSize:
self.presentingViewController.presentedViewController.preferredContentSize = CGSizeMake(320, heightOfTable);
Replace heightOfTable with your computed table or view height.
In order to avoid a lot of duplicate code and to create a common iOS 7 and iOS 8 solution, I created a category on UITableViewController to perform this work when viewDidAppear is called in my tableviews:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self setPopOverViewContentSize];
}
Category.h:
#import <UIKit/UIKit.h>
#interface UITableViewController (PreferredContentSize)
- (void) setPopOverViewContentSize;
#end
Category.m:
#import "Category.h"
#implementation UITableViewController (PreferredContentSize)
- (void) setPopOverViewContentSize
{
[self.tableView layoutIfNeeded];
int heightOfTable = [self.tableView contentSize].height;
if (heightOfTable > 600)
heightOfTable = 600;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.0)
self.preferredContentSize=CGSizeMake(320, heightOfTable);
else
self.presentingViewController.presentedViewController.preferredContentSize = CGSizeMake(320, heightOfTable);
}
}
#end
This is an improvement on krasnyk's answer.
Your solution is great, but it isn't smoothly animated.
A little improvement gives nice animation:
Remove last line in the - (void) forcePopoverSize method:
- (void) forcePopoverSize {
CGSize currentSetSizeForPopover = self.contentSizeForViewInPopover;
CGSize fakeMomentarySize = CGSizeMake(currentSetSizeForPopover.width - 1.0f, currentSetSizeForPopover.height - 1.0f);
self.contentSizeForViewInPopover = fakeMomentarySize;
}
Put [self forcePopoverSize] in - (void)viewWillAppear:(BOOL)animated method:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self forcePopoverSize];
}
And finally - set desired size in - (void)viewDidAppear:(BOOL)animated method:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGSize currentSetSizeForPopover = self.contentSizeForViewInPopover;
self.contentSizeForViewInPopover = currentSetSizeForPopover;
}
You need to set the content size again in viewWillAppear. By calling the delagate method in which you set the size of popovercontroller. I had also the same issue. But when I added this the problem solved.
One more thing: if you are using beta versions lesser than 5. Then the popovers are more difficult to manage. They seem to be more friendly from beta version 5. It's good that final version is out. ;)
Hope this helps.
In the -(void)viewDidLoad of all the view controllers you are using in navigation controller, add:
[self setContentSizeForViewInPopover:CGSizeMake(320, 500)];
I reset the size in the viewWillDisappear:(BOOL)animated method of the view controller that is being navigated back from:
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
CGSize contentSize = [self contentSizeForViewInPopover];
contentSize.height = 0.0;
self.contentSizeForViewInPopover = contentSize;
}
Then when the view being navigated back to appears, I reset the size appropriately:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CGSize contentSize;
contentSize.width = self.contentSizeForViewInPopover.width;
contentSize.height = [[self.fetchedResultsController fetchedObjects] count] * self.tableView.rowHeight;
self.contentSizeForViewInPopover = contentSize;
}
For iOS 8 the following works:
- (void) forcePopoverSize {
CGSize currentSetSizeForPopover = self.preferredContentSize;
CGSize fakeMomentarySize = CGSizeMake(currentSetSizeForPopover.width - 1.0f, currentSetSizeForPopover.height - 1.0f);
self.preferredContentSize = fakeMomentarySize;
self.navigationController.preferredContentSize = fakeMomentarySize;
self.preferredContentSize = currentSetSizeForPopover;
self.navigationController.preferredContentSize = currentSetSizeForPopover;
}
BTW I think, this should be compatible with previous iOS versions...
Well i worked out. Have a look.
Made a ViewController in StoryBoard. Associated with PopOverViewController class.
import UIKit
class PopOverViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = CGSizeMake(200, 200)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: "dismiss:")
}
func dismiss(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
See ViewController:
//
// ViewController.swift
// iOS8-PopOver
//
// Created by Alvin George on 13.08.15.
// Copyright (c) 2015 Fingent Technologies. All rights reserved.
//
import UIKit
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate
{
func showPopover(base: UIView)
{
if let viewController = self.storyboard?.instantiateViewControllerWithIdentifier("popover") as? PopOverViewController {
let navController = UINavigationController(rootViewController: viewController)
navController.modalPresentationStyle = .Popover
if let pctrl = navController.popoverPresentationController {
pctrl.delegate = self
pctrl.sourceView = base
pctrl.sourceRect = base.bounds
self.presentViewController(navController, animated: true, completion: nil)
}
}
}
override func viewDidLoad(){
super.viewDidLoad()
}
#IBAction func onShow(sender: UIButton)
{
self.showPopover(sender)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
}
Note: The func showPopover(base: UIView) method should be placed before ViewDidLoad. Hope it helps !
For me this solutions works.
This is a method from my view controller which extends UITableViewController and is the root controller for UINavigationController.
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.contentSizeForViewInPopover = self.tableView.bounds.size;
}
And don't forget to set content size for view controller you gonna push into navigation stack
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath{
dc = [[DetailsController alloc] initWithBookmark:[[bookmarksArray objectAtIndex:indexPath.row] retain] bookmarkIsNew:NO];
dc.detailsDelegate = self;
dc.contentSizeForViewInPopover = self.contentSizeForViewInPopover;
[self.navigationController pushViewController:dc animated:YES];
}
if you can imagine the assambler, I think this is slightly better:
- (void) forcePopoverSize {
CGSize currentSetSizeForPopover = self.contentSizeForViewInPopover;
self.contentSizeForViewInPopover = CGSizeMake(0, 0);
self.contentSizeForViewInPopover = currentSetSizeForPopover;
}
The accepted answer is not working fine with iOS 8. What I did was creating my own subclass of UINavigationController for use in that popover and override the method preferredContentSize in this way:
- (CGSize)preferredContentSize {
return [[self.viewControllers lastObject] preferredContentSize];
}
Moreover, instead of calling forcePopoverSize (method implemented by #krasnyk) in viewDidAppear I decided to set a viewController (which shows popover) as a delegate for previously mentioned navigation (in popover) and do (what force method does) in:
-(void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animated
delegate method for a passed viewController. One important thing, doing forcePopoverSize in a UINavigationControllerDelegate method is fine if you do not need that animation to be smooth if so then do leave it in viewDidAppear.
I was facing same problem, but you don't want to set contentsize in viewWillAppear or viewWillDisappear method.
AirPrintController *airPrintController = [[AirPrintController alloc] initWithNibName:#"AirPrintController" bundle:nil];
airPrintController.view.frame = [self.view frame];
airPrintController.contentSizeForViewInPopover = self.contentSizeForViewInPopover;
[self.navigationController pushViewController:airPrintController animated:YES];
[airPrintController release];
set contentSizeForViewInPopover property for that controller before pushing that controller to navigationController
I've had luck by putting the following in the viewdidappear:
[self.popoverController setPopoverContentSize:self.contentSizeForViewInPopover animated:NO];
Although this may not animate nicely in the case when you're pushing/popping different-sized popovers. But in my case, works perfectly!
All that you have to do is:
-In the viewWillAppear method of the popOvers contentView, add the snippet given below. You will have to specify the popOver's size first time when it is loaded.
CGSize size = CGSizeMake(width,height);
self.contentSizeForViewInPopover = size;
I had this issue with a popover controller whose popoverContentSize = CGSizeMake(320, 600) at the start, but would get larger when navigating through its ContentViewController (a UINavigationController).
The nav controller was only pushing and popping custom UITableViewControllers, so in my custom table view controller class's viewDidLoad i set self.contentSizeForViewInPopover = CGSizeMake(320, 556)
The 44 less pixels are to account for the Nav controller's nav bar, and now I don't have any issues anymore.
Put this in all view controllers you are pushing inside the popover
CGSize currentSetSizeForPopover = CGSizeMake(260, 390);
CGSize fakeMomentarySize = CGSizeMake(currentSetSizeForPopover.width - 1.0f,
currentSetSizeForPopover.height - 1.0f);
self.contentSizeForViewInPopover = fakeMomentarySize;
self.contentSizeForViewInPopover = currentSetSizeForPopover;
Faced the same issue and fixed it by setting content view size to navigation controller and view controller before the init of UIPopoverController was placed.
CGSize size = CGSizeMake(320.0, _options.count * 44.0);
[self setContentSizeForViewInPopover:size];
[self.view setFrame:CGRectMake(0.0, 0.0, size.width, size.height)];
[navi setContentSizeForViewInPopover:size];
_popoverController = [[UIPopoverController alloc] initWithContentViewController:navi];
I'd just like to offer up another solution, as none of these worked for me...
I'm actually using it with this https://github.com/nicolaschengdev/WYPopoverController
When you first call your popup use this.
if ([sortTVC respondsToSelector:#selector(setPreferredContentSize:)]) {
sortTVC.preferredContentSize = CGSizeMake(popoverContentSortWidth,
popoverContentSortHeight);
}
else
{
sortTVC.contentSizeForViewInPopover = CGSizeMake(popoverContentSortWidth,
popoverContentSortHeight);
}
Then in that popup use this.
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
if ([self respondsToSelector:#selector(setPreferredContentSize:)]) {
self.preferredContentSize = CGSizeMake(popoverContentMainWidth,
popoverContentMainheight);
}
else
{
self.contentSizeForViewInPopover = CGSizeMake(popoverContentMainWidth,
popoverContentMainheight);
}
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:YES];
self.contentSizeForViewInPopover = CGSizeZero;
}
Then repeat for child views...
This is the correct way in iOS7 to do this,
Set the preferred content size in viewDidLoad in each view controller in the navigation stack (only done once). Then in viewWillAppear get a reference to the popover controller and update the contentSize there.
-(void)viewDidLoad:(BOOL)animated
{
...
self.popoverSize = CGSizeMake(420, height);
[self setPreferredContentSize:self.popoverSize];
}
-(void)viewWillAppear:(BOOL)animated
{
...
UIPopoverController *popoverControllerReference = ***GET REFERENCE TO IT FROM SOMEWHERE***;
[popoverControllerReference setPopoverContentSize:self.popoverSize];
}
#krasnyk solution worked well in previous iOS versions but not working in iOS8. The following solution worked for me.
- (void) forcePopoverSize {
CGSize currentSetSizeForPopover = self.preferredContentSize;
//Yes, there are coupling. We need to access the popovercontroller. In my case, the popover controller is a weak property in the app's rootVC.
id mainVC = [MyAppDelegate appDelegate].myRootVC;
if ([mainVC valueForKey:#"_myPopoverController"]) {
UIPopoverController *popover = [mainVC valueForKey:#"_myPopoverController"];
[popover setPopoverContentSize:currentSetSizeForPopover animated:YES];
}
}
It is not the best solution, but it works.
The new UIPopoverPresentationController also has the resizing issue :( .
You need to set the preferredContentSizeproperty of the NavigationController in viewWillAppear:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navigationController.preferredContentSize = CGSizeMake(320, 500);}

UIPageViewControllerSpineLocation Delegate Method Not Firing

Major head-scratcher all day on this one :-(
I have an instance of a UIPageViewController that does not appear to be firing the delegate method:
-(UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
I have tried various methods of displaying the UIPageViewController and have settled on a programatic approach (as opposed to a Storyboard one) that appears to be working correctly, with one exception... when rotating the iPad to landscape the spine does not appear mid-point as expected. I simply cannot find out why the delegate method does not get called.
Code Explanation (simplified for example)
Consider three classes as follows:
RootViewController - loaded when the app starts
PageViewController - loaded by RootViewController upon user initiation
PageContentViewController - loaded by PageViewController when pages are needed
Fairly self-explanatory. The RootViewController is loaded by the app upon launch. When the user taps an image within this view controller's view (think magazine cover opening a magazine) it launches the PageViewController as follows:
PageViewController *pvc = [[PageViewController alloc] initWithNibName:#"PageView"
bundle:[NSBundle mainBundle]];
pvc.view.frame = self.view.bounds;
[self.view addSubview:pvc.view];
In the actual app there is animation etc to make the transition all nice, but essentially the PageViewController's view is loaded and takes fullscreen.
PageViewController
This is the workhorse (only relevant methods shown). I have tried various examples from the infinite world of Google and written directly from the Apple docs...
#interface PageViewController : UIViewController <UIPageViewControllerDelegate, UIPageViewControllerDataSource>
#property (nonatomic, strong) UIPageViewController *pageViewController;
#property (nonatomic, strong) NSMutableArray *modelArray;
#end
#implementation TXCategoryController
-(void)viewDidLoad
{
[super viewDidLoad];
// Simple model for demo
self.modelArray = [NSMutableArray alloc] init];
for (int i=1; i<=20; i++)
[self.modelArray addObject:[NSString stringWithFormat:#"Page: %d", i]];
self.pageViewController = [[UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
self.pageViewController.delegate = self;
self.pageViewController.dataSource = self;
PageContentViewController *startupVC = [[PageContentViewController alloc] initWithNibName:#"PageContent" bundle:nil];
startupVC.pageLabel = [self.modelArray objectAtIndex:0];
[self.pageViewController setViewControllers:[NSArray arrayWithObject:startupVC]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil];
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
[self.pageViewController didMoveToParentViewController:self];
self.pageViewController.view.frame = self.view.bounds;
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerBeforeViewController:(UIViewController *)viewController
{
// Relevant code to add another view...
}
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController
viewControllerAfterViewController:(UIViewController *)viewController
{
// Relevant code to add another view...
}
-(UIPageViewControllerSpineLocation)pageViewController:(UIPageViewController *)pageViewController
spineLocationForInterfaceOrientation:(UIInterfaceOrientation)orientation
{
// Setting a break point in here - never gets called
if (UIInterfaceOrientationIsPortrait(orientation))
{
// Relevant code to create view...
return UIPageViewControllerSpineLocationMin;
}
// Relevant code to create 2 views for side-by-side display and
// set those views using self.pageViewController setViewControllers:
return UIPageViewControllerSpineLocationMid
}
#end
This all works perfectly well as I mentioned earlier. The PageViewController's view gets shown. I can swipe pages left and right in both portrait and landscape and the respective page number appears. However, I don't ever see two pages side-by-side in landscape view. Setting a breakpoint in the spineLocationForInterfaceOrientation delegate method never gets called.
This is such a head-scratcher I have burned out of ideas on how to debug/solve the problem. It almost behaves like the UIPageViewController isn't responding to the orientation changes of the device and therefore isn't firing off the delegate method. However, the view gets resized correctly (but that could be just the UIView autoresizing masks handling that change).
If I create a brand new project with just this code (and appropriate XIb's etc) it works perfectly fine. So something somewhere in my actual project is causing this. I have no idea where to continue looking.
As usual, any and all help would be very much appreciated.
Side Note
I wanted to add the tag 'uipageviewcontrollerspinelocation' but couldn't because it was too long and I didn't have enough reputation (1500 required). I think this is a devious ploy on Apple's part to avoid certain tags in Stackoverflow... ;-)
Finally found the problem. It was something of a red herring in its symptoms, but related just the same.
Putting a break point in the shouldAutorotateToInterfaceOrientation: method was a natural test to see if the UIViewController was even getting a rotation notification. It wasn't which led me to Apple's technical Q&A on the issue: http://developer.apple.com/library/ios/#qa/qa1688/_index.html
The most relevant point in there was:
The view controller's UIView property is embedded inside UIWindow but alongside an additional view controller.
Unfortunately, Apple, in its traditional documentation style, doesn't provide an answer, merely confirmation of the problem. But an answer on Stack Overflow yielded the next clue:
Animate change of view controllers without using navigation controller stack, subviews or modal controllers?
Although my RootViewController was loading the PageViewController, I was doing it as a subview to the main view. This meant I had two UIViewController's in which only the parent would respond to changes.
The solution to get the PageViewController to listen to the orientation changes (thus triggering the associated spine delegate method) was to remove addSubview: and instead present the view controller from RootViewController:
[self presentViewController:pac animated:YES completion:NULL];
Once that was done, the orientation changes were being picked up and the PageViewController was firing the delegate method for spine position. Only one minor detail to consider. If the view was launched in landscape, the view was still displaying portrait until rotated to portrait and back to landscape.
That was easily tweaked by editing viewDidLoad as follows:
PageContentViewController *page1 = [[PageContentViewController alloc] initWithNibName:#"PageContent" bundle:nil];
NSDictionary *pageViewOptions = nil;
NSMutableArray *pagesArray = [NSMutableArray array];
if (IS_IPAD && UIInterfaceOrientationIsLandscape(self.interfaceOrientation))
{
pageViewOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:UIPageViewControllerSpineLocationMid]
forKey:UIPageViewControllerOptionSpineLocationKey];
PageContentViewController *page2 = [[PageContentViewController alloc] initWithNibName:#"PageContent" bundle:nil];
[pagesArray addObject:page1];
[pagesArray addObject:page2];
}
else
{
[pagesArray addObject:page1];
}
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStylePageCurl
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:pageViewOptions];
self.pageViewController.delegate = self;
[self.pageViewController setViewControllers:pagesArray
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
Job done and problem solved.