Delegate method being invoked multiple times, even

2019-04-29 23:03发布

问题:

I'm creating an app that uses the new barcode scanner in iOS 7 but I'm having a some problems with the delegate method. The scanner correctly identifies the barcodes and invokes the delegate method, but it does it too fast so the invocation happens many times in a row resulting in a segue being performed multiple times. Delegate method below.

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
    connection.enabled = NO;
    self.conn = connection;
    for (AVMetadataObject *metadata in metadataObjects) {
        if ([metadata.type isEqualToString:AVMetadataObjectTypeEAN8Code] || [metadata.type isEqualToString:AVMetadataObjectTypeEAN13Code]) {
            self.strValue = [(AVMetadataMachineReadableCodeObject *)metadata stringValue];
            NSLog(@"%@", [(AVMetadataMachineReadableCodeObject *)metadata corners]);
        }
    }
    [self performSegueWithIdentifier:@"newSegue" sender:self];
}

The issue is that if I do not set connection.enabled = NO in the opening line, the delegate is invoked multiple times causing a corrupt view hierarchy (and then a crash). The other issue is that when I do disable connection and then re-enable the connection using self.conn = YES in viewWillAppear, the delegate will be invoked repeatedly from prior scans when returning to the view. This then causes another corruption in the view hierarchy.

So to sum it up: Either the delegate method is being invoked multiple times in quick succession or the delegate is being invoked with (old) scans when returning to the view. Any help would be appreciated.

Edit: I've partially managed to get around the problem with some fidgeting with the delegate, but I still have a problem with the delegate method being invoked multiple times. If you go back from the next viewcontroller in less than five seconds, the delegate method will be invoked again.

回答1:

I think you have started captureSession using captureSession?.startRunning() method but didn't stop it once you got output from QRCode in delegate...

Just Use this [captureSession stopRunning]; // In Objective-C

below is what I have done for same issue in swift

// MARK: - AVCapture delegate to find metadata if detected

func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {

    // Check if the metadataObjects array is not nil and it contains at least one object.
    if metadataObjects == nil || metadataObjects.count == 0 {
        qrCodeFrameView?.frame = CGRectZero
        return
    }

    // Get the metadata object.
    let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject

    if metadataObj.type == AVMetadataObjectTypeQRCode {
        // If the found metadata is equal to the QR code metadata then update the status label's text and set the bounds
        let barCodeObject = videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj as AVMetadataMachineReadableCodeObject) as! AVMetadataMachineReadableCodeObject
        qrCodeFrameView?.frame = barCodeObject.bounds;

        if metadataObj.stringValue != nil {
            captureSession?.stopRunning()    // Stop captureSession here... :)
            self.performSegueWithIdentifier("yourNextViewController", sender: self)
        }
    }
}


回答2:

I hope, it will save other's time. Here is article, how to use bar code scanner http://www.appcoda.com/qr-code-ios-programming-tutorial/

From the Apple documentation: "this method may be called frequently, your implementation should be efficient to prevent capture performance problems, including dropped metadata objects."

Now, to handle multiple invoke do the following :

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    id capturedData;
        if ([metadataObjects count] > 0) {
            // handle your captured data here

            [self performSelectorOnMainThread:@selector(stopReading:) withObject:capturedData waitUntilDone:NO];

        }

}

the stopReading: method looks (assuming your _session is AVCaptureSession object and _prevLayer is AVCaptureVideoPreviewLayer you used earlier) :

-(void)stopReading:(id) data{
    NSLog(@"stop reading");
    [_session stopRunning];
    _session = nil;
    [_prevLayer removeFromSuperlayer];
// do what you want with captured data
    [self.delegate didScanBarCodeWithContext:data];
}


回答3:

Doro's answer is good ,but has bug: function 'stopReading' may not be called in time before the delegate method is invoked second times

So i do some optimization.

based on Doro's answer, i add a Static variable to tell them .

static BOOL hasOutput;
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    if (!hasOutput
        && metadataObjects.count > 0 ) {
        hasOutput=YES;

        [self performSelectorOnMainThread:@selector(stopReading) withObject:nil waitUntilDone:NO];

        for (AVMetadataObject *current in metadataObjects) {
            if ([current isKindOfClass:[AVMetadataMachineReadableCodeObject class]]
                && [_metadataObjectTypes containsObject:current.type]) {
                NSString *scannedResult = [(AVMetadataMachineReadableCodeObject *) current stringValue];

                if (_completionBlock) {
                    _completionBlock(scannedResult);
                }

                break;
            }
        }
    }

}
-(void)stopReading{
    NSLog(@"stop reading");
    [_session stopRunning];
    _session = nil;
    hasOutput=NO;
}


回答4:

The workaround is to add to the delegate class a Boolean property that is switched to false after the first identification of a capture barcode event.

This solution is implemented as Calin Chitu offered.

You'll also need to initialize the property shouldSendReadBarcodeToDelegate once with YES.

@property (nonatomic, assign) BOOL shouldSendReadBarcodeToDelegate;

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection 
  {
     if (!self.shouldSendReadBarcodeToDelegate)
     {
        //this means we have already captured at least one event, then we don't want   to call the delegate again
        return;
     }
     else
     {
        self.shouldSendReadBarcodeToDelegate = NO;
        //Your code for calling  the delegate should be here
     }

  }


回答5:

The boolean property doesn't do the trick for me. I've ended using an operation queue to avoid multiple reads:

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputMetadataObjects:(NSArray *)metadataObjects
       fromConnection:(AVCaptureConnection *)connection
{
    if ([self.queue operationCount] > 0) return;

    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        if ([metadataObjects count] > 0) {
           // Your code here: Don't forget that you are in background now, perform
           // all view related stuff on main thread
        }
    }];
    [self.queue addOperations:@[operation] waitUntilFinished:NO];
}

Initializing the queue in the viewcontroller constructor:

self.queue = [NSOperationQueue new];