一、背景
现在
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 中定义的高阶函数(比如filter
、map
、redux
等),OC是不能调用的。
三、API混编适配
3.1、可选类型
3.1.1、关键字nonnull、nullable
Objective-C
指针既可以是一个有效值,也可以是空值,例如 null 或者 nil,这与Swift
里的可选值
行为十分相似。
如果我们再仔细想一下,就会发现在 Objective-C 里面,每个指针类型实际上都是可选类型,每个非指针类型都是非可选类型。可是大部分时间,一个属性或者方法不会处理输入值是 nil 的情况,或者永远不会返回 nil。
所以,默认情况下 Swift 会把 Objective-C 里的指针当做隐式解析可选类型
,因为它认为这个值大部分情况下不会是 nil,但它也不完全确定。
虽说这种转换规则没什么毛病,但大量的隐式解析可选类型让代码变得意图模糊,好在我们有两个关键字注解可以去描述这个意图,他们分别是 nonnull
和 nullable
。这两个注解在 Objective-C 里面只是用于记录开发者的意图,不是强制的。但 Swift 会用到这些信息来决定是否转换为可选类型。
3.1.2、宏 NS_ASSUME_NONNULL_BEGIN、NS_ASSUME_NONNULL_END
除了
nonnull
和nullable
以外,还有一对配合使用的宏NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
可以让我们的代码更简洁。
在这两个宏包裹的代码片段中,属性
,⽅法参数
和返回值的默认注解
都是 nonnull 类型的,这样一来,我们就可以删掉许多冗余的代码。
3.1.3、底层关键字
但是上面的关键字和宏并不适用所有的场合,例如你将 nonnull 直接放在常量前会触发编译器错误。还好这种错误是有解决办法的!
nonull
和 nullable
只能在方法和属性上使用,如果想拓展其使用场景,就需要直接调用这两关键字底层的内容,也就是 _Nonnull
和 _Nullable
。
这两种注解除了可以用在全局常量,全局函数的场景外,也适用于任何 Objective-C 任何地方的指针类型,甚至那种指向指针类型的指针。
3.2、Int类型
大多数人使用 NSUInteger
是为了表明这个数值是⾮负的,虽然这种用法是可行的,但它还是会存在一些严重的安全漏洞(NSUInteger 的大小
会因架构不同而产生一些变化),所以这种设计思路并没有被 Swift 采用。
Swift
采取的策略是在进⾏有符号运算时,要求开发者必须将⽆符号类型
转换为有符号类型
,如果 Swift 在处理⽆符号运算时,产⽣了负值,就会直接停⽌运算。
也正是这样的策略,会让 Swift
中的 Int
和 UInt
在混合起来使用的时候变得很麻烦,当然,这在 Objective-C
⾥⾯的也是一个棘手的问题。
所以混合使用 Int
和 UInt
并不是 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的无缝衔接调用。