iOS 商品扫描枪开发笔记

iOS 课程期末作业项目,我选择做了个商品码扫描枪App。对我这个没有 iOS 开发经验的人来说这的确有点难度。熬了三天几乎从零开始(我一直都是做 JavaEE 开发,对编程语言已经有了不少的基本认识,对编程基础也已经掌握了)写出来了这个 App 。

我对 iOS 开发的基础经验来源于 Android 开发,毕竟基于 Java ,很多概念学习起来比较轻松。诸如视图、控制器等等。移动端 App 开发很注重 MVC 模式,我开发网站也全是基于这个模式。

需要涉及的东西

机械可阅读码

条形码和二维码都属于同一种东西,叫 Machine Readable Codes,翻译过来就是机器可阅读码。基本原理就是把文字编码成机器容易识别的编码,然后打印出来,方便通过摄像头或者其他光学识别器来识别。其本身就是一个信息的容器,跟 BASE64 和其他编码技术只有载体上的区别。

设备输入输出

接下来解释一下 iOS 里关于视频捕获的类

  • AVCaptureSession
  • AVCaptureDevice
  • AVCaptureDeviceInput
  • 输出

AVCaptureSession 是一个用于管理捕获输入输出会话的类。新建一个会话,就像跟系统说“我要准备抓点东西了”。至于要抓什么东西则是需要我们接下来说明的。抓点东西,那么这个行为肯定有两个要点:从哪里输入,以及输出到哪去。

从哪里输入,需要有一个来源, AVCaptureDevice 便是干这个用的。 AVCaptureDevices 是指代一个系统输入设备的类,这些设备可以是话筒、摄像头等等,通过其静态方法 devicesWithMediaType 能够利用设备的类型来获取一个设备列表。在我的 App 里有一个方法专门获取后置摄像头设备

//Get Back-end camera
- (AVCaptureDevice *) backendCamera{
    
    //通过一个类型来获取设备列表
    NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; 
    
    //遍历这个数组获取符合条件的设备
    for (AVCaptureDevice *camera in cameras) {
    
        if([camera position] == AVCaptureDevicePositionBack){
            //如果匹配则返回
            NSLog(@"Camera found:%ld", (long)camera.position);
            return camera;
        }
    }
    return NULL;
}

有了目标设备就可以拿着这个设备创建一个输入来源了

//这里省略 ScanningHandler 的初始化方法
//获取后置摄像头
AVCaptureDevice *theCamera = [self backendCamera];

//申请一个输入来源
_cameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:theCamera error:nil];

输入有了,接下来就需要有个输出了。输出到哪也是由一些类来指定。在我这个 App 里,输出有两个

  • 视频预览框
  • 机械码识别器

视频预览的输出是由 AVCaptureVideoPreviewLayer 来管理。输出有两点:输入来源和输出目的地,在这个类里目的地是一个 View

//创建一个输出层,从捕获会话里获取输入
_cameraPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_session];

UIView *view = _viewApplicated;
CALayer *viewLayer = [view layer];
    
[viewLayer setMasksToBounds:YES];
    
CGRect bounds = [view bounds];

//设定预览层的大小为指定 View 的大小
[_cameraPreviewLayer setFrame:bounds];
//设定预览层的大小显示方式,这个值是让预览画面充满视图,显示的区域的中心是摄像头画面的中心
[_cameraPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    
//把这个层塞到目的视图里面。
[viewLayer insertSublayer:_cameraPreviewLayer below:[[viewLayer sublayers] objectAtIndex:0]];

搞定了预览之后还有给机械识别码的输出

 //创建一个输出,从会话获取输入
_captureMetadataOutput = [[AVCaptureMetadataOutput alloc] init];

//这个输出有一个回调,当识别到指定的机械码的时候会把数据返回并调用这个函数
//queue参数是指这个读取器要在哪个线程上执行,这里直接使用主线程。
[_captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

//设置捕获区域,如果不设置的话默认识别全部
//虽然上面已经限定了 Camera 的预览区域,但是返回的数据还是摄像头捕获到的所有图像。
//这个范围参数跟普通的 View 的范围参数不一样,它的值是一个占比。

[_captureMetadataOutput setRectOfInterest:CGRectMake(0.375, 0, 0.25, 1)];

//把这个输出放到会话里好让输出有地方出来
[_session addOutput:_captureMetadataOutput];
[self captureBarCode];

结构

现在来说一下这个 App 的结构,从可视化开始讲起就是 View 。这个 App 我设计只有一个 View ,里面包含了一个视频的预览窗口(用于让用户能够比较轻松地瞄准目标机器码)、一个列表视图(TableView)以及一个包含了切换器和“编辑列表”按钮的工具栏。

接下来是管理这个 View 的 Controller。里面包含了一些界面的逻辑代码。为了方便,我直接在这里实现了一个 TableView 的控制器,也就是主页面和列表视图共用一个视图控制器。

为了让 ViewController 更加简洁,我写了一个 ScanningHandler 来负责管理摄像头视频数据的获取和分析。对于主视图来说,它需要关心的只是让摄像头的预览数据能够显示在指定区域里,以及如何获得扫描得来的数据。因此,把关于扫描的设置以及管理扫描设置的相关方法整合到一个类里面会方便很多。

按照 MVC 的设计模式,我把数据获取部分按照 Java 的方式,做了一个 CommodityRepository(其实就是DAO) ,负责接收一个商品的编码并返回一个 Commodity(商品) 对象。

因为习惯问题,我这个项目也是做了国际化的,关于国际化如何实现我不累赘,在文章后面的链接里有。

遇到的一些问题

在获取到一个 MetadataOutput 之后,它的扫描范围是整个摄像头的范围,需要手动设置这个范围。如果是二维码的话,这个不影响效率,但是是条形码的话,这样做会大大降低扫描的速度。最好的做法是设置一个。但这个范围跟 CameraPreviewLayout 不一样,它的范围尺寸是一个比值,例如要扫描摄像头获取的图像的上半部分,则设置宽度为 1 ,高度为 0.5 。如何让其定位到 CameraPreviewLayout 还是一个难点。

CameraPreviewLayout 的 AVLayerVideoGravityResizeAspectFill 选项,会让摄像头画面居中,然后宽度拉伸到跟 View 一样,所以显示出来的位置是摄像头画面的中间部分。

关闭和打开捕获的时候我做了个小动画,这个动画我是创建了一个毛玻璃效果视图,然后通过动画调整器透明度。

- (IBAction)scannerControlSwitch:(UISwitch *)sender {
    if ([sender isOn]) {
        
        _visualEffectView.alpha = 1;
        [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            _visualEffectView.alpha = 0;
        } completion:^(BOOL finished) {
            [_scanningHandler startCapturing];
            [_visualEffectView setHidden:YES];
        }];
        
    }else{
        _visualEffectView.alpha = 0;
        [_visualEffectView setHidden:NO];
        [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            _visualEffectView.alpha = 1;
        } completion:^(BOOL finished) {
            [_scanningHandler stopCapturing];
        }];
    }
}

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容