Swift 【基于 Swift 面向协议编程】
“我们如何在每天的开发过程中使用面向协议编程?Natasha 回答了这个问题,并专门针对 POP 的实际应用开发给出了解决方案,包含视图,视图控制器和网络的实例。关注本篇在App Builders CH大会上的演讲,你将从面向对象编程转向面向协议编程,这样能使你的 Swift 编程更加清晰、更加易读! 回到现实 – 我们假设 Swift 是最棒的编程语言。 今天,我将谈谈基于 Swift 的面向协议编程,我会侧重在如何实现上。让我们称它POP。 基于 Swift 的面向协议编程(00:37)当 Swift 刚刚出现的时候,学习新东西都是令人兴奋的。第一年,我很高兴能学习它,我之前在 Swift 里面使用我的 Objective C 代码 (有的时候用些值类型和更加有趣的东西)。但是直到去年的 WWDC,协议扩展出现了。 Dave Abrahams (让你大开眼界的教授) 做了一次令人大开眼界的演讲“基于 Swift 的面向协议编程”。他声称 “Swift 就是一个面向协议的编程语言。” 如果你看看 Swift 的标准库,那有超过 50 个协议。这就是这门语言的成形之处,它使用了许多的协议而且这也是我们想借鉴的地方。Dave 还给了一个如何使用协议来改进我们现有代码的例子。他使用了 drawables 的例子,比如正方形、三角形,圆形。使用协议能够让它们的实现变得特别令人吃惊。我是被震撼到了,但是对于我来说我却无法直接使用,因为我在每天的工作中不使用 drawables。 回去以后,我冥思苦想,我该如何在每天的程序中使用面向协议编程呢。我们都有些从 Objective-C 和其他编程语言继承下来的编程模式,所以从面向对象转变到面向协议是一件很难的事情。 实践 POP!(03:05)过去一年,我终于有机会实验一下使用协议,我想分享些我改进代码的例子。因为这是实践面向协议编程,我将会讲到 Views(03:24)让我们假设你的产品经理过来和你说,“我们???在点击那个按钮时候出现一个视图,而且它会抖动。” 这是一个非常常见的动画,比如,在你的密码输入框上 – 当用户输入???错误密码时,它就会抖动。 我们常常都是从 Stack Overflow 开始的(笑)。一些人可能已经有了 Swift 抖动对象的基础代码。一些人甚至都有 Swift 的抖动对象的代码,我想都不用想,只要稍稍修改一下。最难的部分当然是架构:我在哪里集成这些代码呢? // FoodImageView.swift
import UIKit
class FoodImageView: UIImageView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
repeatCount = 5
autoreverses = true
fromValue NSValue(CGPointCGPointMake(selfcenterx - 4.0, y))
toValue + layer.addAnimation(animationforKey)
}
}
我将创建一个 // ViewController.swift
ViewControllerUIViewController {
@IBOutlet weak var foodImageViewFoodImageView!
@IBAction onShakeButtonTapsenderAnyObject) {
foodImageView()
在我的 view controller 里面,在 interface builder 里我连接我的 view,把它做为 FoodImageView 的子类,我有一个 shake 函数,然后完成了!。10 分钟我就完成了这个功能。我很开心,我的代码工作得很正常。
然后,你的产品经理过来说,”你需要在抖动视图的时候抖动按钮。” 然后我回去对按钮做了同样的事情。 // ShakeableButton.swift
ActionButtonUIButton }
子类,创建一个按钮,增加一个 !
actionButtonActionButton{
()
actionButton幸运的是,这会给你一个警告:我在两个地方重复了抖动的代码。如果我想改变抖动的幅度,我需要改两处代码,这很不好。
// UIViewExtension.swift
extension UIView 作为一个优秀的程序员,我们马上会意识到这点,而且试图重构。如果你以前使用过 Objective-C,我会创建一个 UIView 的类别,在 Swift 里面,这就是扩展。
我能这样做,因为 {
// other customization here
}
()
马上我们就能发现可读性很差了。例如,对于 foodImageView 和actionButton 来说,你看不出来任何抖动的意图。整个类里面没有任何东西能告诉你它需要抖动。这不清楚,是因为别处会随机存在一个抖动函数。
如果你常常为类别和 UI view 的扩展这样做的话,你可能会有更好的办法。这就是所谓的科学怪人的垃圾地点,你增加了一个 shake 函数然后有人来和你说, “我想要一个可调暗的视图”。然后你增加一个 dim 函数和其他别处随机的调用函数。这样,文件会变得越来越长,不可读,很难找到垃圾,因为这些随机调用的事情都可以在 UI 视图里面完成,尽管有些时候也许只有一两个地方需要这么做。 意图是什么并不清晰。我们如何改变这点呢? 这是一次面向协议编程的演讲,我们当然会用到协议。让我们创建一个 // Shakeable.swift
protocol Shakeable { Shakeable where Self{
// implementation code
在协议扩展的帮助下,你可以把它们限制在一个特定的类里面。在这个例子里面,我能抽出我的 shake 函数,然后用类别,我能说这是我们需要遵循的唯一的东西,只有 UI 视图会有这个函数。
你仍然可以使用你原来想用的同样强大的扩展功能,但是你有协议了。任何遵循协议的非视图不会工作。只有视图才能有这个 shake 的默认实现。 UIImageView{
UIButton我们可以看到 FoodImageView 和ActionButton 会遵循Shakeable 协议。它们会有 shake 函数,现在的可读性强多了 –- 我可以理解 shaking 是有意存在的。如果你在别处使用视图,我需要想想,”在这也需要抖动吗?”。它增强了可读性,但是代码还是闭合的和可重用的。
假设我们想抖动和调暗视图。我们会有另外一个协议,一个 ShakeableDimmable 关于重构,当我们说 “我不想要抖动了”的时候,你只需要删除相关的 Shakeable 协议就好了。
现在它只能调暗了。插入是非常容易的,通过使用协议我们很容易获得乐高似的架构。看看这篇文章如果你想学习使用协议的其他更强大的方式的话,试试创建一个有过渡效果的可调暗的视图。
现在我们高兴了,可以去吃 Pop-tarts 了。 (UITable)ViewControllers(10:09)这是一个应用,Instagram 食物:它给你展示不同地点的美食照片。 // FoodLaLaViewController
override viewDidLoad{
super()
foodCellNib UINibNibName"FoodTableViewCell"bundle: nil)
tableViewregisterNibfoodCellNibforCellReuseIdentifier)
这是一个 tableView 。这是我们一直都会编写的基础代码。当视图加载的时候,我们会从Nib 中加载 cell;我们定制NibName ,然后我们使用一个 ReuseIdentifier 来注册Nib 。
StringFoodTableViewCell),63);">)
))
不幸的是,因为 UIKit 创建方式的限制,我们不得不使用字符串。我喜欢为我的 cell 使用相同的 identifiers 来作为 cell 的名字。 我们立刻就能看到低效的地方。如果你以前使用的是 Objective-C,我常常使用 ReusableView: class {}
ReusableView static reuseIdentifierString return self}
UITableViewCellReusableView }
FoodTableViewCellreuseIdentifier
// FoodTableViewCell
因为我们不再使用 Objective-C 了,我们可以为这些 cell 重用视图协议。 )
NibLoadableViewNibLoadableView NibName再说一次,表格视图里面每一个单独的复用 identifier 都会变成类的字符串版本。我们可以对每一个视图使用协议扩展。这对 UICollectionView UITableView Cell 也适用。这是我们的可复用的 identifier。我们可以把这个不得不用的讨厌逻辑封装起来. 因为UIKit 需要它。现在我们能扩展每一个单独的UITableViewCell 了。
NibLoadableView .NibName
// "FoodTableViewCell"
我们可以对 这依旧很长,但是更易读: UITableView func register<TUITableViewCell where NibLoadableView>_T.Type{
Nib )
Nib我们也可以对 NibName 做同样的事情,因为我们不想处理字符串。我们能创建一个NibLoadableView (任何能从Nib 里加载的类)。我们会有一个NibName ,而且它会返回类名的字符串版本。
)
如何从 我们现在能再进一步,使用泛型来注册我们的 cells,然后提取这两行代码。 register我们可以扩展我们的 tableView 然后创建一个注册类,这个类可以接收一个类型包含这两种协议。它有一个可重用的标识符和一个从那些协议里面获取的Nib 名字。现在我们可以完整地从遵循NibLoadableView 要求的Nib 名字的位置,抽取这两行代码的逻辑出来。我们知道它有一个叫做NibName 的属性,而且 cell 会遵循可重用的视图协议 (它们会有可重用的标识符属性)。这两行代码,本来我们需要在每一个单独的表格视图里面都输入一遍,现在被抽取出来了。只需要一行代码,我们就完成 cell 的注册,这看起来会干净许多。你不需要再处理字符串了。
我们可以更进一步。我们不得不注册 cells,我们也不得不清理 cells。我们可以用泛型和协议来代替这些本来很丑的代码:当你需要清理的时候,你需要指明 dequeueReusableCellReusableViewforIndexPath indexPathNSIndexPath) -> T guard cell = dequeueReusableCellWithIdentifierforIndexPath: ) as? T else {
fatalError("Could not dequeue cell with identifier: (reuseIdentifier)")
}
return cell
当你清理一个 cell 的时候,你需要这个保证声明,这是一个 cell 的清理;如果没有这个声明,你或者有一个严重的错误,或者一个 explicit unwrapping。
= (“FoodTableViewCell",forIndexPath: indexPath) as? FoodTableViewCell else { fatalError("Could not dequeue cell with identifier") }
当你输入这行代码的时候,你都会觉得丑陋。它源自 Objective-C,我们从 UIKit 里面开始有它,我们对它没有太多的办法。但是使用协议,我们可以抽取这些丑陋的地方,因为我们对每一个单独的表格视图 cell 都有 dequeueReusableCellas FoodTableViewCell
我们能如下实现上面这段代码的功能: if indexPathrow == 0 DesertTableViewCell
}
每一次我们清理一个 cell,我们都能这样做。我们在 forIndexPath 里清理 cell,而且我们说明 cell 是哪个。如果你有多个 cells,你可以把它转换成你注册的那个 cell,它马上就会知道它的类型是什么了。
这太神奇了!这是个替代原来我们在 Objective-C 里方式的好方法,这个方法混合了 Swift 和 optionals,并采用了协议和泛型,给我们的项目带来更好看的代码。 iOS Cell 注册 & 用 Swift 协议扩展和泛型来实现复用(17:28)这部分源自Guille Gonzalez,他把这个原则???用到 collection view 上,你也可以把这个方法运用到其他你有问题的 UIKit 的组件上,例如Swift 中面向协议的 Segue 标识符。你可以在每天的编程中都像那样使用协议,这样也会安全些。它也是源自 Apple 去年 WWDC 上的例子。面向协议编程真的很棒。 网络(18:25)使用网络的时候???你一般要调用 API。 我常常这样做:我有一些服务 (比如 我从服务器那获取食物),我有一个 struct FoodService getcompletionHandlerResult<[Food]> Void// make asynchronous API call
// and return appropriate result
我将使用结果枚举,这是在Swift里面源自 Haskel 的常见模式。
结果枚举很简单。当服务器返回结果的时候,我们能把它解析为成功然后返回一个食物条目的数组。如果失败了,我们能返回一个错误码,然后完成句柄中的 view controller 会知道如何处理这些情况。 enum > case Success)
FailureErrorType当服务器异步返回结果的时候,这使用了我们的完成句柄,我们将传入食物条目的结果。
现在 view controller 将会解析它们。我们在 view controller 里有一个 dataSource 。当视图加载的时候,我们将调用异步 API,然后再完成句柄中得到结果。如果结果是一组食物,太棒了:我们重置数据,重新加载表格视图。如果结果是个错误,我们会给用户一个错误提示,然后处理它。
dataSource = ]() didSet reloadData}
()
getFood()
private {
FoodService()[] result in
switch result case food):
?food
errorshowError}
这是一个典型的调用 API 的模式。但是整个 view controller 依赖上食物数组的加载了:如果没有数据或者数据错误,它会失败。确认 view controller 是按预期正确处理了数据的最好方式是……测试。
View Controller 测试?!!!(20:54)View Controller 测试很痛苦。在这个例子中,因为我们有了服务,异步 API 调用,一个完成代码块,和一些结果枚举,测试就会更加痛苦。这些都使得测试 view controller 是否按预期工作变得更加困难。 首先,我们需要对 food 服务有更多的控制;我们需要能够给它注入一个食物的数组或者一个错误。我们能看到问题了:当 getFood() 实例化一个 food 服务的时候,我们的测试没有机会能注入。第一个测试是增加依赖注入。
// FoodLaLaViewController
fromService service{
servicein
// handle result
// FoodLaLaViewControllerTests
testFetchFood{
viewControllerfromService())
// now what?
现在我们的 getFood() 函数接收FoodService 参数,这样我们就有了更多的控制权了,之后我们才能做更多的测试。我们有 controller,叫做getFood 函数,然后我们给它传入FoodService 。当然,我们想要对于FoodService 完整的控制。我们如何实现呢?
这是一个值类型:你不能有子类。相反,你需要用协议。 FoodService 有一个 get 函数,completionHandler 会给出结果。你可以想象你应用里面的每一个服务,每一个 API 调用都需要一个get 函数 (比如 dessert),也会有类似的东西。它有一个完成句柄能够接收结果,然后解析它。
我们马上能让它变得更通用: Gettable associatedtype T
我们能使用协议和相关的类型 (Swift 里面使用泛型的方式)。我们说每一个遵循 Gettable 协议的地方都有 get 函数,而且它接收一个完成句柄和这个类型的结果。在我们的例子中,这会是 food (但是在 dessert 服务中,它会是 dessert;这是能互相交换的)。
回到 food 服务,唯一的改变就是它需要遵循 Gettable 协议。 get 函数已经实现了。它只需要接收一个completionHandler ,这个句柄接收结果的条目……因为相关类型的协议是智能的 (结果是 food 数组,相关类型就是 food 数组)。你不需要描述它。
回到 view controller,这基本上就是一样的了。 ())
getFoodSGettable ST == in
唯一的区别就是你需要的相关类型只能是 food 数组 (你可不希望我们的 food view controller 调用 dessert 服务)。你想限制它,然后说明这个 getFood() 函数只能获得 food 条目的结果。否则,它就是其他遵循Gettable 协议的东西。这使得我们能对传入的,诸如FoodService 的参数有更强的控制 – 因为它不需要一定是FoodService ,我们能注入其他的东西。
在我的测试中,我们创建了一个 Fake_FoodService{
getWasCalled false
true
completionHandler))
它遵循 Gettable ,它接收 food 的结果,然后返回一个 food 数组。因为这是测试,我们想确定Gettable 的get 函数被调用了,因为它能返回,而且函数理论上可以分配一个从任意地方获取的 food 条目的数组。我们需要保证它被调用到;在这个例子里面是成功的例子,但是你能通过注入失败来完成同样的对 view controller 的测试,来保证你的 view controller 的行为在你的输入结果的条件下是正常的。测试如下:
fakeFoodService ()
fakeFoodService)
XCTAssertTruefakeFoodServicegetWasCalled)
XCTAssertEqualdataSourcecountfooddataSource我们有 fakeFoodService :我们能注入我们的fakeFoodService (这个我们有更强的控制力),而且我们能测试get 函数能被调用到,而且通过我们的FoodService 注入的数据源和我们赋给 view controller 的数据源是同组数据。通过增加Gettable 协议,我们有了一个对 view controller 的强大测试,我们有了对所有服务的测试框架。我们能实现一个可删除的,可更新的,可创建的协议;关于服务,你能马上看出哪个函数需要被实现而且容易注入,然后测试它们。
我用协议写了一个注入 storyboards 的例子,而且我强烈推荐 Alexis Gallagher 的这篇演讲相关类型的协议. 我简化了它,但是相关类型的协议也会常常出乎意料。使用它时,你可能会感到沮丧,这篇文章会使你平静些,因为他解释了它的限制。 然后,你可以回来,享受爆米花了。 POP 实践!结论(27:40)我们讨论了协议是如何在实际工作中运用的,特别是在每天编码过程中是如何把它使用到视图控制器,视图和网络中去的。本篇演讲帮助你编写出安全的,可维护的,可重用的,更统一的,模块化代码。更加易于测试的代码。相比于子类而言,协议更棒。 然而,协议也会被滥用。我可能使用了过多的协议了:我学习它,它是个全新的东西而且吸引眼球,我希望无时不刻都使用它……但是这是不必要的。在我的第一个例子里面,当我有一个抖动的视图和抖动函数的时候,这就很棒。只有在两个视图都需要抽象的时候,我需要重构它们的时候,放到协议里才是合理的。不要疯狂地使用协议。 作为结束,两个非常有趣的演讲:
在你每天编程的时候,希望你能考虑使用协议! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |