在ObjC / Swift混合遗留项目中拆分情节提要

您如何将故事板拆分为多个较小的故事板? 那你为什么呢? 在本文中,我将告诉您为什么要这样做,一路遇到什么问题以及如何解决这些问题。

问题:大型情节提要使iOS开发人员陷入困境

随着FINN应用的发展,最近的故事板也不断发展。 使用情节提要意味着您将获得一些有用的功能,并且可以直观地了解应用程序的工作原理。 我说“可以”,因为不一定是这种情况。 另外,使用情节提要具有一些怪异之处,当多个开发人员在同一个项目上工作时,这些怪异之处并不总是被理解。 对情节提要的任何小的更改都可能意味着对情节提要的xml进行了几处更改。 除此之外,XCode只是因为打开文件而具有重新计算几个情节提要项的坐标的烦人趋势! 还有一个烦人的问题是,故事板越大,处理起来就越慢。 通常,我们至少要等待5秒钟才能打开文件,而且操作通常比较缓慢。

因此,iOS团队很久以前就决定将其拆分,但这项工作不一定容易。 还是小。 甚至有趣。 而且,Jira的任务还指出“当心示波器蠕变……”。 是的,它爬了起来。

FINN应用:一些历史

当前的FINN应用程序于2013年8月启动。在这三年中,该应用程序当然有所增长,在过去的一年左右的时间里,使用Swift添加了一些新功能,其中相当一部分是较旧的Objective-C类已在Swift中重写。 但是,大多数代码库仍然是Objective-C。 因此,Swift和Objective-C之间的互操作性至关重要。

快速查看文件数量可知,我们当前在项目中有171个Swift文件和349个.m文件(当然,这还不包括第三方代码)。 这些文件包含> 44,000行代码,其中包括30,000行Objective-C代码和> 14,000行Swift代码(不包括注释和空格)。

从小开始

我们决定从小处着手,首先将应用程序的名为“ Min FINN”(我的FINN)的部分提取到其自己的故事板上。 这是应用程序的一个相当自治的部分,尽管不是很完整。 该应用程序此部分中的大多数导航都在该功能的内部,但是从应用程序其他部分进入该功能的一些入口点。

某些导航是通过segue完成的,但是有多个位置可以直接通过情节提要实例化场景的视图控制器。 当然,这意味着这样的代码会在整个代码库中乱七八糟:

这样做有两个问题:1.引用“ self.storyboard”,这意味着只有在视图控制器与当前视图控制器位于同一个故事板上时,它才会起作用。2.它使用硬编码字符串来引用故事板标识符“ resultListViewController”,容易出错

我们需要更好的方法。

当我们开始时,这就是MainStoryboard_iPhone的样子:

是的,有点混乱,不一定超级可管理。 从概念上讲,它也不是很有用。 那么我们如何进行拆分呢?

有了XCode 7,我们有了一个不错的新功能-重构到情节提要…:

您只需选择要提取的所有场景,然后此重构功能都将为您创建一个新的情节提要,并将旧情节提要和新情节提要中的场景之间的所有连接连接起来。 但是,如果您支持iOS 8,并且正在使用关系查询(即来自UITabBarController的查询),则此方法将不起作用。

当然,这就是我们的情况。 但是,至少它为我们提供了将场景提取到新的故事板上的便捷捷径。 我们只是删除了得到的故事板参考。 此外,对于非关系情况,我们还是要靠自己。

玩弄两个故事板

好的,现在我们有了一个新的故事板,其中只有“ Min FINN”场景:

分散在代码周围的大多数“ self.storyboard InstantiateViewControllerWithIdentifier”调用仍然有效,但并非全部。 例如,在Min FINN故事板上的某些区域中,我们打开了仍在MainStoryboard上的FINObjectViewController。 Min FINN故事板上有几个场景,可从主故事板上的场景访问。

我们如何在呼叫站点知道给定场景位于哪个情节提要上? 有许多这样的调用。 而且,随着我们后来沿线继续拆分主故事板,这将更加分散。 我们需要一个公共的地方来处理此问题,以便呼叫站点不需要知道给定场景的位置。

生成通用代码

我们首先搜索可以帮助我们的现有工具。 我们找到了几个,其中包括Swiftgen,并全部尝试了。 Swiftgen是一个非常详尽且编写得很好的工具,用于生成可处理多个情节提要的枚举和结构。 但是,它仅支持Swift,不能用于Objective-C,这对我们来说是不行的。 它还对枚举,结构,协议和扩展进行了详尽的阐述,并输出了大量的代码。 我们发现的大多数其他工具都是纯Swift或仅Objc的,或者它们仅为标识符创建了常量。

但是我们非常喜欢Swiftgen创建可直接调用的函数的方法,该方法将返回正确类的实例。

因此,我们决定创建自己的发电机。 第一次迭代是创建一个Swift类,该类不依赖于Objc中无法使用的Swift枚举,并且可以从Swift和Objc中调用它。 这似乎绝对可行,并且我们第一个生成的Swift文件具有如下静态函数:

Objective-C编译器抱怨

看起来还不错吧? 有人会这样想。 除此以外,它在Objective-C中不起作用。 为什么? FINWebViewController是一个Objective-C类,因此在表的两边都具有该名称。 赢得! 但是,FrontPageSearchController是Swift类,因此在表的objc端具有名称“ FINFrontPageSearchViewController”。 h! 当从objc调用InstantiateFrontPageSearchViewController时,它不起作用,因为期望的类是FINFrontPageSearchViewController,而返回的类是FrontPageSearchViewController。

经过大量的试验和错误(我可以向您保证,在这里为您省去了很多痛苦的细节),我们终于放弃了,并决定为objc和Swift生成单独的实例化器类。 赢得! 还是吗?

并不是的。 由于objc和Swift都使用了某些Swift视图控制器,因此我们用objc名称对其进行注释:

很好,但是由于我们的Python脚本解析了故事板文件并提取了故事板标识符及其各自的自定义类名(如果有),因此我们有了带有前缀(objc类)的类名和不带前缀(Swift)的类名。类)。 这使我们能够在生成时检查此前缀。 在生成Objc代码时,我们将前缀添加到Swift类名中,而在生成Swift代码时,我们将其保留不变。 请记住,在情节提要中,使用了非前缀的Swift类名。

现在,这导致了如下编译器警告:

您的效率高了吗? 即使UserAdListViewController类使用前缀名称进行注释,这也不起作用。 由于我们在项目中对警告实行零容忍,因此我们需要解决此问题。 将其强制转换为应该返回的类呢? 让我们试一下:

警告不见了! 现在它可以工作了! 是?

不会。尽管我们将在情节提要中实例化的Viewcontroller实例化为UserAdListViewController,并标注为FINUserAdListViewController,但这不会返回FINUserAdListViewController objc实例。 它返回什么? UIViewController…

(要清楚,我在这里列出的障碍可能不足我们遇到的障碍和死胡同的一半。如果我全部列出这些障碍,您可能会很无聊。坦率地说,我什至不记得它们都可以了。这可能是一件好事。

到这个时候,我开始感到有些被淹没了。 并反复问自己,为什么我选择这个任务作为团队的新手来做的第一项任务。

好吧,如果我们在故事板场景中将此视图控制器定义为“ FINUserAdListViewController”(带有objc注释的名称)怎么办? 当然,这必须在Objc中起作用吗?

瞧,确实如此! 现在,Objective-C可以识别该类。

Swift编译器抱怨

等一下。 现在生成的Swift类不起作用吗?

当然,在Swift领域中没有此类。 应该引用UserAdListViewController。 没问题,我们现在在生成Swift代码时就删除前缀。 可是等等。 我们知道一个类是Objc类还是Swift类的方法是检查此前缀,对吗? 那么,如何确定在情节提要中找到的类现在都带有前缀时是否为Swift类呢?

好。 Objc类有什么Swift类所没有的,可以通过脚本轻松访问? 头文件。 因此,我们创建了一个Python函数,该函数爬网项目中的所有文件并收集Set中的所有头文件名。 然后,在生成Swift代码时,我们针对此Set测试每个类名(带有添加的“ .h”)。 集合中是否包含具有该名称的条目? 是-> Objc类。 否-> Swift类。

可以对Swift文件执行此操作吗? 不,因为Swift 文件名之间不一定存在1-1的关联。

信不信由你:这是最后一个障碍,我们现在已经为Objc和Swift生成了代码。 对于Objc,生成的代码如下所示:

在呼叫站点:

Swift生成的代码:

在呼叫站点:

这就是您所能获得的那样简单。

好吧,差不多。 您可能已经注意到我们有一个Storyboard标识符的Swift枚举。 当然,我们可以跳过枚举,而直接在实例化方法中使用标识符字符串。 我们暂时还不确定,因为我们认为我们可能会将枚举用于其他用途。 但是,我们可能最终会在以后删除枚举。

如您所见,这不是一项简单的任务,Swift / Objective-C in(ter)的可操作性有些粗糙。 因此,总结一下,我将列出您应遵循的步骤,如果您面临与我们相同的挑战,则在此列表的末尾有指向Python脚本和演示项目的链接。 不幸的是,该脚本没有经过优化和通用,因此您可以即插即用,但是根据自己的需要进行调整应该很容易。 随意对其进行泛化并创建请求请求,以增强该脚本对其他人的有用性!

设置项目以进行生成

1-选择应用程序中较小的,具有一定自治性的部分,该部分将获得第一个单独的故事板。

2-选择必要的场景,转到“编辑器”->“重构到情节提要”…

3-为新的故事板命名。 如果您不使用关系选项,则生成的情节提要参考将很适合您,您可以离开它们!

4 —在“构建阶段”下创建一个运行脚本,该脚本将触发Python脚本(遵循GitHub项目中的指南)。

在演示项目中可以看到,MWStoryboardScenes.py文件应放在项目中的某个位置。 在我们的项目中,我们有一个用于这些内容的Scripts文件夹,并且不会将其添加到XCode项目中(但是,它是作为项目一部分由Git处理的)。

现在, 构建源文件之前 ,此脚本需要在每次构建项目时运行。 有关如何进行设置的指南,请查看GitHub存储库自述文件。 您还需要设置故事板的路径。

首次运行此脚本时,您具有实例化视图控制器所必需的功能。

5 —查找通过情节提要(而不是通过segue)实例化视图控制器的所有位置,并更改这些调用,以使它们使用生成的情节提要功能。

如前所述,GitHub项目的自述文件中提供了有关如何使用生成器的更详尽的解释。

拆分故事板祝您好运🙂