I have a button in a cell which calls a protocol that has data that needs to be passed to the view controller by the segue. The segue is happening through storyboard. My current code uses the shouldperformsegue to return no when the button is pressed as the first segue that happens does not have the data.
Im guessing the segue and the protocol are being handled asynchronously.
But before I return NO I tell it to perform the segue at a delay. This delayed segue does have the data and works fine.
My question is there a way to wait for the protocol to finish and then perform the segue? Maybe with an execution block?
The other responders have hinted about this, but haven't stated it explicitly, so here goes.
Do not tie a segue directly to the button. Instead, control-drag from the source view controller SCENE to the destination scene to create a segue at the view controller level. Give the segue a unique identifier.
Then, in your button IBAction code, do the async network download. You may want to display a "loading, please wait" message or something while the download is taking place. Most async network calls take a completion block. In that completion block, wrap a call to performSegueWithIdentifier in a call to dispatch_async(dispatch_get_main_queue() so the segue gets invoked on the main thread. (#SantaClaus's answer shows the syntax for that.)
So your button IBAction code might look like this:
- (IBAction) buttonAction: (UIButton *) sender;
{
//Display a "please wait"message to the user if desired
doAsyncCallTakingBlock( completion: ^(NSData *data)
{
//parse the data, (or whatever)
dispatch_async(dispatch_get_main_queue(), ^
{
//This call uses the button as the sender. That might be appropriate,
//or not.
[self performSegueWithIdentifier:#"jumpToOtherViewController"
sender: sender];
});
}
}
With this approach the segue doesn't get called until the async method (Which I called doAsyncCallTakingBlock in my example code) has finished it's work. You might call an Alamofire request method, use an NSURLSession, or any other async method that takes a completion block.
Yes, I would use blocks. For example:
[Api getDataWithCompletion:^(BOOL success, NSString *data) {
if (success) {
self.data = data;
[self performSegueWithIdentifier:#"MySegue" sender:self];
} else {
NSLog(#"Failed to get data");
}
}];
Then, to pass it to the next view controller:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"MySegue"]) {
TargetViewController *targetVC = (TargetViewController*)segue.destinationViewController;
targetVC.data = self.data;
}
}
Check out performSegueWithIdentifier on UIViewController. If you set up a segue between the view controllers in your storyboard (not from the button) and give it an identifier, you can perform the segue as soon as the data is ready.
Since you mentioned that your data is being fetch asynchronously, you might need to dispatch the performSegueWithIdentifier call to the main thread like so:
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:#"jumpToOtherViewController" sender:self];
});
To actually pass the data on to the next view controller, you can use prepareForSegue as described here.
Related
I've encountered something that I've never seen. The scenario is as follows. My initial VC has an enroll button that when hit bring up a couple of separate view controllers deal with the enrollment procedure (I ask for user credentials, and if required by the server, I ask for a verification code). When this process is complete, I save a custom object to NSUserDefaults that is needed later. Then the initial VC is shown again.
Basically, I'm making sure I'm in the main thread and that the custom object is saved. If those conditions are met, I then push the main part of the app.
Here is the relevant code, all my logs are displayed in the output window, but the segue does not happen.
- (void)viewDidLoad {
[super viewDidLoad];
self.complete = false;
if([NSThread isMainThread]){
NSLog(#"Main Thread-Yes");
if ([[NSUserDefaults standardUserDefaults] objectForKey:#"CUSTOMOBJECT"]) {
NSLog(#"Will begin pushToAuthenticate");
[self pushAuthenticateController];
}
} else {
NSLog(#"Main Thread-No");
}
}
- (void)pushAuthenticateController{
[self performSegueWithIdentifier:#"pushMainVC" sender:self];
}
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"pushMainVC"]) {
NSLog(#"PrepareForSegue-pushMainVC");
[segue.destinationViewController.navigationItem setHidesBackButton:YES];
[segue.destinationViewController.navigationItem setTitle:#"MainVC"];
}
}
In storyboard I have a segue that connects the initial VC with the MainVC, the identifier for that segue matches "pushMainVC", and if I close and reopen the app (after enrolling) the segues does occur. The console output for both instances (reloading the initial VC after enrolling and closing and reopening the app) are as follow:
APPNAME[4106:1628238] Main Thread-Yes
APPNAME[4106:1628238] Will begin pushToAuthenticate
APPNAME[4106:1628238] PrepareForSegue-pushMainVC
Any idea why this could happen, any small thing I'm missing, or any help will be greatly appreciated. Thank you so much.
You are attempting to perform a push segue from viewDidLoad. But you can't do that because it's too soon; viewDidLoad means that the view exists but is not yet in the interface. You need to wait until there is an interface so you have something to push from.
You cant try to load it from the storayboard itself.
like this:
pushMainVC* pushMainV = [self.storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([pushMainVC class])];
[self.navigationController pushViewController:pushMainV animated:YES];
NOTICE:
Dont forget to give your viewcontroller Storyboard ID.
I’m I downloaded MZFormSheetController library for my app.
I’ve got a problem on my popup. When I am on my TableViewController, I tap on a row to get popup to open up so that I can change the name. The popup opens, I set the name and when I tap on the button to correct the name, I call the button method but i can’t close my popup while reload my list.
- (IBAction)modifierTournoi:(id)sender {
//code to update database
//this method close the popup but don't call method viewWillAppear to reload database
//I don't know what method i can use..?
[self dismissFormSheetControllerAnimated:YES completionHandler:^(MZFormSheetController *formSheetController) {
}];
}
Before that, I used the method popViewControllerAnimated to come back to my list while recharging my list.
- (IBAction)modifierJoueur:(id)sender {
//code to update database
[self.navigationController popViewControllerAnimated:true];
}
Can you help me please ?
Thank you very much.
It looks like there is a specific completion handler for this purpose built into the library you are using:
- (IBAction)modifierTournoi:(id)sender {
//code to update database
//this method close the popup but don't call method viewWillAppear to reload database
//I don't know what method i can use..?
[self dismissFormSheetControllerAnimated:YES completionHandler:^(MZFormSheetController *formSheetController) {
// Reloading your database should work in here.
}];
}
The reason viewWillAppear will not be being called is because rather than placing a viewController modally above your window, I imagine MZFormSheetController will be adding a UIView above all presented UIViews, so viewWillAppear will never be called. But as I said above, you should be able to reload in the completion handler block.
I am attempting to perform a segue after a done button is pressed from a custom signature alertView. The segue works perfect if I call it from a button inside the same view, but when I call from my signature class, I get a crash that says:
`'Receiver (<SuperSigApproveTableViewController: 0x79eea280>) has no segue with identifier 'approveSwap''
SignatureAlertViewController is the name of the class that calls the done method
- (IBAction)dismissAlertView:(id)sender {
[linerSig sendSigImageToServer];
[self.presentingViewController dismissViewControllerAnimated:YES completion:^{[sp sendToPDF];}];
}
inside the completion block is the method I am trying to call which is located inside of SuperSigApproveTableViewController
-(void)sendToPDF {
NSLog(#"sendtopdf");
[self performSegueWithIdentifier: #"approveSwap" sender: self];
}
I have alloc and init the SuperSigApproveTableView in the viewDidLoad method of SignatureAlertViewController. I am not sure why it is crashing does anyone have any clues?
Iv searched a lot for this and cant find an answer to my specific question. But basically my question is, can i pass a completion block into another view controller and redefine it in the new view controller.
So for example in view controller A i have a method to perform a download with a completion block. First i create my block property in view controller A.
#property (copy)void (^downloadCompleteBlock)(NSArray *downloadItems);
I tried changing this to strong as opposed to copy but this did not solve my problem.
Then i define the completion block as follows.
self.downloadCompleteBlock = ^(NSArray *downloadItems) {
NSLOG(#"download complete in view controller A";
};
Then i call my download method passing in this completion block.
[self download:self.downloadCompleteBlock];
However, if this completion handler is not called by the time i leave this view controller (if the download isn't complete) I would want the completion block to perfrom something different on the next view controller. So in my prepare for segue i attempted to pass in this block to view controller B.
[controllerB setCompletionBlock:self.downloadCompleteBlock];
And this method in view controller B then redefines what happens when this completion block gets called.
- (void)setCompletionBlock:(void(^)(NSArray *downloadItems))downloadFinishedBlock {
downloadFinishedBlock = ^(NSArray *downloadItems) {
self.collectionData = downloadItems;
[self.collectionView reloadData];
};
}
However, the original block in view controller a still gets called when the download finishes as opposed to the block in view controller B. Anyone know how to have the completion block in view controller B called if that view is loaded before the download completes? I know i could use a notifier but i'm curious if i can do this with blocks.
Thanks
This is kind of a tough problem. At its heart is the problem of how to keep the block around after the first view controller goes away. Your current code solves that problem unwittingly by having the block refer to self. The vc is retained by that reference, which is good news if it needs to be around when the request finishes, but it's bad news because now the vc and the block will retain each other forever. (Google 'retain cycle'.)
So how do we get a long-running process that runs a block on completion and might outlive two or more view controllers? For starters, break that process into its own object. The interface of that object would look like:
#interface DownloadThingy
#property (copy)void (^downloadCompleteBlock)(NSArray *); // note, no need for dummy param names here
- (id)initWithRequestParams:(id)whateverIsNeededToStart;
- (void)start;
#end
Now, the view controller that want to start this can declare a strong property to it, create one, give it a completion block (see below**), and start it. When it's time for a segue, it can pass the downloadThingy to another vc, who can give it a different completion block.
** Since the request object is being kept as a property in one or more vcs, and since it retains the block, you still need to look out for a retain cycle:
(vc->downloadThingy->block->vc)
In VcA, do this:
- (void)startADownloadThingy {
self.downloadThingy = [[DownloadThingy alloc] initWithRequestParams:someParams];
__weak VcA *weakSelf = self;
self.downloadThingy.downloadCompleteBlock = ^(NSArray *downloadItems) {
// don't use self in here, use weakSelf
}
}
VcB will get called on the segue; it might or might not need to follow the same precaution. The distinction is whether this second vc retains a downloadThingy property. If it doesn't plan to hand it off to any other vc, it can skip the property, and thereby skip the worry about a retain cycle.
// another vc is handing off a running downloadThingy
- (void)heresARunningDownloadThingy:(DownloadThingy *)downloadThingy {
// if we have our own property, then
self.downloadThingy = downloadThingy;
// and we need to do the weakSelf trick
__weak VcA *weakSelf = self;
self.downloadThingy.downloadCompleteBlock = ^(NSArray *downloadItems) {
// don't use self in here, use weakSelf
}
}
Or...
// another vc is handing off a running downloadThingy
- (void)heresARunningDownloadThingy:(DownloadThingy *)downloadThingy {
// we do not have our own property
downloadThingy.downloadCompleteBlock = ^(NSArray *downloadItems) {
// feel free to use self in here
}
}
One last thing: it's a good practice for the DownloadThingy to aggressively nil out its block after it's through invoking it. So when the request is done, have it do this...
// DownloadThingy.m
// request is complete
self.downloadCompleteBlock(arrayFullOfResults);
self.downloadCompleteBlock = nil;
I have got two UIViewController:
MainVC
PickerVC
In main view controller I have IBAction method:
- showPickerView
In this method I need to create block that will present PickerVC view and wait me while I choose something value on PickerVC view and press Done button.
In this block I need to implement callback that will invoke method in MainVC after I press on button Done.
So, I have used block before, but I don't know how to implement it by myself.
I think first part will be look like this:
- (IBAction)showPickerView {
__block PickerVC *pickerVC = [[PickerVC alloc] init];
[pickerVC setFinishBlock:^{
// Do something after user press on Done button
}];
[pickerVC setFailedBlock:^{
// Do something if something wrong (but this method optional)
}];
[pickerVC showPicker];
}
Add in the header of PickerVC two typedefs
typedef void (^FinishBlock)();
typedef void (^FailedBlock)();
and your declaration of setFinishedBlock takes the FinishBlock
- (void)setFinishBlock:(FinishBlock)finishBlock;
- (void)setFailedBlock:(FailedBlock)failedBlock;
Make an iVar for each block
#interface PickerVC : UIViewController
{
FinishBlock _finishBlock;
FailedBlock _failedBlock;
}
In your definition of setFinishedBlock: and setFailedBlock: set the parameter to the iVars
As soon as PickerVC fails or finishes call _failedBlock or _finishedBlock.
The __block statement is used for variables to stay in memory if they are used in a block. So you don't need it in the above code