I have an app (supported interface orientation - portrait only) with the next hierarchy of modally presented view controllers:
A -> B -> AVP
Where A is a view controller sitting in tab bar controller, tab bar controller is in turn root of window.
B is a rather simple view controller with button, image and labels but presented as a popup:
// ... presentation method in A
let B = // create B
B.modalPresentationStyle = .popover
B.preferredContentSize = CGSize(width: 300, height: 400)
B.isModalInPopover = true
if let BPopover = B.popoverPresentationController {
BPopover.delegate = self
BPopover.permittedArrowDirections = []
let window = // grab current window
BPopover.sourceView = window
BPopover.sourceRect = window.bounds
BPopover.passthroughViews = nil
}
self.tabBarController?.present(B, animated: true, completion: nil)
AVP is a AVPlayerViewController presented from B:
// This method is in B.
@IBAction func playVideoButtonPressed(_ sender: Any) {
if let videoURL = self.videoURL {
let videoPlayer = AVPlayer(url: videoURL)
let videoVC = AVPlayerViewController()
videoVC.player = videoPlayer
self.present(videoVC, animated: true, completion: nil)
}
}
On iOS 10.0 I have an issue if I perform next steps:
When I come back, my view controller B is messed up - moved to the top of window and it size is smaller (also messed up inside, but I guess insides are messed up as a result of my autolayout constraints).
This doesn't seem to happen on iOS 11.
Is there anything I could do to fix it?
EDIT: Screenshots as requested (tabbar was hidden for privacy reasons):
Additional info:
I have also intercepted a delegate callback for more info:
func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController,
willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>,
in view: AutoreleasingUnsafeMutablePointer<UIView>) {
print("willRepositionPopoverTo")
print(popoverPresentationController)
print(rect.pointee)
print(view.pointee)
}
Which prints view
size as (w: 568; h: 320)
so it seems when I rotate AVP controller it changes my app's window orientation and this leads to resizing of my popup. Although it doesn't attempt to resize it back :( after I dismiss AVP.
I've successfully reproduced your issue, so you can rest easy knowing that it's not just you. I also spent a fair amount of time trying to fix the iOS 10 behavior using various "hacks". I was not initially successful. Additionally, it seems that although iOS 11 addressed the positioning of the popover, it has also introduced a bug with the tab bar size (pictures). So my solution needed to address that issue as well.
Upon revisiting this issue, I reconsidered a solution that I had originally ruled out. It turns out that the AVPlayerViewController
will only affect the orientation of it's parent UIWindow
instance. Using additional UIWindow
s is not an often used solution in iOS, but it works out perfectly here.
The solution is to create a clear UIWindow
with a shim rootViewController
whose sole purpose is to dismiss itself without animations the second time it appears. This solution works just as well on iOS 10 and iOS 11. It results in no changes to the user experience (other than fixing the bug).
Steps:
UIWindow
and make it's background clearShimVC
AVPlayerViewController
from the window's rootVC (thus you get animations, just like you otherwise would)Here is a gif showing how well this works:
class FirstViewController: UIViewController, UIViewControllerTransitioningDelegate {
@IBAction func popover(_ sender: UIButton) {
let b = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "B") as! B
b.modalPresentationStyle = .popover
b.preferredContentSize = CGSize(width: 300, height: 400)
b.isModalInPopover = true
if let ppc = b.popoverPresentationController {
ppc.delegate = self
ppc.permittedArrowDirections = []
let window = view.window!
ppc.sourceView = window
ppc.sourceRect = window.frame
ppc.passthroughViews = nil
}
present(b, animated: true, completion: nil)
}
}
extension FirstViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
class ShimVC: UIViewController {
var appearances: Int = 0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if appearances > 0 {
// When the rootViewController of a window dismisses, that window
// gets removed from the view hiearchy and discarded, making the
// the previous window key automatically
dismiss(animated: true)
}
appearances += 1
}
}
class B: UIViewController {
let videoURL: URL? = Bundle.main.url(forResource: "ImAfraidWeNeedToUseMath", withExtension: "m4v")
@IBAction func playVideo(_ sender: UIButton) {
if let videoURL = self.videoURL {
let videoPlayer = AVPlayer(url: videoURL)
let videoVC = AVPlayerViewController()
videoVC.player = videoPlayer
let vc = ShimVC(nibName: nil, bundle: nil)
let videoWindow = UIWindow()
videoWindow.backgroundColor = .clear
videoWindow.rootViewController = vc
videoWindow.makeKeyAndVisible()
// Present the `AVPlayerViewController` from the root
// of the window
vc.present(videoVC, animated: true)
}
}
@IBAction func done(_ sender: UIButton) {
dismiss(animated: true)
}
}
I'm going to propose a workaround. It's a behavior I've seen in some apps before. Basically, instead of presenting the video player from the popover, switch out B with the AVC. There are probably other solutions that require a bit more deviation from stock UIKit functionality. (Such as possibly implementing your own presentation controller to achieve the popover or implementing the popover as a child view controller so that you can present the AVC directly from A.)
In the following solution, A -> B
then when user presses player B informs A that it needs to present AVC. So then you get A -> AVC
. When AVC is dismissed, I re-present B, so you're back to A -> B
. Overall there are no UI problems on either iOS 10 or iOS 11, but your users will be requires to wait an additional fraction of a second. You can disable (or attempt to shorten?) the animations if time is critical, but overall it feels pretty fluid and natural.
Additionally, I recommend filing a radar regarding the tab bar size issue.
Here is my solution:
class A: UIViewController, UIViewControllerTransitioningDelegate {
var popover: B?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let popover = popover {
if let ppc = popover.popoverPresentationController {
ppc.delegate = self
ppc.permittedArrowDirections = []
let window = view.window!
ppc.sourceView = window
ppc.sourceRect = window.frame
ppc.passthroughViews = nil
}
present(popover, animated: true, completion: nil)
}
}
@IBAction func popover(_ sender: UIButton) {
let b = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "B") as! B
b.modalPresentationStyle = .popover
b.preferredContentSize = CGSize(width: 300, height: 400)
b.isModalInPopover = true
b.delegate = self
if let ppc = b.popoverPresentationController {
ppc.delegate = self
ppc.permittedArrowDirections = []
let window = view.window!
ppc.sourceView = window
ppc.sourceRect = window.frame
ppc.passthroughViews = nil
}
self.popover = b
present(b, animated: true, completion: nil)
}
}
extension A: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
extension A: BDelegate {
func dismissB() {
popover?.dismiss(animated: true)
popover = nil
}
func showAVPlayerViewController(_ vc: AVPlayerViewController) {
popover?.dismiss(animated: true) {
// Dispatch async allows it to come up in landscape if the phone is already rotated
DispatchQueue.main.async {
self.present(vc, animated: true)
}
}
}
}
protocol BDelegate: class {
func showAVPlayerViewController(_ vc: AVPlayerViewController)
func dismissB()
}
class B: UIViewController {
weak var delegate: BDelegate?
let videoURL: URL? = Bundle.main.url(forResource: "ImAfraidWeNeedToUseMath", withExtension: "m4v")
@IBAction func playVideo(_ sender: UIButton) {
if let videoURL = self.videoURL {
let videoPlayer = AVPlayer(url: videoURL)
let videoVC = AVPlayerViewController()
videoVC.player = videoPlayer
delegate?.showAVPlayerViewController(videoVC)
}
}
@IBAction func done(_ sender: UIButton) {
delegate?.dismissB()
}
}