iOS 13 seems to use a new UIPresentationController
for presenting modal view controllers, but one that does not rely on taking snapshots of the presenting view controller (as most / all libraries out there do). The presenting view controller is 'live' and continues to display animations / changes while the modal view controller is showing above a transparent / tinted background.
I'm able to replicate this easily (as the aim is to make a backward compatible version for iOS 10 / 11 / 12 etc) by using a CGAffineTransform
on the presenting view controller's view, however frequently while rotating the device, the presenting view begins to de-shape and grow out of bounds seemingly because the system updates its frame
while there's an active transform
applied to it.
According to the documentation, frame
is undefined when there's a transform
applied to the view. Given the system seems to be modifying the frame and not me, how do I solve this without ending up with hacky solutions where I'm updating the presenting view's bounds? I need this presentation controller to remain generic since the presenting controller could be any shape or form, and won't necessarily be a full-screen view.
Here's what I have so far - it's a simple UIPresentationController
subclass, which seems to work fine, however rotating the device and then dismissing the presented view controller seems to de-shape the presenting view controller's bounds (becomes too wide or shrinks, depending on whether you presented the modal controller while in landscape / portrait)
class SheetPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return CGRect(x: 40, y: containerView!.bounds.height / 2, width: containerView!.bounds.width-80, height: containerView!.bounds.height / 2)
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
if let _ = presentingViewController.transitionCoordinator {
// We're transitioning - don't touch the frame yet as it'll
// clash with our transform
} else {
self.presentedView?.frame = self.frameOfPresentedViewInContainerView
}
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
containerView?.backgroundColor = .clear
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.3)
// Scale the presenting view
self?.presentingViewController.view.layer.cornerRadius = 16
self?.presentingViewController.view.transform = CGAffineTransform.init(scaleX: 0.9, y: 0.9)
}, completion: nil)
}
}
override func dismissalTransitionWillBegin() {
if let coordinator = presentingViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.containerView?.backgroundColor = .clear
self?.presentingViewController.view.layer.cornerRadius = 0
self?.presentingViewController.view.transform = .identity
}, completion: nil)
}
}
}
And the Presenting Animation controller:
import UIKit
final class PresentingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
return
}
let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)
let containerView = transitionContext.containerView
containerView.addSubview(presentedViewController.view)
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
presentedViewController.view.frame = finalFrameForPresentedView
// Move it below the screen so it slides up
presentedViewController.view.frame.origin.y = containerView.bounds.height
animator.addAnimations {
presentedViewController.view.frame = finalFrameForPresentedView
}
animator.addCompletion { (animationPosition) in
if animationPosition == .end {
transitionContext.completeTransition(true)
}
}
animator.startAnimation()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
}
As well as the dismissing animation controller:
import UIKit
final class DismissingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedViewController = transitionContext.viewController(forKey: .from) else {
return
}
guard let presentingViewController = transitionContext.viewController(forKey: .to) else {
return
}
let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
let containerView = transitionContext.containerView
let offscreenFrame = CGRect(x: finalFrameForPresentedView.minX, y: containerView.bounds.height, width: finalFrameForPresentedView.width, height: finalFrameForPresentedView.height)
let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)
animator.addAnimations {
presentedViewController.view.frame = offscreenFrame
}
animator.addCompletion { (position) in
if position == .end {
// Complete transition
transitionContext.completeTransition(true)
}
}
animator.startAnimation()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
}