18

I've noticed in a project that the positioning of a popover over a right bar button item seems to be off to the right for some reason. I've tried instead using a UIButton for the custom view, then presenting the popover from that button, but popover seems to ignore the showFromRect if I actually provide it with the 'centered' value.

Off-center popover

The code behind this is quite simple:

- (void)viewDidLoad {
    [super viewDidLoad];

     UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"share"] style:UIBarButtonItemStylePlain target:self action:@selector(shareTap:)];

     self.navigationItem.rightBarButtonItem = button;
}

- (void)shareTap:(UIBarButtonItem *)button {
    self.popover = [[UIPopoverController alloc] initWithContentViewController:[[UIViewController alloc] init]];

    [self.popover presentPopoverFromBarButtonItem:button permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
}

Now, if I switch to using the internal button, as mentioned, I see similar behavior (note color change so image shows up).

using internal button

The code for this is still fairly simple:

- (void)viewDidLoad {
    [super viewDidLoad];

    UIButton *innerButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 22, 22)];
    [innerButton setImage:[UIImage imageNamed:@"share"] forState:UIControlStateNormal];
    [innerButton addTarget:self action:@selector(shareTap:) forControlEvents:UIControlEventTouchUpInside];

    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:innerButton];;
}

- (void)shareTap:(UIButton *)button {
    self.popover = [[UIPopoverController alloc] initWithContentViewController:[[UIViewController alloc] init]];

//    CGRect bottomCenter = CGRectMake(button.frame.size.width / 2, button.frame.size.height, 1, 1);
    CGRect bottomCenter = CGRectMake(2, button.frame.size.height, 1, 1);

    [self.popover presentPopoverFromRect:bottomCenter inView:button permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
}

Note here that instead of using the actual center, I've used the arbitrary value, 2. If I use 1 or 0, the popover becomes left-aligned. Anything greater than one is right aligned again.

left-aligned

I searched around a bit looking for a similar question, but I can't seem to find anyone with the same problem. Any idea around what causes this or how to avoid it would be greatly appreciated. The only thing I've been able to guess at is that due to its proximity to the edge, Apple does some positioning voodoo to force it to be where they want it. The problem is that top-right corner button seems like a pretty standard drop down position.

Edit: I've confirmed this same behavior occurs with a long-press on the "New Message" icon in the native Mail app.

Edit 2: I was able to confirm with Apple that this is an issue. In one of the more recent versions (I can't remember which, one of the 9s I think), they made it so you can manually set this. The default behavior is still wrong I believe (I haven't tried this for a while), but you can use the CGRect offset method to make it work correctly, should you be so inclined.

Moin Shirazi
  • 4,372
  • 2
  • 26
  • 38
shortstuffsushi
  • 2,271
  • 19
  • 33
  • Try this UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(shareTap:)]; – Nishant Jun 08 '15 at 11:52
  • 1
    Could you perhaps explain a bit more? Using a system item doesn't fix the button placement by itself, and in my edit I mentioned that the default apps exhibit the issue as well (Mail) which are presumably using system icons. – shortstuffsushi Jun 08 '15 at 14:42

2 Answers2

1

Here's one way to do this: Instead of using a UIBarButtonSystemItem, use a UIBarButtonItem with a custom view. Just drag a UIButton into the UINavigationBar to get a UIBarButtonItem with an embedded UIButton, which shows up as the UIBarButtonItem's customView.

@IBAction func didTapActionButton(_ sender: Any) {
    if let vc = self.storyboard?.instantiateViewController(withIdentifier: "myPopover") {
        vc.modalPresentationStyle = .popover
        guard let innerView = actionButton.customView else {
            // Unexpected missing custom view
            return
        }

        vc.popoverPresentationController?.sourceView = navigationController?.navigationBar
        vc.popoverPresentationController?.sourceRect = innerView.frame
        self.present(vc, animated: true, completion: nil)
    }
}
stevex
  • 5,589
  • 37
  • 52
  • I think you might be noticing that this is fixed post iOS 9. I mentioned in the beginning of my post that even a UIButton didn't work, but later (in an edit) that Apple updated it so that you *could* fix the positioning using a UIButton. – shortstuffsushi Jul 10 '17 at 04:24
  • I had the popover arrow offset problem you described originally in a project on iOS 10, and this was the fix for me - I guess what you're saying is that this method wouldn't work on iOS 9 where the bug was present. I didn't try that. – stevex Jul 10 '17 at 11:00
0

You must specify the frame of the bar button from which the pop over needs to be presented.

The following code presents a UIAlertController as a popover from a bar button.

    @IBOutlet weak var playerRightBarButton: UIBarButtonItem!

    extension UIBarButtonItem {
       var frame: CGRect? {
        guard let view = self.value(forKey: "view") as? UIView else {
          return nil
      }
      return view.frame
    }
  }

    let alert = UIAlertController(title: "", message: "Sample PopOver", 
    preferredStyle: .actionSheet)
    if alert.responds(to: #selector(getter: popoverPresentationController)) {
            alert.popoverPresentationController?.sourceView = self.view
            alert.popoverPresentationController?.barButtonItem = playerRightBarButton
            alert.popoverPresentationController?.permittedArrowDirections = .up
            if let frame = playerRightBarButton.frame {
                alert.popoverPresentationController?.sourceRect = frame
            }
        }
        self.present(alert, animated: true, completion: nil)
Arets Paeglis
  • 3,856
  • 4
  • 35
  • 44
Swathi
  • 115
  • 2
  • I'm not entirely sure, but I don't think this is related to my question. How does this address the arrow placement? What "frame" are you referring to setting? Are you talking about adding that extension method on the UIBarButtonItem class? – shortstuffsushi Oct 16 '17 at 21:04
  • The extension gives you the frame of the barButton. When we set the frame from which the popover needs to be presented, it centers itself accordingly. I have provided an example of an alertController presenting as popOver in the iPad. – Swathi Oct 17 '17 at 04:28