隨着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)