命名空间
对长期从事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)这中,其实也简单,过程不过多赘述,直接贴上:
/// 命名空间协议
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