Как заставить UIPopoverController сохранять то же положение после поворота?

Я не могу сохранить всплывающее окно в той же позиции на экране после поворота. Есть ли хороший способ сделать это, потому что просто установка какого-либо кадра в всплывающее окно ужасно работает после поворота.popover.frame = CGRectMake(someFrame); После поворота всплывающее окно выглядит нормально, только если оно находится в центре экрана.


person B.S.    schedule 30.01.2012    source источник
comment
Просто проверьте также эту ссылку. 3670981/   -  person dead_soldier    schedule 30.01.2012
comment
Спасибо за: PresentPopoverFromRect:inView можно использовать, когда всплывающее окно видно   -  person B.S.    schedule 30.01.2012


Ответы (13)


У Apple есть вопросы и ответы именно по этому вопросу. Вы можете найти подробности здесь:

Технические вопросы и ответы QA1694 Обработка всплывающих окон при изменении ориентации

По сути, этот метод объясняет, что в методе didRotateFromInterfaceOrientation вашего контроллера представления вы снова представите всплывающее окно следующим образом:

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    [aPopover presentPopoverFromRect:targetRect.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

Для получения дополнительной информации прочитайте статью выше, а также ссылку Ссылка на класс UIPopoverController:

Если пользователь поворачивает устройство, пока отображается всплывающее окно, контроллер всплывающего окна скрывает всплывающее окно, а затем снова показывает его в конце поворота. Контроллер всплывающего окна пытается расположить всплывающее окно соответствующим образом для вас, но вам, возможно, придется представить его снова или полностью скрыть в некоторых случаях. Например, при отображении из элемента кнопки панели контроллер всплывающего окна автоматически регулирует положение (и, возможно, размер) всплывающего окна для учета изменений положения элемента кнопки панели. Однако, если вы удалите элемент кнопки панели во время поворота или если вы представили всплывающее окно из целевого прямоугольника в представлении, контроллер всплывающего окна не пытается изменить положение всплывающего окна. В этих случаях вы должны вручную скрыть всплывающее окно или представить его снова с соответствующей новой позиции. Вы можете сделать это в методе didRotateFromInterfaceOrientation: контроллера представления, который вы использовали для представления всплывающего окна.

person Max MacLeod    schedule 17.04.2013
comment
UIPopoverController устарел. Вы можете использовать UIPopoverPresentationController developer.apple.com/documentation/uikit/. - person fred; 26.04.2018

Начиная с iOS 8.0.2 willRotateToInterfaceOrientation не будет иметь никакого эффекта. Как уже упоминалось mhrrt, вам нужно использовать метод делегата:

- (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView *__autoreleasing *)view

Так, например, если вы хотите, чтобы всплывающее окно отображалось непосредственно под нажатой кнопкой, вы должны использовать следующий код:

- (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;
}
person John Stricker    schedule 09.10.2014

В iOS 7 вы можете использовать - (void)popoverController:(UIPopoverController *)popoverController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView *__autoreleasing *)view для изменения положения представления вашего UIPopoverController при изменении ориентации интерфейса.

См. UIPopoverControllerDelegate документация.

person Markus Rautopuro    schedule 03.02.2014

Вы можете сделать это в методе didRotateFromInterfaceOrientation: контроллера представления, который вы использовали для представления всплывающего окна.

Используйте метод setPopoverContentSize:animated: для установки размера поповера.

person dead_soldier    schedule 30.01.2012
comment
Изменяет ли этот метод происхождение всплывающего окна? Мне не нужно менять размер содержимого всплывающего окна, просто чтобы сохранить происхождение. - person B.S.; 30.01.2012

UIPopoverController устарел в ios9 в пользу UIPopoverPresentationController, представленного в ios8. (Я прошел через этот переход также при переходе от UIActionSheet к UIAlertController.) У вас есть два варианта (пример в obj-C):

A. Реализуйте метод UIViewController ниже (UIKit вызывает этот метод перед изменением размера представленного представления контроллера представления).

- (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. В качестве альтернативы, при настройке вашего UIPopoverPresentationController для представления также установите его делегата. например ваш представляющий vc может реализовать UIPopoverPresentationControllerDelegate и назначить себя делегатом. Затем реализуйте метод делегата:

- (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;
}
person qix    schedule 10.01.2017
comment
Спасибо тебе за это! Вариант B работал безупречно для меня после многих попыток. - person Zach; 04.02.2020

Для Свифта:

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
}
person Zaraki    schedule 25.07.2017

Я попытался просто установить новый прямоугольник (rect.initialize(...)) и он работает.

func popoverPresentationController(popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverToRect rect: UnsafeMutablePointer<CGRect>, inView view: AutoreleasingUnsafeMutablePointer<UIView?>) {

        if popoverPresentationController.presentedViewController.view.tag == Globals.PopoverTempTag
        {
            rect.initialize(getForPopupSourceRect())
        }
    }
person sabiland    schedule 09.02.2016

У меня есть аналогичная проблема, которую я решаю этим

[myPop presentPopoverFromRect:myfield.frame inView:myscrollview permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];

Где myfield — это фрейм, из которого вы хотите показать свое всплывающее окно, а myscrollview — это представление контейнера, в котором вы добавляете свое всплывающее окно в качестве подвида (в моем случае это мой вид прокрутки, вместо того, чтобы ставить inView:self.view, я использую inView:myscrollview).

person mhrrt    schedule 19.05.2012

  1. Инициализировать контроллер PopOver

    var popoverContent: PopoverContentViewController?
    
  2. Напишите определение для контроллера PopOver

    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)
    
  3. Настоящий контроллер PopOver

    self.present (popoverContent, анимированный: правда, завершение: ноль)

  4. Напишите ниже метод и назначьте новый размер для всплывающего окна:

    переопределить func viewWillTransition (для размера: CGSize, с координатором: UIViewControllerTransitionCoordinator) { let popover = popoverContent?.popoverPresentationController! popover?.sourceRect = CGRect(x: size.width/2, y: size.height/2, ширина: 0, высота: 0) }

person sagar.nikam    schedule 28.05.2019

У меня была такая же проблема. Вместо того, чтобы каждый раз выполнять -presentPopoverFromRect, отслеживая исходный прямоугольник/представление, из которого он представлен, я создал подкласс UIPopoverController. После этого все, что вам нужно сделать, это установить либо UIBarButtonItem, либо UIView, откуда должно отображаться всплывающее окно. Вы даже можете выбрать отображение всплывающего окна из пользовательского фрейма, которое можно передать как значение NSString.

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

Использование:

Если это UIBarButtonItem, откуда вы его представляете:

CSPopoverController *popOverCont = [[CSPopoverController alloc]initWithContentViewController:navCont];
self.popOver = popOverCont;
[popOverCont setPopoverDisplaySource:self.settingsButtonItem];

Если это UIView, из которого вы представляете всплывающее окно:

CSPopoverController *popOver = [[CSPopoverController alloc] initWithContentViewController:navigation];
self.iPadPopoverController = popOver;
[newDateVC setIPadPopoverController:self.iPadPopoverController];
[popOver setPopoverDisplaySource:inButton];
[popOver setPopoverDisplayView:inView];
person Raj Pawan Gumdal    schedule 21.10.2013

Для iOS> 8 ответ Джона Стрикера помог, но не сделал то, что я хотел.

Вот решение, которое сработало для меня. (Если вы хотите загрузить полный образец проекта, он находится здесь: https://github.com/appteur/uipopoverExample)

Я создал свойство для хранения любого всплывающего окна, которое я хотел представить, а также добавил свойство для отслеживания sourceRect и еще одно для представления кнопки, на которую я хотел, чтобы стрелка всплывающего окна указывала.

@property (nonatomic, weak) UIView *activePopoverBtn;
@property (nonatomic, strong) PopoverViewController *popoverVC;
@property (nonatomic, assign) CGRect sourceRect; 

Кнопка, которая вызвала мое всплывающее окно, находится на панели UIToolbar. При нажатии он запускает следующий метод, который создает и запускает всплывающее окно.

-(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];

}

Метод setupPopover:withSourceView:sourceRect:contentSize — это просто удобный метод для установки свойств popoverPresentationController, если вы планируете отображать несколько всплывающих окон и хотите, чтобы они были настроены одинаково. Его реализация ниже.

// 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];
}

Для iOS 8 и выше функция viewWillTransitionToSize:withTransitionCoordinator вызывается на контроллере представления при повороте устройства.

Я реализовал этот метод в своем представленном классе контроллера представления, как показано ниже.

// 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
    }];
}
person digitalHound    schedule 20.11.2015

Свифт 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)
        }

}
person Arnaldo    schedule 21.05.2017
comment
didRotate устарел. Вы должны использовать один из двух подходов, которые я упомянул в stackoverflow.com/a/41561021/954643, например. popoverPresentationController(_:willRepositionPopoverTo:in:) (документы) Это отчасти потому, что вы Теперь можно изменить макет экрана не только путем поворота, например, с помощью функций многозадачности с разделенным экраном в ios9+. - person qix; 07.06.2017

У меня есть popoverPresentationController, который я представляю в представлении с «поддельной» панелью навигации. Поэтому я не могу прикрепить popoverPresentationController к элементу barButtonItem. Мое всплывающее окно появляется в нужном месте, но не при повороте экрана.

Так что по какой-то причине popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) мне не звонят.

Чтобы обойти это (iOS 12, Swift 4.2), я добавил ограничения во всплывающее окно в закрытии завершения при вызове present. Теперь мое всплывающее окно остается там, где я и ожидал.

                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
                }
            }
        }
person Rand    schedule 11.10.2018