Background
On iPhone Duo, when the device is unfolded and then partially folded, system alerts (UIAlertController) are automatically repositioned so they stay clear of the hinge. Apple's Tech Talk confirms this:
"The system automatically repositions components around the reserved region."
Our app uses a custom popup: a view controller presented with .overFullScreen that lays out its own dimming view and a custom-drawn view (here, a coupon ticket with notched edges) in the center of the screen. This popup is not repositioned. It stays centered on screen, right on top of the fold.
The recommended approach for custom UI is to query view.reservedRegions(kind: .division) and lay out around the fold manually. Before going that route, I tried something simpler.
What I tried
The Tech Talk lists popovers among the components the system repositions. So I presented the same custom view as a popover with no arrow, anchored to the center of the presenting view:
final class CouponPopoverViewController: UIViewController, UIPopoverPresentationControllerDelegate {
private let ticketView = CouponTicketView() // the custom popup view
init(centeredIn sourceView: UIView) {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .popover
guard let popover = popoverPresentationController else { return }
popover.sourceView = sourceView
popover.sourceRect = Self.centerPoint(of: sourceView.bounds)
popover.permittedArrowDirections = []
// Drop the system popover chrome so only the custom view (and its own shadow) is visible.
popover.popoverBackgroundViewClass = ClearPopoverBackgroundView.self
popover.delegate = self
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
ticketView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(ticketView)
NSLayoutConstraint.activate([
ticketView.topAnchor.constraint(equalTo: view.topAnchor),
ticketView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
ticketView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
ticketView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
preferredContentSize = ticketView.systemLayoutSizeFitting(
CGSize(width: 300, height: UIView.layoutFittingCompressedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
}
private static func centerPoint(of bounds: CGRect) -> CGRect {
CGRect(x: bounds.midX, y: bounds.midY, width: 0, height: 0)
}
// Keep it a popover on iPhone instead of adapting to a sheet.
func adaptivePresentationStyle(for controller: UIPresentationController,
traitCollection: UITraitCollection) -> UIModalPresentationStyle {
.none
}
// Re-anchor to the center after rotation or fold changes.
func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController,
willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>,
in view: AutoreleasingUnsafeMutablePointer<UIView>) {
rect.pointee = Self.centerPoint(of: view.pointee.bounds)
}
}
/// A popover background that draws nothing: no arrow, no insets, no default appearance.
final class ClearPopoverBackgroundView: UIPopoverBackgroundView {
override class func arrowBase() -> CGFloat { 0 }
override class func arrowHeight() -> CGFloat { 0 }
override class func contentViewInsets() -> UIEdgeInsets { .zero }
override class var wantsDefaultContentAppearance: Bool { false }
override var arrowOffset: CGFloat { get { 0 } set {} }
override var arrowDirection: UIPopoverArrowDirection { get { .any } set {} }
}
Result
Partially folded, portrait (hinge runs horizontally across the middle of the screen):
Before: the custom popup (.overFullScreen) stays centered, right across the hinge.
After: the same view presented as an arrowless popover moves into the top half, clear of the hinge.
Partially folded, book pose (vertical hinge): the popover moves to the leading page, clear of the hinge. This matches where a system UIAlertController goes in the same pose.
I also checked that the popover shows correctly on the outer display (closed) and on a non-Duo iPhone (iPhone 18 Pro, iOS 27.0).
Tested on:
iPhone Duo simulator (iOS 27.1), Xcode 27.1 RC (27A9275)
iPhone 18 Pro simulator (iOS 27.0)
Things to handle when switching to a popover
Tap-outside dismissal: Popovers dismiss on outside taps by default. Set isModalInPresentation = true or return false from presentationControllerShouldDismiss(_:) if the popup should only close via its buttons.
Dimming: Popovers don't dim the background like a typical custom popup (compare the before and after screenshots). Add your own dimming view if needed.
Chrome: By default, the system draws its own background behind the content. For a custom-shaped view, this shows through any transparent areas and clips the view's shadow. A UIPopoverBackgroundView subclass that draws nothing (above) removes it.
Sizing: Set preferredContentSize explicitly. Recompute it for dynamic content and Dynamic Type.
Questions
This works well in my testing, but I couldn't find any documentation saying that an arrowless popover anchored to the center gets the same hinge avoidance as alerts.
Is this supported behavior that we can rely on going forward?
Or is reservedRegions(kind:options:) with manual layout the only recommended approach for custom popups?
It would also be great to have a public API that lets custom presentations opt into the same hinge-avoidance placement that system alerts use.
Hope this helps anyone else dealing with custom popups on iPhone Duo.
2
0
59