Skip to content

xiaohuier/SwiftToOC

Repository files navigation

#  Objective-C to Swift(Pod Library)
## 前言
随着Swift版本的更新,API也越来越稳定了,所以最近笔者就把自己长期维护的OC库,开始引入Swift混编,这篇文章就是记录引入Swift的过程和遇到的问题。
## 创建示例的OC仓库,并且引入Swift
首先,通过```Pod Lib Create```命令创建一个OC仓库,模块名未OCToSwiftDemo,并且给仓库里面添加了一些OC的代码和文件,项目的目录结构大概如下:
![list1](list1.png)
项目分为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部分已经没有问题了,是Demo编译错误了,Demo这把新建一个Swift文件来创建bridge-header文件即可
编译通过后,来看下Pods下的Products目录下的SDk.a文件,目录结构大概是这样:
![alist](alist.png)
其中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中的类:
```
#import <OCToSwiftDemo/FirstViewController.h> //可以引用SDK中的OC类
@import OCToSwiftDemo; //这种方式,既可以引用OC类,也包含Swift类
```
主项目中的Swift类,引用SDK中的类
```
import OCToSwiftDemo
```

SDK中的OC,Swift类互相引用
Swift可以直接引用OC,因为他通过```OCToSwiftDemo-umbrella.h```来获取所有的OC头文件
OC引入Swift需要```#import "OCToSwiftDemo-Swift.h"```,注意Swift的类和代码,需要使用@objc或者@objcMembers后才能被转换成OC代码

## 开发过程中遇到的问题

### 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方法
```
- (void)filterVideoURL:(NSURL *)originalVideoURL withStreamData:(id)streamData currentBitStreamItem:(id)currentBitStreamItem completion:(void (^)(NSURL * _Nullable, NSError * _Nullable))completion
```
在Swift里面,会自动提示出这样的方法
```
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的部分看来的样子
```@objc(filterVideoURL:withStreamData:currentBitStreamItem:completion:)```,这样写后。消息转发就可以正常进行了

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

### 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](PropertySetError.png)
这么个情况,不过既然这个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)```

## 打包成framework

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published