一、背景

現(xiàn)在
Objective-C在Apple那邊已經(jīng)是放養(yǎng)的孩子了,除了每年的修修補(bǔ)補(bǔ),已經(jīng)不再做大的改動(dòng),而Swift變成了親兒子,每年一個(gè)大版本的更新,特別是Swift3.0版本之后,Swift已經(jīng)趨于穩(wěn)定,使用的用戶已超過(guò)了Ojective-C,所以對(duì)于iOS開(kāi)發(fā)者來(lái)說(shuō),掌握Swift開(kāi)發(fā)變成了必備的技能。
對(duì)于公司新項(xiàng)目來(lái)說(shuō)可以直接上純Swift項(xiàng)目,但對(duì)于一些老項(xiàng)目,留給開(kāi)發(fā)者的就只有使用Swift
重構(gòu)和混編兩條路了,本文就針對(duì)混編重點(diǎn)講解下一些實(shí)用的tips,以便在進(jìn)行混編時(shí)候更好的使用。
注(這里只講具體的小技巧,對(duì)于基礎(chǔ)的混編環(huán)境網(wǎng)上很多,可以自己搜索,這里不做展開(kāi))。
二、常用混編tips
1、使用 @objc 修飾
如果Swift類里面的某個(gè)成員變量或者方噶想要暴露給Objective-C調(diào)用,需要在前面加上 @objc
@objc let name: String
@objc func eat() {
print('aaa')
}
2、使用 @objcMembers 修飾類
使用Tip1方法,如果遇到多個(gè)成員變量和方法都需要暴露,每個(gè)都加@objc顯得太冗余,這時(shí)候可以使用 @objcMembers 修飾這個(gè)類,這樣默認(rèn)所有成員都會(huì)暴露給OC(包括擴(kuò)展中定義的成員)
最終是否成功暴露,還需要考慮成員自身的訪問(wèn)級(jí)別(private、fileprivate不會(huì)暴露)
@objcMembers class Car: NSObject {
var price: Double
var band: String
init(price: Double, band: String) {
self.price = price
self.band = band
}
func run() { print(price, band, "run") }
static func run() { print("Car run")
}
}
extension Car {
func test() { print(price, band, "test") }
}
3、通過(guò) @objc 重命名Swift暴露給OC的類名、屬性名、函數(shù)名等
因?yàn)镺bjective-C沒(méi)有命名空間,所以類名一般都會(huì)加上前綴,而Swift則不需要,為了符合OC的使用習(xí)慣,可以將Swift的類重命名后暴露給OC進(jìn)行混編調(diào)用,這樣使用起來(lái)就很nice了。
@objc(EHICar)
@objcMembers class Car: NSObject {
var price: Double
@objc(name)
var band: String
init(price: Double, band: String) {
self.price = price
self.band = band
}
@objc(drive)
func run() { print(price, band, "run") }
static func run() { print("Car run") }
}
extension Car {
@objc(newTest)
func test() { print(price, band, "test") }
}
重命名后在OC中的調(diào)用如下:
EHICar *car = [[EHICar alloc] initWithPrice:30 band:@"BMW"];
car.name = @"525LI";
[car drive];
[EHICar run];
4、選擇器
在Swift里面也可以使用選擇器,但是對(duì)應(yīng)地方法必須使用 @objc 修飾或者當(dāng)前類被 @objcMembers 修飾才能使用。
@objcMembers class Car: NSObject {
func textSelector(str: String) {
print(str)
}
func run() {
perform(#selector(textSelector(str:)))
}
}
5、String與NSString
使用過(guò)Swift的應(yīng)該都知道Swift在3.0版本對(duì)String進(jìn)行了大改,API設(shè)計(jì)上和NSString有了很大的不同,如前綴、后綴、索引、Substring等:
var str = "123456"
func textPrint() {
print(str.hasPrefix("123")) // true
print(str.hasSuffix("456")) // true
print(str.prefix(3)) // 從開(kāi)頭截取三位,結(jié)果為:123
print(str.suffix(3)) // 從末尾截取三位,結(jié)果為:456
}
var str = "1_2"
func textStr() {
// 插入 單個(gè)字符,結(jié)果是:1_2_
str.insert("_", at: str.endIndex)
// 插入 字符串,結(jié)果是:1_2_3_4
str.insert(contentsOf: "3_4", at: str.endIndex)
// 在某個(gè)索引后面插入,結(jié)果是:1666_2_3_4
str.insert(contentsOf: "666", at: str.index(after: str.startIndex))
// 在某個(gè)索引后面插入,結(jié)果是:1666_2_3_8884
str.insert(contentsOf: "888", at: str.index(before: str.endIndex))
// 在某個(gè)索引后面插入,偏移索引,結(jié)果是:1666hello_2_3_8884
str.insert(contentsOf: "hello", at: str.index(str.startIndex, offsetBy: 4))
// 刪除值為1的第一個(gè)索引的值,,結(jié)果是:666hello_2_3_8884
str.remove(at: str.firstIndex(of: "1")!)
// 刪除值為字符為 6 的字符,結(jié)果是:hello_2_3_8884
str.removeAll { $0 == "6" }
//刪除某個(gè)區(qū)間的字符
var range = str.index(str.endIndex, offsetBy: -4)..<str.index(before: str.endIndex)
// hello_2_3_4
str.removeSubrange(range)
}
所以在混編的時(shí)候使用起來(lái)就很不方便了,這時(shí)候可以考慮將String轉(zhuǎn)換為NSString使用。
6、協(xié)議
protocol對(duì)大家來(lái)說(shuō)都很熟悉了,但是OC中的協(xié)議對(duì)開(kāi)發(fā)者有一個(gè)痛點(diǎn)就是,OC的協(xié)議嚴(yán)格來(lái)說(shuō)只能說(shuō)是接口,因?yàn)椴荒軐?duì)協(xié)議中定義的方法進(jìn)行默認(rèn)的實(shí)現(xiàn),具體的實(shí)現(xiàn)還需要依賴實(shí)現(xiàn)類,這樣在使用時(shí)候就有很大的局限性。而Swift里面的協(xié)議相對(duì)來(lái)說(shuō)就很強(qiáng)大了,可以在 extension 中提供默認(rèn)實(shí)現(xiàn)。所以在混編的時(shí)候可以使用Swift來(lái)定義協(xié)議(需要@objc修飾才可以在OC中使用),然后在OC和Swift中進(jìn)行使用,這樣就很棒了。且如果是不必實(shí)現(xiàn)的函數(shù),函數(shù)前要加上 @objc optional。
@objc protocol CarProtocol {
func run()
}
extension CarProtocol {
func run() {
print("Car run")
}
}
7、runtime
OC的東西在Swift里面調(diào)用,會(huì)調(diào)用了 runtime 那套機(jī)制;而Swift的東西在OC里面調(diào)用,我們打斷點(diǎn)看匯編可以發(fā)現(xiàn)調(diào)用的也是runtime那套機(jī)制,而對(duì)于swift里面自己的方法走的肯定是Swift的流程,如果我們強(qiáng)行讓它走OC那套runtime機(jī)制,可以在 run() 函數(shù)前加 dynamic。
class Car: NSObject {
@objc dynamic func run() {
printf("Car run")
}
}
8、swift中使用KVO
Swift 要使用 KVO ,必須滿足以下條件:
屬性所在的類、監(jiān)聽(tīng)器最終繼承自 NSObject
用 @objc dynamic 修飾對(duì)應(yīng)的屬性
import Foundation
class Acount:NSObject {
dynamic var balance:Double = 0.0
}
class Person:NSObject {
var name:String
var account:Acount?{
didSet{
if account != nil {
account!.addObserver(self, forKeyPath: "balance", options: .Old, context: nil);
}
}
}
init(name:String){
self.name = name
super.init()
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {
if keyPath == "balance" {
var oldValue = change[NSKeyValueChangeOldKey] as! Double
var newValue = (account?.balance)!
print("oldValue=\(oldValue),newValue=\(newValue)")
}
}
}
var p = Person(name: "Kenshin Cui")
var account = Acount()
account.balance = 10000000.0
p.account = account
p.account!.balance = 999999999.9 //結(jié)果:oldValue=10000000.0,newValue=999999999.9
9、枚舉
OC如果想要調(diào)Swift中的枚舉值時(shí),Swift的枚舉需要使用 @objc 進(jìn)行修飾,然后OC就可以使用,需要注意的是,如果需要在OC中進(jìn)行該枚舉值的調(diào)用,書(shū)寫(xiě)規(guī)則為枚舉名+case的值。
注: Swift的枚舉比OC強(qiáng)大的很多,所以在混編時(shí),需要定義為Int類型后,才能供OC調(diào)用。
@objc enum CarType: Int {
case baoma = 0
case benchi
}
OC調(diào)用時(shí)該枚舉值時(shí),可以直接使用 CarType這個(gè)枚舉,需要使用具體值時(shí)如 baoma這個(gè)值,可以直接使用 CarTypeBaoma,這個(gè)是swift編譯器編譯后的值,OC可以使用。
10、結(jié)構(gòu)體
在oc中是不能調(diào)用struct里面的內(nèi)容的,你想在類似class前面加個(gè) @objc 的方法加在struct 前面是不行的,那但是我們又想在oc中調(diào)用struct的屬性,那怎么辦呢?我們只能夠再建一個(gè)Swift的類,在類里寫(xiě)個(gè)方法來(lái)返回struct中的值
Swift代碼如下:
struct CarStruct {
var name: String?
var price: Int?
init(name: String, price: Int) {
self.name = name
self.price = price
}
}
@objcMembers class CarClass: NSObject {
var car = CarStruct(name: "BMW", price: 30)
func getCarName() -> String {
return car.name ?? ""
}
func getCarPrice() -> Int {
return car.price ?? 0
}
}
在OC中調(diào)用結(jié)構(gòu)體會(huì)提示找不到,所以可以使用
CarClass這個(gè)類來(lái)間接的使用CarStruct這個(gè)結(jié)構(gòu)體。
@interface ViewController ()
//@property(nonatomic, strong) CarStruct car;
@property(nonatomic, strong) CarClass* car;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (NSString *)getCarName {
return [self.car getCarName];
}
11、OC的block與Swift的閉包
在混編中,OC中的block在Swift中可以正常使用,Swift的閉包在OC中也是可以正常使用的,測(cè)試代碼如下,可以看下:
- OC類:
@interface ViewController : UIViewController
@property (nonatomic, strong) void (^myblock) (NSString *name);
@property(nonatomic, strong) SwiftText *swiftVc;
@end
// 測(cè)試swift閉包
- (void)textSwiftClosures {
self.swiftVc = [[SwiftText alloc] init];
self.swiftVc.textClosures = ^{
printf("aaaaa");
};
}
- Swift類
@objcMembers class SwiftText: NSObject {
// OC類
var ocViewController: ViewController?
// 測(cè)試閉包
var textClosures = {}
override init() {
super.init()
}
func textOcBlock() {
self.ocViewController = ViewController()
self.ocViewController?.myblock = { name in
print(name ?? "")
}
}
}
12、OC中的宏
Swift 中是不能使用OC中的宏定義語(yǔ)法,Swift是有命名空間的,所以我們可以將原本OC中不需要接受參數(shù)的宏,定義成 let常量或枚舉,將需要接受參數(shù)的宏定義成函數(shù)。
- 如oc的宏:
#define kScreenHeight [UIScreen mainScreen].bounds.size.height
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
- 在swift中定義為全局常量:
let kScreenHeight = UIScreen.main.bounds.height
let kScreenWidth = UIScreen.main.bounds.width
13、元組
元組是Swift特有的,在OC中是沒(méi)有的,OC調(diào)用不了Swift中的元組,所以在Swift中對(duì)于OC可能用到的方法中,返回值和參數(shù)都不能是元組,Swift中OC可能用到的屬性變量也不能是元組。
15、高階函數(shù)
Swift 中定義的高階函數(shù)(比如filter、map、redux等),OC是不能調(diào)用的。
三、API混編適配
3.1、可選類型
3.1.1、關(guān)鍵字nonnull、nullable
Objective-C指針既可以是一個(gè)有效值,也可以是空值,例如 null 或者 nil,這與Swift里的可選值行為十分相似。
如果我們?cè)僮屑?xì)想一下,就會(huì)發(fā)現(xiàn)在 Objective-C 里面,每個(gè)指針類型實(shí)際上都是可選類型,每個(gè)非指針類型都是非可選類型。可是大部分時(shí)間,一個(gè)屬性或者方法不會(huì)處理輸入值是 nil 的情況,或者永遠(yuǎn)不會(huì)返回 nil。
所以,默認(rèn)情況下 Swift 會(huì)把 Objective-C 里的指針當(dāng)做隱式解析可選類型,因?yàn)樗J(rèn)為這個(gè)值大部分情況下不會(huì)是 nil,但它也不完全確定。
雖說(shuō)這種轉(zhuǎn)換規(guī)則沒(méi)什么毛病,但大量的隱式解析可選類型讓代碼變得意圖模糊,好在我們有兩個(gè)關(guān)鍵字注解可以去描述這個(gè)意圖,他們分別是 nonnull 和 nullable。這兩個(gè)注解在 Objective-C 里面只是用于記錄開(kāi)發(fā)者的意圖,不是強(qiáng)制的。但 Swift 會(huì)用到這些信息來(lái)決定是否轉(zhuǎn)換為可選類型。

3.1.2、宏 NS_ASSUME_NONNULL_BEGIN、NS_ASSUME_NONNULL_END
除了
nonnull和nullable以外,還有一對(duì)配合使用的宏NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END可以讓我們的代碼更簡(jiǎn)潔。
在這兩個(gè)宏包裹的代碼片段中,屬性,?法參數(shù)和返回值的默認(rèn)注解都是 nonnull 類型的,這樣一來(lái),我們就可以刪掉許多冗余的代碼。

3.1.3、底層關(guān)鍵字
但是上面的關(guān)鍵字和宏并不適用所有的場(chǎng)合,例如你將 nonnull 直接放在常量前會(huì)觸發(fā)編譯器錯(cuò)誤。還好這種錯(cuò)誤是有解決辦法的!
nonull 和 nullable 只能在方法和屬性上使用,如果想拓展其使用場(chǎng)景,就需要直接調(diào)用這兩關(guān)鍵字底層的內(nèi)容,也就是 _Nonnull 和 _Nullable。
這兩種注解除了可以用在全局常量,全局函數(shù)的場(chǎng)景外,也適用于任何 Objective-C 任何地方的指針類型,甚至那種指向指針類型的指針。

3.2、Int類型
大多數(shù)人使用 NSUInteger 是為了表明這個(gè)數(shù)值是?負(fù)的,雖然這種用法是可行的,但它還是會(huì)存在一些嚴(yán)重的安全漏洞(NSUInteger 的大小會(huì)因架構(gòu)不同而產(chǎn)生一些變化),所以這種設(shè)計(jì)思路并沒(méi)有被 Swift 采用。
Swift 采取的策略是在進(jìn)?有符號(hào)運(yùn)算時(shí),要求開(kāi)發(fā)者必須將?符號(hào)類型轉(zhuǎn)換為有符號(hào)類型,如果 Swift 在處理?符號(hào)運(yùn)算時(shí),產(chǎn)?了負(fù)值,就會(huì)直接停?運(yùn)算。
也正是這樣的策略,會(huì)讓 Swift 中的 Int 和 UInt 在混合起來(lái)使用的時(shí)候變得很麻煩,當(dāng)然,這在 Objective-C ??的也是一個(gè)棘手的問(wèn)題。
所以混合使用 Int 和 UInt 并不是 Swift 里的最佳實(shí)踐,在 Swift 里面,我們建議將所有進(jìn)行數(shù)值計(jì)算的類型聲明為 Int,即使它永遠(yuǎn)不可能為負(fù)數(shù)。
對(duì)于 Apple 自己的框架,他們?cè)O(shè)置了一個(gè)白名單用于將 NSUInteger 轉(zhuǎn)換為 Int。
對(duì)于開(kāi)發(fā)者而言,決定權(quán)在我們自己手里,我們可以??選擇是否使? NSInteger,但 Apple 的工程師強(qiáng)烈推薦你這么做。
或許在 Objective-C ??差距不是很?,但在 Swift ??很重要!
3.3、對(duì)Swift隱藏某個(gè)API
在做一個(gè)公共庫(kù)時(shí),可能會(huì)面臨一個(gè)問(wèn)題:其中的某個(gè)方法不希望Swift使用,這時(shí)候只需要在原有的頭?件?將相應(yīng)的 Objective-C 的?法標(biāo)記為NS_REFINED_FOR_SWIFT即可。
例如:
- (instancetype)initWithNameComponent:(nullable NSString *)name NS_REFINED_FOR_SWIFT;
這樣在Swift調(diào)用的時(shí)候,編譯器會(huì)將該方法隱藏起來(lái),比如代碼補(bǔ)全的時(shí)候。其實(shí)這樣不代表就不能調(diào)用了,這個(gè)標(biāo)記做的工作其實(shí)很簡(jiǎn)單,是在對(duì)應(yīng)地Swift版本的API開(kāi)頭增加了兩個(gè)下劃線,所以如果非要使用,也可以通過(guò)調(diào)用__+方法調(diào)用。
3.4、對(duì)Swift重命名方法名
Swift 和 Objectiv-C 的命名風(fēng)格是有所不同,為了解決 API 風(fēng)格上的問(wèn)題,Swift 會(huì)根據(jù)一些規(guī)則重命名,通常這個(gè)結(jié)果還不錯(cuò),但這畢竟是計(jì)算機(jī)的審美結(jié)果,很難滿足開(kāi)發(fā)者的訴求,所以針對(duì)一些不滿足的地方,咱們可以自己使用NS_SWIFT_NAME來(lái)進(jìn)行命名OC方法對(duì)應(yīng)地Swift中API的方法名。
OC:
- (BOOL)driveCarByHand:(Int)handType
NS_SWIFT_NAME(driveCar(handType:));
重命名后的供Swift調(diào)用的API:
func driveCar(handType: Int) -> bool
四、總結(jié)
寫(xiě)到這里,基本已經(jīng)總結(jié)了項(xiàng)目中常見(jiàn)的在混編過(guò)程中會(huì)遇到的問(wèn)題,從常用的屬性、方法、類等到框架的API設(shè)計(jì),當(dāng)然本篇文章主要寫(xiě)的是在混編時(shí)候適配的Tips,所以重點(diǎn)寫(xiě)的是編譯器沒(méi)有幫我們做好的工作,其實(shí)在混編中,編譯器大部分幫助我們做的還是比較友好的,在大部分功能上可以做到OC和Swift的無(wú)縫銜接調(diào)用。