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)
}
}
- This will show a ViewController with a scroll view that contains two child view controllers
- The red view controller on the left hand side will present a progress view controller that should present over current context.
- If the view controller was presented properly "over current context" then you would be able to scroll the scrollview, and if you checked the "presentedViewController" property of the blue right hand side view controller then it should be nil. Neither of these are 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.
shouldPresentInFullscreenon presentation controller be if not to provide this functionality? Overriding it in this example does nothing when the docs seem to indicate that it should. - pmickshouldPresentInFullscreento turn this into a non-fullscreen transition, but this doesn't behave like a current context transition (or over current context) when I return false. Changing what is returned does nothing from what I can tell for a custom presentation. - pmickdefinedPresentationContext, it is never even being called when the presentation style is.custom. So I would have to say that this feature doesn't work as advertised. It's easy to change the animation for a.currentContextbut I don't think you can usefully change the presentation controller. - mattshouldPresentInFullscreendoes nothing. - matt