简介
- 二维条码/二维码是用某种
特定的几何图形
按一定规律在平面分布的黑白相间的图形记录数据符号信息的 - 在编码上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息
- 通过图象输入设备或光电扫描设备自动识读以实现信息自动处理
特点
- 每种码制有其特定的字符集
- 每个字符占有一定的宽度
- 具有一定的校验功能
功能
- 信息获取(名片、地图、WIFI密码、资料)
- 网站跳转(跳转到微博、手机网站、网站)
- 广告推送(用户扫码,直接浏览商家推送的视频、音频广告)
- 手机电商(用户扫码、手机直接购物下单)
- 防伪溯源(用户扫码、即可查看生产地;同时后台可以获取最终消费地)
- 优惠促销(用户扫码,下载电子优惠券,抽奖)
- 会员管理(用户手机上获取电子会员信息、VIP服务)
- 手机支付(扫描商品二维码,通过银行或第三方支付提供的手机端通道完成支付)
优点
- 高密度编码,信息容量大:可容纳多达1850个大写字母或2710个数字或1108个字节,或500多个汉字,比普通条码信息容量约高几十倍
- 编码范围广:该条码可以把图片、声音、文字、签字、指纹等可以数字化的信息进行编码,用条码表示出来;可以表示多种语言文字;可表示图像数据
- 容错能力强,具有纠错功能:这使得二维条码因穿孔、污损等引起局部损坏时,照样可以正确得到识读,损毁面积达50%仍可恢复信息 4.译码可靠性高:它比普通条码译码错误率百万分之二要低得多,误码率不超过千万分之一 5.可引入加密措施:保密性、防伪性好 6.成本低,易制作,持久耐用 7.条码符号形状、尺寸大小比例可变 8.二维条码可以使用激光或CCD阅读器识读
安全提示 —— 不要见码就扫
更多内容请参阅:http://baike.baidu.com/view/132241.htm
用户界面搭建
文件准备
- 新建
QRCodeViewController.swift
&QRCode.storyboard
- 在
Storyboard
中添加UIViewController
并且指定子类 - 在视图控制器上嵌入 UINavigationController
加载视图控制器
- 在
HomeTableViewController
中增加scanQRCode
函数,显示QRCode
控制器
@IBAction func scanQRCode() { presentViewController(UIStoryboard.initViewController("QRCode"), animated: true, completion: nil) }
界面布局
- 添加素材
- 将
Navigation Bar
的 Style 设置为Black
-
增加
UITabBar
- 在之前版本中,分
二维码扫描
和条形码扫描
两种方式
- 在之前版本中,分
-
在
AppDelegate
中增加设置外观函数
/// 设置外观(一经设置,全局有效,外观设置要尽量的早)
private func setupAppearance() { UINavigationBar.appearance().tintColor = UIColor.orangeColor() UITabBar.appearance().tintColor = UIColor.orangeColor() }
-
在屏幕中心添加扫描视图
- 垂直居中
- 水平居中
- 宽度:300
- 高度:300
- 背景色:clearColor
-
在扫描视图内部添加
边框图像视图
,边框图片切片如下
Tabbar选择切换
- TabBar 的
Style
修改为Black
- 默认选中第一项
@IBOutlet weak var tabBar: UITabBar!
override func viewDidLoad() { super.viewDidLoad() tabBar.selectedItem = tabBar.items![0] as? UITabBarItem }
- 通过代理监听 Item 选中事件
func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem!) {
heightConstraint.constant = weightConstraint.constant * (item.tag == 1 ? 0.5 : 1) }
冲击波动画
- 添加冲击波图片,并且设置相对于
扫描视图
的参照
- 实现冲击波动画
override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) scanAnimation() } /// 冲击波动画 func scanAnimation() { // 停止图层动画 scanImage.layer.removeAllAnimations() // 设定动画初始约束 self.topScanConstraint.constant = -heightConstraint.constant // 更新视图布局 self.view.layoutIfNeeded() // 开始动画 UIView.animateWithDuration(2.0, animations: { () -> Void in self.topScanConstraint.constant = self.heightConstraint.constant UIView.setAnimationRepeatCount(MAXFLOAT) self.view.layoutIfNeeded() }) } // MARK: - UITabBarDelegate func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem!) { heightConstraint.constant = weightConstraint.constant * (item.tag == 1 ? 0.5 : 1) scanAnimation() }
注意:动画函数的前三句话非常重要!
细节处理
- 勾选扫描视图的
Clip Subviews
属性 - 修改
冲击波
初始Top
约束数值 -300
扫描二维码
第三方框架
ZXing
Android使用多-
ZBar
iOS使用多 -
提示:以上两个框架都是老牌二维码框架,不过都不支持 64 位
- 目前在 iOS 开发中普遍使用苹果的
AVFoundation
框架,但是不支持图片识别功能 AVFoundation
只支持通过摄像头扫描识别
识别原理
代码实现
- 拍摄会话
/// 拍摄会话,是扫描的桥梁
lazy var session: AVCaptureSession = {
return AVCaptureSession() }()
- 摄像头输入设备
/// 摄像头输入
lazy var videoInput: AVCaptureDeviceInput? = {
// 获取摄像头设备
if let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) { return AVCaptureDeviceInput(device: device, error: nil) } return nil }()
- 数据输出
/// 数据输出
lazy var dataOutput: AVCaptureMetadataOutput = {
return AVCaptureMetadataOutput() }()
- 建立通道
func scan() {
// 1. 添加输入设备 if !session.canAddInput(videoInput) { print("无法添加输入设备") return } session.addInput(videoInput) // 2. 添加输出设备 if !session.canAddOutput(dataOutput) { println("无法添加输出设备") return } session.addOutput(dataOutput) println(dataOutput.availableMetadataObjectTypes) }
注意,一定要把输出设备添加到会话后,才有可用数据类型
- 设置数据类型、代理,启动会话
// 2.1 设置扫描数据类型(全部支持)
dataOutput.metadataObjectTypes = dataOutput.availableMetadataObjectTypes
// 2.2 设置输出代理
dataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
// 3. 启动会话
session.startRunning()
- 实现协议方法
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) { println(metadataObjects) }
必须要启动会话,才能开始扫描
- 添加预览视图
/// 预览图层
lazy var previewLayer: AVCaptureVideoPreviewLayer = {
let layer = AVCaptureVideoPreviewLayer(session: self.session) layer.frame = self.view.bounds return layer }()
进一步体会一下此处的
self.
- 闭包内部需要使用
self.
-
在
OC
中使用self.
能够调用属性的getter
方法,确保对象一定能够拿到 -
完整的扫描函数代码
/// 扫描函数
func scan() { // 1. 添加输入设备 if !session.canAddInput(videoInput) { print("无法添加输入设备") return } session.addInput(videoInput) // 2. 添加输出设备 if !session.canAddOutput(dataOutput) { println("无法添加输出设备") return } session.addOutput(dataOutput) println(dataOutput.availableMetadataObjectTypes) // 2.1 设置扫描数据类型(全部支持) dataOutput.metadataObjectTypes = dataOutput.availableMetadataObjectTypes // 2.2 设置输出代理 dataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue()) // 3. 添加预览图层 view.layer.insertSublayer(previewLayer, atIndex: 0) // 4. 启动会话 session.startRunning() }
- 修改扫描代理方法,提取数值
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
for dataObject in metadataObjects { println(dataObject) } }
绘制线条
AVMetadataMachineReadableCodeObject
- bounds
- corners
代码实现
- 绘制图层
lazy var drawLayer: CALayer = {
let layer = CALayer()
layer.frame = self.view.bounds return layer }()
- 添加图层
// 3. 添加图层
view.layer.insertSublayer(drawLayer, atIndex: 0)
view.layer.insertSublayer(previewLayer, atIndex: 0)
// 4. 启动会话
session.startRunning()
注意:一定要用
insertSublayer
,否则会遮挡住TabBar
- 坐标转换
for dataObject in metadataObjects as! [AVMetadataMachineReadableCodeObject] {
println(dataObject) let obj = previewLayer.transformedMetadataObjectForMetadataObject(dataObject) println(obj) }
# 转换前
<AVMetadataMachineReadableCodeObject: 0x170220720,
type="org.iso.QRCode",
bounds={ 0.4,0.4 0.1x0.2 }>
corners { 0.4,0.6 0.5,0.6 0.5,0.4 0.4,0.4 },
time 155921691680958,
stringValue "http://weibo.cn/qr/userinfo?uid=5365823342"
# 转换后
<AVMetadataMachineReadableCodeObject: 0x170622cc0,
type="org.iso.QRCode",
bounds={ 116.6,224.9 79.5x80.0 }>
corners { 116.6,226.1 117.2,304.4 196.1,304.9 195.7,224.9 },
time 155921691680958,
stringValue "http://weibo.cn/qr/userinfo?uid=5365823342"
转换的目的是将采集到的坐标转换成能够识别的坐标数值
- 创建路径
/// 创建路径
///
/// :param: points The value of this property is an array of CFDictionary objects
///
/// :returns: 贝赛尔路径 private func createPath(points: NSArray)-> UIBezierPath { let path = UIBezierPath() var point = CGPoint() var index = 0 // 起始点 CGPointMakeWithDictionaryRepresentation(points[index++] as! CFDictionaryRef, &point) path.moveToPoint(point) // 遍历剩余的点 while index < points.count { CGPointMakeWithDictionaryRepresentation(points[index++] as! CFDictionaryRef, &point) path.addLineToPoint(point) } // 关闭路径 path.closePath() return path }
注意
corners
是保存CFDictionary
对象的数组
- 绘制条码边线
/// 绘制编码变线
private func drawCodeCorners(codeObject: AVMetadataMachineReadableCodeObject) { if codeObject.corners.count == 0 { return } // 建立形状图层 let shapeLayer = CAShapeLayer() shapeLayer.strokeColor = UIColor.greenColor().CGColor shapeLayer.fillColor = UIColor.clearColor().CGColor shapeLayer.lineWidth = 4 shapeLayer.path = createPath(codeObject.corners).CGPath // 添加形状图层 drawLayer.addSublayer(shapeLayer) }
注意:一定要判断
corners
是否包含数据,否则会崩溃
- 清空绘图图层
/// 清空绘图图层
private func clearDrawLayer() { if drawLayer.sublayers != nil { for l in drawLayer.sublayers { l.removeFromSuperlayer() } } }
注意:一定要判断
subLayers
否则会崩溃
- 调整后的代码
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
clearDrawLayer()
for dataObject in metadataObjects as! [AVMetadataMachineReadableCodeObject] { // 转换编码对象 let codeObject = previewLayer.transformedMetadataObjectForMetadataObject(dataObject) as! AVMetadataMachineReadableCodeObject drawCodeCorners(codeObject) println(codeObject) } }
生成二维码
/// 生成二维码
public func generateImage(stringValue: String, avatarImage: UIImage?, avatarScale: CGFloat = 0.25, color: CIColor = CIColor(red: 0, green: 0, blue: 0), backColor: CIColor = CIColor(red: 1, green: 1, blue: 1)) -> UIImage? { let qrFilter = CIFilter(name: "CIQRCodeGenerator") qrFilter.setDefaults() qrFilter.setValue(stringValue.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false), forKey: "inputMessage") let ciImage = qrFilter.outputImage let colorFilter = CIFilter(name: "CIFalseColor") colorFilter.setDefaults() colorFilter.setValue(ciImage, forKey: "inputImage") colorFilter.setValue(color, forKey: "inputColor0") colorFilter.setValue(backColor, forKey: "inputColor1") let transform = CGAffineTransformMakeScale(5, 5) let transformedImage = colorFilter.outputImage.imageByApplyingTransform(transform) let image = UIImage(CIImage: transformedImage) if avatarImage != nil && image != nil { return insertAvatarImage(image!, avatarImage: avatarImage!, scale: avatarScale) } return image } func insertAvatarImage(codeImage: UIImage, avatarImage: UIImage, scale: CGFloat) -> UIImage { let rect = CGRectMake(0, 0, codeImage.size.width, codeImage.size.height) UIGraphicsBeginImageContext(rect.size) codeImage.drawInRect(rect) let avatarSize = CGSizeMake(rect.size.width * scale, rect.size.height * scale) let x = (rect.width - avatarSize.width) * 0.5 let y = (rect.height - avatarSize.height) * 0.5 avatarImage.drawInRect(CGRectMake(x, y, avatarSize.width, avatarSize.height)) let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return result }