随着 Swift 和 SwiftUI 的发展,协议所扮演的角色越来越重要,面向协议编程(POP)已成为日常开发的常规操作。本文不会细谈协议的基础概念(需要了解基础的同学,可以移步官方文档:Protocols),而是以实际案例来阐述协议的运用,希望读者能有所获益。
1、自定义 Button
我们现在需要自定义一个按钮控件,要求可以快速生成不同样式的按钮,还能方便地维护和扩展。
我们首先定义一个协议,该协议只定义了一个方法 config(_:)
来配置按钮的样式:
1 | protocol ButtonStyle { |
然后,我们定义一个按钮控件 Button,该类暴露了一个 style(_:)
方法来快速生成不同样式的按钮。Buton 的初始化方法里是该控件的一些公共配置,这里使用了笔者自己写的一个小工具来实现链式调用,方法名和系统 api 基本一致。
1 | import KZRazor |
我们现在来实现一个带模糊效果的按钮,定义一个遵循 ButtonStyle
协议的类并实现相应的方法:
1 | class BlurButtonStyle: ButtonStyle { |
然后,我们就可以通过下面的方法来调用了:
1 | Button().style(BlurButtonStyle(blurStyle: .systemThinMaterialLight)).kz.title("Hello World!") |
但是在写代码的时候不够智能,我们必须知道 ButtonStyle 的具体类型才能实现相应的 style,我们可以通过下面的改进来提供更友好的 api,在调用的时候,直接通过点语法就可以获得编译器的智能提示。
1 | extension ButtonStyle where Self == BlurButtonStyle { |
上面的调用代码就可以改写成:
1 | Button().style(.blur(style: .systemThickMaterialLight)).kz.title("Hello World!") |
文章开头的图片还提供了 .automatic
和 .shadow
两种样式,思路与上面一致,参考代码如下:
1 | class DefaultButtonStyle: ButtonStyle { |
对于 shadow 样式的按钮,为了获取到按钮的真实尺寸并添加阴影效果,我们在 Button 类重写 layoutSubViews 方法:
1 | override func layoutSubviews() { |
以上就是一个自定义按钮的实现,如果你熟悉 SwiftUI,就会发现这样的代码结构和 SwiftUI 中的各种视图的 style 实现思路是一致的。在遵循协议的基础上,我们可以放肆地修改和新增功能而将影响范围降至最小,这也意味着我们可以更好地理解我们的代码和进行单元测试。
我们并没有完全摒弃 OOP,而是与 POP 结合起来一起使用,都是为了更好地组织和管理代码。POP 并不能完全取代 OOP 这样的纵向继承方式,但它的横向扩展特性补充了 OOP 的不足。合纵连横,方为上策。
2、一个基础路由的实现
如上图,我们现在需要实现一个路由,通过路由来跳转到 A、B 界面,一个不带参数,一个带参数,同时,我们还需要能够在跳转过程中加入自定义转场动画。
首先,我们定义如下协议:
1 | public protocol Routable { |
Routable
协议只定义一个核心属性,就是路由跳转的控制器。这里我们并没有定义要跳转的方法,因为我们的需求是要在跳转过程中可以加入自定义转场动画,所以将跳转方法交给后文介绍的 Transition
协议来管理。
接着,我们定一个枚举类型,实现该协议。
1 | enum Router1 { |
Router1
这里可以理解为某个功能模块,比如我们还有其它功能模块,可以依样画葫芦,将各个模块的业务实现组织在一起,方便管理和维护。比如,RouterAuth、RouterShop、RouterCart 等。
下一步我们实现一个单例类 Router
,该类实现了 open
和 close
两个方法,分别用于界面的跳转和返回。
1 | class Router { |
路由的雏形基本出来了,接下来就是让 Transition 实例去实现 open 和 close 方法。根据这个思路,Transiton 协议定义如下:
1 | protocol Transition { |
我们在 Router 中的 open 和 close 方法内加上 Transition 实例的调用方法,更新后的代码:
1 | class Router { |
光有转场,没有动画,是转不起来的,所以我们需要给 Transition 协议新增如下两个属性(其中 animated 是用来标记是否需要动画的,以应对不需要动画的场景):
1 | var animated: Bool { get set } |
Animator
协议继承自系统的 UIViewControllerAnimatedTransitioning
协议:
1 | protocol Animator: UIViewControllerAnimatedTransitioning { |
接下来,我们来实现一个渐变的 Push。如果你熟悉自定义转场,那么下面的代码就很好理解了。
UIApplication.navigationController 是通过 extension 实现的一个小功能,用来获取 app 当前的导航控制器,方便我们在非控制器界面获取导航控制器,读者不必纠结具体实现。
1 | final class PushTransition: NSObject, Transition { |
我们只差最后一步的动画了,前面我们已经定了 Animator 协议,现在我们实现一个 FadeAnimator:
1 | final class FadeAnimator: NSObject, Animator { |
代码很简单,除了实现我们自己定义的 duration 属性,还需要实现 UIViewControllerAnimatedTransitioning 的协议方法。
我们的工作已接近尾声,现在尝试调用一下:
1 | Router.shared.open( |
结果和预期一致:
同理,对于 pop 操作,我们也可以用同样的思路实现,这里就不展开来说了,有兴趣的读者可以参看源码。
至此,一个基础的路由就实现了。如果需要实现更为复杂的功能比如 Universal Links 跳转、加入手势驱动转场等,读者可自行研究和实现。