iOS开发基础64-二维码生成(CoreImage)与扫描(AVFoundation)
iOS 二维码开发深度解析:生成(CoreImage)与扫描(AVFoundation)
本文系统梳理 iOS 二维码开发:用 CoreImage 的 CIFilter 生成二维码(清晰缩放、容错级别、自定义颜色、中心 Logo),用 AVFoundation 摄像头扫描二维码(权限请求、完整会话配置、代理方法、扫描区域限制、手电筒),以及从相册图片识别二维码(CIDetector)。
一、二维码的生成
1. 简介
从 iOS 7 开始,系统集成了二维码的生成和读取功能(CoreImage 框架)。此前广泛使用的第三方库 zbarSDK 已停止维护,且不支持 64 位处理器(arm64),现代开发应使用系统原生 API。
二维码可存储的内容:
- 纯文本
- URL
- 名片(vCard 格式)
- Wi-Fi 配置
- 等等(理论上任意字符串)
传统条形码(Barcode)只能存储数字,二维码可存储任意文本且容量更大、容错能力更强。
2. 基本生成步骤
#import <CoreImage/CoreImage.h>
- (UIImage *)generateQRCodeWithString:(NSString *)str {
// 1. 实例化二维码滤镜
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
// 2. 恢复滤镜默认属性
[filter setDefaults];
// 3. 将字符串转换为 NSData
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
// 4. 通过 KVO 设置滤镜输入数据
[filter setValue:data forKey:@"inputMessage"];
// 5. 获取滤镜输出的 CIImage
CIImage *outputImage = [filter outputImage];
// 6. 转换为 UIImage(注意:直接转换会模糊,见下方清晰生成方案)
return [UIImage imageWithCIImage:outputImage];
}
3. 生成清晰的二维码(重点)
CIQRCodeGenerator 输出的 CIImage 默认尺寸很小(版本 1 二维码约 23×23 像素),直接用 UIImage imageWithCIImage: 或 scale: 放大会模糊。正确做法是用 CGAffineTransform 缩放 CIImage,让每个像素点整数倍放大,保持锐利:
- (UIImage *)generateQRCodeWithString:(NSString *)str size:(CGFloat)size {
// 1. 创建滤镜
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[filter setDefaults];
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
[filter setValue:data forKey:@"inputMessage"];
CIImage *outputImage = [filter outputImage];
// 2. 计算缩放比例(CIImage 尺寸 → 目标尺寸)
CGRect extent = CGRectIntegral(outputImage.extent);
CGFloat scale = MIN(size / CGRectGetWidth(extent), size / CGRectGetHeight(extent));
// 3. 用 CGAffineTransform 缩放 CIImage(整数倍缩放,保持清晰)
CIImage *scaledImage = [outputImage imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
// 4. 转换为 UIImage
return [UIImage imageWithCIImage:scaledImage];
}
为什么不用
imageWithCIImage:scale::scale:参数是 UIImage 的屏幕缩放因子(类似 @2x/@3x),不是图像内容缩放,不能用来放大二维码尺寸。必须用 CIImage 的 transform 缩放。
4. 容错级别(inputCorrectionLevel)
二维码有四个容错级别,级别越高可遮挡面积越大但二维码越密集:
| 级别 | 标识 | 容错率 | 适用场景 |
|---|---|---|---|
| L | 7% | 低 | 普通二维码 |
| M | 15% | 中 | 默认 |
| Q | 25% | 较高 | 中心带小 Logo |
| H | 30% | 高 | 中心带大 Logo / 部分遮挡 |
// 设置容错级别为 H(最高,适合中心加 Logo)
[filter setValue:@"H" forKey:@"inputCorrectionLevel"];
5. 自定义颜色二维码
默认二维码是黑白的。可以通过 CIFalseColor 滤镜修改前景色和背景色:
- (UIImage *)generateQRCodeWithString:(NSString *)str
size:(CGFloat)size
frontColor:(UIColor *)frontColor
backColor:(UIColor *)backColor {
// 1. 生成基础二维码 CIImage
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[filter setDefaults];
[filter setValue:[str dataUsingEncoding:NSUTF8StringEncoding] forKey:@"inputMessage"];
[filter setValue:@"H" forKey:@"inputCorrectionLevel"];
CIImage *outputImage = [filter outputImage];
// 2. 缩放
CGFloat scale = size / CGRectGetWidth(outputImage.extent);
CIImage *scaledImage = [outputImage imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
// 3. 用 CIFalseColor 滤镜修改颜色
CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor"];
[colorFilter setValue:scaledImage forKey:@"inputImage"];
[colorFilter setValue:[CIColor colorWithCGColor:frontColor.CGColor] forKey:@"inputColor0"]; // 前景色
[colorFilter setValue:[CIColor colorWithCGColor:backColor.CGColor] forKey:@"inputColor1"]; // 背景色
CIImage *coloredImage = [colorFilter outputImage];
return [UIImage imageWithCIImage:coloredImage];
}
6. 中心带 Logo 的二维码
在二维码中心绘制 Logo(需容错级别设为 H,确保遮挡后仍可识别):
- (UIImage *)generateQRCodeWithString:(NSString *)str
size:(CGFloat)size
logo:(UIImage *)logo
logoSize:(CGFloat)logoSize {
// 1. 生成二维码(容错级别 H)
UIImage *qrImage = [self generateQRCodeWithString:str size:size]; // 复用上面的清晰生成方法
// 2. 开启图形上下文,绘制二维码
UIGraphicsBeginImageContextWithOptions(CGSizeMake(size, size), NO, [UIScreen mainScreen].scale);
[qrImage drawInRect:CGRectMake(0, 0, size, size)];
// 3. 绘制中心 Logo(白色圆角背景 + Logo)
CGFloat logoX = (size - logoSize) / 2;
CGFloat logoY = (size - logoSize) / 2;
CGRect logoRect = CGRectMake(logoX, logoY, logoSize, logoSize);
// 白色背景(圆角)
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:logoRect cornerRadius:8];
[[UIColor whiteColor] setFill];
[path fill];
// 裁剪圆角并绘制 Logo
[path addClip];
[logo drawInRect:logoRect];
// 4. 获取结果
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
Logo 面积不宜超过二维码的 30%,否则可能无法识别。
二、二维码的读取(扫描)
1. 简介
读取二维码使用 AVFoundation 框架,通过摄像头实时识别。模拟器没有摄像头,无法扫描,需真机测试。
扫描流程的核心组件:
| 组件 | 作用 |
|---|---|
AVCaptureDevice |
摄像头硬件设备 |
AVCaptureDeviceInput |
摄像头输入(采集图像) |
AVCaptureMetadataOutput |
元数据输出(识别二维码/条形码) |
AVCaptureSession |
会话,协调输入和输出 |
AVCaptureVideoPreviewLayer |
预览图层,显示摄像头画面 |
2. 摄像头权限(必须)
iOS 7+ 访问摄像头需要用户授权,必须在 Info.plist 中添加 NSCameraUsageDescription(摄像头使用说明),否则调用摄像头会崩溃:
<key>NSCameraUsageDescription</key>
<string>需要访问摄像头进行二维码扫描</string>
3. 完整扫描实现
#import <AVFoundation/AVFoundation.h>
@interface QRCodeScannerViewController () <AVCaptureMetadataOutputObjectsDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
@property (nonatomic, copy) void (^completion)(NSString *result);
@end
@implementation QRCodeScannerViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupScanner];
}
- (void)setupScanner {
// 0. 请求摄像头权限
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
if (granted) {
[self startScanner];
} else {
NSLog(@"用户拒绝了摄像头权限");
// 引导用户去设置开启权限
}
});
}];
}
- (void)startScanner {
NSError *error = nil;
// 1. 获取摄像头设备(后置摄像头)
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 或指定后置摄像头:
// AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack];
// 2. 创建输入设备
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input) {
NSLog(@"创建输入失败: %@", error);
return;
}
// 3. 创建元数据输出
AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init];
// 设置代理和回调队列(主线程)
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// 4. 创建会话
self.session = [[AVCaptureSession alloc] init];
// 添加输入
if ([self.session canAddInput:input]) {
[self.session addInput:input];
}
// 添加输出
if ([self.session canAddOutput:output]) {
[self.session addOutput:output];
}
// 5. 设置识别的元数据类型(必须在 addOutput 之后设置,否则报错)
// 只识别二维码
output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];
// 或同时识别二维码和条形码:
// output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeCode128Code];
// 6. 创建预览图层
self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.previewLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:self.previewLayer atIndex:0];
// 7. 启动会话(startRunning 是耗时操作,建议在后台线程调用)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.session startRunning];
});
}
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
// 扫描到元数据时调用
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
if (metadataObjects.count > 0) {
AVMetadataMachineReadableCodeObject *metadataObj = metadataObjects.firstObject;
if ([metadataObj.type isEqualToString:AVMetadataObjectTypeQRCode]) {
NSString *result = metadataObj.stringValue;
NSLog(@"扫描结果: %@", result);
// 停止扫描
[self.session stopRunning];
// 播放音效 + 震动(可选)
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
// 回调结果
if (self.completion) {
self.completion(result);
}
// 关闭页面(根据需求)
// [self dismissViewControllerAnimated:YES completion:nil];
}
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.previewLayer.frame = self.view.bounds;
}
- (void)dealloc {
[self.session stopRunning];
}
@end
注意:
metadataObjectTypes必须在addOutput:之后设置,否则会报错(原文已说明)。startRunning是耗时操作,会阻塞线程,建议在后台队列调用。
4. 限制扫描区域(rectOfInterest)
默认整个屏幕都在扫描,可以通过 rectOfInterest 限制扫描区域(如只扫描屏幕中间的方框):
// 注意:rectOfInterest 的坐标系是归一化的(0~1),且原点在左上角,y 轴向下
// 需要将视图坐标转换为元数据输出的坐标系
CGRect scanRect = CGRectMake(50, 200, 300, 300); // 扫描框在视图中的位置
CGRect interestRect = [self.previewLayer metadataOutputRectOfInterestForRect:scanRect];
output.rectOfInterest = interestRect;
rectOfInterest的坐标系特殊:x/y 范围 0~1,原点在左上角,且 y 轴方向与 UIKit 相反。必须用metadataOutputRectOfInterestForRect:转换,不要手动计算。
5. 手电筒/闪光灯开关
扫描时可开启手电筒(后置摄像头的闪光灯):
- (void)toggleTorch:(BOOL)on {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([device hasTorch] && [device isTorchAvailable]) {
[device lockForConfiguration:nil];
device.torchMode = on ? AVCaptureTorchModeOn : AVCaptureTorchModeOff;
[device unlockForConfiguration];
}
}
三、从相册识别二维码
除了实时扫描,还可以从相册选择图片识别二维码,使用 CIDetector(CoreImage 框架):
#import <CoreImage/CoreImage.h>
// 从 UIImage 识别二维码
- (NSString *)detectQRCodeFromImage:(UIImage *)image {
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode
context:nil
options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];
CIImage *ciImage = [[CIImage alloc] initWithImage:image];
NSArray *features = [detector featuresInImage:ciImage];
for (CIQRCodeFeature *feature in features) {
NSLog(@"识别到二维码: %@", feature.messageString);
return feature.messageString;
}
return nil;
}
配合 UIImagePickerController 从相册选择图片:
#import <UIKit/UIKit.h>
@interface ViewController () <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@end
- (IBAction)selectFromAlbum {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info {
UIImage *image = info[UIImagePickerControllerOriginalImage];
NSString *result = [self detectQRCodeFromImage:image];
NSLog(@"相册识别结果: %@", result);
[picker dismissViewControllerAnimated:YES completion:nil];
}
iOS 14+ 访问相册需要在 Info.plist 添加
NSPhotoLibraryUsageDescription(如果只选一张图片,iOS 14+ 可用 PHPickerViewController,不需要权限)。
四、Swift 版本对照
生成二维码(Swift)
import CoreImage
func generateQRCode(_ string: String, size: CGFloat) -> UIImage? {
guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
filter.setDefaults()
filter.value(string.data(using: .utf8), forKey: "inputMessage")
filter.setValue("H", forKey: "inputCorrectionLevel")
guard let outputImage = filter.outputImage else { return nil }
let scale = size / outputImage.extent.width
let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
return UIImage(ciImage: scaledImage)
}
扫描二维码(Swift)
import AVFoundation
class ScannerVC: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var session: AVCaptureSession!
var previewLayer: AVCaptureVideoPreviewLayer!
override func viewDidLoad() {
super.viewDidLoad()
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted { self.startScan() }
}
}
}
func startScan() {
let device = AVCaptureDevice.default(for: .video)!
let input = try! AVCaptureDeviceInput(device: device)
let output = AVCaptureMetadataOutput()
output.setMetadataObjectsDelegate(self, queue: .main)
session = AVCaptureSession()
session.addInput(input)
session.addOutput(output)
output.metadataObjectTypes = [.qr]
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)
DispatchQueue.global().async {
self.session.startRunning()
}
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let obj = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
obj.type == .qr, let result = obj.stringValue {
session.stopRunning()
print("扫描结果: \(result)")
}
}
}
相册识别二维码(Swift)
import CoreImage
func detectQRCode(from image: UIImage) -> String? {
guard let ciImage = CIImage(image: image),
let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil,
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else { return nil }
let features = detector.features(in: ciImage)
return (features.first as? CIQRCodeFeature)?.messageString
}
五、总结
- 二维码生成:iOS 7+ 原生支持,CoreImage 的
CIQRCodeGenerator滤镜;步骤:创建滤镜 → setDefaults → inputMessage(NSData)→ outputImage;默认输出尺寸小,必须用CGAffineTransform缩放 CIImage 才能生成清晰二维码(不能用 UIImage scale:);inputCorrectionLevel设容错级别(L/M/Q/H,中心加 Logo 用 H);CIFalseColor滤镜自定义前景色/背景色;中心 Logo 用 UIGraphics 绘制(Logo 面积 ≤30%)。 - 二维码扫描:AVFoundation 框架,模拟器不支持需真机;核心组件 AVCaptureDevice(摄像头)、AVCaptureDeviceInput(输入)、AVCaptureMetadataOutput(输出,识别二维码)、AVCaptureSession(会话)、AVCaptureVideoPreviewLayer(预览);iOS 7+ 必须 Info.plist 添加 NSCameraUsageDescription 并 requestAccessForMediaType 请求权限;metadataObjectTypes 必须在 addOutput 之后设置;startRunning 耗时建议后台线程;代理方法 captureOutput:didOutputMetadataObjects:fromConnection: 获取结果;扫描到后 stopRunning + 震动/音效 + 回调。
- 扫描区域限制:
rectOfInterest归一化坐标系(0~1,原点左上,y 轴与 UIKit 相反),必须用metadataOutputRectOfInterestForRect:转换,不要手动算。 - 手电筒:AVCaptureDevice torchMode 开关(hasTorch/isTorchAvailable 判断)。
- 相册识别:CIDetector(CIDetectorTypeQRCode)识别 UIImage 中的二维码;配合 UIImagePickerController(iOS 14+ 推荐 PHPickerViewController 无需权限)。
- zbarSDK:已停止维护,不支持 arm64,现代开发用系统原生 API。

浙公网安备 33010602011771号