my requirement is when i swipe from right of the screen(viewcontroller A) then need to push to next viewcontroller(viewcontroller B) using interactive transition.Use the same mechanism for dismiss also, when i swipe from left of the screen(viewcontroller B) it dismiss the controller using interactive transition. How can wen implement it in right way .I have implemented dismiss a viewcontroller using interactive transition but can't able to implement pushing to a viewcontroller using interactive transition
#import "AMSimpleAnimatedDismissal.h"
#implementation AMSimpleAnimatedDismissal
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 1.5f;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[[transitionContext containerView]addSubview:toVC.view];
CGRect toFrame = toVC.view.frame;
CGRect initialFrame = fromVC.view.frame;
toVC.view.frame = CGRectMake(-320 ,0, CGRectGetWidth(toFrame) , CGRectGetHeight(toFrame));
CGRect finalFrame = CGRectMake(initialFrame.size.width, initialFrame.origin.y, initialFrame.size.width, initialFrame.size.height);
UIViewAnimationOptions opts = UIViewAnimationOptionCurveLinear;
[UIView animateWithDuration:1.0 delay:0 options:opts animations:^{
fromVC.view.frame = finalFrame;
toVC.view.frame = CGRectMake(0, 0, CGRectGetWidth(fromVC.view.frame), CGRectGetHeight(fromVC.view.frame));
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
these are my line of code for interactive transition section
It is possible to set the swipe gesture's direction: when the action method gets called, simply check the swipe's direction and if the direction is UISwipeGestureRecognizerDirectionRight then use [self.navigationController popViewControllerAnimated:BOOL]. You may want to add a "isPop" BOOL property to AMSimpleAnimatedDismissal and let this class to handle both presentation and dismissal.
If doing interactive custom transitions with with a navigation controller, you must:
Specify the delegate for your UINavigationController
Your UINavigationControllerDelegate must specify the animation controller in the following delegate protocol method:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
If you want the push to be interactive you have to specify an interaction controller, too, in the following method:
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
This method should generally return a UIPercentDrivenInteractiveTransition if initiated by gesture, and return nil if not.
You must add the gesture recognizer (e.g., a UISwipeFromEdgeGestureRecognizer) on the view, which would initiate the transition and update the UIPercentDrivenInteractiveTransition returned by the above interactionControllerForAnimationController method, and complete or cancel the transition when the gesture was done.
Related
I got an gridview. Each cell within that grid is clickable. If a cell is clicked, another viewcontroller must be presented as a modal viewcontroller. The presentedviewcontroller must slide in fro the right to the left. After that, the modalviewcontroller can be dismissed with a slide. How do i achieve this? I got some images to show it :
Both views are separate viewcontrollers.
[Solution]
The answer from Matthew pointed me in the right direction. What i needed was a UIPanGestureRecognizer. Because UISwipeGestureRecognizer only registers one single swipe and i needed the view to follow the users finger. I did the following to accomplish it :
If i cell is tapped inside my UICollectionView, the extra view needs to pop up. So i implemented the following code first :
/* The next piece of code represents the action called when a touch event occours on
one of the UICollectionviewCells.
*/
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
NSString* release_id = releases[indexPath.row][0];
// Next boolean makes sure that only one new view can be seen. In the past, a user can click multiple cells and it allocs multiple instances of ReleaseViewController.
if(releaseViewDismissed) {
// Alloc UIViewController and initWithReleaseID does a request to a server to initialize some data.
ReleaseViewController *releaseViewController = [[ReleaseViewController alloc] initWithReleaseID: release_id];
// Create a new UIView and assign the height and width of the grid
UIView *releaseViewHolder = [[UIView alloc] initWithFrame:CGRectMake(gridSize.width, 0, gridSize.width, gridSize.height)];
// Add the view of the releaseViewController as a subview of the newly created view.
[releaseViewHolder addSubview:releaseViewController.view];
// Then add the UIView with the view of the releaseViewController to the current UIViewController's view.
[self.view addSubview:releaseViewHolder];
// Place the x coordinate of the new view to the same as width of the screen. Then after that get the x to 0 with an animation.
[UIView animateWithDuration:0.3 animations:^{
releaseViewHolder.frame = CGRectMake(0, 0, releaseViewHolder.frame.size.width, releaseViewHolder.frame.size.height);
// This is important. alloc an UIPanGestureRecognizer and set the method that handles those events to handleSwipes.
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleSwipes:)];
// Add the UIPanGestureRecognizer to the created view.
[releaseViewHolder addGestureRecognizer:_panGestureRecognizer];
releaseViewDismissed = NO;
}];
}
}
Then my handleSwipes is as follows:
-(void)handleSwipes:(UIPanGestureRecognizer *)sender {
CGPoint translatedPoint = [(UIPanGestureRecognizer*)sender translationInView:self.view];
CGPoint translation = [sender translationInView:sender.view];
CGRect newFrame = [sender view].frame;
[sender setTranslation:CGPointZero inView:sender.view];
if (sender.state == UIGestureRecognizerStateChanged)
{
newFrame.origin.x = newFrame.origin.x + translation.x;
// Makes sure it can't go beyond the left of the screen.
if(newFrame.origin.x > 0) {
[sender view].frame = newFrame;
}
}
if(sender.state == UIGestureRecognizerStateEnded){
CGRect newFrame = [sender view].frame;
CGFloat velocityX = (0.3*[(UIPanGestureRecognizer*)sender velocityInView:self.view].x);
// If the user swipes less then half of the screen, it has to bounce back.
if(newFrame.origin.x < ([sender view].bounds.size.width/2)) {
newFrame.origin.x = 0;
}
// If a user swipes fast, the velocity is added to the new x of the frame.
if(newFrame.origin.x + velocityX > ([sender view].bounds.size.width/2)) {
newFrame.origin.x = [sender view].bounds.size.width + velocityX;
releaseViewDismissed = YES;
}
// Do it all with a animation.
[UIView animateWithDuration:0.25
animations:^{
[sender view].frame = newFrame;
}
completion:^(BOOL finished){
if(releaseViewDismissed) {
// Finally remove the new view from the superView.
[[sender view] removeFromSuperview];
}
}];
}
}
If you want the presented view controller to slide in from the right to the left, it cannot be a modal view. #Juan suggested one way to achieve the right to left and swipe back, but it would result in the grid view being pushed out of the way by the new view. If you would like the new view to cover the grid view when it slides in, you will either need to accept the vertical slide of modal views or write your own code to slide the view in from the right -- the latter would not actually be all that difficult*.
As for the swipe to get back, the easiest way to do that from either a modally presented view or a view you animate in yourself is to use a UISwipeGestureRecognizer. You create the recognizer, tell it what direction of swipe to look for, and you tell it what method to call when the swipe occurs.
*The gist of this approach is to create a UIView, add it as a subview of the grid view, and give it the same frame as your grid view but an x-position equal to the width of the view, and then use the following code to make the view animate in from the right.
[UIView animateWithDuration:0.3 animations:^{
slidingView.frame = CGRectMake(0, 0, slidingView.frame.size.width, slidingView.frame.size.height);
}];
I believe what you need is the following:
Create another controller that is going to handle navigation between these two (ContentViewController for example). This controller should have a ScrollView with paging enabled.
Here is a simple tutorial if you don´t already know how to do this: click here
Once the cell is clicked you have to:
Create the new ViewController to be shown.
Enable paging and add this ViewController to the ContentViewController
Force paging to this newly created ViewController
Additionally you have to add some logic so that when the user swipes to change back to the first page, paging is disabled until a new cell is clicked to repeat the process.
I've added a UIView (which contains the UIImageView for the background, three UIButtons which say "Test" and the final UIButton to dismiss the view called "Finished") as a subview of my UIActionSheet.
Why won't any of these buttons detect touches? My UIView has User Interaction Enabled checked.
I'd appreciate some help with this as I've been pulling my hair out (not literally of course)!
Here's my set up:
If you're just looking mimic the UIActionSheet, I'd just create a UIVew subclass called something like "CoolActionSheet" and programatically place the buttons on there. Then, when you press the buttons it triggers a delegate method in a protocol which will be implemented in your main view controller so do something.
To show and hide the action picker use UIView animations in the CoolActionSheet class like so:
-(void)showSheet {
NSLog(#"Showing sheet...");
//Set the x/y position of the action sheet to JUST off-screen
CGFloat xPos = parentView.frame.origin.x;
CGFloat yPos = parentView.frame.size.height+kActionSheetHeight;
[self setFrame:CGRectMake(xPos, yPos, kActionSheetWidth, kActionSheetHeight)];
/*Here is where you would add your other UI objects such as buttons
and set their #selector to a method in your CoolActionSheet protocol. You could then implement this delegate method in
your main view controller to carry out a custom action. You might also want to add a background image to the view or something else.
For example: */
UIButton *coolButton = [[UIButton alloc] initWithFrame:buttonDimensions];
[coolButton addTarget:self action:#selector(didDismissActionSheet) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:coolButton];
[self.parentView addSubview:self.view];
//Slide the sheet up from the bottom of the screen
[UIView animateWithDuration:0.2 animations:^(void) {
//Slide banner in from left to right
[self setFrame:CGRectMake(0, yPos-kActionSheetHeight, kActionSheetWidth, kActionSheetHeight)];
}];
}
And to hide:
-(void)hideSheet {
NSLog(#"Hiding");
CGFloat xPos = parentView.frame.origin.x;
CGFloat yPos = parentView.frame.size.height+kActionSheetHeight;
[UIView animateWithDuration:0.2 animations:^(void) {
[self setFrame:CGRectMake(xPos, yPos, 320, 65)];
}completion:^(BOOL finished) {
[self removeFromSuperview]; //Clean up
}];
}
You may also want to grey-out the parent view. Again, in the CoolActionSheet.m:
-(void)shadeParentView {
UIView *shadedView = [[UIView alloc] initWithFrame:CGRectMake(, 0, 320, 480)];
[shadedView addGestureRecognizer:gestureRecognizer];
[shadedView setBackgroundColor:[UIColor blackColor]];
[shadedView setAlpha:0.0];
[self addSubview:shadedView];
[UIView animateWithDuration:0.5 animations:^{
[shadedView setAlpha:0.5];
}];
}
You would need to set your parentView to the view controller's view. So, to call the action sheet in your main view controller you would:
CoolActionSheet *coolSheet = [[CoolActionSheet alloc] init];
[coolSheet setParentView:self.view];
[coolSheet setSheetDelegate:self]; //set the delegate to implement button press methods in this view controller
This might seem a bit long-winded, but it's a good MVC pattern to separate it out into another view class like this. And you now have a custom class that you can just import into any other project and it'll work!
I haven't had a chance to test out this specific code, but the whole approach is good. Points to take away from this:
Use a custom UIView class
Implement delegate methods to execute tasks in your main view controller when a button is pressed in your subview.
Implement a good MVC structure to avoid spaghetti code.
Let me know if this helps :)
Try This:
[actionSheet showInView:self.view.window];
From all of the view controllers within my application if I am processing a long running task I present the user a 'progress view'. This is a UIView that lives in my MainWindow.xib. I show (fade in) the view using an AppDelegate method...
- (void)showProgressView
{
self.progressView.hidden = NO;
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 1.0; }];
}
When the long running task has finished I fade out the 'progress view' with the following AppDelegate method...
- (void)hideProgressView
{
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 0.0; }
completion:^(BOOL f) { self.progressView.hidden = YES; }
}];
}
My problem is that as the progress view fades away and as the buttons/controls in the under-lying view below become visible again they ARE NOT usable (they don't respond to touch events) until the animation has fully finished and the 'progress view' is hidden.
Is there anyway for me to pass control back to the underlying view, before starting the fade out animation, so that its buttons etc do work whilst the progress view fades away?
EDIT: Things I have already tried unsuccessfully...
Using resignFirstResponder
- (BOOL)findAndResignFirstResponder:(UIView*)view
{
NSLog(#"looping %#", [view description]);
if (view.isFirstResponder)
{
[view resignFirstResponder];
return YES;
}
for (UIView *subView in view.subviews)
{
if ([self findAndResignFirstResponder: subView])
{
return YES;
}
}
return NO;
}
- (void)hideProgressView
{
// Recursively attempt to remove control from the progress view
BOOL result = [self findAndResignFirstResponder:self.progressView];
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 0.0; }
completion:^(BOOL f) { self.progressView.hidden = YES; }
}];
}
Using endEditing
- (void)hideProgressView
{
// Attempt to remove control from the progress view
[self.progressView endEditing:YES];
[UIView animateWithDuration:1.0
animations:^(void) { self.progressView.alpha = 0.0; }
completion:^(BOOL f) { self.progressView.hidden = YES; }
}];
}
You can try sending your progress view the message resignFirstResponder. This should make the view below it the first responder, so you can use its controls.
PS: I also think that maybe your progress view could be filling the whole display; in this case, changing the first responder might not help...
EDIT: after you confirmed that your view is taking the full screen...
If your view is full screen, it is intercepting all the touches that you do (because when it is not fully hidden/transparent it is covering the views behind it). You have two options, either you make the view smaller, so that you have no overlapping, or you make so that the touches are forwarded to the view behind it.
You can do the latter in several ways, I hope that one works for you:
you can try and override in your progress view (it needs be a custom UIView), the touchesBegan method;
you can try and override the hitTest method in your progress view;
This is what I would try, e.g. in touchesBegan:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (![self viewIsDisappearing])
[self.nextResponder touchesBegan:touches withEvent:event];
}
viewIsDisappearing is a method you should implement to return YES if the animation to hide the progress view has already begun. During the animation the view is not yet hidden, so it will intercept touches, and you forward those touches to the next responder.
It is possible that you also need to override the other UIResponder's touche-related methods:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
– touchesCancelled:withEvent:
EDIT:
I have found a class of mine where I do something similar to what I am suggesting here, only, without using the nextResponder.
The idea is: SDSTransparentView is a UIView that covers the whole screen. You initialize it like this:
[[SDSTransparentView alloc] initWithContent:contentView andDelegate:delegate];
The delegate implements an SDSTransparentViewProtocol which simply contains one method:
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event outsideOfView:(UIView*)view;
When the user touches anywhere on the transparent view, it relays the touch to the delegate by calling the protocol method. I would suggest you to ignore the contentView and outsideOfView arguments (they where useful for me, but possibly not for you; you can either pass nil or, better, the view behind the progress view).
You can find the class on my github. You only need the SDSTransparentView.* files. Actually, I only suggest having a look at how the class is implemented (very short) and do the same in your progress view.
I can assure that this approach works!
+ (void) AnimateSwitchingWithParent: (UIViewController *) ParentController From: (UIViewController *) From To: (UIViewController* ) To {
To.view.frame = ParentController.view.bounds;
[UIView transitionFromView:From.view toView:To.view
duration:1
options:UIViewAnimationOptionTransitionFlipFromLeft completion:NULL];
ParentController.view = To.view;
[To viewWillAppear:true];
}
this is my function to make animation that will be called if want to change view from 1 view to another view, but I have problem, I have a viewController named filter, at there I have Button called Reset that will reset all of content inside
but to show that the reset have done, I want to call
[self AnimateSwitchingWithParent:self From:self To:self];
but the result is My View was blank. how can it be? any one have another way?
If you have to flip the current view controller's view while resetting content, use the transitionWithView:duration:options:animations:completion: method.
Example
[UIView transitionWithView:self.view
duration:1.0f
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^(void) {
[self resetStuff];
}
completion:nil];
Side Note
You current implementation in the question will be useful to switching views in the view hierarchy. Say From.view is the subview of ParentController.view then the statement below will replace From.view with To.view as the subview of ParentController.view.
[UIView transitionFromView:From.view toView:To.view
duration:1
options:UIViewAnimationOptionTransitionFlipFromLeft completion:NULL];
You definitely don't need to do this,
ParentController.view = To.view;
Doing that will replace the ParentController's view which might be different from what you intended and definitely in contradiction to the transition animation on the line before.
You must definitely not be calling,
[To viewWillAppear:true];
directly.
To my understanding you should be able to remove the last two lines.
When in landscape, transitioning from one view (that's part of a Navigation Controller stack) to another as a modal view, with UIModalTransitionStyleFlipHorizontal set as the modalTransitionStyle, the view flips vertically in landscape mode.
Everything else about the look of the views is fine after the animation, though I did notice that the frame size of the views isn't changing which is causing issues in other places of my code as well. I figured if I fix whatever is making this particular flip vertical instead of horizontal, it will fix the other issue.
I assume it has something to do with the window itself not changing orientation, but I'm not sure that's it.
Anyone have any ideas?
Talked to an Apple Engineer at WWDC and figured out the UIModalTransitionStyleFlipHorizontal does not work in landscape, it will flip what looks like in vertical.
The other issue I mentioned was because I wasn't adapting a frame to the view correctly.
There is a solution for this if you use iOS7 custom view controller transition. The ViewController which initiates the transition should confirm to protocols an implement the following methods.
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController: (UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return (id<UIViewControllerAnimatedTransitioning>)self;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return (id<UIViewControllerAnimatedTransitioning>)self;
}
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return 0.7f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UIView *containerView = [transitionContext containerView];
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[containerView addSubview:fromVC.view];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[containerView addSubview:toVC.view];
UIViewAnimationOptions animationOption;
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad && UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
animationOption = ([toVC.presentedViewController isEqual:fromVC])?UIViewAnimationOptionTransitionFlipFromTop:UIViewAnimationOptionTransitionFlipFromBottom;
}
else {
animationOption = ([toVC.presentedViewController isEqual:fromVC])?UIViewAnimationOptionTransitionFlipFromLeft:UIViewAnimationOptionTransitionFlipFromRight;
}
[UIView transitionFromView:fromVC.view
toView:toVC.view
duration:[self transitionDuration:transitionContext]
options:animationOption
completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
The transitioning delegate of the modal ViewController to be displayed should be set like this:
[modalViewController setTransitioningDelegate:self];
For example this linke can be put in the prepareForSegue: method.
That's it.