├── CDZQRScanningViewController.podspec ├── LICENSE ├── CDZQRScanningViewController.h ├── README.md └── CDZQRScanningViewController.m /CDZQRScanningViewController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'CDZQRScanningViewController' 3 | s.version = '1.0.7' 4 | s.summary = 'Easy QR code scanning on iOS 7.' 5 | s.homepage = 'https://github.com/cdzombak/CDZQRScanningViewController' 6 | s.license = 'MIT' 7 | s.author = { 'Chris Dzombak' => 'chris@chrisdzombak.net' } 8 | 9 | s.source = { :git => 'https://github.com/cdzombak/CDZQRScanningViewController.git', :tag => 'v1.0.7' } 10 | s.platform = :ios, '7.0' 11 | 12 | s.source_files = '*.{h,m}' 13 | s.public_header_files = 'CDZQRScanningViewController.h' 14 | s.frameworks = ['AVFoundation', 'UIKit'] 15 | s.requires_arc = true 16 | end 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Chris Dzombak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CDZQRScanningViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CDZQRScanningViewController.h 3 | // 4 | // Created by Chris Dzombak on 10/27/13. 5 | // Copyright (c) 2013 Chris Dzombak. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | typedef void (^CDZQRScanResultBlock)(NSString *scanResult); 12 | typedef void (^CDZQRScanErrorBlock)(NSError *error); 13 | typedef void (^CDZQRScanCancelBlock)(); 14 | 15 | extern NSString * const CDZQRScanningErrorDomain; 16 | 17 | typedef NS_ENUM(NSInteger, CDZQRScanningViewControllerErrorCode) { 18 | CDZQRScanningViewControllerErrorUnavailableMetadataObjectType = 1, 19 | }; 20 | 21 | /** 22 | * Easy barcode scanning view controller for iOS 7. 23 | */ 24 | 25 | @interface CDZQRScanningViewController : UIViewController 26 | 27 | /** 28 | * Returns a scanning view controller configured to accept the given metadata object types. 29 | * 30 | * @param metadataObjectTypes An array of `AVMetadataMachineReadableCodeObject`s 31 | * 32 | * @return Scanning view controller configured to accept the given metadata object types 33 | */ 34 | - (instancetype)initWithMetadataObjectTypes:(NSArray *)metadataObjectTypes; 35 | 36 | /** 37 | * Returns a scanning view controller configured to accept QR codes 38 | * 39 | * @note This is equivalent to calling `initWithMetadataObjectTypes:@[ AVMetadataObjectTypeQRCode ]` 40 | * 41 | * @return Scanning view controller configured to accept QR codes 42 | */ 43 | - (instancetype)init; 44 | 45 | // Your blocks will be called on the main queue. 46 | @property (nonatomic, copy) CDZQRScanResultBlock resultBlock; 47 | @property (nonatomic, copy) CDZQRScanErrorBlock errorBlock; 48 | @property (nonatomic, copy) CDZQRScanCancelBlock cancelBlock; 49 | 50 | /** 51 | * An array of `AVMetadataMachineReadableCodeObject`s 52 | */ 53 | @property (nonatomic, strong, readonly) NSArray *metadataObjectTypes; 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | # CDZQRScanningViewController 4 | 5 | Easy QR code scanning on iOS 7. 6 | 7 | ## Installation 8 | 9 | Add the dependency to your `Podfile`: 10 | 11 | ```ruby 12 | platform :ios 13 | pod 'CDZQRScanningViewController' 14 | ... 15 | ``` 16 | 17 | Run `pod install` to install the dependencies. 18 | 19 | ## Usage 20 | 21 | `#import "CDZQRScanningViewController.h"`, then: 22 | 23 | ```objc 24 | // assume this text field is defined elsewhere: 25 | UITextField *field = self.someTextField; 26 | 27 | [field becomeFirstResponder]; 28 | 29 | // create the scanning view controller and a navigation controller in which to present it: 30 | CDZQRScanningViewController *scanningVC = [CDZQRScanningViewController new]; 31 | UINavigationController *scanningNavVC = [[UINavigationController alloc] initWithRootViewController:scanningVC]; 32 | 33 | // configure the scanning view controller: 34 | scanningVC.resultBlock = ^(NSString *result) { 35 | field.text = result; 36 | [scanningNavVC dismissViewControllerAnimated:YES completion:nil]; 37 | }; 38 | scanningVC.cancelBlock = ^() { 39 | [scanningNavVC dismissViewControllerAnimated:YES completion:nil]; 40 | }; 41 | scanningVC.errorBlock = ^(NSError *error) { 42 | // todo: show a UIAlertView orNSLog the error 43 | [scanningNavVC dismissViewControllerAnimated:YES completion:nil]; 44 | }; 45 | 46 | // present the view controller full-screen on iPhone; in a form sheet on iPad: 47 | scanningNavVC.modalPresentationStyle = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? UIModalPresentationFullScreen : UIModalPresentationFormSheet; 48 | [self presentViewController:scanningNavVC animated:YES completion:nil]; 49 | ``` 50 | 51 | If you don't want to present the scanner in its own modal `UIViewController`, [use view controller containment to embed it in your own view controller](https://github.com/cdzombak/CDZQRScannerThatDoesntSuck/blob/master/QRScanner/CDZRootViewController.m#L55), instead of presenting it inside a nav controller as in this example. 52 | 53 | ## Other features 54 | 55 | Tap and hold on the live video view for 0.25 seconds to activate the device's flashlight for use in low-light. 56 | 57 | ## Why use this instead of ZBarSDK? 58 | 59 | * smaller surface area 60 | * easier to use 61 | * less configuration 62 | * less impact on your code 63 | * much smaller, thanks to new APIs in iOS 7 64 | * MIT license instead of GPL 65 | 66 | ## Requirements 67 | 68 | `CDZQRScanningViewController` requires iOS 7+. 69 | 70 | ## License 71 | 72 | MIT. See `LICENSE` included in this repo. 73 | 74 | ## Developer 75 | 76 | Chris Dzombak 77 | 78 | * [chris.dzombak.name](http://chris.dzombak.name/) 79 | * chris@chrisdzombak.net 80 | * [t@cdzombak](https://twitter.com/cdzombak) 81 | * [a@dzombak](https://alpha.app.net/dzombak) 82 | -------------------------------------------------------------------------------- /CDZQRScanningViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // CDZQRScanningViewController.m 3 | // 4 | // Created by Chris Dzombak on 10/27/13. 5 | // Copyright (c) 2013 Chris Dzombak. All rights reserved. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | #import "CDZQRScanningViewController.h" 12 | 13 | #ifndef CDZWeakSelf 14 | #define CDZWeakSelf __weak __typeof__((__typeof__(self))self) 15 | #endif 16 | 17 | #ifndef CDZStrongSelf 18 | #define CDZStrongSelf __typeof__(self) 19 | #endif 20 | 21 | static AVCaptureVideoOrientation CDZVideoOrientationFromInterfaceOrientation(UIInterfaceOrientation interfaceOrientation) 22 | { 23 | switch (interfaceOrientation) { 24 | case UIInterfaceOrientationPortrait: 25 | return AVCaptureVideoOrientationPortrait; 26 | break; 27 | case UIInterfaceOrientationLandscapeLeft: 28 | return AVCaptureVideoOrientationLandscapeLeft; 29 | break; 30 | case UIInterfaceOrientationLandscapeRight: 31 | return AVCaptureVideoOrientationLandscapeRight; 32 | break; 33 | case UIInterfaceOrientationPortraitUpsideDown: 34 | return AVCaptureVideoOrientationPortraitUpsideDown; 35 | break; 36 | } 37 | } 38 | 39 | static const float CDZQRScanningTorchLevel = 0.25; 40 | static const NSTimeInterval CDZQRScanningTorchActivationDelay = 0.25; 41 | 42 | NSString * const CDZQRScanningErrorDomain = @"com.cdzombak.qrscanningviewcontroller"; 43 | 44 | @interface CDZQRScanningViewController () 45 | 46 | @property (nonatomic, strong) AVCaptureSession *avSession; 47 | @property (nonatomic, strong) AVCaptureDevice *captureDevice; 48 | @property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer; 49 | 50 | @property (nonatomic, copy) NSString *lastCapturedString; 51 | 52 | @property (nonatomic, strong, readwrite) NSArray *metadataObjectTypes; 53 | 54 | @end 55 | 56 | @implementation CDZQRScanningViewController 57 | 58 | - (instancetype)initWithMetadataObjectTypes:(NSArray *)metadataObjectTypes { 59 | self = [super init]; 60 | if (!self) return nil; 61 | self.metadataObjectTypes = metadataObjectTypes; 62 | self.title = NSLocalizedString(@"Scan QR Code", nil); 63 | return self; 64 | } 65 | 66 | - (instancetype)init { 67 | return [self initWithMetadataObjectTypes:@[ AVMetadataObjectTypeQRCode ]]; 68 | } 69 | 70 | - (void)viewDidLoad { 71 | [super viewDidLoad]; 72 | 73 | self.view.backgroundColor = [UIColor blackColor]; 74 | 75 | UILongPressGestureRecognizer *torchGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleTorchRecognizerTap:)]; 76 | torchGestureRecognizer.minimumPressDuration = CDZQRScanningTorchActivationDelay; 77 | [self.view addGestureRecognizer:torchGestureRecognizer]; 78 | } 79 | 80 | - (void)viewWillAppear:(BOOL)animated { 81 | [super viewWillAppear:animated]; 82 | 83 | if (self.cancelBlock) { 84 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelItemSelected:)]; 85 | } else { 86 | self.navigationItem.leftBarButtonItem = nil; 87 | } 88 | 89 | self.lastCapturedString = nil; 90 | 91 | if (self.cancelBlock && !self.errorBlock) { 92 | CDZWeakSelf wSelf = self; 93 | self.errorBlock = ^(NSError *error) { 94 | CDZStrongSelf sSelf = wSelf; 95 | if (sSelf.cancelBlock) { 96 | [self.avSession stopRunning]; 97 | sSelf.cancelBlock(); 98 | } 99 | }; 100 | } 101 | 102 | self.avSession = [[AVCaptureSession alloc] init]; 103 | 104 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ 105 | self.captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; 106 | if ([self.captureDevice isLowLightBoostSupported] && [self.captureDevice lockForConfiguration:nil]) { 107 | self.captureDevice.automaticallyEnablesLowLightBoostWhenAvailable = YES; 108 | [self.captureDevice unlockForConfiguration]; 109 | } 110 | 111 | [self.avSession beginConfiguration]; 112 | 113 | NSError *error = nil; 114 | AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:self.captureDevice error:&error]; 115 | if (input) { 116 | [self.avSession addInput:input]; 117 | } else { 118 | NSLog(@"QRScanningViewController: Error getting input device: %@", error); 119 | [self.avSession commitConfiguration]; 120 | if (self.errorBlock) { 121 | dispatch_async(dispatch_get_main_queue(), ^{ 122 | [self.avSession stopRunning]; 123 | self.errorBlock(error); 124 | }); 125 | } 126 | return; 127 | } 128 | 129 | AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init]; 130 | [self.avSession addOutput:output]; 131 | for (NSString *type in self.metadataObjectTypes) { 132 | if (![output.availableMetadataObjectTypes containsObject:type]) { 133 | if (self.errorBlock) { 134 | dispatch_async(dispatch_get_main_queue(), ^{ 135 | [self.avSession stopRunning]; 136 | self.errorBlock([NSError errorWithDomain:CDZQRScanningErrorDomain code:CDZQRScanningViewControllerErrorUnavailableMetadataObjectType userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Unable to scan object of type %@", type]}]); 137 | }); 138 | } 139 | return; 140 | } 141 | } 142 | 143 | output.metadataObjectTypes = self.metadataObjectTypes; 144 | [output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; 145 | 146 | [self.avSession commitConfiguration]; 147 | 148 | dispatch_async(dispatch_get_main_queue(), ^{ 149 | if (self.previewLayer.connection.isVideoOrientationSupported) { 150 | self.previewLayer.connection.videoOrientation = CDZVideoOrientationFromInterfaceOrientation(self.interfaceOrientation); 151 | } 152 | 153 | [self.avSession startRunning]; 154 | }); 155 | }); 156 | 157 | self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.avSession]; 158 | self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; 159 | self.previewLayer.frame = self.view.bounds; 160 | if (self.previewLayer.connection.isVideoOrientationSupported) { 161 | self.previewLayer.connection.videoOrientation = CDZVideoOrientationFromInterfaceOrientation(self.interfaceOrientation); 162 | } 163 | [self.view.layer addSublayer:self.previewLayer]; 164 | } 165 | 166 | - (void)viewDidDisappear:(BOOL)animated { 167 | [super viewDidDisappear:animated]; 168 | 169 | [self.previewLayer removeFromSuperlayer]; 170 | self.previewLayer = nil; 171 | self.avSession = nil; 172 | self.captureDevice = nil; 173 | } 174 | 175 | - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { 176 | [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; 177 | 178 | if (self.previewLayer.connection.isVideoOrientationSupported) { 179 | self.previewLayer.connection.videoOrientation = CDZVideoOrientationFromInterfaceOrientation(toInterfaceOrientation); 180 | } 181 | } 182 | 183 | - (void)viewDidLayoutSubviews { 184 | [super viewDidLayoutSubviews]; 185 | 186 | CGRect layerRect = self.view.bounds; 187 | self.previewLayer.bounds = layerRect; 188 | self.previewLayer.position = CGPointMake(CGRectGetMidX(layerRect), CGRectGetMidY(layerRect)); 189 | } 190 | 191 | #pragma mark - UI Actions 192 | 193 | - (void)cancelItemSelected:(id)sender { 194 | [self.avSession stopRunning]; 195 | if (self.cancelBlock) self.cancelBlock(); 196 | } 197 | 198 | - (void)handleTorchRecognizerTap:(UIGestureRecognizer *)sender { 199 | switch(sender.state) { 200 | case UIGestureRecognizerStateBegan: 201 | [self turnTorchOn]; 202 | break; 203 | case UIGestureRecognizerStateChanged: 204 | case UIGestureRecognizerStatePossible: 205 | // no-op 206 | break; 207 | case UIGestureRecognizerStateRecognized: // also UIGestureRecognizerStateEnded 208 | case UIGestureRecognizerStateFailed: 209 | case UIGestureRecognizerStateCancelled: 210 | [self turnTorchOff]; 211 | break; 212 | } 213 | } 214 | 215 | #pragma mark - Torch 216 | 217 | - (void)turnTorchOn { 218 | if (self.captureDevice.hasTorch && self.captureDevice.torchAvailable && [self.captureDevice isTorchModeSupported:AVCaptureTorchModeOn] && [self.captureDevice lockForConfiguration:nil]) { 219 | [self.captureDevice setTorchModeOnWithLevel:CDZQRScanningTorchLevel error:nil]; 220 | [self.captureDevice unlockForConfiguration]; 221 | } 222 | } 223 | 224 | - (void)turnTorchOff { 225 | if (self.captureDevice.hasTorch && [self.captureDevice isTorchModeSupported:AVCaptureTorchModeOff] && [self.captureDevice lockForConfiguration:nil]) { 226 | self.captureDevice.torchMode = AVCaptureTorchModeOff; 227 | [self.captureDevice unlockForConfiguration]; 228 | } 229 | } 230 | 231 | #pragma mark - AVCaptureMetadataOutputObjectsDelegate 232 | 233 | - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { 234 | NSString *result; 235 | 236 | for (AVMetadataObject *metadata in metadataObjects) { 237 | if ([self.metadataObjectTypes containsObject:metadata.type]) { 238 | result = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; 239 | break; 240 | } 241 | } 242 | 243 | if (result && ![self.lastCapturedString isEqualToString:result]) { 244 | self.lastCapturedString = result; 245 | [self.avSession stopRunning]; 246 | if (self.resultBlock) self.resultBlock(result); 247 | } 248 | } 249 | 250 | @end 251 | --------------------------------------------------------------------------------