像AlertView一样自定义一个弹出式View

这两天项目中有一个点击cell需要弹出详细信息的需求,像下图这样:

弹出式View.png

我最初的想法是像实现hudView一样通过自定义一个View来实现,后来通过查阅资料了解到还可以自定义一个Controller,类似苹果官方的AlertViewController,然后通过自定义转场来实现,而且后者自定义的功能更加强大,话不多说,上手撸一拨代码就很清楚了。

先来第一种方法,通过自定义一个View来实现:

新建一个项目,在ViewController中添加两个button,测试不同方法对应的效果,如下图:

两个button.png

既然是自定义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方法);

其次我们需要重写UIViewdraw方法,draw方法用来将View绘制到屏幕上,我们已经自定义了一个hudView,然而却不知道应该将其绘制到哪里,此时就需要用到draw方法,在这个方法中,我们使用CGRect结构体定义了hudView的x轴,y轴的位置以及大小。如果要在View上添加text,那么还需要对text进行绘制,方法类似,需要设定text的字体样式和位置,然后执行textdraw方法。需要注意的是:hudView目前是一个矩形,我们可以新建一个UIBezierPath对象roundedRect使得边界更平滑。UIColorsetFill方法用来填充颜色,roundedRectfill方法用来填充界面以完成绘制;

最后是show方法,这个用来展示hudView出现时的动画,这个也很简单,定义动画前的透明度、缩放程度和动画后的透明度、缩放程度,然后通过UIViewanimate方法实现。需要注意的是:animate方法是一个多态方法,不同的动画效果对应该方法中不同的参数,有兴趣的读者可以自行查阅。

至此,我们已经完成了HudView类的创建,需要使用的时候直接拿来调用就好了,比如像下面这样:

let hudView = HudView.hud(inView: self.view, animated: true)
hudView.text = "这是一个Pop-up View"

这里就能看出类方法作为便利构造器的方便,否则我们还需要在当前Controller中实现添加hudView的逻辑,动画方法也需要写在当前Controller中,虽然不影响功能,但那不是一种好的编程方式。

此方法效果如下图:

第一种方法效果图.png

第二种方法稍微有些复杂,但是可自定义化的东西更多,适合更复杂的自定义需求:

  • 第一种方法是基于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%的白色;

到现在准备工作基本完成(准备工作就比第一个复杂了太多)。

现在我们可以跑一下看看效果,差不多应该是这样:

第二种方法第一次尝试.png

我们会发现整个背景现在都是黑色的,然而之前明明已经设置了背景是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切换的逻辑,shouldRemovePresentersViewUIPresentationController的一个计算属性,这个计算属性需要返回一个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
    }

初始化方法中,将UIViewControllermodalPresentationStyle属性设置为自定义,即customUIViewControllerTransitioningDelegate的代理设置为self自身,至此,我们已经完成了自定义弹出式View的基本雏形。

运行一下,看下效果:

第二种方法雏形.png

很棒,讨厌的黑色背景消失了!

但是这仍然不是我们想要的!它的出现还是系统的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协议,这个协议有两个核心方法:transitionDurationanimateTransitiontransitionDuration决定了动画的显示时间,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()
    }
}

现在再运行一次,查看效果:

关键帧动画Pop-up View.gif

View的自定义关闭动画实现类似于弹出动画,此处不再阐述。

貌似还有一个问题,当View弹出时,背景View也会同时以相同的动画弹出,这不是我们想要的效果,一般来说,我们可以有两种解决方案:1.修改AnimationControllertoViewframe属性;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,其中默认的背景色为透明,所以我们看不到,我们看到的,只是一个自定义的背景。

Pop-up-gif.gif

当然我们还可以做一些其它的优化,比如背景淡入淡出等,限于本文主题不再详细阐述,大家有兴趣可以自行查阅资料或者参考我的完整项目

总结

两种方法都各有优劣,使用Controller来实现弹出式View确实体现出了高度的自定义,你可以自定义View,自定义背景View,自定义出现及消失效果,你可以用炫酷的动画和变换来使你的App更加独特;至于第一种,如果你的弹出式View比较简单的话可以选择,毕竟实现起来也更加方便,在实际项目中我们可以根据需求来选择不同的方法。

完整的项目参考,大家可以在我的github上找到。

最后,这只是我平时学习iOS的一点小小的心得,如有错误之处欢迎大家批评指正,共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,086评论 4 62
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,997评论 25 707
  • 寒衣节 落叶萧萧归旧痕,花尽枝枯映重门。 山寂水寒迢递梦,云澹风清杳缈魂。 奉衣殷情寄幽冥,未觉霜露泪沾襟。 尘寰...
    文抒苑阅读 1,050评论 2 10
  • 人到三十,看着身边那群一起翘课、喝酒,半夜对着对面宿舍的女生吹口哨的老同学,突然就结婚生子,突然就买车买房了,而你...
    莫主编阅读 225评论 0 1
  • 小蛮姐/文 看多了明星出轨的新闻,朋友在我耳边恨恨地嘟囔着:“XXX这么好,他还出去打野食,难道得不到的永远都是最...
    小蛮姐阅读 462评论 0 0