Commit f35b3f9a70a903bfec0345c8e625e2e11dc77a2d

Authored by phong
1 parent 256868a43c

fix bug baseviewcontroller

Showing 8 changed files with 822 additions and 0 deletions Inline Diff

LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h
File was created 1 //
2 // TPKeyboardAvoidingCollectionView.h
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel & The CocoaBots. All rights reserved.
7 //
8
9 #import <UIKit/UIKit.h>
10 #import "UIScrollView+TPKeyboardAvoidingAdditions.h"
11
12 @interface TPKeyboardAvoidingCollectionView : UICollectionView <UITextFieldDelegate, UITextViewDelegate>
13 - (BOOL)focusNextTextField;
14 - (void)scrollToActiveTextField;
15 @end
16
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m
File was created 1 //
2 // TPKeyboardAvoidingCollectionView.m
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel & The CocoaBots. All rights reserved.
7 //
8
9 #import "TPKeyboardAvoidingCollectionView.h"
10
11 @interface TPKeyboardAvoidingCollectionView () <UITextFieldDelegate, UITextViewDelegate>
12 @end
13
14 @implementation TPKeyboardAvoidingCollectionView
15
16 #pragma mark - Setup/Teardown
17
18 - (void)setup {
19 if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return;
20
21 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil];
22 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
23 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil];
24 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil];
25 }
26
27 -(id)initWithFrame:(CGRect)frame {
28 if ( !(self = [super initWithFrame:frame]) ) return nil;
29 [self setup];
30 return self;
31 }
32
33 - (id)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {
34 if ( !(self = [super initWithFrame:frame collectionViewLayout:layout]) ) return nil;
35 [self setup];
36 return self;
37 }
38
39 -(void)awakeFromNib {
40 [super awakeFromNib];
41 [self setup];
42 }
43
44 -(void)dealloc {
45 [[NSNotificationCenter defaultCenter] removeObserver:self];
46 #if !__has_feature(objc_arc)
47 [super dealloc];
48 #endif
49 }
50
51
52 -(BOOL)hasAutomaticKeyboardAvoidingBehaviour {
53 if ( [[[UIDevice currentDevice] systemVersion] integerValue] >= 9
54 && [self.delegate isKindOfClass:[UICollectionViewController class]] ) {
55 // Theory: It looks like iOS 9's collection views automatically avoid the keyboard. As usual
56 // Apple have totally failed to document this anywhere, so this is just a guess.
57 return YES;
58 }
59
60 return NO;
61 }
62
63 -(void)setFrame:(CGRect)frame {
64 [super setFrame:frame];
65 [self TPKeyboardAvoiding_updateContentInset];
66 }
67
68 -(void)setContentSize:(CGSize)contentSize {
69 if (CGSizeEqualToSize(contentSize, self.contentSize)) {
70 // Prevent triggering contentSize when it's already the same that
71 // cause weird infinte scrolling and locking bug
72 return;
73 }
74 [super setContentSize:contentSize];
75 [self TPKeyboardAvoiding_updateContentInset];
76 }
77
78 - (BOOL)focusNextTextField {
79 return [self TPKeyboardAvoiding_focusNextTextField];
80
81 }
82 - (void)scrollToActiveTextField {
83 return [self TPKeyboardAvoiding_scrollToActiveTextField];
84 }
85
86 #pragma mark - Responders, events
87
88 -(void)willMoveToSuperview:(UIView *)newSuperview {
89 [super willMoveToSuperview:newSuperview];
90 if ( !newSuperview ) {
91 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self];
92 }
93 }
94
95 - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
96 [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder];
97 [super touchesEnded:touches withEvent:event];
98 }
99
100 -(BOOL)textFieldShouldReturn:(UITextField *)textField {
101 if ( ![self focusNextTextField] ) {
102 [textField resignFirstResponder];
103 }
104 return YES;
105 }
106
107 -(void)layoutSubviews {
108 [super layoutSubviews];
109 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self];
110 [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1];
111 }
112
113 @end
114
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h
File was created 1 //
2 // TPKeyboardAvoidingScrollView.h
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel. All rights reserved.
7 //
8
9 #import <UIKit/UIKit.h>
10 #import "UIScrollView+TPKeyboardAvoidingAdditions.h"
11
12 @interface TPKeyboardAvoidingScrollView : UIScrollView <UITextFieldDelegate, UITextViewDelegate>
13 - (void)contentSizeToFit;
14 - (BOOL)focusNextTextField;
15 - (void)scrollToActiveTextField;
16 @end
17
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m
File was created 1 //
2 // TPKeyboardAvoidingScrollView.m
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel. All rights reserved.
7 //
8
9 #import "TPKeyboardAvoidingScrollView.h"
10
11 @interface TPKeyboardAvoidingScrollView () <UITextFieldDelegate, UITextViewDelegate>
12 @end
13
14 @implementation TPKeyboardAvoidingScrollView
15
16 #pragma mark - Setup/Teardown
17
18 - (void)setup {
19 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil];
20 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
21 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil];
22 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil];
23 }
24
25 -(id)initWithFrame:(CGRect)frame {
26 if ( !(self = [super initWithFrame:frame]) ) return nil;
27 [self setup];
28 return self;
29 }
30
31 -(void)awakeFromNib {
32 [super awakeFromNib];
33 [self setup];
34 }
35
36 -(void)dealloc {
37 [[NSNotificationCenter defaultCenter] removeObserver:self];
38 #if !__has_feature(objc_arc)
39 [super dealloc];
40 #endif
41 }
42
43 -(void)setFrame:(CGRect)frame {
44 [super setFrame:frame];
45 [self TPKeyboardAvoiding_updateContentInset];
46 }
47
48 -(void)setContentSize:(CGSize)contentSize {
49 [super setContentSize:contentSize];
50 [self TPKeyboardAvoiding_updateFromContentSizeChange];
51 }
52
53 - (void)contentSizeToFit {
54 self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames];
55 }
56
57 - (BOOL)focusNextTextField {
58 return [self TPKeyboardAvoiding_focusNextTextField];
59
60 }
61 - (void)scrollToActiveTextField {
62 return [self TPKeyboardAvoiding_scrollToActiveTextField];
63 }
64
65 #pragma mark - Responders, events
66
67 -(void)willMoveToSuperview:(UIView *)newSuperview {
68 [super willMoveToSuperview:newSuperview];
69 if ( !newSuperview ) {
70 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self];
71 }
72 }
73
74 - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
75 [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder];
76 [super touchesEnded:touches withEvent:event];
77 }
78
79 -(BOOL)textFieldShouldReturn:(UITextField *)textField {
80 if ( ![self focusNextTextField] ) {
81 [textField resignFirstResponder];
82 }
83 return YES;
84 }
85
86 -(void)layoutSubviews {
87 [super layoutSubviews];
88 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self];
89 [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1];
90 }
91
92 @end
93
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h
File was created 1 //
2 // TPKeyboardAvoidingTableView.h
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel. All rights reserved.
7 //
8
9 #import <UIKit/UIKit.h>
10 #import "UIScrollView+TPKeyboardAvoidingAdditions.h"
11
12 @interface TPKeyboardAvoidingTableView : UITableView <UITextFieldDelegate, UITextViewDelegate>
13 - (BOOL)focusNextTextField;
14 - (void)scrollToActiveTextField;
15 @end
16
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m
File was created 1 //
2 // TPKeyboardAvoidingTableView.m
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel. All rights reserved.
7 //
8
9 #import "TPKeyboardAvoidingTableView.h"
10
11 @interface TPKeyboardAvoidingTableView () <UITextFieldDelegate, UITextViewDelegate>
12 @end
13
14 @implementation TPKeyboardAvoidingTableView
15
16 #pragma mark - Setup/Teardown
17
18 - (void)setup {
19 if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return;
20
21 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil];
22 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(TPKeyboardAvoiding_keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
23 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextViewTextDidBeginEditingNotification object:nil];
24 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollToActiveTextField) name:UITextFieldTextDidBeginEditingNotification object:nil];
25 }
26
27 -(id)initWithFrame:(CGRect)frame {
28 if ( !(self = [super initWithFrame:frame]) ) return nil;
29 [self setup];
30 return self;
31 }
32
33 -(id)initWithFrame:(CGRect)frame style:(UITableViewStyle)withStyle {
34 if ( !(self = [super initWithFrame:frame style:withStyle]) ) return nil;
35 [self setup];
36 return self;
37 }
38
39 -(void)awakeFromNib {
40 [super awakeFromNib];
41 [self setup];
42 }
43
44 -(void)dealloc {
45 [[NSNotificationCenter defaultCenter] removeObserver:self];
46 #if !__has_feature(objc_arc)
47 [super dealloc];
48 #endif
49 }
50
51 -(BOOL)hasAutomaticKeyboardAvoidingBehaviour {
52 if ( [self.delegate isKindOfClass:[UITableViewController class]] ) {
53 // Theory: Apps built using the iOS 8.3 SDK (probably: older SDKs not tested) seem to handle keyboard
54 // avoiding automatically with UITableViewController. This doesn't seem to be documented anywhere
55 // by Apple, so results obtained only empirically.
56 return YES;
57 }
58
59 return NO;
60 }
61
62 -(void)setFrame:(CGRect)frame {
63 [super setFrame:frame];
64 if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) return;
65 [self TPKeyboardAvoiding_updateContentInset];
66 }
67
68 -(void)setContentSize:(CGSize)contentSize {
69 if ( [self hasAutomaticKeyboardAvoidingBehaviour] ) {
70 [super setContentSize:contentSize];
71 return;
72 }
73 if (CGSizeEqualToSize(contentSize, self.contentSize)) {
74 // Prevent triggering contentSize when it's already the same
75 // this cause table view to scroll to top on contentInset changes
76 return;
77 }
78 [super setContentSize:contentSize];
79 [self TPKeyboardAvoiding_updateContentInset];
80 }
81
82 - (BOOL)focusNextTextField {
83 return [self TPKeyboardAvoiding_focusNextTextField];
84
85 }
86 - (void)scrollToActiveTextField {
87 return [self TPKeyboardAvoiding_scrollToActiveTextField];
88 }
89
90 #pragma mark - Responders, events
91
92 -(void)willMoveToSuperview:(UIView *)newSuperview {
93 [super willMoveToSuperview:newSuperview];
94 if ( !newSuperview ) {
95 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self];
96 }
97 }
98
99 - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
100 [[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] resignFirstResponder];
101 [super touchesEnded:touches withEvent:event];
102 }
103
104 -(BOOL)textFieldShouldReturn:(UITextField *)textField {
105 if ( ![self focusNextTextField] ) {
106 [textField resignFirstResponder];
107 }
108 return YES;
109 }
110
111 -(void)layoutSubviews {
112 [super layoutSubviews];
113 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) object:self];
114 [self performSelector:@selector(TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:) withObject:self afterDelay:0.1];
115 }
116
117 @end
118
LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h
File was created 1 //
2 // UIScrollView+TPKeyboardAvoidingAdditions.h
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel. All rights reserved.
7 //
8
9 #import <UIKit/UIKit.h>
10
11 @interface UIScrollView (TPKeyboardAvoidingAdditions)
12 - (BOOL)TPKeyboardAvoiding_focusNextTextField;
13 - (void)TPKeyboardAvoiding_scrollToActiveTextField;
14
15 - (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification;
16 - (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification;
17 - (void)TPKeyboardAvoiding_updateContentInset;
18 - (void)TPKeyboardAvoiding_updateFromContentSizeChange;
19 - (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view;
20 - (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view;
21 -(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames;
22 @end
23
LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m
File was created 1 //
2 // UIScrollView+TPKeyboardAvoidingAdditions.m
3 // TPKeyboardAvoiding
4 //
5 // Created by Michael Tyson on 30/09/2013.
6 // Copyright 2015 A Tasty Pixel. All rights reserved.
7 //
8
9 #import "UIScrollView+TPKeyboardAvoidingAdditions.h"
10 #import "TPKeyboardAvoidingScrollView.h"
11 #import <objc/runtime.h>
12
13 static const CGFloat kCalculatedContentPadding = 10;
14 static const CGFloat kMinimumScrollOffsetPadding = 20;
15
16 static NSString * const kUIKeyboardAnimationDurationUserInfoKey = @"UIKeyboardAnimationDurationUserInfoKey";
17
18 static const int kStateKey;
19
20 #define _UIKeyboardFrameEndUserInfoKey (&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey")
21
22 @interface TPKeyboardAvoidingState : NSObject
23 @property (nonatomic, assign) UIEdgeInsets priorInset;
24 @property (nonatomic, assign) UIEdgeInsets priorScrollIndicatorInsets;
25 @property (nonatomic, assign) BOOL keyboardVisible;
26 @property (nonatomic, assign) CGRect keyboardRect;
27 @property (nonatomic, assign) CGSize priorContentSize;
28 @property (nonatomic, assign) BOOL priorPagingEnabled;
29 @property (nonatomic, assign) BOOL ignoringNotifications;
30 @property (nonatomic, assign) BOOL keyboardAnimationInProgress;
31 @property (nonatomic, assign) CGFloat animationDuration;
32 @end
33
34 @implementation UIScrollView (TPKeyboardAvoidingAdditions)
35
36 - (TPKeyboardAvoidingState*)keyboardAvoidingState {
37 TPKeyboardAvoidingState *state = objc_getAssociatedObject(self, &kStateKey);
38 if ( !state ) {
39 state = [[TPKeyboardAvoidingState alloc] init];
40 objc_setAssociatedObject(self, &kStateKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
41 #if !__has_feature(objc_arc)
42 [state release];
43 #endif
44 }
45 return state;
46 }
47
48 - (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification {
49 NSDictionary *info = [notification userInfo];
50 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
51
52 state.animationDuration = [[info objectForKey:kUIKeyboardAnimationDurationUserInfoKey] doubleValue];
53
54 CGRect keyboardRect = [self convertRect:[[info objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
55 if (CGRectIsEmpty(keyboardRect)) {
56 return;
57 }
58
59 if ( state.ignoringNotifications ) {
60 return;
61 }
62
63 state.keyboardRect = keyboardRect;
64
65 if ( !state.keyboardVisible ) {
66 state.priorInset = self.contentInset;
67 state.priorScrollIndicatorInsets = self.scrollIndicatorInsets;
68 state.priorPagingEnabled = self.pagingEnabled;
69 }
70
71 state.keyboardVisible = YES;
72 self.pagingEnabled = NO;
73
74 if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) {
75 state.priorContentSize = self.contentSize;
76
77 if ( CGSizeEqualToSize(self.contentSize, CGSizeZero) ) {
78 // Set the content size, if it's not set. Do not set content size explicitly if auto-layout
79 // is being used to manage subviews
80 self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames];
81 }
82 }
83
84 // Delay until a future run loop such that the cursor position is available in a text view
85 // In other words, it's not available (specifically, the prior cursor position is returned) when the first keyboard position change notification fires
86 // NOTE: Unfortunately, using dispatch_async(main_queue) did not result in a sufficient-enough delay
87 // for the text view's current cursor position to be available
88 dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC));
89 dispatch_after(delay, dispatch_get_main_queue(), ^{
90
91 // Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited
92 [UIView beginAnimations:nil context:NULL];
93
94 [UIView setAnimationDelegate:self];
95 [UIView setAnimationWillStartSelector:@selector(keyboardViewAppear:context:)];
96 [UIView setAnimationDidStopSelector:@selector(keyboardViewDisappear:finished:context:)];
97
98 [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
99 [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]];
100
101 UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];
102 if ( firstResponder ) {
103
104 self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
105
106 CGFloat viewableHeight = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
107 [self setContentOffset:CGPointMake(self.contentOffset.x,
108 [self TPKeyboardAvoiding_idealOffsetForView:firstResponder
109 withViewingAreaHeight:viewableHeight])
110 animated:NO];
111 }
112
113 self.scrollIndicatorInsets = self.contentInset;
114 [self layoutIfNeeded];
115
116 [UIView commitAnimations];
117 });
118 }
119
120 - (void)keyboardViewAppear:(NSString *)animationID context:(void *)context {
121 self.keyboardAvoidingState.keyboardAnimationInProgress = true;
122 }
123
124 - (void)keyboardViewDisappear:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
125 if (finished.boolValue) {
126 self.keyboardAvoidingState.keyboardAnimationInProgress = false;
127 }
128 }
129
130 - (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification {
131 CGRect keyboardRect = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
132 if (CGRectIsEmpty(keyboardRect) && !self.keyboardAvoidingState.keyboardAnimationInProgress) {
133 return;
134 }
135
136 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
137
138 if ( state.ignoringNotifications ) {
139 return;
140 }
141
142 if ( !state.keyboardVisible ) {
143 return;
144 }
145
146 state.keyboardRect = CGRectZero;
147 state.keyboardVisible = NO;
148
149 // Restore dimensions to prior size
150 [UIView beginAnimations:nil context:NULL];
151 [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
152 [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]];
153
154 if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) {
155 self.contentSize = state.priorContentSize;
156 }
157
158 self.contentInset = state.priorInset;
159 self.scrollIndicatorInsets = state.priorScrollIndicatorInsets;
160 self.pagingEnabled = state.priorPagingEnabled;
161 [self layoutIfNeeded];
162 [UIView commitAnimations];
163 }
164
165 - (void)TPKeyboardAvoiding_updateContentInset {
166 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
167 if ( state.keyboardVisible ) {
168 self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
169 }
170 }
171
172 - (void)TPKeyboardAvoiding_updateFromContentSizeChange {
173 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
174 if ( state.keyboardVisible ) {
175 state.priorContentSize = self.contentSize;
176 self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
177 }
178 }
179
180 #pragma mark - Utilities
181
182 - (BOOL)TPKeyboardAvoiding_focusNextTextField {
183 UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];
184 if ( !firstResponder ) {
185 return NO;
186 }
187
188 UIView *view = [self TPKeyboardAvoiding_findNextInputViewAfterView:firstResponder beneathView:self];
189
190 if ( view ) {
191 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), dispatch_get_main_queue(), ^{
192 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
193 state.ignoringNotifications = YES;
194 [view becomeFirstResponder];
195 state.ignoringNotifications = NO;
196 });
197 return YES;
198 }
199
200 return NO;
201 }
202
203 -(void)TPKeyboardAvoiding_scrollToActiveTextField {
204 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
205
206 if ( !state.keyboardVisible ) return;
207
208 UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];
209 if ( !firstResponder ) {
210 return;
211 }
212 // Ignore any keyboard notification that occur while we scroll
213 // (seems to be an iOS 9 bug that causes jumping text in UITextField)
214 state.ignoringNotifications = YES;
215
216 CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
217
218 CGPoint idealOffset
219 = CGPointMake(self.contentOffset.x,
220 [self TPKeyboardAvoiding_idealOffsetForView:firstResponder
221 withViewingAreaHeight:visibleSpace]);
222
223 // Ordinarily we'd use -setContentOffset:animated:YES here, but it interferes with UIScrollView
224 // behavior which automatically ensures that the first responder is within its bounds
225 [UIView animateWithDuration:state.animationDuration animations:^{
226 self.contentOffset = idealOffset;
227 } completion:^(BOOL finished) {
228 state.ignoringNotifications = NO;
229 }];
230 }
231
232 #pragma mark - Helpers
233
234 - (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view {
235 // Search recursively for first responder
236 for ( UIView *childView in view.subviews ) {
237 if ( [childView respondsToSelector:@selector(isFirstResponder)] && [childView isFirstResponder] ) return childView;
238 UIView *result = [self TPKeyboardAvoiding_findFirstResponderBeneathView:childView];
239 if ( result ) return result;
240 }
241 return nil;
242 }
243
244 - (UIView*)TPKeyboardAvoiding_findNextInputViewAfterView:(UIView*)priorView beneathView:(UIView*)view {
245 UIView * candidate = nil;
246 [self TPKeyboardAvoiding_findNextInputViewAfterView:priorView beneathView:view bestCandidate:&candidate];
247 return candidate;
248 }
249
250 - (void)TPKeyboardAvoiding_findNextInputViewAfterView:(UIView*)priorView beneathView:(UIView*)view bestCandidate:(UIView**)bestCandidate {
251 // Search recursively for input view below/to right of priorTextField
252 CGRect priorFrame = [self convertRect:priorView.frame fromView:priorView.superview];
253 CGRect candidateFrame = *bestCandidate ? [self convertRect:(*bestCandidate).frame fromView:(*bestCandidate).superview] : CGRectZero;
254 CGFloat bestCandidateHeuristic = [self TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:candidateFrame];
255
256 for ( UIView *childView in view.subviews ) {
257 if ( [self TPKeyboardAvoiding_viewIsValidKeyViewCandidate:childView] ) {
258 CGRect frame = [self convertRect:childView.frame fromView:view];
259
260 // Use a heuristic to evaluate candidates
261 CGFloat heuristic = [self TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:frame];
262
263 // Find views beneath, or to the right. For those views that match, choose the view closest to the top left
264 if ( childView != priorView
265 && ((fabs(CGRectGetMinY(frame) - CGRectGetMinY(priorFrame)) < FLT_EPSILON && CGRectGetMinX(frame) > CGRectGetMinX(priorFrame))
266 || CGRectGetMinY(frame) > CGRectGetMinY(priorFrame))
267 && (!*bestCandidate || heuristic > bestCandidateHeuristic) ) {
268
269 *bestCandidate = childView;
270 bestCandidateHeuristic = heuristic;
271 }
272 } else {
273 [self TPKeyboardAvoiding_findNextInputViewAfterView:priorView beneathView:childView bestCandidate:bestCandidate];
274 }
275 }
276 }
277
278 - (CGFloat)TPKeyboardAvoiding_nextInputViewHeuristicForViewFrame:(CGRect)frame {
279 return (-frame.origin.y * 1000.0) // Prefer elements closest to top (most important)
280 + (-frame.origin.x); // Prefer elements closest to left
281 }
282
283 - (BOOL)TPKeyboardAvoiding_viewHiddenOrUserInteractionNotEnabled:(UIView *)view {
284 while ( view ) {
285 if ( view.hidden || !view.userInteractionEnabled ) {
286 return YES;
287 }
288 view = view.superview;
289 }
290 return NO;
291 }
292
293 - (BOOL)TPKeyboardAvoiding_viewIsValidKeyViewCandidate:(UIView *)view {
294 if ( [self TPKeyboardAvoiding_viewHiddenOrUserInteractionNotEnabled:view] ) return NO;
295
296 if ( [view isKindOfClass:[UITextField class]] && ((UITextField*)view).enabled ) {
297 return YES;
298 }
299
300 if ( [view isKindOfClass:[UITextView class]] && ((UITextView*)view).isEditable ) {
301 return YES;
302 }
303
304 return NO;
305 }
306
307 - (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view {
308 for ( UIView *childView in view.subviews ) {
309 if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) ) {
310 [self TPKeyboardAvoiding_initializeView:childView];
311 } else {
312 [self TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:childView];
313 }
314 }
315 }
316
317 -(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames {
318
319 BOOL wasShowingVerticalScrollIndicator = self.showsVerticalScrollIndicator;
320 BOOL wasShowingHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;
321
322 self.showsVerticalScrollIndicator = NO;
323 self.showsHorizontalScrollIndicator = NO;
324
325 CGRect rect = CGRectZero;
326 for ( UIView *view in self.subviews ) {
327 rect = CGRectUnion(rect, view.frame);
328 }
329 rect.size.height += kCalculatedContentPadding;
330
331 self.showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator;
332 self.showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator;
333
334 return rect.size;
335 }
336
337
338 - (UIEdgeInsets)TPKeyboardAvoiding_contentInsetForKeyboard {
339 TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
340 UIEdgeInsets newInset = self.contentInset;
341 CGRect keyboardRect = state.keyboardRect;
342 newInset.bottom = keyboardRect.size.height - MAX((CGRectGetMaxY(keyboardRect) - CGRectGetMaxY(self.bounds)), 0);
343 return newInset;
344 }
345
346 -(CGFloat)TPKeyboardAvoiding_idealOffsetForView:(UIView *)view withViewingAreaHeight:(CGFloat)viewAreaHeight {
347 CGSize contentSize = self.contentSize;
348 __block CGFloat offset = 0.0;
349
350 CGRect subviewRect = [view convertRect:view.bounds toView:self];
351
352 __block CGFloat padding = 0.0;
353
354 void(^centerViewInViewableArea)() = ^ {
355 // Attempt to center the subview in the visible space
356 padding = (viewAreaHeight - subviewRect.size.height) / 2;
357
358 // But if that means there will be less than kMinimumScrollOffsetPadding
359 // pixels above the view, then substitute kMinimumScrollOffsetPadding
360 if (padding < kMinimumScrollOffsetPadding ) {
361 padding = kMinimumScrollOffsetPadding;
362 }
363
364 // Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview.
365 // If there is a top contentInset, also compensate for this so that subviewRect will not be placed under
366 // things like navigation bars.
367 offset = subviewRect.origin.y - padding - self.contentInset.top;
368 };
369
370 // If possible, center the caret in the visible space. Otherwise, center the entire view in the visible space.
371 if ([view conformsToProtocol:@protocol(UITextInput)]) {
372 UIView <UITextInput> *textInput = (UIView <UITextInput>*)view;
373 UITextPosition *caretPosition = [textInput selectedTextRange].start;
374 if (caretPosition) {
375 CGRect caretRect = [self convertRect:[textInput caretRectForPosition:caretPosition] fromView:textInput];
376
377 // Attempt to center the cursor in the visible space
378 // pixels above the view, then substitute kMinimumScrollOffsetPadding
379 padding = (viewAreaHeight - caretRect.size.height) / 2;
380
381 // But if that means there will be less than kMinimumScrollOffsetPadding
382 // pixels above the view, then substitute kMinimumScrollOffsetPadding
383 if (padding < kMinimumScrollOffsetPadding ) {
384 padding = kMinimumScrollOffsetPadding;
385 }
386
387 // Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview.
388 // If there is a top contentInset, also compensate for this so that subviewRect will not be placed under
389 // things like navigation bars.
390 offset = caretRect.origin.y - padding - self.contentInset.top;
391 } else {
392 centerViewInViewableArea();
393 }
394 } else {
395 centerViewInViewableArea();
396 }
397
398 // Constrain the new contentOffset so we can't scroll past the bottom. Note that we don't take the bottom
399 // inset into account, as this is manipulated to make space for the keyboard.
400 CGFloat maxOffset = contentSize.height - viewAreaHeight - self.contentInset.top;
401 if (offset > maxOffset) {
402 offset = maxOffset;
403 }
404
405 // Constrain the new contentOffset so we can't scroll past the top, taking contentInsets into account
406 if ( offset < -self.contentInset.top ) {
407 offset = -self.contentInset.top;
408 }
409
410 return offset;
411 }
412
413 - (void)TPKeyboardAvoiding_initializeView:(UIView*)view {
414 if ( [view isKindOfClass:[UITextField class]]
415 && ((UITextField*)view).returnKeyType == UIReturnKeyDefault
416 && (![(UITextField*)view delegate] || [(UITextField*)view delegate] == (id<UITextFieldDelegate>)self) ) {
417 [(UITextField*)view setDelegate:(id<UITextFieldDelegate>)self];
418 UIView *otherView = [self TPKeyboardAvoiding_findNextInputViewAfterView:view beneathView:self];
419
420 if ( otherView ) {
421 ((UITextField*)view).returnKeyType = UIReturnKeyNext;
422 } else {
423 ((UITextField*)view).returnKeyType = UIReturnKeyDone;
424 }
425 }
426 }
427
428 @end
429
430
431 @implementation TPKeyboardAvoidingState
432 @end
433