swift开发之扩展实现命名空间(实例方法,类方法)

\color{red}{着急的火鸡们,最终全部代码在最下面}

命名空间

对长期从事objective-c语言开发的我们来说,命名空间可能是一个比较陌生的名称。

“命名空间”,简单地说,就是不允许有相同类名的区域。从事过java或者js开发的同学可能会有经验,这类语言的命名空间其实就是他们的目录名,即只要在不同目录下,就可以允许有相同的类名。

OC就比较尴尬了,它没有命名空间一说,也就是全局都不允许有相同的类名。那如何保证这一点?苹果是建议在类名前加2-3个唯一的字符来将自己的类名与其他区分开,于是就出现了UIView, NSString, MBProgressHUD, CALayer, AFNetworking, SDWebImage等

swift中,苹果终于引入了命名空间一说,在任意类中打印一下self 会出现"命名空间.className",swift中的命名空间的使用不是一个项目,而是需要跨项目,在一个项目中,都是一个命名空间,在同一个命名空间下,所有全局变量或者函数共享,不需要import,从swift开始,官方更多的建议大家使用pod来管理第三方框架,不然倒入一个框架到处都可以用

扩展方法前缀

在OC中,苹果建议在扩展中的方法需要增加前缀,原因是防止与自带方法或者其他库的扩展中方法重名,事实上也应该这么做,因为我们有前车之鉴,往往这类由于重写了方法造成的闪退,一旦xcode不能正常捕捉错误,将很难排查。

swift扩展中,同样需要关心方法覆盖的问题,对于原生类自带的方法,我们可以覆盖重复定义,并且最终调用走的是扩展中的方法,但是扩展中的方法不能重复定义,xcode会检测并报错

自定义命名空间

综上所述,我们自己模拟出类似“命名空间”,是个不错的选择,原因如下:

1.防止扩展中的方法或属性覆盖了原来已有的,造成无法预期的错误

2.有了命名空间,我们就不需要加前缀这种影响美观的操作,代码可读性更高

3.有了命名空间,开发过程中,尤其对于新人,可一眼看出方法或属性是属于原类自带的还是扩展的,防止长时间使用造成下意识的认知疲劳

Swift扩展模拟“命名空间”

首先,我们要知道swift中几个概念:

协议:与OC中协议类似,都是定义一套遵守者需要实现的规则,但是与OC不同的是,在swift中我们也可以对协议进行扩展,最终效果是所有遵守该协议的类都会增加协议被扩展的内容

泛型:swift提供了“泛型”来最大程度使函数、变量、容器等灵活化,如果你在架构一个应用或者sdk,那么泛型可以提供最大的便利性。swift中的标准库都是通过泛型定义的,例如Array可以塞进Int,也可以塞进String

泛型约束:顾名思义,就是通过泛型,来约束协议遵守者的类型

正式开始,我们的思路是通过扩展模拟出“命名空间”,其实这不是正儿八经的命名空间,只是期望通过一个特殊的符号,将我们自己扩展的方法属性等和官方的以及第三方的区分开来,类似于:


self.circleView.wm.moveToBottom()

加入circleView是一个UIView实例,这里的wm就是我们所说的特殊符号,其实也就是一个属性,moveToBottom就是我们自己扩展出的方法。

看到这里,第一个问题就抛出来了,如何给circleView扩展一个名叫wm的属性。很多聪明火鸡们就马上会想到两种方式,一种是扩展UIView,增加一个属性;另一种是使UIView遵守一个协议,通过扩展协议来增加一个属性。

假设,我们扩展的属性类型是:


public class NameSpace {

}

方法一:


extension UIView {

  public var wm: NameSpace {

        get {

            return NameSpace()

        }

    }

}

方法二:


/// 命名空间协议

public protocol NameSpaceProtocol {

    public var wm: NameSpace { get }

}

/// 扩展协议

extension NameSpaceProtocol {

    public var wm: NameSpace {

        get {

            return NameSpace()

        }

    }

}

/// UIView实现协议

extension UIView: NameSpaceProtocol {



}

我们将这两种方式做个比较,结论还是显而易见的,方式二的好处有:

1.我们在扩展每个类的时候,不需要像方式一那样都声明一个mw的属性,而是只要实现NameSpaceProtocol就可以了

2.对于子类,如果我们不希望其有这个属性,那么方式一就无解了,方式二则可以利用泛型约束的方式,可以随心所欲的控制

3.方式二写法更多的采用了swift独有的特性,风格上更加优雅,简单说就是更装*

我们在此基础上,在对NameSpace进行扩展,就实现了最终想要的效果


extension NameSpace {

  public func moveToBottom() {



}

}

/// 此时,UIView已经达到了想要的效果

let circleView = UIView()

circleView.wm.moveToBottom()

第二个问题就来了,这里我们真正扩展的其实是NameSpace,我们这里目标只有UIView,如果接下来还要给Date, Int, String等等扩展,实际上都是对NameSpace扩展,那么如果不做区分,那在其中一个类调用方法时,Xcode会提示出所有,包括其他目标扩展出的方法,事实上真的去调用非本目标的方法,编译也是不会报错的,但是这不是我们想要的。于是,我们引入泛型约束来做区分:


/// 命名空间

public final class NameSpace<T> {



}

/// 扩展UIView

extension NameSpace where T == UIView {

}

这下舒服了,在使用过程中不是对UIView的扩展不会出现在快捷提示。这里也做了个小优化,就是不希望NameSpace再做它用,所以加了个final描述一下

第三个问题,circleView.wm.moveToBottom()这个方法,如果moveToBottom方法中需要访问circleView的方法或属性怎么整?我们知道我们实际上扩展的是NameSpace类,所以我们需要在NameSpace中记录下来原来的对象就完事了:


/// 命名空间协议

public protocol NameSpaceProtocol {

    associatedtype TargetType



    /// 实例变量及方法命名空间

    var wm: NameSpace<TargetType> { get }

}

/// 命名空间

public final class NameSpace<T> {

    internal var base: T

    init(_ base: T) {

        self.base = base

    }

}

/// 扩展协议

extension NameSpaceProtocol {

    public var wm: NameSpace<Self> {

        get {

            return NameSpace<Self>(self)

        }

    }

}

/// 在扩展过程中通过self.base访问原来对象

extension NameSpace where T == UIView {

public func moveToBottom() {

    print("my x is \(self.base.frame.origin.x)")

}

}

写到这里,已经让大部分火鸡们满足了需求,实际上网上大多数资料也就到此为止了,但是仍然有部分不满意,因为我们一直做的都是对实例属性或者是方法做扩展,如果是类属性或者方法,类似于UIColor.wm.color(hexString)这中,其实也简单,过程不过多赘述,直接贴上\color{red}{最终代码}


/// 命名空间协议

public protocol NameSpaceProtocol {

    associatedtype TargetType



    /// 实例变量及方法命名空间

    var wm: NameSpace<TargetType> { get }



    /// 类变量及方法命名空间

    static var wm: NameSpace<TargetType>.Type { get }

}

/// 命名空间

public final class NameSpace<T> {

    internal var base: T

    internal var BASE: Self.Type

    init(_ base: T) {

        self.base = base

        self.BASE = Self.self

    }

}

/// 扩展协议

extension NameSpaceProtocol {

    public var wm: NameSpace<Self> {

        get {

            return NameSpace<Self>(self)

        }

    }

    public static var wm: NameSpace<Self>.Type {

        get {

            return NameSpace<Self>.self

        }

    }

}

/// 例子:扩展UIImage

extension UIImage: NameSpaceProtocol {}

extension NameSpace where T == UIImage {



    /// 根据颜色生成图片, 类方法

    public class func image(color: UIColor?, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {

        UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale);

        if let color = color, let currentContext = UIGraphicsGetCurrentContext() {

            let fillRect = CGRect(x: 0, y: 0, width: size.width, height: size.height);

            currentContext.setFillColor(color.cgColor)

            currentContext.fill(fillRect)

            let colorImage = UIGraphicsGetImageFromCurrentImageContext()

            UIGraphicsEndImageContext()

            return colorImage ?? UIImage()

        }

        return UIImage()

    }



  /// 宽度,实例属性

  public var width: CGFloat {

      return self.base.size.width

}

}

/// 实际使用

let image = UIImage.wm.image(color: .red, size: CGSize(width: 100, height: 200))

print("the image width is\(image.wm.width)")

注意一:在扩展NameSpace之前,我们需要将目标实现一下NameSpaceProtocol协议,但是实际开发过程中你会发现有些会报警告说已经实现过了,不必惊慌,那是因为父类实现过,子类就不必实现了,比如可以将NSObject实现NameSpaceProtocol协议,之后UIView等类就不用再写这一步骤了

注意二:也许有火鸡想利用runtime扩展属性,请注意,在扩展NameSpace时,属性runtime方式添加时,务必添加到self.base中:


/// 响应对象

    private var target: ButtonActionTarget? {

        get {

            return objc_getAssociatedObject(self.base, "buttonActionTarget") as? ButtonActionTarget

        }

        set {

            if let newValue = newValue {

                objc_setAssociatedObject(self.base, "buttonActionTarget", newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

            }

        }

    }

如果你添加到self,会发现并不生效,也就是get的时候一直为nil,这是因为本身NameSpace的作用域并不大,因为我们在扩展NameSpaceProtocol时只是临时初始化了NameSpace,并没有引用保存。

然后关于Self, .self, .Type的理解,大家可以执行查询,不过简单来说:

Self:用在协议中,代表的是协议自身或者实现者或者子类的类型

.self:用在哪代表的就是什么的自身,比如用在实例后面就是实例本身,类型后面就是类型本身

.Type:获取调用者的类型

最后再次声明下为什么我们要实现这个命名空间的效果:

1.在调用的时候,Xcode的快捷提示中不会显示目标的自带方法,不会产生混淆,对新人来说非常友好

2.加了一层命名空间,有效避免覆盖重写的风险

3.更加优雅,许多知名的第三方也都这么做了,比如RxSwift

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