Objective-C與Swift混編tips

一、背景

TIOBE網(wǎng)站發(fā)布了2020年的編程語(yǔ)言排行榜.png

現(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è)意圖,他們分別是 nonnullnullable。這兩個(gè)注解在 Objective-C 里面只是用于記錄開(kāi)發(fā)者的意圖,不是強(qiáng)制的。但 Swift 會(huì)用到這些信息來(lái)決定是否轉(zhuǎn)換為可選類型。

可選.png
3.1.2、宏 NS_ASSUME_NONNULL_BEGIN、NS_ASSUME_NONNULL_END

除了 nonnullnullable 以外,還有一對(duì)配合使用的宏 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 可以讓我們的代碼更簡(jiǎn)潔。

在這兩個(gè)宏包裹的代碼片段中,屬性,?法參數(shù)返回值的默認(rèn)注解都是 nonnull 類型的,這樣一來(lái),我們就可以刪掉許多冗余的代碼。

宏.png

3.1.3、底層關(guān)鍵字

但是上面的關(guān)鍵字和宏并不適用所有的場(chǎng)合,例如你將 nonnull 直接放在常量前會(huì)觸發(fā)編譯器錯(cuò)誤。還好這種錯(cuò)誤是有解決辦法的!

nonullnullable 只能在方法和屬性上使用,如果想拓展其使用場(chǎng)景,就需要直接調(diào)用這兩關(guān)鍵字底層的內(nèi)容,也就是 _Nonnull_Nullable。

這兩種注解除了可以用在全局常量,全局函數(shù)的場(chǎng)景外,也適用于任何 Objective-C 任何地方的指針類型,甚至那種指向指針類型的指針。

底層關(guān)鍵字.png

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 中的 IntUInt 在混合起來(lái)使用的時(shí)候變得很麻煩,當(dāng)然,這在 Objective-C ??的也是一個(gè)棘手的問(wèn)題。

所以混合使用 IntUInt 并不是 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)用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容