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。

posted @ 2015-10-12 19:43  Mr.陳  阅读(471)  评论(0)    收藏  举报