一、背景

iOS项目之Objective-C与Swift混编tips

现在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)..<str.index(before: str.endIndex)
  // hello_2_3_4
  str.removeSubrange(range)
}

所以在混编的时分运用起来就很不方便了,这时分能够考虑将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 会用到这些信息来决定是否转换为可选类型。

iOS项目之Objective-C与Swift混编tips

3.1.2、宏 NS_ASSUME_NONNULL_BEGIN、NS_ASSUME_NONNULL_END

除了 nonnullnullable 以外,还有一对配合运用的宏 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 能够让咱们的代码更简练。

在这两个宏包裹的代码片段中,特点⽅法参数回来值的默许注解都是 nonnull 类型的,这样一来,咱们就能够删掉许多冗余的代码。

iOS项目之Objective-C与Swift混编tips

3.1.3、底层关键字

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

nonullnullable 只能在办法和特点上运用,假如想拓宽其运用场景,就需求直接调用这两关键字底层的内容,也便是 _Nonnull_Nullable

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

iOS项目之Objective-C与Swift混编tips

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的无缝衔接调用。