自定义转场动画实现popover效果

/ 3

完成效果:

popover

JFHomeViewController中首先为导航栏的titleView设置一个自定义的UIButton按钮

JFHomeViewController.swift

let titleButton = JFTitleButton(title: "周剑峰先生")
navigationItem.titleView = titleButton

在自定义UIButton类里修改内部子控件实现titleLabel和imageView交换位置

JFTitleButton.swift

override func layoutSubviews() {
        super.layoutSubviews()
        
        // 把label移动到左边
        titleLabel?.frame.origin.x = 0
        
        // 把图片移到label的后面
        imageView?.frame.origin.x = titleLabel!.frame.width + 2
    }

在自定义UIButton类里实现一个便利构造方法,将上、下箭头分别设置为按钮的选中、默认样式的imageView

JFTitleButton.swift

convenience init(title: String) {
        self.init()
        
        setTitle(title, forState: UIControlState.Normal)
        adjustsImageWhenHighlighted = false
        titleLabel?.font = UIFont.systemFontOfSize(17)
        setImage(UIImage(named: "navigationbar_arrow_down"), forState: UIControlState.Normal)
        setImage(UIImage(named: "navigationbar_arrow_up"), forState: UIControlState.Selected)
        setBackgroundImage(UIImage(named: "tabbar_compose_below_button_highlighted"), forState: UIControlState.Highlighted)
        setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
        sizeToFit()
    }

创建一个继承UIViewController的自定义控制器类,并为这个普通控制器设置一个背景图片和tableView。注意背景图片是全屏的,而tableView根据背景图设置外边距,并且控制器view的背景颜色需要设置透明。这里布局我使用的是SnapKit。

JFPopViewController.swift

import UIKit

class JFPopViewController: UIViewController {
    
    // MARK: - 属性
    var lists = [["首页", "好友圈", "群微博", "我的微博", "新浪微博"],
        ["特别关注", "网络好友", "我的推荐", "明星", "科技", "兄弟连", "舞蹈", "傻逼", "企业", "我的朋友", "名人明星", "悄悄关注"],
        ["周边微博"]
    ]
    
    // tableview重用标识符
    let popoverIdentifier = "popoverCell"

    // MARK: - 视图声明周期
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 背景颜色透明
        view.backgroundColor = UIColor.clearColor()
        
        // 注册cell
        tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: popoverIdentifier)

        // 准备UI
        prepareUI()
    }
    
    // MARK: - 准备UI
    private func prepareUI() {
        
        // 添加背景图片
        view.addSubview(backgroundView)
        view.addSubview(tableView)
        
        // 约束子控件
        backgroundView.snp_makeConstraints { (make) -> Void in
            make.edges.equalTo(view.snp_edges)
        }
        tableView.snp_makeConstraints { (make) -> Void in
            make.edges.equalTo(UIEdgeInsets(top: 15, left: 12, bottom: -12, right: -12))
        }
        
    }
    
    // MARK: - 懒加载
    // 背景图片
    lazy var backgroundView: UIImageView = {
        let imageView = UIImageView()
        var image = UIImage(named: "popover_background")!
        image = image.resizableImageWithCapInsets(UIEdgeInsets(top: image.size.height * 0.5, left: image.size.width * 0.5 - 100, bottom: image.size.height * 0.5, right: image.size.width * 0.5 - 100), resizingMode: UIImageResizingMode.Stretch)
        imageView.image = image
        return imageView
    }()
    
    // tableview
    lazy var tableView: UITableView = {
        let tableView = UITableView(frame: CGRectZero, style: UITableViewStyle.Grouped)
        tableView.dataSource = self
        tableView.delegate = self
        return tableView
    }()

}

// MARK: - UITableViewDataSource、UITableViewDelegate数据源、代理
extension JFPopViewController: UITableViewDataSource, UITableViewDelegate {
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return lists.count
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return lists[section].count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCellWithIdentifier(popoverIdentifier)!
        cell.textLabel?.text = lists[indexPath.section][indexPath.row]
        return cell
    }
    
}

再创建一个继承自UIPresentationController的类,他的作用是作为一个呈现我们自定义的控制器视图的容器,我们可以在这个类中自定义我们modal出来的控制器的视图大小。并且为这个容器视图添加一个敲击手势,dismiss我们modal出来的控制器。

JFPresentationController.swift

import UIKit

class JFPresentationController: UIPresentationController {

    override func containerViewWillLayoutSubviews() {
        super.containerViewWillLayoutSubviews()

        // 设置容器视图透明背景
        containerView?.backgroundColor = UIColor(white: 0, alpha: 0.2)
        
        // 呈现视图
        presentedView()?.frame = CGRectMake((kScreenW - 200) * 0.5, 56, 200, 300)
        
        // 添加点击手势
        let tap = UITapGestureRecognizer(target: self, action: "didTappedContainerView")
        containerView?.addGestureRecognizer(tap)
    }
    
    // MARK: - 容器视图区域的点击手势
    @objc private func didTappedContainerView() {
        NSNotificationCenter.defaultCenter().postNotificationName("PopoverDismiss", object: nil)
        presentedViewController.dismissViewControllerAnimated(false, completion: nil)
    }
}

然后再创建两个类,遵守UIViewControllerAnimatedTransitioning协议并实现对应方法,这两个类的作用是自定义我们modal时的转场动画。

JFPopoverModalAnimation.swift

import UIKit

class JFPopoverModalAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    
    // 动画时间
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.25
    }
    
    // modal动画
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        // 获取到需要modal的控制器的view
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
        
        // 将需要modal的控制器的view添加到容器视图
        transitionContext.containerView()?.addSubview(toView)
        
        toView.transform = CGAffineTransformMakeScale(1, 0)
        toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
        
        // 动画缩放modal的控制器的view到正常大小
        UIView.animateWithDuration(transitionDuration(nil), delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 2, options: UIViewAnimationOptions(rawValue: 0), animations: { () -> Void in
            toView.transform = CGAffineTransformIdentity
            }, completion: { (_) -> Void in
            transitionContext.completeTransition(true)
        })
    }
}

JFPopoverDismissAnimation.swift

import UIKit

class JFPopoverDismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {

    // 动画时间
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.25
    }
    
    // dismiss动画
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        // 获取到modal出来的控制器的view
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        
        // 动画缩放modal出来的控制器的view到看不到
        UIView.animateWithDuration(transitionDuration(nil), delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 2, options: UIViewAnimationOptions(rawValue: 0), animations: { () -> Void in
            fromView.transform = CGAffineTransformMakeScale(1, 0.001)
            }, completion: { (_) -> Void in
                transitionContext.completeTransition(true)
        })
    }
}

然后回到JFHomeViewController添加导航栏标题按钮的点击事件,指定transitioningDelegate委托对象和modalPresentationStyle样式。当点击了按钮就改变箭头方向,并创建我们需要modal的控制器。这里监听按钮点击事件我使用了ReactiveCocoa,只要看闭包里的代码就行了。

JFHomeViewController.swift

titleButton.rac_signalForControlEvents(UIControlEvents.TouchUpInside).subscribeNext({ (button) -> Void in
                
                // 改变箭头方向
                titleButton.selected = !titleButton.selected
                
                // 标题下的控制器
                let vc = JFPopViewController()
                vc.transitioningDelegate = self
                vc.modalPresentationStyle = UIModalPresentationStyle.Custom
                self.presentViewController(vc, animated: false, completion: nil)
            })

扩展JFHomeViewController遵守协议,实现委托方法,返回我们自定义的UIPresentationController类的对象。这样我们就可以通过我们自定义的这个类去控制器modal的控制器视图了。

JFHomeViewController.swift

// MARK: - UIViewControllerTransitioningDelegate委托方法
extension JFHomeViewController: UIViewControllerTransitioningDelegate {
    
    // 返回一个控制modal视图大小的对象
    func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? {
        return JFPresentationController(presentedViewController: presented, presentingViewController: presenting)
    }
    
    // 返回一个控制器modal动画效果的对象
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return JFPopoverModalAnimation()
    }
    
    // 返回一个控制dismiss动画效果的对象
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return JFPopoverDismissAnimation()
    }
}

在viewDidLoad里注册通知,当接收到popoverDismiss的通知就改变按钮的状态,从而达到实现箭头旋转。这里通知也使用的ReactiveCocoa,如果没有接触过,请看闭包里的代码就好。

JFHomeViewController.swift

NSNotificationCenter.defaultCenter().rac_addObserverForName("PopoverDismiss", object: nil).subscribeNext({ (_) -> Void in
                // 获取导航栏标题按钮
                let button = self.navigationItem.titleView as! JFTitleButton
                button.selected = false
            })

最后别忘了注销通知

JFHomeViewController.swift

deinit {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }