Objective-C製バーコードライブラリZXingObjCをSwiftで使ってハマった話
はじめに
こんにちは! 最近は湿度が高く蒸し暑いのでげんなりしがちな加藤 潤です。
今日はObjective-Cのバーコードライブラリ ZXingObjCをSwiftで使ってみていくつかハマった所があったので共有したいと思います。
開発環境
- Xcode 7.3.1 (7D1014)
- Swift 2.2
- ZXingObjC 3.1.0
ハマリポイント その1
バーコード読み取りに使用するZXCaptureのデフォルトのsessionPresetがAVCaptureSessionPresetMedium
これはライブラリの仕様なのでSwift限定の話ではありませんがハマリポイントとして挙げておきます。
バーコードの読み取りにZXCapture
を使用していましたが、カメラがぼやけていてバーコードの認識率が悪いことに気づきました。
ZXCapture
の実装を見てみると、以下のようにAVCaptureSessionPresetMedium
がデフォルトのsessionPresetとして設定されていました。
@implementation ZXCapture - (ZXCapture *)init { if (self = [super init]) { _captureDeviceIndex = -1; _captureQueue = dispatch_queue_create("com.zxing.captureQueue", NULL); _focusMode = AVCaptureFocusModeContinuousAutoFocus; _hardStop = NO; _hints = [ZXDecodeHints hints]; _lastScannedImage = NULL; _onScreen = NO; _orderInSkip = 0; _orderOutSkip = 0; if (NSClassFromString(@"ZXMultiFormatReader")) { _reader = [NSClassFromString(@"ZXMultiFormatReader") performSelector:@selector(reader)]; } _rotation = 0.0f; _running = NO; _sessionPreset = AVCaptureSessionPresetMedium; _transform = CGAffineTransformIdentity; _scanRect = CGRectZero; } return self; }
この問題を解決するには、以下のようにsessionPreset
をAVCaptureSessionPreset1920x1080
などの認識率が良い解像度に変更する必要があります。
let capture = ZXCapture() capture.sessionPreset = AVCaptureSessionPreset1920x1080 capture.camera = capture.back()
ハマリポイント その2
バーコード生成時に指定する幅と高さはピクセルで指定する
これもライブラリの仕様ですが、ハマリポイントとして挙げておきます。
バーコードの生成にはZXWriter
プロトコルに定義されている下記メソッドを使用していました。
/** * Encode a barcode using the default settings. * * @param contents The contents to encode in the barcode * @param format The barcode format to generate * @param width The preferred width in pixels * @param height The preferred height in pixels */ - (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height error:(NSError **)error;
引数でバーコードの幅と高さを指定するのですが、ポイントではなくピクセルで指定する必要があります。
ちゃんとメソッドのコメントにもwidth The preferred width in pixels
やheight The preferred height in pixels
と書いてありますが、
「バーコードを表示するビューのframeの幅と高さを指定すればいいんでしょ?」と勝手に思い込んでるとポイントで指定してしまい、「なんかバーコードが小さい...」みたいなことが起こります。
参考までにピクセル指定のサンプルを載せておきます。
barcodeImage
はバーコード表示領域のUIImageView
です。
ピクセルなのでUIScreen.mainScreen().scale
でスケールを取得して幅と高さそれぞれに掛ける必要があります。
guard let writer = ZXMultiFormatWriter.writer() as? ZXWriter else { return } let scale = UIScreen.mainScreen().scale writer.encode( "9999999999999", format: kBarcodeFormatEan13, width: Int32(barcodeImage.bounds.width * scale), height: Int32(barcodeImage.bounds.height * scale))
ハマリポイント その3
バーコード生成時にNSExceptionが発生する可能性がある
先ほどのencode
メソッドですが、ZXMultiFormatWriter
の実装を見ると、実際はパラメーターで指定したバーコード形式に応じてwriter
オブジェクトを生成し、そのオブジェクトのencode
メソッドを呼んでいることがわかります。
@implementation ZXMultiFormatWriter + (id)writer { return [[ZXMultiFormatWriter alloc] init]; } - (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height error:(NSError **)error { return [self encode:contents format:format width:width height:height hints:nil error:error]; } - (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height hints:(ZXEncodeHints *)hints error:(NSError **)error { id<ZXWriter> writer; switch (format) { case kBarcodeFormatEan8: writer = [[ZXEAN8Writer alloc] init]; break; case kBarcodeFormatEan13: writer = [[ZXEAN13Writer alloc] init]; break; case kBarcodeFormatUPCA: writer = [[ZXUPCAWriter alloc] init]; break; case kBarcodeFormatQRCode: writer = [[ZXQRCodeWriter alloc] init]; break; case kBarcodeFormatCode39: writer = [[ZXCode39Writer alloc] init]; break; case kBarcodeFormatCode128: writer = [[ZXCode128Writer alloc] init]; break; case kBarcodeFormatITF: writer = [[ZXITFWriter alloc] init]; break; case kBarcodeFormatPDF417: writer = [[ZXPDF417Writer alloc] init]; break; case kBarcodeFormatCodabar: writer = [[ZXCodaBarWriter alloc] init]; break; case kBarcodeFormatDataMatrix: writer = [[ZXDataMatrixWriter alloc] init]; break; case kBarcodeFormatAztec: writer = [[ZXAztecWriter alloc] init]; break; default: if (error) *error = [NSError errorWithDomain:ZXErrorDomain code:ZXWriterError userInfo:@{NSLocalizedDescriptionKey: @"No encoder available for format"}]; return nil; } return [writer encode:contents format:format width:width height:height hints:hints error:error]; } @end
例えば、バーコード形式がkBarcodeFormatEan13
ならZXEAN13Writer
のオジェクトが生成されてそのencode
が呼ばれる事になります。
ZXEAN13Writer
の実装は下記のようになっています。
@implementation ZXEAN13Writer - (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height hints:(ZXEncodeHints *)hints error:(NSError **)error { if (format != kBarcodeFormatEan13) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Can only encode EAN_13, but got %d", format] userInfo:nil]; } return [super encode:contents format:format width:width height:height hints:hints error:error]; } - (ZXBoolArray *)encode:(NSString *)contents { if ([contents length] != 13) { [NSException raise:NSInvalidArgumentException format:@"Requested contents should be 13 digits long, but got %d", (int)[contents length]]; } if (![ZXUPCEANReader checkStandardUPCEANChecksum:contents]) { [NSException raise:NSInvalidArgumentException format:@"Contents do not pass checksum"]; } int firstDigit = [[contents substringToIndex:1] intValue]; int parities = ZX_EAN13_FIRST_DIGIT_ENCODINGS[firstDigit]; ZXBoolArray *result = [[ZXBoolArray alloc] initWithLength:ZX_EAN13_CODE_WIDTH]; int pos = 0; pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_START_END_PATTERN patternLen:ZX_UPC_EAN_START_END_PATTERN_LEN startColor:YES]; for (int i = 1; i <= 6; i++) { int digit = [[contents substringWithRange:NSMakeRange(i, 1)] intValue]; if ((parities >> (6 - i) & 1) == 1) { digit += 10; } pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_L_AND_G_PATTERNS[digit] patternLen:ZX_UPC_EAN_L_PATTERNS_SUB_LEN startColor:FALSE]; } pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_MIDDLE_PATTERN patternLen:ZX_UPC_EAN_MIDDLE_PATTERN_LEN startColor:FALSE]; for (int i = 7; i <= 12; i++) { int digit = [[contents substringWithRange:NSMakeRange(i, 1)] intValue]; pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_L_PATTERNS[digit] patternLen:ZX_UPC_EAN_L_PATTERNS_SUB_LEN startColor:YES]; } [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_START_END_PATTERN patternLen:ZX_UPC_EAN_START_END_PATTERN_LEN startColor:YES]; return result; } @end
処理を辿ってみると、- (ZXBoolArray *)encode:(NSString *)contents
が呼ばれることがわかったのですが、ここではバーコード形式がkBarcodeFormatEan13
なのに桁数が13ではない場合やチェックサムでエラーの場合にNSException
を発生させています。
APIなど外部から取得したバーコード文字列やバーコード形式をそのままこのライブラリに渡しているとNSException
が発生する可能性があるということになります。
SwiftでNSExceptionに対処する
Apple Developer Forumsでも議論されていますが、SwiftでNSException
はハンドリングできません。
対処法としては以下のようにObjective-Cのラッパーを作り、NSException
をキャッチしてNSError
に変換してやることでSwiftでもハンドリング可能となります。
- ObjC.h
#import <Foundation/Foundation.h> @interface ObjC : NSObject /** * 例外をキャッチする * * @param tryBlock 例外が発生する可能性のある処理 * @param error エラー * * @return 処理が成功したかどうか */ + (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error; @end
- ObjC.m
#import "ObjC.h" @implementation ObjC + (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error { @try { tryBlock(); return YES; } @catch (NSException *exception) { *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo]; } } @end
Swiftでのハンドリング例は下記のようになります。
NSException
が発生してもNSError
に変換されるのでcatch
ブロックでハンドリングできるようになります。
do { let scale = UIScreen.mainScreen().scale try ObjC.catchException { if let result = try? writer.encode( "123456789012345", format: kBarcodeFormatEan13, width: Int32(self.barcodeImage.bounds.width * scale), height: Int32(self.barcodeImage.bounds.height * scale)) { // 正常処理 } else { // エラー処理 } } } catch { // エラー処理(NSExceptionが発生してもNSErrorに変換されるのでここでハンドリングできる!) }
まとめ
今回はZXingObjCをSwiftで使ってみてハマった所をまとめてみました。 Swiftでアプリを作る場合でも、要件によってはObjective-CのOSSを使う場合も多々あるかと思います。 導入する際には中の実装がどうなっているか、Swiftから使った場合に問題ないかを十分に検討することをオススメします。(自戒)