I use the Apple's new UISheetPresentationController and want to update the layout of the underlying view controller when the sheet is presented and dismissed.
When the user starts to pull down the sheet, func presentationControllerWillDismiss(_ presentationController: UIPresentationController) of UISheetPresentationControllerDelegate is called and I start my animation like so
presentationController.presentedViewController.transitionCoordinator?.animate(
alongsideTransition: { _ in
// my animations
},
completion: nil
)
This works perfectly when the interaction is not too fast. If it is fast, it seems like the animation gets out of sync:
I also added a handler which logs when the animation is cancelled:
transitionCoordinator.notifyWhenInteractionChanges() {
print("cancelled: \(context.isCancelled)")
}
And it looks like the animation is cancelled but the func presentationControllerWillDismiss is not called again. When the the gesture is not too fast, the animation is also cancelled but afterwards presentationControllerWillDismiss is called.
Do you think it is a bug that the animation is cancelled but presentationControllerWillDismiss is not called again? And does someone maybe know a simple workaround?
Minimal reproducible Example:
import UIKit
class ViewController: UIViewController {
lazy var openSheetButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Open Sheet", for: .normal)
button.backgroundColor = .systemGreen
button.addTarget(
self,
action: #selector(didTapOpenSheetButton(_:)),
for: .touchDown
)
return button
}()
let orangeView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemOrange
return view
}()
var bottomConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
positionElements()
}
func positionElements() {
// Orange view
view.addSubview(orangeView)
orangeView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16).isActive = true
orangeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true
orangeView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16).isActive = true
bottomConstraint = orangeView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -200)
bottomConstraint?.isActive = true
// Open Sheet Button
orangeView.addSubview(openSheetButton)
openSheetButton.centerXAnchor.constraint(equalTo: orangeView.centerXAnchor).isActive = true
openSheetButton.topAnchor.constraint(equalTo: orangeView.topAnchor, constant: 8).isActive = true
}
@objc
func didTapOpenSheetButton(_ sender: Any) {
// Create modal view controller
let viewController = UIViewController()
viewController.view.backgroundColor = .systemGreen
guard let sheet = viewController.sheetPresentationController else { return }
// Configure sheet
sheet.detents = [.medium()]
sheet.prefersGrabberVisible = true
sheet.delegate = self
// Present modal view controller
present(viewController, animated: true)
// Animate something alondside transition (e.g. bottom constraint of orange container)
transitionCoordinator?.animate(
alongsideTransition: { _ in
self.bottomConstraint?.constant = -UIScreen.main.bounds.height / 2
self.view.layoutIfNeeded()
},
completion: nil
)
}
}
extension ViewController: UISheetPresentationControllerDelegate {
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
print("presentationControllerWillDismiss called")
// Animate something alongside dismissal transition
presentationController.presentedViewController.transitionCoordinator?.animate(
alongsideTransition: { _ in
self.bottomConstraint?.constant = -UIScreen.main.bounds.height / 2
self.view.layoutIfNeeded()
self.bottomConstraint?.constant = -200
self.view.layoutIfNeeded()
},
completion: nil
)
// Print when transition was cancelled
presentationController.presentedViewController.transitionCoordinator?.notifyWhenInteractionChanges() { context in
print("notifyWhenInteractionChanges: \(context.isCancelled)")
}
}
}
