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 <- UIViewController
在AppDelegate
注册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(_:)
方法。
如果我们可以共享一个ImageManager
和MovieManager
都可以使用的URLSession
实例怎么办? 让我们看看Weaver如何提供帮助。
首先,我们需要在ImageManager
和MovieManager
中都声明URLSession
的引用。
以前我们输入过:
// weaver: urlSession = URLSession
我们将其更改为:
// weaver: urlSession <- URLSession
。
如果尝试对此进行编译,则会看到Weaver抱怨urlSession
无法解析。
为了解析这样的引用,Weaver递归地遍历了ImageManager
所有直接祖先,并检查是否存在为它们每个注册的URLSession
实例。 如果有一个祖先没有,那么它以相同的方式探索其祖先,依此类推,直到找到该实例或没有更多祖先要探索。 在这种情况下,Weaver碰到了根( AppDelegate
),然后失败了,因为没有更多祖先可供探索,并且我们仍然缺少URLSession
实例。
为了解决这个问题,我们需要在MovieManager
和ImageManager
的共同祖先中注册URLSession
的共享实例。
查看上面的依赖关系图,我们可以轻松推断HomeViewController
和AppDelegate
是MovieManager
和ImageManager
的仅有的两个共同祖先。 为了方便起见,我们将选择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
:
请注意,我们重用了与MovieManager
和ImageManager
相同的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
ReviewManager
和WSReviewViewController+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