Can't interact with (some) UI elements in a ScrollView when the keyboard is displayed - objective-c

I've got a fairly basic interface that normally looks like this:
Fairly uninspired, but that's what the spec says to build. In any case, part of the issue is that when the onscreen keyboard pops up, this happens:
Now that's not a big issue by itself; I've got everything inside of a UIScrollView, and am using the following code to handle the keyboard showing and hiding:
- (void) moveTextViewForKeyboard:(NSNotification*)aNotification up: (BOOL) up{
NSDictionary* userInfo = [aNotification userInfo];
// Get animation info from userInfo
NSTimeInterval animationDuration;
UIViewAnimationCurve animationCurve;
CGRect keyboardEndFrame;
[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&animationCurve];
[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration];
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
// Animate up or down
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:animationDuration];
[UIView setAnimationCurve:animationCurve];
CGRect newFrame = referrerInfoView.frame;
CGRect keyboardFrame = [self.view convertRect:keyboardEndFrame toView:nil];
newFrame.size.height -= (keyboardFrame.size.height - 71) * (up? 1 : -1);
referrerInfoView.frame = newFrame;
referrerInfoView.contentSize = CGSizeMake(703, 633);
//FIXME: doesn't play nice with rotation when the keyboard is displayed
if (up && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
UIView* focusedField = [referrerInfoView findFirstResponder];
if (focusedField && focusedField.frame.origin.y > 340.0) {
referrerInfoView.contentOffset = CGPointMake(0.0, focusedField.frame.origin.y - 200.0);
}
}
else {
referrerInfoView.contentOffset = CGPointMake(0.0, 0.0);
}
[UIView commitAnimations];
}
Not the most robust thing in the world, but it does well enough for now. That gets me to here:
Now that's fine, except none of the elements in the shaded boxes respond to user interaction while the keyboard remains onscreen. The UIScrollView responds to interaction, as do all the other controls in the view. But all the 'address' fields stop working.
Essentially it seems that all the controls that were being hidden behind the keyboard before I scrolled them back into view still think that they're being hidden behind the keyboard. Any ideas on how to fix this?

I found the solution to this. Basically the layout that I originally had was like this:
UIScrollView
UIView
<Components>
The container view inside of the scrollview wasn't strictly necessary, but it didn't seem like it should hurt anything either. That assumption, it turns out, was incorrect. Long story short, I changed the interface so that it is structured like this:
UIScrollView
<Components>
And that solved the problem. Can't say I really understand why, but hopefully this information will help the next person who happens to run into this issue.

Related

Alternative to "self" in calling methods in Objective-C

This may sound really noob, but i've spent an entire day wrestling with this problem and would appreciate some help.
You see i have a method which I call more than once inside gameplay. If I use [self myMethod]; then it works for ONE time. And then when I call it again, the animation in the method doesn't commence anymore.
What I need is to replace "self" with an alternative that can be "alloc'ed" and "released" to make my animations work.
I've tried;
#implementation gameViewController
gameViewController *object = [[gameViewController alloc] init];
[object myMethod];
However the above substitute for self doesn't even call on the method. I don't know what I did wrong, it's suppose to work just like "self".
Is there something i missed? How do you make an object of the class to work just like "self" does?
Thanks so much.
Here is a more detailed look of my code;
[self explosionAnimations];
- (void) explosionAnimations
{
UIImage *image = [UIImage imageNamed: #"Yellow Explosion.png"];
[bomb setImage:image];
[UIView beginAnimations:#"bomb1ExplosionIncrease" context:NULL];
[UIView setAnimationDuration:0.5];
[UIView setAnimationDelegate:self];
bomb.transform = CGAffineTransformMakeScale(3.4, 3.4);
[UIView commitAnimations];
}
The setImage works fine every time. But the animations stop working on the second call of the method. While in the console it logs "animation completed" with nothing happening to the image.
This leads me to believe that somehow "self" believes that the animation was already done and will not bother to do it again. So I thought a new "alloc" might give it a kick awake.
The problem doesn't have anything to do with "self". The problem is that you set the transform in your animation, and then when you run it again, you're setting the same transform, so it does nothing. You need to reset the frame to the new frame and then set the transform back to the identity transform before you do the animation again. Also, you should be using block based animations. I'm not sure this is the best way to do it, but this worked for me (if you have auto layout turned off).
- (void)explosionAnimations {
UIImage *image = [UIImage imageNamed: #"Yellow Explosion.png"];
[self.bomb setImage:image];
[UIView animateWithDuration:.5 animations:^{
self.bomb.transform = CGAffineTransformMakeScale(3.4, 3.4);
} completion:^(BOOL finished) {
CGRect newFrame = self.bomb.frame;
self.bomb.transform = CGAffineTransformIdentity;
self.bomb.frame = newFrame;
}];
}
If you're doing this in an app with auto layout turned on (which it is by default), then I would not use a transform, but just resize the width and height of the image view by adjusting its height and width constraints. So, in this method, you should make IBOutlets to height and width constraints you make in IB, then change their constant values in an animation block:
[UIView animateWithDuration:.5 animations:^{
self.heightCon.constant = self.heightCon.constant * 3.4;
self.widthCon.constant = self.widthCon.constant * 3.4;
[self.view layoutIfNeeded];
}];

UIView animation not working with addSubview

I have an iPad app that is displaying a "note" (subclassed UILabel) with text on it. In order to navigate to the next note, I'd like it to slide it off the screen to the left while having the next one slide in from the right. I can get either animation to work, but not both at the same time.
Here's the code in my controller:
- (void)slideOutLeft {
// create the new note
flSlidingNote *newNote = [[flSlidingNote alloc] init];
newNote.text = #"blah blah";
CGRect newFrame = CGRectMake(1000, 70, 637, 297); // off the right
newNote.frame = newFrame;
[self.view addSubview:newNote];
// slide off the current one
CGRect currentFrameEnd = noteLabel.frame; // noteLabel is the existing note
currentFrameEnd.origin.x = 0 - noteLabel.frame.size.width; // off the left
[UIView animateWithDuration:1.0 animations:^{
noteLabel.frame = currentFrameEnd;
} completion:nil];
}
noteLabel does not animate at all. If I comment out the addSubview:newNote part it does. I'm still relatively new at this, so it's probably just something simple.
The problem happens whether newNote is animated or not (not animated in the code snippet).
You want to put the animation for both your views and the addSubview call for your new view in one animation block, like so:
// layout the new label like the old, but 300px offscreen right
UILabel * newNote = [[UILabel alloc] initWithFrame:CGRectOffset(self.noteLabelView.frame, 300, 0)];
newNote.text = #"NEW NOTE";
[UIView animateWithDuration:2.0f
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^{
// animate the old label left 300px to offscreen left
self.noteLabelView.center = CGPointMake(self.noteLabelView.center.x-300, self.noteLabelView.center.y);
// add the new label to the view hierarchy
[self.view addSubview:newNote];
// animate the new label left 300px into the old one's spot
newNote.center = CGPointMake(newNote.center.x-300, newNote.center.y);
}
completion:^(BOOL finished) {
// remove the old label from the view hierarchy
[self.noteLabelView removeFromSuperview];
// set the property to point to the new label
self.noteLabelView = newNote;
}];
In this snippet above, I'm assuming the old label is addressable via the property self.noteLabelView.
(Also have a look at https://github.com/algal/SlidingNotes , though I can't promise that will stay there long so maybe SO isn't the right format for such a link? )
Adding to what Gabriele Petronella commented, here is a good resource on KeyFrames and AnimationGroup, with a sample GitHub project linked:
http://blog.corywiles.com/using-caanimationgroup-for-view-animations
This really helped me understand animations better.
not sure but i've seen other code that loops through subviews and performs animations, you may want to try the following:
for(UIView *view in self.subviews)
{
// perform animations
}

UIScrollView not receiving scroll action

In the app I am working on, my goal right now is to scroll content when the keyboard shows and to allow users to scroll while it is showing. I have tried a few different solutions and none have been able to achieve this yet.
I'm using storyboards in the app and here is the element hierarchy within the view controller:
View Controller
UIScrollView
UIView
Buttons/textfields/labels/UIPickerView
I first set the UIScrollView's content size to be the same size of the view that was inside of it holding all of the form elements. When that didn't work, I tried over-exagerating the height of the content manually setting the content size to be 320 x 2000. Again, that didn't work. I have user interaction enabled set to YES on the scroll view as well. This is the code I have in there at this point.
CGSize contentSize = CGSizeMake(320, 2000);
[self.scrollView setContentSize:contentSize];
Within the scroll view I had a button that sits behind the whole form that has an action to close the keyboard if a user touches outside of it. I disabled that to see if it may have been a conflict in events that would keep it from scrolling. Again, didn't work.
-(IBAction)closeKeyboard:(id)sender
{
if(![self isFirstResponder]){
[self.view endEditing:YES];
}
}
I even set up some observers to see if the keyboard is about to appear or disappear. The observers would adjust the height of the scroll view, not the content size, just the scroll view itself, based on where the keyboard was currently sitting. So at this point, the content in the scroll view would be much taller than the scroll view itself, but still no scrolling is happening.
Here is the code for my observers:
// adjust view based on keyboard
- (void)keyboardWillHide:(NSNotification *)n
{
NSDictionary* userInfo = [n userInfo];
// get the size of the keyboard
CGSize keyboardSize = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
// resize the scrollview
CGRect viewFrame = self.view.frame;
// I'm also subtracting a constant kTabBarHeight because my UIScrollView was offset by the UITabBar so really only the portion of the keyboard that is leftover pass the UITabBar is obscuring my UIScrollView.
//viewFrame.size.height += keyboardSize.height;
CGRect scrollRect = CGRectMake(viewFrame.origin.x, viewFrame.origin.y, viewFrame.size.width, viewFrame.size.height + keyboardSize.height + 100);
[UIScrollView beginAnimations:nil context:NULL];
[UIScrollView setAnimationBeginsFromCurrentState:YES];
[UIScrollView setAnimationDuration:0.3];
[self.scrollView setFrame:scrollRect];
self.scrollView.userInteractionEnabled = YES;
[UIScrollView commitAnimations];
keyboardShowing = false;
}
- (void)keyboardWillShow:(NSNotification *)n
{
// This is an ivar I'm using to ensure that we do not do the frame size adjustment on the UIScrollView if the keyboard is already shown. This can happen if the user, after fixing editing a UITextField, scrolls the resized UIScrollView to another UITextField and attempts to edit the next UITextField. If we were to resize the UIScrollView again, it would be disastrous. NOTE: The keyboard notification will fire even when the keyboard is already shown.
if (keyboardShowing) {
return;
}
NSDictionary* userInfo = [n userInfo];
// get the size of the keyboard
CGSize keyboardSize = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
// resize the noteView
CGRect viewFrame = self.view.frame;
// I'm also subtracting a constant kTabBarHeight because my UIScrollView was offset by the UITabBar so really only the portion of the keyboard that is leftover pass the UITabBar is obscuring my UIScrollView.
CGRect scrollRect = CGRectMake(viewFrame.origin.x, viewFrame.origin.y, viewFrame.size.width, viewFrame.size.height - keyboardSize.height - 100);
//scrollView.frame.size.height -= keyboardSize.height;
//viewFrame.size.height -= keyboardSize.height;
[UIScrollView beginAnimations:nil context:NULL];
[UIScrollView setAnimationBeginsFromCurrentState:YES];
[UIScrollView setAnimationDuration:0.3];
[self.scrollView setFrame:scrollRect];
self.scrollView.userInteractionEnabled = YES;
[UIScrollView commitAnimations];
keyboardShowing = YES;
}
I would not be surprised if this is one of those simple mistakes that keeps slipping my mind, but this sort of accessibility feature would be really nice to have in the app. Any help would be much appreciated, or even other possible solutions to the problem I am trying to solve would be great too.
It looks like you're using a gesture recognizer in IB to detect a tap outside event. Because this recognizer is in the highest view in the hierarchy, it overrides the scrollview's detectors. You might need to change it slightly depending on which areas you want to be able to tap on to close the keyboard.
This is the UIResponder class reference. It lists all of the UIResponder events that your view controller automatically inherits. What might fix your problem is subclassing your UIScrollView and adding the keyboard closing code to it. Also, make sure that you set it to the first responder.
Final code that worked:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
mouseSwiped = NO;
UITouch *touch = [touches anyObject];
if ([touch tapcount] == 1)
for(UIView *view in self.view.subviews){
if([view isKindOfClass:[UITextField class]]){
[view resignFirstResponder];
}
}
}
}

Is there a recommended or even automatic way to move a UITextField into view when editing it?

Having implemented this now in various flavors, I wonder: if editing starts on a UITextField and the keyboard appears, is there a recommended or even automated way that would keep the textfield visible by scrolling it up?
I think it would be easiest and best to scroll up the whole root view. Is there something in the API I've been missing so far, that would save me from writing this code myself?
I sit all my UITextFields on a contentView (In my example I have called this view 'movableView') and then when the user taps one of the text fields
//The hardcoded 10's and 20's are the origin of the view before
//the user starts messing with it!
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
[self scrollViewToTextField:textField];
//Other stuff I want to do here
return YES;
}
- (void)scrollViewToTextField:(id)textField
{
UITextField* tf = (UITextField*)textField;
CGPoint newOffset = tf.frame.origin;
newOffset.x = 10;
newOffset.y = 20 - newOffset.y;
//This is a category method on UIView which simply adjusts the views
//frame over a delay.
[self.movableView moveToX:newOffset.x andY:newOffset.y withDuration:0.3f];
}
When the editing is finished you have to move the view back
-(void)textFieldDidEndEditing:(UITextField *)textField {
[self resetView];
// do other stuff here such as grab the text and stick it in ivars etc.
}
-(void)resetView {
[self.movableView moveToX:10.0f andY:10.0f withDuration:0.3f];
}
Just in case - here is the category method for completeness
// UIView+BasicAnimation.h
-(void) moveToX:(CGFloat) x andY:(CGFloat) y withDuration:(NSTimeInterval) duration;
// UIView+BasicAnimation.m
-(void) moveToX:(CGFloat) x andY:(CGFloat) y withDuration:(NSTimeInterval) duration {
CGRect newFrame = self.frame;
newFrame.origin.x = x;
newFrame.origin.y = y;
[UIView beginAnimations:#"BasicAnimation" context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:duration];
self.frame = newFrame;
[UIView commitAnimations];
}
I don't think any such thing exists and it seems like an odd omission. When you consider the existence of iPad split keyboards, it seems even more like something that should be done correctly once and provided in the API.
For a current project, I'm trying out TPKeyboardAvoidingScrollView. (https://github.com/michaeltyson/TPKeyboardAvoiding)
It's common to implement this by using an UIScrollView and modifying the content offset when a field gains firstResponder status.

Objective-C: Issue with CGRect .frame intersect/contains

I have two UIImageViews located about center of a horizontal screen and when the user clicks a button another UIImageView, located off screen, slides in from the right. I'm am just trying to detect when the view being brought onto the screen collides with the two static views. The problem is when I run my code and check the CGRect frames they are returned from where the views start, not end, regardless of where I place the frame calls in my method or even if I place them outside the method in a separate one. I'm a bit new to Obj-C and I understand that Core Animation runs on a separate thread and I am guessing that is why I am getting the starting values. (Correct me if I am wrong here).
So, I guess the question here is how do I detect collisions when one item is static and another is animated. Here's my code (feel free to clean it up):
- (IBAction)movegirl
{
//disabling button
self.movegirlbtn.enabled = NO;
//getting graphics center points
CGPoint pos = bushidogirl.center;
CGPoint box1 = topbox.center;
CGPoint box2 = bottombox.center;
//firing up animation
[UIView beginAnimations: nil context: NULL];
//setting animation speed
[UIView setAnimationDuration:2.0 ];
//setting final position of bushidogirl, then resetting her center position
pos.x = 260;
bushidogirl.center = pos;
//running animations
[UIView commitAnimations];
//playing gong sound
[self.audioplayer play];
//enabling button
self.movegirlbtn.enabled = YES;
[self collisiondetection: bushidogirl : topbox];
}
- (void)collisiondetection:(UIImageView *)item1 : (UIImageView *)item2
{
CGRect item1frame = item1.frame;
CGRect item2frame = item2.frame;
NSLog(#"Item1 frame = %d and Item 2 frame = %d", item1frame, item2frame);
}
How about:
if (CGRectIntersectsRect(item1frame, item2frame))
{
// collision!!
}