效果:
假设有:
1.两个视图控制器:presentingVC, presentedVC
2.一个继承于UIPercentDrivenInteractiveTransition
,并遵守协议UIViewControllerAnimatedTransitioning
的实例:transitionAnimator
3.presentingVC.present(presentedVC, animated: true, completion: nil)
一. 自定义modal转场动画
- 要自定义dismiss转场动画,presentedVC视图控制器就要遵守
UIViewControllerTransitioningDelegate
协议(不要忘记设置presentedVC.transitioningDelegate = self
),并且实现以下两个方法:
1) 提供present动画
//这个方法在调用presentingVC.present(presentedVC, animated: true, completion: nil)时,被调用
// 返回transitionAnimator,正是由这个实例提供自定义转场的
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?{
return transitionAnimator
}
2) 提供dismiss动画
// 这个方法在调用presentedVC.dismiss(animated: true, completion: nil)时,被调用
// 返回实例 transitionAnimator
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?{
return transitionAnimator
}
- 类
TransitionAnimator
遵守UIViewControllerAnimatedTransitioning
协议,并必须实现以下两个协议方法:
//返回转场动画的持续时间
1) func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
//实现转场动画
2) func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
二. 滑动手势控制 dismiss 过程
- 根据滑动手势的偏移量来设置dismiss动画的完成百分比,再让presentedVC视图控制器实现
UIViewControllerTransitioningDelegate
协议的另一个方法:
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?{
return transitionAnimator
}
- 根据滑动手势的偏移量,设置dismiss 动画的完成百分比
transitionAnimator.transitionAnimator.update(precent)
二. 关键代码
presentedVC
import UIKit
protocol DetialViewControllerDelegate {
func detialViewController(_ controller: DetialViewController, scrollViewPanGestureRecognizerDidChange:UIPanGestureRecognizer)
}
struct PanningData {
let contentOffset: CGPoint
let isDragging: Bool
let translation: CGPoint
let velocity: CGPoint
}
/// 转场状态
enum TransitionState: String{
case none
case started
case updating
case canceling
case finishing
fileprivate var active: Bool {
return self == .started || self == .updating
}
}
class DetialViewController: UITableViewController{
var delegate: DetialViewControllerDelegate?
var transitionAnimator = TransitionAnimator()
var transitionPhase : TransitionState = .none
override func viewDidLoad() {
super.viewDidLoad()
self.transitioningDelegate = self
tableView.delegate = self
tableView.dataSource = self
// tableView.bounces = false
tableView.panGestureRecognizer.addTarget(self, action: #selector(detailViewController(_:)))
}
}
extension DetialViewController:UIViewControllerTransitioningDelegate{
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell:UITableViewCell? = tableView.dequeueReusableCell(withIdentifier: "sss")
if (cell == nil) {
cell = UITableViewCell(style: .default, reuseIdentifier: "sss")
}
cell?.textLabel?.text = String(indexPath.row)
return cell!
}
@objc func detailViewController(_ recognizer: UIPanGestureRecognizer){
guard let scrollView = recognizer.view as? UIScrollView else { return }
let data = PanningData(contentOffset: scrollView.contentOffset, isDragging: scrollView.isTracking, translation: recognizer.translation(in: scrollView), velocity: recognizer.velocity(in: scrollView))
if data.contentOffset.y > 0 {
transitionPhase = transitionPhase == .none ? .none : .canceling
}else if data.isDragging && data.translation.y > 0 && !transitionPhase.active {
transitionPhase = .started
}
else if data.isDragging && data.translation.y > 0 && transitionPhase.active {
transitionPhase = .updating
}
else if data.isDragging && data.translation.y < 0 && transitionPhase.active {
transitionPhase = .canceling
}
else if !data.isDragging && data.translation.y > 0 && transitionPhase.active {
transitionPhase = data.velocity.y > 0 ? .finishing : .canceling
}
if transitionPhase == .started || transitionPhase == .updating{
self.transitionAnimator.isInFlight = true
}else {
self.transitionAnimator.isInFlight = false
}
print(transitionPhase.rawValue)
if transitionPhase == .started {
scrollView.bounces = false
self.dismiss(animated: true, completion: nil)
}
if transitionPhase == .updating || transitionPhase == .started {
let precent = data.translation.y / self.view.bounds.height
self.transitionAnimator.update(precent)
}
if transitionPhase == .canceling {
scrollView.bounces = true
self.transitionAnimator.cancel()
}
if transitionPhase == .finishing {
self.transitionAnimator.finish()
}
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self.transitionAnimator
}
func animationController(forPresented presented: UIViewController,presenting: UIViewController,source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self.transitionAnimator
}
///这个协议用来返回控制 dismiss 完成度的类UIPercentDrivenInteractiveTransition,此类遵守 UIViewControllerInteractiveTransitioning
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning? {
return self.transitionAnimator.isInFlight ? self.transitionAnimator : nil
}
}
TransitionAnimator
import UIKit
class TransitionAnimator: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning{
private let dismissedOverlayColor = UIColor(white: 1, alpha: 0)
private let presentedOverlayColor = UIColor(white: 1, alpha: 1)
fileprivate let darkOverlayView = UIView()
internal var isInFlight = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return transitionContext?.isInteractive == .some(true) ? 0.6:0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else{ return }
let containerView = transitionContext.containerView
if toVC.isBeingPresented {
self.animatePresentation(fromViewController: fromVC,
toViewController: toVC,
containerView: containerView,
transitionContext: transitionContext)
}else{
self.animateDismissal(fromViewController: fromVC,
toViewController: toVC,
containerView: containerView,
transitionContext: transitionContext)
}
}
fileprivate func animatePresentation(
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController,
containerView: UIView,
transitionContext: UIViewControllerContextTransitioning) {
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
containerView.insertSubview(self.darkOverlayView, belowSubview: toVC.view)
self.darkOverlayView.frame = containerView.bounds
self.darkOverlayView.backgroundColor = dismissedOverlayColor
let bottomLeftCorner = CGPoint(x: 0, y: containerView.bounds.height)
let finalFrame = CGRect(origin: .zero, size: containerView.bounds.size)
toVC.view.frame = CGRect(origin: bottomLeftCorner, size: containerView.bounds.size)
UIView.animate(
withDuration: self.transitionDuration(using: transitionContext),
delay: 0,
options: [.curveEaseOut],
animations: {
toVC.view.frame = finalFrame
self.darkOverlayView.backgroundColor = self.presentedOverlayColor
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
/// toViewController 表示将要出现的VC
/// fromViewController 表示将要dismiss的VC
fileprivate func animateDismissal(fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController,
containerView: UIView,
transitionContext: UIViewControllerContextTransitioning) {
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
containerView.insertSubview(self.darkOverlayView, belowSubview: fromVC.view)
self.darkOverlayView.frame = containerView.bounds
self.darkOverlayView.backgroundColor = presentedOverlayColor
let bottomLeftCorner = CGPoint(x: 0, y: containerView.bounds.height)
let finalFrame = CGRect(origin: bottomLeftCorner, size: containerView.bounds.size)
toVC.view.frame = containerView.bounds
let animationCurve: UIViewAnimationOptions = transitionContext.isInteractive == .some(true)
? .curveLinear
: .curveEaseOut
UIView.animate(
withDuration: self.transitionDuration(using: transitionContext),
delay: 0,
options: [animationCurve],
animations: {
fromVC.view.frame = finalFrame
if transitionContext.isInteractive {
fromVC.view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
}
self.darkOverlayView.backgroundColor = self.dismissedOverlayColor
},
completion: { _ in
self.darkOverlayView.backgroundColor = transitionContext.transitionWasCancelled
? .black
: self.darkOverlayView.backgroundColor
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
}