弹窗转场/过度动画(Popover效果)
避免浪费大家时间,快速查看运行效果可以直接拉到最后看 【五.完整代码】 部分,如果要看递推逻辑,可以从前往后看。
一.基本设置
弹出一个控制器:系统提供了以下的方法
@available(iOS 5.0, *)
open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil)
但是如果想自定义弹出控制器的大小及动画的展示形式,就需要自定义转场 首先UIModalPresentationStyle定义了控制器弹出的方式
@available(iOS 3.2, *)
open var modalPresentationStyle: UIModalPresentationStyle
要设置为**.custom**,这样弹出控制器后,背后的控制器不会消失,如下:
二.调整大小
popoverVc.modalPresentationStyle = .custom
其次要使控制器遵守转场动画的协议:UIViewControllerTransitioningDelegate
@available(iOS 2.0, *)
optional func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
@available(iOS 2.0, *)
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
optional func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
optional func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
@available(iOS 8.0, *)
optional func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
协议中先看下第五个方法,此方法中,我们可以拿到要弹出的控制器和被弹出的控制器,返回值是UIPresentationController 可以认为,系统就是通过这个类型的控制器将我们要弹出的控制器,弹出来的。所以我们自定义一个控制器,继承自UIPresentationController,然后就改变一些东西,比如弹出控制器view的大小:
open var presentingViewController: UIViewController { get }
open var presentedViewController: UIViewController { get }
open var containerView: UIView? { get }
open var presentedView: UIView? { get }
可以看到,我们可以拿到要弹出来的控制器presentedViewController和它的view presentedView,那么我们改变presentedView的frame,就可以调整弹出控制器的位置大小了。自定义示例如下:
import UIKit
class WXPresentationController: UIPresentationController {
var presentedFrame:CGRect = CGRect(x: 0, y: 0, width: 0, height: 0)
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView!.frame = presentedFrame
setupCoverView()
}
}
extension WXPresentationController{
private func setupCoverView(){
let coverView = UIView(frame: containerView!.bounds)
coverView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(clickCoverView))
coverView.addGestureRecognizer(tapGesture)
containerView!.insertSubview(coverView, at: 0)
}
}
extension WXPresentationController{
@objc func clickCoverView(){
presentedViewController.dismiss(animated: true, completion: nil)
}
}
extension HomeViewController:UIViewControllerTransitioningDelegate{
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = WXPresentationController(presentedViewController: presented, presenting: presenting)
presentationController.presentedFrame = presentedFrame
return presentationController
}
}
截止到此处位置,动画依然用的是系统的动画,frame是我们来定义的
三.调整动画
此时再回头看UIViewControllerTransitioningDelegate 中的前两个方法
@available(iOS 2.0, *)
optional func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
@available(iOS 2.0, *)
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
见名知意,这两个方法,提供两个控制器,forPresented 和 forDismissed,也就是执行弹出动画的控制器和执行消失动画的控制器,看返回值:UIViewControllerAnimatedTransitioning? 这是个什么类型呢?进去看下:
public protocol UIViewControllerAnimatedTransitioning : NSObjectProtocol {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
很明显,这里是一个协议,那么返回值是一个协议是是什么意思呢?是指的返回一个遵守了这个协议的对象即可。 所以,可以直接返回当前控制器,也就是self,然后在当前控制器中,实现这个协议的两个方法即可。
extension HomeViewController:UIViewControllerAnimatedTransitioning{
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
}
但是弹出动画和消失动画走的是同样的两个代理方法,所以在此通过定义 isPresented变量来加以区分
extension HomeViewController:UIViewControllerTransitioningDelegate{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresented = true
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresented = false
return self
}
}
extension HomeViewController:UIViewControllerAnimatedTransitioning{
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
isPresented ? presentedTransitioin(using: transitionContext) : dismissedTransitioin(using: transitionContext)
}
func presentedTransitioin(using transitionContext: UIViewControllerContextTransitioning){
let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
presentedView.layer.anchorPoint = CGPoint(x: 0.5, y: 0)
transitionContext.containerView.addSubview(presentedView)
presentedView.transform = CGAffineTransform.init(scaleX: 1, y: 0.0001)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .curveEaseOut) {
presentedView.transform = CGAffineTransform.identity
} completion: { _ in
transitionContext.completeTransition(true)
}
}
func dismissedTransitioin(using transitionContext: UIViewControllerContextTransitioning){
let dismissedView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .curveEaseOut) {
dismissedView.transform = CGAffineTransform.init(scaleX: 1.0, y: 0.0001)
} completion: { _ in
dismissedView.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
}
四.优化工作
代理方法及动画逻辑都在HomeViewController中,耦合性太强,可以自定义一个PopoverAnimator,继承自NSObject,然后实现相应的代理逻辑,这样就可以将代理逻辑抽取出来加以复用,代理就成了PopoverAnimator的一个实例对象。
extension HomeViewController{
@objc func titleBtnClick(sender:TitleBtn){
let popoverVc = PopoverViewController()
popoverVc.modalPresentationStyle = .custom
popoverVc.transitioningDelegate = popoverAnimator
popoverAnimator.presentedFrame = CGRect(x: view.frame.width/2 - 75, y: 100, width: 150, height: 250)
present(popoverVc, animated: true, completion: nil)
}
}
至此:我们定义了一个WXPresentationController 来调整弹出控制器的view的层级,加蒙版等,以及一个PopoverAnimator 来调整弹出控制的大小及动画的类。
五.完整代码
import UIKit
class WXPresentationController: UIPresentationController {
var presentedFrame:CGRect = CGRect(x: 0, y: 0, width: 0, height: 0)
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView!.frame = presentedFrame
setupCoverView()
}
}
extension WXPresentationController{
private func setupCoverView(){
let coverView = UIView(frame: containerView!.bounds)
coverView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(clickCoverView))
coverView.addGestureRecognizer(tapGesture)
containerView!.insertSubview(coverView, at: 0)
}
}
extension WXPresentationController{
@objc func clickCoverView(){
presentedViewController.dismiss(animated: true, completion: nil)
}
}
import UIKit
class PopoverAnimator: NSObject {
var presentedFrame:CGRect = CGRect(x: 0, y: 0, width: 0, height: 0)
var isPresented:Bool = false
var presentedCallBack:((_ isPresented:Bool)->())?
}
extension PopoverAnimator:UIViewControllerTransitioningDelegate{
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let presentationController = WXPresentationController(presentedViewController: presented, presenting: presenting)
presentationController.presentedFrame = presentedFrame
return presentationController
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresented = true
if let callBack = presentedCallBack{
callBack(isPresented)
}
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresented = false
if let callBack = presentedCallBack{
callBack(isPresented)
}
return self
}
}
extension PopoverAnimator:UIViewControllerAnimatedTransitioning{
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
isPresented ? presentedTransitioin(using: transitionContext) : dismissedTransitioin(using: transitionContext)
}
func presentedTransitioin(using transitionContext: UIViewControllerContextTransitioning){
let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
presentedView.layer.anchorPoint = CGPoint(x: 0.5, y: 0)
transitionContext.containerView.addSubview(presentedView)
presentedView.transform = CGAffineTransform.init(scaleX: 1, y: 0.0001)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .curveEaseOut) {
presentedView.transform = CGAffineTransform.identity
} completion: { _ in
transitionContext.completeTransition(true)
}
}
func dismissedTransitioin(using transitionContext: UIViewControllerContextTransitioning){
let dismissedView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, options: .curveEaseOut) {
dismissedView.transform = CGAffineTransform.init(scaleX: 1.0, y: 0.0001)
} completion: { _ in
dismissedView.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
}
调用
import UIKit
class PopoverViewController: UIViewController {
lazy var tableView:UITableView = {
let tableview = UITableView(frame: CGRect(x: 0, y: 0, width: 150, height: 250), style: .plain)
return tableview
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
}
extension PopoverViewController{
private func setupUI(){
view.addSubview(tableView)
}
}
extension HomeViewController{
@objc func titleBtnClick(sender:TitleBtn){
let popoverVc = PopoverViewController()
popoverVc.modalPresentationStyle = .custom
popoverVc.transitioningDelegate = popoverAnimator
popoverAnimator.presentedFrame = CGRect(x: view.frame.width/2 - 75, y: 100, width: 150, height: 250)
popoverAnimator.presentedCallBack = { [weak self]isPresented in
self?.titleBtn.isSelected = isPresented
}
present(popoverVc, animated: true, completion: nil)
}
over
|