iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能

iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能

在iOS开发中,会遇到扫一扫功能,扫一扫是使用摄像头扫码二维码或者条形码,获取对应二维码或条形码内容字符串。通过获得的字符串进行跳转或者打开某个页面开启下一步的业务逻辑。

https://blog.csdn.net/gloryFlow/article/details/132249830
https://img-blog.csdnimg.cn/b6b9b7416e7b45e9ab06c02083ac091f.jpeg#pic_center

一、使用前权限设置

扫一扫功能需要开启相机权限,需要在info.plist文件中添加NSCameraUsageDescription

例如:

<key>NSCameraUsageDescription</key>
<string>开启相机权限,活动扫一扫更快捷</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>开启定位权限</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>开启定位权限</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>开启定位权限</string>
<key>NSMicrophoneUsageDescription</key>
<string>开启麦克风权限</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>添加照片需要您的同意</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>开启照片权限</string>

这里还有其他权限,暂时扫一扫只需要NSCameraUsageDescription。

二、AVCaptureSession扫一扫功能

2.1 需要了解的几个类

  • AVCaptureSession

AVCaptureSession是iOS提供的一个管理和协调输入设备到输出设备之间数据流的对象。

  • AVCaptureDevice

AVCaptureDevice是指硬件设备。

  • AVCaptureDeviceInput

AVCaptureDeviceInput是用来从AVCaptureDevice对象捕获Input数据。

  • AVCaptureMetadataOutput

AVCaptureMetadataOutput是用来处理AVCaptureSession产生的定时元数据的捕获输出的。

  • AVCaptureVideoDataOutput

AVCaptureVideoDataOutput是用来处理视频数据输出的。

  • AVCaptureVideoPreviewLayer

AVCaptureVideoPreviewLayer是相机捕获的视频预览层,是用来展示视频的。

2.2 实现扫一扫功能

在熟悉几个类之后,我们可以初始化session了。
我们为AVCaptureSession添加功能实现所需要的input与output

/** 创建扫描器 */
- (void)loadScanView {AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];if (self.scanConfig.scannerArea == SDScannerAreaDefault) {metadataOutput.rectOfInterest = CGRectMake([self.scannerView scannerOriginX]/self.view.frame.size.height, [self.scannerView scannerOriginY]/self.view.frame.size.width, [self.scannerView scannerWidth]/self.view.frame.size.height, [self.scannerView scannerWidth]/self.view.frame.size.width);}AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];[videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];self.session = [[AVCaptureSession alloc]init];[self.session setSessionPreset:AVCaptureSessionPresetHigh];if ([self.session canAddInput:deviceInput]) {[self.session addInput:deviceInput];}if ([self.session canAddOutput:metadataOutput]) {[self.session addOutput:metadataOutput];}if ([self.session canAddOutput:videoDataOutput]) {[self.session addOutput:videoDataOutput];}metadataOutput.metadataObjectTypes = [SDQrScanTool metadataObjectType:self.scanConfig.scannerType];AVCaptureVideoPreviewLayer *videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;videoPreviewLayer.frame = self.view.layer.bounds;[self.videoPreView.layer insertSublayer:videoPreviewLayer atIndex:0];[self.session startRunning];
}

AVCaptureMetadataOutput实现了方法,设置了delegate

[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

实现AVCaptureMetadataOutputObjectsDelegate的代理方法

- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection;

该方法每当输出捕获并发出新对象时,委托都会收到此消息,获得对应metadataObjectTypes。通过此方法,我们可以当扫描二维码时候获得对应的结果。

#pragma mark -- AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {// 获取扫一扫结果if (metadataObjects && metadataObjects.count > 0) {[self pauseScanning];AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects[0];NSString *stringValue = metadataObject.stringValue;//AVMetadataMachineReadableCodeObject *obj = (AVMetadataMachineReadableCodeObject *)[self.lay transformedMetadataObjectForMetadataObject:metadataObjects.lastObject];//[self changeVideoScale:metadataObject];[self handleScanValue:stringValue];}
}

在AVCaptureVideoDataOutput实现了

[videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];

这里实现了代理AVCaptureVideoDataOutputSampleBufferDelegate中方法

- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;

当捕获新的视频采样缓冲区时,将使用captureOutput:didOutputSampleBuffer:fromConnection:delegate方法将其提供给采样缓冲区代理。

在此方法中,可以观察亮度值,决定是否需要开启灯光。

#pragma mark -- AVCaptureVideoDataOutputSampleBufferDelegate
/** 此方法会实时监听亮度值 */
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer, kCMAttachmentMode_ShouldPropagate);NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];CFRelease(metadataDict);NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy];// 亮度值float brightnessValue = [[exifMetadata objectForKey:(NSString *)kCGImagePropertyExifBrightnessValue] floatValue];if (![self.scannerView flashlightOn]) {if (brightnessValue < -1.0) {[self.scannerView showFlashlight:YES];} else {[self.scannerView hideFlashlight:YES];}}
}

2.3 设置metadataOutput的metadataObjectTypes

扫一扫时候,我们需要设置metadataOutput的metadataObjectTypes,决定扫描的二维码、条形码等。

/** 根据扫描器类型配置支持编码格式 */
+ (NSArray *)metadataObjectType:(SDScannerType)scannerType {switch (scannerType) {case SDScannerTypeQRCode:{return @[AVMetadataObjectTypeQRCode];}break;case SDScannerTypeBarCode:{return @[AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;case SDScannerTypeBoth:{return @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;default:break;}
}

根据扫描器类型配置支持编码格式,我们根据不同类型进行设置。

2.4 扫描开启与关闭

扫一扫使用了摄像头,这里关闭开启需要通过AVCaptureSession来控制

恢复扫一扫功能

/**恢复扫一扫功能*/
- (void)resumeScanning {if (self.session) {[self.session startRunning];[self.scannerView startLineAnimation];}
}

暂停扫一扫

/**暂停扫一扫*/
- (void)pauseScanning {if (self.session) {[self.session stopRunning];[self.scannerView stopLineAnimation];}
}

2.5 开启扫一扫或者打开相册的权限检查

  • 校验是否有相机权限
+ (void)checkCameraAuthorizationStatus:(void(^)(BOOL granted))permissionGranted
{AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];switch (videoAuthStatus) {// 已授权case AVAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case AVAuthorizationStatusNotDetermined:{// 提示用户授权[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {permissionGranted(granted);}];}break;// 用户拒绝授权或权限受限case AVAuthorizationStatusRestricted:case AVAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相机”选项中,允许访问你的相机" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}
}
  • 校验是否有相册权限
/** 校验是否有相册权限 */
+ (void)checkAlbumAuthorizationStatus:(void(^)(BOOL granted))permissionGranted {PHAuthorizationStatus photoAuthStatus = [PHPhotoLibrary authorizationStatus];switch (photoAuthStatus) {// 已授权case PHAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case PHAuthorizationStatusNotDetermined:{[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {permissionGranted(status == PHAuthorizationStatusAuthorized);}];}break;// 用户拒绝授权或权限受限case PHAuthorizationStatusRestricted:case PHAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相片”选项中,允许访问你的相册" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}}

2.6 扫描线条动画

实现扫一扫功能,我们需要实现一下扫一扫的动画效果,看起来界面更加提升用户体验。

我们使用基础动画CABasicAnimation来实现扫描线条的动画效果。

  • 开启动画
/** 添加扫描线条动画 */
- (void)startLineAnimation {// 若已添加动画,则先移除动画再添加[self.scannerLineView.layer removeAllAnimations];CABasicAnimation *lineAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];lineAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, [self scannerWidth] - kScannerLineHeight, 1)];lineAnimation.duration = 4;lineAnimation.repeatCount = MAXFLOAT;lineAnimation.autoreverses = YES; // 动画结束时执行逆动画[self.scannerLineView.layer addAnimation:lineAnimation forKey:scannerLineViewAnmationKey];// 重置动画运行速度为1.0self.scannerLineView.layer.speed = 2.0;
}
  • 暂停动画
/** 暂停扫描器动画 */
- (void)stopLineAnimation {// 取出当前时间,转成动画暂停的时间CFTimeInterval pauseTime = [self.scannerLineView.layer convertTime:CACurrentMediaTime() fromLayer:nil];// 设置动画的时间偏移量,指定时间偏移量的目的是让动画定格在该时间点的位置self.scannerLineView.layer.timeOffset = pauseTime;// 将动画的运行速度设置为0, 默认的运行速度是1.0self.scannerLineView.layer.speed = 0;
}

2.7 界面的其他手电筒操作

在我们需要开启和关闭手电筒,比如在比较黑暗的时候开启手电筒,在有光亮的地方关闭手电筒。

  • 显示手电筒
/** 显示手电筒 */
- (void)showFlashlight:(BOOL)animated {if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;} completion:^(BOOL finished) {self.lightButton.enabled = YES;}];} else {self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;self.lightButton.enabled = YES;}
}
  • 藏手电筒
/** 隐藏手电筒 */
- (void)hideFlashlight:(BOOL)animated {self.lightButton.enabled = NO;if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;self.tipsLabel.alpha = 1.0;} completion:^(BOOL finished) {}];} else {self.tipsLabel.alpha = 1.0;self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;}
}

2.8 绘制扫描区域

通过UIBezierPath绘制出扫描区域,扫描区域之外的则显示透明度为0.7的黑色遮罩效果。

/**draw绘制@param rect rect*/
- (void)drawRect:(CGRect)rect {[super drawRect:rect];// 半透明区域[[UIColor colorWithWhite:0 alpha:0.7] setFill];UIRectFill(rect);// 透明区域CGRect scanner_rect = CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth]);[[UIColor clearColor] setFill];UIRectFill(scanner_rect);// 边框UIBezierPath *borderPath = [UIBezierPath bezierPathWithRect:CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth])];borderPath.lineCapStyle = kCGLineCapRound;borderPath.lineWidth = kScannerBorderWidth;[self.config.scannerBorderColor set];[borderPath stroke];for (int index = 0; index < 4; ++index) {UIBezierPath *tempPath = [UIBezierPath bezierPath];tempPath.lineWidth = kScannerCornerWidth;[self.config.scannerCornerColor set];switch (index) {// 左上角棱角case 0:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + kScannerCornerLength)];}break;// 右上角case 1:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + kScannerCornerLength)];}break;// 左下角case 2:{[tempPath moveToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];}break;// 右下角case 3:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];}break;default:break;}[tempPath stroke];}
}

三、实现打开相册,识别图片二维码

我们打开相册,识别相册中图片的二维码,识别图片中的二维码需要CIDetector。

具体代码如下

#pragma mark -- UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{UIImage *pickImage = info[UIImagePickerControllerOriginalImage];CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];// 获取选择图片中识别结果NSArray *features = [detector featuresInImage:[CIImage imageWithData:UIImagePNGRepresentation(pickImage)]];[picker dismissViewControllerAnimated:YES completion:^{if (features.count > 0) {CIQRCodeFeature *feature = features[0];NSString *stringValue = feature.messageString;[self handleScanValue:stringValue];} else {[self readFromAlbumFailed];}}];
}

四、实现扫一扫及识别图片中二维码的全部代码

实现扫一扫及识别图片中二维码的全部代码,主要是

SDQrScanViewController:UIViewController
SDQrScanView:UIView显示界面
SDQrScanTool:扫一扫权限及设置
SDQrScanConfig:扫一扫边框颜色等

完整代码如下

SDQrScanConfig.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "SDQrScanConfig.h"/**扫描类型*/
typedef NS_ENUM(NSInteger, SDScannerType) {SDScannerTypeQRCode,SDScannerTypeBarCode,SDScannerTypeBoth,
};/**扫描区域*/
typedef NS_ENUM(NSInteger, SDScannerArea) {SDScannerAreaDefault,SDScannerAreaFullScreen,
};/**扫一扫基础配置文件*/
@interface SDQrScanConfig : NSObject/**类型*/
@property (nonatomic, assign) SDScannerType scannerType;/**扫描区域*/
@property (nonatomic, assign) SDScannerArea scannerArea;/**棱角颜色*/
@property (nonatomic, strong) UIColor *scannerCornerColor;/**边框颜色*/
@property (nonatomic, strong) UIColor *scannerBorderColor;/**指示器风格*/
@property (nonatomic, assign) UIActivityIndicatorViewStyle indicatorViewStyle;@end

SDQrScanConfig.m

#import "SDQrScanConfig.h"@implementation SDQrScanConfig- (instancetype)init {self = [super init];if (self) {self.scannerCornerColor = [UIColor colorWithRed:63/255.0 green:187/255.0 blue:54/255.0 alpha:1.0];self.scannerBorderColor = [UIColor whiteColor];self.indicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;self.scannerType = SDScannerTypeQRCode;}return self;
}@end

SDQrScanTool.h

#import <Foundation/Foundation.h>
#import <Photos/PHPhotoLibrary.h>
#import <AVFoundation/AVFoundation.h>
#import "SDQrScanConfig.h"@interface SDQrScanTool : NSObject/**校验是否有相机权限@param permissionGranted 获取相机权限回调*/
+ (void)checkCameraAuthorizationStatus:(void(^)(BOOL granted))permissionGranted;/**校验是否有相册权限@param permissionGranted 获取相机权限回调*/
+ (void)checkAlbumAuthorizationStatus:(void(^)(BOOL granted))permissionGranted;/**根据扫描器类型配置支持编码格式@param scannerType 扫描器类型@return 编码格式组成的数组*/
+ (NSArray *)metadataObjectType:(SDScannerType)scannerType;/**根据扫描器类型配置导航栏标题@param scannerType 扫描器类型@return 标题*/
+ (NSString *)navigationItemTitle:(SDScannerType)scannerType;/**手电筒开关@param on YES:打开 NO:关闭*/
+ (void)flashlightOn:(BOOL)on;@end

SDQrScanTool.m

#import "SDQrScanTool.h"@implementation SDQrScanTool/** 校验是否有相机权限 */
+ (void)checkCameraAuthorizationStatus:(void(^)(BOOL granted))permissionGranted
{AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];switch (videoAuthStatus) {// 已授权case AVAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case AVAuthorizationStatusNotDetermined:{// 提示用户授权[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {permissionGranted(granted);}];}break;// 用户拒绝授权或权限受限case AVAuthorizationStatusRestricted:case AVAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相机”选项中,允许访问你的相机" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}
}/** 校验是否有相册权限 */
+ (void)checkAlbumAuthorizationStatus:(void(^)(BOOL granted))permissionGranted {PHAuthorizationStatus photoAuthStatus = [PHPhotoLibrary authorizationStatus];switch (photoAuthStatus) {// 已授权case PHAuthorizationStatusAuthorized:{permissionGranted(YES);}break;// 未询问用户是否授权case PHAuthorizationStatusNotDetermined:{[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {permissionGranted(status == PHAuthorizationStatusAuthorized);}];}break;// 用户拒绝授权或权限受限case PHAuthorizationStatusRestricted:case PHAuthorizationStatusDenied:{UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"请在”设置-隐私-相片”选项中,允许访问你的相册" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];[alert show];permissionGranted(NO);}break;default:break;}}/** 根据扫描器类型配置支持编码格式 */
+ (NSArray *)metadataObjectType:(SDScannerType)scannerType {switch (scannerType) {case SDScannerTypeQRCode:{return @[AVMetadataObjectTypeQRCode];}break;case SDScannerTypeBarCode:{return @[AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;case SDScannerTypeBoth:{return @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code,AVMetadataObjectTypeEAN8Code,AVMetadataObjectTypeUPCECode,AVMetadataObjectTypeCode39Code,AVMetadataObjectTypeCode39Mod43Code,AVMetadataObjectTypeCode93Code,AVMetadataObjectTypeCode128Code,AVMetadataObjectTypePDF417Code];}break;default:break;}
}/** 根据扫描器类型配置导航栏标题 */
+ (NSString *)navigationItemTitle:(SDScannerType)scannerType {switch (scannerType) {case SDScannerTypeQRCode:{return @"二维码";}break;case SDScannerTypeBarCode:{return @"条码";}break;case SDScannerTypeBoth:{return @"二维码/条码";}break;default:break;}
}/** 手电筒开关 */
+ (void)flashlightOn:(BOOL)on {AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];if ([captureDevice hasTorch] && [captureDevice hasFlash]) {[captureDevice lockForConfiguration:nil];if (on) {[captureDevice setTorchMode:AVCaptureTorchModeOn];[captureDevice setFlashMode:AVCaptureFlashModeOn];}else{[captureDevice setTorchMode:AVCaptureTorchModeOff];[captureDevice setFlashMode:AVCaptureFlashModeOff];}[captureDevice unlockForConfiguration];}
}@end

SDQrScanView.h

#import <UIKit/UIKit.h>
#import "SDQrScanTool.h"
#import "SDBaseControllerView.h"@interface SDQrScanView : SDBaseControllerView- (instancetype)initWithFrame:(CGRect)frame config:(SDQrScanConfig *)config;/**启动扫描线条动画*/
- (void)startLineAnimation;/**停止扫描线条动画*/
- (void)stopLineAnimation;/**添加指示器*/
- (void)addActivityIndicator;/**移除指示器*/
- (void)removeActivityIndicator;/**扫描器坐标点X@return 坐标点X*/
- (CGFloat)scannerOriginX;/**扫描器坐标点Y@return 坐标点Y*/
- (CGFloat)scannerOriginY;/**扫描器宽度@return 宽度*/
- (CGFloat)scannerWidth;/**显示手电筒@param animated 是否附带动画*/
- (void)showFlashlight:(BOOL)animated;/**隐藏手电筒@param animated 是否附带动画*/
- (void)hideFlashlight:(BOOL)animated;/**设置手电筒开关@param on YES:开  NO:关*/
- (void)setFlashlightOn:(BOOL)on;/**获取手电筒当前开关状态@return YES:开  NO:关*/
- (BOOL)flashlightOn;@end

SDQrScanView.m

#import "SDQrScanView.h"
#import "objc/runtime.h"static const CGFloat kScannerScale = 0.7;           //屏幕宽度的比例static const CGFloat kBottomSpace = 50.0;           //居中对齐后向上偏移的距离static const CGFloat kScannerLineHeight = 10.0;     //扫描器线条高度static const CGFloat kTipsHeight = 50.0;            //底部提示高度static const CGFloat kLightSize = 20.0f;            //灯光sizestatic const CGFloat kLightTipsHeight = 15.0f;      //灯光提示间距static const CGFloat kLightTipsPadding = 10.0f;     //灯光提示间距static const CGFloat kScannerBorderWidth = 1.0f;    //扫描器边框宽度static const CGFloat kScannerCornerWidth = 3.0f;    //扫描器棱角宽度static const CGFloat kScannerCornerLength = 20.0f;  //扫描器棱角长度NSString *const scannerLineViewAnmationKey = @"scannerLineViewAnmationKey"; //扫描线条动画Key值@interface SDQrScanView()@property (nonatomic, strong) UIImageView *scannerLineView; /** 扫描线条 */
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator; /** 加载指示器 */
@property (nonatomic, strong) UIButton *lightButton;  /** 手电筒开关 */
@property (nonatomic, strong) UILabel *lightTipsLabel;   /** 手电筒提示文字 */
@property (nonatomic, strong) UILabel *tipsLabel;  /** 扫描器下方提示文字 */@property (nonatomic, strong) SDQrScanConfig *config;@property (nonatomic, assign) BOOL lightOn; //手电筒开关是否打开@end@implementation SDQrScanView- (instancetype)initWithFrame:(CGRect)frame config:(SDQrScanConfig *)config {self = [super initWithFrame:frame];if (self) {self.config = config;[self setupViews];[self bringSubviewToFront:self.navigationBar];}return self;
}- (void)setupViews {self.backgroundColor = [UIColor clearColor];[self addSubview:self.scannerLineView];[self addSubview:self.tipsLabel];[self addSubview:self.lightButton];[self addSubview:self.lightTipsLabel];[self startLineAnimation];
}- (void)layoutSubviews {[super layoutSubviews];CGFloat width = CGRectGetWidth(self.bounds);CGFloat height = CGRectGetHeight(self.bounds);CGFloat scannerWidth = kScannerScale * width;CGFloat originX = (width - scannerWidth)/2;CGFloat originY = (height - scannerWidth)/2 - kBottomSpace;self.scannerLineView.frame = CGRectMake(originX, originY, scannerWidth, kScannerLineHeight);self.tipsLabel.frame = CGRectMake(0, originY + scannerWidth, width, kTipsHeight);self.lightButton.frame = CGRectMake((width - kLightSize)/2.0, CGRectGetMinY(self.tipsLabel.frame) - kLightTipsPadding - kLightTipsHeight - kLightSize, kLightSize, kLightSize);self.lightTipsLabel.frame = CGRectMake(originX, CGRectGetMinY(self.tipsLabel.frame) - kLightTipsPadding - kLightTipsHeight, scannerWidth, kLightTipsHeight);
}#pragma mark -- 手电筒点击事件
- (void)flashlightClicked:(UIButton *)button {button.selected = !button.selected;[self setFlashlightOn:self.lightButton.selected];
}/** 添加扫描线条动画 */
- (void)startLineAnimation {// 若已添加动画,则先移除动画再添加[self.scannerLineView.layer removeAllAnimations];CABasicAnimation *lineAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];lineAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, [self scannerWidth] - kScannerLineHeight, 1)];lineAnimation.duration = 4;lineAnimation.repeatCount = MAXFLOAT;lineAnimation.autoreverses = YES; // 动画结束时执行逆动画[self.scannerLineView.layer addAnimation:lineAnimation forKey:scannerLineViewAnmationKey];// 重置动画运行速度为1.0self.scannerLineView.layer.speed = 2.0;
}/** 暂停扫描器动画 */
- (void)stopLineAnimation {// 取出当前时间,转成动画暂停的时间CFTimeInterval pauseTime = [self.scannerLineView.layer convertTime:CACurrentMediaTime() fromLayer:nil];// 设置动画的时间偏移量,指定时间偏移量的目的是让动画定格在该时间点的位置self.scannerLineView.layer.timeOffset = pauseTime;// 将动画的运行速度设置为0, 默认的运行速度是1.0self.scannerLineView.layer.speed = 0;
}/** 显示手电筒 */
- (void)showFlashlight:(BOOL)animated {if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;} completion:^(BOOL finished) {self.lightButton.enabled = YES;}];} else {self.lightTipsLabel.alpha = 1.0;self.lightButton.alpha = 1.0;self.tipsLabel.alpha = 0;self.lightButton.enabled = YES;}
}/** 隐藏手电筒 */
- (void)hideFlashlight:(BOOL)animated {self.lightButton.enabled = NO;if (animated) {[UIView animateWithDuration:0.6 animations:^{self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;self.tipsLabel.alpha = 1.0;} completion:^(BOOL finished) {}];} else {self.tipsLabel.alpha = 1.0;self.lightTipsLabel.alpha = 0;self.lightButton.alpha = 0;}
}/** 添加指示器 */
- (void)addActivityIndicator {if (!self.activityIndicator) {self.activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:self.config.indicatorViewStyle];self.activityIndicator.center = self.center;[self addSubview:self.activityIndicator];}[self.activityIndicator startAnimating];
}/**移除指示器*/
- (void)removeActivityIndicator {if (self.activityIndicator) {[self.activityIndicator removeFromSuperview];self.activityIndicator = nil;}
}/**设置手电筒开关@param on 是否打开,YES打开,NO,关闭*/
- (void)setFlashlightOn:(BOOL)on {[SDQrScanTool flashlightOn:on];self.lightTipsLabel.text = on ? @"轻触关闭":@"轻触照亮";self.lightButton.selected = on;self.lightOn = on;
}/**获取手电筒当前开关状态@return 开关状态, YES 打开状态, NO 关闭状态*/
- (BOOL)flashlightOn {return self.lightOn;
}/**draw绘制@param rect rect*/
- (void)drawRect:(CGRect)rect {[super drawRect:rect];// 半透明区域[[UIColor colorWithWhite:0 alpha:0.7] setFill];UIRectFill(rect);// 透明区域CGRect scanner_rect = CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth]);[[UIColor clearColor] setFill];UIRectFill(scanner_rect);// 边框UIBezierPath *borderPath = [UIBezierPath bezierPathWithRect:CGRectMake([self scannerOriginX], [self scannerOriginY], [self scannerWidth], [self scannerWidth])];borderPath.lineCapStyle = kCGLineCapRound;borderPath.lineWidth = kScannerBorderWidth;[self.config.scannerBorderColor set];[borderPath stroke];for (int index = 0; index < 4; ++index) {UIBezierPath *tempPath = [UIBezierPath bezierPath];tempPath.lineWidth = kScannerCornerWidth;[self.config.scannerCornerColor set];switch (index) {// 左上角棱角case 0:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + kScannerCornerLength)];}break;// 右上角case 1:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + kScannerCornerLength)];}break;// 左下角case 2:{[tempPath moveToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];[tempPath addLineToPoint:CGPointMake([self scannerOriginX], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];}break;// 右下角case 3:{[tempPath moveToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth] - kScannerCornerLength, [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth])];[tempPath addLineToPoint:CGPointMake([self scannerOriginX] + [self scannerWidth], [self scannerOriginY] + [self scannerWidth] - kScannerCornerLength)];}break;default:break;}[tempPath stroke];}
}#pragma mark - 扫描器坐标点位置
/**扫描器坐标点X@return 坐标点X*/
- (CGFloat)scannerOriginX {CGFloat width = CGRectGetWidth(self.bounds);CGFloat scannerWidth = kScannerScale * width;CGFloat originX = (width - scannerWidth)/2;return originX;
}/**扫描器坐标点Y@return 坐标点Y*/
- (CGFloat)scannerOriginY {CGFloat width = CGRectGetWidth(self.bounds);CGFloat height = CGRectGetHeight(self.bounds);CGFloat scannerWidth = kScannerScale * width;CGFloat originY = (height - scannerWidth)/2 - kBottomSpace;return originY;
}/**扫描器宽度@return 宽度*/
- (CGFloat)scannerWidth {CGFloat width = CGRectGetWidth(self.bounds);CGFloat scannerWidth = kScannerScale * width;return scannerWidth;
}#pragma mark - SETTER/GETTER
/**扫描线条@return 扫描线条ImageView*/
- (UIImageView *)scannerLineView {if (!_scannerLineView) {_scannerLineView = [[UIImageView alloc] initWithFrame:CGRectZero];_scannerLineView.image = [UIImage imageNamed:@"ScannerLine"];}return _scannerLineView;
}/**扫描器下方提示文字@return 下方提示文字Label*/
- (UILabel *)tipsLabel {if (!_tipsLabel) {_tipsLabel = [[UILabel alloc]initWithFrame:CGRectZero];_tipsLabel.textAlignment = NSTextAlignmentCenter;_tipsLabel.textColor = [UIColor lightGrayColor];_tipsLabel.text = @"将二维码/条码放入框内,即可自动扫描";_tipsLabel.font = [UIFont systemFontOfSize:12];}return _tipsLabel;
}/**手电筒开关按钮@return 开关按钮Button*/
- (UIButton *)lightButton {if (!_lightButton) {_lightButton = [UIButton buttonWithType:UIButtonTypeCustom];_lightButton.enabled = NO;_lightButton.alpha = 0;[_lightButton addTarget:self action:@selector(flashlightClicked:) forControlEvents:UIControlEventTouchUpInside];[_lightButton setBackgroundImage:[UIImage imageNamed:@"Flashlight_Off"] forState:UIControlStateNormal];[_lightButton setBackgroundImage:[UIImage imageNamed:@"Flashlight_On"] forState:UIControlStateSelected];}return _lightButton;
}/**手电筒提示文字@return 提示文字控件Label*/
- (UILabel *)lightTipsLabel {if (!_lightTipsLabel) {_lightTipsLabel = [[UILabel alloc] initWithFrame:CGRectZero];_lightTipsLabel.font = [UIFont systemFontOfSize:12];_lightTipsLabel.textColor = [UIColor whiteColor];_lightTipsLabel.text = @"轻触照亮";_lightTipsLabel.alpha = 0;_lightTipsLabel.textAlignment = NSTextAlignmentCenter;}return _lightTipsLabel;
}@end

SDQrScanViewController.h

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import "SDQrScanTool.h"
#import "SDBaseViewController.h"@interface SDQrScanViewController : SDBaseViewController@property (nonatomic, strong) SDQrScanConfig *scanConfig;@end

SDQrScanViewController.m

#import "SDQrScanViewController.h"
#import "SDQrScanView.h"@interface SDQrScanViewController ()<AVCaptureMetadataOutputObjectsDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>@property (nonatomic, strong) SDQrScanView *scannerView;
@property (nonatomic, strong) AVCaptureSession *session;@property (nonatomic, strong) UIView *videoPreView; //视频预览显示视图@end@implementation SDQrScanViewController- (void)dealloc {[[NSNotificationCenter defaultCenter] removeObserver:self];
}- (SDQrScanConfig *)scanConfig {if (!_scanConfig) {_scanConfig = [[SDQrScanConfig alloc] init];}return _scanConfig;
}- (SDQrScanView *)scannerView {if (!_scannerView) {_scannerView = [[SDQrScanView alloc] initWithFrame:self.view.bounds config:self.scanConfig];}return _scannerView;
}- (UIView *)videoPreView {if (!_videoPreView) {_videoPreView = [[UIView alloc] initWithFrame:self.view.bounds];}_videoPreView.backgroundColor = [UIColor clearColor];return _videoPreView;
}#pragma mark - Configure NavigationBar
- (void)configureNavigationBar {SDNavButtonItem *leftButtonItem = [[SDNavButtonItem alloc] initWithTitle:nil image:[UIImage imageNamed:@"ic_nav_back_gray"] target:self action:@selector(leftBarClicked)];SDNavButtonItem *rightButtonItem = [[SDNavButtonItem alloc] initWithTitle:nil image:[UIImage imageNamed:@"ic_nav_common_download"] target:self action:@selector(rightBarClicked)];self.scannerView.navigationBar.navTitleView = [[SDNavigationTitleView alloc] initWidthTitle:@"扫一扫" subView:nil];self.scannerView.navigationBar.leftNavItem = leftButtonItem;// self.scannerView.navigationBar.rightNavItem = rightButtonItem;
}- (void)leftBarClicked {[self.navigationController popViewControllerAnimated:YES];
}- (void)rightBarClicked {// 扫一扫
}#pragma mark - loadView
- (void)loadView {[super loadView];
}- (void)viewDidLoad {[super viewDidLoad];self.navigationItem.title = @"扫一扫";[self configureNavigationBar];[self setupScannerLayer];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appDidBecomeActive:)name:UIApplicationDidBecomeActiveNotificationobject:nil];[[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(appWillResignActive:)name:UIApplicationWillResignActiveNotificationobject:nil];
}- (void)viewWillAppear:(BOOL)animated {[super viewWillAppear:animated];[self resumeScanning];
}- (void)viewWillDisappear:(BOOL)animated
{[super viewWillDisappear:animated];[self.scannerView setFlashlightOn:NO];[self.scannerView hideFlashlight:YES];
}- (void)setupScannerLayer {self.view.backgroundColor = [UIColor blackColor];UIBarButtonItem *albumItem = [[UIBarButtonItem alloc]initWithTitle:@"相册" style:UIBarButtonItemStylePlain target:self action:@selector(showAlbum)];[albumItem setTintColor:[UIColor blackColor]];self.navigationItem.rightBarButtonItem = albumItem;[self.view addSubview:self.videoPreView];[self.view addSubview:self.scannerView];// 校验相机权限[SDQrScanTool checkCameraAuthorizationStatus:^(BOOL granted) {if (granted) {dispatch_async(dispatch_get_main_queue(), ^{[self loadScanView];});}}];
}/** 创建扫描器 */
- (void)loadScanView {AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];if (self.scanConfig.scannerArea == SDScannerAreaDefault) {metadataOutput.rectOfInterest = CGRectMake([self.scannerView scannerOriginX]/self.view.frame.size.height, [self.scannerView scannerOriginY]/self.view.frame.size.width, [self.scannerView scannerWidth]/self.view.frame.size.height, [self.scannerView scannerWidth]/self.view.frame.size.width);}AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];[videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];self.session = [[AVCaptureSession alloc]init];[self.session setSessionPreset:AVCaptureSessionPresetHigh];if ([self.session canAddInput:deviceInput]) {[self.session addInput:deviceInput];}if ([self.session canAddOutput:metadataOutput]) {[self.session addOutput:metadataOutput];}if ([self.session canAddOutput:videoDataOutput]) {[self.session addOutput:videoDataOutput];}metadataOutput.metadataObjectTypes = [SDQrScanTool metadataObjectType:self.scanConfig.scannerType];AVCaptureVideoPreviewLayer *videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;videoPreviewLayer.frame = self.view.layer.bounds;[self.videoPreView.layer insertSublayer:videoPreviewLayer atIndex:0];[self.session startRunning];
}#pragma mark -- 跳转相册
- (void)imagePicker {UIImagePickerController *imagePicker = [[UIImagePickerController alloc]init];imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;imagePicker.delegate = self;[self presentViewController:imagePicker animated:YES completion:nil];
}#pragma mark -- AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {// 获取扫一扫结果if (metadataObjects && metadataObjects.count > 0) {[self pauseScanning];AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects[0];NSString *stringValue = metadataObject.stringValue;//AVMetadataMachineReadableCodeObject *obj = (AVMetadataMachineReadableCodeObject *)[self.lay transformedMetadataObjectForMetadataObject:metadataObjects.lastObject];//[self changeVideoScale:metadataObject];[self handleScanValue:stringValue];}
}#pragma mark -- AVCaptureVideoDataOutputSampleBufferDelegate
/** 此方法会实时监听亮度值 */
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer, kCMAttachmentMode_ShouldPropagate);NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];CFRelease(metadataDict);NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy];// 亮度值float brightnessValue = [[exifMetadata objectForKey:(NSString *)kCGImagePropertyExifBrightnessValue] floatValue];if (![self.scannerView flashlightOn]) {if (brightnessValue < -1.0) {[self.scannerView showFlashlight:YES];} else {[self.scannerView hideFlashlight:YES];}}
}- (void)showAlbum {// 校验相册权限[SDQrScanTool checkAlbumAuthorizationStatus:^(BOOL granted) {if (granted) {[self imagePicker];}}];
}#pragma mark -- UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{UIImage *pickImage = info[UIImagePickerControllerOriginalImage];CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];// 获取选择图片中识别结果NSArray *features = [detector featuresInImage:[CIImage imageWithData:UIImagePNGRepresentation(pickImage)]];[picker dismissViewControllerAnimated:YES completion:^{if (features.count > 0) {CIQRCodeFeature *feature = features[0];NSString *stringValue = feature.messageString;[self handleScanValue:stringValue];} else {[self readFromAlbumFailed];}}];
}- (void)changeVideoScale:(AVMetadataMachineReadableCodeObject *)objc {NSArray *array = objc.corners;CGPoint point = CGPointZero;int index = 0;CFDictionaryRef dict = (__bridge CFDictionaryRef)(array[index++]);// 把点转换为不可变字典// 把字典转换为点,存在point里,成功返回true 其他falseCGPointMakeWithDictionaryRepresentation(dict, &point);NSLog(@"X:%f -- Y:%f",point.x,point.y);CGPoint point2 = CGPointZero;CGPointMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)array[2], &point2);NSLog(@"X:%f -- Y:%f",point2.x,point2.y);NSLog(@"bounds:%@",NSStringFromCGRect(objc.bounds));}#pragma mark -- App 从后台进入前台
- (void)appDidBecomeActive:(NSNotification *)notify {[self resumeScanning];
}#pragma mark -- App 从前台进入后台
- (void)appWillResignActive:(NSNotification *)notify {[self pauseScanning];
}/**恢复扫一扫功能*/
- (void)resumeScanning {if (self.session) {[self.session startRunning];[self.scannerView startLineAnimation];}
}/**暂停扫一扫*/
- (void)pauseScanning {if (self.session) {[self.session stopRunning];[self.scannerView stopLineAnimation];}
}#pragma mark -- 扫一扫API
/**处理扫一扫结果@param value 扫描结果*/
- (void)handleScanValue:(NSString *)value {NSLog(@"handleScanValue === %@", value);
}/**相册选取图片无法读取数据*/
- (void)readFromAlbumFailed {NSLog(@"readFromAlbumFailed");
}- (void)didReceiveMemoryWarning {[super didReceiveMemoryWarning];// Dispose of any resources that can be recreated.
}@end

至此实现了二维码扫一扫Scan及识别图片中二维码功能。

五、小结

iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能。扫一扫是使用摄像头扫码二维码或者条形码,获取对应二维码或条形码内容字符串。识别图中二维码通过CIDetector来识别出内容字符串。最后实现响应的业务逻辑。

学习记录,每天不停进步。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RFID工业识别技术:供应链智能化的科技颠覆

RFID工业识别技术&#xff0c;作为物联网的先锋&#xff0c;正在供应链管理领域展现着前所未有的科技颠覆。从物料追踪到库存管理&#xff0c;再到物流配送&#xff0c;RFID技术以其高效的数据采集和智能的自动化处理&#xff0c;彻底改变着传统供应链的运营方式。 RFID在物料追…

Linux学习————redis服务

目录 一、redis主从服务 一、redis主从服务概念 二、redis主从服务作用 三、缺点 四、主从复制流程 五、搭建主从服务 配置基础环境 下载epel源&#xff0c;下载redis​编辑 二、哨兵模式 一、概念 二、作用 三、缺点 四、结构 五、搭建 修改哨兵配置文件 启动服务…

深入探索:解读创意的力量——idea的下载、初步使用

目录 ​编辑 1.IDEA的简介 2.IDEA的下载 2.1下载路径https://www.jetbrains.com/zh-cn/idea/download/?sectionwindows​编辑​ 2.2下载的步骤 3 idea的初步使用 3.1新建一个简单的Java项目 3.1.1首先需要创建一个新的工程 3.1.2创建一个新的项目&#xff08;模块&am…

信息论基础知识

注意&#xff1a;本文只针对离散随机变量做出探讨&#xff0c;连续随机变量的情况不适用于本文探讨的内容&#xff01; &#xff08;一&#xff09;自信息 1. 自信息 I ( x ) − l o g n P ( x ) \color{blue}I(x) - log_{n}{P(x)} I(x)−logn​P(x) 注意&#xff1a; 若n …

网络协议栈-基础知识

1、分层模型 1.1、OSI七层模型 1、OSI&#xff08;Open System Interconnection&#xff0c;开放系统互连&#xff09;七层网络模型称为开放式系统互联参考模型 &#xff0c;是一个逻辑上的定义&#xff0c;一个规范&#xff0c;它把网络从逻辑上分为了7层。 2、每一层都有相关…

vue报错‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件。

运行我的后台管理项目的时候报错&#xff1a;‘vue-cli-service’ 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 查看自己package.json中是否有vue 或者vue-cli-service 查看自己项目目录下有没有node_module文件夹&#xff0c;如果有删除&#xff0c;然后…

06 为什么需要多线程;多线程的优缺点;程序 进程 线程之间的关系;进程和线程之间的区别

为什么需要多线程 CPU、内存、IO之间的性能差异巨大多核心CPU的发展线程的本质是增加一个可以执行代码工人 多线程的优点 多个执行流&#xff0c;并行执行。&#xff08;多个工人&#xff0c;干不一样的活&#xff09; 多线程的缺点 上下文切换慢&#xff0c;切换上下文典型值…

Python Opencv实践 - 在图像上绘制图形

import cv2 as cv import numpy as np import matplotlib.pyplot as pltimg cv.imread("../SampleImages/pomeranian.png") print(img.shape)plt.imshow(img[:,:,::-1])#画直线 #cv.line(img,start,end,color,thickness) #参考资料&#xff1a;https://blog.csdn.ne…

复古游戏库管理器RomM

什么是 RomM &#xff1f; RomM&#xff08;代表 Rom Manager&#xff09;是一个专注于复古游戏的游戏库管理器。通过 Web 浏览器管理和组织您的所有游戏。受 Jellyfin 的启发&#xff0c;允许您从现代界面管理所有游戏&#xff0c;同时使用 IGDB 元数据丰富它们。 RomM 支持的…

【算法挨揍日记】day02——双指针算法_快乐数、盛最多水的容器

202. 快乐数 202. 快乐数https://leetcode.cn/problems/happy-number/ 题目&#xff1a; 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个…

宋浩高等数学笔记(十一)曲线积分与曲面积分

个人认为同济高数乃至数学一中最烧脑的一章。。。重点在于计算方式的掌握&#xff0c;如果理解不了可以暂时不强求&#xff0c;背熟积分公式即可。此外本贴暂时忽略两类曲面积分之间的联系&#xff0c;以及高斯公式的相关内容&#xff0c;日后会尽快更新&#xff0c;争取高效率…

PHP 之房贷计算器、组合贷

一、等额本金 // &#xff08;等额本金&#xff09; //$loanAmount>贷款金额 //$loanPeriod>贷款年限 //$interestRate>贷款利息 function calculateEqualPrincipalPayment($loanAmount, $loanPeriod, $interestRate) {$monthlyPrincipal $loanAmount / ($loanPerio…

【数据结构】二叉树常见题目

文章目录 前言二叉树概念满二叉树完全二叉树二叉搜索树(二叉排序树)平衡⼆叉搜索树存储⽅式 二叉树OJ二叉树创建字符串二叉树的分层遍历1二叉树的分层遍历2给定一个二叉树, 找到该树中两个指定节点的最近公共祖先二叉树搜索树转换成排序双向链表二叉树展开为链表根据一棵树的前…

山东布谷科技直播系统源码热点分析:不同芯片实现高质量编码与渲染视频的GPU加速功能

在现代科技的迅猛发展下&#xff0c;直播系统源码平台被开发搭建出来&#xff0c;为人们的生活方式带来了很大的改变&#xff0c;直播系统源码平台的好友、短视频、直播、社区等功能让很多人越来越热衷于去在平台上刷视频、看直播、分享生活。用户的喜爱也督促了直播系统源码平…

nodejs+vue+elementui小区物业管理系统_78ahx

课题主要分为四大模块&#xff1a;即管理员模块&#xff0c;物业管理模块、业主模块和维修员模块&#xff0c;主要功能包括&#xff1a;个人中心、物业管理、业主管理、维修员管理、小区公告管理、小区信息管理、房产信息管理、车位信息管理、停车位管理、停车信息管理、缴费信…

Grafana技术文档--基本安装-docker安装并挂载数据卷-《十分钟搭建》-附带监控服务器

阿丹&#xff1a; Prometheus技术文档--基本安装-docker安装并挂载数据卷-《十分钟搭建》_一单成的博客-CSDN博客 在正确安装了Prometheus之后开始使用并安装Grafana作为Prometheus的仪表盘。 一、拉取镜像 搜索可拉取版本 docker search Grafana拉取镜像 docker pull gra…

RabbitMQ:可靠消息传递的强大消息中间件

消息中间件在现代分布式系统中起着关键作用&#xff0c;它们提供了一种可靠且高效的方法来进行异步通信和解耦。在这篇博客中&#xff0c;我们将重点介绍 RabbitMQ&#xff0c;一个广泛使用的开源消息中间件。我们将深入探讨 RabbitMQ 的特性、工作原理以及如何在应用程序中使用…

移动端APP测试常见面试题精析

现在面试测试职位&#xff0c;要求非常全面&#xff0c;那么APP测试一般需要哪些技术呢&#xff1f;下面总结了APP测试常见面试题&#xff1a; 1.Android四大组件? Activity:描述UI&#xff0c;并且处理用户与机器屏幕的交互。应用程序中&#xff0c;一个Activity就相当于手…

Python-OpenCV中的图像处理-霍夫变换

Python-OpenCV中的图像处理-霍夫变换 霍夫变换霍夫直线变换霍夫圆环变换 霍夫变换 霍夫(Hough)变换在检测各种形状的技术中非常流行&#xff0c;如果要检测的形状可以用数学表达式描述&#xff0c;就可以是使用霍夫变换检测它。即使要检测的形状存在一点破坏或者扭曲也是可以使…

FinClip 支持小程序维度域名配置;桌面端体验活动进行中

FinClip 的使命是使您&#xff08;业务专家和开发人员&#xff09;能够通过小程序解决关键业务流程挑战&#xff0c;并完成数字化转型的相关操作。不妨让我们看看在本月的产品与市场发布亮点&#xff0c;看看是否有助于您实现目标。 产品方面的相关动向&#x1f447;&#x1f…