Amending UINavigationController viewControllers stack before animation - ios

I have an app where you can customize products to varying degrees. In some cases the options are split to two views, while in some other cases the first step isn't necessary.
What I would like is to treat all products the same and push the first customization step view controller to the navigation controller stack, let that view controller decide whether or not this step is necessary. If it is not necessary I want it to apply some default options to the product and immediately skip (before the transition animation) to step 2 while not allowing the user to back up to the first step.
The normal UINavigationController.viewControllers stack may look like this when at step 2:
[ListView (root)] -> [CustomizeStep1] -> [CustomizeStep2]
But I want it to apply the default values to the product and amend the view controller stack so that:
[ListView (root)] -> [CustomizeStep1]
----- becomes -----
[ListView (root)] -> [CustomizeStep2]
What I've tried is to use code like this in the CustomizeStep1 view controller:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (shouldSkipToStep2) {
UINavigationController *navController = self.navigationController;
// Move directly to step 2
UIStoryboard *storyboardLoader = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *customizeStep2VC = [storyboardLoader instantiateViewControllerWithIdentifier:#"customizeStep2"];
// Replace current view contoller
NSMutableArray *viewHierarchy = [NSMutableArray arrayWithArray:navController.viewControllers];
[viewHierarchy removeObject:self];
[viewHierarchy addObject:customizeVC];
// Apply new viewController stack
[navController setViewControllers:viewHierarchy animated:NO];
}
}
If I take a look at the navigation controller's viewControllers array after this has been set, everything looks as expected.
What happens in iOS 7
When doing this, the entire functionality of the UINavigationController breaks. The CustomizeStep1 view controller still animates in but is nonfunctional. Tapping the back button still shows CustomizeStep1. Trying to interact with the view controller crashes the app. (It works as expected if the view controller is displayed without the sliding transition, though.)
What happens in iOS 8
The CustomizeStep1 view controller still animates in, but immediately after the transition ends it snaps over to show CustomizeStep2. Other than that it works as intended.
So, my question is if there is a better place to add the code to amend the view controller stack on the navigation controller?
I obviously need to wait until the view controller has been added to the navigation controller, otherwise I can't replace the view controller in the stack. However, I need to be able to cancel the transition animation so that I can animate in CustomizeStep2 instead.
I appreciate if this is impossible, just wanted to check if anyone knows a good way around this.
Edit:
How I would like it to ideally appear to the user

Instead of viewWillAppear:, use viewDidAppear: which is called after the animation finishes.
You could have a boolean on your view controller denoting whether it is filled in or not:
#interface ViewControllerOne : UIViewController
#property (nonatomic, assign, getter = isInitiallyFilledIn) BOOL initiallyFilledIn;
#end
Then, when it is initially filled in, just denote this boolean value.
ViewControllerOne *viewController = [[ViewControllerOne alloc] init];
[viewController setInitiallyFilledIn:YES];
Now, in viewDidAppear:, check this boolean value and check whether that method has been launched before. If it hasn't been launched before (to allow editing) and it is initially filled in, push the next controller!
#interface ViewControllerOne
#property (nonatomic, assign) BOOL hasCheckedFillInStatusBefore;
#end
#implementation ViewControllerOne
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([self isInitiallyFilledIn] && ![self hasCheckedFillInStatusBefore]) {
// push the next view controller
}
[self setHasCheckedFillInStatusBefore:YES];
}
#end
Alternatively, if you want to display the two view controllers at the same time, you could alter the navigation stack:
// create instances of ViewControllerOne and ViewControllerTwo
NSMutableArray *viewControllers = [[[self navigationController] viewControllers] mutableCopy];
[viewControllers addObjectsFromArray:#[viewControllerOne, viewControllerTwo]];
[[self navigationController] setViewControllers:viewControllers animated:YES];
Note, the ViewControllerOne will not have viewDidLoad called so if you do any setup in that method (such as a back button title or the view controller title), you will either have to manually invoke that method before setting the view controllers or move that setup to the initializer.

Related

How to come back from view controller to tabbed view controller

I have an issue the dismissing view controller to the tabbed view controller. I will explain my flow.
view controller to tabbed view controller(push)
I have 5 tabs in the tabbed view controller in the 3 tabbed i have an camera view from their i am presenting the view controller and passing some parameters by using this code
UIStoryboard *storybord=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
shareViewController *shareview=[storybord instantiateViewControllerWithIdentifier:#"share"];
[self presentViewController:shareview animated:YES completion:nil];
//shareview.finalvideourl=videoURL;
shareview.videooutputstring=videoPath;
from the share view controller I want send the data back to the 1st tab for this I am using the below code
UIStoryboard *story=[UIStoryboard storyboardWithName:#"Main" bundle:nil];
TabedfirstViewController *Tabedfirst=[story instantiateViewControllerWithIdentifier:#"id"];
UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:Tabedfirst];
[self presentViewController: nc animated:YES completion:^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"ShareArray" object:_selectedimgarray];
[[NSNotificationCenter defaultCenter] postNotificationName:#"SharetitleArray" object:_newtile];
[[NSNotificationCenter defaultCenter] postNotificationName:#"sharevideooutputstring" object:_videooutputstring];
}];
When I do this very thing working good I am sending the data from share view to the tabbed view and I am printing it.
The problem is when I send the second time data from share viewcontroller to the tabbed view controller first data is deleting and the second passed data is replacing the first i.e. I have the 15 objects in an array, from share view controller I am passing array to the tabbed view controller now the array count is 16 and I am printing it, now again I am passing one more object from the share view to tabbed view the array count must increase to 17 but it 16 only.
In the App delegate add/make properties of your arrays like
#property (nonatomic, retain) NSMutableArray *array1;
In AppDelegate.m initiate all your properties
After that in the controller where you are storing the values add instance of AppDelegate as
In
- (void)viewDidLoad
{
[super viewDidLoad];
objAppDelegate = (AppDelegate*)[[UIApplication sharedApplication]delegate];
objAppDelegate.array1= _imagearray=[#[#"bootimages",#"fall-photography-in-hocking-hills", #"gift",#"hillimages",#"Mercedes-classe-S-W116_large_dettaglio_articolo",#"prini‌​mages",#"resplendent",#"tnb4",#"Tomato-plant",#"Vole",#"waterimages",#"fall-photo‌​graphy-in-hocking-hills", #"gift",#"hillimages",#"Mercedes-classe-S-W116_large_dettaglio_articolo"]mutable‌​Copy];
then
[objAppDelegate.array1 insertObject:[userInfo firstObject] atIndex:0];
The values are maintained and can be used anywhere in the whole project. If your number of objects keeps on increasing then you NSMutableDictionary and add as much key/value pairs as you like.
I'm not sure why you are creating new storyboards using your main storyboard. All you have to do is presentViewController from one the viewcontroller you want to return from with the id of a segue from that viewcontroller back to the root of your tabbed view controller. In your storyboard, ctrl+drag from a button (or whatever is triggering the segue) and drag it to the root of the tab view controller. Select the segue and in the attributes editor give it an id. then in the viewcontroller call presentViewcontroller with the id and it should work fine.
In the shareViewcontroller.h write the add the following method
#protocol childDelegate <NSObject>
-(void) sendImage:(UIImage *)img
#end
and the property
#property (assign) id <childDelegate> cDelegate;
once you have done that on the controller where you have the method
-(void) receivedArray:(NSNotification*)notification
add the method
-(void) sendImage:(UIImage *)img
{
// Add image to app.array1 here.
}
In the share view controller where you are sending the image to the last controller call this method
[self.cDelegate sendImage:"your image comes here"];
Still if you face as issue, then refer to How do I set up a simple delegate to communicate between two view controllers?
Let me know if you still face an issue.

Go to second viewcontroller in NavigationViewController

I have a UINavigationController which points to a UITableViewController (a list of items) where there is a segue from a cell to another UITableViewController (a screen to edit an item).
On first run of the application, I'd like to skip the first list and immediately go to the second screen, to edit a new item.
The problem is I need to pass the first UITableViewController, as I need to be able to go back to that one (or is there a way to set the controller the back button is pointing to?).
Things I've tried and failed:
Set a boolean shouldPresentNewItem on the UINavigationController and in the viewDidLoad if it is set to true, present the first UITableViewController, also setting a boolean so I can go to the edit screen.
Using self.navigationController!.popToViewController(arr[index] as UIViewController, animated: true) in the UINavigationControllers viewDidLoad. This gave an error as self.navigationController was nil. (I don't get why this happens)
How can this be done?
In navigation controller set some boolean indicating that you're going to show edit screen and in viewDidLoad just push edit view controller without animation:
- (void) viewDidLoad {
[super viewDidLoad];
if (self.presentEditScreen) {
self.presentEditScreen = NO;
EditViewController *e = [[DetailViewController alloc] init];
[self pushViewController:e animated:NO];
}
}
simplest way will be. just push from second view to first view
firstViewController *objFirstViewController = [[firstViewController alloc]initWithNibName:#"firstViewController" bundle:nil];
[self.navigationController pushViewController:objFirstViewController animated:No];

iOS make popup with table view [duplicate]

I need to pop up a quick dialog for the user to select one option in a UITableView from a list of roughly 2-5 items. Dialog will be modal and only take up about 1/2 of screen. I go back and forth between how to handle this. Should I subclass UIView and make it a UITableViewDelegate & DataSource?
I'd also prefer to lay out this view in IB. So to display I'd do something like this from my view controller (assume I have a property in my view controller for DialogView *myDialog;)
NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:#"DialogView" owner:myDialog options:nil];
myDialog = [nibViews objectAtIndex:0];
[self.view addSubview:myDialog];
problem is i'm trying to pass owner:myDialog which is nil as it hasn't been instantiated...i could pass owner:self but that would make my view controller the File's Owner and that's not how that dialog view is wired in IB.
So that leads me to think this dialog wants to be another full blown UIViewController... But, from all I've read you should only have ONE UIViewController per screen so this confuses me because I could benefit from viewDidLoad, etc. that come along with view controllers...
Can someone please straighten this out for me?
There is no such thing as a view controller being on the screen; its view is on the screen. With that said, you can present as many views as you want on the screen at once.
I would create a new view and view controller. You would not make a UIView be a UITableViewDelegate, you make a UIViewController be a UITableViewDelegate. But instead of doing that manually, instead make your new view controller a subclass of UITableViewController, if you're using iPhone OS 3.x+. You can then present this view controller modally.
You probably want to give the user a chance to cancel out of the selection. A good way to do that is to wrap your new dialog view controller in a UINavigationController and then put a "Cancel" button in the nav bar. Then use the delegate pattern to inform the parent view controller that the user has made their choice so you can pop the stack.
Here's what the code will look like inside your parent view controller, when you want to present this option dialog:
- (void)showOptionView
{
OptionViewController* optionViewController = [[OptionViewController alloc] initWithNibName:#"OptionView" bundle:nil];
optionViewController.delegate = self;
UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:optionViewController];
[self.navigationController presentModalViewController:navController animated:YES];
[navController release];
[optionViewController release];
}
Your OptionViewController .h will look like this:
#protocol OptionViewControllerDelegate;
#interface OptionViewController : UITableViewController
{
id<OptionViewControllerDelegate> delegate;
}
#property (nonatomic, assign) id<OptionViewControllerDelegate> delegate;
#end
#protocol OptionViewControllerDelegate <NSObject>
- (void)OptionViewController:(OptionViewController*)OptionViewController didFinishWithSelection:(NSString*)selection;
// or maybe
- (void)OptionViewController:(OptionViewController*)OptionViewController didFinishWithSelection:(NSUInteger)selection;
// etc.
#end
Your OptionViewController.m will have something like this:
- (void)madeSelection:(NSUInteger)selection
{
[delegate OptionViewController:self didFinishWithSelection:selection];
}
Which has a matching method back in your original view controller like:
- (void)OptionViewController:(OptionViewController*)OptionViewController didFinishWithSelection:(NSUInteger)selection
{
// Do something with selection here
[self.navigationController dismissModalViewControllerAnimated:YES];
}
There are plenty of examples throughout Apple's sample source code that follow this general pattern.

iOS7 UINavigationController pushViewController:animated back to back with animation locks up the main thread

From what I can tell, it seems that pushing into UINavigationController back to back with animation created a dead lock on iOS7.
I initially struggled with a crash on iOS6 and came up with the solution of:
Create an array of view controllers to push onto the navigation controller after the initial push. So if there are three view controllers to be pushed (A, B and C) then this array would have B and C.
Implement UINavigationControllerDelegate's navigationController:didShowViewController:animated:
In the delegate method, simply check if there are more elements in the view controller array. If so, take the first element out of it and push that into the navigation controller.
So essentially if there are B and C view controllers to be pushed, navigationController:didShowViewController:animated: will be called three times; every time after the view controller gets pushed starting with A. Obviously, the last call wouldn't do anything since the array should be empty at that point.
Note that this approach worked fine on iOS6. However, this breaks in iOS7. It seems that when it tries to animate in the second push, the app freezes. After digging a little bit more I came up with the solution of pushing the second view controller in the following manner in the delegate implementation.
dispatch_async(dispatch_get_main_queue(), ^{
[navigationController pushViewController:viewController
animated:YES];
});
This seems to fix the problem, but I was wondering if anyone has experienced a similar thing and have a better explanation of what exactly is going on.
I'm not sure what problem you're having. I created a test app that has a navigation controller and four other controllers. The first controller has a button to push to the second controller, which is FirstPushedViewController. In that controller I have this code to push the next two controllers, and it worked fine in iOS 7:
#interface FirstPushedViewController ()
#property (strong,nonatomic) NSMutableArray *vcs;
#end
#implementation FirstPushedViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.delegate = self;
UIViewController *orange = [self.storyboard instantiateViewControllerWithIdentifier:#"Orange"];
UIViewController *red = [self.storyboard instantiateViewControllerWithIdentifier:#"Red"];
self.vcs = [#[red,orange] mutableCopy];
}
-(void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.vcs.count > 0) {
[self.navigationController pushViewController:self.vcs.lastObject animated:YES];
[self.vcs removeLastObject];
}
}

iOS: Pushing a view controller with animation only works once

I am creating an iPhone client for one of my apps that has an API. I am using the GTMOAuth2 library for authentication. The library takes care of opening a web view for me with the correct url. However I have to push the view controller myself. Let me show you some code to make things more clear:
- (void)signInWithCatapult
{
[self signOut];
GTMOAuth2ViewControllerTouch *viewController;
viewController = [[GTMOAuth2ViewControllerTouch alloc] initWithAuthentication:[_account catapultAuthenticaiton]
authorizationURL:[NSURL URLWithString:kCatapultAuthURL]
keychainItemName:kCatapultKeychainItemName
delegate:self
finishedSelector:#selector(viewController:finishedWithAuth:error:)];
[self.navigationController pushViewController:viewController animated:YES];
}
I have a "plus"/"add" button that I add to the view dynamically and that points to that method:
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(signInWithCatapult)];
When I press the "add" button, what is supposed to happen is to open the web view with an animation, and then add an account to the accounts instance variable which populates the table view. This works fine if I add one account, but as soon as I try to add a second account, the screen goes black and two errors appear in the console:
nested pop animation can result in corrupted navigation bar
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
The only way that I found to avoid this problem was to disable animations when pushing the view controller.
What am I doing wrong please?
Typical situations
You push or pop controllers inside viewWillAppear: or similar methods.
You override viewWillAppear: (or similar methods) but you are not calling [super viewWillAppear:].
You are starting two animations at the same time, e.g. running an animated pop and then immediately running an animated push. The animations then collide. In this case, using [UINavigationController setViewControllers:animated:] must be used.
Have you tried the following for dismissing once you're in?
[self dismissViewControllerAnimated:YES completion:nil];
I got the nested pop animation can result in corrupted navigation bar message when I was trying to pop a view controller before it had appeared. Override viewDidAppear to set a flag in your UIViewController subclass indicating that the view has appeared (remember to call [super viewDidAppear] as well). Test that flag before you pop the controller. If the view hasn't appeared yet, you may want to set another flag indicating that you need to immediately pop the view controller, from within viewDidAppear, as soon as it has appeared. Like so:
#interface MyViewController : UIViewController {
bool didAppear, needToPop;
}
...and in the #implementation...
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
didAppear = YES;
if (needToPop)
[self.navigationController popViewControllerAnimated:YES];
}
- (void)myCrucialBackgroundTask {
// this task was presumably initiated when view was created or loaded....
...
if (myTaskFailed) { // o noes!
if (didAppear)
[self.navigationController popViewControllerAnimated:YES];
else
needToPop = YES;
}
}
The duplicated popViewControllerAnimated call is a bit ugly, but the only way I could get this to work in my currently-tired state.

Resources