Objective-C与Swift混编tips

一、背景

TIOBE网站发布了2020年的编程语言排行榜.png

现在Objective-C在Apple那边已经是放养的孩子了,除了每年的修修补补,已经不再做大的改动,而Swift变成了亲儿子,每年一个大版本的更新,特别是Swift3.0版本之后,Swift已经趋于稳定,使用的用户已超过了Ojective-C,所以对于iOS开发者来说,掌握Swift开发变成了必备的技能。

对于公司新项目来说可以直接上纯Swift项目,但对于一些老项目,留给开发者的就只有使用Swift重构混编两条路了,本文就针对混编重点讲解下一些实用的tips,以便在进行混编时候更好的使用。
(这里只讲具体的小技巧,对于基础的混编环境网上很多,可以自己搜索,这里不做展开)。

二、常用混编tips

1、使用 @objc 修饰

如果Swift类里面的某个成员变量或者方噶想要暴露给Objective-C调用,需要在前面加上 @objc

    @objc let name: String
    
    @objc func eat() {
        print('aaa')
    }
2、使用 @objcMembers 修饰类

使用Tip1方法,如果遇到多个成员变量和方法都需要暴露,每个都加@objc显得太冗余,这时候可以使用 @objcMembers 修饰这个类,这样默认所有成员都会暴露给OC(包括扩展中定义的成员)
最终是否成功暴露,还需要考虑成员自身的访问级别(private、fileprivate不会暴露)

@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、通过 @objc 重命名Swift暴露给OC的类名、属性名、函数名等

因为Objective-C没有命名空间,所以类名一般都会加上前缀,而Swift则不需要,为了符合OC的使用习惯,可以将Swift的类重命名后暴露给OC进行混编调用,这样使用起来就很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中的调用如下:

EHICar *car = [[EHICar alloc] initWithPrice:30 band:@"BMW"]; 
car.name = @"525LI";
[car drive];
[EHICar run]; 
4、选择器

在Swift里面也可以使用选择器,但是对应地方法必须使用 @objc 修饰或者当前类被 @objcMembers 修饰才能使用。

@objcMembers class Car: NSObject {
    
    func textSelector(str: String) {
        print(str)
    }
    
    func run() {
        perform(#selector(textSelector(str:)))
    }
}
5、String与NSString

使用过Swift的应该都知道Swift在3.0版本对String进行了大改,API设计上和NSString有了很大的不同,如前缀、后缀、索引、Substring等:

var str = "123456" 

func textPrint() {
    print(str.hasPrefix("123")) // true 
    print(str.hasSuffix("456")) // true
    print(str.prefix(3)) // 从开头截取三位,结果为:123
    print(str.suffix(3)) // 从末尾截取三位,结果为:456
}


var str = "1_2"
func textStr() {
  // 插入 单个字符,结果是:1_2_
  str.insert("_", at: str.endIndex)
  // 插入 字符串,结果是:1_2_3_4
  str.insert(contentsOf: "3_4", at: str.endIndex)
  // 在某个索引后面插入,结果是:1666_2_3_4
  str.insert(contentsOf: "666", at: str.index(after: str.startIndex))
  // 在某个索引后面插入,结果是:1666_2_3_8884
  str.insert(contentsOf: "888", at: str.index(before: str.endIndex))
  // 在某个索引后面插入,偏移索引,结果是:1666hello_2_3_8884
  str.insert(contentsOf: "hello", at: str.index(str.startIndex, offsetBy: 4))
  // 删除值为1的第一个索引的值,,结果是:666hello_2_3_8884
  str.remove(at: str.firstIndex(of: "1")!)
  // 删除值为字符为 6 的字符,结果是:hello_2_3_8884
  str.removeAll { $0 == "6" }
  //删除某个区间的字符
  var range = str.index(str.endIndex, offsetBy: -4)..

所以在混编的时候使用起来就很不方便了,这时候可以考虑将String转换为NSString使用。

6、协议

protocol对大家来说都很熟悉了,但是OC中的协议对开发者有一个痛点就是,OC的协议严格来说只能说是接口,因为不能对协议中定义的方法进行默认的实现,具体的实现还需要依赖实现类,这样在使用时候就有很大的局限性。而Swift里面的协议相对来说就很强大了,可以在 extension 中提供默认实现。所以在混编的时候可以使用Swift来定义协议(需要@objc修饰才可以在OC中使用),然后在OC和Swift中进行使用,这样就很棒了。且如果是不必实现的函数,函数前要加上 @objc optional

@objc protocol CarProtocol {
    
    func run()
}

extension CarProtocol {
    func run() {
        print("Car run")
    }
}
7、runtime

OC的东西在Swift里面调用,会调用了 runtime 那套机制;而Swift的东西在OC里面调用,我们打断点看汇编可以发现调用的也是runtime那套机制,而对于swift里面自己的方法走的肯定是Swift的流程,如果我们强行让它走OC那套runtime机制,可以在 run() 函数前加 dynamic。

class Car: NSObject {
    @objc dynamic func run() {
       printf("Car run")
    }
}
8、swift中使用KVO

Swift 要使用 KVO ,必须满足以下条件:

  • 属性所在的类、监听器最终继承自 NSObject

  • 用 @objc dynamic 修饰对应的属性

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 //结果:oldValue=10000000.0,newValue=999999999.9
9、枚举

OC如果想要调Swift中的枚举值时,Swift的枚举需要使用 @objc 进行修饰,然后OC就可以使用,需要注意的是,如果需要在OC中进行该枚举值的调用,书写规则为枚举名+case的值。

: Swift的枚举比OC强大的很多,所以在混编时,需要定义为Int类型后,才能供OC调用。


@objc enum CarType: Int {
    case baoma = 0
    case benchi
}

OC调用时该枚举值时,可以直接使用 CarType这个枚举,需要使用具体值时如 baoma这个值,可以直接使用 CarTypeBaoma,这个是swift编译器编译后的值,OC可以使用。

10、结构体

在oc中是不能调用struct里面的内容的,你想在类似class前面加个 @objc 的方法加在struct 前面是不行的,那但是我们又想在oc中调用struct的属性,那怎么办呢?我们只能够再建一个Swift的类,在类里写个方法来返回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中调用结构体会提示找不到,所以可以使用 CarClass 这个类来间接的使用 CarStruct 这个结构体。

@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中也是可以正常使用的,测试代码如下,可以看下:

  • OC类:
@interface ViewController : UIViewController

@property (nonatomic, strong) void (^myblock) (NSString *name);

@property(nonatomic, strong) SwiftText *swiftVc;

@end

// 测试swift闭包
- (void)textSwiftClosures {
    
    self.swiftVc = [[SwiftText alloc] init];
    self.swiftVc.textClosures = ^{
        printf("aaaaa");
    };
}
  • Swift类
@objcMembers class SwiftText: NSObject {

    // OC类
    var ocViewController: ViewController?
    // 测试闭包
    var textClosures = {}
    
    override init() {
        super.init()
    }
    
    func textOcBlock() {
        self.ocViewController = ViewController()
        self.ocViewController?.myblock = { name in
            print(name ?? "")
        }
    }
}
12、OC中的宏

Swift 中是不能使用OC中的宏定义语法,Swift是有命名空间的,所以我们可以将原本OC中不需要接受参数的宏,定义成 let常量枚举,将需要接受参数的宏定义成函数

  • 如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中是没有的,OC调用不了Swift中的元组,所以在Swift中对于OC可能用到的方法中,返回值和参数都不能是元组,Swift中OC可能用到的属性变量也不能是元组。

15、高阶函数

Swift 中定义的高阶函数(比如filtermapredux等),OC是不能调用的。

三、API混编适配

3.1、可选类型

3.1.1、关键字nonnull、nullable

Objective-C 指针既可以是一个有效值,也可以是空值,例如 null 或者 nil,这与 Swift 里的可选值行为十分相似。

如果我们再仔细想一下,就会发现在 Objective-C 里面,每个指针类型实际上都是可选类型,每个非指针类型都是非可选类型。可是大部分时间,一个属性或者方法不会处理输入值是 nil 的情况,或者永远不会返回 nil。

所以,默认情况下 Swift 会把 Objective-C 里的指针当做隐式解析可选类型,因为它认为这个值大部分情况下不会是 nil,但它也不完全确定。

虽说这种转换规则没什么毛病,但大量的隐式解析可选类型让代码变得意图模糊,好在我们有两个关键字注解可以去描述这个意图,他们分别是 nonnullnullable。这两个注解在 Objective-C 里面只是用于记录开发者的意图,不是强制的。但 Swift 会用到这些信息来决定是否转换为可选类型。

可选.png
3.1.2、宏 NS_ASSUME_NONNULL_BEGIN、NS_ASSUME_NONNULL_END

除了 nonnullnullable 以外,还有一对配合使用的宏 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 可以让我们的代码更简洁。

在这两个宏包裹的代码片段中,属性⽅法参数返回值的默认注解都是 nonnull 类型的,这样一来,我们就可以删掉许多冗余的代码。

宏.png

3.1.3、底层关键字

但是上面的关键字和宏并不适用所有的场合,例如你将 nonnull 直接放在常量前会触发编译器错误。还好这种错误是有解决办法的!

nonullnullable 只能在方法和属性上使用,如果想拓展其使用场景,就需要直接调用这两关键字底层的内容,也就是 _Nonnull_Nullable

这两种注解除了可以用在全局常量,全局函数的场景外,也适用于任何 Objective-C 任何地方的指针类型,甚至那种指向指针类型的指针。

底层关键字.png

3.2、Int类型

大多数人使用 NSUInteger 是为了表明这个数值是⾮负的,虽然这种用法是可行的,但它还是会存在一些严重的安全漏洞(NSUInteger 的大小会因架构不同而产生一些变化),所以这种设计思路并没有被 Swift 采用。

Swift 采取的策略是在进⾏有符号运算时,要求开发者必须将⽆符号类型转换为有符号类型,如果 Swift 在处理⽆符号运算时,产⽣了负值,就会直接停⽌运算。

也正是这样的策略,会让 Swift 中的 IntUInt 在混合起来使用的时候变得很麻烦,当然,这在 Objective-C ⾥⾯的也是一个棘手的问题。

所以混合使用 IntUInt 并不是 Swift 里的最佳实践,在 Swift 里面,我们建议将所有进行数值计算的类型声明为 Int,即使它永远不可能为负数。

对于 Apple 自己的框架,他们设置了一个白名单用于将 NSUInteger 转换为 Int。

对于开发者而言,决定权在我们自己手里,我们可以⾃⾏选择是否使⽤ NSInteger,但 Apple 的工程师强烈推荐你这么做。

或许在 Objective-C ⾥⾯差距不是很⼤,但在 Swift ⾥⾯很重要!

3.3、对Swift隐藏某个API

在做一个公共库时,可能会面临一个问题:其中的某个方法不希望Swift使用,这时候只需要在原有的头⽂件⾥将相应的 Objective-C 的⽅法标记为NS_REFINED_FOR_SWIFT即可。

例如:

- (instancetype)initWithNameComponent:(nullable NSString *)name NS_REFINED_FOR_SWIFT;

这样在Swift调用的时候,编译器会将该方法隐藏起来,比如代码补全的时候。其实这样不代表就不能调用了,这个标记做的工作其实很简单,是在对应地Swift版本的API开头增加了两个下划线,所以如果非要使用,也可以通过调用__+方法调用。

3.4、对Swift重命名方法名

Swift 和 Objectiv-C 的命名风格是有所不同,为了解决 API 风格上的问题,Swift 会根据一些规则重命名,通常这个结果还不错,但这毕竟是计算机的审美结果,很难满足开发者的诉求,所以针对一些不满足的地方,咱们可以自己使用NS_SWIFT_NAME来进行命名OC方法对应地Swift中API的方法名。

OC:

- (BOOL)driveCarByHand:(Int)handType
NS_SWIFT_NAME(driveCar(handType:));

重命名后的供Swift调用的API:

func driveCar(handType: Int) -> bool

四、总结

写到这里,基本已经总结了项目中常见的在混编过程中会遇到的问题,从常用的属性、方法、类等到框架的API设计,当然本篇文章主要写的是在混编时候适配的Tips,所以重点写的是编译器没有帮我们做好的工作,其实在混编中,编译器大部分帮助我们做的还是比较友好的,在大部分功能上可以做到OC和Swift的无缝衔接调用。

你可能感兴趣的:(Objective-C与Swift混编tips)