During the initialization of an app, I naturally have some critical things that have to be done to get the app running. For example, in this case I need to get the AVCaptureDevice pointer for the back camera.
So if it fails, (which it never should, but you never know), I want to display a UIAlertView with only one option, "Try again". When the user selects this, the app will try to get the AVCaptureDevice again.
The problem is that I need to wait for the user to press "Try again" before I continue, but UIAlertView is not modal.
If there was only one piece of code like this, I could potentially handle it with the UIAlertViewDelegate callback. But since there will be multiple critical pieces of initialization like this, I don't see how I could use the callback without things getting really messy.
Is there an elegant way to handle this?
Edit: Some code:
- (void)setup
{
NSError *error = nil;
// get all the video devices. (this should be the back camera and the front camera.)
NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *backVideoDevice;
// find the back camera.
do
{
for (AVCaptureDevice *videoDevice in videoDevices)
{
if (videoDevice.position == AVCaptureDevicePositionBack)
{
backVideoDevice = videoDevice;
break;
}
}
if (backVideoDevice == nil)
{
// display UIAlertView???
}
} while (backVideoDevice == nil);
// if no back camera was found, then we can't continue.
AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:backVideoDevice error:&error];
AVCaptureStillImageOutput *stillImageOutput = [AVCaptureStillImageOutput new];
AVCaptureSession *captureSession = [AVCaptureSession new];
if ([captureSession canAddInput:videoDeviceInput])
{
[captureSession addInput:videoDeviceInput];
}
if ([captureSession canAddOutput:stillImageOutput])
{
[captureSession addOutput:stillImageOutput];
}
// etc, etc.
}
Most of the steps will require checking if they succeeded, like the first one.
Simply have a initialization method like that :
- (void)initDevice {
// If x device is not already initialized
if (!_x) {
_x = ...
if (/* some error with _x initialization */) {
// Show the alert view
...
// Exit initialization
return;
}
}
...
}
And call this method where you want to start the initialization and in the UIAlertViewDelegate callbacks.
If one of the variable is already initialized, it will not be initialized again due to the if statement.
You can also have a int variable called step that you set at each step passed and check the step variable to know where you need to continue the initialization.
Related
I am occasionally getting an NSInvalidArgumentException exception when I start recording video in a viewController but only after taking photos in a previous view controller. I've tried a couple suggestions from Google and So but still get this error at the startRecordingToOutputFileURL:fileURL call.
I never get the error if I don't visit the other view controller that takes photos - it only occurs when I take photos, and then switch to the new view controller which does the video recording.
I think there is some cruft left behind from taking photos, but when I initialize my Video recorder view controller I get no errors setting up the sessions and whatnot. Any ideas what is going on or how to recover from this? Why is it an NSInvalidArgumentException exception? Thanks!
Here is my code:
dispatch_async(dispatch_get_main_queue(), ^{
// Try to Fix bug:
// http://stackoverflow.com/questions/5979962/error-while-recording-video-on-iphone-using-avfoundation
[self.captureSession beginConfiguration];
// Ensure session is running
if ( [self.captureSession isRunning] == NO ) {
NSLog(#"Capture session is NOT running... Starting it now!");
[self.captureSession startRunning];
}
else {
NSLog(#"Capture session is ALREADY running...");
}
NSLog(#"File URL is: %#",fileURL);
NSLog(#"FileOutput is: %#",self.fileOutput);
[self.fileOutput startRecordingToOutputFileURL:fileURL recordingDelegate:self];
// Try to Fix bug:
// http://stackoverflow.com/questions/5979962/error-while-recording-video-on-iphone-using-avfoundation
[self.captureSession commitConfiguration];
});
Here is the error traceback:
2014-05-18 16:01:38.818 app[1699:60b] *** Start recording
2014-05-18 16:01:38.820 app[1699:60b] Capture session is ALREADY running...
2014-05-18 16:01:38.827 app[1699:60b] Capture session is ALREADY running...
2014-05-18 16:01:38.828 app[1699:60b] File URL is: file:////var/mobile/Applications/73FFC590-05A8-4D74-82D9-EBA122B00A20/Documents/2014-05-18-16-01-38-0.mp4
2014-05-18 16:01:38.828 app[1699:60b] FileOutput is: <AVCaptureMovieFileOutput: 0x16513b10>
2014-05-18 16:01:38.829 app[1699:60b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[AVCaptureMovieFileOutput startRecordingToOutputFileURL:recordingDelegate:] - no active/enabled connections.'
*** First throw call stack:
(0x2fe5ff0b 0x3a5f6ce7 0x2ed5751d 0xfb4b5 0x3aadfd53 0x3aadfd3f 0x3aae26c3 0x2fe2a681 0x2fe28f4d 0x2fd93769 0x2fd9354b 0x34d006d3 0x326f2891 0xe40c9 0x3aaf4ab7)
libc++abi.dylib: terminating with uncaught exception of type NSException
This is how the captureSession is initialized ( from the OpenSource project here: https://github.com/shu223/SlowMotionVideoRecorder ):
- (id)initWithPreviewView:(UIView *)previewView {
self = [super init];
if (self) {
NSError *error;
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetInputPriority;
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *videoIn = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (error) {
NSLog(#"Video input creation failed");
return nil;
}
if (![self.captureSession canAddInput:videoIn]) {
NSLog(#"Video input add-to-session failed");
return nil;
}
[self.captureSession addInput:videoIn];
// save the default format
self.defaultFormat = videoDevice.activeFormat;
defaultVideoMaxFrameDuration = videoDevice.activeVideoMaxFrameDuration;
AVCaptureDevice *audioDevice= [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioIn = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];
[self.captureSession addInput:audioIn];
self.fileOutput = [[AVCaptureMovieFileOutput alloc] init];
[self.captureSession addOutput:self.fileOutput];
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
self.previewLayer.frame = previewView.bounds;
self.previewLayer.contentsGravity = kCAGravityResizeAspectFill;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[previewView.layer insertSublayer:self.previewLayer atIndex:0];
[self.captureSession startRunning];
}
return self;
}
My code utilizes this initialization code like this in viewDidLoad:
self.captureManager = [[AVCaptureManager alloc] initWithPreviewView:self.view];
self.captureManager.delegate = self;
The code that actually starts and stops recording is done from an IBAction method like this:
- (IBAction)recButtonTapped:(id)sender {
// REC START
if (self.captureManager.isRecording == NO ) {
NSLog(#"*** Start recording");
// change UI
[self.recBtn setImage:self.recStopImage
forState:UIControlStateNormal];
self.fpsControl.enabled = NO;
// timer start
startTime = [[NSDate date] timeIntervalSince1970];
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:#selector(timerHandler:)
userInfo:nil
repeats:YES];
[self.captureManager startRecording];
}
// REC STOP
else {
NSLog(#"*** Stop recording");
isNeededToSave = YES;
[self.captureManager stopRecording];
[self.timer invalidate];
self.timer = nil;
// change UI
[self.recBtn setImage:self.recStartImage
forState:UIControlStateNormal];
self.fpsControl.enabled = YES;
}
}
EDIT - I am definitely closing the session in the Photo view, here is that code. I verified that it is being called when I leave the Photo view controller.
NSLog(#"RELEASE PHOTO SESSION NOW!");
for(AVCaptureInput *input1 in _mySesh.inputs) {
[_mySesh removeInput:input1];
}
for(AVCaptureOutput *output1 in _mySesh.outputs) {
[_mySesh removeOutput:output1];
}
[_mySesh stopRunning];
// Fix closing of session
dispatch_after(
dispatch_time(0,500000000),
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
_mySesh = nil;
}
);
UPDATE #####
According to the only answer below, I tried to 'unlink' the file prior to starting recording. It still did not work.
NSURL *fileURL = [NSURL URLWithString:[#"file://" stringByAppendingString:filePath]];
//NSLog(#"Beginning to record to output file...");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// Wait for session to start
//[NSThread sleepForTimeInterval:1.0];
dispatch_async(dispatch_get_main_queue(), ^{
// Ensure session is running
if ( [self.captureSession isRunning] == NO ) {
NSLog(#"Capture session is NOT running... Starting it now!");
[self.captureSession startRunning];
}
else {
NSLog(#"Capture session is ALREADY running...");
}
NSLog(#"File URL is: %#",fileURL);
NSLog(#"FileOutput is: %#",self.fileOutput);
// Delete the file
unlink([[#"file://" stringByAppendingString:filePath] UTF8String]);
[self.fileOutput startRecordingToOutputFileURL:fileURL recordingDelegate:self];
});
});
UPDATE
Just for posterity, I am calling the 'didFinishRecordingToOutputFileAtURL' delegate method:
- (void) captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections error:(NSError *)error
{
// Print any errors
if ( error ) {
NSLog(#"Error Recording Video! %#",error.localizedDescription);
}
_isRecording = NO;
if ([self.delegate respondsToSelector:#selector(didFinishRecordingToOutputFileAtURL:error:)]) {
[self.delegate didFinishRecordingToOutputFileAtURL:outputFileURL error:error];
}
}
First of all, start AVCaptureSession first. After that you can create AVPreviewLayer and AVCaptureMovieFileOutput.
Second, use -[AVCaptureSession canAddOutput:] and -[AVCaptureSession canAddInput:] before adding anything to the capture session, this will save you a lot of time and frustration.
Third, you only need beginConfiguration and commitConfiguration when you want to change lots of things in captureSession at once, e.g. remove input, change preset. It's useful on front/back camera switch if you're going to implement it. Starting or stopping session in between these calls is no bueno.
From what I see this is due to the file already existing.
Try removing the file before your call to startRecordingToOutputFileURL: with:
[[NSFileManager defaultManager] removeItemAtPath:fileURL];
You can double check with:
[[NSFileManager defaultManager] fileExistsAtPath:fileURL];
If a file at the given URL already exists when capturing starts, recording to the new file will fail.
Another thing that might cause the crash is if you don't have the delegate method implemented.
It is required to implement:
captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:
because that is the only reliable place to get the output.
Another suspicious thing:
When you create fileURL, you do
NSURL *fileURL = [NSURL URLWithString:[#"file://" stringByAppendingString:filePath]];
Can you change that to
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
And make sure filePath is valid.
From the documentation: This method throws an NSInvalidArgumentException if the URL is not a valid file URL.
Try to comment self.captureSession.sessionPreset = ... line and see if it helps. It made the trick for me. The reason of the problem was that I tried to capture photos with highest quality and FOV, but with this setting I was getting "no active/enabled connections" exception when I tried to capture a video.
Just make sessionPreset high.
swift 4
self.captureSession.sessionPreset = .high
I am writing a program that reads a QR code and then uses the data collected. I am having a strange problem though. I believe it may have to do something with threads. Basically all the NSLogs in the if statement work fine and print rapidly every second yet when i try to update a label text or change the background color it takes ~40 seconds. I don't understand why. Here is the code.
#import "VerifyPassViewController.h"
#import "NewPassViewController.h"
#interface VerifyPassViewController ()
-(BOOL)startReading;
-(void)stopReading;
-(void)loadBeepSound;
#end
#implementation VerifyPassViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// Initially make the captureSession object nil.
_captureSession = nil;
// Set the initial value of the flag to NO.
_isReading = NO;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - IBAction method implementation
- (IBAction)startStopReading:(id)sender {
if (!_isReading) {
// This is the case where the app should read a QR code when the start button is tapped.
if ([self startReading]) {
// If the startReading methods returns YES and the capture session is successfully
// running, then change the start button title and the status message.
[_bbITem setTitle:#"Stop"];
[_bbITem setTitle:#"Scanning for QR Code..."];
}
}
else{
// In this case the app is currently reading a QR code and it should stop doing so.
[self stopReading];
// The bar button item's title should change again.
[_bbITem setTitle:#"Start!"];
}
// Set to the flag the exact opposite value of the one that currently has.
_isReading = !_isReading;
}
#pragma mark - Private method implementation
- (BOOL)startReading {
NSError *error;
// Get an instance of the AVCaptureDevice class to initialize a device object and provide the video
// as the media type parameter.
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// Get an instance of the AVCaptureDeviceInput class using the previous device object.
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
if (!input) {
// If any error occurs, simply log the description of it and don't continue any more.
NSLog(#"%#", [error localizedDescription]);
return NO;
}
// Initialize the captureSession object.
_captureSession = [[AVCaptureSession alloc] init];
// Set the input device on the capture session.
[_captureSession addInput:input];
// Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session.
AVCaptureMetadataOutput *captureMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
[_captureSession addOutput:captureMetadataOutput];
// Create a new serial dispatch queue.
dispatch_queue_t dispatchQueue;
dispatchQueue = dispatch_queue_create("myQueue", NULL);
[captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatchQueue];
[captureMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
// Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer.
_videoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
[_videoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[_videoPreviewLayer setFrame:_viewPreview.layer.bounds];
[_viewPreview.layer addSublayer:_videoPreviewLayer];
// Start video capture.
[_captureSession startRunning];
return YES;
}
-(void)stopReading{
// Stop video capture and make the capture session object nil.
[_captureSession stopRunning];
_captureSession = nil;
// Remove the video preview layer from the viewPreview view's layer.
[_videoPreviewLayer removeFromSuperlayer];
}
#pragma mark - AVCaptureMetadataOutputObjectsDelegate method implementation
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
for(AVMetadataObject *metadataObject in metadataObjects)
{
AVMetadataMachineReadableCodeObject *readableObject = (AVMetadataMachineReadableCodeObject *)metadataObject;
if([metadataObject.type isEqualToString:AVMetadataObjectTypeQRCode])
{
NSLog(#"QR Code = %#", readableObject.stringValue);
self.view.backgroundColor = [UIColor greenColor];
NSLog(#"If the background isnt green i might throw up");
}
else if ([metadataObject.type isEqualToString:AVMetadataObjectTypeEAN13Code])
{
NSLog(#"EAN 13 = %#", readableObject.stringValue);
}
}
}
#end
Let me know if you have any ideas. Thank you
You can only update the UI from the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
... update here your ui ..
});
To state clearly what the other posters hint at: Your code is setting up a dispatch queue that runs on a background thread to process any QR codes it recognizes.
Your captureOutput:didOutputMetadataObjects:fromConnection: method will be run on a background thread.
UI code must be performed on the main thread. rafaperez posted code that you can use from a background thread to dispatch UI changes to a queue on the main thread.
You could also use the method performSelectorOnMainThread:withObject:waitUntilDone: in order to send single method calls to the main thread.
I am using AV Foundation and I have setup a button that I want to toggle between the front facing and rear facing camera when tapped.
If I run the app, and tap the button, the console tells me that I have successfully changed the device to the front facing camera, but the the screen still shows me what the rear camera is seeing.
I need to update the screen so that it really is showing what the front camera sees and really does capture what the front camera sees.
Here is my IBAction code that is hooked up to my toggle button:
-(IBAction)toggleCamera {
_inputDevice = [self frontCamera];
}
And here is the method implementation that is powering "frontCamera":
- (AVCaptureDevice *)frontCamera {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if ([device position] == AVCaptureDevicePositionFront) {
NSLog(#"Device position during frontCamera method: %d", [device position]);
return device;
}
}
return nil;
}
You need to do more than just setting the input device. Do you have an AVCaptureSession set up? If so, each time you swap the camera, you would need to (1) reconfigure the session, (2) remove the current input, (3) add the new input and (4) commit the session configuration:
// Define a flag in your header file to keep track of the current camera input
BOOL isUsingFrontFacingCamera;
-(IBAction)toggleCamera
{
// determine the mode to switch to
AVCaptureDevicePosition desiredPosition;
if (isUsingFrontFacingCamera)
{
desiredPosition = AVCaptureDevicePositionBack;
}
else
desiredPosition = AVCaptureDevicePositionFront;
}
for (AVCaptureDevice *d in [AVCaptureDevice devicesWithMediaType: AVMediaTypeVideo])
{
if ([d position] == desiredPosition)
{
// configure the session
[session beginConfiguration];
// find and clear all the existing inputs
for (AVCaptureInput *oldInput in [session inputs])
{
[session removeInput:oldInput];
}
// add the new input to session
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:d error:nil];
[session addInput:input];
// commit the configuration
[session commitConfiguration];
break;
}
}
// update the flag to reflect the current mode
isUsingFrontFacingCamera = !isUsingFrontFacingCamera;
}
Answer adapted from this post
I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.
Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.
For example:
#implementation aViewController
- (void) viewDidLoad {
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ) NSLog( #"There was a problem opening the database" );
}];
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( ???DatabaseNotAvailableYet??? ) {
[self putSpinnerOnScreen];
???BlockProgressHereUntilDatabaseAvailable???
[self takeSpinnerOffScreen];
}
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.
I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.
I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.
I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.
I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.
This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.
I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.
The following should work:
#implementation aViewController
- (void) viewDidLoad {
self.waitingForDB = NO;
self.databaseReady = NO;
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ){
NSLog( #"There was a problem opening the database" )
}else{
[self performSelectorOnMainThread:#selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
};
}];
}
- (void)handleDatabaseReady{
self.databaseReady = YES;
if(self.waitingForDB){
[self takeSpinnerOffScreen];
[self go];
}
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( !self.databaseReady ) {
self.waitingForDB = YES;
[self putSpinnerOnScreen];
else{
[self go];
}
}
-(void)go{
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.
I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.
This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:#selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.
There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.
I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.
In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.
I'm using the AVFoundation api to create a camera preview view and I'm having trouble cleaning up after I'm done.
The best answer I've found to this problem is in this SO thread, thanks Codo.
However, he doesn't address the deallocation of the AVCaptureVideoPreviewLayer, and that's where I'm having trouble.
In my view controller class I have some initialization code in a startCameraCapture method. Listening to Codo's answer, I'm using dispatch_set_finalizer_f(_captureQueue, capture_cleanup); to register a callback to be called when the queue is truly closed.
I'm also retaining self, to make sure my object doesn't go away before the queue is done calling my object. I then use the capture_cleanup callback to release self.
-(void) startCameraCapture {
_camSession = [[AVCaptureSession alloc] init];
if (_previewLayer == nil) {
_previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_camSession];
}
_previewLayer.frame = self.compView.bgView.frame;
[self.compView.bgView.layer addSublayer:_previewLayer];
// Get the default camera device
AVCaptureDevice* camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// Create a AVCaptureInput with the camera device
NSError *error=nil;
AVCaptureInput* cameraInput = [[AVCaptureDeviceInput alloc] initWithDevice:camera error:&error];
if (cameraInput == nil) {
NSLog(#"Error to create camera capture:%#",error);
}
AVCaptureVideoDataOutput* videoOutput = [[[AVCaptureVideoDataOutput alloc] init] autorelease];
// create a queue to run the capture on
_captureQueue=dispatch_queue_create("captureQueue", NULL);
dispatch_set_context(_captureQueue, self);
dispatch_set_finalizer_f(_captureQueue, capture_cleanup);
// setup our delegate
[videoOutput setSampleBufferDelegate:self queue:_captureQueue];
dispatch_release(_captureQueue);
// retain self as a workouround a queue finalization bug in apples's sdk
// per Stackoverflow answer https://stackoverflow.com/questions/3741121/how-to-properly-release-an-avcapturesession
[self retain];
// configure the pixel format
videoOutput.videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA], (id)kCVPixelBufferPixelFormatTypeKey,
nil];
// and the size of the frames we want
[_camSession setSessionPreset:AVCaptureSessionPresetMedium];
// Add the input and output
[_camSession addInput:cameraInput];
[_camSession addOutput:videoOutput];
[cameraInput release];
// Start the session
[_camSession startRunning];
}
Here the capture_cleanup callback:
static void capture_cleanup(void* p)
{
LiveCompViewController* ar = (LiveCompViewController*)p;
[ar release]; // releases capture session if dealloc is called
}
Then my cleanup code looks like this:
-(void) stopCameraCapture {
[_camSession stopRunning];
[_camSession release];
_camSession=nil;
// Remove the layer in order to release the camSession
[_previewLayer removeFromSuperlayer];
_previewLayer = nil;
}
The problem I'm having is that removing the _previewLayer from the superlayer in stopCameraCapture is causing the following console error:
"...modifying layer that is being finalized..."
But I need to remove the layer so that it gets released and deallocated so that it releases the _camSession which in turn releases the dispatch_queue and then finally calls my capture_cleanup callback which finally releases self.
I don't understand why I'm getting the console error and how to fix it. Why is the Layer being finalized at the time I'm calling [_previewLayer removeFromSuperlayer] if self.dealloc hasn't been called.
Note: self is a viewController and I haven't popped it yet, so it is retained by the NavigationContoller.
Try stopping the session before releasing:
[captureSession stopRunning];