Browse files

More robustness when frame or content size changes; Disable TPKeyboar…

…dAvoidingTableView on iOS 4.3+
  • Loading branch information...
1 parent d6ebc02 commit 33d145f6b79d6902e256bb12421540a410b72e07 @michaeltyson committed Aug 19, 2011
Showing with 185 additions and 84 deletions.
  1. +7 −0 README.md
  2. +4 −2 TPKeyboardAvoidingScrollView.h
  3. +62 −33 TPKeyboardAvoidingScrollView.m
  4. +4 −4 TPKeyboardAvoidingTableView.h
  5. +108 −45 TPKeyboardAvoidingTableView.m
View
7 README.md
@@ -21,6 +21,13 @@ For use with `UITableViewController` classes, drop `TPKeyboardAvoidingTableView.
For non-UITableViewControllers, drop the `TPKeyboardAvoidingScrollView.m` and `TPKeyboardAvoidingScrollView.h` source files into your project, pop a `UIScrollView` into your view controller's xib, set the scroll view's class to `TPKeyboardAvoidingScrollView`, and put all your controls within that scroll view. You can also create it programmatically, without using a xib - just use the TPKeyboardAvoidingScrollView as your top-level view.
+Notes
+-----
+
+On iOS 4.3 and up, UITableView automatically supports moving out of the way of the keyboard. Thus, using TPKeyboardAvoidingTableView on iOS 4.3+ environments will have no effect -- in fact, if iOS 4.3 or up is detected, the init methods for TPKeyboardAvoidingTableView will return a UITableView instead.
+
+These classes currently adjust the contentInset parameter to avoid content moving beneath the keyboard. This is done, as opposed to adjusting the frame, in order to work around an iOS bug that results in a jerky animation where the view jumps upwards, before settling down. In order to facilitate this workaround, the contentSize is maintained to be at least same size as the view's frame.
+
Licence
-------
View
6 TPKeyboardAvoidingScrollView.h 100644 → 100755
@@ -6,8 +6,10 @@
//
@interface TPKeyboardAvoidingScrollView : UIScrollView {
- UIEdgeInsets priorInset;
- BOOL _keyboardVisible;
+ UIEdgeInsets _priorInset;
+ BOOL _keyboardVisible;
+ CGRect _keyboardRect;
+ CGSize _originalContentSize;
}
- (void)adjustOffsetToIdealIfNeeded;
View
95 TPKeyboardAvoidingScrollView.m
@@ -11,7 +11,9 @@
@interface TPKeyboardAvoidingScrollView ()
- (UIView*)findFirstResponderBeneathView:(UIView*)view;
+- (UIEdgeInsets)contentInsetForKeyboard;
- (CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space;
+- (CGRect)keyboardRect;
@end
@implementation TPKeyboardAvoidingScrollView
@@ -39,58 +41,71 @@ -(void)dealloc {
[super dealloc];
}
+-(void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+
+ CGSize contentSize = _originalContentSize;
+ contentSize.width = MAX(contentSize.width, self.frame.size.width);
+ contentSize.height = MAX(contentSize.height, self.frame.size.height);
+ [super setContentSize:contentSize];
+
+ if ( _keyboardVisible ) {
+ self.contentInset = [self contentInsetForKeyboard];
+ }
+}
+
+-(void)setContentSize:(CGSize)contentSize {
+ _originalContentSize = contentSize;
+
+ contentSize.width = MAX(contentSize.width, self.frame.size.width);
+ contentSize.height = MAX(contentSize.height, self.frame.size.height);
+ [super setContentSize:contentSize];
+
+ if ( _keyboardVisible ) {
+ self.contentInset = [self contentInsetForKeyboard];
+ }
+}
+
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[[self findFirstResponderBeneathView:self] resignFirstResponder];
[super touchesEnded:touches withEvent:event];
}
- (void)keyboardWillShow:(NSNotification*)notification {
+ _keyboardRect = [[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue];
+ _keyboardVisible = YES;
+
UIView *firstResponder = [self findFirstResponderBeneathView:self];
if ( !firstResponder ) {
// No child view is the first responder - nothing to do here
return;
}
- priorInset = self.contentInset;
-
- // Use this view's coordinate system
- CGRect keyboardBounds = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
- CGRect screenBounds = [self convertRect:[UIScreen mainScreen].bounds fromView:nil];
- if ( keyboardBounds.origin.y == 0 ) keyboardBounds.origin = CGPointMake(0, screenBounds.size.height - keyboardBounds.size.height);
-
- CGFloat spaceAboveKeyboard = keyboardBounds.origin.y - self.bounds.origin.y;
-
- UIEdgeInsets newInset = self.contentInset;
-
- newInset.bottom = keyboardBounds.size.height -
- ((keyboardBounds.origin.y+keyboardBounds.size.height)
- - (self.bounds.origin.y+self.bounds.size.height));
-
-
- CGFloat offset = [self idealOffsetForView:firstResponder withSpace:spaceAboveKeyboard];
+ _priorInset = self.contentInset;
// Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
[UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]];
- self.contentInset = newInset;
-
-
- [self setContentOffset:CGPointMake(self.contentOffset.x, offset) animated:YES];
+
+ self.contentInset = [self contentInsetForKeyboard];
+ [self setContentOffset:CGPointMake(self.contentOffset.x,
+ [self idealOffsetForView:firstResponder withSpace:[self keyboardRect].origin.y - self.bounds.origin.y])
+ animated:YES];
[UIView commitAnimations];
- _keyboardVisible = true;
}
- (void)keyboardWillHide:(NSNotification*)notification {
-
+ _keyboardRect = CGRectZero;
+ _keyboardVisible = NO;
+
// Restore dimensions to prior size
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
[UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]];
- self.contentInset = priorInset;
+ self.contentInset = _priorInset;
[UIView commitAnimations];
- _keyboardVisible = false;
}
- (UIView*)findFirstResponderBeneathView:(UIView*)view {
@@ -103,14 +118,19 @@ - (UIView*)findFirstResponderBeneathView:(UIView*)view {
return nil;
}
+- (UIEdgeInsets)contentInsetForKeyboard {
+ UIEdgeInsets newInset = self.contentInset;
+ CGRect keyboardRect = [self keyboardRect];
+ newInset.bottom = keyboardRect.size.height - ((keyboardRect.origin.y+keyboardRect.size.height) - (self.bounds.origin.y+self.bounds.size.height));
+ return newInset;
+}
--(CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space
-{
+-(CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space {
- //Convert the rect to get the view's distance from the top of the scrollView.
+ // Convert the rect to get the view's distance from the top of the scrollView.
CGRect rect = [view convertRect:view.bounds toView:self];
- //set starting offset to that point
+ // Set starting offset to that point
CGFloat offset = rect.origin.y;
@@ -133,10 +153,10 @@ -(CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space
return offset;
}
--(void)adjustOffsetToIdealIfNeeded
-{
- //only do this if the keyboard is already visible
- if (!_keyboardVisible) return;
+-(void)adjustOffsetToIdealIfNeeded {
+
+ // Only do this if the keyboard is already visible
+ if ( !_keyboardVisible ) return;
CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
@@ -145,4 +165,13 @@ -(void)adjustOffsetToIdealIfNeeded
[self setContentOffset:idealOffset animated:YES];
}
+- (CGRect)keyboardRect {
+ CGRect keyboardRect = [self convertRect:_keyboardRect fromView:nil];
+ if ( keyboardRect.origin.y == 0 ) {
+ CGRect screenBounds = [self convertRect:[UIScreen mainScreen].bounds fromView:nil];
+ keyboardRect.origin = CGPointMake(0, screenBounds.size.height - keyboardRect.size.height);
+ }
+ return keyboardRect;
+}
+
@end
View
8 TPKeyboardAvoidingTableView.h
@@ -5,11 +5,11 @@
// Copyright 2011 A Tasty Pixel. All rights reserved.
//
-#import <UIKit/UIKit.h>
-
-
@interface TPKeyboardAvoidingTableView : UITableView {
- CGRect priorFrame;
+ UIEdgeInsets _priorInset;
+ BOOL _keyboardVisible;
+ CGRect _keyboardRect;
}
+- (void)adjustOffsetToIdealIfNeeded;
@end
View
153 TPKeyboardAvoidingTableView.m
@@ -11,6 +11,9 @@
@interface TPKeyboardAvoidingTableView ()
- (UIView*)findFirstResponderBeneathView:(UIView*)view;
+- (UIEdgeInsets)contentInsetForKeyboard;
+- (CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space;
+- (CGRect)keyboardRect;
@end
@implementation TPKeyboardAvoidingTableView
@@ -20,86 +23,90 @@ - (void)setup {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}
-- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
- if ( !(self = [super initWithFrame:frame style:style]) ) return nil;
+-(id)initWithFrame:(CGRect)frame {
+ if ( !(self = [super initWithFrame:frame]) ) return nil;
+
+ if ( [[[UIDevice currentDevice] systemVersion] floatValue] >= 4.3 ) {
+ // Not required above iOS 4.3! Just return an ordinary table view
+ [self release];
+ return (self = [[UITableView alloc] initWithFrame:frame]);
+ }
+
[self setup];
return self;
}
--(void)awakeFromNib {
+-(id)initWithCoder:(NSCoder *)aDecoder {
+ if ( !(self = [super initWithCoder:aDecoder]) ) return nil;
+
+ if ( [[[UIDevice currentDevice] systemVersion] floatValue] >= 4.3 ) {
+ // Not required above iOS 4.3! Just return an ordinary table view
+ [self release];
+ return (self = [[UITableView alloc] initWithCoder:aDecoder]);
+ }
+
[self setup];
+ return self;
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
+-(void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+ if ( _keyboardVisible ) {
+ self.contentInset = [self contentInsetForKeyboard];
+ }
+}
+
+-(void)setContentSize:(CGSize)contentSize {
+ [super setContentSize:contentSize];
+ if ( _keyboardVisible ) {
+ self.contentInset = [self contentInsetForKeyboard];
+ }
+}
+
+- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+ [[self findFirstResponderBeneathView:self] resignFirstResponder];
+ [super touchesEnded:touches withEvent:event];
+}
- (void)keyboardWillShow:(NSNotification*)notification {
- if ( !CGRectEqualToRect(priorFrame, CGRectZero) ) return;
+ _keyboardRect = [[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue];
+ _keyboardVisible = YES;
UIView *firstResponder = [self findFirstResponderBeneathView:self];
if ( !firstResponder ) {
// No child view is the first responder - nothing to do here
return;
}
- priorFrame = self.frame;
-
- // Use this view's coordinate system
- CGRect keyboardBounds = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
- CGRect screenBounds = [self convertRect:[UIScreen mainScreen].bounds fromView:nil];
- if ( keyboardBounds.origin.y == 0 ) keyboardBounds.origin = CGPointMake(0, screenBounds.size.height - keyboardBounds.size.height);
-
- CGFloat spaceAboveKeyboard = keyboardBounds.origin.y - self.bounds.origin.y;
- CGFloat offset = -1;
-
- CGRect newFrame = self.frame;
- newFrame.size.height -= keyboardBounds.size.height -
- ((keyboardBounds.origin.y+keyboardBounds.size.height)
- - (self.bounds.origin.y+self.bounds.size.height));
-
- CGRect firstResponderFrame = [firstResponder convertRect:firstResponder.bounds toView:self];
- if ( firstResponderFrame.origin.y + firstResponderFrame.size.height >= screenBounds.origin.y + screenBounds.size.height - keyboardBounds.size.height ) {
- // Prepare to scroll to make sure the view is above the keyboard
- offset = firstResponderFrame.origin.y + self.contentOffset.y;
- if ( self.contentSize.height - offset < newFrame.size.height ) {
- // Scroll to the bottom
- offset = self.contentSize.height - newFrame.size.height;
- } else {
- if ( firstResponder.bounds.size.height < spaceAboveKeyboard ) {
- // Center vertically if there's room
- offset -= floor((spaceAboveKeyboard-firstResponder.bounds.size.height)/2.0);
- }
- if ( offset + newFrame.size.height > self.contentSize.height ) {
- // Clamp to content size
- offset = self.contentSize.height - newFrame.size.height;
- }
- }
- }
+ _priorInset = self.contentInset;
- // Shrink view's height by the keyboard's height, and scroll to show the text field/view being edited
+ // Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
[UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]];
- self.frame = newFrame;
- if ( offset != -1 ) {
- [self setContentOffset:CGPointMake(self.contentOffset.x, offset) animated:YES];
- }
+ self.contentInset = [self contentInsetForKeyboard];
+ [self setContentOffset:CGPointMake(self.contentOffset.x,
+ [self idealOffsetForView:firstResponder withSpace:[self keyboardRect].origin.y - self.bounds.origin.y])
+ animated:YES];
+
[UIView commitAnimations];
}
- (void)keyboardWillHide:(NSNotification*)notification {
- if ( CGRectEqualToRect(priorFrame, CGRectZero) ) return;
+ _keyboardRect = CGRectZero;
+ _keyboardVisible = NO;
// Restore dimensions to prior size
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
[UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]];
- self.frame = priorFrame;
- priorFrame = CGRectZero;
+ self.contentInset = _priorInset;
[UIView commitAnimations];
}
@@ -113,4 +120,60 @@ - (UIView*)findFirstResponderBeneathView:(UIView*)view {
return nil;
}
+- (UIEdgeInsets)contentInsetForKeyboard {
+ UIEdgeInsets newInset = self.contentInset;
+ CGRect keyboardRect = [self keyboardRect];
+ newInset.bottom = keyboardRect.size.height - ((keyboardRect.origin.y+keyboardRect.size.height) - (self.bounds.origin.y+self.bounds.size.height));
+ return newInset;
+}
+
+-(CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space {
+
+ // Convert the rect to get the view's distance from the top of the scrollView.
+ CGRect rect = [view convertRect:view.bounds toView:self];
+
+ // Set starting offset to that point
+ CGFloat offset = rect.origin.y;
+
+
+ if ( self.contentSize.height - offset < space ) {
+ // Scroll to the bottom
+ offset = self.contentSize.height - space;
+ } else {
+ if ( view.bounds.size.height < space ) {
+ // Center vertically if there's room
+ offset -= floor((space-view.bounds.size.height)/2.0);
+ }
+ if ( offset + space > self.contentSize.height ) {
+ // Clamp to content size
+ offset = self.contentSize.height - space;
+ }
+ }
+
+ if (offset < 0) offset = 0;
+
+ return offset;
+}
+
+-(void)adjustOffsetToIdealIfNeeded {
+
+ // Only do this if the keyboard is already visible
+ if ( !_keyboardVisible ) return;
+
+ CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
+
+ CGPoint idealOffset = CGPointMake(0, [self idealOffsetForView:[self findFirstResponderBeneathView:self] withSpace:visibleSpace]);
+
+ [self setContentOffset:idealOffset animated:YES];
+}
+
+- (CGRect)keyboardRect {
+ CGRect keyboardRect = [self convertRect:_keyboardRect fromView:nil];
+ if ( keyboardRect.origin.y == 0 ) {
+ CGRect screenBounds = [self convertRect:[UIScreen mainScreen].bounds fromView:nil];
+ keyboardRect.origin = CGPointMake(0, screenBounds.size.height - keyboardRect.size.height);
+ }
+ return keyboardRect;
+}
+
@end

0 comments on commit 33d145f

Please sign in to comment.