I have a custom segue animation that occurs when pushing a new view controller onto the stack. When popping the view controller that was presented with said custom segue, however, the default navigation controller animation happens (that is, the current view controller animates to the right while the parent view controller translates on-screen from the left edge).
So my question is this: is there a way to write a custom pop segue animation which happens when popping a view controller off the stack?
Edit (solution):
I ended up defining a custom segue similar to the selected answer. In the Storyboard, I dragged a custom segue from the child view controller back to its parent, gave it an identifier and the newly written reverse segue as its class. Yes, I realize it is virtually identical to a modal transition. Client requirements necessitated this madness, so before anyone comments, understand that I know one shouldn't have to do this under normal circumstances.
- (void)perform {
UIViewController *src = (UIViewController *)self.sourceViewController;
UIViewController *dest = (UIViewController *)self.destinationViewController;
[UIView animateWithDuration:0.3 animations:^{
CGRect f = src.view.frame;
f.origin.y = f.size.height;
src.view.frame = f;
} completion:^(BOOL finished){
src.view.alpha = 0;
[src.navigationController popViewControllerAnimated:NO];
}];
}
Yes. Here is an example where I pop to the top level. When your create the segue in Storyboard. Use select or enter the new new segue class in the attributes inspector.
//
// FlipTopPop.h
#import <UIKit/UIKit.h>
#interface FlipTopPopToRoot : UIStoryboardSegue
#end
and
// FlipTopPop.m
#import "FlipTopPopToRoot.h"
#implementation FlipTopPopToRoot
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
[UIView transitionWithView:src.navigationController.view duration:0.5
options:UIViewAnimationOptionTransitionFlipFromBottom
animations:^{
[src.navigationController popToViewController:[src.navigationController.viewControllers objectAtIndex:0] animated:NO];;
}
completion:NULL];
}
#end
If you want to pop up just one level change use this custom segue:
// PopSegue.h
#import <UIKit/UIKit.h>
#interface PopSegue : UIStoryboardSegue
#end
and
// PopSegue.m
#import "PopSegue.h"
#implementation PopSegue
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
[src.navigationController popViewControllerAnimated:YES];
}
#end
For anyone following this now, iOS 7 lets you animate both ways:
Set the segue to Push, then see code below for a push implementation.
https://github.com/Dzamir/OldStyleNavigationControllerAnimatedTransition
As the commenter Linus pointed out, the other solutions presented will create another instance of the UIViewController. I think this link here describe other alternatives to enabling reverse segue animations.
http://robsprogramknowledge.blogspot.com/2012/05/back-segues.html
Related
I'm playing around with storyboarding in an OS X 10.10 app. I have an NSTableView that, when you click a specific row opens a segue that goes to a popover that contains an NSViewController.
How do you specify the origin NSPoint of the arrow for the popover? Right now, it just points to the NSTableView in the middle. I assumed that I could do this in prepareForSegue, but I can't seem to figure it out. prepareForSegue doesn't seem to have an understanding that the NSViewController is contained in an NSPopover
Any ideas?
You should file an enhancement request Radar for this behavior if you think it should be provided by the framework in some way.
But to workaround this in the meantime, you can create your own custom NSStoryboardSegue subclass to help with this.
#interface TablePopoverSegue : NSStoryboardSegue
#property (weak) NSTableView *anchorTableView;
#property NSRectEdge preferredEdge;
#property NSPopoverBehavior popoverBehavior;
#end
#implementation TablePopoverSegue
- (void)perform {
if ([self anchorTableView]) {
NSInteger selectedColumn = [[self anchorTableView] selectedColumn];
NSInteger selectedRow = [[self anchorTableView] selectedRow];
// If we can pick a specific row to show from, do that; otherwise just fallback to showing from the tableView
NSView *anchorView = [self anchorTableView];
if (selectedRow >= 0) {
anchorView = [[self anchorTableView] viewAtColumn:selectedColumn row:selectedRow makeIfNecessary:NO];
}
// Use the presentation API so that the popover can be dismissed using -dismissController:.
[[self sourceController] presentViewController:[self destinationController] asPopoverRelativeToRect:[anchorView bounds] ofView:anchorView preferredEdge:[self preferredEdge] behavior:[self popoverBehavior]];
}
}
#end
This can be specified in IB in the inspector panel for the segue (just like iOS):
And then in your source view controller's prepareForSegue:, you can just set up the segue:
- (void)prepareForSegue:(NSStoryboardSegue *)segue sender:(id)sender {
if ([segue isKindOfClass:[TablePopoverSegue class]]) {
TablePopoverSegue *popoverSegue = (TablePopoverSegue *)segue;
popoverSegue.preferredEdge = NSMaxXEdge;
popoverSegue.popoverBehavior = NSPopoverBehaviorTransient;
popoverSegue.anchorTableView = [self tableView];
}
}
I have UIViewController (for example, loginVC) and I'm trying to add it's view on top of all views.
I tried to add this view to AppDelegate
[[AppDelegate sharedDelegate].window addSubview:loginVC.view];
But in this case autorotation doesn't work, so I tried to add this view to NavigationController's view. NavigationController is rootViewController:
[[AppDelegate sharedDelegate].navigationController.view addSubview:loginVC.view];
It looks good and autorotating, but it has strange behavior when rotating.
After beginning of rotation, navigation bar is showing on top of loginVC.view and at the end of rotation is going behind this view, like it shown on screenshots (I've set red background to make it more visible, background is transparent, to see all stuff behind this view):
What I've tried:
I found this somewhere on stackoverflow: disable UIView animations before rotating and enable them after rotating - doesn't look good, because rotating occurs without animation (it's a bit obvious)
tried to make navigationBar hidden before rotation and make it visible after rotation, but in this case navigationBar bringing on top of loginVC.view
Next thing I gonna do - add this view on AppDelegate's window and handle rotation manually, but maybe there is some better way to do this?
UPD:
screenshots:
You can see issue on second screenshot: navigation bar is on top
add your viewController in uinavigationcontroller and push uinavigationcontroller then always navigation bar is visible.
My friend helped me with this problem
Here is the solution:
In AppDelegate I've created UIWindow property:
//AppDelegate.h
#property (nonatomic, strong) UIWindow *loginWindow;
Initialized it when application starts
//AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
....
self.loginWindow = [[UIWindow alloc] init];
self.loginWindow.windowLevel = UIWindowLevelStatusBar;
self.loginWindow.frame = [[UIScreen mainScreen] bounds];
self.loginWindow.backgroundColor = [UIColor clearColor];
....
return YES;
}
And then, in loginVC:
#interface loginVC ()
#property (nonatomic, weak) UIWindow *loginWindow;
#end
#implementation
....
- (void)show {
// setting up loginVC view
if (!self.loginWindow) {
self.loginWindow = [[AppDelegate sharedDelegate] loginWindow];
}
if (![self.loginWindow.rootViewController isEqual:self]) {
[self.loginWindow setRootViewController:self];
}
self.loginWindow.hidden = NO;
//UPD:
//[self.loginWindow makeKeyAndVisible];
//UPD2:
[self.loginWindow makeKeyWindow];
}
- (void)hide {
// hiding view and stuff
[[[AppDelegate sharedDelegate] loginWindow] setHidden:YES];
//UPD:
//[[[AppDelegate sharedDelegate] window] makeKeyAndVisible];
//UPD2:
[[[AppDelegate sharedDelegate] window] makeKeyWindow];
}
#end
UPD:
No need to use makeKeyAndVisible method of UIWindow, second window will be always on top of first one.
UPD2:
Again updating my answer, maybe it will be useful for somebody.
Without makeKeyAndVisible I couldn't use UITestFields so I uncommented that code and faced another problem:
I have UIViewController, create an instance of another UIViewController inside this controller and call [self presentViewController:...]. In presented UIViewController I'm creating loginVC, but when I call
[[[AppDelegate sharedDelegate] window] makeKeyAndVisible];
presented viewController disappears, but first view controller still has this controller as presentedViewController, so I can't present other view controllers.
My solution was change makeKeyAndVisible on makeKeyWindow.
I'm implementing an IOS6 app using storyboards. I want every screen--excuse me, scene--for the app to have a view at the top containing different image buttons of different sizes. Tapping the buttons takes the user to different scenes of the app.
That's too complex for a UITabController, as far as I can tell. I tried making a separate view controller for that view and including the view in each scene, but any functionality in the view--such as the buttons--causes the app to crash.
It looks like I may have to implement this view in the storyboard in one scene, then copy and paste it into every other scene, wiring up the segues from every scene to every other scene. What a maintenance nightmare! Is there a better way?
Since you are trying to create a custom UITabBarController, you should use a container view controller. To do that:
Open your storyboard and add a custom UIVIewController (let's call it ContainerViewController).
Insert the UIVIews that represent your tabs into that controller and then insert another UIVIew (*currentView in the code below) that will take the rest of the screen. That's what the child controllers will use to display their scene.
Create a UIVIewController for each scene (child controller) you need, as you would normally, and give each of them a unique identifier (Identity Inspector -> Storyboard ID)
Now you have to add the following code your ContainerViewController:
#interface ContainerViewController ()
#property (strong, nonatomic) IBOutlet UIView *currentView; // Connect the UIView to this outlet
#property (strong, nonatomic) UIViewController *currentViewController;
#property (nonatomic) NSInteger index;
#end
#implementation ContainerViewController
// This is the method that will change the active view controller and the view that is shown
- (void)changeToControllerWithIndex:(NSInteger)index
{
if (self.index != index){
self.index = index;
[self setupTabForIndex:index];
// The code below will properly remove the the child view controller that is
// currently being shown to the user and insert the new child view controller.
UIViewController *vc = [self setupViewControllerForIndex:index];
[self addChildViewController:vc];
[vc didMoveToParentViewController:self];
if (self.currentViewController){
[self.currentViewController willMoveToParentViewController:nil];
[self transitionFromViewController:self.currentViewController toViewController:vc duration:0 options:UIViewAnimationOptionTransitionNone animations:^{
[self.currentViewController.view removeFromSuperview];
[self.currentView addSubview:vc.view];
} completion:^(BOOL finished) {
[self.currentViewController removeFromParentViewController];
self.currentViewController = vc;
}];
} else {
[self.currentView addSubview:vc.view];
self.currentViewController = vc;
}
}
}
// This is where you instantiate each child controller and setup anything you need on them, like delegates and public properties.
- (UIViewController *)setupViewControllerForIndex:(NSInteger)index {
// Replace UIVIewController with your custom classes
if (index == 0){
UIViewController *child = [self.storyboard instantiateViewControllerWithIdentifier:#"STORYBOARD_ID_1"];
return child;
} else {
UIViewController *child = [self.storyboard instantiateViewControllerWithIdentifier:#"STORYBOARD_ID_2"];
return child;
}
}
// Use this method to change anything you need on the tabs, like making the active tab a different colour
- (void)setupTabForIndex:(NSInteger)index{
}
// This will recognize taps on the tabs so the change can be done
- (IBAction)tapDetected:(UITapGestureRecognizer *)gestureRecognizer {
[self changeToControllerWithIndex:gestureRecognizer.view.tag];
}
Finally, each view you create that represents a tab should have it's own TapGestureRecognizer and a number for its tag.
By doing all this you will have a single controller with the buttons you need (they don't have to be reusable), you can add as much functionality you want in them (that's what the setupTabBarForIndex: method will be used) and you won't violate DRY.
Right now I have a UIButton setup in my storyboard that pushes to a Table View Controller. This is working as expected. What I am trying to do is have the UIButton load an xml file when it is pressed and still move onto the Table View Controller.
How can I do this? I already have the code the XML pieces, it's a question of where the code would go and how I would add the .h and .m files for a new ViewController subclass UIViewController. How do I link those files to the UIButton I've created in storyboard?
Drag your button connection to the new view and select custom Segue instead of Push or Modal.
Change the new custom Segue's class to "mySegueClass1" or what ever you'd like to call it.
Create a new Objective-C class with the same name as you just assigned to the custom segue.
Then inside your mySegueClass1.m file add the following code, and add what ever additional actions you want to -(void)perform
-(void)perform{
UIViewController *dst = [self destinationViewController];
UIViewController *src = [self sourceViewController];
[dst viewWillAppear:NO];
[dst viewDidAppear:NO];
[src.view addSubview:dst.view];
CGRect original = dst.view.frame;
dst.view.frame = CGRectMake(dst.view.frame.origin.x, 0-dst.view.frame.size.height, dst.view.frame.size.width, dst.view.frame.size.height);
[UIView beginAnimations:nil context:nil];
dst.view.frame = CGRectMake(original.origin.x, original.origin.y, original.size.height, original.size.width);
[UIView commitAnimations];
[self performSelector:#selector(animationDone:) withObject:dst afterDelay:0.2f];
}
- (void)animationDone:(id)vc{
UIViewController *dst = (UIViewController*)vc;
UINavigationController *nav = [[self sourceViewController] navigationController];
[nav popViewControllerAnimated:NO];
[nav pushViewController:dst animated:NO];
}
There is a method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//You can access the tableviewcontroller using segue.destinationViewController
//Maybe you want to set your model here.
}
That is called before a segue is made. Here you can do all the setup you need. You should place this method inside the controller that performs the segue, not the one receiving it. (In fact, probably xcode already put that piece of code for you).
I've got two classes. ManagingViewController, a subclass of NSViewController, and ViewController, a subclass auf ManagingViewController. In Viewcontroller I've got a NSTextField which I want to become the firstResponder, but I didn't manage that.
So it is nearly the same like the Chapter 29 in Hillegass' book Cocoa Programming for Mac OS X (Download of the book's examples) except of an NSTextField which is set to firstResponder.
Can anybody point me to the correct way?
You need to set the text field as the first responder by using -[NSWindow makeFirstResponder:].
Since this is an NSWindow method, it only makes sense after you’ve added the corresponding view to the window, i.e., after you’ve added the view as a subview inside the window view hierarchy. In the book’s example, this happens when you set the view as the content view of the box inside the window. For example:
- (void)displayViewController:(ManagingViewController *vc) {
// Try to end editing
NSWindow *w = [box window];
…
// Put the view in the box
NSView *v = [vc view];
[box setContentView:v];
// Set the first responder
if ([vc class] == [ViewController class]) {
[w makeFirstResponder:[(ViewController *)vc myTextField]];
}
}
This assumes ViewController exposes a getter method called -myTextField.
You can make this more generic by having your view controllers expose a method that returns the object that the view controller recommends as the first responder. Something like:
#interface ManagingViewController : NSViewController
…
- (NSResponder *)recommendedFirstResponder;
#end
#implementation ManagingViewController
…
- (NSResponder *)recommendedFirstResponder { return nil; }
#end
And, in your concrete subclasses of ManagingViewController, have -recommendedFirstResponder return the object that should be the window’s first responder:
#implementation ViewController
…
- (NSResponder *)recommendedFirstResponder { return myTextField; }
#end
Having done that, you can change your -displayViewController: to something like:
- (void)displayViewController:(ManagingViewController *vc) {
// Try to end editing
NSWindow *w = [box window];
…
// Put the view in the box
NSView *v = [vc view];
[box setContentView:v];
// Set the first responder
NSResponder *recommendedResponder = [vc recommendedFirstResponder];
if (recommendedResponder) [w makeFirstResponder:recommendedResponder];
}
Have you tried [[myTextField window] makeFirstResponder:myTextField]; ?
simple. Goto you xib file in interface builder. right click the first responder field. it will show the connection , remove the connection and connect it to the desired responder. let me know if this works