Objective-C to Swift(SDK引入Swift混编记录)

前言

随着Swift版本更新到5,API也愈来愈稳定了,因此最近笔者就把本身长期维护的OC库,开始引入Swift混编,这篇文章就是记录引入Swift的过程和遇到的问题。swift

建立示例的OC仓库,而且引入Swift文件

首先,经过Pod Lib Create命令建立一个OC仓库,而且给仓库里面添加了一些OC的代码和文件,项目的目录结构大概以下:
bash

list1

项目分为OCToSwiftDemo部分下的主项目模块和Pods下的Development Pods,既咱们要开发的SDK部分
而后开始添加一个Swift文件,把podSpec里面的source_files添加Swift文件 s.source_files = 'OCToSwiftDemo/Classes/**/*.{h,m,swift}'
而且把项目的Podfile加上use_modular_headers!,或者把依赖的OC库挨个加上:modular_headers => true,否则在pod install的时候会给出响应的错误。这是由于Swift只能经过modular来引用其余的模块。
这时候会产生一些编译的错误,好比原先的 #import <Masonry.h> 这种写法就不行了,须要改为 #import <Masonry/Masonry.h>
还有一个是linker command failed,会告诉你一些swift的基本库,像UIKit之类的连接不到。。这时SDK部分已经没有问题了,是App编译错误了,App这边新建一个Swift文件来建立bridge-header文件便可
编译经过后,来看下Pods下的Products目录下的SDk.a文件,目录结构大概是这样:
alist

其中Modulemap文件中会产生2个module

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

代码开发过程当中,遇到的转换问题

OC Swift转换后的方法名不一致

在笔者的项目中,存在着一些动态转发的代码。。。函数

- (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

block和闭包的转换

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]
    }
复制代码

而后编译。。。

SwiftBlockError

这时候就能看出来,Swift的闭包,本质上是一种特殊的函数,isa指针指向了SwiftValue这个隐藏类型。它与OC的Block不一样,是须要进行转换的。。。
转换的方法呢,就是使用@convention(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类型

SwiftBlockSuccess

其实,若是协议方法不是可选类型的话,编译器是能提示出正确的方法名的

OC的get和set方法,在Swift中的转换

在笔者的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修饰
然而,两个方法都是不可行的。。。。都会撞上这么个状况:

PropertySetError

不过既然这个var就已经生成了set和get方法了。。。那么把这set方法在Swift下废弃,get方法改为属性的形式就能够了。给set方法后面加上 NS_SWIFT_UNAVAILABLE("use statusController instead"),get方法改为 @property (nonatomic, readonly)id statusController;而后只须要在Swift中提供一个var就实现好了三个协议了。
虽然这么写会致使单独使用SetStatusController协议的时候,Swift类会认为没有任何方法是须要实现的。。。可是提供一个 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)

相关文章
相关标签/搜索