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)

相關文章
相關標籤/搜索