iOS上的Weaver依赖注入教程(第1部分)

在本教程中,您将探索如何使用依赖注入(DI)和DI容器来开发强大的iOS应用程序。 为此,我将逐步解释Weaver的示例应用程序的编写方式以及原因。

依赖注入 (DI)基本上意味着“给对象一个实例变量” ¹。

这是一种组织代码的方法,以便对象的逻辑可以将其工作的一部分委托给其他对象(依赖项),而无需对其实例化负责。 因此,依赖性也可以作为抽象对象(协议)注入。 事实证明,对象之间的这种松散耦合使代码更具模块化和灵活性,因此更易于进行单元测试。

尽管DI可以手动实现,但对象初始化器很容易变得非常复杂,从而鼓励开发人员使用单例之类的反模式。 通过生成必要的样板代码将依赖项注入Swift类型并确保干净的依赖关系图 Weaver可以为您提供帮助。

在本教程的最后,您将启动并运行以下应用程序。


在进入代码之前,让我们看看Weaver的功能:

1.扫描Swift代码中的注解
2.组装收集的注释以创建依赖关系图
3.验证图,寻找潜在的依赖周期和不可解决的依赖
4. 生成样板代码

请注意,所有这些步骤都是在编译时发生的,因此,如果其中任何一个失败,它将停止项目的编译。

安装Weaver的最简单方法是使用Homebrew。

如果您的计算机上尚未安装它,请运行以下命令:

  $ / usr / bin / ruby​​ -e“ $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)” 

然后像这样安装Weaver:

  $ brew install韦弗 

如果您不想使用Homebrew,也可以按照项目自述文件中的说明手动安装Weaver。

首先,让我们创建一个名为Sample的项目。

创建在项目根目录下Generated的目录。 在那里,Weaver将生成样板代码。 确保创建目录而不是组

现在,项目层次结构应如下所示:

由于Weaver在编译时工作,因此需要在编译发生之前执行它。 最简单的方法是在项目中添加一个构建阶段。

让我们点击按钮Sample -> Sample -> Build Phases -> + -> New Run Script Phase ,如以下屏幕截图所示。

将其重命名为Weaver ,然后在Shell窗口中添加以下命令行:

  weaver swift --project-path $ PROJECT_DIR / $ PROJECT_NAME --output-path生成的--input-path“ * .swift” 

此命令搜索项目中的所有Swift源文件,并将它们作为Weaver的参数提供,以便它可以分析代码并在Sample/Generated.目录下生成样板Sample/Generated.

确保将此构建阶段移至编译阶段之上。 如果没有,它将执行后期编译,这很可能会阻止项目正确编译。

现在,构建阶段的列表应如下所示:

在这一点上,您应该可以按cmd+b并且项目应正确编译。 如果这样做,您可能会注意到在Generated目录中实际上没有写入任何内容。 这是因为我们还没有开始注入依赖项。

让我们谈谈我们要在此处构建的应用程序。 它将包括:

  • 带有电影列表的屏幕( HomeViewController
  • 带有电影详细信息的屏幕( MovieViewController
  • 带有电影评论的屏幕( ReviewViewController

HomeViewController将是应用程序的根控制器,轻按影片单元时将显示MovieViewController 。 然后,从MovieViewController ,用户将可以点击电影封面图像以显示ReviewViewController

我们将从创建前两个控制器开始,然后我们将负责获取和注入电影。

首先,让我们重写AppDelegate.

在尝试在屏幕上显示电影之前,让我们定义什么是电影。

请注意, Movie实现了Decodable因为稍后需要从JSON有效负载中构建它。

现在我们已经定义了Movie类,让我们创建HomeViewController类。

这里没有什么新鲜的,这只是一个带有实现UITableViewDelegate/UITableViewDataSource.UITableView的常规控制器UITableViewDelegate/UITableViewDataSource.

现在,让我们回到AppDelegate 。 当然,我们可以手动构建HomeViewController并将其设置为窗口,但这对于Weaver来说效果不佳。 我们要做的是像这样使用Weaver在AppDelegate注册HomeViewController的实例。

在这个新版本的AppDelegate ,我们使用// weaver: homeViewController = HomeViewController <- UIViewControllerAppDelegate注册HomeViewController的实例。

默认情况下, HomeViewController实例化一次,然后重用。 稍后我们将看到如何个性化此行为。

还要注意,我们已经实例化了由Weaver生成的AppDelegate的DI容器。 最后,我们将HomeViewController实例附加到窗口。

通过使用Dependencies.homeViewController访问HomeViewController ,DI容器将自动创建HomeViewController的实例,并且由于我们在范围container注册了该实例,因此该实例将仅创建一次,然后再使用。

在这一点上,如果尝试编译,则会注意到编译器失败,因为它找不到协议AppDelegateDependencyResolver或类AppDelegateDependencyContainer 。 这是因为我们需要 将为我们生成 的文件 Weaver.AppDelegate.swift Weaver 添加 到项目中 。 它应该位于Generated目录中。 添加文件后,项目应会编译,并且您应该能够在屏幕上看到一个空的UITableView

就是这样,您刚刚注入了第一个依赖项。

接下来,我们将一些电影注入HomeViewController 。 为此,我建议编写一个名为MovieManager的类,该类基本上将从The Movie DB API中获取电影,然后将其注入HomeViewController.

对于此样本,我们将使用TMDB Discover端点,该端点返回包含电影页面的JSON有效负载。

为了能够解码电影的页面,让我们首先声明一个结构Page

该结构以通用Model为参数,该Model必须实现Decodable ,使其能够包含和解码任何类型的项目。

在上面的MovieManager类中,我们声明了getDiscoverMovies(_:)方法,该方法以Page 。 我们使用URLSession的共享实例来创建任务,该任务命中https://api.themoviedb.org/3/discover/movie?api_key=1a6eb1225335bbb37278527537d28a5d。 当任务获得响应时,它将对其进行解析并将其转换为Page并完成。 这段代码远非完美,但可以做到。 稍后我将讨论改进它的方法。

现在我们有了MovieManager ,可以将其注入HomeViewController.

与将HomeViewController注入AppDelegate方式相同,我们可以使用Weaver将MovieManager注入HomeViewController

请注意,这次,我们没有直接实例化依赖项容器,而是编写了一个新的初始化程序init(injecting:)并将其存储。 这样做的原因是因为我们希望能够解析共享的依赖关系,并且为此,我们需要从调用站点获取依赖关系容器。 请注意,这与AppDelegate的特殊情况(作为应用程序的根类)有什么不同,因此没有任何调用站点,除了手动实例化依赖项容器外,我们别无选择。

还要注意我们如何私有存储依赖项容器。 除非您想将所有依赖项公开给外界,否则我会说最好的做法是将其保密。 我将在本文后面解释如何与Weaver共享类之间的依赖关系。

正确注入依赖项后,我们可以调用dependencies.movieManager.getDiscoverMovies(_:)并将影片加载到UITableView

当然, 不要忘记 在项目中 添加文件 Weaver.MovieViewController.swift 以便正确编译

到目前为止,还没有什么新东西。 让我们使此示例稍微复杂一点,并添加一个MovieViewController来显示电影的详细信息。 为此,我们需要使用Movie实例化MovieViewController 。 让我们跳到代码上,看看Weaver如何帮助我们做到这一点。

您可能注意到了我以前从未提到过的注释;
// weaver: movie <= Movie

它使Weaver意识到解析MovieViewController需要电影。

在这里,我们使用注释// weaver: movieController = MovieViewController <- UIViewController ,它告诉weaver从HomeViewControllerDependencyContainer生成对movieController的访问器。 这样,当用户点击单元格时,我们可以使用dependencies.movieController(movie:)方法来构建新的MovieViewController并将其推入堆栈。

然后,我们使用新的注释// weaver: movieController.scope = .transient oder中的// weaver: movieController.scope = .transient来个性化默认实例化行为。 transient作用域告诉Weaver为每个分辨率创建一个新实例,这意味着每次用户轻按一个单元格都会推送一个新的MovieViewController 。 您可以在文档中找到不同的可用范围。

还要注意, 使用非 transient 以外的任何作用域 来解析接受参数的依赖都没有多大意义 。 Weaver允许这样做,但请注意,第二次解决依赖关系时,由于初始对象已共享,因此将忽略参数。

到目前为止, MovieViewController并不出色。 它仅显示文本。 我们应该显示电影封面的图像,以使其感觉更真实。

为此,我们需要从TMDB中获取图像,例如,从以下URL:https://image.tmdb.org/t/p/w1280/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg。

这将是以下ImageManager类的目的。

到目前为止,该管理器与MovieManager并没有太大不同。 就依赖项注入而言,它也不是完全正确的,因为它使用了注入的URLSession单例。 还记得当我告诉你我将介绍改善MovieManager吗? 好吧,这就是其中之一。

下面的注入URLSession就像我们在AppDelegate注入HomeViewController AppDelegate

如果此时尝试编译和运行该应用程序,则会看到它崩溃,因为需要使用配置参数来构建URLSession才能正常工作。 为了使此工作有效,Weaver需要一个自定义URLSession构建器。

在上面的代码中,我添加了注释: // weaver: urlSession.builder = URLSession.make ,使Weaver知道我们要实现自己的URLSession构建器。 然后,我编写了方法make() -> URLSession ,该方法在解析URLSession时由依赖项容器自动调用。

顺便说一下, MovieManager存在相同的问题,我们也可以对其执行相同的操作。

现在可以在MovieViewController使用MovieViewController ,这将在以下代码中看到。

尽管所有这些工作都很好,但是仅写入URLSession,仍然要编写太多代码,在整个项目中很可能会使用很多代码。 最好只编写一次make(_:)方法。

如果我们可以共享一个ImageManagerMovieManager都可以使用的URLSession实例怎么办? 让我们看看Weaver如何提供帮助。

首先,我们需要在ImageManagerMovieManager中都声明URLSession的引用。

以前我们输入过:
// weaver: urlSession = URLSession
我们将其更改为:
// weaver: urlSession <- URLSession

如果尝试对此进行编译,则会看到Weaver抱怨urlSession无法解析。

为了解析这样的引用,Weaver递归地遍历了ImageManager所有直接祖先,并检查是否存在为它们每个注册的URLSession实例。 如果有一个祖先没有,那么它以相同的方式探索其祖先,依此类推,直到找到该实例或没有更多祖先要探索。 在这种情况下,Weaver碰到了根( AppDelegate ),然后失败了,因为没有更多祖先可供探索,并且我们仍然缺少URLSession实例。

为了解决这个问题,我们需要在MovieManagerImageManager的共同祖先中注册URLSession的共享实例。

查看上面的依赖关系图,我们可以轻松推断HomeViewControllerAppDelegateMovieManagerImageManager的仅有的两个共同祖先。 为了方便起见,我们将选择AppDelegate

注意我们如何在这里使用新的作用域container 。 此作用域与默认作用域具有相同的作用,但也将依赖项暴露给依赖关系图中的相邻对象。 在此示例中,由于我们从AppDelegate公开了urlSession ,所以整个图都可以访问它。 如果我们从MovieViewController公开了它, MovieViewController只有ImageManager可以访问它。

最后,我们可以在MovieManager声明URLSession的引用,以便它使用与ImageManager相同的实例。

这是一个非常常见的场景,如果您开始在项目中使用Weaver,将会看到很多情况。 实际上,此urlSession共享实例显示了单例的所有优点,而没有缺点。 确实,由于我们是在根目录注册的,因此可以在整个应用程序中访问它,但仍然可以对其进行注入,因此可以轻松重写和测试。 我将在另一个教程中说明。

到目前为止,我们已经看到Weaver如何顺利地集成到Swift代码库中,但是如果我们正在处理一个包含用ObjC编写的类的项目,该怎么办?

韦弗再次让我们得到了覆盖。 让我们看看如何实现该示例项目的最后一个控制器。 ReviewViewController

MovieViewController点击电影封面图像时,将显示此控制器。 它将用ObjC编写,并且基本上是评论的UITableView

为了获得电影的评论,我们需要一个ReviewManager 。 我保证,这将是最后一次。

ReviewManager使用TMDB Review端点。 例如,它将点击URL; https://api.themoviedb.org/3/movie/550/reviews?api_key=1a6eb1225335bbb37278527537d28a5d&language=zh-CN&page=1并获取以下JSON负载作为响应。

首先,让我们声明一个新的模型类,我们将其称为Review 。 由于此类需要在ObjC中使用,因此需要相应地进行注释。

由于这些评论来自页面,因此我们还需要编写一个ObjC友好的ReviewPage

现在,让我们编写ObjC友好的ReviewManager

请注意,我们重用了与MovieManagerImageManager相同的urlSession引用。

我们还使用了新的注释// weaver: self.isIsolated = true ,它告诉Weaver ReviewManager尚未在依赖关系图中。 Weaver知道这一点后,就避免在ReviewManager上执行依赖项解析检查。 否则,它将在urlSession参考解析上失败。 直到可以在依赖关系图中引入ReviewManager之前,这只是一个临时解决方案。

最后,我们可以在ObjC中编写WSReviewViewController。

目前,没有注入, WSReviewController仅显示一个空的UITableView

现在我们有一个问题,因为Weaver无法解析ObjC源代码,因此,向该控制器添加注释将无效。 但是我们可以做的是在一个Weaver可以理解的快速文件中扩展WSReviewController

通过扩展WSReviewViewControllerObjCDependencyInjectable ,我们告诉Weaver将此扩展视为常规类,从而使其生成协议WSReviewViewControllerDependencyResolver.

剩下要做的就是在WSReviewViewController中编写一个初始化器,该初始化器使用id并将其存储,然后使用它来解析我们的ReviewManager

现在,我们可以在用户点击MovieViewController电影封面图像时显示WSReviewViewController 。 另外,由于我们正在注册WSReviewViewController ,因此它现在已成为依赖关系图的一部分,这意味着我们必须摆脱// weaver: self.isIsolated = true的注释// weaver: self.isIsolated = true ReviewManagerWSReviewViewController+Injectable // weaver: self.isIsolated = true

在这里,我们使用transient注册注释注册了WSReviewViewController ,并将其与DI容器的func reviewController(movieID:)方法func reviewController(movieID:)

请注意,我们用ObjC编写控制器的事实不会影响其注入方式。 这意味着您想要在Swift中重写它的那一天,您将不必在代码库中重写任何Weaver注释。


这就是本教程第一部分的全部内容,您在其中学习了如何:

  • 编写可注入的Swift类
  • 通过注册注入依赖项
  • 通过引用注入依赖项
  • 注入依赖项作为参数
  • 使用自定义依赖项构建器
  • 在整个代码库中共享依赖项
  • 编写可注射的ObjC类

在探索以上所有内容时,我们重写了Weaver示例应用程序的稍微简化的版本。

在第二部分中,我们将看到如何使用Weaver编写简洁灵活的单元测试

我希望很快见到您,当然,对于任何问题或建议,请随时在Twitter @thrupin上发表评论或与我联系。

谢谢阅读!

干杯

如果您想改变世界的阅读方式,请加入我们! www.scribd.com/careers