二维码与扫码

在目前二维码遍行天下的时候, 学会制作二维码与扫码是相当重要的。

如何创建二维码

生成二维码的步骤

  • 创建CIFilter,并设置inputMessage与inputCorrectionLevel参数
  • 调用outputImage得到一个二维码图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class func createQRCode(QRCode: String) -> UIImage? {
guard let data = QRCode.dataUsingEncoding(NSISOLatin1StringEncoding) else {
return nil
}
guard let filter: CIFilter = CIFilter(name: "CIQRCodeGenerator") else {
return nil
}
filter.setValue(data, forKey: "inputMessage")
filter.setValue("H", forKey: "inputCorrectionLevel")
guard let QRImage = filter.outputImage else {
return nil
}
return UIImage(CIImage: QRImage)
}

QRCode

从图片上可以看到二维码很模糊, 这个问题稍后解决,先看看创建二维码过程中用到一些知识点。

CIQRCodeGenerator是在CICategoryGenerator类别中的一种编码格式。
CIQRCodeGenerator从输入数据中快速生成一个二维码。接收两个参数inputMessageinputCorrectionLevel

通过ISO/IEC 18004:2006标准,从输入数据中生成一个图像。

参数

参数名 类型 描述
inputMessage NSData 将会编码为QR码
inputCorrectionLevel NSString String(字符串) 单个字母标注纠错格式,默认值M

inputMessage

从一个字符串或者URL中创建一个QRCode,使用NSISOLatin1StringEncoding字符串编码将其转换成一个NSData对象。

inputCorrectionLevel

用于定义生成二维码对象的容错率。QRCode有容错能力,QRCode被破坏,仍然有可能被机器读取内容。范围在7% ~ 30%。容错率越高、QRCode图像面积越大。

  • L: 7%
  • M: 15%
  • Q: 25%
  • H: 30%

解决生成的二维码模糊问题

方案1

1
2
3
4
5
6
7
8
9
10
let cgImage = CIContext().createCGImage(QRImage, fromRect: QRImage.extent)
UIGraphicsBeginImageContext(size)
let context = UIGraphicsGetCurrentContext()
CGContextSetInterpolationQuality(context, .None)
CGContextScaleCTM(context, 1.0, -1.0)
CGContextDrawImage(context, CGContextGetClipBoundingBox(context), cgImage)
let QRCodeImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

这需要用到Quartz绘制图片的知识点,可以查看 使用Quartz绘制文本、图片、PDF

在这里只是用到CGContextScaleCTM坐标轴翻转,是因为绘制的图片的大小就是整个坐标系的大小。
所以不管从左下角还是右上角都没有关系。

方案2

相对于第一种方案,那么这种方案更简单也更容易理解,即缩放比例。

1
2
3
4
let scale = min(size.width / CGRectGetWidth(QRImage.extent),
size.height / CGRectGetHeight(QRImage.extent))
let transform = CGAffineTransformMakeScale(scale, scale)
return UIImage(CIImage: QRImage.imageByApplyingTransform(transform))

QRCode

如何创建带图的二维码

一个高清的二维码图片已经生成了,但是大多数的时候大家都希望自己的头像,商标、广告等显示二维码上面,所以接下来就来看看如何创建一个带图的二维码。

主要就是在二维码图片的中心画上一个图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
UIGraphicsBeginImageContext(sourceImage.size)
sourceImage.drawInRect(CGRectMake(0, 0, sourceImage.size.width, sourceImage.size.height))
let logoImageRect = CGRectMake((sourceImage.size.width - size.width) * 0.5,
(sourceImage.size.height - size.height) * 0.5,
size.width,
size.height)
logoImage.drawInRect(logoImageRect);
let resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext()
return resultImage;

需要注意上面没有使用坐标系翻转,是因为使用的UIKit绘图。详细可以查看 使用Quartz绘制文本、图片、PDF

QRCode

上面的二维码看上去很突兀,所以为了美观给logo与二维码之间加一个白色缓冲带

1
2
3
4
5
6
7
8
9
10
11
let context = UIGraphicsGetCurrentContext()
let addWidth: CGFloat = 10.0
CGContextSaveGState(context)
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextFillRect(context,
CGRectMake(CGRectGetMinX(logoImageRect) - addWidth,
CGRectGetMinY(logoImageRect) - addWidth,
size.width + addWidth * 2,
size.height + addWidth * 2))
CGContextRestoreGState(context)

QRCode

扫码

既然生成了二维码,那当然是需要进行识别的,那么接下来扫描二维码。而我们需要使用AVFoundation框架提供的功能来实现扫码功能。 主要需要使用以下几个类来获取扫码。

  • AVCaptureDevice 相机硬件接口,镜头位置、曝光、闪光灯等
  • AVCaptureDeviceInput 提供来自设备的数据
  • AVCaptureMetadataOutput 启用检测二维码
  • AVCaptureSession 管理输入输出之间的数据流,
  • AVCaptureVideoPreviewLayer 被用于自动显示相机产生的实时图像。

如何扫描二维码

  • 创建AVCaptureDevice
  • 创建AVCaptureDeviceInput
  • 创建AVCaptureMetadataOutput,设置代理,显示扫码结果
  • 创建AVCaptureSession,开始扫码
  • 创建AVCaptureVideoPreviewLayer,加入到视图中。
1
2
3
4
5
6
7
lazy var device: AVCaptureDevice = {
return AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
}()
lazy var input: AVCaptureDeviceInput = {
return try! AVCaptureDeviceInput(device: self.device)
}()

创建AVCaptureMetadataOutput,并设置代理,放进主线程。

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

创建AVCaptureSession,并且设置输入、输出。

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
}()

添加AVCaptureVideoPreviewLayer到UIView中,设置输出类型,开启扫描。

1
2
3
4
5
6
7
8
9
10
override func viewDidLoad() {
super.viewDidLoad()
let layer = AVCaptureVideoPreviewLayer(session: self.session)
layer.frame = self.view.layer.bounds
self.view.layer.insertSublayer(layer, atIndex: 0)
self.output.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
self.session.startRunning()
}

实现AVCaptureMetadataOutputObjectsDelegate协议,扫描到二维码就停止扫描。

1
2
3
4
5
6
7
8
9
10
11
func captureOutput(captureOutput: AVCaptureOutput!,
didOutputMetadataObjects metadataObjects: [AnyObject]!,
fromConnection connection: AVCaptureConnection!) {
if metadataObjects.count > 0 {
self.session.stopRunning()
if let metadataObject = metadataObjects.first
as? AVMetadataMachineReadableCodeObject {
print(metadataObject)
}
}
}

相机权限

这里会出现1个问题,扫码会用到相机,而相机在iOS7后需要用户授权才能访问相机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let authorizationStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
switch authorizationStatus {
case .NotDetermined:
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo) { (granted) in
if granted {
self.startCapture()
} else {
print("没有授权访问相机")
}
}
break;
case .Restricted:
print("访问相机受限")
break;
case .Denied:
print("拒绝访问相机")
break;
case .Authorized:
startCapture()
break;
}

授权状态是一个枚举值

  • NotDetermined 第一次启动相机,相机还未知是否允许授权访问,所以这个时候使用AVCaptureDevice.requestAccessForMediaType来确定用户是否允许授权访问相机。
  • Restricted 未授权,且用户无法更新,如家长控制情况下
  • Denied 用户直接拒绝
  • Authorized已授权可以使用

这里只是简单的打印了一下,而实际上我们更多的会给一个提示需要访问相机, 并且去哪里可以打开打开相机功能。
在iOS8后可以直接跳转到Settings中直接进行设置。

设置屏幕上扫描二维码区域

主流的扫码功能都是有一个框,二维码对准在框内。
先在界面上画一个框,二维码对准框内才能够识别并扫描。
在storyboard中画了一个scanRectView,横坐标、中坐标居中,宽高200,并连线到控制器中。
设置透明、边框为蓝色,宽度为2。

1
2
3
4
5
6
@IBOutlet weak var scanRectView: UIView!
scanRectView.layer.borderColor = UIColor.blueColor().CGColor
scanRectView.layer.borderWidth = 2.0
scanRectView.backgroundColor = nil
scanRectView.opaque = false

设置扫描二维码区域,测试了多种方式,使用通知的方式来计算区域是最简单不过了。
通知记得有添加就得有删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(changeOutputRectOfInterest), name: AVCaptureInputPortFormatDescriptionDidChangeNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
@objc func changeOutputRectOfInterest(notification: NSNotification) {
self.output.rectOfInterest = self.previewLayer.metadataOutputRectOfInterestForRect(self.scanRectView.frame)
}

如何扫描图中二维码

这个功能需要iOS 8+
这个需要导入AVFoundation框架
给二维码控制器加一个长按手势,并添加处理方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 长按识别二维码
@IBAction func touchLongPressGesture(sender: UILongPressGestureRecognizer) {
if NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 {
return
}
let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
guard let QRCGImage = QRView.QRCodeImageView.image?.CGImage else {
return
}
let image = CIImage(CGImage: QRCGImage)
guard let features: [CIFeature] = detector.featuresInImage(image, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]),
feature = features.first else {
return
}
guard let QRCodeFeature = feature as? CIQRCodeFeature else {
return
}
let QRCodeString = QRCodeFeature.messageString
print("识别出来的二维码 = \(QRCodeString)")
}

一维码

特别是超时、商家用的特别多。比如超时付钱的时候收银员都是使用扫描枪扫描一维码进行结算。
一般都是128码。
与生存二维码是一样的 使用CICode128BarcodeGenerator参数,

  • inputMessage必须是ASCII字符。
  • inputQuietSpace 对条形码的每一侧加入空白的像素数。默认值是7.0,最大值是20.0,最小值是0.0.

主要代码

1
2
3
4
5
guard let filter: CIFilter = CIFilter(name: "CICode128BarcodeGenerator") else {
return nil
}
filter.setValue(data, forKey: "inputMessage")
filter.setValue(20.0, forKey: "inputQuietSpace")

BarCode

扫描一维码

与扫描二维码一样,只需要设置AVCaptureMetadataOutput的metadataObjectTypes,发现如果同时设置二维码与一维码,一维码的识别很慢。

1
self.output.metadataObjectTypes = [AVMetadataObjectTypeCode128Code]

参考

CICode128BarcodeGenerator
CIQRCodeGenerator

源码

源码环境

  • Xcode 8.0 (8A218a)
  • Swift 3.0

博客中的代码是Swift2.2,Xcode7.3。
目前源代码已经更新到了Swift3.0。

源码地址

QRCode

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