Why is animated UIButton's position reset when clicked? - ios

Short description: When the keyboard is shown I translate a button up, but upon clicking the button it pops back down to its original position.
Long description: I have two buttons, one on top of the other. One is hidden until certain criteria are met, then it becomes visible and the other is hidden.
#property (weak, nonatomic) IBOutlet UIButton *skipButton;
#property (weak, nonatomic) IBOutlet UIButton *saveButton;
#property (assign, nonatomic) BOOL saveButtonEnabled;
#property (assign, readonly, nonatomic) CGRect fieldContainerDefaultFrame;
#property (assign, readonly, nonatomic) CGRect skipButtonDefaultFrame;
When view loads I cache the default positions
- (void)viewDidLoad
{
[super viewDidLoad];
...
_fieldContainerDefaultFrame = self.fieldContainerView.frame;
_skipButtonDefaultFrame = self.skipButton.frame;
[self.saveButton setHidden:YES];
self.saveButtonEnabled = NO;
}
The buttons are wired to outlets correctly and these are their "touch up inside" actions
#pragma mark - Actions
- (IBAction)didPressSaveButton:(id)sender
{
// Code here to persist data
// Code here to push segue to next scene
}
- (IBAction)didPressSkipButton:(id)sender
{
// Code here to push segue to next scene
}
Here are the methods that respond to the keyboard notifications. I do register for the notifications I'm just not including that code. (There is no issue there, all this stuff gets run correctly.)
NOTE: Both buttons are contained in the same "container" frame (along with other fields), which translates up. The buttons translate up an additional amount. The translation works in general, the only issue is the reseting behavior of the button upon click.
#pragma mark - Notification handlers
- (void)handleKeyboardWillShowNotification:(NSNotification *)note
{
if([self.bioTextView isFirstResponder])
{
CGRect newFieldContainerFrame = self.fieldContainerDefaultFrame;
CGRect newSkipButtonFrame = self.skipButtonDefaultFrame;
newFieldContainerFrame.origin.y += AGSTSignupDetailsFieldContainerEditingOffset;
newSkipButtonFrame.origin.y += AGSTSignupDetailsSkipButtonEditingOffset;
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve animationCurve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
[UIView animateWithDuration:duration animations:^{
[UIView setAnimationCurve:animationCurve];
self.fieldContainerView.frame = newFieldContainerFrame;
self.skipButton.frame = newSkipButtonFrame;
self.saveButton.frame = newSkipButtonFrame;
}];
}
}
- (void)handleKeyboardWillHideNotification:(NSNotification *)note
{
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve animationCurve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
[UIView animateWithDuration:duration animations:^{
[UIView setAnimationCurve:animationCurve];
self.fieldContainerView.frame = self.fieldContainerDefaultFrame;
self.skipButton.frame = self.skipButtonDefaultFrame;
self.saveButton.frame = self.skipButtonDefaultFrame;
}];
}
I'd appreciate any insights or suggestions as to how to fix the bug or how to craft a better solution. Thanks.

Okay, I fixed the problem. While I appreciate rob mayoff's response as an indication of better practices (i.e., don't set frames when using autolayout), that was not the actual cause of the problem in my case, thus I don't consider my Q&A to be a duplicate of anything I could find. I'm going to post this answer in case someone else runs across a similar problem.
It turns out, as I suspected earlier this morning, that the button's push segue did two mutually incompatible things: (1) it caused the keyboard to disappear, which caused the fields to be translated downward; and (2) it caused the whole scene to animate away as the new VC comes into place, which apparently cancelled all pending animations (i.e., the translation) and caused those translations to abruptly jump to their destination states.
The quickest solution was to set a BOOL didPressSaveButton to NO on viewDidLoad and only set it to YES once the button is pushed, and then check that BOOL before every relevant translation animation: if the BOOL is NO, go ahead and do the animation, otherwise do nothing. This will prevent the abrupt final animation before the push segue.
The better solution, and one I will implement soon, will be to replace my use of frames with autolayout constraints.

Related

How can I make a UITextField the first responder, correctly? (Tried 3 methods)

Here's the setup:
SNController is an NSObject and it has inside:
UIView
UITextField
UISegmentedControl
UILabel
UIButton
The view is the same as the view inside the ViewController.
One can be visible at the time. When the UITextField is visible (alpha = 1.0)
all the others are invisible (alpha = 0.0).
(1) I perform an animation and I want the text field:
UITextField *textField;
to become the first responder while the animation happens:
[textField becomeFirstResponder];
[UIView animateWithDuration: ...];
Unfortunately it doesn't work.
I've logged the text field and it showed that it cannot become the first responder and I have no idea why...
I've tried another two methods but none of them worked so far:
(2) A complicated way to communicate with the ViewController from the SNController:
Inside the SNController.h/.m:
#property (nonatomic, copy) void(^eventCallBack)();
if (self.eventCallBack) {
self.eventCallBack();
}
Inside the ViewController.m:
__weak ViewController *self_ = self;
self.restoration.controller.eventCallBack = ^(){
[self_.restoration.controller.textField becomeFirstResponder];
};
(3) I also tried these methods from inside the SNController:
[self.textField performSelector:#selector(becomeFirstResponder) withObject:nil afterDelay:0.0];
[self.textField performSelectorOnMainThread:#selector(becomeFirstResponder) withObject:nil waitUntilDone:YES];
It just won't give up, it doesn't become the first responder no matter what.
Any ideas?
I realised there was a mistake. There isn't a UITextField inside the SNController but a UIView subclass named SNTextField.
I implemented this method where the UITextField was initialised:
- (void)activateKeyboard
{
[self.textField becomeFirstResponder];
}
and it worked!

How to setup Container View so as to swap two children

I am having what seems like a typical Container View problem in iOS. I have a ViewController with two subviews: a UISegmentedControl and a Container View. Now having placed my Container View, in the storyboard, I am not sure how to proceed. Naturally I thought my next step was to subclass UIContainerView to do all the stuff that I read in the iOS Documentation. But there is no such class as UIContainerView. So now, beyond what I was able to place in the storyboard, I am stuck. Hoping someone can help me I will posit what seems like a simple scenario.
Imagine:
One ViewController with two buttons (Cat, Dog) and a ContainerView.
When user clicks on catButton, then the ContainerView should show the CatViewController (and do similarly for dogButton)
Image that already I have the storyboard setup.
For simplicity, let CatViewController contain a single UILabel with the word CAT (and similarly for DogViewController).
also, in the storyboard, I have already created CatViewController and DogViewController as two stand-alone, unreachable, View Controllers.
So at this point, how do I proceed? Since I cannot subclass such a class as UIContainerView, what do I do?
I believe this scenario is simple enough for someone to provide an example, but if you deem it too complicated, please provide an example to yet a simpler scenario. I just want to see how a simple one is done.
P.S. I have already taken a tour here on StackOverflow, such as:
Swapping child views in a container view
and I have already read the docs at https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/CreatingCustomContainerViewControllers/CreatingCustomContainerViewControllers.html#//apple_ref/doc/uid/TP40007457-CH18-SW6
I think is better use an UISegmentedControl instead two UIButtons.
The container view subviews (_vwContainer.subviews) contains initially the CatViewController's view, automatically instantiated.
// ViewController.m
#import "ViewController.h"
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIView *vwContainer;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_vwContainer.clipsToBounds = YES;
}
- (IBAction)onSegmentValueChanged:(UISegmentedControl *)sender {
NSLog(#"Value changed to: %zd",sender.selectedSegmentIndex);
NSLog(#"BEFORE: self.childViewControllers: %#",self.childViewControllers);
NSLog(#"BEFORE: _vwContainer.subviews: %#",_vwContainer.subviews);
// set oldVC & newVC
UIViewController *oldVC = self.childViewControllers.firstObject;
NSString *strIdNewVC;
switch (sender.selectedSegmentIndex) {
case 0: strIdNewVC = #"catVC"; break;
default: strIdNewVC = #"dogVC";
}
UIViewController *newVC = [self.storyboard instantiateViewControllerWithIdentifier:strIdNewVC];
//
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Prepare animation transition, for example left to right
newVC.view.frame = oldVC.view.frame;
CGPoint pntEnd = oldVC.view.center;
CGPoint pntInit = pntEnd;
pntInit.x += oldVC.view.frame.size.width;
newVC.view.center = pntInit;
[self transitionFromViewController:oldVC toViewController:newVC
duration:0.25 options:0
animations:^{
newVC.view.center = pntEnd;
} completion:^(BOOL finished) {
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
NSLog(#"AFTER: self.childViewControllers: %#",self.childViewControllers);
NSLog(#"AFTER: _vwContainer.subviews: %#",_vwContainer.subviews);
}];
}
#end

textFieldDidBeginEditing: not getting called

I got the code below from this SO question. I'm trying to slide up textfields when I begin editing (because they otherwise get covered by the iPhone keyboard). However, the log statement reveals that the textFieldDidBeginEditing method is not getting called.
I have the code below in two different subclasses of UIViewController. In one of them, for example, I have a textfield connected from the storyboard to the UIViewController like this
#property (strong, nonatomic) IBOutlet UITextField *mnemonicField;
I moved the textfield up to the top of the view (i.e. not covered by keyboard) so that I could edit it to try to trigger the log statement but it didn't work. The text field otherwise works as expected i.e. the data I enter is getting saved to coreData etc etc.
Can you explain what I might be doing wrong?
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
NSLog(#"The did begin edit method was called");
[self animateTextField: textField up: YES];
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
[self animateTextField: textField up: NO];
}
- (void) animateTextField: (UITextField*) textField up: (BOOL) up
{
const int movementDistance = 180; // tweak as needed
const float movementDuration = 0.3f; // tweak as needed
int movement = (up ? -movementDistance : movementDistance);
[UIView beginAnimations: #"anim" context: nil];
[UIView setAnimationBeginsFromCurrentState: YES];
[UIView setAnimationDuration: movementDuration];
self.view.frame = CGRectOffset(self.view.frame, 0, movement);
[UIView commitAnimations];
}
You have not assigned your delegate of UITextField in your ViewController class:
In your viewcontroller.m file, In ViewDidLoad method, do this:
self.mnemonicField.delegate=self;
In your viewcontroller.h file, do this:
#interface YourViewController : ViewController<UITextFieldDelegate>
You should set up text field delegate to self.
Add this line to viewDidLoad method:
self.mnemonicField.delegate = self;
and remember to add this line <UITextFieldDelegate> to conform to that protocol.
You can achieve the same effect in storyboard by control drag from desired UITextField to view controller and select delegate.
You created IBOutlet so just drag your textfield to viewController and set delegate
Then in .h add the following
#interface ViewController : ViewController<UITextFieldDelegate>
In other case if you click any other button without moving the focus of Uitextfield then delegate will not be called, for that you need to explicitly call
yourtextfield.resignFirstResponder()

UIView Flip Animation with Core Animation

Dear Core Animation/iOS Experts,
I am trying to get an effect similar to the question below.
Core animation animating the layer to flip horizontally
The accepted answer seems to describe exactly what I require, except there is no code with this answer and I can't get any code I write to work.
Here is exactly what I'm trying to do:
1) Have 1 master view controller/view and on a portion of the main view have 2 UIViews which overlap, with only one shown (1 at the 'front' and 1 at the 'back')
2) A separate UIControl/UIButton gets pressed and then a 3D flip transition occurs which rotates the front (visible) view out of view and at same time rotates the back (hidden) view to the front...just like seeing the reverse of a playing card.
3) Be able to keep pressing the UIControl to toggle between the two views
4) Be able to interact with just the controls on the front view (i.e. pressing the front layer won't inadvertently fire a control on the back which happens to lie underneath the tap)
I may be approaching this the wrong way so let me know. Ideally, I would like to use Core Animation, and not the UIView built in flip transactions, as I want the animation to be 3D and also want to use this task as a stepping stone for doing more complex CA stuff.
At the moment I can get the front view to flip nicely (only once) but the back view doesn't show.
Cheers,
Andy
Here is the code I've got:
MainViewController.h
#interface MainViewController : UIViewController
- (IBAction)changeViewTapped:(UITapGestureRecognizer *)recognizer;
#end
MainViewController.m
#import "MainViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface MainViewController ()
#property (weak, nonatomic) IBOutlet UIView *detailView;
#property (weak, nonatomic) IBOutlet UIView *listView;
#property (nonatomic) CATransform3D rotationAndPerspectiveTransform;
#end
#implementation MainViewController
#synthesize detailView = _detailView;
#synthesize listView = _listView;
#synthesize rotationAndPerspectiveTransform = _rotationAndPerspectiveTransform;
- (void)viewDidLoad {
[super viewDidLoad];
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -500;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, M_PI, 0.0f, 1.0f, 0.0f);
self.rotationAndPerspectiveTransform = rotationAndPerspectiveTransform;
CALayer *listLayer = self.listView.layer;
listLayer.doubleSided = NO;
listLayer.transform = self.rotationAndPerspectiveTransform;
}
- (IBAction)changeViewTapped:(UITapGestureRecognizer *)recognizer {
CALayer *detailLayer = self.detailView.layer;
CALayer *listLayer = self.listView.layer;
detailLayer.doubleSided = NO;
listLayer.doubleSided = NO;
[UIView animateWithDuration:0.5 animations:^{
detailLayer.transform = self.rotationAndPerspectiveTransform;
listLayer.transform = self.rotationAndPerspectiveTransform;
} completion:^(BOOL finished){
// code to be executed when flip is completed
}];
}
#end
A quick tip about the flip animation in iOS -- the system needs time to render the back view (the one that will be flipped to) before you start the flip animation. So if you try to change the contents of that view, and trigger off the flip animation in the same method, you will have problems.
To get around this, I've had success with code like this:
- (void)flipView {
// Setup the view for the back side of the flip
[self performSelector:#selector(performFlip) withObject:nil afterDelay: 0.1];
}
- (void)performFlip {
[UIView transitionWithView: tileToFlip
duration: 0.5
options: UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
// My flip specific code
}
completion:^(BOOL finished) {
}
];
}
In a nutshell, I'm setting everything up in the flipView method, returning control to iOS so it has time to do it's rendering, then kicking off the flipTile selector after a tenth of a second to do the actual animation.
Good luck!
- (void)perform
{
UIViewController *src = (UIViewController *) self.sourceViewController;
UIViewController *dst = (UIViewController *) self.destinationViewController;
[UIView beginAnimations:#"LeftFlip" context:nil];
[UIView setAnimationDuration:0.8];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:src.view.superview cache:YES];
[UIView commitAnimations];
[src presentViewController:dst animated:NO completion:nil];
}

Objc (IOS) , simple free fall animation gone bad

I'm currently taking my first shaky steps in ios development. I'm trying to animate a free falling ball. I have a button connected to an IBAction, and and UIImageView containing an image of my ball.
Inside my action, I have a while loop that's timed using NSTimeInterval, and based on the time it takes it calculates a new position until the ball reaches 'the ground'(Yes, I realize this is probably the least optimal way to do it. But I'm having a hard time grasping the syntax (and therein my problem probably lays), the optimisation will have to come later). From what I can understand, NSTimeInterval returns the elapsed time in seconds, so even though it will increment incredibly small steps, it should work. But I may have a serious case of brain fart.
So far so good. But when I tap the button, the ball moves straight from it's starting point to it's finishing point without an animation.
-(IBAction)doshit:(id)sender{
int G = 10;
CGPoint center = [myImage center];
NSDate *startTime = [NSDate date];
NSTimeInterval T;
T = fabs([startTime timeIntervalSinceNow]);
while (center.y<480)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:T];
center.y=center.y+((G*T*T)/2);
[myImage setCenter:center];
T = fabs([startTime timeIntervalSinceNow]);
[UIView commitAnimations];
}
}
I welcome all suggestions! =)
One way to do it is to use a CADisplayLink to provide the timing -- it is tied to the display refresh rate of 1/60 of a second. If you 're using auto layout (which is on by default now), it is better to animate a constraint, rather then set a frame. So, this example uses a button at the top of the screen, whose constraint to the top of the view is connected to the IBOutlet topCon.
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (weak,nonatomic) IBOutlet NSLayoutConstraint *topCon;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(startDisplayLink) withObject:nil afterDelay:2];
}
- (void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink {
static BOOL first = YES;
static double startTime = 0;
if (first) {
startTime = displayLink.timestamp;
}
first = NO;
double T = (double)displayLink.timestamp - startTime;
self.topCon.constant += ((10 * T * T)/2);
if (self.topCon.constant > 420) {
[self stopDisplayLink];
}
}
As Carl notes, you cannot perform animations by repeatedly changing things in the middle of a method call. See What is the most robust way to force a UIView to redraw? for more discussion on that.
As you may suspect, not only is this non-optimal, it actively fights iOS. iOS has many easy-to-use techniques for performing smooth animations without resorting to timers of any kind. Here is one simple approach:
- (IBAction)dropAnimate:(id)sender {
[UIView animateWithDuration:3 animations:^{
self.circleView.center = CGPointMake(100, 300);
}];
}
In the animations block, you change the thing you want to change to the final value (myImage.center in your case). UIKit will sample the current value and the final value, and figure out a path to get you there in the time you requested. For a full, runnable example with a few other features (like chained animations), see the example code from iOS:PTL Chapter 9.
The above code will use the default timing function. Once you understand that, you can move on to customizing the timing function. See Animation Pacing in the Animation Types and Timing Programming Guide. Also How to create custom easing function with Core Animation? and Parametric acceleration curves in Core Animation. But I would get your head around simple, default animations first.

Resources