用私有的Objective-C成员创建Swift框架。 黄金三镖客

我最近正在开发一个封闭源代码Swift框架。 不幸的是,代码的某些部分位于Objective-C中(依赖于纯C库)。 在本文中,我重点介绍了使用混合Swift / ObjC代码的框架时的一些问题。 我展示了一些使ObjC代码内部化时尝试的方法。 使它对于框架用户不可见,但是仍然可以从Swift代码中访问,这让我感到非常头痛。 另外,我也分享了一个最终解决方案。

问题

关于Swift / ObjC互操作性的文章和教程很多,但是很少关注框架目标。 看来,即使您已准备好一切, 也仍然几乎不可能对框架用户有效地隐藏Objective-C部分 (至少只要您希望将其公开给Swift部分即可)。

假设您创建了一个名为MyFramework的框架, 其中包含以下成员:

好的,我的意思是“默认”的Swift ObjC互操作性。 简要回顾一下:

要将Swift暴露给Objective-C

  • 目标需要定义一个模块
  • 将Swift成员标记为@objc
  • 使Swift成员从NSObject继承

Objective-C暴露给Swift

  • 将ObjC导入放置在桥接头文件中(如果可用)
  • 将ObjC导入放置在伞状标题中(对于框架)

你可以在这里读更多关于它的内容:

  • 苹果:将Swift导入Objective-C
  • 苹果:将Objective-C导入Swift
  • Jen Sipila关于Swift互操作性的文章

这种方法存在一个重大问题:

“ Swift会 在您的伞形标题中 看到您公开 公开的 每个 标题。 该框架中的Objective-C文件的内容可以从该框架目标内的任何Swift文件中自动获得,而无需导入语句。”

因此,它没有提供所需的行为 。 此外,我注意到,这常常导致一连串的“包括非模块化标头”问题。 从字面上看,您可以重写一半的标头。 只要您不导入控件之外的任何标头,例如某些静态库中的标头,它仍然是可行的。 加起来:

优点:

  • ObjC的预期方式-> Swift互操作性

缺点:

  • 暴露给Swift的所有Objective-C成员也是公开可见的
  • 导入非模块化标头的常见问题

2. Bad –一个私有模块映射几乎可以正常工作

当寻找可能的解决方案时,这是最受欢迎的解决方案之一。 它依赖于创建定义显式submodule私有modulemap文件。 我不想过多地了解如何做,这里是一些有用的链接:

  • stackoverflow:如何在Swift框架中导入私有框架头?
  • https://code.i-harness.com/zh/q/21e3c64
  • 境界学院
  • Omar Abdelhafith博客

简而言之,您可以手动定义带有私有头文件的显式子模块,例如MyFramework.Private 。 然后,您可以通过以下Swift代码访问它:

就我而言,仅模块映射还不够。 研究成果没有希望。 似乎没有办法仅在同一框架目标内私有地公开Objective-C标头 ,而又不对它们公开或多或少地公开。 到目前为止,都是如此。

您可能会想知道在写有关找到可行解决方案时我当时的想法。 让我与我自己分享一下讨论的一部分:

我: “好,让我们再想一想,您想要实现什么?”

Me2: “我想将Objective-C标头公开给Swift,但只在框架目标内部公开,而又不向框架用户公开。”

我: “你呢? 这正是您所需要的吗?”

Me2: “好吧,我的ObjC类本来是内部的,但我想从Swift代码中使用它们。”

我: “好。 您实际上将如何“使用”这些课程?”

Me2: “调用一些方法,访问变量?”

我: “您需要’知道班级’吗?”

Me2: “好吧,我只需要知道有一个方法或一个变量,那么协议就可以了。”

我: “如果您解决了这个问题? 您可以拥有一个内部Swift成员并在Objective-C中使用它吗?”

Me2: “似乎答案是肯定的,当然有一些限制。”

我: “好。 那么您可以让私有的Objective-C成员采用内部的Swift协议吗?”

Me2: “似乎如此……这是否意味着我不需要了解任何Objective-C类,因为我仅与采用Swift协议的事物进行交互?

我: “好吧,让我们尝试。”

第1步:创建与ObjC成员匹配的Swift协议

让我们根据这个想法稍微更新一下框架。 迅速的代码根本不需要看到Objective-C部分。 但是,它可以在采用与我们的ObjC类功能匹配的协议的任何设备上运行:

与私有ObjC类相同的功能

步骤2:公开内部Swift协议

由于MyPrivateClassProtocol是内部的,因此默认情况下它不会成为MyFramework-Swift.h的一部分。 因此,让我们创建一个附加的头文件来链接我们所有的内部Swift和ObjC成员:

注意: 如果不确定如何填充此标头,可以将Swift成员设为公开并进行构建。 这将在派生数据中生成“ ModuleName-Swift.h”标头。 检查它可以使您对如何执行操作有所了解。 然后再次使其内部。

步骤3:在ObjC中采用Swift协议

现在,我们可以将Objective-C中的Swift协议视为MyPrivateClassProtocol ,我们可以采用它。

剩下的最后一个问题是……

步骤4:如何创建实例

Me2: “好的,从理论上讲,我可以使用它。 但是再次,如果我不知道采用协议的类,如何获得它的实例?”

我: “还记得工厂模式吗?”

上面解决方案的问题在于,如果不在具体类上使用init ,就无法实例化Protocol的新实例,可以吗? 让我们考虑一下:

让我们专注于最后一部分。 我们可以创建一个包含MyPrivateClass.Type的帮助器工厂类:

示例工厂类

注意: 在Swift部分,MyPrivateClass是一个协议,而在ObjC中,它是一个类。 看起来似乎模棱两可,但我想在双方保持相同的名字。

上面的工厂将以与MyPrivateClass协议定义相同的方式(内部)公开给Objective-C。

最后要做的一件事是注册工厂要使用的Objective-C类。 我们需要先使用它,然后才能使用它。

幸运的是,Obj-C运行时可以完成一些工作:

利用ObjC运行时的私有类实现在Swift端的工厂内进行注册

看来, 我们可以在Swift中使用Objective-C成员,而根本不向Swift公开ObjC代码! 我们需要做的就是将Swift部件暴露给Objective-C,我们可以在内部进行😎。

让我们总结一下:

优点:

  • 它有效地向框架用户隐藏了Objective-C成员
  • 您被迫进行界面交互。 通常这是一件好事,可以提高整体可测试性

缺点:

  • 您需要一些其他成员来跨越Swift-ObjC边界
  • 为每个ObjC成员生成Swift协议是一项手动工作

摘要

示例项目在GitHub上可用:

https://github.com/amichnia/Swift-framework-with-private-ObjC-example

整个方法需要一些手动工作,并且需要重新设计如何稍微初始化依赖项。 您需要生成协议,将其手动桥接到ObjC,并一路维护工厂。 但是,这是迄今为止真正拥有内部Objective-C实现并在Swift代码中使用它的唯一方法。

注意: 在示例中,我使用了允许您注册类型的工厂,然后在该类型上使用了init。 您可以使用这种方法。 如果您不喜欢将初始化添加到协议中,则可以尝试使用闭包/阻止作为工厂: static var createPrivateClass: (() -> MyPrivateClass)!

感谢您的阅读

如果喜欢的话,👏

特别感谢蒂莫西·安德森(Timothy Anderson)在本文中允许我使用他的插图。