这两天项目中有一个点击cell需要弹出详细信息的需求,像下图这样:
我最初的想法是像实现hudView一样通过自定义一个View来实现,后来通过查阅资料了解到还可以自定义一个Controller,类似苹果官方的AlertViewController
,然后通过自定义转场来实现,而且后者自定义的功能更加强大,话不多说,上手撸一拨代码就很清楚了。
先来第一种方法,通过自定义一个View来实现:
新建一个项目,在ViewController
中添加两个button,测试不同方法对应的效果,如下图:
既然是自定义View,就需要新建一个HudView
的swift文件,我们在HudView.swift
中加入如下代码:
import UIKit
class HudView: UIView {
var text = ""
class func hud(inView view: UIView, animated: Bool) -> HudView {
let hudView = HudView(frame: view.bounds)
hudView.isOpaque = false
view.addSubview(hudView)
view.isUserInteractionEnabled = false
hudView.show(animated: animated)
return hudView
}
override func draw(_ rect: CGRect) {
let boxWidth: CGFloat = 240
let boxHeight: CGFloat = 240
let boxRect = CGRect( x: round((bounds.size.width - boxWidth) / 2), y: round((bounds.size.height - boxHeight) / 2), width: boxWidth, height: boxHeight)
let roundedRect = UIBezierPath(roundedRect: boxRect, cornerRadius: 10)
UIColor(white: 0.3, alpha: 0.8).setFill()
roundedRect.fill()
let attribs = [ NSFontAttributeName: UIFont.systemFont(ofSize: 16), NSForegroundColorAttributeName: UIColor.white ]
let textSize = text.size(attributes: attribs)
let textPoint = CGPoint( x: center.x - round(textSize.width / 2), y: center.y - round(textSize.height / 2) + boxHeight / 4)
text.draw(at: textPoint, withAttributes: attribs)
}
func show(animated: Bool) {
if animated {
alpha = 0 // 动画前透明度
transform = CGAffineTransform(scaleX: 1.3, y: 1.3) // 动画前缩放程度
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: {
self.alpha = 1 // 动画后透明度
self.transform = CGAffineTransform.identity // 动画后缩放程度,即本来大小
}, completion: nil)
}
}
}
这么多代码是不是很头疼?没关系,我们从上到下来捋一遍。
首先我们定义了一个HudView
类,继承自UIView
基类,这没什么可说的;
text
属性用来展示View上的文字;
然后我们定义了一个类方法,这个类方法的主要作用是作为一个便利构造器,在这个类方法中我们可以直接定义将hudView
作为子View添加到父View中的逻辑,isUserInteractionEnabled
值确定了用户是否可以和当前View交互,show
方法用来展示动画。当然我们也可以不这么做,但是这样写更高效,而且后续添加其他逻辑时也更方便(比如用来展示动画的show
方法);
其次我们需要重写UIView
的draw
方法,draw
方法用来将View绘制到屏幕上,我们已经自定义了一个hudView
,然而却不知道应该将其绘制到哪里,此时就需要用到draw
方法,在这个方法中,我们使用CGRect
结构体定义了hudView
的x轴,y轴的位置以及大小。如果要在View上添加text
,那么还需要对text
进行绘制,方法类似,需要设定text
的字体样式和位置,然后执行text
的draw
方法。需要注意的是:hudView
目前是一个矩形,我们可以新建一个UIBezierPath
对象roundedRect
使得边界更平滑。UIColor
的setFill
方法用来填充颜色,roundedRect
的fill
方法用来填充界面以完成绘制;
最后是show
方法,这个用来展示hudView
出现时的动画,这个也很简单,定义动画前的透明度、缩放程度和动画后的透明度、缩放程度,然后通过UIView
的animate
方法实现。需要注意的是:animate
方法是一个多态方法,不同的动画效果对应该方法中不同的参数,有兴趣的读者可以自行查阅。
至此,我们已经完成了HudView
类的创建,需要使用的时候直接拿来调用就好了,比如像下面这样:
let hudView = HudView.hud(inView: self.view, animated: true)
hudView.text = "这是一个Pop-up View"
这里就能看出类方法作为便利构造器的方便,否则我们还需要在当前Controller中实现添加hudView
的逻辑,动画方法也需要写在当前Controller中,虽然不影响功能,但那不是一种好的编程方式。
此方法效果如下图:
第二种方法稍微有些复杂,但是可自定义化的东西更多,适合更复杂的自定义需求:
- 第一种方法是基于View的,第二种方法是基于Controller的,首先我们会给hudView一个它自己Controller;
- 新建一个ViewController,命名为
HudViewController
; - 然后在storyboard中拖入一个ViewController并继承自
HudViewController
; - 接下来拖一个View到
HudViewController
中,并且拖一个label和一个button到View中,这个View就是稍后要弹出的View,label用来显示测试文本,button用来关闭此Controller; - 最后将初始ViewController中的第二个button连接到
HudViewController
,界面切换方法选择Present Modally
,Identifier设置为ShowPopView
,同时为了对比醒目,我们将HudViewController
的背景设置成50%的黑色,弹出的View
设置成90%的白色;
到现在准备工作基本完成(准备工作就比第一个复杂了太多)。
现在我们可以跑一下看看效果,差不多应该是这样:
我们会发现整个背景现在都是黑色的,然而之前明明已经设置了背景是50%的半透明黑色,经过仔细观察会发现,半透明的效果在View弹出的时候短暂地出现了一下,这就是我们需要解决的第一个问题。在解决第一个问题之前,有一些基本概念需要了解一下:在iOS中,ViewController和ViewController之间的切换有一个默认的效果,一般来说,我们不需要更改这个默认的切换,但是有些时候默认的切换效果并不能满足我们开发的需求,此时就需要自定义切换效果。比如说在上述例子中,我们选择的是默认的Present Modally
切换方法,此方法在HudViewController
加载结束后会隐藏之前ViewController的内容,这样做是为了节省iOS的系统资源,在系统看来,新的ViewController出现,那旧的ViewController就不再需要绘制了,因此旧的ViewController被隐藏,即使我们设置了半透明的黑色背景,也因为系统Controller切换方法的调用被隐藏了。
接下来需要自定义Controller的切换效果:
新建一个CustomPresentationController
类,继承自UIPresentationController
,在类中添加如下代码:
import UIKit
class CustomPresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return false
}
}
UIPresentationController
类包含了全部ViewController切换的逻辑,shouldRemovePresentersView
是UIPresentationController
的一个计算属性,这个计算属性需要返回一个false
,用来保留切换前的View,这样HudViewController
的背景就不会是一片黑。需要注意的是:UIPresentationController
并不是一个特定的ViewController,它只是提供了ViewController转换时的控制方法,可以理解为是一个抽象类。
最基础的自定义Controller切换其实已经搞定了,然后我们需要在之前定义的HudViewController
中调用这个自定义切换。当然是不能直接调用的,HudViewController
必须遵循UIViewControllerTransitioningDelegate
这个协议,实现协议的presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController)
方法,代码如下:
extension HudViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
最后一步,我们需要更改HudViewController
的初始化方法:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom
transitioningDelegate = self
}
初始化方法中,将UIViewController
的modalPresentationStyle
属性设置为自定义,即custom
,UIViewControllerTransitioningDelegate
的代理设置为self
自身,至此,我们已经完成了自定义弹出式View的基本雏形。
运行一下,看下效果:
很棒,讨厌的黑色背景消失了!
但是这仍然不是我们想要的!它的出现还是系统的Present Modally
效果!Of course,虽然我们自定义了Controller的切换方法类CustomPresentationController
,但是我们没有具体定义如何实现切换,类似于UIPresentationController
,我们还需要定义一个动画抽象类AnimationController
,代码如下:
import UIKit
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if let toViewController = transitionContext.viewController( forKey: UITransitionContextViewControllerKey.to), let toView = transitionContext.view( forKey: UITransitionContextViewKey.to) {
let containerView = transitionContext.containerView
toView.frame = transitionContext.finalFrame(for: toViewController)
containerView.addSubview(toView)
toView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
UIView.animateKeyframes( withDuration: transitionDuration(using: transitionContext), delay: 0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.334, animations: { toView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) })
UIView.addKeyframe(withRelativeStartTime: 0.334, relativeDuration: 0.333, animations: { toView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) })
UIView.addKeyframe(withRelativeStartTime: 0.666, relativeDuration: 0.333, animations: { toView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) })
}, completion: { finished in
transitionContext.completeTransition(finished) })
}
}
}
接下来简单分析下代码:
一个动画Controller抽象类需要继承自NSObject
并且遵循UIViewControllerAnimatedTransitioning
协议,这个协议有两个核心方法:transitionDuration
和animateTransition
。transitionDuration
决定了动画的显示时间,animateTransition
用来展示动画。在animateTransition
方法中,实现动画的思路非常清晰,下面我分条列举一下具体实现:
- 首先需要确定动画作用在哪里,通过
transitionContext
参数确定动画作用的ViewController,View和容器View,并且设置View的初始缩放参数; - 然后开始实现具体动画,该动画的类型为关键帧动画,关键帧动画可以在不同的阶段动画化视图,在本例中,各关键帧连续执行,每一帧动画占总动画时间的三分之一,缩放效果依次为0.7->1.2->0.9->1.0;
- 最后执行动画完成方法。
动画Controller定义结束后,需要将动画显示出来,需要更改ViewController切换的动画效果,需要实现UIViewControllerTransitioningDelegate
协议中的animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController)
方法,代码如下:
extension HudViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return AnimationController()
}
}
现在再运行一次,查看效果:
View的自定义关闭动画实现类似于弹出动画,此处不再阐述。
貌似还有一个问题,当View弹出时,背景View也会同时以相同的动画弹出,这不是我们想要的效果,一般来说,我们可以有两种解决方案:1.修改AnimationController
中toView
的frame
属性;2.自定义一个背景View。如果弹出的View本身大小固定而且结构简单,那么可以直接修改动画的作用范围,但是如果View有圆角,那第一种解决方案就不可以了,下面我们主要讲下第二种方法:
要使用自定义的背景View,首先需要将当前的背景View的背景色设置为透明:view.backgroundColor = UIColor.clear
;
然后在之前定义的Controller切换类中加入如下代码:
var backgroundView = BackgroundView(frame: CGRect.zero)
override func presentationTransitionWillBegin() {
backgroundView.frame = containerView!.bounds
containerView!.insertSubview(backgroundView, at: 0)
backgroundView.alpha = 0
if let coordinator = presentedViewController.transitionCoordinator {
coordinator.animate(alongsideTransition: { (_) in
self.backgroundView.alpha = 1
}, completion: nil)
}
}
presentationTransitionWillBegin()
方法在自定义切换即将开始时执行,BackgroundView
是我们的背景View自定义类,backgroundView
的大小和容器View的大小相等,然后容器View将backgroundView
添加为子View;
BackgroundView
中可以实现任意背景,这里我们仅仅做一个简单的示例:
import UIKit
class BackgroundView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.darkGray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.darkGray
}
}
再运行一次,nice,动画现在看起来只作用在弹出的View上,注意:只是看起来,本质上现在有两个背景View,其中默认的背景色为透明,所以我们看不到,我们看到的,只是一个自定义的背景。
当然我们还可以做一些其它的优化,比如背景淡入淡出等,限于本文主题不再详细阐述,大家有兴趣可以自行查阅资料或者参考我的完整项目。
总结
两种方法都各有优劣,使用Controller来实现弹出式View确实体现出了高度的自定义,你可以自定义View,自定义背景View,自定义出现及消失效果,你可以用炫酷的动画和变换来使你的App更加独特;至于第一种,如果你的弹出式View比较简单的话可以选择,毕竟实现起来也更加方便,在实际项目中我们可以根据需求来选择不同的方法。
完整的项目参考,大家可以在我的github上找到。
最后,这只是我平时学习iOS的一点小小的心得,如有错误之处欢迎大家批评指正,共同进步。