Reputation: 469
I am running into a problem with view controller containment and wanting to present view controllers "over current context" with a custom presentation/animation.
I have a root view controller that has two child view controllers that can be added and removed as children to the root. When these child view controllers present a view controller I want the presentation to be over current context so that when the child that is presenting is removed from the view heirarchy and deallocated the presented modal will be removed as well. Also, if child A presents a view controller, I would expect child B's 'presentedViewController' property to be nil in an "over current context" presentation even if A was still presenting.
Everything works as expected when I set the modalPresentationStyle
of my presented view controller to overCurrentContext
, and if the child view controllers have definesPresentationContext
set to true.
This doesn't work when I would expect it to however if I have modalPresentationStyle
set to custom
and override shouldPresentInFullscreen
returning false in my custom presentation controller.
Here is an example illustrating the problem:
import UIKit
final class ProgressController: UIViewController {
private lazy var activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .white)
private lazy var progressTransitioningDelegate = ProgressTransitioningDelegate()
// MARK: Lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override public func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0, alpha: 0)
view.addSubview(activityIndicatorView)
activityIndicatorView.startAnimating()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
activityIndicatorView.center = CGPoint(x: view.bounds.width/2, y: view.bounds.height/2)
}
// MARK: Private
private func setup() {
modalPresentationStyle = .custom
modalTransitionStyle = .crossDissolve
transitioningDelegate = progressTransitioningDelegate
}
}
final class ProgressTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return DimBackgroundPresentationController(presentedViewController: presented, presenting: source)
}
}
final class DimBackgroundPresentationController: UIPresentationController {
lazy var overlayView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(white: 0, alpha: 0.5)
return v
}()
override var shouldPresentInFullscreen: Bool {
return false
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
overlayView.alpha = 0
containerView!.addSubview(overlayView)
containerView!.addSubview(presentedView!)
if let coordinator = presentedViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { _ in
self.overlayView.alpha = 1
}, completion: nil)
}
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
overlayView.frame = presentingViewController.view.bounds
}
override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
let coordinator = presentedViewController.transitionCoordinator
coordinator?.animate(alongsideTransition: { _ in
self.overlayView.alpha = 0
}, completion: nil)
}
}
class ViewControllerA: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
definesPresentationContext = true
let vc = ProgressController()
self.present(vc, animated: true) {
}
}
}
class ViewController: UIViewController {
let container = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
container.frame = view.bounds
view.addSubview(container)
let lhs = ViewControllerA()
lhs.view.backgroundColor = .red
let rhs = UIViewController()
rhs.view.backgroundColor = .blue
addChildViewController(lhs)
lhs.view.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
container.addSubview(lhs.view)
lhs.didMove(toParentViewController: self)
addChildViewController(rhs)
rhs.view.frame = CGRect(x: view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height)
container.addSubview(rhs.view)
rhs.didMove(toParentViewController: self)
container.contentSize = CGSize(width: view.bounds.width * 2, height: view.bounds.height)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// let rect = CGRect(x: floor(view.bounds.width/2.0), y: 0, width: view.bounds.width, height: view.bounds.height)
// container.scrollRectToVisible(rect, animated: true)
}
}
If you change the setup()
function on ProgressController
to:
private func setup() {
modalPresentationStyle = .overCurrentContext
modalTransitionStyle = .crossDissolve
transitioningDelegate = progressTransitioningDelegate
}
the presentation/view heirarchy will behave as expected, but the custom presentation will not be used.
Apple's docs for shouldPresentInFullscreen
seem to indicate that this should work:
The default implementation of this method returns true, indicating that the presentation covers the entire screen. You can override this method and return false to force the presentation to display only in the current context.
If you override this method, do not call super.
I tested this in Xcode 8 on iOS 10 and Xcode 9 on iOS 11 and the above code would not work as expected in either case.
Upvotes: 18
Views: 7356
Reputation: 99
I ran into this same problem myself recently. You're right that UIKit documentation seems to be misleading about the behavior of UIPresentationController.shouldPresentInFullscreen. However, when experimenting with this property, I discovered that when it's set to NO, the UITransitionView is added to the window, but when it's set to YES, it's added to UITransitionView of the nearest presented UIViewController. This does not include child UIViewControllers whose definesPresentationContext property returns YES.
However, if you present a UIViewController with UIModalPresentationStyleOverCurrentContext on a UIViewController with definesPresentationContext == YES, then UIKit inserts a presentation container view in the hierarchy above the presenting view controller. If you present upon that presented UIViewController using UIModalPresentationStyleCustom with a UIPresentationController whose shouldPresentInFullscreen property returns NO, the presented UIViewController's presentation container view will be inserted into the first UIViewController that was presented over the current context of the child UIViewController.
Upvotes: 2
Reputation: 54121
I also think that shouldPresentInFullscreen
does essentially nothing. A bug if you ask me.
To fix this, you should override frameOfPresentedViewInContainerView
and return the adjusted frame.
Then, override containerViewWillLayoutSubviews
and set self.presentedViewController.view.frame = self.frameOfPresentedViewInContainerView
. Here you should also set the frames of any background or chrome views you might have.
Upvotes: 1
Reputation: 535138
I'm going to guess that you've found a bug. The docs say that shouldPresentInFullscreen
can turn this into a currentContext
presentation, but it does nothing. (In addition to your test and my test, I found a few online complaints about this, leading me to think that that's the case.)
The conclusion is that you cannot get the default currentContext
behavior (where the runtime consults the source view controller and up the hierarchy looking for definesPresentationContext
) if you use presentation style of .custom
.
I suggest filing a bug with Apple.
Upvotes: 4