I've created a RootViewController / RootView that:
Handles the content layout for the app
Exposes and interface for performing application level behaviors, like presenting the "hamburger" menu or overlay views with CAKeyframe animations.
This is in accordance with good practice.
The Problem:
When the main content view presents a form, there's a utility to animate the frame of that view, when a field is selected that would otherwise be obscured by the keyboard. This has been working fine all the way up until iOS 8.0.2
On iOS 8.0.2 the frame for the form will no longer animate if you set a negative value for origin.y. Instead of going from the current origin.y to the required origin.y it jerks down by the amount it was supposed to move, then animates back to 0.
If I present the form outside of the RootVC it works correctly.
What I've tried:
Checked that RootView is not doing anything in layout subviews to prevent the animation. (In iOS8.0 it was. I removed this and problem was solved. Only to return in iOS8.0.2)
Checked the BeginFromCurrentState flags.
Instead of animating the form view, animate [UIScreen mainScreen].keyWindow. Works but causes some other side effects that I don't want.
Question:
What has changed with animation of UIViewControllers that are contained in another view in iOS8.0.2. It seems to be something very fundamental.
The code that animates frame to move input fields out the keyboard's way:
Looks something like this:
- (void)scrollToAccommodateField:(UIView *)view
{
UIView *rootView = [UIApplication sharedApplication].keyWindow.rootViewController.view;
CGPoint position = [view convertPoint:view.bounds.origin toView:rootView];
CGFloat y = position.y;
CGFloat scrollAmount = 0;
CGFloat margin = 25;
CGSize screenSize = [self screenSizeWithOrientation:[UIApplication sharedApplication].statusBarOrientation];
CGSize accessorySize = CGSizeMake(_view.width, 44);
CGFloat maxVisibleY = screenSize.height - [self keyboardSize].height - accessorySize.height - margin;
if (y > maxVisibleY)
{
scrollAmount = maxVisibleY - y;
CGFloat scrollDelta = scrollAmount - _currentScrollAmount;
_currentScrollAmount = scrollAmount;
[self scrollByAmount:scrollDelta];
}
else
{
if (_currentScrollAmount != 0)
{
_currentScrollAmount = 0;
[UIView transitionWithView:_view duration:0.30
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut animations:^
{
_view.frame = [_view bounds];
} completion:nil];
}
}
}
Update:
I've since installed TPKeyboardAvoiding pod and its working very well. . leaving this open, in case its of interest to others.
I suppose it's not strictly in line with Apple guidelines but I guess it must be possible somehow. I'd like to change the height of navigation bar inside UINavigationController and the height of UIBarButtonItem elements inside that bar.
Using a trick from this question I managed to change the height of navigation bar but I can see no way of adjusting the height of bar button items.
If anyone knows how to change the size of bar button items, please help me out.
This is my solution. It works very well.
#interface UINavigationBar (CustomHeight)
#end
#implementation UINavigationBar (CustomHeight)
- (CGSize)sizeThatFits:(CGSize)size {
// Change navigation bar height. The height must be even, otherwise there will be a white line above the navigation bar.
CGSize newSize = CGSizeMake(self.frame.size.width, 40);
return newSize;
}
-(void)layoutSubviews {
[super layoutSubviews];
// Make items on navigation bar vertically centered.
int i = 0;
for (UIView *view in self.subviews) {
NSLog(#"%i. %#", i, [view description]);
i++;
if (i == 0)
continue;
float centerY = self.bounds.size.height / 2.0f;
CGPoint center = view.center;
center.y = centerY;
view.center = center;
}
}
Maybe this tutorial about a customized navbar will help you: Recreating the iBooks wood themed navigation bar
If you create a BarButtonItem with a UIImageView you can maybe change the framesize/boundsize of the custom UIImageView
UIImageView* imageView = [[[UIImageView alloc] initWithFrame:navigationController.navigationBar.frame] autorelease];
imageView.contentMode = UIViewContentModeLeft;
imageView.image = [UIImage imageNamed:#"NavBar-iPhone.png"];
[navigationController.navigationBar insertSubview:imageView atIndex:0];
So for your need you would give the -initWithFrame method appropriate values.
static CGFloat const CustomNavigationBarHeight = 74;
#implementation WTNavigationBar
- (CGSize)sizeThatFits:(CGSize)size{
size.width = 1024;
size.height = CustomNavigationBarHeight;
return size;
}
-(void)layoutSubviews {
[super layoutSubviews];
for (UIView *view in self.subviews) {
SFLog(#"view.class=%#",[view class]);
if ([view isKindOfClass:NSClassFromString(#"UINavigationItemButtonView")]) {
float centerY = self.bounds.size.height / 2.0f;
CGPoint center = view.center;
center.y = centerY;
view.center = center;
}
}
}
#end
in my iPad app,which has a fixed landscape orientation,I found I have to hardcode the size's width
I managed to do something similar by subclassing UINavigationBar and overriding -layoutSubviews. The code looks like:
-(void)layoutSubviews {
[super layoutSubviews];
int i = 0;
for (UIView *view in self.subviews) {
NSLog(#"%i. %#", i++, [view description]);
if ([view isKindOfClass:NSClassFromString(#"UINavigationButton")]) {
view.frame = CGRectMake(0, 0, 100, 50);
}
}
}
If you need to know how to subclass UINavigationBar, have a look at this very good answer.
I am not really sure about the NSClassFromString(#"UINavigationButton")] part. It works, but I did this as an experiment, and I'm not sure if this will get approved by Apple. I hope someone with a better knowledge might shed some light.
For the UINavigationbar
In iOS SDK 4.3 and beyond, there is a way (hack) to change the height of the UINavigationBar.
To change the height of UINavigationController, change its frame size in viewWillAppear:animated: function. Then, the height will stay customized throughout whole app.
For the UIBarButtonItems
I've actually run into this myself and the only thing I could come up with was leveraging initWithCustomView and passing in a UIButton with a defined frame.
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
/*
* Insert button styling
*/
button.frame = CGRectMake(0, 0, width, height);
UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
Otherwise UIBarButtonItem only has a width property that can be set but unfortunately not a height property. Another nifty thing I've done with initWithCustomView is to pass in a toolbar with a button and other things like activity indicators. Hope this helps.
How badly do you want this? And, how thin (or thick) do you want to make your navbar?
One approach would be to set the transform of the navbar to scale and translate it. If you scale it too much the title and button text will look wonky, but if you only need to shave a few pixels you might be allright.
Here's the result of scaling it to be 75% of full height (33 pixels tall):
And the code that produced this:
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.title = #"Thin Navigation Bar";
self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle: #"Press Me" style:UIBarButtonItemStyleBordered target: nil action: NULL ] autorelease];
CGFloat scale = .75;
CGFloat cy = 44.0 - ( 44.0 * scale );
self.navigationController.navigationBar.transform = CGAffineTransformScale( CGAffineTransformMakeTranslation( 0, -cy / 2.0 ), 1.0, scale ) ;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGFloat scale = .75;
CGFloat cy = 44.0 - ( 44.0 * scale );
CGRect r = self.view.frame;
r.origin.y -= cy;
r.size.height += cy;
self.view.frame = r;
}
Now, this does have a number of problems, which may or may not be solvable. #1 is that you're fighting with the UINavigationController to size and position the navbar and the view-controller views. Animating between view controllers that use this technique is likely going to look weird.
I'd be curious if you could solve the related issues...
One last thought: If you dont use a UINavigationController then there really aren't a whole lot of issues with this other than squished text. Or, you could use a navigation controller but hide the default navbar, and add the thin navbar to each of your child-view controller views. You could even subclass UINavigationBar and set the transform from within:
#interface TSThinNavBar : UINavigationBar
{
}
#end
#implementation TSThinNavBar
// assuming we'll always be constructed from a nib
- (id) initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder: aDecoder];
if ( self != nil )
{
CGFloat scale = .75;
CGFloat cy = 44.0 - ( 44.0 * scale );
self.transform = CGAffineTransformScale( CGAffineTransformMakeTranslation( 0, -cy / 2.0 ), 1.0, scale ) ;
}
return self;
}
#end
I'm currently using the method shown in this Cocoa with Love article to create a custom NSWindow subclass. As in the example, I needed to have a roughly 10px margin around the content of the window in order to draw an arrow (I'm creating a popover style window). I had to have the margin around the entire window instead of just the side with the arrow on it because I wanted to be able to change the arrow position without having to reposition the content.
To summarize, the method I'm using to do this is (relevant code is at the bottom):
Override the contentRectForFrameRect: and frameRectForContentRect:styleMask: methods of NSWindow to add the padding around the content:
Sets the custom drawn frame view of the window as the contentView and then overrides the setter and getter for the contentView so that the view that is passed in is added as a subview of the frame view.
The problem is that the autoresizing masks of views inside the actual content view of the window are completely messed up. Here is how I'm setting up the content in interface builder:
Here's how the autoresizing mask of the table view scroll view is set up:
And here's how the text label's autoresizing mask is set:
And here's what the result looks like in-app:
Relevant code (derived from the aforementioned article)
#define CONTENT_MARGIN 10.0
- (NSRect)contentRectForFrameRect:(NSRect)windowFrame
{
windowFrame.origin = NSZeroPoint;
return NSInsetRect(windowFrame, CONTENT_MARGIN, ICONTENT_MARGIN);
}
- (NSRect)frameRectForContentRect:(NSRect)contentRect
{
return NSInsetRect(contentRect, -CONTENT_MARGINT, -CONTENT_MARGIN);
}
+ (NSRect)frameRectForContentRect:(NSRect)contentRect
styleMask:(NSUInteger)windowStyle
{
return NSInsetRect(contentRect, -CONTENT_MARGIN, -CONTENT_MARGIN);
}
- (NSView*)contentView
{
return _popoverContentView;
}
- (void)setContentView:(NSView *)aView
{
if ([_popoverContentView isEqualTo:aView]) { return; }
NSRect bounds = [self frame];
bounds.origin = NSZeroPoint;
SearchPopoverWindowFrame *frameView = [super contentView];
if (!frameView) {
frameView = [[[SearchPopoverWindowFrame alloc] initWithFrame:bounds] autorelease];
[super setContentView:frameView];
}
if (_popoverContentView) {
[_popoverContentView removeFromSuperview];
}
_popoverContentView = aView;
[_popoverContentView setFrame:[self contentRectForFrameRect:bounds]];
[_popoverContentView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
[frameView addSubview:_popoverContentView];
}
I thought that maybe the popover content was going over the margins somehow, so I drew a border around the content view, but no, everything is as should be. The only issue is that the autoresizing masks of the label and table view inside the content view do not work as they should. Any advice is greatly appreciated.
EDIT: If anyone's interested, I've open-sourced the complete code for this popover window/controller on github as INPopoverController. Includes a sample project in case you want to try and reproduce the issue.
-( void )scaleWindowForHeight:( float )height
{
if (height > 22)
{
NSWindow* window = [self window];
NSRect old_window_frame = [window frame];
NSRect old_content_rect = [window contentRectForFrameRect: old_window_frame];
NSSize new_content_size = NSMakeSize( old_window_frame.size.width, height );
// need to move window by Y-axis because NSWindow origin point is at lower side:
NSRect new_content_rect = NSMakeRect( NSMinX( old_content_rect ), NSMaxY( old_content_rect ) - new_content_size.height, new_content_size.width, new_content_size.height );
NSRect new_window_frame = [window frameRectForContentRect: new_content_rect];
[window setFrame: new_window_frame display:YES animate: [window isVisible] ];
}
else
NSLog(#"window size too small");
}
I've created an NSScrollView which itself contains a NSClippedView as content view (this is all default, created by IB). Inside the contents view there is the (default) document view.
This NSScrollView has horizontal scroller disabled and vertical enabled and, most importantly auto hide scrollers enabled.
When I add new views (via code, runtime) to the document view the scroller does not unhide automatically, until the moment I vertically resize the window (and which in turn resizes the scrollview as well). 1px is enough. Just the new painting of the window seems enough.
What I am looking for is triggering this by code: so when I add views to the scrollviews' content view I would like the scrollbar to appear.
int numberOfChildViews = 10; //hard coded for example here
int childViewHeight = 80; //same as above
NSRect rect = NSMakeRect(0, 0, [[self.scrollView contentView] bounds].size.width, [numberOfChildViews*childViewHeight);
[[self.scrollView documentView] setFrame:rect];
[[self.scrollView documentView] setBounds:rect]; //just added to make sure
Then I added the custom views to the document view, like:
for (int i=0; i<numberOfChildViews; i++) {
NZBProgressViewController *item = [nzbProgressArray objectAtIndex:i];
int y=i*[[item view] bounds].size.height;
rect= NSMakeRect(0, y, [[scrollView contentView] frame].size.width, [[item view] bounds].size.height);
[[item view] setFrame:rect];
currentPosition++;
}
I am using a FlippedView so the origin will be displayed in left-top, like so:
#interface NSFlippedClipView : NSClipView {
}
#implementation NSFlippedClipView
- (BOOL)isFlipped {
return YES;
}
- (BOOL)isOpaque {
return YES;
}
#end
And added the following code to the awakeFromNib
NSFlippedClipView *documentView = [[NSFlippedClipView alloc] init];
[documentView setAutoresizingMask:NSViewWidthSizable];
[documentView setBackgroundColor:[self.scrollView backgroundColor]];
[self.scrollView setDocumentView:documentView];
[documentView release];
The scrollbars should become visible as soon as the document view is resized to be larger than the current viewport of the scroll view. Are you resizing the document view when you add your subviews to it?
Ahh, it's my own bad. For future reference: if you want to move the origin (0,0) to left-top use a NSView instead of NSClippedView extended class with IsFlipped method overriden to YES.
Thanks irsk for answering.
I have 6 UITextFields on my UIScrollView. Now, I can scroll by user request. But when the keyboard appear, some textfields are hidden.
That is not user-friendly.
How scroll programmatically the view so I get sure the keyboard not hide the textfield?
Here's what worked for me. Having an instance variable that holds the value of the UIScrollView's offset before the view is adjusted for the keyboard so you can restore the previous state after the UITextField returns:
//header
#interface TheViewController : UIViewController <UITextFieldDelegate> {
CGPoint svos;
}
//implementation
- (void)textFieldDidBeginEditing:(UITextField *)textField {
svos = scrollView.contentOffset;
CGPoint pt;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:scrollView];
pt = rc.origin;
pt.x = 0;
pt.y -= 60;
[scrollView setContentOffset:pt animated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[scrollView setContentOffset:svos animated:YES];
[textField resignFirstResponder];
return YES;
}
Finally, a simple fix:
UIScrollView* v = (UIScrollView*) self.view ;
CGRect rc = [textField bounds];
rc = [textField convertRect:rc toView:v];
rc.origin.x = 0 ;
rc.origin.y -= 60 ;
rc.size.height = 400;
[self.scroll scrollRectToVisible:rc animated:YES];
Now I think is only combine this with the link above and is set!
I've put together a universal, drop-in UIScrollView and UITableView subclass that takes care of moving all text fields within it out of the way of the keyboard.
When the keyboard is about to appear, the subclass will find the subview that's about to be edited, and adjust its frame and content offset to make sure that view is visible, with an animation to match the keyboard pop-up. When the keyboard disappears, it restores its prior size.
It should work with basically any setup, either a UITableView-based interface, or one consisting of views placed manually.
Here it is.
(For google: TPKeyboardAvoiding, TPKeyboardAvoidingScrollView, TPKeyboardAvoidingCollectionView.)
Editor's note: TPKeyboardAvoiding seems to be continually updated and fresh, as of 2014.
If you set the delegate of your text fields to a controller object in your program, you can have that object implement the textFieldDidBeginEditing: and textFieldShouldReturn: methods. The first method can then be used to scroll to your text field and the second method can be used to scroll back.
You can find code I have used for this in my blog: Sliding UITextViews around to avoid the keyboard. I didn't test this code for text views in a UIScrollView but it should work.
simple and best
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
// self.scrlViewUI.contentOffset = CGPointMake(0, textField.frame.origin.y);
[_scrlViewUI setContentOffset:CGPointMake(0,textField.center.y-90) animated:YES];
tes=YES;
[self viewDidLayoutSubviews];
}
The answers posted so far didn't work for me as I've a quite deep nested structure of UIViews. Also, the I had the problem that some of those answers were working only on certain device orientations.
Here's my solution, which will hopefully make you waste some less time on this.
My UIViewTextView derives from UIView, is a UITextView delegate and adds a UITextView after having read some parameters from an XML file for that UITextView (that XML part is left out here for clarity).
Here's the private interface definition:
#import "UIViewTextView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <CoreGraphics/CGColor.h>
#interface UIViewTextView (/**/) {
#private
UITextView *tf;
/*
* Current content scroll view
* position and frame
*/
CGFloat currentScrollViewPosition;
CGFloat currentScrollViewHeight;
CGFloat kbHeight;
CGFloat kbTop;
/*
* contentScrollView is the UIScrollView
* that contains ourselves.
*/
UIScrollView contentScrollView;
}
#end
In the init method I have to register the event handlers:
#implementation UIViewTextView
- (id) initWithScrollView:(UIScrollView*)scrollView {
self = [super init];
if (self) {
contentScrollView = scrollView;
// ...
tf = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 241, 31)];
// ... configure tf and fetch data for it ...
tf.delegate = self;
// ...
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(keyboardWasShown:) name: UIKeyboardWillShowNotification object:nil];
[nc addObserver:self selector:#selector(keyboardWasHidden:) name: UIKeyboardWillHideNotification object:nil];
[self addSubview:tf];
}
return(self);
}
Once that's done, we need to handle the keyboard show event. This gets called before the textViewBeginEditing is called, so we can use it to find out some properties of the keyboard. In essence, we want to know the height of the keyboard. This, unfortunately, needs to be taken from its width property in landscape mode:
-(void)keyboardWasShown:(NSNotification*)aNotification {
NSDictionary* info = [aNotification userInfo];
CGRect kbRect = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGSize kbSize = kbRect.size;
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat sWidth = screenRect.size.width;
CGFloat sHeight = screenRect.size.height;
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if ((orientation == UIDeviceOrientationPortrait)
||(orientation == UIDeviceOrientationPortraitUpsideDown)) {
kbHeight = kbSize.height;
kbTop = sHeight - kbHeight;
} else {
//Note that the keyboard size is not oriented
//so use width property instead
kbHeight = kbSize.width;
kbTop = sWidth - kbHeight;
}
Next, we need to actually scroll around when we start editing. We do this here:
- (void) textViewDidBeginEditing:(UITextView *)textView {
/*
* Memorize the current scroll position
*/
currentScrollViewPosition = contentScrollView.contentOffset.y;
/*
* Memorize the current scroll view height
*/
currentScrollViewHeight = contentScrollView.frame.size.height;
// My top position
CGFloat myTop = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow.rootViewController.view].y;
// My height
CGFloat myHeight = self.frame.size.height;
// My bottom
CGFloat myBottom = myTop + myHeight;
// Eventual overlap
CGFloat overlap = myBottom - kbTop;
/*
* If there's no overlap, there's nothing to do.
*/
if (overlap < 0) {
return;
}
/*
* Calculate the new height
*/
CGRect crect = contentScrollView.frame;
CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight + overlap);
/*
* Set the new height
*/
[contentScrollView setFrame:nrect];
/*
* Set the new scroll position
*/
CGPoint npos;
npos.x = contentScrollView.contentOffset.x;
npos.y = contentScrollView.contentOffset.y + overlap;
[contentScrollView setContentOffset:npos animated:NO];
}
When we end editing, we do this to reset the scroll position:
- (void) textViewDidEndEditing:(UITextView *)textView {
/*
* Reset the scroll view position
*/
CGRect crect = contentScrollView.frame;
CGRect nrect = CGRectMake(crect.origin.x, crect.origin.y, crect.size.width, currentScrollViewHeight);
[contentScrollView setFrame:nrect];
/*
* Reset the scroll view height
*/
CGPoint npos;
npos.x = contentScrollView.contentOffset.x;
npos.y = currentScrollViewPosition;
[contentScrollView setContentOffset:npos animated:YES];
[tf resignFirstResponder];
// ... do something with your data ...
}
There's nothing left to do in the keyboard was hidden event handler; we leave it in anyway:
-(void)keyboardWasHidden:(NSNotification*)aNotification {
}
And that's it.
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
#end
I know this is old, but still none of the solutions above had all the fancy positioning stuff required for that "perfect" bug-free, backwards compatible and flicker-free animation.
Let me share my solution (assuming you have set up UIKeyboardWill(Show|Hide)Notification):
// Called when UIKeyboardWillShowNotification is sent
- (void)keyboardWillShow:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardFrameInWindow;
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameInWindow];
// the keyboard frame is specified in window-level coordinates. this calculates the frame as if it were a subview of our view, making it a sibling of the scroll view
CGRect keyboardFrameInView = [self.view convertRect:keyboardFrameInWindow fromView:nil];
CGRect scrollViewKeyboardIntersection = CGRectIntersection(_scrollView.frame, keyboardFrameInView);
UIEdgeInsets newContentInsets = UIEdgeInsetsMake(0, 0, scrollViewKeyboardIntersection.size.height, 0);
// this is an old animation method, but the only one that retains compaitiblity between parameters (duration, curve) and the values contained in the userInfo-Dictionary.
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
_scrollView.contentInset = newContentInsets;
_scrollView.scrollIndicatorInsets = newContentInsets;
/*
* Depending on visual layout, _focusedControl should either be the input field (UITextField,..) or another element
* that should be visible, e.g. a purchase button below an amount text field
* it makes sense to set _focusedControl in delegates like -textFieldShouldBeginEditing: if you have multiple input fields
*/
if (_focusedControl) {
CGRect controlFrameInScrollView = [_scrollView convertRect:_focusedControl.bounds fromView:_focusedControl]; // if the control is a deep in the hierarchy below the scroll view, this will calculate the frame as if it were a direct subview
controlFrameInScrollView = CGRectInset(controlFrameInScrollView, 0, -10); // replace 10 with any nice visual offset between control and keyboard or control and top of the scroll view.
CGFloat controlVisualOffsetToTopOfScrollview = controlFrameInScrollView.origin.y - _scrollView.contentOffset.y;
CGFloat controlVisualBottom = controlVisualOffsetToTopOfScrollview + controlFrameInScrollView.size.height;
// this is the visible part of the scroll view that is not hidden by the keyboard
CGFloat scrollViewVisibleHeight = _scrollView.frame.size.height - scrollViewKeyboardIntersection.size.height;
if (controlVisualBottom > scrollViewVisibleHeight) { // check if the keyboard will hide the control in question
// scroll up until the control is in place
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y += (controlVisualBottom - scrollViewVisibleHeight);
// make sure we don't set an impossible offset caused by the "nice visual offset"
// if a control is at the bottom of the scroll view, it will end up just above the keyboard to eliminate scrolling inconsistencies
newContentOffset.y = MIN(newContentOffset.y, _scrollView.contentSize.height - scrollViewVisibleHeight);
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
} else if (controlFrameInScrollView.origin.y < _scrollView.contentOffset.y) {
// if the control is not fully visible, make it so (useful if the user taps on a partially visible input field
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y = controlFrameInScrollView.origin.y;
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
}
}
[UIView commitAnimations];
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillHide:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = notification.userInfo;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
// undo all that keyboardWillShow-magic
// the scroll view will adjust its contentOffset apropriately
_scrollView.contentInset = UIEdgeInsetsZero;
_scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
[UIView commitAnimations];
}
You may check it out: https://github.com/michaeltyson/TPKeyboardAvoiding (I used that sample for my apps). It is working so well. I hope that helps you.
Actually, here's a full tutorial on using TPKeyboardAvoiding, which may help someone
(1) download the zip file from the github link. add these four files to your Xcode project:
(2) build your beautiful form in IB. add a UIScrollView. sit the form items INSIDE the scroll view. (Note - extremely useful tip regarding interface builder: https://stackoverflow.com/a/16952902/294884)
(3) click on the scroll view. then at the top right, third button, you'll see the word "UIScrollView". using copy and paste, change it to "TPKeyboardAvoidingScrollView"
(4) that's it. put the app in the app store, and bill your client.
(Also, just click on the Inspector tab of the scroll view. You may prefer to turn on or off bouncing and the scroll bars - your preference.)
Personal comment - I strongly recommend using scroll view (or collection view) for input forms, in almost all cases. do not use a table view. it's problematic for many reasons. and quite simply, it's incredibly easier to use a scroll view. just lay it out any way you want. it is 100% wysiwyg in interface builder. hope it helps
This is my code, hope it will help you. It work ok in case you have many textfield
CGPoint contentOffset;
bool isScroll;
- (void)textFieldDidBeginEditing:(UITextField *)textField {
contentOffset = self.myScroll.contentOffset;
CGPoint newOffset;
newOffset.x = contentOffset.x;
newOffset.y = contentOffset.y;
//check push return in keyboar
if(!isScroll){
//180 is height of keyboar
newOffset.y += 180;
isScroll=YES;
}
[self.myScroll setContentOffset:newOffset animated:YES];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
//reset offset of content
isScroll = NO;
[self.myScroll setContentOffset:contentOffset animated:YES];
[textField endEditing:true];
return true;
}
we have a point contentOffset to save contentoffset of scrollview before keyboar show. Then we will scroll content for y about 180 (height of keyboar). when you touch return in keyboar, we will scroll content to old point(it is contentOffset). If you have many textfield, you don't touch return in keyboar but you touch another textfield, it will +180 . So we have check touch return
Use any of these,
CGPoint bottomOffset = CGPointMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height);
[self.MainScrollView setContentOffset:bottomOffset animated:YES];
or
[self.MainScrollView scrollRectToVisible:CGRectMake(0, self.MainScrollView.contentSize.height - self.MainScrollView.bounds.size.height-30, MainScrollView.frame.size.width, MainScrollView.frame.size.height) animated:YES];
I think it's better use keyboard notifications because you don't know if the first responder (the control with focus on) is a textField or a textView (or whatever). So juste create a category to find the first responder :
#import "UIResponder+FirstResponder.h"
static __weak id currentFirstResponder;
#implementation UIResponder (FirstResponder)
+(id)currentFirstResponder {
currentFirstResponder = nil;
[[UIApplication sharedApplication] sendAction:#selector(findFirstResponder:) to:nil from:nil forEvent:nil];
return currentFirstResponder;
}
-(void)findFirstResponder:(id)sender {
currentFirstResponder = self;
}
#end
then
-(void)keyboardWillShowNotification:(NSNotification*)aNotification{
contentScrollView.delegate=nil;
contentScrollView.scrollEnabled=NO;
contentScrollViewOriginalOffset = contentScrollView.contentOffset;
UIResponder *lc_firstResponder = [UIResponder currentFirstResponder];
if([lc_firstResponder isKindOfClass:[UIView class]]){
UIView *lc_view = (UIView *)lc_firstResponder;
CGRect lc_frame = [lc_view convertRect:lc_view.bounds toView:contentScrollView];
CGPoint lc_point = CGPointMake(0, lc_frame.origin.y-lc_frame.size.height);
[contentScrollView setContentOffset:lc_point animated:YES];
}
}
Eventually disable the scroll and set the delegate to nil then restore it to avoid some actions during the edition of the first responder. Like james_womack said, keep the original offset to restore it in a keyboardWillHideNotification method.
-(void)keyboardWillHideNotification:(NSNotification*)aNotification{
contentScrollView.delegate=self;
contentScrollView.scrollEnabled=YES;
[contentScrollView setContentOffset:contentScrollViewOriginalOffset animated:YES];
}
In Swift 1.2+ do something like this:
class YourViewController: UIViewController, UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
_yourTextField.delegate = self //make sure you have the delegate set to this view controller for each of your textFields so textFieldDidBeginEditing can be called for each one
...
}
func textFieldDidBeginEditing(textField: UITextField) {
var point = textField.convertPoint(textField.frame.origin, toView: _yourScrollView)
point.x = 0.0 //if your textField does not have an origin at 0 for x and you don't want your scrollView to shift left and right but rather just up and down
_yourScrollView.setContentOffset(point, animated: true)
}
func textFieldDidEndEditing(textField: UITextField) {
//Reset scrollview once done editing
scrollView.setContentOffset(CGPoint.zero, animated: true)
}
}