谈谈Swift面向协议编程

从一个具体需求说起

应用中有多个页面内的UICollectionViewCell需要实现一个相同的小动画:被选中时,先缩小到原来的0.8倍,再回弹到0.9倍。动画本身实现起来不难:

    func selectWithBounce(select:Bool, animated:Bool = true){
        let bounce = CAKeyframeAnimation(keyPath: "transform")
        
        let origin = CATransform3DIdentity
        let smallest = CATransform3DMakeScale(0.8, 0.8, 1)
        let small = CATransform3DMakeScale(0.9, 0.9, 1)
        
        let originValue = NSValue(CATransform3D: origin)
        let smallestValue = NSValue(CATransform3D:smallest)
        let smallValue = NSValue(CATransform3D:small)
        
        if animated {
            bounce.duration = 0.2
            bounce.removedOnCompletion = false
            if select {
                bounce.values = [originValue, smallestValue, smallValue]
                self.layer.addAnimation(bounce, forKey: "bounce")
            }else{
                bounce.values = [smallestValue, originValue]
                self.layer.addAnimation(bounce, forKey: "bounce")
            }
        }
        if select {
            self.layer.transform = small
        }else{
            self.layer.transform = origin
        }
    }

然而不同的页面有不同的UICollectionViewCell子类,怎样方便地让它们都能复用这个动画实现呢?

面向对象的复用方式

  • 继承

如果用面向对象的思维解决问题,最容易想到的就是定义个一个继承自UICollectionViewCell的类,比如叫MYCollectionViewCell,实现这个动画,然后所有需要这个动画的cell都继承它。

能解决问题,但缺点也很明显:继承很容易带来耦合。

比如,现在需要给UICollectionViewCell再增加一个功能,这个功能的接口声明了依赖ABC另外三个类。Swift/Objective-C只能单继承,如果把新的功能实现也放到这个MYCollectionViewCell里,就引入了不必要的耦合。这时我们想用这个动画功能,依赖MYCollectionViewCell的同时也跟着依赖了和动画毫无关系的ABC三个类。代码就开始僵化了。

有的项目里定义了继承UIViewController的父类,实现了很多功能,要求项目里所有的页面都要继承它。这种僵化的毛病就很明显了,下面的子类代码全都依赖这个父类,想抽出来复用非常难。而且往这个自定义UIViewController里塞代码实在太方便了,这个类很容易随着功能迭代逐渐膨胀,越来越难以维护。

而且,假如一个UITableViewCell也需要这个功能,使用继承无法实现。UITableViewCellUICollectionViewCell已经是UIView的不同子类,除非改动UIView,否则无法同时给它们增加功能。然而我们拿不到UIView的实现文件。

  • Extension/Category

严格意义上讲这个不能算典型的面向对象,而是Swift/Objective-C特有的功能。使用它,可以不修改类的实现文件的情况下,给它增加新的方法。我们恰好可以通过给UIView添加category解决UITableViewCellUICollectionViewCell同时增加功能的问题。缺点也是Extension/Category固有的,给一个类加上这东西会污染所有的对象,即使他们根本不需要这个功能。对于Objective-C,即使在一个文件中没有 import这个category,使用runtime还是可以访问到category中的方法的。

  • 组合

组合优于继承这是老生常谈了,用在这里就要定义一个类专门负责做这个动画,然后把layer作为方法参数或者成员变量传进来。缺点嘛,就是有点麻烦。这种小工具类多了,要写很多胶水代码。

面向协议的复用方式

协议扩展这个特性的引入,使得Swift支持了一种新的编程范式:面向协议编程。针对某个要实现的功能,我们可以使用协议定义出接口,然后利用协议扩展提供默认的实现。

上代码:

protocol BounceSelect {
    func selectWithBounce(select:Bool, animated:Bool)
}

extension BounceSelect where Self:UIView {
    
    func selectWithBounce(select:Bool, animated:Bool = true){
        let bounce = CAKeyframeAnimation(keyPath: "transform")
        
        let origin = CATransform3DIdentity
        let smallest = CATransform3DMakeScale(0.8, 0.8, 1)
        let small = CATransform3DMakeScale(0.9, 0.9, 1)
        
        let originValue = NSValue(CATransform3D: origin)
        let smallestValue = NSValue(CATransform3D:smallest)
        let smallValue = NSValue(CATransform3D:small)
        
        if animated {
            bounce.duration = 0.2
            bounce.removedOnCompletion = false
            if select {
                bounce.values = [originValue, smallestValue, smallValue]
                self.layer.addAnimation(bounce, forKey: "bounce")
            }else{
                bounce.values = [smallestValue, originValue]
                self.layer.addAnimation(bounce, forKey: "bounce")
            }
        }
        if select {
            self.layer.transform = small
        }else{
            self.layer.transform = origin
        }
    }
}

这样,一个UICollectionViewCellUITableViewCell需要这个功能,只需要声明遵守了这个协议即可,其他什么都不用做,直接就可以调用func selectWithBounce(select:Bool, animated:Bool)这个方法:

class XYZCollectionViewCell : UICollectionViewCell, BounceSelect {
    ...
}

let cell: XYZCollectionViewCell
cell.selectWithBounce(true)

遵守某个协议的类的对象调用协议声明的方法时,如果类本身没有提供实现,协议扩展提供的默认实现会被调用。

事实上,我们还可以进一步解除耦合。这个方法依赖的只有layer,不一定要是UIView,还可以是NSView,甚至可以是CALayer本身。

另外我们可以允许用户自定义动画的时长,并提供默认的时长。

稍微改下代码:

protocol BounceSelect {
    var layer:CALayer {get}
    var animationDuration:NSTimeInterval {get}
    func selectWithBounce(select:Bool, animated:Bool)
    
}

extension BounceSelect {
    
    var animationDuration:NSTimeInterval {
        return 0.2
    }
    
    func selectWithBounce(select:Bool, animated:Bool = true){
        let bounce = CAKeyframeAnimation(keyPath: "transform")
        
        let origin = CATransform3DIdentity
        let smallest = CATransform3DMakeScale(0.8, 0.8, 1)
        let small = CATransform3DMakeScale(0.9, 0.9, 1)
        
        let originValue = NSValue(CATransform3D: origin)
        let smallestValue = NSValue(CATransform3D:smallest)
        let smallValue = NSValue(CATransform3D:small)
        
        if animated {
            bounce.duration = animationDuration
            bounce.removedOnCompletion = false
            if select {
                bounce.values = [originValue, smallestValue, smallValue]
                self.layer.addAnimation(bounce, forKey: "bounce")
            }else{
                bounce.values = [smallestValue, originValue]
                self.layer.addAnimation(bounce, forKey: "bounce")
            }
        }
        if select {
            self.layer.transform = small
        }else{
            self.layer.transform = origin
        }
    }
}

这个协议通过var layer:CALayer {get}定义了接入的条件,通过func selectWithBounce(select:Bool, animated:Bool)方法的声明和实现,定义了它的功能。它就像一个插件,只要提供一个 CALayer 属性,任何类都可以方便地接入这个功能。这样就可以在不同的类之间复用这段代码了,解除了对类型的依赖。这是继承和
Extension/Category都无法做到的。

如果是CALayer本身呢?很简单:

class XYZLayer : CALayer, BounceSelect {

    //如果想修改动画时长就重载这个属性
    var animationDuration:NSTimeInterval {
        return 0.3
    }
    var layer:CALayer {
        return self
    }
}

面向协议编程的好处在于,通过协议+扩展实现一个功能,能够定义所需要的充分必要条件,不多也不少。这样就最大程度减少了耦合。使用者可以像搭积木一样随意组合这些协议,写一个class或struct来完成复杂的功能。实际上,Swift的标准库几乎是everything is starting out as a protocol。

传统的协议(比如Objective-C的protocol,Java的Interface)只能定义接口,不能复用实现,遵守同一个协议的不同的类,只能分别实现协议接口,使用场景受限了很多。Swift只是多了一个协议扩展的特性,但却带来了编程范式的进化。

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 前言: 最近看到很多大神都写过关于Swift 面向协议开发的文章,受益匪浅,加上最近工作不是很多,潜下心来好好搞了...
    Buddha_like阅读 823评论 0 3
  • 到底是寒冬让人退萎 躲进厚厚的大袄棉裤 连精神都随冷风散去 仅留一身鸡母皮惊悚 潮湿粘腻歪的回南天 蒙着濡湿奶白色...
    kelly_M阅读 311评论 0 0
  • 生活百态万千变化,无论你经历的哪件事情相信都会给你带来一定的收获,不止酸甜 不止苦辣 甚至是说幸运。 生活...
    安放这颗心阅读 350评论 0 0
  • 星期一 天气 : 阴 2017年6月5日 芒种芒种,样样都中。 诗句理解 时雨 这首诗很好理解,讲的是芒种时的...
    Euphoria_阅读 539评论 2 5