从零开始打造一个扫码框架

从零开始打造一个iOS框架,因为最近工作中用到了二维码,所以就选择这么一个相对比较简单的功能来演示如何构建一个iOS框架。

create a framework

打开Xcode,在菜单栏中选择 File -> Project,选中[Cocoa Touch Framework]。(快捷键:command + shift + n )

create cocoa touch framework

点击[Next]
choose options for your new project

  • Product Name(产品名称) 填入 LZQRcode。
  • Team(团队,实际上就是证书) 选择个人。
  • Organization Name(组织名称) 填入三只小猪(很可爱的名字)
  • Organization Identifier (组织标识), 填入个人网站com.lucaslz的反转。
  • Language 选择Swift

点击[Next]

选择产品需要保存的文件路径。Xcode就会自动生成产品了。也会自动生成一些代码以及文件。

Cocoa Touch Framework vs Cocoa Touch Static Library

Framework

Framework 是一个动态库,iOS系统框架都是以.framework结尾的,他们就是动态库。为了App Extension,苹果在Xcode6引入了Framework新特性,仅支持iOS 8+,构建工具Carthage 只支持iOS 8+。而Cocoapods可以兼容iOS 8 以下,但是是以静态库的方式了。

而Swift是只支持Framework。所以使用swift那么最低要求就需要iOS 8。

Static Library

静态库就是模块被编译时就会合并到应用中。就是一个.a后缀的文件。在iOS 8 以下,iOS只支持以静态库的方式来使用第三方库代码。

Framework vs Static Library

静态库不能包含xib、图片等资源文件,只能手动拷贝到main bundle。 而framework就可以把这些资源包含在自己的bundle中。

随着App Extension的出现,将同一个.a拷贝在app以及App Extension中,这就是重复了。

静态库只能随着应用一起加载,可以理解为就是自己的app中的代码。而动态框架加载到内存后就不需要再次加载了,明显了加快了速度。比如多个app都同时用到了一个framework。只要有一个app加载了,其他的app启动的时候就不会加载了。

create ScanCodeView

选中LZQRCode文件夹(选中哪一个文件夹,后面创建的文件就会在这个文件夹里面),选择Xcode菜单栏中的 File -> New -> File。

选择[Cocoa Touch Class],如下图箭头

choose a template for your new file

点击 Next -> Create, 创建的文件就会在LZQRCode目录下。

choose options for your new file

  • class 填入需要创建的类名,也是文件名。这里写上ScanCodeView。
  • subclass of 因为这个是一个视图,在iOS中视图都需要继承自UIView
  • Language 选择Swift

扫描是需要用到系统的框架AVFoundation

import AVFoundation

使用关键字import 就能够导入需要的框架。

1
import AVFoundation

扫描需要用到AVCaptureDevice、AVCaptureDeviceInput、AVCaptureMetadataOutput、AVCaptureSession、以及AVCaptureVideoPreviewLayer。

二维码与扫码 中有详细的使用说明。

create AVCaptureDevice

使用懒加载的方式创建一个device变量。

1
2
3
lazy var device: AVCaptureDevice = {
return AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
}()

create input

1
2
3
lazy var input: AVCaptureInput = {
return try! AVCaptureDeviceInput(device: self.device)
}()

create output

可以设置输出的代理,并且运行在主线程上。这是为了能够获取设备扫描的结果。

1
2
3
4
5
lazy var output: AVCaptureMetadataOutput = {
let result = AVCaptureMetadataOutput()
result.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
return result
}()

因为代理设置为self(即ScanCodeView本身),Xcode会报错,要求我们遵守AVCaptureMetadataOutputObjectsDelegate代理方法。

最佳实践, 我们都会以extension(扩展)来实现代理,如果都多个代理,那么我们就会有多个extension

1
2
3
4
5
6
extension ScanCodeView: AVCaptureMetadataOutputObjectsDelegate {

func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {

}
}

mark

经常会在Xcode中看到#pragma mark -,这实际上是一条编译指令。不过在Xcode它的作用却是告诉Xcode编译器,在编辑器顶部窗口,类这属性、方法等弹出框中可以分隔代码。

而在Swift中这么好的属性当然也需要支持。使用// MARK: -就可以了。-的作用是否需要一个一条线进行分隔。

最佳实践,我们都会为extension添加一个MARK,如下
为协议ScanCodeViewDelegate添加一个MARK。

1
// MARK: - ScanCodeViewDelegate

在Xcode编辑区顶部,即点击ScanCodeView.Swift后面的内容。如下图,点击箭头指向的内容。

lzqrcode-mark1

弹出的列表框如下图

lzqrcode-mark4

如果又-,就会先以横线分隔,然后再显示一个标题。标题名称就是mark 后面的内容。

create session

1
2
3
4
5
6
7
8
9
10
11
lazy var session: AVCaptureSession = {
let result = AVCaptureSession()
result.sessionPreset = AVCaptureSessionPresetHigh
if result.canAddInput(self.input) {
result.addInput(self.input)
}
if result.canAddOutput(self.output) {
result.addOutput(self.output)
}
return result
}()

create previewLayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 声明变量 previewLayer
var previewLayer: AVCaptureVideoPreviewLayer?

// 初始化
override init(frame: CGRect) {
super.init(frame: frame)
self.setupViews()
}

// 初始化
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupViews()
}

// 初始化previewLayer
func setupViews() {
self.previewLayer = AVCaptureVideoPreviewLayer(session: self.session)
if let previewLayer = self.previewLayer {
self.layer.insertSublayer(previewLayer, at: 0)
}
}

// 布局
override func layoutSubviews() {
super.layoutSubviews()
self.previewLayer?.frame = self.bounds
}

因为AVCaptureVideoPreviewLayer初始化返回一个可能会为空的previewLayer。所以这里用if let previewLayer = self.previewLayer进行判断。

add start scan code method

1
2
3
4
func startScanCode() {
self.output.metadataObjectTypes = [AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeQRCode]
self.session.startRunning()
}

add stop scan code method

1
2
3
func stopScanCode() {
self.session.stopRunning()
}

create ScanCodeViewDelegate

因为这个是一个View,我们使用MVC模式,在View中只做用户界面的显示,因此需要让ViewController知道扫描的结果。

自定义一个协议,让ViewController遵守协议并实现相应方法,就可以获取到扫描结果。

1
2
3
4
protocol ScanCodeViewDelegate {

func scanCodeView(scanCodeView: ScanCodeView, didScanCodeResult codeResult: String)
}

也为协议ScanCodeViewDelegate添加一个MARK。

1
// MARK: - ScanCodeViewDelegate

lzqrcode-mark3

ScanCodeView添加delegate变量。

UIViewController可能并不会处理delegate,因此使用Optional,表示可以为空,Xcode也不会强制让你去初始化delegate。

1
var delegate: ScanCodeViewDelegate?

实现AVCaptureMetadataOutputObjectsDelegate。如果delegate会实现,那么就会被回调。返回扫描结果字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension ScanCodeView: AVCaptureMetadataOutputObjectsDelegate {

func captureOutput(_ captureOutput: AVCaptureOutput!,
didOutputMetadataObjects metadataObjects: [Any]!,
from connection: AVCaptureConnection!) {

guard metadataObjects.count > 0 else {
return
}
guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject else {
return
}

self.delegate?.scanCodeView(scanCodeView: self, didScanCodeResult: metadataObject.stringValue)
}
}

终于ScanCodeView会告一段落了。现在就来生成framework,供其他项目使用。

build framework

使用Carthage来构建framework。

- 使用Carthage用户量已经很高了。
- 这个相对`Cocoapods`来说比较简单。
- 这个框架是用Swift写的, 学习成本相对比较低,遇见问题容易解决

to the root directory of the producat

项目创建时选择保存在了桌面(当然这不是一个好的习惯)。
在命令行中切换到项目根目录。

1
cd ~/Desktop/LZQRcode/

run carthage build

使用carthage的命令进行framework构建。

1
carthage build --no-skip-current

出现了错误

1
2
*** Skipped building LZQRcode due to the error: 
Dependency "LZQRcode" has no shared framework schemes

根据错误提示可以看出,schemes需要shared。

shared schemes

在Xcode左上角,停止(正方形)后面的Schema(即LZQRcode)。如下图
lzqrcode selected schema

在弹出的列表中选择[Manage Schames...]。会弹出Schames管理界面。

lzqrcode manage schema

再次执行构建命令

1
carthage build --no-skip-current

控制台输出

1
*** Building scheme "LZQRcode" in LZQRcode.xcodeproj

到此,我们就构建好了一个框架,可以在项目根目录找到Carthage文件夹。如下图,箭头指向就是生成的framework。

lzqrcode build framework

拷贝生成的LZQRcode.framework文件到需要的项目中,就可以使用啦。

现在我们就来创建一个Example项目,使用LZQRcode.framework来进行扫码。

create example project

这次使用快捷键的方式 Command + shift + n 来创建项目,选择Single View Application。

create example project step1

点击 [Next]

create example project step2

选择LZQRCode作为根目录。这是为了框架与Example在一起,其他的小伙伴在使用的时候就可以直接运行Example查看框架的功能,所见即所得。

点击 [Create] 按钮,就创建了Example工程。

create example project complete

import LZQRCode

要想在Example工程中使用LZQRCode,就需要把LZQRcode.framework 拷贝到项目中(目前先使用这种方式)。

如上面的[Example]整个工程中,点击红色框中箭头指向的[+]号就可以添加一个framework到工程中。

import lzqrcode framework step 1

选择Carthage构建的目录,找到LZQRcode.framework。并选中。
import lzqrcode framework step 2

点击[Open]。

import lzqrcode framework step 3

Destination 一定要勾选上[Copy items if needed]。如果没有勾选上,会报编译错误。当勾选上[Copy items if needed],那么Added folder中的复选就无关紧要了。随便选择哪一个都是可以的。

ViewController use LZQRcode

接下来,我们就打开ViewController,在ViewController中去使用LZQRcode

ViewController import LZQRcode。

使用关键字[import] 引入 `LZQRcode framework’。

1
import LZQRcode

declared scanCode property

1
var scanCode: ScanCodeView?

Xcode 报错,查看错误提示信息。

1
Use of undeclared type 'ScanCodeView'

错误提示的意思是 ScanCodeView没有被声明,即找不到ScanCodeView这个类。去查看LZQRCode-Swift.h头文件也没有任何关于ScanCodeView的信息。

实际上这是因为在iOS开发中,Objective-C 与 Swift有一点不一样,那就是Swift有访问控制。简单一点就是Swift中有五个访问级别。public 没有那么开放了,而private更加私有了。

访问控制符 描述
open swift 3.0 新加入,在同一个模块中是一样的。在模块外使用时,open声明的变量或者函数是可以override(即覆盖,或者叫重写)。 相当于swift 2 中的public
public 模块外是可以访问的,Swift3.0 后只是把接口公开出去。不能够override。一般用于公开的API。
internal 默认级别,模块内部使用。
fileprivate swift 3.0 新加入的,extension中可以访问。相当于swift 2中的private
private 只有在当前作用域范围中才可以使用。extension中不能访问。更加私有化。

瞬间感觉swift 3.0 又变复杂了。 没关系,这篇文章将会有一些实践。

在LZQRCode中,ScanCodeView类、类中变量、方法都没有使用任何访问控制修饰符。那就是默认级别internal,所以外部使用LZQRCode框架时,是找不到ScanCodeView的。

modify ScanCodeView access control

为了让外界能够使用ScanCodeView类,那么肯定需要修改成public或者使用open。但是申明为public的类不能够被override,因此我们选择使用open。

1
open class ScanCodeView: UIView

接着就会报出三个错误信息。

solved error message

  1. 初始化错误信息
1
2
~/Desktop/LZQRcode/LZQRcode/ScanCodeView.swift:49:14:
'required' initializer must be as accessible as its enclosing type

因为声明的ScanCodeView类是open,所以初始化也必须是open。

1
required open init?(coder aDecoder: NSCoder)

修改之后依旧报错。

1
2
~/Desktop/LZQRcode/LZQRcode/ScanCodeView.swift:49:14: 
Only classes and overridable class members can be declared 'open'; use 'public'

错误信息中明确指出了open只能修饰类和类成员。而init?(coder aDecoder: NSCoder)这是构造方法。因此不适用,在这里就只能选择public

1
required public init?(coder aDecoder: NSCoder)

既然init?(coder aDecoder: NSCoder)也是public,那么init(frame: CGRect)也需要修改成public。这两个初始化方法一般而言都是成对出现的。

1
override public init(frame: CGRect)
  1. 重写的实例方法错误信息
1
2
~/Desktop/LZQRcode/LZQRcode/ScanCodeView.swift:64:19: 
Overriding instance method must be as accessible as the declaration it overrides

重写的实例方法也必须同类的访问修饰符一致,所以func layoutSubviews()也需要修改成open。

1
override open func layoutSubviews()
  1. 协议错误信息
    1
    2
    3
    4
    ~/Desktop/LZQRcode/LZQRcode/ScanCodeView.swift:91:10: 
    Method 'captureOutput(_:didOutputMetadataObjects:from:)'
    must be declared public because it matches a requirement
    in public protocol 'AVCaptureMetadataOutputObjectsDelegate'

从错误信息可以知道,因为协议AVCaptureMetadataOutputObjectsDelegate是public的,所以方法也需要变成public。

1
2
3
public func captureOutput(_ captureOutput: AVCaptureOutput!,
didOutputMetadataObjects metadataObjects: [Any]!,
from connection: AVCaptureConnection!)

shortcut fix errors

在Xcode中,左边导航栏第四个按钮[Show the issue navigator]中可以看到所有的错误信息。在单个文件中也会有错误信息的提示。

lzqrcode auto fixed message step 1

选中其中一个错误信息,会弹出自动修复的内容。
选择Fix-it, 错误就会自动解决了。

lzqrcode auto fixed message step 2

看上去自动修复的功能还是挺好用的,但有时候就不管用了。所以核心还是要自己明白错误的原因,一味依赖IDE工具还是不靠谱。

declared public methods

因为startScanCode以及stopScanCode都需要外界能够直接调用,因此可以使用public或者open,但是这个功能我们不希望子类去覆盖,导致系统出现问题,所以修改成public,只单纯的让外部模块调用。

1
2
public func startScanCode()
public func stopScanCode()

declared delegate property

代理属性也需要外部直接进行赋值,因此也需要添加public或者open修饰。public与open都是可以的,目前而言,public足够了。

1
public var delegate: ScanCodeViewDelegate?

修改之后出现了错误信息

1
Property cannot be declared public its type uses an internal type.

属性不能声明为public,因为ScanCodeViewDelegate的访问控制符是internal。所以为了满足要求,只能够把ScanCodeViewDelegate也声明为public。

1
public protocol ScanCodeViewDelegate

again run build framework

因为ScanCodeView源代码已经修改了,需要再次构建,Example重新导入,使用的才会是最新的框架。

先删除以前引入的LZQRcode.framework。右击[LZQRcode],点击[Delete]

lzqrcode delete lzqrcodeframework step1

这里的删除有两种方式。

  • Remove Reference 只是删除引用,实际上还是存在。
  • Move to Trash 彻底删除

在这里我们选择[Move to Trash] 彻底删除。
lzqrcode delete lzqrcodeframework step2

in ViewController use LZQRcode

再次打开ViewController可以看到的错误已经可以不存在了。可以 [Command + 单击] 可以查看类中的声明。

init ScanCodeView

一如既往的使用懒加载的方式,使用init(frame: CGRect)方式创建ScanCodeView的实例,并设置代理。

1
2
3
4
5
lazy var scanCodeView: ScanCodeView = {
let result = ScanCodeView(frame: self.view.bounds)
result.delegate = self
return result
}()

assignment delegate

既然对代理进行了赋值,就需要遵守代理,即实现代理。为了代码的可读性更好,我们一直都会使用extension(扩展)来遵守代理。

1
2
3
4
5
6
extension ViewController: ScanCodeViewDelegate {

func scanCodeView(scanCodeView: ScanCodeView, didScanCodeResult codeResult: String) {
print("扫码结果:\(codeResult)")
}
}

viewDidLoad add scanCodeView

把scanCodeView添加到UIViewController的View中。

1
2
3
4
5
override func viewDidLoad() {
super.viewDidLoad()

self.view.addSubview(self.scanCodeView)
}

viewWillAppear add startScanCode method

1
2
3
4
5
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.scanCodeView.startScanCode()
}

run Example

点击左上角的三角形符号,或者直接快捷键 [Command + R]. 来启动项目。

lzqrcode run example

  • 三角形启动项目。
  • 正方形是停止项目,只有在项目运行的时候才能停止,所以现在是灰色的不可点击。

在Example右边,随意选择一个设备启动。

白茫茫的一片,好像啥都没有?

因为扫码是需要调用摄像头的,所以我们需要申请摄像头的权限。

request camera application permission

申请摄像头权限需要用到系统的框架AVFoundation

1
import AVFoundation

viewWillAppear中添加一下方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let authorizationStatus = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
switch authorizationStatus {
case .notDetermined:
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (granted) in
if granted {
self.scanCodeView.startScanCode()
}
}
break;
case .restricted:
print("受限")
break;
case .denied:
print("拒绝")
break;
case .authorized:
self.scanCodeView.startScanCode()
break
}

again run Example

因为摄像头只有真机才有,模拟器是没有该功能的。所以需要运行在真机上。

1
2
3
4
5
This app has crashed 
because it attempted to
access privacy-sensitive data without a usage description.
The app's Info.plist must contain an NSCameraUsageDescription key
with a string value explaining to the user how the app uses this data.

居然又崩溃了,这是因为需要在Info.plist添加NSCameraUsageDescription描述。(这是iOS10新添加的,如果是iOS8、9就不会有这个问题)。

打开Info.plist,点击[+]按钮,找到Privacy - Camera Usage Description

lzqrcode add camera example

填入描述信息。

lzqrcode add camera example

右击[Info.plist] -> [Open As] -> [Source Code],可以看到如下信息。

1
2
<key>NSCameraUsageDescription</key>
<string>使用相机进行二维码扫描</string>

也可以打开Source Code直接进行编辑。

运行真机设备,选择OK,允许app访问Camera的权限。
终于跑起来,看样子没有问题了。 赶紧找一个二维码扫描试试成果。

在控制台可以看到不断的输出结果。

1
2
3
4
扫码结果:lucaslz.com
扫码结果:lucaslz.com
扫码结果:lucaslz.com
......

至此,我们完成了一个比较简单的二维码扫码框架。

manage source code

你可能会想,既然已经完成了功能,以及都直接打包出来就可以了。这样不也是挺好的吗?

但是如果你本地可能不小心删除了该项目,那么就永远回不来了。也有人要说了,我们可以选择备份。

备份确实可以轻松解决这个问题,但为了让更多人使用这个框架,也可能有很多bugs。而自己可能又不知道。因此使用源代码管理,并放在互联网上面。与大家互利共赢才是最好的选择。

我使用过许多的源代码版本管理工具,从CVSSVN到现在的Git。CVS在我开始工作的时候已经被淘汰了。后面就转战到SVN上面。再后来就使用了现在的Git。

使用git就需要记住很多命令。而我们可以选择SourceTree工具来取代以提高工作效率。

在命令行中新建忽略文件。

1
touch .gitignore

加入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Carthage
# macOS
.DS_Store

# Xcode

## Other
xcuserdata

# Carthage
Carthage/Build

# Frameworks
LZQRCode.framework

我个人觉得忽略文件是相当重要的。当一个项目依赖的一些framework或者静态库的时候,如果不忽略,就会导致git仓库超级大,如果有新的同事加入,下载的时候就会超级慢。我当时亲历了一个产品。整个iOS产品有一个多G,超级吓人。

submit to GitHub

使用GitHub做为源代码仓库,因为这个全世界的小伙伴们都在用,上面有非常优秀的框架。可以说是应有尽有。

使用SourceTree把代码提交到git本地,再push到服务器。

使用SourceTree把代码加入到Git管理,即点击[New Repository], 选择[Add existing local repository]。

lzqrcode add existing local repository step 1

选择LZQRcode的目录。名称为LZQRcode,这个名称只是SourceTree中的名字而已,可以随意命名。当然最好还是与产品名一致。

lzqrcode add existing local repository step 1-1

选择需要提交的文件,并写上提交信息。(个人觉得每一次都填写提交信息是非常重要的)。
lzqrcode add existing local repository step 2

点击[Commit], 就Commit了一条记录到本地Git仓库中,在history中就可以查看到提交信息。

lzqrcode view code history

既然要push到远程仓库,所以需要添加远程仓库的地址。如果还没有github账号,建议马上就去注册一个。

create a new repository

GitHub上创建一个新的仓库,填入仓库名称,以及描述。

Public 是公开的库,所有的人都可以看到,免费。
Private 是私有的库,比如公司内部开发者使用,收费。

选择了初始化README。这个一般是项目的一些说明信息。添加这个是为了让大家更好的了解这个LZQRCode产品。

lzqrcode create a new repository in github step1

创建成功之后,选择[Clone or download]

lzqrcode create a new repository in github step2

获取仓库地址, 这里使用HTTPS的方式,比较简单。
复制仓库地址。

lzqrcode create a new repository in github step3

回到SourceTree中,点击右上角的[Settings]。

点击[Add]按钮。

lzqrcode create a new repository in github step4

把刚刚复制的仓库地址添加到[URL/path]中, Remote name填入刚刚在GitHub上的名字LZQRCode。

lzqrcode create a new repository in github step5

最后点击OK

pull

先把在GitHub上面创建的LZQRCode使用Pull拉取下来。不然是提交不上去的。

点击SourceTree左上角的[Pull]按钮。

再次查看history就可以看到SourceTree自动为我们进行了一次merage操作。
lzqrcode create a new repository in github step6

push

这个时候就可以使用push把代码推送到GitHub中了。选中master

lzqrcode create a new repository in github step7

推送之后没有报错,那么基本上就是推送成功了。 为了确认可以在GitHub上面查看。

minimum version

到现在,LZQRCode已经能够让人使用了。但是为了让更多的iOS设备使用,我们就需要修改最小版本。 如果我们手动把Example的Deployment Target 设置为8.0. 那么Xcode编译就会报错。

1
2
3
4
~/LZQRcode/Example/Example/ViewController.swift:10:8: 
Module file's minimum deployment target is ios10.0 v10.0:
~/LZQRcode/Example/LZQRcode.framework/Modules/
LZQRcode.swiftmodule/arm64.swiftmodule

修改TARGETS 中的依赖版本为iOS 8。
选中[LZQRcode] -> TARGETS中的[LZQRcode] -> [Deployment Info] -> [Deployment Target]。

lzqrcode modify minimum version step2

这时候可能看不到TARGETS。这时候最顶上的红色框是灰色的。点击就会出现TARGETS。

修改PROJECT中的依赖版本为iOS8。

选中[LZQRcode] -> PROJECT中的[LZQRcode] -> [Deployment Target] -> [iOS Deployment Target]。

lzqrcode modify minimum version step2

修改了代码就应该提交到GitHub仓库中去。并用README.md进行说明

1
2
3
4
5
## Requirements

- iOS 8.0+
- Xcode 8.0+
- Swift 3.0+

让大家能够知道项目环境,一眼就知道是否可以在项目中使用。

memory leak

在现在的iOS开发中,Xcode默认就是ARC了。并不是古老的MRC。所以已经大大减少了开发量,但是稍不注意就还是会引发内存泄露问题。

在Example中

ViewController 持有 ScanCodeView,而ScanCodeView持有delegate(即ScanCodeViewDelegate),而后delegate又被赋值成self(即ViewController)。

lzqrcode cycle references 1

常见的循环依赖。导致内存得不到释放。

在这里使用关键字weak就可以解决了。

1
public weak var delegate: ScanCodeViewDelegate?

lzqrcode cycle references 1

不出意外,又出现了编译错误。

1
2
3
~/LZQRcode/LZQRcode/ScanCodeView.swift:14:21: 
'weak' may only be applied to class and class-bound protocol types,
not 'ScanCodeViewDelegate'

这是因为Swift中的protocol(协议)是可以除了class以外的其他类型遵守,比如structenum。而struct等不通过引用计数来管理内存。因此就不能用weak这种ARC(内存管理)中的关键字来修饰。

class

在protocol声明的名字后加上class,显示的指定这个protocol只能由class实现

1
public protocol ScanCodeViewDelegate: class

@objc

另外一种解决方式就是在protocol前面加上@objc。Objective-C 的 protocol 都只有类能实现,所以就合理了。

1
@objc public protocol ScanCodeViewDelegate

class vc @objc

既然两种方式都可以,那么肯定就存在了差异。

  • 都能解决weak关键字错误。
  • class只能够用于swift中,而@objc却可以兼容Objective-C

两种方式可以看自己的出发点,如果需要兼容Objective-C那就选择@objc,如果只支持swift那么就使用class。

camera permission

既然扫码需要申请使用相机的权限,那么直接在在框架LZQRcode中申请使用相机的权限岂不是更好,让使用者能够更简单的使用,不关系实现细节。

ScanCodeView中的startScanCode方法修改如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
self.output.metadataObjectTypes = [AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeQRCode]
let authorizationStatus = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
switch authorizationStatus {
case .notDetermined:
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (granted) in
if granted {
print("current thread \(Thread.current)")
self.session.startRunning()
}
}
break;
case .restricted:
print("受限")
break;
case .denied:
print("拒绝")
break;
case .authorized:
self.session.startRunning()
break
}

在Example中,把ViewController中导入的AVFoundation删除。

1
import AVFoundation

并在viewWillAppear方法中修改如下,

1
2
3
4
5
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.scanCodeView.startScanCode()
}

直接调用scanCodeView的startScanCode方法。

extract method of refactor

startScanCode方法中,我们可以看到有大片的代码。并且可以都是模板,并且无主要的业务逻辑,开始扫码并没有太大的关系。因此我们就选择抽取出来。

当我们满怀欣喜的选中代码右击 选择 [Refactor] -> [Extract], Xcode却提示,不支持Swift的重构,仅仅只支持C和Objective-C。 这时,内心是崩溃的,Swift才是你的亲儿子啊~~~

lzqrcode cycle references 1

转战到AppCode, 重构神器,可惜对Swift的支持也不是很好,特别是Swift3.0 毕竟才出来。AppCode 还没有来得及发布最新的版本。

没办法,最后只好自己手动去重构。
修改如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public func startScanCode() {
self.output.metadataObjectTypes = [AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeQRCode]

self.checkCameraPermission()
}

private func checkCameraPermission() {
let authorizationStatus = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
switch authorizationStatus {
case .notDetermined:
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (granted) in
if granted {
print("current thread \(Thread.current)")
self.session.startRunning()
}
}
break;
case .restricted:
print("受限")
break;
case .denied:
print("拒绝")
break;
case .authorized:
self.session.startRunning()
break
}
}

发现在checkCameraPermission方法中,会去调用self.session.startRunning。这个与方法的名称有些冲突。毕竟方法名字仅仅只是检查是否有相机权限。

这时候,我们可以选择让checkCameraPermission方法返回bool来判断是否拥有相机权限。

但是又会有问题了,requestAccessForMediaType:completionHandler:方法是一个Closure,所以直接返回bool是不可以的。

既然他是一个Closure,那么我们就可以传递一个Closure给它。Closure就是一个函数。

在Swift中,函数是第一等公民(first class)。

  • 可以当做参数传递
  • 可以当做返回值传递
  • 可以当做成员变量

startScanCode 修改如下

1
2
3
4
5
6
7
8
9
10
public func startScanCode() {
self.output.metadataObjectTypes = [AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeQRCode]

self.checkCameraPermission { (granted) in
if granted {
self.session.startRunning()
}
}

}

checkCameraPermission修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private func checkCameraPermission(function: @escaping (Bool)->()) {

let authorizationStatus = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
switch authorizationStatus {
case .notDetermined:
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (granted) in
function(granted)
}
break;
case .restricted:
print("受限")
function(false)
break;
case .denied:
print("拒绝")
function(false)
break;
case .authorized:
function(true)
break
}
}

线程问题

requestAccessForMediaType:completionHandler:回调中加入如下代码:

1
2
print("current thread \(Thread.current)")
print("current thread isMainThread \(Thread.isMainThread)")

在第一次申请相机权限时会执行。
执行结果如下:

1
2
current thread <NSThread: 0x174076a00>{number = 3, name = (null)}
current thread isMainThread false

说明执行的结果不在主线程,因此可能就会扫码启动迟迟不能生效,界面也就呈现卡死状态。

把回调函数加入到主线程中

1
2
3
DispatchQueue.main.async {
function(granted)
}

escaping

重构无止境。

模拟器闪退

项目重命名

坚持原创技术分享,您的支持将鼓励我继续创作!