Closed pixelmatrix closed 5 months ago
Can you please add an example of how you would customize the sheet presentation duration in UIKit? I myself have not done this before, and simply adding UIView.animate
to call present
does not work.
Ah, sure. You need to provide a custom animator object from your UIViewControllerTransitioningDelegate
, without customizing the UIPresentationController
. Here's an example:
import UIKit
import SwiftUI
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let button = UIButton()
button.setTitle("Show Sheet", for: .normal)
view.addSubview(button)
button.sizeToFit()
button.center = view.center
button.setTitleColor(.systemBlue, for: .normal)
button.addTarget(self, action: #selector(showSheet), for: .primaryActionTriggered)
}
@objc private func showSheet() {
let vc = UIViewController()
vc.view.backgroundColor = .systemBackground
vc.transitioningDelegate = self
vc.sheetPresentationController?.detents = [.medium()]
present(vc, animated: true)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = CoverSpringTransitionAnimator()
animator.isPresenting = false
return animator
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
CoverTransitionAnimator(duration: 1)
}
}
open class CoverTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
public var isPresenting = true
public var duration = 0.5
public override init() {
super.init()
}
public init(duration: TimeInterval) {
super.init()
self.duration = duration
}
/// Customization point for sub-classes to customize animation parameters
open func animate(animations: @escaping () -> Void, completion: @escaping () -> Void) {
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: animations) { _ in
completion()
}
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)
let fromFrame = transitionContext.viewController(forKey: .from).map { transitionContext.finalFrame(for: $0) } ?? container.bounds
let toFrame = transitionContext.viewController(forKey: .to).map { transitionContext.finalFrame(for: $0) } ?? container.bounds
let completion = {
transitionContext.completeTransition(true)
}
if isPresenting {
performPresentingTransition(
animated: transitionContext.isAnimated,
containerView: container,
fromView: fromView,
toView: toView,
targetFrame: toFrame,
completion: completion
)
} else {
performDismissalTransition(
animated: transitionContext.isAnimated,
containerView: container,
fromView: fromView,
toView: toView,
targetFrame: fromFrame,
completion: completion
)
}
}
public func performPresentingTransition(animated: Bool, containerView: UIView, fromView: UIView?, toView: UIView?, targetFrame: CGRect, completion: @escaping () -> Void) {
// transitioning view is toView
toView?.frame = targetFrame
toView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
if let fromView {
containerView.addSubview(fromView)
}
if let toView {
containerView.addSubview(toView)
}
let animations: () -> Void = {
toView?.transform = CGAffineTransform.identity
}
if animated {
animate(animations: animations, completion: completion)
} else {
animations()
completion()
}
}
public func performDismissalTransition(animated: Bool, containerView: UIView, fromView: UIView?, toView: UIView?, targetFrame: CGRect, completion: @escaping () -> Void) {
// transitioning view is fromView
if let toView {
containerView.addSubview(toView)
}
if let fromView {
containerView.addSubview(fromView)
}
let animations: () -> Void = {
fromView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
}
if animated {
animate(animations: animations, completion: completion)
} else {
animations()
completion()
}
}
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
}
#Preview {
ViewController()
}
This should work automatically with Transmission
already. Please let me know if there is any functionality that is not working, but from some quick testing it looks to work as expected.
PresentationLink(
transition: .custom(CustomTransition())
) {
// ..
} label: {
Text("PresentationLink")
}
struct CustomTransition: PresentationLinkCustomTransition {
func presentationController(
sourceView: UIView,
presented: UIViewController,
presenting: UIViewController?
) -> UIPresentationController {
UISheetPresentationController(
presentedViewController: presented,
presenting: presenting
)
}
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
let animator = CoverTransitionAnimator(duration: 1)
animator.isPresenting = false
return animator
}
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
CoverTransitionAnimator(duration: 1)
}
}
class CoverTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let isPresenting: Bool
let duration: TimeInterval
var animator: UIViewPropertyAnimator?
init(isPresenting: Bool, duration: TimeInterval) {
self.isPresenting = isPresenting
self.duration = duration
super.init()
}
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?
) -> TimeInterval {
return transitionContext?.isAnimated == true ? duration : 0
}
func animateTransition(
using transitionContext: UIViewControllerContextTransitioning
) {
let animator = makeAnimatorIfNeeded(using: transitionContext)
animator.startAnimation()
if !transitionContext.isAnimated {
animator.stopAnimation(false)
animator.finishAnimation(at: .end)
}
}
func interruptibleAnimator(
using transitionContext: UIViewControllerContextTransitioning
) -> UIViewImplicitlyAnimating {
makeAnimatorIfNeeded(using: transitionContext)
}
func animationEnded(_ transitionCompleted: Bool) {
animator = nil
}
func makeAnimatorIfNeeded(
using transitionContext: UIViewControllerContextTransitioning
) -> UIViewPropertyAnimator {
if let animator = animator {
return animator
}
let animator = UIViewPropertyAnimator(
duration: duration,
curve: .linear
)
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)
let fromFrame = transitionContext.viewController(forKey: .from).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
let toFrame = transitionContext.viewController(forKey: .to).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
if isPresenting {
// transitioning view is toView
toView?.frame = toFrame
toView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
if let fromView {
containerView.addSubview(fromView)
}
if let toView {
containerView.addSubview(toView)
}
animator.addAnimations {
toView?.transform = CGAffineTransform.identity
}
} else {
// transitioning view is fromView
if let toView {
containerView.addSubview(toView)
}
if let fromView {
containerView.addSubview(fromView)
}
animator.addAnimations {
fromView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
}
}
animator.addCompletion { animatingPosition in
switch animatingPosition {
case .end:
transitionContext.completeTransition(true)
default:
transitionContext.completeTransition(false)
}
}
self.animator = animator
return animator
}
}
Ah, okay. This seems to be working for me now. Thanks!
I'm looking to simply control the animation curves of the sheet presentation, rather than the presentation itself. In UIKit you can do this by returning
nil
for theUIPresentationController
, or omittingpresentationController(forPresented:presenting:source:)
in yourUIViewControllerTransitioningDelegate
.This library offers the ability to provide an animator through a
custom
PresentationLinkTransition
, but in order to customize the animation you must also provide the presentation controller. Looking at how much setup there is for the.sheet
transition's UIPresentationController, I'd prefer to avoid this.A couple of ideas come to mind here:
PresentationLinkCustomTransition
allow you to return a nilUIPresentationController
to use the implementation from sheet instead?