很多應用都有掃描二維碼的功能,在開發這些功能時大家都可能接觸過 ZXing 或 ZBar 這類第三方掃碼庫,但從 iOS 7 開始系統原生 API 就支援通過掃描擷取二維碼的功能。今天就來說說原生掃碼的那些事。
0、相機權限
也是從 iOS 7 開始,應用要使用相機功能首先要獲得使用者的授權,是以要先判斷授權情況。
- 判斷授權情況方法:
AVAuthorizationStatus authorizationStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
- AVAuthorizationStatus enum 值有:
typedef NS_ENUM(NSInteger, AVAuthorizationStatus) {
AVAuthorizationStatusNotDetermined = 0,
AVAuthorizationStatusRestricted, // 受限,有可能開啟了通路限制
AVAuthorizationStatusDenied,
AVAuthorizationStatusAuthorized
} NS_AVAILABLE_IOS(7_0);
- 請求授權方法:
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler: ^(BOOL granted) {
if (granted) {
[self startCapture]; // 獲得授權
} else {
NSLog(@"%@", @"通路受限");
}
}];
- 完整授權處理邏輯:
AVAuthorizationStatus authorizationStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
switch (authorizationStatus) {
case AVAuthorizationStatusNotDetermined: {
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler: ^(BOOL granted) {
if (granted) {
[self startCapture];
} else {
NSLog(@"%@", @"通路受限");
}
}];
break;
}
case AVAuthorizationStatusAuthorized: {
[self startCapture];
break;
}
case AVAuthorizationStatusRestricted:
case AVAuthorizationStatusDenied: {
NSLog(@"%@", @"通路受限");
break;
}
default: {
break;
}
}
1、掃碼
- AVCaptureSession
掃碼是一個從攝像頭(input)到 解析出字元串(output) 的過程,用AVCaptureSession 來協調。其中是通過 AVCaptureConnection 來連接配接各個 input 和 output,還可以用它來控制 input 和 output 的 資料流向。它們的關系如下圖:
- 掃碼的代碼很簡單,如下:
AVCaptureSession *session = [[AVCaptureSession alloc] init];
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (deviceInput) {
[session addInput:deviceInput];
AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];
[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
[session addOutput:metadataOutput]; // 這行代碼要在設定 metadataObjectTypes 前
metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
previewLayer.frame = self.view.frame;
[self.view.layer insertSublayer:previewLayer atIndex:0];
[session startRunning];
} else {
NSLog(@"%@", error);
}
- 掃碼結果從委托方法傳回:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject;
if ([metadataObject.type isEqualToString:AVMetadataObjectTypeQRCode] && !self.isQRCodeCaptured) { // 成功後系統不會停止掃描,可以用一個變量來控制。
self.isQRCodeCaptured = YES;
NSLog(@"%@", metadataObject.stringValue);
}
}
2、從圖檔檔案解析(iOS 8 起)
從 iOS 8 開始你也可以從圖檔檔案解析出二維碼,用到 Core Image 的 CIDetector。
代碼也很簡單:
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{ CIDetectorAccuracy:CIDetectorAccuracyHigh }];
CIImage *image = [[CIImage alloc] initWithImage:[UIImage imageNamed:@"foobar.png"]];
NSArray *features = [detector featuresInImage:image];
for (CIQRCodeFeature *feature in features) {
NSLog(@"%@", feature.messageString);
}
(foobar.png)
3、生成二維碼圖檔
生成二維碼用到了 CIQRCodeGenerator 這種 CIFilter。它有兩個字段可以設定,inputMessage 和 inputCorrectionLevel。inputMessage 是一個 NSData 對象,可以是字元串也可以是一個 URL。inputCorrectionLevel 是一個單字母(@"L", @"M", @"Q", @"H" 中的一個),表示不同級别的容錯率,預設為 @"M"。
QR碼有容錯能力,QR碼圖形如果有破損,仍然可以被機器讀取内容,最高可以到7%~30%面積破損仍可被讀取。是以QR碼可以被廣泛使用在運輸外箱上。
相對而言,容錯率愈高,QR碼圖形面積愈大。是以一般折衷使用15%容錯能力。
錯誤修正容量 L水準 7%的字碼可被修正
M水準 15%的字碼可被修正
Q水準 25%的字碼可被修正
H水準 30%的字碼可被修正
是以很多二維碼的中間都有頭像之類的圖檔但仍然可以識别出來就是這個原因。
代碼如下,應該注意的是:
- 1)官方建議使用 NSISOLatin1StringEncoding 來編碼,但經測試這種編碼對中文或表情無法生成,改用 NSUTF8StringEncoding 就可以了。
- 2)生成的圖檔尺寸(outputImage.extent.size)會比較小,需要對它進行縮放。
- 3)生成的 CIImage 需要先轉成 CGImage 才可以儲存,因為 UIImagePNGRepresentation 接受的 UIImage 要有 CGImage,如果沒有或者位圖格式不對都會傳回 nil。
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
NSData * UIImagePNGRepresentation(UIImage *image);
NSString *urlString = @"http://weibo.com/u/2255024877";
NSData *data = [urlString dataUsingEncoding:NSISOLatin1StringEncoding]; // NSISOLatin1StringEncoding 編碼
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[filter setValue:data forKey:@"inputMessage"];
CIImage *outputImage = filter.outputImage;
NSLog(@"%@", NSStringFromCGSize(outputImage.extent.size));
CGAffineTransform transform = CGAffineTransformMakeScale(scale, scale); // scale 為放大倍數
CIImage *transformImage = [outputImage imageByApplyingTransform:transform];
// 儲存
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef imageRef = [context createCGImage:transformImage fromRect:transformImage.extent];
UIImage *qrCodeImage = [UIImage imageWithCGImage:imageRef];
[UIImagePNGRepresentation(qrCodeImage) writeToFile:path atomically:NO];
CGImageRelease(imageRef);
(生成的二維碼圖檔)
4、掃描框
- rectOfInterest
掃碼時 previewLayer 的掃描範圍是整個可視範圍的,但有些需求可能需要指定掃描的區域,雖然我覺得這樣很沒有必要,因為整個螢幕都可以掃又何必指定到某個框呢?但如果真的需要這麼做可以設定 metadataOutput 的 rectOfInterest。
需要注意的是:
- 1) rectOfInterest 的值比較特别,需要進行轉化。它的預設值是 (0.0, 0.0, 1.0, 1.0)。
metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:CGRectMake(80, 80, 160, 160)]; // 假設掃碼框的 Rect 是 (80, 80, 160, 160)
- 2) rectOfInterest 不可以直接在設定 metadataOutput 時接着設定,而需要在這個 AVCaptureInputPortFormatDescriptionDidChangeNotification 通知裡設定,否則 metadataOutputRectOfInterestForRect: 轉換方法會傳回 (0, 0, 0, 0)。
[[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification
object:nil
queue:[NSOperationQueue currentQueue]
usingBlock: ^(NSNotification *_Nonnull note) {
metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:CGRectMake(80, 80, 160, 160)];
}];
- 掃碼框的外觀
掃碼框四周一般都是半透明的黑色,而它裡面是沒有顔色的。
(掃碼框)
你可以在掃碼框四周各添加視圖,但更簡單的方法是自定義一個視圖,在 drawRect: 畫掃碼框的 path。代碼如下:
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat width = CGRectGetWidth([UIScreen mainScreen].bounds);
[[[UIColor blackColor] colorWithAlphaComponent:0.5] setFill];
CGMutablePathRef screenPath = CGPathCreateMutable();
CGPathAddRect(screenPath, NULL, self.bounds);
CGMutablePathRef scanPath = CGPathCreateMutable();
CGPathAddRect(scanPath, NULL, CGRectMake(80, 80, 160, 160);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddPath(path, NULL, screenPath);
CGPathAddPath(path, NULL, scanPath);
CGContextAddPath(ctx, path);
CGContextDrawPath(ctx, kCGPathEOFill); // kCGPathEOFill 方式
CGPathRelease(screenPath);
CGPathRelease(scanPath);
CGPathRelease(path);