Commit 7597b958ff8048902c85e111ff941a72265a1432
1 parent
f35b3f9a70
Exists in
master
and in
1 other branch
fix bug baseviewcontroller
Showing 16 changed files with 822 additions and 822 deletions Inline Diff
- LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h
- LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m
- LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h
- LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m
- LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h
- LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m
- LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h
- LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m
- LifeLog/LifeLog/TPKeyboardAvoidingCollectionView.h
- LifeLog/LifeLog/TPKeyboardAvoidingCollectionView.m
- LifeLog/LifeLog/TPKeyboardAvoidingScrollView.h
- LifeLog/LifeLog/TPKeyboardAvoidingScrollView.m
- LifeLog/LifeLog/TPKeyboardAvoidingTableView.h
- LifeLog/LifeLog/TPKeyboardAvoidingTableView.m
- LifeLog/LifeLog/UIScrollView+TPKeyboardAvoidingAdditions.h
- LifeLog/LifeLog/UIScrollView+TPKeyboardAvoidingAdditions.m
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.h
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m
1 | // | File was deleted | |
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 | 1 | // |
LifeLog/LifeLog/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/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/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/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/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/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/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/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 |