11

Using Ray Wenderlich's QRCode reader from Chapter 22 of iOS7 Tutorials, I am successfully reading QRCodes for my current app. I am now extending it that upon successfully reading a QRCode, I want to store the stringValue of the AVMetadataMachineReadableCodeObject that was read, segue to a new view, and use that data on the new view, more or less exactly how most QRCode reader apps (like RedLaser, etc...) process barcodes and QRCodes.

However, I call [captureSession stopRunning] (so that it does not read any more QRCodes and trigger additional segues) and there is a 10+ second hang. I have tried to implement an async call per this SO question, however to no avail. I have also looked at these SO Questions and they seem not to be appropriate for this purpose.

Does anyone have an idea how to remove this hanging?

Here is the code:

#import "BMQRCodeReaderViewController.h"
#import "NSString+containsString.h"
#import "BMManualExperimentDataEntryViewController.h"
@import AVFoundation;

@interface BMQRCodeReaderViewController ()
    <AVCaptureMetadataOutputObjectsDelegate>
@end

@implementation BMQRCodeReaderViewController {
    AVCaptureSession *_captureSession;
    AVCaptureDevice *_videoDevice;
    AVCaptureDeviceInput *_videoInput;
    AVCaptureVideoPreviewLayer *_previewLayer;
    BOOL _running;
    AVCaptureMetadataOutput *_metadataOutput;
}

- (void)setupCaptureSession { // 1
    if (_captureSession) return;
    // 2
    _videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    if (!_videoDevice) {
        NSLog(@"No video camera on this device!"); return;
    }
    // 3
    _captureSession = [[AVCaptureSession alloc] init];
    // 4
    _videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:nil];
    // 5
    if ([_captureSession canAddInput:_videoInput]) { [_captureSession addInput:_videoInput];
    }
    // 6
    _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
    _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

    _metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    dispatch_queue_t metadataQueue = dispatch_queue_create("com.razeware.ColloQR.metadata", 0);

    [_metadataOutput setMetadataObjectsDelegate:self queue:metadataQueue];
    if ([_captureSession canAddOutput:_metadataOutput]) { [_captureSession addOutput:_metadataOutput];
    }
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection {

    // This fancy BOOL is just helping me fire the segue when the correct string is found
     __block NSNumber *didFind = [NSNumber numberWithBool:NO];

    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop) {

        AVMetadataMachineReadableCodeObject *readableObject = (AVMetadataMachineReadableCodeObject *)obj;

        NSLog(@"Metadata: %@", readableObject);

        // [ containsString is a category I extended for NSString, just FYI
        if ([readableObject.stringValue containsString:@"CorrectString"]) {

            didFind = [NSNumber numberWithBool:YES];
             NSLog(@"Found it");
             _testName = @"NameOfTest";
             *stop = YES;
             return;
         }
    }];

    if ([didFind boolValue]) {
        NSLog(@"Confirming we found it");
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self stopRunning];
        });
        _labelTestName.text = _testName;
        [self performSegueWithIdentifier:@"segueFromFoundQRCode" sender:self];
    }
    else {
        NSLog(@"Did not find it");
    }
}

- (void)startRunning {
    if (_running)
        return;

    [_captureSession startRunning];
    _metadataOutput.metadataObjectTypes = _metadataOutput.availableMetadataObjectTypes;
    _running = YES;
}
- (void)stopRunning {
    if (!_running) return;

    [_captureSession stopRunning];
    _running = NO;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    [self setupCaptureSession];
    [self setupNavBar];
    [self startRunning];
    _previewLayer.frame = _previewView.bounds;
    [_previewView.layer addSublayer:_previewLayer];
}
Community
  • 1
  • 1
Maximilian
  • 1,107
  • 11
  • 20

1 Answers1

18

I have successively solved the issue. The issue was that the delegate method call

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection

is running in the background. This was determined with a [NSThread isMainThread] call, which it fails.

The solution is to find the proper stringValue from the QRCode, stop your captureSession in the background, THEN call your segue on the main thread. The solution looks as such:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection {

    // This fancy BOOL is just helping me fire the segue when the correct string is found
     __block NSNumber *didFind = [NSNumber numberWithBool:NO];

    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop) {

        AVMetadataMachineReadableCodeObject *readableObject = (AVMetadataMachineReadableCodeObject *)obj;

        NSLog(@"Metadata: %@", readableObject);
        if ([NSThread isMainThread]) {
            NSLog(@"Yes Main Thread");
        }
        else {
            NSLog(@"Not main thread");
        }
        // [ containsString is a category I extended for NSString, just FYI
        if ([readableObject.stringValue containsString:@"Biomeme"]) {
            //NSLog(@"this is a test: %@", getTestName);
            didFind = [NSNumber numberWithBool:YES];
             NSLog(@"Found it");
             _testName = readableObject.stringValue;
             *stop = YES;
             return;
         }
    }];


    if ([didFind boolValue]) {
        NSLog(@"Confirming we found it");
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSDate *start = [NSDate date];

            [self stopRunning];
            NSLog(@"time took: %f", -[start timeIntervalSinceNow]);

            // *** Here is the key, make your segue on the main thread
            dispatch_async(dispatch_get_main_queue(), ^{
                [self performSegueWithIdentifier:@"segueFromFoundQRCode" sender:self];
                _labelTestName.text = _testName;
            });

        });



    }
    else {
        NSLog(@"Did not find it");
    }
}
Maximilian
  • 1,107
  • 11
  • 20
  • 6
    This is a subject that hasn't been touched on very much on StackOverflow it seems, this helps a ton, thank you. – Alioo Nov 20 '13 at 21:40
  • 1
    One thing, I'm now getting a crash with the message Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Only run on the main thread!' Do you think my prepareForsegue is causing this? – Alioo Nov 20 '13 at 21:53
  • Hi @Alioo, sorry for the lateness, I haven't revisited this code in a while (we aren't doing QR for a while), however I was running into the same issue -- it might need to be reconsidered about running this method in the background, or just waiting to fire the segue. With some tinkering, I think we could get this to perform with less issues. – Maximilian May 06 '14 at 15:41
  • 1
    Oh! @Max I actually solved this, I need to go back through my code soon any way so when I remember what I did I'll post it here, It took a whole lot of messing with! – Alioo May 06 '14 at 15:45
  • @Alioo I would love to see that! You should add an answer and I'll up vote/change the accepted answer! – Maximilian May 06 '14 at 15:58
  • 1
    Thank you so much this is awesome and the only answer I've found yet on the net. – Okhan Okbay Oct 07 '15 at 18:19
  • 1
    I am having exact same issue in 2021, in swift 5.0. What makes it worse is my AVCaptureSession is inside tabbed view, that needs to "stay on" even if qr code is found and other view is presented on top of it – Async- Nov 17 '21 at 13:24