Fork me on GitHub

Swift 面向协议编程/组件化

得益于面向对象语言的特性 (封装、继承、多态) 在我们熟悉的设计模式中渐渐形成统一的软件开发思想,但是由于OC的局限性, 使得iOS开发组件化编程变得十分繁琐,需要将一些功能拆分为类,在抽取某些功能作为基类的不断运用中,代码的可移植性逐渐减弱。就如同一棵树,从主干到各个分支,每个分支再长成细枝末叶。代码的耦合性也相应增加。随着苹果 swift 语言的推出,对于传统OC 语言取其精华,弃其糟粕。 加上开源社区众大神的齐力维护。逐渐成为一门非常优秀的高级语言。其中Swift中的面向协议编程思想就是其中很有代表性的一项优秀组成部分。

Swift的 POP 是使用了继承的思想,它模拟了多继承关系,实现了代码的跨父类复用,同时也不存在 is-a 关系。swift中主类和 extension扩展类 的协同工作,保留了 在主类中定义方法 在所继承的类中进行实现的特性,又新增了在 extension拓展类 中定义并实现的方法在任何继承自此协议的类中可以任意调用,从而实现组件化编程。

两个简单的使用场景可以充分体现swift组件化编程的优势:

1.实现一个能抖动的 View

让我们假设你的产品经理过来和你说,“我们在点击那个按钮时候出现一个视图,而且它会抖动。” 这是一个非常常见的动画,比如,在你的密码输入框上 – 当用户输入错误密码时,它就会抖动。

我们常常都是从 Stack Overflow 开始的(笑)。一些人可能已经有了 Swift 抖动对象的基础代码。一些人甚至都有 Swift 的抖动对象的代码,我想都不用想,只要稍稍修改一下。最难的部分当然是架构:我在哪里集成这些代码呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  FoodImageView.swift

import UIKit

class FoodImageView: UIImageView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

我将创建一个 UIImageView 的子类,创建我的 FoodImageView 然后增加一个抖动的动画:

1
2
3
4
5
6
7
8
9
10
11
12
//  ViewController.swift

import UIKit

class ViewController: UIViewController {

@IBOutlet weak var foodImageView: FoodImageView!

@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
}
}

在我的 view controller 里面,在 interface builder 里我连接我的 view,把它做为 FoodImageView 的子类,我有一个 shake 函数,然后 完成了!。10 分钟我就完成了这个功能。我很开心,我的代码工作得很正常。

然后,你的产品经理过来说,”你需要在抖动视图的时候抖动按钮。” 然后我回去对按钮做了同样的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  ShakeableButton.swift

import UIKit

class ActionButton: UIButton {

func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

新建UIImageView子类,创建一个按钮,增加一个 shake() 函数,和我的 ViewController。现在我能抖动我的 foodImageView 和 Button 了,完成了。

1
2
3
4
5
6
7
8
9
10
11
12
//  ViewController.swift

class ViewController: UIViewController {

@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!

@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}

幸运的是,这会给你一个警告:我在两个地方重复了抖动的代码。如果我想改变抖动的幅度,我需要改两处代码,这很不好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  UIViewExtension.swift

import UIKit

extension UIView {

func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

作为一个优秀的程序员,我们马上会意识到这点,而且试图重构。如果你以前使用过 Objective-C,我会创建一个 UIView 的类别,在 Swift 里面,这就是扩展。

我能这样做,因为 UIButton 和 UIImageView 都是 UI 视图。我能扩展 UI 视图而且增加一个 shake 函数。现在我仍然可以给我的按钮和图像视图都加上其他的逻辑,但是 shake 函数就到处都是了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FoodImageView: UIImageView {
// other customization here
}

class ActionButton: UIButton {
// other customization here
}

class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!

@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}

马上我们就能发现可读性很差了。例如,对于 foodImageView 和 actionButton 来说,你看不出来任何抖动的意图。整个类里面没有任何东西能告诉你它需要抖动。这样表意不明确,因为别处可能会随机存在一个抖动函数,你甚至不知道它是从哪里来的。

最主要的是,如果你常常为类别和 UIView 的扩展这样做的话,你可能会有更好的办法。在你增加了 shake 函数的地方,就成了所谓的 科学怪人的垃圾场。然后有人来和你说, “我想要一个可调暗的视图”,然后你增加一个 dim 函数和其他别处随机的调用函数。这样,代码文件文件就会变成一个很长的垃圾文件,因为这些随机调用的事情都是在 UI View 里面完成,尽管有些时候也许只有一两个地方需要这么做,这导致代码变得不可读,难以查看。我们该如何改变这点呢?

这是一次面向协议编程的演讲,我们当然会用到协议。首先我们创建一个 Shakeable 的协议:

1
2
3
4
5
6
7
8
9
10
11
12
//  Shakeable.swift

import UIKit

protocol Shakeable { }

extension Shakeable where Self: UIView {

func shake() {
// implementation code
}
}

在协议扩展的帮助下,你可以把它们限制在一个特定的类里面。在这个例子里面,我能抽出我的 shake 函数,然后用类别,我能说这是我们需要遵循的唯一的东西,只有 UI 视图会有这个函数。

你仍然可以使用你原来想用的同样强大的扩展功能,但是你有协议了。任何遵循协议的非视图不会工作。只有视图才能有这个 shake 的默认实现。

1
2
3
4
5
6
7
class FoodImageView: UIImageView, Shakeable {

}

class ActionButton: UIButton, Shakeable {

}

我们可以看到 FoodImageView 和 ActionButton 会遵循 Shakeable 协议。它们会有 shake 函数,现在的可读性强多了 –- 我可以理解 shaking 是有意存在的。如果你在别处使用视图,我需要想想,”在这也需要抖动吗?”。它增强了可读性,但是代码还是闭合的和可重用的。

假设我们想抖动和调暗视图。我们会有另外一个协议,一个 Dimmable 协议,然后我们可以为了调暗做一个协议扩展。再强调一遍,通过看类的定义来知晓这个类的用途,这样意图就会很明显了。

1
2
3
class FoodImageView: UIImageView, Shakeable, Dimmable {

}

关于重构,当产品经理说 “我不想要抖动了” 的时候,你只需要删除相关的 Shakeable 协议就好了。

1
2
3
class FoodImageView: UIImageView, Dimmable {

}

现在它只能调暗了。功能插拔是非常容易的,通过使用协议我们很容易获得乐高似的架构。如果你想学习使用协议的其他更强大的使用方式,可以看看这篇文章 ,然后尝试创建一个有过渡效果的可调暗的视图。

2.xib 加载 UIView

我们使用XIB加载的自定义View都会使用相同的方法:

1
Bundle.main.loadNibNamed("RedView", owner: nil, options: nil)?.last

通常在自定义View的实现中定义一个类方法。
例如:

1
2
3
class func loadWithNib() -> RedView {
return Bundle.main.loadNibNamed("RedView", owner: nil, options: nil)?.last! as! RedView
}

如果我们定义一个可从XIB加载某类的协议,在自定义类中仅仅需要遵守这个协议,无需实现协议中的方法,便可实现此协议中实现的所有方法.例如定义一个 可从XIB加载的协议 NibLoadable。

1
2
3
4
5
6
7
8
9
10
11
import Foundation

protocol NibLoadable {

}

extension NibLoadable where Self: UIView {
static func loadViewWithNib() -> Self {
return Bundle.main.loadNibNamed("\(self)", owner: nil, options: nil)?.last! as! Self
}
}

只要在xib加载的类中继承此协议。

1
2
3
4
5
import UIKit

class GreenView: UIView , NibLoadable {

}

就可以在需要初始化此对象时直接调用。

1
GreenView.loadViewWithNib()

写在最后,虽然Swift已经十分强大,但是技术毕竟是要为业务服务。缺少了运行时特性,也是很多项目无法从OC完全迁移到Swift的原因之一,所以在开发项目的时候需要慎重选择OC、Swift,或者在业务更新不是十分频繁的时候也可以选择OC与Swift混编(业务更新频繁的时候并不建议这么做,遇到坑耗时导致项目delay毕竟不是好事,OC虽老,尚能再战),希望今年即将发布的Swift5能够给我们带来惊喜

参考文章:

iOS-Swift 面向协议编程/组件化(模块化)编程思想
基于 Swift 的面向协议编程
从 Swift 的面向协议编程说开去
Swift面向协议编程(附代码)
Swift protocol extension method dispatch

------------- 本文结束感谢您的阅读 -------------