如何正确编写扫描QR码的ViewController

让我们编写一个解耦的代码,通过使用组合甚至可以测试我们的ViewController! 😱

当我不得不编写QR码扫描仪应用程序时,我以前从未使用过AVFoundation。 因此,我遵循了通常的脚本:我阅读了一些文档,阅读了一些教程并将学习的内容应用于我的需求。 通用方法的问题在于,它使ViewController符合另一种协议。 因此,它增加了另一个责任!

我将以一个简单的应用程序为例,向您展示一种更好的方法。

第一个ViewController需要一种使用摄像机捕获视频并显示在此辣味混合框中捕获的内容的方法。 另外,当用户将相机指向QR码时,它需要解释并解码其值。 之后,它可以停止捕获视频。 AVFoundation助我们一臂之力!

AVFoundation

尽管QR代码已经很老了,但是直到iOS 7才有了AV Nativeation的巨大改进,我们没有在iOS上读取它们的原生方法。 现在,这是在iOS上实现代码读取器的最简单方法!

它具有我们完成此任务所需的所有功能。 AVCaptureDevice Input负责捕获视频。 AVCaptureVideo 预览层,用于显示实时视频。 AVCaptureMetadata 输出是将解释和解码QR码的输出。 AVCapture Session可以管理输入和输出之间的数据流。

我将所有这些逻辑包装在名为AVCodeReader的类中。 它的界面由两个函数和一个属性组成。 该函数开始读取QR码并使用转义闭包来通知何时解码值。 另一个停止使用相机资源。 还有CALayer属性,其中包含正在捕获的视频。

定义AVCodeReader接口的协议

AVCodeReader

我使用init来设置前面提到的所有AVFoundation类。

https://github.com/danielCarlosCE/aguente/blob/master/Aguente/Model/Implementation/AVCodeReader.swift

首先要记住的是,该设备(如模拟器)可能没有摄像头。 您必须决定接下来会发生什么。 我选择不破坏应用程序,而是选择一个空的videoPreview和一个nil captureSession

现在,它可以在不为captureSession时使用captureSession启动和停止读取QR码。

https://github.com/danielCarlosCE/aguente/blob/master/Aguente/Model/Implementation/AVCodeReader.swift

请注意,我将completion闭包分配给didRead属性,因此可以从另一个函数调用它。

要用作输出的委托,它必须符合AVCaptureMetadataOutputObjectsDelegate 。 该协议需要NSObjectProtocol ,因此我将类NSObject的子类。

https://github.com/danielCarlosCE/aguente/blob/master/Aguente/Model/Implementation/AVCodeReader.swift

ReaderViewController

承诺是写一个解耦的代码! 让我们看看ViewController的样子。

https://github.com/danielCarlosCE/aguente/blob/master/Aguente/Controller/ReaderViewController.swift

尽管ViewController不知道如何扫描QR码,但它具有依赖关系。 由于在使用情节提要时不能使用构造函数注入,因此我在此处使用了强制展开属性。 因此,如果有人在调用此视图之前忘记设置依赖项,则应用程序将崩溃!

请注意,我不仅要添加videoPreview图层,而且还要在视图布局其子视图时对其进行更新,因为在viewDidLoad尚未设置自动布局的约束。

测试用例

上面的ViewController具有一些关闭属性,在这里我省略了这些属性,以便于解释。 但是,我使用它们(而不是委托)来通知感兴趣的任何人。例如,当找到卡片时,ViewController不知道该怎么办。 它只是通知发生了什么。

这是在ViewController上进行单元测试的示例。 由于它取决于协议,因此我通过了MockReader。 该模拟甚至不了解AVFoundation。 我正在使用此completion属性来伪造代码读取。

结论

所以你怎么看? 这段代码实际上更具可测试性吗? 您可以在此处查看此应用的完整代码,包括其他单元测试。

通过分离每个类的关注点,我们可以使我们的代码更具可测试性且更易于维护。 我在这里使用合成,而不是使用具有CodeReader的ViewController,而不是具有CodeReader的ViewController。 另外,请注意ReaderViewController取决于协议,而不取决于具体类型。 这样,我可以轻松地将模拟作为依赖项传递。

这是我的第一篇文章,我很高兴我终于做到了! 如果您在此处留下任何反馈,它将对我有很大帮助,请发表评论!

这篇文章已更新为Swift 4.2。 您可以在这里https://github.com/danielCarlosCE/aguente/commit/cdf9606bf1bed5c41fe2145d268e6361f85d0934查看更改。 这是一个很好的机会,了解ViewController与AVFoundation之间的脱钩情况。