ios使用AVFoundation讀取二維碼的方法
二維碼(Quick Response Code,簡稱QR Code)是由水平和垂直兩個方向上的線條設(shè)計而成的一種二維條形碼(barcode)??梢跃幋a網(wǎng)址、電話號碼、文本等內(nèi)容,能夠存儲大量的數(shù)據(jù)信息。自iOS 7以來,二維碼的生成和讀取只需要使用Core Image框架和AVFoundation框架就能輕松實(shí)現(xiàn)。在這里,我們主要介紹二維碼的讀取。關(guān)于二維碼的生成,可以查看使用CIFilter生成二維碼文章中的介紹。
1 二維碼的讀取
讀取二維碼也就是通過掃描二維碼圖像以獲取其所包含的數(shù)據(jù)信息。需要知道的是,任何條形碼(包括二維碼)的掃描都是基于視頻采集(video capture),因此需要使用AVFoundation框架。
掃描二維碼的過程即從攝像頭捕獲二維碼圖像(input)到解析出字符串內(nèi)容(output)的過程,主要是通過AVCaptureSession對象來實(shí)現(xiàn)的。該對象用于協(xié)調(diào)從輸入到輸出的數(shù)據(jù)流,在執(zhí)行過程中,需要先將輸入和輸出添加到AVCaptureSession對象中,然后通過發(fā)送startRunning或stopRunning消息來啟動或停止數(shù)據(jù)流,最后通過AVCaptureVideoPreviewLayer對象將捕獲的視頻顯示在屏幕上。在這里,輸入對象通常是AVCaptureDeviceInput對象,主要是通過AVCaptureDevice的實(shí)例來獲得,而輸出對象通常是AVCaptureMetaDataOutput對象,它是讀取二維碼的核心部分,與AVCaptureMetadataOutputObjectsDelegate協(xié)議結(jié)合使用,可以捕獲在輸入設(shè)備中找到的任何元數(shù)據(jù),并將其轉(zhuǎn)換為可讀的格式。下面是具體步驟:
1、導(dǎo)入AVFoundation框架。
#import <AVFoundation/AVFoundation.h>
2、創(chuàng)建一個AVCaptureSession對象。
AVCaptureSession *captureSession = [[AVCaptureSession alloc] init];
3、為AVCaptureSession對象添加輸入和輸出。
// add input NSError *error; AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; [captureSession addInput:deviceInput]; // add output AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init]; [captureSession addOutput:metadataOutput];
4、配置AVCaptureMetaDataOutput對象,主要是設(shè)置代理和要處理的元數(shù)據(jù)對象類型。
dispatch_queue_t queue = dispatch_queue_create("MyQueue", NULL); [metadataOutput setMetadataObjectsDelegate:self queue:queue]; [metadataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];
需要注意的是,一定要在輸出對象被添加到captureSession之后才能設(shè)置要處理的元數(shù)據(jù)類型,否則會出現(xiàn)下面的錯誤:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: [AVCaptureMetadataOutput setMetadataObjectTypes:] Unsupported type found - use -availableMetadataObjectTypes'
5、創(chuàng)建并設(shè)置AVCaptureVideoPreviewLayer對象來顯示捕獲到的視頻。
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession]; [previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill]; [previewLayer setFrame:self.view.bounds]; [self.view.layer addSublayer:previewLayer];
6、給AVCaptureSession對象發(fā)送startRunning消息以啟動視頻捕獲。
[captureSession startRunning];
7、實(shí)現(xiàn)AVCaptureMetadataOutputObjectsDelegate的captureOutput:didOutputMetadataObjects:fromConnection:方法來處理捕獲到的元數(shù)據(jù),并將其讀取出來。
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { if (metadataObjects != nil && metadataObjects.count > 0) { AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject; if ([[metadataObject type] isEqualToString:AVMetadataObjectTypeQRCode]) { NSString *message = [metadataObject stringValue]; [self.label performSelectorOnMainThread:@selector(setText:) withObject:message waitUntilDone:NO]; } } }
需要提醒的是,由于AVCaptureMetaDataOutput對象代理的設(shè)置,該代理方法會在setMetadataObjectsDelegate:queue:指定的隊(duì)列上調(diào)用,如果需要更新用戶界面,則必須在主線程中進(jìn)行。
2 應(yīng)用示例
下面,我們就做一個如下圖所示的二維碼閱讀器:
其中主要實(shí)現(xiàn)的功能有:
- 通過攝像頭實(shí)時掃描并讀取二維碼。
- 解析從相冊中選擇的二維碼圖片。
由于二維碼的掃描是基于實(shí)時的視頻捕獲,因此相關(guān)的操作無法在模擬器上進(jìn)行測試,也不能在沒有相機(jī)的設(shè)備上進(jìn)行測試。如果想要查看該應(yīng)用,需要連接自己的iPhone設(shè)備來運(yùn)行。
2.1 創(chuàng)建項(xiàng)目
打開Xcode,創(chuàng)建一個新的項(xiàng)目(File\New\Project...),選擇iOS一欄下的Application中的Single View Application模版,然后點(diǎn)擊Next,填寫項(xiàng)目選項(xiàng)。在Product Name中填寫QRCodeReaderDemo,選擇Objective-C語言,點(diǎn)擊Next,選擇文件位置,并單擊Create創(chuàng)建項(xiàng)目。
2.2 構(gòu)建界面
打開Main.storyboard文件,在當(dāng)前控制器中嵌入導(dǎo)航控制器,并添加標(biāo)題QR Code Reader:
在視圖控制器中添加ToolBar、Flexible Space Bar Button Item、Bar Button Item、View,布局如下:
其中,各元素及作用:
- ToolBar:添加在控制器視圖的最底部,其Bar Item標(biāo)題為Start,具有雙重作用,用于啟動和停止掃描。
- Flexible Space Bar Button Item:分別添加在Start的左右兩側(cè),用于固定Start 的位置使其居中顯示。
- Bar Button Item:添加在導(dǎo)航欄的右側(cè),標(biāo)題為Album,用于從相冊選擇二維碼圖片進(jìn)行解析。
- View:添加在控制器視圖的中間,用于稍后設(shè)置掃描框。在這里使用自動布局固定寬高均為260,并且水平和垂直方向都是居中。
創(chuàng)建一個名為ScanView的新文件(File\New\File…),它是UIView的子類。然后選中視圖控制器中間添加的View,將該視圖的類名更改為ScanView:
打開輔助編輯器,將storyboard中的元素連接到代碼中:
注意,需要在ViewController.m文件中導(dǎo)入ScanView.h文件。
2.3 添加代碼
2.3.1 掃描二維碼
首先在ViewController.h文件中導(dǎo)入AVFoundation框架:
#import <AVFoundation/AVFoundation.h>
切換到ViewController.m文件,添加AVCaptureMetadataOutputObjectsDelegate協(xié)議,并在接口部分添加下面的屬性:
@interface ViewController ()<AVCaptureMetadataOutputObjectsDelegate> // properties @property (assign, nonatomic) BOOL isReading; @property (strong, nonatomic) AVCaptureSession *captureSession; @property (strong, nonatomic) AVCaptureVideoPreviewLayer *previewLayer;
在viewDidLoad方法中添加下面代碼:
- (void)viewDidLoad { [super viewDidLoad]; self.isReading = NO; self.captureSession = nil; }
然后在實(shí)現(xiàn)部分添加startScanning方法和stopScanning方法及相關(guān)代碼:
- (void)startScanning { self.captureSession = [[AVCaptureSession alloc] init]; // add input NSError *error; AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; AVCaptureDeviceInput *deviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error]; if (!deviceInput) { NSLog(@"%@", [error localizedDescription]); } [self.captureSession addInput:deviceInput]; // add output AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init]; [self.captureSession addOutput:metadataOutput]; // configure output dispatch_queue_t queue = dispatch_queue_create("MyQueue", NULL); [metadataOutput setMetadataObjectsDelegate:self queue:queue]; [metadataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]]; // configure previewLayer self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession]; [self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill]; [self.previewLayer setFrame:self.view.bounds]; [self.view.layer addSublayer:self.previewLayer]; // start scanning [self.captureSession startRunning]; } - (void)stopScanning { [self.captureSession stopRunning]; self.captureSession = nil; [self.previewLayer removeFromSuperlayer]; }
找到startStopAction:并在該方法中調(diào)用上面的方法:
- (IBAction)startStopAction:(id)sender { if (!self.isReading) { [self startScanning]; [self.view bringSubviewToFront:self.toolBar]; [self.startStopButton setTitle:@"Stop"]; } else { [self stopScanning]; [self.startStopButton setTitle:@"Start"]; } self.isReading = !self.isReading; }
至此,二維碼掃描相關(guān)的代碼已經(jīng)完成,如果想要它能夠正常運(yùn)行的話,還需要在Info.plist文件中添加NSCameraUsageDescription鍵及相應(yīng)描述以訪問相機(jī):
需要注意的是,現(xiàn)在只能掃描二維碼但是還不能讀取到二維碼中的內(nèi)容,不過我們可以連接設(shè)備,運(yùn)行試下:
2.3.2 讀取二維碼
讀取二維碼需要實(shí)現(xiàn)AVCaptureMetadataOutputObjectsDelegate協(xié)議的captureOutput:didOutputMetadataObjects:fromConnection:方法:
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { if (metadataObjects != nil && metadataObjects.count > 0) { AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject; if ([[metadataObject type] isEqualToString:AVMetadataObjectTypeQRCode]) { NSString *message = [metadataObject stringValue]; [self performSelectorOnMainThread:@selector(displayMessage:) withObject:message waitUntilDone:NO]; [self performSelectorOnMainThread:@selector(stopScanning) withObject:nil waitUntilDone:NO]; [self.startStopButton performSelectorOnMainThread:@selector(setTitle:) withObject:@"Start" waitUntilDone:NO]; self.isReading = NO; } } } - (void)displayMessage:(NSString *)message { UIViewController *vc = [[UIViewController alloc] init]; UITextView *textView = [[UITextView alloc] initWithFrame:vc.view.bounds]; [textView setText:message]; [textView setFont:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]]; textView.editable = NO; [vc.view addSubview:textView]; [self.navigationController showViewController:vc sender:nil]; }
在這里我們將掃碼結(jié)果顯示在一個新的視圖中,如果你運(yùn)行程序的話應(yīng)該可以看到掃描的二維碼內(nèi)容了。
另外,為了使我們的應(yīng)用更逼真,可以在掃描到二維碼信息時讓它播放聲音。這首先需要在項(xiàng)目中添加一個音頻文件:
然后在接口部分添加一個AVAudioPlayer對象的屬性:
@property (strong, nonatomic) AVAudioPlayer *audioPlayer;
在實(shí)現(xiàn)部分添加loadSound方法及代碼,并在viewDidLoad中調(diào)用該方法:
- (void)loadSound { NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"beep" ofType:@"mp3"]; NSURL *soundURL = [NSURL URLWithString:soundFilePath]; NSError *error; self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error:&error]; if (error) { NSLog(@"Could not play sound file."); NSLog(@"%@", [error localizedDescription]); } else { [self.audioPlayer prepareToPlay]; } } - (void)viewDidLoad { ... [self loadSound]; }
最后,在captureOutput:didOutputMetadataObjects:fromConnection:方法中添加下面的代碼來播放聲音:
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection { if (metadataObjects != nil && metadataObjects.count > 0) { AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject; if ([[metadataObject type] isEqualToString:AVMetadataObjectTypeQRCode]) { ... self.isReading = NO; // play sound if (self.audioPlayer) { [self.audioPlayer play]; } } }
2.3.3 設(shè)置掃描框
目前點(diǎn)擊Start按鈕,整個視圖范圍都可以掃描二維碼?,F(xiàn)在,我們需要設(shè)置一個掃描框,以限制只有掃描框區(qū)域內(nèi)的二維碼被讀取。在這里,將掃描區(qū)域設(shè)置為storyboard中添加的視圖,即scanView。
在實(shí)現(xiàn)部分找到startReading方法,添加下面的代碼:
- (void)startScanning { // configure previewLayer ... // set the scanning area [[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { metadataOutput.rectOfInterest = [self.previewLayer metadataOutputRectOfInterestForRect:self.scanView.frame]; }]; // start scanning ... }
需要注意的是,rectOfInterest屬性不能在設(shè)置 metadataOutput 時直接設(shè)置,而需要在AVCaptureInputPortFormatDescriptionDidChangeNotification通知里設(shè)置,否則 metadataOutputRectOfInterestForRect:方法會返回 (0, 0, 0, 0)。
為了讓掃描框更真實(shí)的顯示,我們需要自定義ScanView,為其繪制邊框、四角以及掃描線。
首先打開ScanView.m文件,在實(shí)現(xiàn)部分重寫initWithCoder:方法,為scanView設(shè)置透明的背景顏色:
- (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { self.backgroundColor = [UIColor clearColor]; } return self; }
然后重寫drawRect:方法,為scanView繪制邊框和四角:
- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // 繪制白色邊框 CGContextAddRect(context, self.bounds); CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); CGContextSetLineWidth(context, 2.0); CGContextStrokePath(context); // 繪制四角: CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor); CGContextSetLineWidth(context, 5.0); // 左上角: CGContextMoveToPoint(context, 0, 30); CGContextAddLineToPoint(context, 0, 0); CGContextAddLineToPoint(context, 30, 0); CGContextStrokePath(context); // 右上角: CGContextMoveToPoint(context, self.bounds.size.width - 30, 0); CGContextAddLineToPoint(context, self.bounds.size.width, 0); CGContextAddLineToPoint(context, self.bounds.size.width, 30); CGContextStrokePath(context); // 右下角: CGContextMoveToPoint(context, self.bounds.size.width, self.bounds.size.height - 30); CGContextAddLineToPoint(context, self.bounds.size.width, self.bounds.size.height); CGContextAddLineToPoint(context, self.bounds.size.width - 30, self.bounds.size.height); CGContextStrokePath(context); // 左下角: CGContextMoveToPoint(context, 30, self.bounds.size.height); CGContextAddLineToPoint(context, 0, self.bounds.size.height); CGContextAddLineToPoint(context, 0, self.bounds.size.height - 30); CGContextStrokePath(context); }
如果希望在掃描過程中看到剛才繪制的掃描框,還需要切換到ViewController.m文件,在startStopAction:方法中添加下面的代碼來顯示掃描框:
- (IBAction)startStopAction:(id)sender { if (!self.isReading) { ... [self.view bringSubviewToFront:self.toolBar]; // display toolBar [self.view bringSubviewToFront:self.scanView]; // display scanView ... } ... }
現(xiàn)在運(yùn)行,你會看到下面的效果:
接下來我們繼續(xù)添加掃描線。
首先在ScanView.h文件的接口部分聲明一個NSTimer對象的屬性:
@property (nonatomic, strong) NSTimer *timer;
然后切換到ScanView.m文件,在實(shí)現(xiàn)部分添加loadScanLine方法及代碼,并在initWithCoder:方法中調(diào)用:
- (void)loadScanLine { self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:YES block:^(NSTimer * _Nonnull timer) { UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 1.0)]; lineView.backgroundColor = [UIColor greenColor]; [self addSubview:lineView]; [UIView animateWithDuration:3.0 animations:^{ lineView.frame = CGRectMake(0, self.bounds.size.height, self.bounds.size.width, 2.0); } completion:^(BOOL finished) { [lineView removeFromSuperview]; }]; }]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { ... if (self) { ... [self loadScanLine]; } ... }
然后切換到ViewController.m文件,在startStopAction:方法中添加下面代碼以啟用和暫停計時器:
- (IBAction)startStopAction:(id)sender { if (!self.isReading) { ... [self.view bringSubviewToFront:self.scanView]; // display scanView self.scanView.timer.fireDate = [NSDate distantPast]; //start timer ... } else { [self stopScanning]; self.scanView.timer.fireDate = [NSDate distantFuture]; //stop timer ... } ... }
最后,再在viewWillAppear:的重寫方法中添加下面代碼:
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.scanView.timer.fireDate = [NSDate distantFuture]; }
可以運(yùn)行看下:
2.3.4 從圖片解析二維碼
從iOS 8開始,可以使用Core Image框架中的CIDetector解析圖片中的二維碼。在這個應(yīng)用中,我們通過點(diǎn)擊Album按鈕,從相冊選取二維碼來解析。
在寫代碼之前,需要在Info.plist文件中添加NSPhotoLibraryAddUsageDescription鍵及相應(yīng)描述以訪問相冊:
然后在ViewController.m文件中添加UIImagePickerControllerDelegate和UINavigationControllerDelegate協(xié)議:
@interface ViewController ()<AVCaptureMetadataOutputObjectsDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
在實(shí)現(xiàn)部分找到readingFromAlbum:方法,添加下面代碼以訪問相冊中的圖片:
- (IBAction)readingFromAlbum:(id)sender { UIImagePickerController *picker = [[UIImagePickerController alloc] init]; picker.delegate = self; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; picker.allowsEditing = YES; [self presentViewController:picker animated:YES completion:nil]; }
然后實(shí)現(xiàn)UIImagePickerControllerDelegate的imagePickerController:didFinishPickingMediaWithInfo:方法以解析選取的二維碼圖片:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info { [picker dismissViewControllerAnimated:YES completion:nil]; UIImage *selectedImage = [info objectForKey:UIImagePickerControllerEditedImage]; CIImage *ciImage = [[CIImage alloc] initWithImage:selectedImage]; CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy:CIDetectorAccuracyLow}]; NSArray *features = [detector featuresInImage:ciImage]; if (features.count > 0) { CIQRCodeFeature *feature = features.firstObject; NSString *message = feature.messageString; // display message [self displayMessage:message]; // play sound if (self.audioPlayer) { [self.audioPlayer play]; } } }
現(xiàn)在可以運(yùn)行試下從相冊選取二維碼來讀?。?/p>
上圖顯示的是在模擬器中運(yùn)行的結(jié)果。
至此,我們的二維碼閱讀器已經(jīng)全部完成,如果需要完整代碼,可以下載QRCodeReaderDemo查看。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
iOS中.a和.framework靜態(tài)庫的創(chuàng)建與.bundle資源包的使用詳解
這篇文章主要給大家介紹了關(guān)于在iOS中.a和.framework靜態(tài)庫的創(chuàng)建與.bundle資源包的使用的相關(guān)資料,文中介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12如何通過Objective-C的枚舉學(xué)習(xí)iOS中位操作.md詳解
這篇文章主要給大家介紹了關(guān)于如何通過Objective-C的枚舉學(xué)習(xí)iOS中位操作.md的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對各位iOS開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Swift 2.1 為 UIView 添加點(diǎn)擊事件和點(diǎn)擊效果
本文主要介紹 Swift UIView,這里給大家提供代碼示例作為參考為UIView 添加點(diǎn)擊事件和點(diǎn)擊效果,希望能幫助IOS開發(fā)的同學(xué)2016-07-07iOS在頁面銷毀時如何優(yōu)雅的cancel網(wǎng)絡(luò)請求詳解
這篇文章主要給大家介紹了關(guān)于iOS在頁面銷毀時如何優(yōu)雅的cancel網(wǎng)絡(luò)請求的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05