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);}
Related
So. Just started transitioning my IOS code to IOS7, and ran into a bit of problem.
I've got a UINavigationController, which has child ViewControllers and I'm using pushViewController to display the next views. To create a parallax animation with a set of images, if customized the UINavigationController to animate a set of UIImageViews and my child ViewControllers all have a self.backgroundColor = [UIColor clearColor], transparency.
Since iOS7, the way the UINavController animates it child vc's, is updated, by partially moving the current view controller and on top pushing the new viewcontroller, my parallax animation looks crap. I see the previous VC move a bit and then disappear. Is there any way I can restore the previous UINavigationController pushViewController animation? I can't seem to find this in the code.
WelcomeLoginViewController* welcomeLoginViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"WelcomeLogin"];
[self.navigationController pushViewController:welcomeLoginViewController animated:YES];
Even tried using:
[UIView animateWithDuration:0.75
animations:^{
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[self.navigationController pushViewController:welcomeLoginViewController animated:NO];
[UIView setAnimationTransition:<specific_animation_form> forView:self.navigationController.view cache:NO];
}];
Does anyone have any clue?
I managed to workaround the new transition type by creating a category for UINavigationController. In my case I needed to revert it to the old transition style because I have transparent viewControllers that slide over a static background.
UINavigationController+Retro.h
#interface UINavigationController (Retro)
- (void)pushViewControllerRetro:(UIViewController *)viewController;
- (void)popViewControllerRetro;
#end
UINavigationController+Retro.m
#import "UINavigationController+Retro.h"
#implementation UINavigationController (Retro)
- (void)pushViewControllerRetro:(UIViewController *)viewController {
CATransition *transition = [CATransition animation];
transition.duration = 0.25;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromRight;
[self.view.layer addAnimation:transition forKey:nil];
[self pushViewController:viewController animated:NO];
}
- (void)popViewControllerRetro {
CATransition *transition = [CATransition animation];
transition.duration = 0.25;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
[self.view.layer addAnimation:transition forKey:nil];
[self popViewControllerAnimated:NO];
}
#end
I have the same problem with clear background colors and crappy animations, so I create custom transitioning for ViewController with new iOS7 API. All you need is simply set a delegate for your navigation controller:
// NavigationController does not retain delegate, so you should hold it.
self.navigationController.delegate = self.navigationTransitioningDelegate;
Just add this files into your project: MGNavigationTransitioningDelegate.
I had a problem where when UIViewController A did a pushViewController to push UIViewController B, the push animation would stop at about 25%, halt, and then slide B in the rest of the way.
This DID NOT happen on iOS 6, but as soon as I started using iOS 7 as the base SDK in XCode 5, this started happening.
The fix is that view controller B did not have a backgroundColor set on its root view (the root view is the one that is the value of viewController.view, that you typically set in loadView). Setting a backgroundColor in that root view's initializer fixed the problem.
I managed to fix this as follows:
// CASE 1: The root view for a UIViewController subclass that had a halting animation
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
// Do some initialization ...
// self.backgroundColor was NOT being set
// and animation in pushViewController was slow and stopped at 25% and paused
}
return self;
}
// CASE 2: HERE IS THE FIX
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
// Do some initialization ...
// Set self.backgroundColor for the fix!
// and animation in pushViewController is no longer slow and and no longer stopped at 25% and paused
self.backgroundColor = [UIColor whiteColor]; // or some other non-clear color
}
return self;
}
First of, I'm not using Storyboard. I tried using UINavigationController+Retro. For some reason, the UINavigationController is having a hard time releasing the UIViewController at the top of the stack. Here's the solution that works for me using iOS 7 custom transition.
Set delegate to self.
navigationController.delegate = self;
Declare this UINavigationControllerDelegate.
- (id<UIViewControllerAnimatedTransitioning>)navigationController (UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
TransitionAnimator *animator = [TransitionAnimator new];
animator.presenting = YES;
return animator;
}
Note that it'll only get called when animated is set to YES. For example
[navigationController pushViewController:currentViewController animated:YES];
Create the animator class extending NSObject. I called mine TransitionAnimator, which was modified from TeehanLax's TLTransitionAnimator inside UIViewController-Transitions-Example.
TransitionAnimator.h
#import <Foundation/Foundation.h>
#interface TransitionAnimator : NSObject <UIViewControllerAnimatedTransitioning>
#property (nonatomic, assign, getter = isPresenting) BOOL presenting;
#end
TransitionAnimator.m
#import "TransitionAnimator.h"
#implementation TransitionAnimator
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return 0.5f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
if (self.presenting) {
//ANIMATE VC ENTERING FROM THE RIGHT SIDE OF THE SCREEN
[transitionContext.containerView addSubview:fromVC.view];
[transitionContext.containerView addSubview:toVC.view];
toVC.view.frame = CGRectMake(0, 0, 2*APP_W0, APP_H0); //SET ORIGINAL POSITION toVC OFF TO THE RIGHT
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
fromVC.view.frame = CGRectMake(0, (-1)*APP_W0, APP_W0, APP_H0); //MOVE fromVC OFF TO THE LEFT
toVC.view.frame = CGRectMake(0, 0, APP_W0, APP_H0); //ANIMATE toVC IN TO OCCUPY THE SCREEN
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}else{
//ANIMATE VC EXITING TO THE RIGHT SIDE OF THE SCREEN
}
}
#end
Use presenting flag to set the direction you want to animate or which ever condition you prefer. Here's the link to Apple reference.
Thanks guys for the feedback. Found a solution in completely recreating the UINavigationController's behavior. When I was nearly finished I ran into Nick Lockwood's solution:
https://github.com/nicklockwood/OSNavigationController
OSNavigationController is a open source re-implementation of UINavigationController. It currently features only a subset of the functionality of UINavigationController, but the long-term aim is to replicate 100% of the features.
OSNavigationController is not really intended to be used as-is. The idea is that you can fork it and then easily customize its appearance and behaviour to suit any special requirements that your app may have. Customizing OSNavigationController is much simpler than trying to customize UINavigationController due to the fact that the code is open and you don't need to worry about private methods, undocumented behavior, or implementation changes between versions.
By overriding my UINavigationController with his code, I was able to work with background images in UINavigationcontrollers
Thanks!
Simply add in:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
This:
[[self window] setBackgroundColor:[UIColor whiteColor]];
The final result:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions (NSDictionary *)launchOptions {
[[self window] setBackgroundColor:[UIColor whiteColor]];
// Override point for customization after application launch.
return YES;
}
Apparently in iOS7 there's a new way define your own custom UIViewController transitions. Look in the docs for UIViewControllerTransitioningDelegate. Also, here's a link to an article about it: http://www.doubleencore.com/2013/09/ios-7-custom-transitions/
Swift 5 implementation of Arne's answer:
extension UINavigationController {
func pushViewControllerLegacyTransition(_ viewController: UIViewController) {
let transition = CATransition()
transition.duration = 0.25
transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
transition.type = .push
transition.subtype = .fromRight
view.layer.add(transition, forKey: nil)
pushViewController(viewController, animated: false)
}
func popViewControllerLegacyTransition() {
let transition = CATransition()
transition.duration = 0.25
transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
transition.type = .push
transition.subtype = .fromLeft
view.layer.add(transition, forKey: nil)
popViewController(animated: false)
}
}
Found another great resource to help out:
http://www.teehanlax.com/blog/custom-uiviewcontroller-transitions
Using iOS7 TLTransitionAnimator to create custom animations
I voted for #Arne's answer, because I find it the most elegant solution to this problem. I would just like to add some code in order to answer to #Bill's problem from his comment on #Arne's solution. Here's comment quote:
Thanks, this works for me. However, when the user taps the Back
button, it reverts to the busted animation (because the back button
doesn't call popViewControllerRetro). – Bill Oct 3 at 12:36
In order to call popViewControllerRetro when back button is pressed, there's a small hack you can perform in order to achieve this. Go into your pushed view controller, import UIViewController+Retro.h and add this code in your viewWillDisappear method:
- (void)viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) {
[self.navigationController popViewControllerRetro];
}
[super viewWillDisappear:animated];
}
This if statement will detect when Back button is pressed and will call popViewControllerRetro from category class.
Best regards.
Is it possible to pop up an UIViewController (xib file) like UIPopOverControl in iPad ?
I have a separate NIB file which is linked to an UIViewController. I want to popup that NIB file along with the button pressed with a customised size (200,200).
Is this possible?
I am trying to get something like this on the iPhone - http://www.freeimagehosting.net/c219p
You can also use one of these custom made clases to show a popup:
https://github.com/sonsongithub/PopupView
https://github.com/werner77/WEPopover
https://github.com/50pixels/FPPopover
Example with FPPopover:
//the view controller you want to present as popover
YourViewController *controller = [[YourViewController alloc] init];
//our popover
FPPopoverController *popover = [[FPPopoverController alloc] initWithViewController:controller];
//the popover will be presented from the okButton view
[popover presentPopoverFromView:okButton];
//release if you arent using ARC
[controller release];
yes it is. load Your pOpOver controller lazily at the point when it is needed. add its view as a subview (you could animate the addition). make its frame size what You need and add the image You have shown as a background subview of the pOpOver controller along with other controls You want in the pop up.
good luck
UPDATE:
alright, ii will show You how ii do this in my app Lucid Reality Check (deployment target iOS4.3).
one can use a UIPopoverController to present another controllers view. what ii do first is to make sure ii always know the current orientation of the device, so ii can reposition the popup on rotation (maybe this works by itself on iOS6?). so in my base controller (from where ii want to show a popup) ii have an instance variable like this:
UIInterfaceOrientation toOrientation;
and also:
UIPopoverController *popover;
UIButton *popover_from_button;
BOOL representPopover;
popover will be reused for all popups, and popover_from_button will hold the button from which the popup is initiated.
then the next code comes into the base controller:
- (void)popoverWillRotate {
if ([popover isPopoverVisible]) {
[self dismissPopover];
representPopover = YES;
}
}
- (void)popoverDidRotate {
if (popover && representPopover) {
representPopover = NO;
[self representPopover];
}
}
these two methods have to be called every time the device is rotated, like this:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
//DLOG(#"willRotateTo %i", toInterfaceOrientation);
toOrientation = toInterfaceOrientation;
if ([Kriya isPad ]) {
[self popoverWillRotate];
}
}
as one can see, first the orientation is captured then popoverWillRotate is called. this would hide the popover during the orientation animation. and after rotating, the popover must be redisplayed like this:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
//DLOG(#"didRotateFrom %i", fromInterfaceOrientation);
//[self layout:toOrientation]; //do some layout if You need
if ([Kriya isPad]) {
[self popoverDidRotate];
}
}
- (void)layout:(UIInterfaceOrientation)toInterfaceOrientation {
//one can do view layout here, and call other controllers to do their layout too
}
now that the orientation changes are worked out, the code for presenting the popover arrives here:
#pragma mark Popovers
- (void)presentPopoverWith:(id)controller fromButton:(UIButton*)button {
if (popover)
[popover release];
if (popover_from_button)
[popover_from_button release];
popover_from_button = [button retain];
popover = [[UIPopoverController alloc] initWithContentViewController:controller];
[popover setDelegate:self];
[self representPopover];
}
- (void)representPopover{
if (popover) {
UIPopoverArrowDirection arrowDirection = UIPopoverArrowDirectionAny;
UIViewController *vc = (UIViewController*)[popover contentViewController];
CGSize contentSize = [vc contentSizeForViewInPopover];
if (contentSize.width > 0 && contentSize.height > 0) {
[popover setPopoverContentSize:contentSize animated:NO];
}
//DLOG(#"representPopover rect:%#", [Kriya printRect:popover_from_button.frame]);
[popover presentPopoverFromRect:CGRectOffset(popover_from_button.frame, 0, popover_from_button.frame.size.height + 7.0) inView:self.view permittedArrowDirections:arrowDirection animated:YES];
}
}
- (void)dismissPopover {
if (popover) {
[popover dismissPopoverAnimated:YES];
}
}
finally, if one wants to be notified when the popover is dismissed, the base controller must implement a delegate method:
#pragma mark UIPopoverControllerDelegate
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
//do something important here like drink some water
}
and don't forget to make the base controller a UIPopoverControllerDelegate in its header.
a use case for this way of doing popups would then look like this:
- (void)takeImage {
UIImagePickerController *picker = [[[UIImagePickerController alloc] init] autorelease];
[picker setDelegate:self];
[picker setAllowsEditing:NO];
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
[picker setSourceType:UIImagePickerControllerSourceTypeCamera];
if ([Kriya isPad]) {
[self presentPopoverWith:picker fromButton:backgroundImageButton];
} else {
//modals on iPhone/iPod
//DLOG(#"takeImage addSubview picker");
[self presentModalViewController:picker animated:YES];
}
} else {
//DLOG(#"no camera");
}
}
this would use an image picker as the content for the popup, but one can use any controller with a valid view. so just do this:
[self presentPopoverWith:popupsContentController fromButton:tappedButton];
one should not have any missing information, :), the method [Kriya isPad] is just this:
+ (BOOL)isPad {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 30200
// iPad capable OS
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
//this is an iPad
return YES;
}else {
//this is an iPod/iPhone
return NO;
}
#else
//can not pissible be iPad
return NO;
#endif
}
ENJOY!
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;
}
I can't keep popover the same position on the screen after rotation. Is there any good way to do that, because just setting some frame to popover works terrible after rotating.popover.frame = CGRectMake(someFrame); After rotation popover looks fine only if it is in the center of the screen.
Apple has a Q&A on exactly this issue. You can find the details here:
Technical Q&A QA1694 Handling Popover Controllers During Orientation Changes
Basically, the technique explains that in your view controller's didRotateFromInterfaceOrientation method, you will present the pop over again as follows:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[aPopover presentPopoverFromRect:targetRect.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
For more information, have a read of the article above, and also the UIPopoverController Class Reference:
If the user rotates the device while a popover is visible, the popover
controller hides the popover and then shows it again at the end of the
rotation. The popover controller attempts to position the popover
appropriately for you but you may have to present it again or hide it
altogether in some cases. For example, when displayed from a bar
button item, the popover controller automatically adjusts the position
(and potentially the size) of the popover to account for changes to
the position of the bar button item. However, if you remove the bar
button item during the rotation, or if you presented the popover from
a target rectangle in a view, the popover controller does not attempt
to reposition the popover. In those cases, you must manually hide the
popover or present it again from an appropriate new position. You can
do this in the didRotateFromInterfaceOrientation: method of the view
controller that you used to present the popover.
As of iOS 8.0.2 willRotateToInterfaceOrientation will not have any effect. As mhrrt mentioned, you need to use the delegate method:
- (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView *__autoreleasing *)view
So for example if you want your popover to appear directly below a button that was pressed, you would use the following code:
- (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView *__autoreleasing *)view
{
CGRect rectInView = [self.theButton convertRect:self.theButton.frame toView:self.view];
*rect = CGRectMake(CGRectGetMidX(rectInView), CGRectGetMaxY(rectInView), 1, 1);
*view = self.view;
}
In iOS 7 you can use - (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView *__autoreleasing *)view to reposition your UIPopoverController's view on interface orientation change.
See the UIPopoverControllerDelegate documentation.
You can do this in didRotateFromInterfaceOrientation: method of the view controller that you used to present the popover.
Use setPopoverContentSize:animated: method for setting the size of the popover.
UIPopoverController was deprecated in ios9 in favor of UIPopoverPresentationController introduced in ios8. (I went through this transition also when going from UIActionSheet to UIAlertController.) You have two choices (example in obj-C):
A. Implement the UIViewController method below (UIKit calls this method before changing the size of a presented view controller’s view).
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:nil
completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// Fix up popover placement if necessary, *after* the transition.
// Be careful here if a subclass also overrides this method.
if (self.presentedViewController) {
UIPopoverPresentationController *presentationController =
[self.presentedViewController popoverPresentationController];
UIView *selectedView = /** YOUR VIEW */;
presentationController.sourceView = selectedView.superview;
presentationController.sourceRect = selectedView.frame;
}
}];
}
B. Alternatively, when configuring your UIPopoverPresentationController to present, also set its delegate. e.g. your presenting vc can implement UIPopoverPresentationControllerDelegate and assign itself as the delegate. Then implement the delegate method:
- (void)popoverPresentationController:(UIPopoverPresentationController *)popoverPresentationController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView * _Nonnull *)view {
UIView *selectedView = /** YOUR VIEW */;
// Update where the arrow pops out of in the view you selected.
*view = selectedView;
*rect = selectedView.bounds;
}
For Swift:
func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>)
{
rect.pointee = CGRect(x: self.view.frame.size.width, y: 0, width: 1, height: 1) // Set new rect here
}
I've tried just to set new rect (rect.initialize(...)) and it works.
func popoverPresentationController(popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverToRect rect: UnsafeMutablePointer<CGRect>, inView view: AutoreleasingUnsafeMutablePointer<UIView?>) {
if popoverPresentationController.presentedViewController.view.tag == Globals.PopoverTempTag
{
rect.initialize(getForPopupSourceRect())
}
}
I have similar problem which I resolve by this
[myPop presentPopoverFromRect:myfield.frame inView:myscrollview permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
Where myfield is frame from which you want to show your popover and myscrollview is container view in which you add your popover as subview(in my case its my scrollview, instead of putting inView:self.view I use inView:myscrollview).
Initialize PopOver Controller
var popoverContent: PopoverContentViewController?
Write Defination for PopOver Controller
popoverContent = self.storyboard?.instantiateViewController(withIdentifier: "PopoverContentViewController") as? PopoverContentViewController
popoverContent?.modalPresentationStyle = .popover
let popover = popoverContent?.popoverPresentationController!
popover?.delegate = self
popoverContent?.preQuestionInfoPopUpViewDelegateObject = self
popover?.permittedArrowDirections = UIPopoverArrowDirection()
popover?.sourceView = self.view
popover?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 330, height: 330)
Present PopOver Controller
self.present(popoverContent, animated: true, completion:nil)
Write below method and assign new size to popover:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let popover = popoverContent?.popoverPresentationController!
popover?.sourceRect = CGRect(x: size.width/2, y: size.height/2, width: 0, height: 0)
}
I had a same problem. Instead of performing -presentPopoverFromRect each time by keeping track of the source rectangle / view from which it is presented, I subclassed UIPopoverController. After doing it, all you have to do is set either the UIBarButtonItem / UIView from where the popover has to be displayed. You can even opt for displaying the popover from custom frame which can be passed in as a NSString value.
CSPopoverController.h:
#import <UIKit/UIKit.h>
// The original popover controller would not re-orientate itself when the orientation change occurs. To tackle that issue, this subclass is created
#interface CSPopoverController : UIPopoverController
#property (nonatomic, strong) NSString *popoverDisplaySourceFrame; // Mutually Exclusive. If you want to set custom rect as source, make sure that popOverDisplaySource is nil
#property (nonatomic, strong) id popoverDisplaySource; // Mutually exclusive. If UIBarButtonItem is set to it, popoverDisplaySourceFrame is neglected.
#property (nonatomic, strong) UIView *popoverDisplayView;
#property (nonatomic, assign, getter = shouldAutomaticallyReorientate) BOOL automaticallyReorientate;
-(void)reorientatePopover;
#end
CSPopoverController.m:
#import "CSPopoverController.h"
#implementation CSPopoverController
#synthesize popoverDisplaySourceFrame = popoverDisplaySourceFrame_;
-(NSString*)popoverDisplaySourceFrame
{
if (nil==popoverDisplaySourceFrame_)
{
if (nil!=self.popoverDisplaySource)
{
if ([self.popoverDisplaySource isKindOfClass:[UIView class]])
{
UIView *viewSource = (UIView*)self.popoverDisplaySource;
[self setPopoverDisplaySourceFrame:NSStringFromCGRect(viewSource.frame)];
}
}
}
return popoverDisplaySourceFrame_;
}
-(void)setPopoverDisplaySourceFrame:(NSString *)inPopoverDisplaySourceFrame
{
if (inPopoverDisplaySourceFrame!=popoverDisplaySourceFrame_)
{
popoverDisplaySourceFrame_ = inPopoverDisplaySourceFrame;
[self reorientatePopover];
}
}
#synthesize popoverDisplaySource = popoverDisplaySource_;
-(void)setPopoverDisplaySource:(id)inPopoverDisplaySource
{
if (inPopoverDisplaySource!=popoverDisplaySource_)
{
[self unlistenForFrameChangeInView:popoverDisplaySource_];
popoverDisplaySource_ = inPopoverDisplaySource;
[self reorientatePopover];
if ([popoverDisplaySource_ isKindOfClass:[UIView class]])
{
UIView *viewSource = (UIView*)popoverDisplaySource_;
[self setPopoverDisplaySourceFrame:NSStringFromCGRect(viewSource.frame)];
}
if (self.shouldAutomaticallyReorientate)
{
[self listenForFrameChangeInView:popoverDisplaySource_];
}
}
}
#synthesize popoverDisplayView = popoverDisplayView_;
-(void)setPopoverDisplayView:(UIView *)inPopoverDisplayView
{
if (inPopoverDisplayView!=popoverDisplayView_)
{
popoverDisplayView_ = inPopoverDisplayView;
[self reorientatePopover];
}
}
#synthesize automaticallyReorientate = automaticallyReorientate_;
-(void)setAutomaticallyReorientate:(BOOL)inAutomaticallyReorientate
{
if (inAutomaticallyReorientate!=automaticallyReorientate_)
{
automaticallyReorientate_ = inAutomaticallyReorientate;
if (automaticallyReorientate_)
{
[self listenForAutorotation];
[self listenForFrameChangeInView:self.popoverDisplaySource];
}
else
{
[self unlistenForAutorotation];
[self unlistenForFrameChangeInView:self.popoverDisplaySource];
}
}
}
-(void)listenForAutorotation
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
-(void)unlistenForAutorotation
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
-(void)listenForFrameChangeInView:(id)inView
{
// Let's listen for changes in the view's frame and adjust the popover even if the frame is updated
if ([inView isKindOfClass:[UIView class]])
{
UIView *viewToObserve = (UIView*)inView;
[viewToObserve addObserver:self
forKeyPath:#"frame"
options:NSKeyValueObservingOptionNew
context:nil];
}
}
-(void)unlistenForFrameChangeInView:(id)inView
{
if ([inView isKindOfClass:[UIView class]])
{
UIView *viewToObserve = (UIView*)inView;
[viewToObserve removeObserver:self
forKeyPath:#"frame"];
}
}
// TODO: Dealloc is not called, check why? !!!
- (void)dealloc
{
[self unlistenForFrameChangeInView:self.popoverDisplaySource];
[self unlistenForAutorotation];
DEBUGLog(#"dealloc called for CSPopoverController %#", self);
}
#pragma mark - Designated initializers
-(id)initWithContentViewController:(UIViewController *)viewController
{
self = [super initWithContentViewController:viewController];
if (self)
{
[self popoverCommonInitializations];
}
return self;
}
-(void)popoverCommonInitializations
{
[self setAutomaticallyReorientate:YES];
}
#pragma mark - Frame
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object==self.popoverDisplaySource)
{
[self setPopoverDisplaySourceFrame:nil];
[self reorientatePopover];
}
}
#pragma mark - Orientation
-(void)orientationChanged:(NSNotification *)inNotification
{
[self reorientatePopover];
}
-(void)reorientatePopover
{
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:#selector(performReorientatePopover)
object:nil];
// if ([self isPopoverVisible])
{
[self performSelector:#selector(performReorientatePopover)
withObject:nil
afterDelay:0.0];
}
}
-(void)performReorientatePopover
{
if (self.popoverDisplaySourceFrame && self.popoverDisplayView)
{
[self presentPopoverFromRect:CGRectFromString(self.popoverDisplaySourceFrame)
inView:self.popoverDisplayView
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
else if (self.popoverDisplaySource && [self.popoverDisplaySource isKindOfClass:[UIBarButtonItem class]])
{
UIBarButtonItem *barButton = (UIBarButtonItem*)self.popoverDisplaySource;
[self presentPopoverFromBarButtonItem:barButton
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
}
}
#end
Usage:
If it is a UIBarButtonItem from where you are presenting it:
CSPopoverController *popOverCont = [[CSPopoverController alloc]initWithContentViewController:navCont];
self.popOver = popOverCont;
[popOverCont setPopoverDisplaySource:self.settingsButtonItem];
If it is a UIView from where you are presenting the popover:
CSPopoverController *popOver = [[CSPopoverController alloc] initWithContentViewController:navigation];
self.iPadPopoverController = popOver;
[newDateVC setIPadPopoverController:self.iPadPopoverController];
[popOver setPopoverDisplaySource:inButton];
[popOver setPopoverDisplayView:inView];
For iOS > 8 John Strickers answer helped but didn't do what I wanted it to do.
Here's the solution that worked for me. (If you want to download a full sample project it's here: https://github.com/appteur/uipopoverExample)
I created a property to hold any popover I wanted to present and also added a property to track the sourceRect and another for the view of the button I wanted the popover arrow to point at.
#property (nonatomic, weak) UIView *activePopoverBtn;
#property (nonatomic, strong) PopoverViewController *popoverVC;
#property (nonatomic, assign) CGRect sourceRect;
The button that triggered my popover is in a UIToolbar. When tapped it runs the following method that creates and launches the popover.
-(void) buttonAction:(id)sender event:(UIEvent*)event
{
NSLog(#"ButtonAction");
// when the button is tapped we want to display a popover, so setup all the variables needed and present it here
// get a reference to which button's view was tapped (this is to get
// the frame to update the arrow to later on rotation)
// since UIBarButtonItems don't have a 'frame' property I found this way is easy
UIView *buttonView = [[event.allTouches anyObject] view];
// set our tracker properties for when the orientation changes (handled in the viewWillTransitionToSize method above)
self.activePopoverBtn = buttonView;
self.sourceRect = buttonView.frame;
// get our size, make it adapt based on our view bounds
CGSize viewSize = self.view.bounds.size;
CGSize contentSize = CGSizeMake(viewSize.width, viewSize.height - 100.0);
// set our popover view controller property
self.popoverVC = [[UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:#"PopoverVC"];
// configure using a convenience method (if you have multiple popovers this makes it faster with less code)
[self setupPopover:self.popoverVC
withSourceView:buttonView.superview // this will be the toolbar
sourceRect:self.sourceRect
contentSize:contentSize];
[self presentViewController:self.popoverVC animated:YES completion:nil];
}
The 'setupPopover:withSourceView:sourceRect:contentSize method is simply a convenience method to set the popoverPresentationController properties if you plan to display multiple popovers and want them configured the same. It's implementation is below.
// convenience method in case you want to display multiple popovers
-(void) setupPopover:(UIViewController*)popover withSourceView:(UIView*)sourceView sourceRect:(CGRect)sourceRect contentSize:(CGSize)contentSize
{
NSLog(#"\npopoverPresentationController: %#\n", popover.popoverPresentationController);
popover.modalPresentationStyle = UIModalPresentationPopover;
popover.popoverPresentationController.delegate = self;
popover.popoverPresentationController.sourceView = sourceView;
popover.popoverPresentationController.sourceRect = sourceRect;
popover.preferredContentSize = contentSize;
popover.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown;
popover.popoverPresentationController.backgroundColor = [UIColor whiteColor];
}
For iOS 8 and up the viewWillTransitionToSize:withTransitionCoordinator get's called on the view controller when the device rotates.
I implemented this method in my presenting view controller class as shown below.
// called when rotating a device
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
NSLog(#"viewWillTransitionToSize [%#]", NSStringFromCGSize(size));
// resizes popover to new size and arrow location on orientation change
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context)
{
if (self.popoverVC)
{
// get the new frame of our button (this is our new source rect)
CGRect viewframe = self.activePopoverBtn ? self.activePopoverBtn.frame : CGRectZero;
// update our popover view controller's sourceRect so the arrow will be pointed in the right place
self.popoverVC.popoverPresentationController.sourceRect = viewframe;
// update the preferred content size if we want to adapt the size of the popover to fit the new bounds
self.popoverVC.preferredContentSize = CGSizeMake(self.view.bounds.size.width -20, self.view.bounds.size.height - 100);
}
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// anything you want to do when the transition completes
}];
}
Swift 3:
class MyClass: UIViewController, UIPopoverPresentationControllerDelegate {
...
var popover:UIPopoverPresentationController?
...
// Where you want to set the popover...
popover = YourViewController?.popoverPresentationController
popover?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
popover?.delegate = self
...
// override didRotate...
override func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
popover?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
}
}
I have popoverPresentationController that I present on a view that has a "fake" nav bar. So I can't attach the popoverPresentationController to a barButtonItem. My popup appears in the right place but does not when the screen rotates.
So for some reason popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) does not get called for me.
To work around this (iOS 12, Swift 4.2) I added constraints to the popup in the completion closure when calling present. Now my popup stays where I would expect it too.
present(viewController, animated: true) { [weak self] in
DDLogDebug(String(describing: viewController.view.frame))
if let containerView = viewController.popoverPresentationController?.containerView,
let presentedView = viewController.popoverPresentationController?.presentedView,
let imageView = self?.headerView.settingsButton {
withExtendedLifetime(self) {
let deltaY:CGFloat = presentedView.frame.origin.y - imageView.frame.maxY
let topConstraint = NSLayoutConstraint.init(item: presentedView, attribute: .top, relatedBy: .equal, toItem: imageView.imageView, attribute: .bottom, multiplier: 1, constant: deltaY)
topConstraint?.priority = UILayoutPriority(rawValue: 999)
topConstraint?.isActive = true
let heightContraint = NSLayoutConstraint.init(item: presentedView, attribute: .height, relatedBy: .equal, toItem: containerView, attribute: .height, multiplier: 0.75, constant: -deltaY)
heightContraint?.isActive = true
let leftConstraint = NSLayoutConstraint.init(item: presentedView, attribute: .left, relatedBy: .equal, toItem: containerView, attribute: .left, multiplier: 1, constant: presentedView.frame.origin.x)
leftConstraint.isActive = true
let widthConstraint = NSLayoutConstraint.init(item: presentedView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: presentedView.frame.width)
widthConstraint.isActive = true
presentedView.translatesAutoresizingMaskIntoConstraints = false
}
}
}
I have a strange issue on ios 4.3.I have one of my screen in landscape mode, a button click presents a popover.My popover has a search bar.Whenever keyboard appears it automatically pushes my popover bit up.When I resign the keyboard , popover reduces in height.This is the issue only on ios 4.3.While in rest of the ios , my popover doesnot reduces in height after keyboard dismissal.
None of the answers above worked for me. Apparently the keyboard scales the view and restores this scaling after the UIKeyboardDidHideNotification notification, making the presentPopoverFromRect method useless when applied handling this notification.
The way I solved it was by delaying the latter call as follows:
- (void)viewDidLoad
{
[super viewDidLoad];
popup = nil; //my ViewController with UITextField
popover = nil; //my UIPopoverController
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(resizePopup:)
name:UIKeyboardDidHideNotification
object:nil];
}
- (void)doDelayedResize
{
[popover presentPopoverFromRect:myButton.bounds inView:myButton permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
- (void)resizePopup:(NSNotification*)note
{
[self performSelector:#selector(doDelayedResize) withObject:nil afterDelay:0.01];
}
I answered a very similar question here: UIPopoverController's view controller gets resized after keyboard disappears
The way I got around it was to observe the keyboard disappearing in the controller which controls the UIPopoverController:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(presentSearchPopover) name:UIKeyboardDidHideNotification object:nil];
And then in -presentSearchPopover, present the UIPopoverController again (it's quite a seamless transition):
- (void)presentSearchPopover
{
self.searchPopoverController.popoverContentSize = CGSizeMake(width, height));
[self.searchPopoverController presentPopoverFromRect:someRect) inView:self.view permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
}
Don't forget to remove the observer in -dealloc or similar too:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil];
[super dealloc];
}
I found an answer for this.It was a bug with the top arrow of popover.If I use the left arrow direction for popover, everything works fine.
I ran into this issue as well - specifically, the popover wasn't growing back to its pre-keyboard size after tapping away from the popover. (The popover would grow back if the user dismissed the keyboard directly or the popover's view controller resigned first responder).
Unfortunately, I have to use the top arrow direction for the popover due to the UI's layout. To solve this, the view controller responsible for the popover implements - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController. For example:
#interface MyController : UIViewController <UIPopoverControllerDelegate>
{
// ...
}
//...
#end
Set that controller as the popover's delegate:
MyPopoverViewController *popoverVC = [[MyPopoverViewController alloc] init];
UIPopoverController *myPopover = [[UIPopoverController alloc] initWithContentViewController:popoverVC];
myPopover.delegate = self;
// Hang on to popoverVC, myPopover or release them as desired...
In addition, my popover's view controller sets its contentSizeForViewInPopover property to the desired size:
#implementation MyPopoverViewController
- (id)init
{
self = [super init];
if (self)
{
// ...
self.contentSizeForViewInPopover = CGSizeMake(320, 400); // desired size
}
return self;
}
When the keyboard causes the popover to shrink, it affects the popover's popoverContentSize and not its view controller's contentSizeForViewInPopover. Therefore, reset popoverContentSize in MyController's delegate method:
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
// Check if popoverController is valid, the popover you want, etc
popoverController.popoverContentSize = popoverController.contentViewController.contentSizeForViewInPopover;
}
Here is my solution:
1. Register for keyboard Notifications (UIKeyboardWillShowNotification, UIKeyboardWillHideNotification)
2. Create local variables:
CGSize _currentPopoverContentSize; //if you want to have custom size for popover
UIView *_currentPopoverSender; //to remember from wich view you will present popover
BOOL _keyboardIsShown; //enable in keyboardWillShow, and disable in keyboardWillHide
3. In my presentPopover method:
- (void)presentPopoverControllerWithSize:(CGSize)size fromView:(UIView *)sender{
MyController *controller = [[[MyController alloc] init] autorelease];
if (self.popover)
{
[_popover release];
_popover = nil;
}
_popover = [[UIPopoverController alloc] initWithContentViewController:controller];
_popover.popoverContentSize = size;
_popover.delegate = self;
//checking if keyboard is shown - if NO, than present popover, if YES - just `resignFirstResponder` for your _`activeTextField`(you can set it in -textFieldDidBeginEditing: and nullify in -textFieldDidEndEditing:)
if (!_keyboardIsShown)
{
[_popover presentPopoverFromRect:[sender bounds]
inView:sender
permittedArrowDirections:UIPopoverArrowDirectionUp
animated:YES];
}
else
{
[_activeTextField resignFirstResponder];
}
_currentPopoverContentSize = size;
_currentPopoverSender = sender;
}
4. Than:
- (void)keyboardWillBeHidden:(NSNotification*)aNotification{
[UIView animateWithDuration:0.3
animations:^{
//do some stuff
[self.scrollView setContentSize:_scrollViewContentSize];
} completion:^(BOOL finished) {
if (_popover && _currentPopoverSender)
{
[_popover presentPopoverFromRect:[_currentPopoverSender bounds]
inView:_currentPopoverSender
permittedArrowDirections:UIPopoverArrowDirectionUp
animated:YES];
}
}];
_keyboardIsShown = NO;
}
Hi After going through the forum, I don't think it's a bug after playing with frame sizes a lot, working on IOS 4,5,6,7 it's the same behaviour.
The solution for me was to:
1) Go into the designer by
2) Opening the XIB ViewController that is causing the problem (i.e. the PopOver one).
3) Click to select it's VIEW.
4) Uncheck "AutoResizeSubviews"
5) When loading the PopOver in code, make sure you do:
6) Your_Popup_Window.popoverContentSize = Your_ViewController.view.bounds.size;
I hope this helps.
Kind Regards
Heider Sati