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 Side-by-side 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 | -// | |
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingCollectionView.m
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.h
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingScrollView.m
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.h
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/TPKeyboardAvoidingTableView.m
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.h
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 |
LifeLog/LifeLog/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m
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 |
LifeLog/LifeLog/TPKeyboardAvoidingCollectionView.h
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 |
LifeLog/LifeLog/TPKeyboardAvoidingCollectionView.m
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 |
LifeLog/LifeLog/TPKeyboardAvoidingScrollView.h
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 |
LifeLog/LifeLog/TPKeyboardAvoidingScrollView.m
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 |
LifeLog/LifeLog/TPKeyboardAvoidingTableView.h
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 |
LifeLog/LifeLog/TPKeyboardAvoidingTableView.m
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 |
LifeLog/LifeLog/UIScrollView+TPKeyboardAvoidingAdditions.h
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 |
LifeLog/LifeLog/UIScrollView+TPKeyboardAvoidingAdditions.m
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 |