媒体捕捉-iOS自定义二维码扫描功能

引言

随着iOS 7引入AV Foundation框架,二维码扫描功能已经成为iOS应用程序中不可或缺的一部分。现今,几乎每个应用都充分利用这一功能,为用户提供了诸如扫码登录、扫码填充等丰富多彩的便捷体验。这项技术不仅丰富了应用功能,也为开发人员的调试工作带来了极大便利。在本博客中,我们将深入探讨如何实现自定义二维码扫描,为您打开更广阔的应用开发可能性。

主要类-AVCaptureMetadataOutput

在二维码扫描中,我们仍然以AVCaptureSession为核心,配置输入(AVCaptureDeviceInput)和输出(AVCaptureOutput)。特别是在输出方面,我们使用AVCaptureMetadataOutput来专门处理摄像头捕获到的二维码、条形码等元数据。

通过设置metadataObjectTypes属性,我们可以灵活地指定要捕获的元数据类型,如二维码、QR码、条形码等。这样的设置允许我们精确控制扫描的类型,提高识别效率。

通过实现AVCaptureMetadataOutputObjectsDelegate代理,我们能够在识别到指定类型的元数据时触发相应的代理方法,从而处理这些元数据。这为开发者提供了处理扫描结果的机会,使得在应用中集成二维码扫描功能变得更为灵活和可定制。

//MARK:检测到指定类型元数据
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{

}

功能实现

为了使描述更清晰,我们将二维码识别和元数据处理分别放置到两个不同的类里面来处理。

PHCameraController:主要负责启动会话,开启进行二维码识别。

PHPreviewView:我们在这个view里面来处理和呈现元数据。

PHCameraController

和其它媒体捕捉的功能几乎一样,配置会话,启动会话。不同的是会话的输出实现,另外我们还在里面定义了一个协议,当有二维码被识别到时会调用这个协议中的方法,将元数据传递给PHPreviewView。

接口:

下面看一下PHCameraController中接口的定义。

#import <AVFoundation/AVFoundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol PHCodeDetectionDelegate <NSObject>
- (void)didDetectCodes:(NSArray *)codes;
@end
@interface PHCameraController : NSObject
@property(nonatomic,weak)id <PHCodeDetectionDelegate>  codeDetectionDelegate;
@property(nonatomic,strong,readonly)AVCaptureSession * captureSession;

///设置会话
- (BOOL)setupSession:(NSError **)error;
///开始会话
- (void)startSession;
///停止会话
- (void)stopSession;

@end
NS_ASSUME_NONNULL_END

外漏了一个只读的AVCaptureSession和三个核心的方法。以及一个codeDetectionDelegate。

实现:

接下来看一下PHCameraController的实现,会相对复杂一点,但我们只需要将关注点放到配置会话输出以及AVCaptureMetadataOutputObjectsDelegate的代理方法上面。

设置会话:
#import "PHCameraController.h"
#import <UIKit/UIKit.h>
@interface PHCameraController ()<AVCaptureMetadataOutputObjectsDelegate>
@property(nonatomic,strong)AVCaptureMetadataOutput * metadataOutput;
@end
@implementation PHCameraController

- (NSString *)sessionPreset{
    return AVCaptureSessionPreset640x480;
}

//MARK:设置会话
- (BOOL)setupSession:(NSError *__autoreleasing  _Nullable *)error{
    self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = self.sessionPreset;
    if (![self setupSessionInputs:error]) {
        return NO;
    }
    if (![self setupSessionOutputs:error]) {
        return NO;
    }
    return YES;
}
配置会话输入:

//MARK:配置会话输入
- (BOOL)setupSessionInputs:(NSError *__autoreleasing  _Nullable *)error{
    BOOL success = [super setupSessionInputs:error];
    if (success) {
        if (self.activeCamera.autoFocusRangeRestrictionSupported) {
            if ([self.activeCamera lockForConfiguration:error]) {
                self.activeCamera.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear;
                [self.activeCamera unlockForConfiguration];
            }
        }
    }
    return success;
}

配置会话输出:
//MARK:配置会话输出
- (BOOL)setupSessionOutputs:(NSError *__autoreleasing  _Nullable *)error{
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    if ([self.captureSession canAddOutput:self.metadataOutput]) {
        [self.captureSession addOutput:self.metadataOutput];
        NSArray * metadataObjectTypes = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeAztecCode,AVMetadataObjectTypeUPCECode];
        self.metadataOutput.metadataObjectTypes = metadataObjectTypes;
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        [self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
        return YES;
    }else{
        return NO;
    }
}
识别到指定类型元数据的回调:
//MARK:检测到指定兴趣点的代理
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    [self.codeDetectionDelegate didDetectCodes:metadataObjects];
}

PHPreviewView

主要负责显示预览画面,及显示元数据内容。

接口:
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface PHPreviewView : UIView

@property(nonatomic,strong)AVCaptureSession * session;


@end

NS_ASSUME_NONNULL_END
实现:
initWithFrame:方法:
@interface PHPreviewView ()<PHCodeDetectionDelegate>


@end

@implementation PHPreviewView

- (id)initWithFrame:(CGRect)frame{
    if ([super initWithFrame:frame]) {
        [self setupView];
    }
    return self;
}


- (void)setupView{
    self.codeLayers = @{}.mutableCopy;
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspect;
}

遵守了一个PHCodeDetectionDelegate协议。

设置预览layer的画面填充方式。

重写方法:

同样我们通过重写layerClass类方法来返回一个AVCaptureVideoPreviewLayer类。

通过重写session的setter来为AVCaptureVideoPreviewLayer设置session。

通过重写session的getter来返回当前session。

+ (Class)layerClass{
    return [AVCaptureVideoPreviewLayer class];
}

- (void)setSession:(AVCaptureSession *)session{
    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}

- (AVCaptureSession *)session{
    return [(AVCaptureVideoPreviewLayer*)self.layer session];
}
定义previewLayer的getter:
//MARK:当前显示画面layer
- (AVCaptureVideoPreviewLayer *)previewLayer{
    return (AVCaptureVideoPreviewLayer *)self.layer;
}
实现PHCodeDetectionDelegate代理:

//MARK:代理 检测到条码
- (void)didDetectCodes:(NSArray *)codes{
    if (codes.count <= 0) {
        return;
        
    }
    NSArray * transformedCodes = [self transformedCodesFromCodes:codes];
    for (AVMetadataMachineReadableCodeObject * code in transformedCodes) {
        NSString * stringValue = code.stringValue;
        if (self.codeArrowLayers.count == 0) {
            self.codeArrowLayers = [self makeCornersLayers];
        }
        for (int i = 0; i < self.codeArrowLayers.count; i ++) {
            if (i >= code.corners.count) {
                break;
            }
            CAShapeLayer * layer = self.codeArrowLayers[i];
            [self.previewLayer addSublayer:layer];
            layer.path = [self bezierPathWithCodes:code.corners index:i].CGPath;
        }
        NSLog(@"String :%@",stringValue);
    }
    [self.delegate didDetectCodes:codes];
}

关于这个方法我们需要分成几部分来理解。

首先调用了一个transformedCodesFromCodes:方法,只是我们自己定义的方法,在里面我们只是调用了AVCaptureVideoPreviewLayer提供的坐标转发方法将设备坐标空间元数据对象转换为视图坐标空间对象,实现如下:

//MARK:坐标转换
- (NSArray *)transformedCodesFromCodes:(NSArray *)codes{
    NSMutableArray * transformedCodes = [NSMutableArray array];
    for (AVMetadataObject * code in codes) {
        AVMetadataObject * transformedCode = [self.previewLayer transformedMetadataObjectForMetadataObject:code];
        [transformedCodes addObject:transformedCode];
    }
    return transformedCodes;
}

读取到的AVMetadataMachineReadableCodeObject对象里面会有一个bounds和corners两个数组,bounds属性提供了识别码的按坐标轴对其的矩形边界,corners属性提供角点字典表示的NSArray。后一个属性更实用,因为使用它可以让我们构建一个与条码的角点坐标紧密对其的Bezier路径。当然还有一个更重要的属性是stringValue也就是我们从二维码中读取到的内容。

示例中我们使用它的corners属性绘制出了二维码的顶点轮廓,实现如下:

创建轮廓layer:

//MARK: makeCornersLayers
- (NSArray *)makeCornersLayers{
    CAShapeLayer * leftTop = [[CAShapeLayer alloc] init];
    CAShapeLayer * leftBottom = [[CAShapeLayer alloc] init];
    CAShapeLayer * rightBottom = [[CAShapeLayer alloc] init];
    CAShapeLayer * rightTop = [[CAShapeLayer alloc] init];
    NSArray * array = @[leftTop,leftBottom,rightBottom,rightTop];
    for (CAShapeLayer * layer in array) {
        layer.strokeColor = [UIColor redColor].CGColor;
        layer.fillColor = [UIColor clearColor].CGColor;
        layer.lineWidth = 2.0;
    }
    return  array;
}

绘制轮廓path:

- (UIBezierPath *)bezierPathWithCodes:(NSArray *)codes index:(NSInteger)index{
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)codes[index], &point);
    CGPoint prePoint = CGPointZero;
    CGPoint postPoint = CGPointZero;
    switch (index) {
        case 0:
        {
            prePoint = CGPointMake(point.x, point.y + 10);
            postPoint = CGPointMake(point.x + 10, point.y);
        }
            break;
        case 1:
        {
            prePoint = CGPointMake(point.x, point.y - 10);
            postPoint = CGPointMake(point.x + 10, point.y);
        }
            break;
        case 2:
        {
            prePoint = CGPointMake(point.x - 10, point.y);
            postPoint = CGPointMake(point.x, point.y - 10);
        }
            break;
        case 3:
        {
            prePoint = CGPointMake(point.x, point.y + 10);
            postPoint = CGPointMake(point.x - 10, point.y);
        }
            break;
    }
    UIBezierPath * path = [UIBezierPath bezierPath];
    [path moveToPoint:prePoint];
    [path addLineToPoint:point];
    [path addLineToPoint:postPoint];
    return path;
}

最后代理调用didDetectCodes:方法将结果传递到视图控制器,并停止会话。

视图控制器实现:

import "ViewController.h"
#import "PHPreviewView"
#import "PHCameraController.h"

@interface ViewController ()<PHPreviewViewDelegate>

///相机控制
@property(nonatomic,strong)PHCameraController * controller;
///预览view
@property(nonatomic,strong)PHPreviewView * previewView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupView];
}

- (void)setupView{
    [self addPreviewView];
    [self configController];
}

//MARK: 添加预览视图
- (void)addPreviewView{
    self.previewView = [[PHPreviewView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.previewView];
    self.previewView.delegate = self;
}

- (void)didDetectCodes:(NSArray *)codes{
    [self.controller stopSession];
}


- (BOOL)prefersStatusBarHidden{
    return YES;
}


@end

结语

当我们深入研究了上述简单的二维码扫描示例后,我们不禁会发现这只是冰山一角。iOS提供了丰富的功能和灵活的接口,让我们能够进一步深挖二维码扫描的世界。举例来说,我们可以定义扫描范围,通过调整参数来适应各种应用场景,从而提高扫描的精准度和效率。

此外,iOS还支持各种花哨的识别效果,可以为用户提供更为生动和愉悦的扫描体验。通过巧妙运用动画、声音等元素,我们能够使二维码扫描不仅仅是一项实用的功能,更是一种与用户互动的方式。

在这个不断创新和演变的移动应用时代,探索二维码扫描功能的可能性就像打开了一扇通往无限可能性的大门。无论是为了提升用户体验,还是为了创造独特的应用功能,二维码扫描都为开发者提供了丰富的创作空间。让我们在不断探索的道路上,发现更多精彩的可能性吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/299992.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

校园-智慧门禁(卡码脸)解决方案

前言 入职新公司也已经一年有余&#xff0c;入职后主要从事的是门禁项目&#xff0c;公司设计的项目是偏saas化的智慧门禁系统&#xff0c;目前已经在多所大学上线&#xff0c;以下是对该项目的个人总结复盘。 一、系统主要功能和扩展功能 可实现学校统一门禁设备管理可实现人…

第14届蓝桥杯省赛scratch真题+解题思路+详细解析

一、选择题 一共有5道选择题&#xff0c;每题10分&#xff0c;共50分&#xff0c;严禁使用程序验证&#xff0c;选择题不答和答错不得分。 1. 运行以下程序&#xff0c;舞台上能看到几只小猫&#xff1f;&#xff08; &#xff09; A. 4 B. 5 C. 6 D. 7 答案&#xff…

软件测试|Linux三剑客之grep命令详解

简介 grep是一款在 Linux 和类 Unix 系统中广泛使用的文本搜索工具。它的名字来源于 Global Regular Expression Print&#xff08;全局正则表达式打印&#xff09;&#xff0c;它的主要功能是根据指定的模式&#xff08;正则表达式&#xff09;在文本文件中搜索并打印匹配的行…

reiserfs文件系统的磁盘布局

reiserfs文件系统的磁盘布局比较简单&#xff0c;它把整块分区分成相同大小的block块&#xff0c;一个block块的大小默认是4K&#xff0c;而最大块数未2^32次方&#xff0c;即一个分区最大大小为16TB。 reiserfs文件系统分区的前64KB总是为分区标签&#xff08;partition labe…

推荐收藏!万字长文带入快速使用 keras

这些年&#xff0c;有很多感悟&#xff1a;一个人精力是有限的&#xff0c;一个人视野也有有限的&#xff0c;你总会不经意间发现优秀人的就在身边。 看我文章的小伙伴应该经常听我说过的一句话&#xff1a;技术要学会交流、分享&#xff0c;不建议闭门造车。一个人可以走的很…

CSS基础笔记-03选择器

CSS基础笔记系列 《CSS基础笔记-01CSS概述》《CSS基础笔记-02动画》 前言 在前面两篇博客中&#xff0c;我实际上已经使用过了选择器。但到底什么是选择器、有什么作用&#xff0c;我反而不能表达出来。因此&#xff0c;决定记录了我的学习和思考。 什么是选择器 selector…

基于 HTTPS 协议配置 Git 连接 GitHub

文章目录 0.安装 Git1.注册 GitHub 账号2.配置 Git 的用户名和邮箱3.远程连接 GitHub 有两种传输协议4.基于 SSH 协议配置 Git 连接 GitHub5.基于 HTTPS 协议配置 Git 连接 GitHub5.1 创建 GitHub 个人访问令牌5.2 有两种方法将本地仓库和远程仓库关联起来5.2.1 第一种方法&…

【大数据】基于 Flink CDC 构建 MySQL 和 Postgres 的 Streaming ETL

基于 Flink CDC 构建 MySQL 和 Postgres 的 Streaming ETL 1.准备阶段1.1 准备教程所需要的组件1.2 下载 Flink 和所需要的依赖包1.3 准备数据1.3.1 在 MySQL 数据库中准备数据1.3.2 在 Postgres 数据库中准备数据 2.启动 Flink 集群和 Flink SQL CLI3.在 Flink SQL CLI 中使用…

x-cmd pkg | norwegianblue - 软件生命周期查询工具

目录 简介首次用户功能特点进一步探索 简介 norwegianblue 由 Hugo van Kemenade 使用 Python 开发&#xff0c;于 2021 年推出。用于显示多种产品的生命周期终止&#xff08;EOL&#xff09;日期的 CLI 工具。基于 endoflife.date 网站的接口&#xff0c;提供有关各种产品的最…

new和delete表达式的工作步骤

new表达式工作步骤 调用一个operator new库函数开辟未类型化的空间 void *operator new(size_t); 在为类型化的空间上调用构造函数&#xff0c;初始化对象的成员 返回相应类型的指针 delete表达式工作步骤 调用相应类型的析构函数,但析构函数并不能删除对象所在的空间&…

处cp社交类微信小程序前端开源(二)

在上一篇文章介绍如何用SpringBoot整合websocket实现在线聊天&#xff0c;这篇文章介绍如何将uniapp社交类前端源码打包部署微信小程序&#xff0c;和如何上线微信小程序&#xff0c;上线需要的资料&#xff0c;并且介绍我是如何获取用户&#xff0c;如何变现&#xff0c;现在的…

nginx下upstream模块详解

目录 一&#xff1a;介绍 二&#xff1a;特性介绍 一&#xff1a;介绍 Nginx的upstream模块用于定义后端服务器组&#xff0c;以及与这些服务器进行通信的方式。它是Nginx负载均衡功能的核心部分&#xff0c;允许将请求转发到多个后端服务器&#xff0c;并平衡负载。 在upst…

前端-基础 常用标签-超链接标签( 锚点链接 )

锚点链接 &#xff1a; 点击链接&#xff0c;可以快速定位到 页面中的某个位置 如果不好理解&#xff0c;讲一个例子&#xff0c;您就马上明白了 >>> 这个是 刘德华的百度百科 &#xff0c;可以看到&#xff0c;页面里面有很多内容&#xff0c;那就得有个目录了 …

RabbitMQ高级

文章目录 一.消息可靠性1.生产者消息确认2.消息持久化3.消费者确认4.消费者失败重试 MQ的一些常见问题 1.消息可靠性问题:如何确保发送的消息至少被消费一次 2.延迟消息问题:如何实现消息的延迟投递 3.高可用问题:如何避免单点的MQ故障而导致的不可用问题 4.消息堆积问题:如…

狂肝100小时,各大厂20W字面试真题分享

有很多童靴问我&#xff0c;有没有大厂的面试集合&#xff0c;可以针对性备考一下&#xff0c;我说面试题网络上有很多&#xff0c;随便搜索一下&#xff0c;就一大把吧。他们回复说&#xff0c;都是针对各个知识点的题目&#xff0c;想要吃透&#xff0c;至少要1-3个月的时间&…

NCC基础开发技能培训

YonBuilder for NCC 是一个带插件的eclipse工具&#xff0c;跟eclipse没什么区别 NC Cloud2021.11版本开发环境搭建改动 https://nccdev.yonyou.com/article/detail/495 不管是NC Cloud 新手还是老NC开发&#xff0c;在开发NC Cloud时开发环境搭建必看&#xff01;&#xff…

命令行模式的rancher如何安装?

在学习kubectl操作的时候&#xff0c;发现rancher也有命令行模式&#xff0c;学习整理记录此文。 说明 rancher 命令是 Rancher 平台提供的命令行工具&#xff0c;用于管理 Rancher 平台及其服务。 前提 已经参照前文安装过了rancher环境了&#xff0c;拥有了自己的k8s集群…

腾讯云代金券介绍及领取教程分享

腾讯云为了吸引用户经常推出各种优惠活动&#xff0c;其中就包括腾讯云代金券&#xff0c;领取之后可用于抵扣腾讯云平台上购买的部分产品或服务的费用。以下是腾讯云代金券的详细介绍及领取教程。 一、腾讯云代金券介绍 腾讯云代金券是腾讯云优惠券的一种&#xff0c;代金券是…

IMU用于无人机故障诊断

最近&#xff0c;来自韩国的研究团队通过开发以IMU为中心的数据驱动诊断方法&#xff0c;旨在多旋翼飞行器可以自我评估其性能&#xff0c;即时识别和解决推进故障。该方法从单纯的常规目视检查跃升为复杂的诊断细微差别&#xff0c;标志着无人机维护的范式转变。 与依赖额外传…

深入理解并解析Flutter Widget

文章目录 完整代码程序入口构建 Widget 结构定义 widget 状态定义 widget UI获取上下文关于build()build() 常用使用 完整代码 import package:english_words/english_words.dart; import package:flutter/material.dart; import package:provider/provider.dart;void main() …
最新文章