与Objective-C Swift代码兼容

(本文最初是用俄语撰写的,并在此处发布。)

尽管Apple已向我们提供了有关如何在Objective-C应用程序中使用Swift代码的详细文档(反之亦然),但在此方面还远远不够。 当我需要为Swift框架提供与Objective-C的兼容性时,Apple文档提出了更多问题,然后给出了答案(或者至少留有很多空白)。 密集的搜索证明该主题的照明效果很差:有关StackOverflow的几个问题和一些介绍性文章–这就是我所发现的全部。

本出版物是对找到的信息和我自己的经验的概括。 所描述的所有方法并不能声称是一个很好的实践,它们只是提供了一种解决问题的方法。

TL; DR。 为了在Objective-C中使用Swift代码,必须牺牲一些Swift功能,并为原始的Swift代码编写一个包装程序,该代码将不使用不兼容的功能(例如结构,泛型,枚举关联值,协议扩展等)。 所有包装器类都必须继承NSObject。

开始

因此,我们有一个基于Objective-C的项目以及一些想要在其中使用的Swift代码模块。 例如,它可以是在CocoaPods的帮助下添加的Swift框架。 像往常一样,我们向Podfile添加依赖项,运行pod install ,打开xcworkspace文件。

要使Swift框架可见,就不需要使用整个模块(就像我们在Swift中所做的那样)或单个文件(就像我们在Objective-C中所做的那样)导入。 必须导入的是一个名为-Swift.h的文件-这是自动生成的头文件,该文件是Objective-C代码与Swift公共API之间的连接链接。 看起来像这样:

  #import“ YourProjectName-Swift.h” 

在Objective-C中使用Swift类

如果您可以立即使用Objective-C中的某些Swift类或方法,那么您很幸运:有人为您解决了兼容性问题。 事实是,Objective-C仅消化NSObject继承者和属性的公共API,初始化程序和方法必须由@objc属性标记。

处理自己的代码时,您始终可以继承所需的任何内容并添加任何属性。 当然,在这种情况下,您甚至可以用Objective-C编写,对吗? 因此,最好专注于他人的代码。 我们可以做什么? 写包装纸。

例如,考虑以下Swift类:

 公共类SwiftClass { 
公共功能swiftMethod(){
//此处执行。
}
}

我们创建自己的Swift文件,导入外部Swift模块,创建NSObject继承类,并在该类内部创建想要的Swift类型的私有属性。 最主要的是一个通过属性调用原始Swift类型方法的方法:

 进口基金会 
导入SwiftFramework
 公共类SwiftClassObjCWrapper:NSObject { 
 私人让swiftClass = SwiftClass() 
  @objc 
公共功能swiftMethod(){
swiftClass.swiftMethod()
}
  } 

NSObject@objc属性都可以从Foundation获得。)

显然,我们不能使用相同的名称。 但是我们可以使用原始名称将包装器API公开给Objective-C:

  @objc(SwiftClass) 
公共类SwiftClassObjCWrapper:NSObject {/ * ... * /}

现在,Objective-C调用看起来像原始的Swift类调用:

  SwiftClass * swiftClass = [SwiftClass新]; 
[swiftClass swiftMethod];

在Obective-C中使用Swift方法

不幸的是,我们不能通过@objc标记任何公共方法,而只能在Objective-C中使用它。 我们正在处理不同的编程语言,这些编程语言通常具有不同的机会,有时实现不同的逻辑,并且在Objective-C中显然缺少一些Swift功能。

例如,必须忘记默认参数值。 该方法:

  @objc 
public func anotherSwiftMethod(参数:Int = 1){
//此处执行。
}

…将在Objective-C中看起来像这样:

  [swiftClassObject anotherSwiftMethodWithParameter:1]; 

(这里1只是一个传递的值,默认值不存在。)

方法名称

语言的互操作性具有其自己的桥接名称系统。 在大多数情况下,它令人满意地工作,但有时需要进行干预才能使其可读。 例如, do(thing:)将变成doWithThing:这破坏了想要的命名逻辑)。 @objc再次解救:

  @objc(doThing :) 
公共功能(类型:) {
//此处执行。
}

引发异常的方法

投掷方法(而不是throws功能)具有一个额外的参数-桥接到原始Error NSError指针:

  @objc(doThing:error :) 
公共功能do(thing:Type)throw {
//此处执行。
}

方法用法看起来很熟悉:

  NSError *错误=无; 
[swiftClassObject doThing:thingValue error:&error];
如果(错误){
//处理错误。
}

Swift类型作为参数和返回值

如果method具有非桥接Swift类型的参数或返回了它,那么再次进行Objective-C就无法使用它,而无需进行其他工作。 考虑原始代码:

 公共类SwiftClass { 
 公共功能swiftMethod(){/ * ... * /} 
  } 
 公共类AnotherSwiftClass { 
 公共功能anotherSwiftMethod()-> SwiftClass { 
返回SwiftClass()
}
  } 

合格包装:

  @objc(SwiftClass) 
公共类SwiftClassObjCWrapper:NSObject {
 私人让swiftClassObject:SwiftClass 
  //不需要Objective-C曝光。 
公共初始化(swiftClassObject:SwiftClass){
self.swiftClassObject = swiftClassObject
super.init()
}
  @objc 
公共功能swiftMethod(){
swiftClassObject.swiftMethod()
}
  } 
  @objc(AnotherSwiftClass) 
公共类AnotherSwiftClassWrapper:NSObject {
 私人让anotherSwiftClassObject = AnotherSwiftClass() 
  @objc 
公共功能anotherSwiftMethod()-> SwiftClassObjCWrapper {
返回SwiftClassObjCWrapper(swiftClassObject:anotherSwiftClassObject.anotherSwiftMethod())
}
  } 

和用法:

  AnotherSwiftClass * anotherSwiftClassObject 
= [AnotherSwiftClass new];
SwiftClass * swiftClassObject
= [anotherSwiftClassObject anotherSwiftMethod];
[swiftClassObject swiftMethod];

在Objective-C中使用Swift协议

公开不使用非桥接Swift类型的Swift类协议是没有问题的。 考虑一个相反的:

 公共类SwiftClass {/ * ... * /} 
 公共协议SwiftProtocol { 
func swiftProtocolMethod()-> SwiftClass
}
 公共函数swiftMethod(swiftProtocolObject:SwiftProtocol){ 
//此处执行。
}

这种情况下的类包装器:

  @objc(SwiftClass) 
公共类SwiftClassObjCWrapper:NSObject {
让swiftClassObject = SwiftClass()
}

“包装”协议:

  @objc(SwiftProtocol) 
公用协议SwiftProtocolObjCWrapper:类{
func swiftProtocolMethod()-> SwiftClassObjCWrapper
}

(一个人只能使用类协议, NSObjectProtocol继承人和受NSObject约束的协议。)

最有趣的地方是:让我们声明符合原始协议的Swift类-这就像是原始协议与“包装”协议之间的桥梁:

  class SwiftProtocolWrapper:SwiftProtocol { 
 私人让swiftProtocolObject:SwiftProtocolObjCWrapper 
  init(swiftProtocolObject:SwiftProtocolObjCWrapper){ 
self.swiftProtocolObject = swiftProtocolObject
}
  func swiftProtocolMethod()-> SwiftClass { 
返回swiftProtocolObject
.swiftProtocolMethod()
.swiftClassObject
}
  } 

不幸的是,使用原始协议的方法必须包装为:

  @objc 
公共函数swiftMethodWith(swiftProtocolObject:SwiftProtocolObjCWrapper){
让swiftProtocolObject = SwiftProtocolWrapper(swiftProtocolObject:swiftProtocolObject)
methodOwnerObject.swiftMethodWith(swiftProtocolObject:swiftProtocolObject)
}

不是很明显的连锁店,对不对? 但是,如果类型和协议具有大量方法,则包装代码不会出现在如此不成比例的卷中。

严格来说,符合Objective-C协议很干净:

  @interface ObjectiveCClass:NSObject  
@结束
  @实现ObjectiveCClass 
  -(SwiftClass *)swiftProtocolMethod { 
返回[SwiftClass new];
}
  @结束 

及其用法:

  (ObjectiveCClass *)objectiveCClassObject = [ObjectiveCClass new]; 
[methodOwnerObject swiftMethodWithSwiftProtocolObject:objectiveCClassObject];

在Objective-C中使用Swift枚举

在Objective-C中使用Swift enum ,需要满足的唯一条件是它们必须具有整数原始类型。 之后,可以添加@objc属性。

当我们无法修改现有的enum ,可以像往常一样包装它并使用它的方法:

 枚举SwiftEnum { 
案例优先
案例二
}
  SwiftClass类{ 
  func swiftMethod()-> SwiftEnum { 
//此处执行。
}
  } 
  @objc(SwiftEnum)枚举SwiftEnumObjCWrapper:Int { 
案例优先
案例二
}
  @objc(SwiftClass) 
公共类SwiftClassObjCWrapper:NSObject {
 让swiftClassObject = SwiftClass() 
  @objc 
公共功能swiftMethod()-> SwiftEnumObjCWrapper {
切换swiftClassObject.swiftMethod(){
case .firstCase:返回.firstCase
案例.secondCase:返回.secondCase
}
}
  } 

结论

这就是我今天讨论的全部内容。 当然,将Swift代码集成到Objective-C方面还有更多方面,但是我确信所有这些方面都可以借助上述逻辑来处理。

另一方面,这种方法无疑存在缺陷。 显而易见的例子是,必须编写一些额外的代码,通常是很多代码。 另一个缺陷是,Swift代码被有效地转移到了Objective-C运行时中,并且在工作时有一些开销,运行速度稍慢。 但是,在许多情况下,这种减速是无法察觉的,尤其是在快速的现代设备上。 但是,必须记住,解决方法始终是一种解决方法。

并感谢您的阅读!

如果您想关注我,这是我的Twitter。