随着Swift版本更新到5,API也愈来愈稳定了,因此最近笔者就把本身长期维护的OC库,开始引入Swift混编,这篇文章就是记录引入Swift的过程和遇到的问题。swift
首先,经过Pod Lib Create
命令建立一个OC仓库,而且给仓库里面添加了一些OC的代码和文件,项目的目录结构大概以下:
bash
s.source_files = 'OCToSwiftDemo/Classes/**/*.{h,m,swift}'
#import <Masonry.h>
这种写法就不行了,须要改为
#import <Masonry/Masonry.h>
module OCToSwiftDemo {
umbrella header "OCToSwiftDemo-umbrella.h"
export *
module * { export * }
}
module OCToSwiftDemo.Swift {
header ******/OCToSwiftDemo-Swift.h" requires objc } 复制代码
其中的OCToSwiftDemo-umbrella.h中包含了全部的OC头文件 OCToSwiftDemo-Swift.h中包含了全部Swift文件转换成OC后的代码
代码在开发的时候,分为四种状况,主项目的OC类引用SDK中的OC类,Swift类,SDK中的OC,Swift类互相引用对方
写法分别是这样
主项目OC类,引用Demo中的OC类或者Swift类:闭包
#import <OCToSwiftDemo/FirstViewController.h> //引用SDK中的OC类
@import OCToSwiftDemo; //这种方式,既能够引用OC类,也包含Swift类
复制代码
主项目Swift类,引用Demo中的OC类或者Swift类:app
import OCToSwiftDemo;
复制代码
SDK中的OC,Swift类互相引用 其中Swift类经过umbrella文件就已经拿到了全部的OC类了。OC的类使用Swift,须要#import "OCToSwiftDemo-Swift.h"
ide
在笔者的项目中,存在着一些动态转发的代码。。。函数
- (void)forwardInvocation:(NSInvocation *)invocation {
NSString *selectorName = NSStringFromSelector(invocation.selector);
NSArray *observeObjects = self.observeObjects[selectorName];
for (id obj in observeObjects) {
if ([obj respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:obj];
}
}
}
复制代码
好比有一个须要被转发的OC方法ui
- (void)filterVideoURL:(NSURL *)originalVideoURL withStreamData:(id)streamData currentBitStreamItem:(id)currentBitStreamItem completion:(void (^)(NSURL * _Nullable, NSError * _Nullable))completion
复制代码
在Swift里面,会自动提示出这样的方法this
open func filterVideoURL(_ originalVideoURL: URL!, with streamData: Any!, currentBitStreamItem: Any!, completion: ((URL?, Error?) -> Void)!) {}
复制代码
而后再转发的时候,respondsToSelector会判断不过,由于
oc的方法名为filterVideoURL:withStreamData:currentBitStreamItem:completion:
Swift的方法名为filterVideoURL:with:currentBitStreamItem:completion:
在Swift像OC转换的时候,系统自动忽略了和参数名同样的方法名部分。
解决办法是,使用@objc()关键词,这个关键词是能够指定该方法在OC的部分看来的样子atom
@objc(filterVideoURL:withStreamData:currentBitStreamItem:completion:)
open func filterVideoURL(_ originalVideoURL: URL!, with streamData: Any!, currentBitStreamItem: Any!, completion: ((URL?, Error?) -> Void)!) {}
复制代码
这样改写后。消息转发就能够正常进行了url
OC中的block和Swift的闭包,苹果是会默认的去帮忙转换的。。。好比:
OC的block在Swift中使用:
@interface Model : NSObject
- (void)useBlock:(void(^)(NSString *))block;
@end
复制代码
let model = Model()
model.use { (string) in
print("swift \(string)")
}
复制代码
Swift的闭包在OC中一样能够直接调用
class SwiftModel: NSObject {
@objc func useClosure(closure :(String) -> ()) {
closure("123")
}
}
复制代码
SwiftModel *swift = [[SwiftModel alloc] init];
[swift useClosureWithClosure:^(NSString * _Nonnull string) {
NSLog(@"%@", string)
}];
复制代码
然而在一些特殊状况下,编译器没能帮咱们自动转换block和闭包,这时候就会出现问题:
首先,在OC中定义这样的协议方法
typedef void (^ObserveKeyBlock)(id _Nonnull obj, _Nullable id oldVal, _Nullable id newVal);
@protocol ModelProtocol <NSObject>
- (NSDictionary<NSString *, ObserveKeyBlock> *)dictoryBlock;
@end
复制代码
而后,在Swift中敲下dictionary,便会自动提示出完整的方法名
func dictionaryBlock() -> [String : (Any, Any?, Any?) -> Void] {
let block :ObserveKeyBlock = { (oldValue, newValue, key) in
print("oldValue = \(oldValue) newValue = \(newValue) key = \(key)")
}
return ["key" : block]
}
复制代码
而且会看到这样的警告
Instance method 'dictoryBlock()' nearly matches optional requirement 'dictoryBlock()' of protocol 'ModelProtocol'
Make 'dictoryBlock()' private to silence this warning
复制代码
看起来很是的难以想象,编译器告诉咱们Swift类中的dictoryBlock方法和协议里面的dictoryBlock方法名相似,建议咱们使用private关键词来消除警告。。。
然而奇怪的是,咱们就是要实现这个方法呀。。。。先试试使用下private不看警告
OC边的调用方法以下
SwiftModel *swift = [[SwiftModel alloc] init];
ObserveKeyBlock block = swift.dictionaryBlock[@"key"];
block(@"1", @"2", @"key");
复制代码
而后编译一下
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[test.SwiftModel dictionaryBlock]: unrecognized selector sent to instance 0x6000016fc2b0'
复制代码
结果很合理,private的方法OC消息转发时,会找不到它,那去掉private,而后加上@objc,结果编译器警告:
Method cannot be marked @objc because its result type cannot be represented in Objective-C
复制代码
说是这个方法没法被转换成OC的方法。。。。 而后尝试着去修改了方法的参数类型,让编译器忽略报错
@objc func dictionaryBlock() -> [String : Any] {
let block :ObserveKeyBlock = { (oldValue, newValue, key) in
print("oldValue = \(oldValue) newValue = \(newValue) key = \(key)")
}
return ["key" : block]
}
复制代码
而后编译。。。
func dictionaryBlock() -> [String : @convention(block) (Any, Any?, Any?) -> Void] {
let block :ObserveKeyBlock = { (oldValue, newValue, key) in
print("oldValue = \(oldValue) newValue = \(newValue) key = \(key)")
}
return ["key" : block]
}
复制代码
编译一下的结果,也能够看到被转换成了OC中的Block类型
在笔者的SDk中,大量使用了协议来对模块进行解耦,好比一个属性statusController,某些组件负责生成这个对象,某些组件负责持有这个对象,某些组件须要读取这个对象的一些值。。那么就会有这样的三个协议
@protocol StatusControllerProtocol <NSObject>
@property (nonatomic, strong) id statusController;
@end
@protocol SetStatusControllerProtocol <NSObject>
- (void)setStatusController:(id)statusController;
@end
@protocol GetStatusControllerProtocol <NSObject>
- (id)statusController;
@end
复制代码
调用方大概是这样
Model *model = [[Model alloc] init];
if ([model respondsToSelector:@selector(setStatusController:)]) {
[model setStatusController:statusController];
}
NSLog(@"%@", model.statusController);
复制代码
在OC的类中,实现这三个协议方法很是的简单,由于OC中的属性等于iVar+get+set,只须要有@property (nonatomic, strong) id statusController
,或者使用 @synthesize statusController = _statusController
,均可以一会儿实现三个方法
在引入Swift后,我须要在Swift类中实现这些协议方法,这时会赶上方法名的冲突
首先,单独实现 StatusControllerProtocol 这个协议,很是简单,让Swift类提供var statusController: Any
便可
若是要实现SetStatusControllerProtocol和StatusControllerProtocol一块儿的话,咱们只提供一个var statusController: Any
是不行的,编译器会告诉你没有SetStatusController:的方法,是不行的。
就算咱们加上这个方法,也会在var statusController: Any
这一行,出现Setter for 'statusController' with Objective-C selector 'setStatusController:' conflicts with method 'setStatusController' with the same Objective-C selector
这样的编译报错。
看来在Swift里面,属性并不等于iVar加上get,set方法这样的组合的。。。
那么既然是Swift的方法和OC方法名的冲突,就有2个修改方法名的办法,既Swift类里面的方法名用@objc来修饰,和把OC协议里面的方法用NS_SWIFT_NAME修饰
然而,两个方法都是不可行的。。。。都会撞上这么个状况:
NS_SWIFT_UNAVAILABLE("use statusController instead")
,get方法改为
@property (nonatomic, readonly)id statusController;
而后只须要在Swift中提供一个var就实现好了三个协议了。
var statusController
也不会对调用有任何影响
在swift中,咱们没法使用宏定义好大的方法,因此都须要把他们改为具体类的方法,或者常量的形式
简单的常量,Swift会把它转换成一个常量的。。。可是复杂的不行。
建一个新的Swift文件,把须要定义的宏常量改为对类型的拓展
例如 在SDK中获取image,对于OC,写法以下:
#define OW_UIImageNamed(A) [UIImage OW_imageNamed:A]
@implementation OWBundleTool
+ (NSBundle *)bundle
{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSURL *url = [bundle URLForResource:@"OCToSwiftDemo" withExtension:@"bundle"];
return [NSBundle bundleWithURL:url];
}
@end
@implementation UIImage (Add)
+ (UIImage *)OW_imageNamed:(NSString *)name
{
UIImage *image = [UIImage imageNamed:name inBundle:[PPBundleTool bundle] compatibleWithTraitCollection:nil];
NSAssert(image, @"not found named %@ image, you need add to images.xcassets, and clean build", name);
return image;
}
@end
复制代码
Swift中写法以下:
extension UIImage {
func OWImageNamed(name: String) -> UIImage {
UIImage(named: name, in: PPBundleTool.bundle(), compatibleWith: nil)
}
}
复制代码
在SDK中,每每会有本身定制log日志格式而且输出到文件的需求,对CocoaLumberjack库进行了一系列封装,而后提供一组相似于DDLog宏,#define SDKLogDebug(frmt, ...)
而后再宏里面实际的调用本身的logger的
- (void)log:(NSString *)module level:(DDLogLevel)level prefix:(NSString *)prefix format:(NSString * _Nonnull)format arguments:(va_list)argList;
复制代码
虽然CocoaLumberjack自己提供了Swift版本,可是引入更多的包会增大包体积,因此把原先的SDKLogger提供一个Swift的桥接版本会比较好 具体代码是建立一个SDKSwiftLogger类,提供以下的方法
open class MYSwiftLogging {
static let mouduleName = "OCToSwiftSDK"
static func logInfo(_ format: String, _ args: CVarArg...) {
let funcName = "\(#function) - \(#line)"
let arguments = getVaList(args)
SDKSharedLogger.log(mouduleName, level: DDLogLevel.info, prefix: funcName, format: format, arguments:arguments);
}
}
复制代码
最后调用就相似于NSLog的使用了,MYSwiftLogging.logInfo("hello %@", string)