虽然说Swift是作为一种全新的语言被推出的,但是不可避免的需要借助于Apple生态来对它进行推广,在推广的过程中,就不可避免的需要被使用在Cocoa框架中,所以我们今天来总结一下当Swift被使用在Cocoa框架中时需要注意的一些事项。
在我们开始讨论之前,我们先来了解一下Swift与Objective-C的一些不同点。
区别
我们通过使用Swift与Objective-C来编写具有一个存储属性、一个计算属性、一个实例方法的类:
Objective-C
@interface OCModel : NSObject
@property (nonatomic, copy) NSString *privateName;
@property (nonatomic, copy) NSString *publicName;
- (void)showName;@end@implementation OCModel- (void)setPublicName:(NSString *)publicName {self.privateName = publicName;
}- (NSString *)publicName {return self.privateName;
}
- (void)showName {NSLog(@"OCModel name is %@", self.privateName);
}@end
Swift
class SwiftModel {var privateName: String?var publicName: String {get {return privateName ?? "none"}set {privateName = newValue}}func showName() -> Void {print("SwiftModel name is \(privateName ?? "none")")}
}
现在我们对两种语言定义的类通过Runtime来读取一下相关的内容,结果如下:
Objective-C
Swift
从上面结果可见,Swift中的属性即方法并不能通过Runtime机制读取出,这是因为Runtime的API是基于运行时机制的,而Swift本身是静态语言,在编译时就已经确定变量、方法等内容。
但我们在使用Objective-C进行iOS开发时,或多或少的都使用到了Runtime,同时Runtime也可以为我们解决许多问题,那么在Swift中就无法使用Runtime了吗?当然不是。
Swift中使用Runtime
我们了解了Swift为何无法使用Runtime的原因,要解决这个问题,最简单的方法就是将Swift中想要使用Runtime的内容桥接至Objective-C上,所幸苹果已经为我们做到了这一点,那就是@objc关键字。
我们将上述Swift中的类桥接至Objective-C之后看一下结果:
桥接之后
@objc class SwiftModel: NSObject {@objc var privateName: String?@objc var publicName: String {get {return privateName ?? "none"}set {privateName = newValue}}@objc func showName() -> Void {print("SwiftModel name is \(privateName ?? "none")")}
}
结果
可见,在将Swift桥接至Objective-C之后,我们可以使用Runtime访问到所有的变量、属性以及方法。
注意
- 使用@objc可以将Swift编写的类桥接至Objective-C,但是必须保证该类的基类为NSObject。
- 在基于Swift3以及之前版本的Swift语言项目中,当Swift编写的类被标记为@objc时,编译器会自动为该类中所有非private访问级别的成员默认添加@objc关键字。但是在Swift4之后,该功能被关闭,我们需要为我们想要桥接至Objective-C的类、属性、方法进行手动显式地添加@objc标记。
现在,我们再来讨论一下在Cocoa框架中使用Swift,首先我们来看一下Target-Action模式。
Target-Action模式
首先我们看一下Target-Action模式中的两个关键点:Target以及Action。
我们使用Timer的API来查看一下:
public init(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool)
首先,target为Any类型,其余没有特殊要求。
其次,我们来重点探讨一下action。
Action
从API中可以看出,Action的类型为Selector。在官方文档中,Selector的描述如下:The Objective-C SEL type.
由此我们可以看出,该Selector必须是桥接到Objective-C的方法。
Selector的初始化方式有两种,其中有一些注意点如下:
1.#selector()
我们可以使用#selector()来初始化一个Selector。
由Xcode的提示我们可知,所需要使用到的方法也必须是经过@objc标记的方法。
2.init(_ str: String)
使用Selector的初始化方法来进行创建,通过传入一个方法名称的字符串来创建Selector。
在使用该方法创建时,同样需要注意使用到的方法也必须是进过@objc标识的方法。
同时,在使用该方法创建时,方法名称字符串必须是Objective-C语法类型的方法名。
例如,我们使用如下方法创建Selector:
@objc func repeatFunction(timer: Timer) -> Void {print(timer)
}
那么对应的方法应该是:
Selector("repeatFunctionWithTimer:")
接下里我们再看一下Cocoa中的另一个模式。
Key-Value Observing模式
首先我们查看一下与KVO相关的几个方法:
extension NSObject {open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
}extension NSObject {open func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?)@available(iOS 5.0, *)open func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?)open func removeObserver(_ observer: NSObject, forKeyPath keyPath: String)
}
由方法定义我们可知,想要使用KVO,那么被观察的对象需要是NSObject的子类。那好,我们定义一个NSObject的子类:
class SwiftModel: NSObject {var name: String?
}
接下来我们创建一个该类的实例,并对该实例进行 name 变量的观察:
override func viewDidLoad() {super.viewDidLoad()let model = SwiftModel()model.addObserver(self, forKeyPath: "name", options: [.new], context: nil)model.name = "new name"model.removeObserver(self, forKeyPath: "name")}override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {if object is SwiftModel, keyPath == "name" {print(change!)}
}
接下来运行,我们发现并没有触发KVO观察的回调,这是为什么呢?
我们知道,KVO机制是在建立观察时动态的为被观察的类创建一个子类,同时重写被观察键的setter方法(关于KVO的探讨,可以查看该篇博客)。
此时我们没有触发KVO的观察回调,是由于@objc虽然将属性桥接至Objective-C上,但是Swift编译器还是无法实现动态调用,我们需要将被观察的属性设置为动态调用,即使用dynamic关键词来标识属性:
class SwiftModel: NSObject {@objc dynamic var name: String?
}
至此,我们实现了在Swift中使用KVO,下面是在使用KVO时的几点总结:
1.被观察的类需要继承自NSObject。
2.被观察的属性/变量需要制定动态调用,即使用dynamic来标识。
3.在使用dynamic标识时,需要将属性/变量桥接至Objective-C,即同时使用@objc dynamic标识。
其他一些相关注意事项
在实际开发中,我们需要对方法进行Swizzle,此时我们一般使用下面的方法:
func swizzle(instanceMethod: Selector, method: Selector) -> Void {let cls = type(of: self)let origMethodOptional = class_getInstanceMethod(cls, instanceMethod)let newMethodOptional = class_getInstanceMethod(cls, method)guard let origMethod = origMethodOptional, let newMethod = newMethodOptional else {return}if class_addMethod(cls,instanceMethod,method_getImplementation(newMethod),method_getTypeEncoding(newMethod)) {class_replaceMethod(cls,method,method_getImplementation(origMethod),method_getTypeEncoding(origMethod))} else {class_replaceMethod(cls,method,class_replaceMethod(cls,instanceMethod,method_getImplementation(newMethod),method_getTypeEncoding(newMethod))!,method_getTypeEncoding(origMethod))}
}
此时如果我们不对需要交换的方法进行特殊的处理,那么可能会造成交换失败,如下例:
class SuperClass: NSObject {@objc func sayHello() -> Void {print("Super Say Hello")}
}class SubClass: SuperClass {override init() {super.init()self.swizzle(instanceMethod: }@objc func hookSayHello() -> Void {print("Sub Say hello")}
}
接下来我们初始化一个SubClass对象,然后分别调用sayHello()和hookSayHello()方法,理论上两个方法进行了交换,打印结果也应该进行交换,但结果并没有交换。
这是为什么呢?
其实不难想象,KVO中要将观察的属性设置为dynamic,目的就是为了在调用时动态访问到重写之后的setter方法。而在此处,两个方法并没有指定dynamic,虽然两个方法进行了调换,但是在调用时并没有动态调用,而仅仅是直接访问,所以并没有达到我们想要的效果。
我们可以通过以下步骤进行验证:
1.验证方法是否被交换
我们改动一下Runtime交换方法的函数,在其中打印一下交换前与交换后的方法指向:
然后我们运行代码,此时方法的打印结果没有交换,但是方法的指向发生了改变:
2.验证单个dynamic标识效果
- 我们为sayHello方法添加dynamic,运行代码,发现两次打印都是”Sub Say Hello”。
- 我们为hookSayHello方法添加dynamic,允许代码,发现两次打印都是”Super Say Hello”。
不难理解,当其中一个方法被指定为dynamic时,当访问该方法时,会采用动态调用的方式,进而访问到调换之后的实现。而没有指定dynamic的方法,还是按照直接调用的方法,访问到它自己本身的实现上。
3.验证两个dynamic标识效果
当两个方法都被指定为dynamic时,打印结果发生了交换,证明方法调换成功。
接下来我们继续对方法调换进行探索,假设hookSayHello方法是在SubClass的Extension中,那么对于dynamic的使用有和不同:
class SuperClass: NSObject {@objc func sayHello() -> Void {print("Super Say Hello")}
}class SubClass: SuperClass {override init() {super.init()self.swizzle(instanceMethod: }
}extension SubClass {@objc func hookSayHello() -> Void {print("Sub Say hello")}
}
我们依旧使用上述步骤进行验证:
1.验证方法是否被交换
我们运行代码,此时方法的指向发生了变化,同时两次打印结果均为”Super Say Hello”。
2.验证单个dynamic标识效果
- 我们为sayHello方法添加dynamic,运行代码,发现两次分别是”Sub Say Hello”,”Super Say Hello”。
- 我们为hookSayHello方法添加dynamic,允许代码,发现两次打印都是”Super Say Hello”。
3.验证两个dynamic标识效果
当两个方法都被指定为dynamic时,打印结果发生了交换,证明方法调换成功。
从上述结果,我们可以总结出,sayHello方法的表现与之前验证结果一直,而hookSayHello的表现无论有没有指定dynamic,都进行了动态调用,由此可见,通过Extension新增的方法,均表现为dynamic。
至此,我们可以进行一个总结:
1.Runtime可以将Swift中的@objc方法进行调换。
2.Swift调用方法时,并不依赖于Runtime,即使方法对应的IMP已经改变,但Swift依旧可以使用直接调用方式调用到原始的IMP。
3.可以使用dynamic关键字将Swift中的方法指定为动态调用,此时的Swift方法与Objective-C中方法表现一致,会受到Runtime的影响。
4.通过Extension新增的方法,默认为dynamic的,这些方法受Runtime影响。