浅谈Swift中的限定扩展

现在很多公司的iOS新项目都开始用Swift来代替OC开发了,Swift带来的亮点和新功能很多,但我觉得最重要的一点是引导我们编程思想的改变,将我们在OC中用到的传统的面向对象编程思想OOP(object-oriented programming)向面向协议编程思想POP(protocol oriented programming)以及面向值的编程思想VOP(value-oriented programming)上转变,苹果也让我们开发者在编程的时候“从一个protocol开始,别从一个class开始”,再加上Swift的标准库中出现了比重很大的结构体struct和枚举类型enum,所以在Swift中灵活运用各种协议protocol和值类型value来实现功能既实用又优雅。而在Swift编程过程中无处不在的extension,不仅让我们在代码结构上发生了巨大改变,同时在编程理念上也与POP不谋而合。

extension

extension,顾名思义就是扩展,类似于OC中的category,但Swift中的extension功能却强大的多, 可以扩展class struc enum 甚至是protocol,来给他们:提供便利构造方法、增加运算属性、定义实例方法和类方法、定义下标、使已有的类型遵循协议等等。

现在有一个需求,A、B和C三个类,都需要扩展一个属性或者方法用处理同样的功能,OOP的做法:让A、B和C继承于一个基类D,然后给D中加上这个属性,或者当A、B和C三个类不方便继承自一个基类时,各自分别扩展一个属性。不过这样做感觉很死板的样子。

而面向协议编程POP的做法:写声明一个协议someProtocol,协议中声明这个属性,然后让A、B、C三个类都遵循这个someProtocol,分别再实现这个协议中的属性,如下所示:

//定义protocol
protocol someProtocol {
    var clickCount: Int { set get }
    func ClickEvent(action: String,value: NSNumber)
}

extension A: someProtocol{
    var clickCount: int { set get } 
    func ClickEvent(action: String,value: NSNumber){
       //实现代码
    }
}

限定扩展 extesion...where..

而此时来了新的需求,需要给A类扩展另外一个属性,但B和C暂时不需要,大部分人的思维肯定是既然只有A需要,那我们就单独给A来扩展一个属性不就可以了吗,这样肯定行得通,但既然我们都已经走上了POP编程的道路,而且Swift也鼓励我们尽量用protocol来处理问题,那我们应该怎么在protocol上做文章呢?

首先我们想到了给someProtocol扩展一个属性,但这样不只是A能用,同样遵循了该协议的B和C,以及将来遵循这个协议的所有class、struct等等都扩展得到了这个属性,这从代码的逻辑性上来说是不严谨的,于是苹果在Swift2.0的时候加入了一个新功能,可以在给protocol扩展的时候添加限定,就是说在满足该限定条件(遵循另一个协议或者满足某个类型)下才能允许使用此扩展下的属性或方法, 而这个限定就是通过where来添加的, 先看代码:

extension someProtocol where Self: UIViewController{
    var otherProperty: String{
        return "something you want"
    }

    func handleError(error: String) {
        //实现代码
}}

以上代码的意思就是:Self代表遵循了someProtocol的某个类,只有这个类是继承于UIViewController时,才可以使用otherProperty这个属性和handleError这种方法,这就是限定扩展的基本写法,当然此处的UIViewController也可以是另外的一个协议anotherProtocol,就是说你只有遵循了anotherProtocol的前提下,你才能使用someProtocol中的扩展内容。

RxSwift/RxCocoa中的限定扩展应用

Swift开发到一定阶段,通常都会引入RxSwift框架来进行响应式编程和敏捷开发,而在代码中与RxSwift同时会引入的另一个库RxCocoa,RxCocoa是Rx对iOS的原生API中UIKit以及Foundation中的视图(UIView)、控制事件(Control Event)、键值观察(KVO)、通知(Notification)等的扩展,以便在开发时更方便的对这些原生组件进行Rx应用。例如:

//nameTextField是一个UITextField控件,可以直接通过.rx.text获取到该控件中输入内容的事件序列,然后再进行处理

let nameObservable = nameTextField.rx.text .shareReplay(1).map {
    $0!.characters.count > 0 
}

//registerButton是一个UIButton,通过.rx.tap能获取到该button的点击事件序列
registerButton.rx.tap .bindTo(someObservar) .addDisposableTo(disposeBag)

而以上例子中的rx.text以及rx.tap是怎么实现的呢,咱们来看RxCocoa的源码一窥究竟:

/// Extend NSObject with `rx` proxy.将所有NSObject子类都遵循ReactiveCompatible协议 
extension NSObject: ReactiveCompatible { }
  //ReactiveCompatible这个协议中扩展了rx属性,类型为struct类型的Reactive

extension ReactiveCompatible {
  /// Reactive extensions.
  public var rx: Reactive {
    get {
        return Reactive(self) //此处返回Reactive实体,将self赋给base属性
    }
    set {
        // this enables using Reactive to "mutate" base object
    }
  }
}

以UIButton举例,通过以上实现,我们明白了通过UIButton.rx可以获得一个Reactive类型的属性,通过Reactive(self) 将button自身赋给了Reactive这个struct的Base属性,来看源码:

public struct Reactive<Base>{   
    /// Base object to extend.    
    public let base: Base    
    /// Creates extensions with base object.   
    ///
    /// - parameter base: Base object.   
    public init(_ base: Base) {       
        self.base = base   //上面的return Reactive(self),将self赋给了base
    }
}

通过Reactive的构造方法,此时UIButton.rx获得的这个Reactive实体中的Base类型就是UIButton了,而base就是这个UIButton对象本身,而再接着通过rx.tap又是怎么获得点击事件的呢,这就用到了限定扩展这个非常实用的功能了,接着看源码:

//点击事件扩展,可通过button.rx.tap来观察订阅
extension Reactive where Base: UIButton {
    /// Reactive wrapper for `TouchUpInside` control event.
    public var tap: ControlEvent {
        return controlEvent(.touchUpInside)
    } 
}

以上就是RxCocoa中对Reactive进行的限定扩展,只有当Reactive的Base类型是UIButton时,才能使用Reactive下的tap属性,而这个tap属性就是RxSwift封装好的点击事件序列。RxCocoa中也同时对改变UIButton的image和title进行了扩展,如下:

//扩展方法,可通过绑定Observable来动态修改UIButton的title和image
extension Reactive where Base: UIButton {
    /// Reactive wrapper for `setTitle(_:for:)`
    public func title(for controlState: UIControlState = []) -> UIBindingObserver<Base, String?> {
        return UIBindingObserver<Base, String?>(UIElement: self.base) { (button, title) -> () in
            button.setTitle(title, for: controlState)
        }
    }

    /// Reactive wrapper for `setImage(_:for:)`
    public func image(for controlState: UIControlState = []) -> UIBindingObserver<Base, UIImage?> {
        return UIBindingObserver<Base, UIImage?>(UIElement: self.base) { (button, image) -> () in
            button.setImage(image, for: controlState)
        }
    }

    /// Reactive wrapper for `setBackgroundImage(_:for:)`
    public func backgroundImage(for controlState: UIControlState = []) -> UIBindingObserver<Base, UIImage?> {
        return UIBindingObserver<Base, UIImage?>(UIElement: self.base) { (button, image) -> () in
            button.setBackgroundImage(image, for: controlState)
        }
    }
}

我们自己也可以对UI控件来进行其他的Rx扩展,以满足特定需要,比如说只有当用户名和密码同时满足某个条件时,登录按钮才可以点击以及改变背景颜色,因此我们可以将某种条件转换为一个Bool类型的可观察序列Observabel<Bool>,然后对UIButton进行一个观察者类型btnValidState的扩展,然后绑定就可以随时进行监控了。

//利用限定扩展来自定义对UIButton的Reactive扩展 可以通过rx来访问
extension Reactive where Base:UIButton{
    var btnValidState:UIBindingObserver<Base,Bool>{
        return UIBindingObserver(UIElement: base, binding: { (button, valid) in
            button.isEnabled = valid
            button.backgroundColor = valid ? mainColor : bgGrayColor2
        })
    }
}
//将用户名和密码的String序列转换成Bool序列
let nameObservable = userNameText.rx.text .shareReplay(1).map {
            $0!.characters.count > 0
 }
let passObservable = passwordText.rx.text .shareReplay(1).map {
            $0!.characters.count > 0
 }
//将以上两个序列合成一个序列,绑定到我们扩展的btnValidState属性上
//可以看到此时可以通过loginBtn.rx.btnValidState获取到,这样保持了RxSwift代码的一致性
 _ = Observable.combineLatest(nameObservable,passObservable){$0 && $1}.bind(to: loginBtn.rx.btnValidState).disposed(by: disposeBag)

以上就是限定扩展的基本使用和一些实战中的应用,这在一定程度上确实能改变我们的编码思维和方式,这也是Swift给我们带来的非常灵活的改变。

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

推荐阅读更多精彩内容