-
Notifications
You must be signed in to change notification settings - Fork 1
/
UIScrollView+TPKeyboardAvoidingAdditions.m
executable file
·307 lines (240 loc) · 12.1 KB
/
UIScrollView+TPKeyboardAvoidingAdditions.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
//
// UIScrollView+TPKeyboardAvoidingAdditions.m
// TPKeyboardAvoidingSample
//
// Created by Michael Tyson on 30/09/2013.
// Copyright 2013 A Tasty Pixel. All rights reserved.
//
#import "UIScrollView+TPKeyboardAvoidingAdditions.h"
#import "TPKeyboardAvoidingScrollView.h"
#import <objc/runtime.h>
static const CGFloat kCalculatedContentPadding = 10;
static const CGFloat kMinimumScrollOffsetPadding = 20;
static const int kStateKey;
#define _UIKeyboardFrameEndUserInfoKey (&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey")
#define fequal(a,b) (fabs((a) - (b)) < DBL_EPSILON)
@interface TPKeyboardAvoidingState : NSObject
@property (nonatomic, assign) UIEdgeInsets priorInset;
@property (nonatomic, assign) UIEdgeInsets priorScrollIndicatorInsets;
@property (nonatomic, assign) BOOL keyboardVisible;
@property (nonatomic, assign) CGRect keyboardRect;
@property (nonatomic, assign) CGSize priorContentSize;
@property (nonatomic) BOOL priorPagingEnabled;
@end
@implementation UIScrollView (TPKeyboardAvoidingAdditions)
- (TPKeyboardAvoidingState*)keyboardAvoidingState {
TPKeyboardAvoidingState *state = objc_getAssociatedObject(self, &kStateKey);
if ( !state ) {
state = [[TPKeyboardAvoidingState alloc] init];
objc_setAssociatedObject(self, &kStateKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
#if !__has_feature(objc_arc)
[state release];
#endif
}
return state;
}
- (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification {
CGRect keyboardRect = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
if (CGRectIsEmpty(keyboardRect)) {
return;
}
TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
if ( state.keyboardVisible ) {
return;
}
UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];
state.keyboardRect = keyboardRect;
state.keyboardVisible = YES;
state.priorInset = self.contentInset;
state.priorScrollIndicatorInsets = self.scrollIndicatorInsets;
state.priorPagingEnabled = self.pagingEnabled;
self.pagingEnabled = NO;
if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) {
state.priorContentSize = self.contentSize;
if ( CGSizeEqualToSize(self.contentSize, CGSizeZero) ) {
// Set the content size, if it's not set. Do not set content size explicitly if auto-layout
// is being used to manage subviews
self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames];
}
}
// 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 = [self TPKeyboardAvoiding_contentInsetForKeyboard];
if ( firstResponder ) {
CGFloat viewableHeight = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
[self setContentOffset:CGPointMake(self.contentOffset.x,
[self TPKeyboardAvoiding_idealOffsetForView:firstResponder
withViewingAreaHeight:viewableHeight])
animated:NO];
}
self.scrollIndicatorInsets = self.contentInset;
[UIView commitAnimations];
}
- (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification {
CGRect keyboardRect = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
if (CGRectIsEmpty(keyboardRect)) {
return;
}
TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
if ( !state.keyboardVisible ) {
return;
}
state.keyboardRect = CGRectZero;
state.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]];
if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) {
self.contentSize = state.priorContentSize;
}
self.contentInset = state.priorInset;
self.scrollIndicatorInsets = state.priorScrollIndicatorInsets;
self.pagingEnabled = state.priorPagingEnabled;
[UIView commitAnimations];
}
- (void)TPKeyboardAvoiding_updateContentInset {
TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
if ( state.keyboardVisible ) {
self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
}
}
- (void)TPKeyboardAvoiding_updateFromContentSizeChange {
TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
if ( state.keyboardVisible ) {
state.priorContentSize = self.contentSize;
self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
}
}
#pragma mark - Utilities
- (BOOL)TPKeyboardAvoiding_focusNextTextField {
UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];
if ( !firstResponder ) {
return NO;
}
CGFloat minY = CGFLOAT_MAX;
UIView *view = nil;
[self TPKeyboardAvoiding_findTextFieldAfterTextField:firstResponder beneathView:self minY:&minY foundView:&view];
if ( view ) {
[view performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.0];
return YES;
}
return NO;
}
-(void)TPKeyboardAvoiding_scrollToActiveTextField {
TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
if ( !state.keyboardVisible ) return;
CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
CGPoint idealOffset = CGPointMake(0, [self TPKeyboardAvoiding_idealOffsetForView:[self TPKeyboardAvoiding_findFirstResponderBeneathView:self]
withViewingAreaHeight:visibleSpace]);
// Ordinarily we'd use -setContentOffset:animated:YES here, but it does not appear to
// scroll to the desired content offset. So we wrap in our own animation block.
[UIView animateWithDuration:0.25 animations:^{
[self setContentOffset:idealOffset animated:NO];
}];
}
#pragma mark - Helpers
- (UIView*)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView*)view {
// Search recursively for first responder
for ( UIView *childView in view.subviews ) {
if ( [childView respondsToSelector:@selector(isFirstResponder)] && [childView isFirstResponder] ) return childView;
UIView *result = [self TPKeyboardAvoiding_findFirstResponderBeneathView:childView];
if ( result ) return result;
}
return nil;
}
- (void)TPKeyboardAvoiding_findTextFieldAfterTextField:(UIView*)priorTextField beneathView:(UIView*)view minY:(CGFloat*)minY foundView:(UIView* __autoreleasing *)foundView {
// Search recursively for text field or text view below priorTextField
CGFloat priorFieldOffset = CGRectGetMinY([self convertRect:priorTextField.frame fromView:priorTextField.superview]);
for ( UIView *childView in view.subviews ) {
if ( childView.hidden ) continue;
if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) && childView.isUserInteractionEnabled) {
CGRect frame = [self convertRect:childView.frame fromView:view];
if ( childView != priorTextField
&& CGRectGetMinY(frame) >= priorFieldOffset
&& CGRectGetMinY(frame) < *minY &&
!(fequal(frame.origin.y, priorTextField.frame.origin.y)
&& frame.origin.x < priorTextField.frame.origin.x) ) {
*minY = CGRectGetMinY(frame);
*foundView = childView;
}
} else {
[self TPKeyboardAvoiding_findTextFieldAfterTextField:priorTextField beneathView:childView minY:minY foundView:foundView];
}
}
}
- (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view {
for ( UIView *childView in view.subviews ) {
if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) ) {
[self TPKeyboardAvoiding_initializeView:childView];
} else {
[self TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:childView];
}
}
}
-(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames {
BOOL wasShowingVerticalScrollIndicator = self.showsVerticalScrollIndicator;
BOOL wasShowingHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
CGRect rect = CGRectZero;
for ( UIView *view in self.subviews ) {
rect = CGRectUnion(rect, view.frame);
}
rect.size.height += kCalculatedContentPadding;
self.showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator;
self.showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator;
return rect.size;
}
- (UIEdgeInsets)TPKeyboardAvoiding_contentInsetForKeyboard {
TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
UIEdgeInsets newInset = self.contentInset;
CGRect keyboardRect = state.keyboardRect;
newInset.bottom = keyboardRect.size.height - MAX((CGRectGetMaxY(keyboardRect) - CGRectGetMaxY(self.bounds)), 0);
return newInset;
}
-(CGFloat)TPKeyboardAvoiding_idealOffsetForView:(UIView *)view withViewingAreaHeight:(CGFloat)viewAreaHeight {
CGSize contentSize = self.contentSize;
CGFloat offset = 0.0;
CGRect subviewRect = [view convertRect:view.bounds toView:self];
// Attempt to center the subview in the visible space, but if that means there will be less than kMinimumScrollOffsetPadding
// pixels above the view, then substitute kMinimumScrollOffsetPadding
CGFloat padding = (viewAreaHeight - subviewRect.size.height) / 2;
if ( padding < kMinimumScrollOffsetPadding ) {
padding = kMinimumScrollOffsetPadding;
}
// Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview.
// If there is a top contentInset, also compensate for this so that subviewRect will not be placed under
// things like navigation bars.
offset = subviewRect.origin.y - padding - self.contentInset.top;
// Constrain the new contentOffset so we can't scroll past the bottom. Note that we don't take the bottom
// inset into account, as this is manipulated to make space for the keyboard.
if ( offset > (contentSize.height - viewAreaHeight) ) {
offset = contentSize.height - viewAreaHeight;
}
// Constrain the new contentOffset so we can't scroll past the top, taking contentInsets into account
if ( offset < -self.contentInset.top ) {
offset = -self.contentInset.top;
}
return offset;
}
- (void)TPKeyboardAvoiding_initializeView:(UIView*)view {
if ( [view isKindOfClass:[UITextField class]]
&& ((UITextField*)view).returnKeyType == UIReturnKeyDefault
&& (![(UITextField*)view delegate] || [(UITextField*)view delegate] == (id<UITextFieldDelegate>)self) ) {
[(UITextField*)view setDelegate:(id<UITextFieldDelegate>)self];
UIView *otherView = nil;
CGFloat minY = CGFLOAT_MAX;
[self TPKeyboardAvoiding_findTextFieldAfterTextField:view beneathView:self minY:&minY foundView:&otherView];
if ( otherView ) {
((UITextField*)view).returnKeyType = UIReturnKeyNext;
} else {
((UITextField*)view).returnKeyType = UIReturnKeyDone;
}
}
}
@end
@implementation TPKeyboardAvoidingState
@end