From 4a94d039e8fa8860be98ab81ca8074dcc906bcb0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 17 Oct 2017 07:07:57 -0700 Subject: [PATCH] Restore the input toolbar's placeholder text. // FREEBIE --- .../ConversationInputTextView.h | 12 ++ .../ConversationInputTextView.m | 137 +++++++++++++++++- .../ConversationInputToolbar.h | 3 - .../ConversationInputToolbar.m | 57 +++----- .../ConversationViewController.m | 2 + 5 files changed, 164 insertions(+), 47 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h index e86bb9ecf..314316124 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h @@ -18,10 +18,22 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - +@protocol ConversationTextViewToolbarDelegate + +- (void)textViewDidChange; + +- (void)textViewReturnPressed; + +@end + +#pragma mark - + @interface ConversationInputTextView : UITextView @property (weak, nonatomic) id inputTextViewDelegate; +@property (weak, nonatomic) id textViewToolbarDelegate; + - (NSString *)trimmedText; @end diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m index 535ac1fbb..2a42eec12 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m @@ -8,6 +8,16 @@ NS_ASSUME_NONNULL_BEGIN +@interface ConversationInputTextView () + +@property (nonatomic) UILabel *placeholderView; +@property (nonatomic) NSArray *placeholderConstraints; +@property (nonatomic) BOOL isEditing; + +@end + +#pragma mark - + @implementation ConversationInputTextView - (instancetype)init @@ -16,9 +26,10 @@ NS_ASSUME_NONNULL_BEGIN if (self) { [self setTranslatesAutoresizingMaskIntoConstraints:NO]; + self.delegate = self; + CGFloat cornerRadius = 6.0f; - self.font = [UIFont ows_dynamicTypeBodyFont]; self.backgroundColor = [UIColor whiteColor]; self.layer.borderColor = [UIColor lightGrayColor].CGColor; self.layer.borderWidth = 0.5f; @@ -26,9 +37,6 @@ NS_ASSUME_NONNULL_BEGIN self.scrollIndicatorInsets = UIEdgeInsetsMake(cornerRadius, 0.0f, cornerRadius, 0.0f); - self.textContainerInset = UIEdgeInsetsMake(4.0f, 2.0f, 4.0f, 2.0f); - self.contentInset = UIEdgeInsetsMake(1.0f, 0.0f, 1.0f, 0.0f); - self.scrollEnabled = YES; self.scrollsToTop = NO; self.userInteractionEnabled = YES; @@ -45,13 +53,89 @@ NS_ASSUME_NONNULL_BEGIN self.text = nil; - // _placeHolder = nil; - // _placeHolderTextColor = [UIColor lightGrayColor]; + self.placeholderView = [UILabel new]; + self.placeholderView.text = NSLocalizedString(@"new_message", @""); + self.placeholderView.textColor = [UIColor lightGrayColor]; + self.placeholderView.textAlignment = NSTextAlignmentLeft; + [self addSubview:self.placeholderView]; + + // We need to do these steps _after_ placeholderView is configured. + self.font = [UIFont ows_dynamicTypeBodyFont]; + self.textContainerInset = UIEdgeInsetsMake(4.0f, 2.0f, 4.0f, 2.0f); + self.contentInset = UIEdgeInsetsMake(1.0f, 0.0f, 1.0f, 0.0f); + + [self ensurePlaceholderConstraints]; + [self updatePlaceholderVisibility]; } return self; } +- (void)setFont:(UIFont *_Nullable)font +{ + [super setFont:font]; + + self.placeholderView.font = font; +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + [super setContentInset:contentInset]; + + [self ensurePlaceholderConstraints]; +} + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + [super setTextContainerInset:textContainerInset]; + + [self ensurePlaceholderConstraints]; +} + +- (void)ensurePlaceholderConstraints +{ + OWSAssert(self.placeholderView); + + if (self.placeholderConstraints) { + [NSLayoutConstraint deactivateConstraints:self.placeholderConstraints]; + } + + // We align the location of our placeholder with the text content of + // this view. The only safe way to do that is by measuring the + // beginning position. + UITextRange *beginningTextRange = + [self textRangeFromPosition:self.beginningOfDocument toPosition:self.beginningOfDocument]; + CGRect beginningTextRect = [self firstRectForRange:beginningTextRange]; + + CGFloat hInset = beginningTextRect.origin.x; + CGFloat topInset = beginningTextRect.origin.y; + + self.placeholderConstraints = @[ + [self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:hInset], + [self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:hInset], + [self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topInset], + ]; +} + +- (void)updatePlaceholderVisibility +{ + self.placeholderView.hidden = self.text.length > 0 || self.isEditing; +} + +- (void)setText:(NSString *_Nullable)text +{ + [super setText:text]; + + [self updatePlaceholderVisibility]; +} + +- (void)setIsEditing:(BOOL)isEditing +{ + _isEditing = isEditing; + + [self updatePlaceholderVisibility]; +} + - (BOOL)canBecomeFirstResponder { return YES; @@ -246,6 +330,47 @@ NS_ASSUME_NONNULL_BEGIN //} //@end +#pragma mark - UITextViewDelegate + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + // TODO: Is this necessary? + + [textView becomeFirstResponder]; + + self.isEditing = YES; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + OWSAssert(self.textViewToolbarDelegate); + + [self updatePlaceholderVisibility]; + + [self.textViewToolbarDelegate textViewDidChange]; +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + [textView resignFirstResponder]; + + self.isEditing = NO; +} + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + OWSAssert(self.textViewToolbarDelegate); + + if (range.length > 0) { + return YES; + } + if ([text isEqualToString:@"\n"]) { + [self.textViewToolbarDelegate textViewReturnPressed]; + return NO; + } + return YES; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index 6d88aeaae..cdf49a0c2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -20,9 +20,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)textViewDidChange; -// TODO: Is this necessary. -//- (void)textViewDidBeginEditing; - @end #pragma mark - diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 0d29f33f3..821aa8e81 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -14,25 +14,26 @@ NS_ASSUME_NONNULL_BEGIN static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext; -@interface ConversationInputToolbar () +@interface ConversationInputToolbar () + +@property (nonatomic, readonly) ConversationInputTextView *inputTextView; +@property (nonatomic, readonly) UIButton *attachmentButton; +@property (nonatomic, readonly) UIButton *sendButton; +@property (nonatomic, readonly) UIButton *voiceMemoButton; +@property (nonatomic, readonly) UIView *leftButtonWrapper; +@property (nonatomic, readonly) UIView *rightButtonWrapper; -@property (nonatomic) ConversationInputTextView *inputTextView; -@property (nonatomic) UIButton *attachmentButton; -@property (nonatomic) UIButton *sendButton; @property (nonatomic) BOOL shouldShowVoiceMemoButton; -@property (nonatomic) UIButton *voiceMemoButton; -@property (nonatomic) UIView *leftButtonWrapper; -@property (nonatomic) UIView *rightButtonWrapper; @property (nonatomic) NSArray *contentContraints; #pragma mark - Voice Memo Recording UI @property (nonatomic, nullable) UIView *voiceMemoUI; -@property (nonatomic) UIView *voiceMemoContentView; +@property (nonatomic, nullable) UIView *voiceMemoContentView; @property (nonatomic) NSDate *voiceMemoStartTime; @property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer; -@property (nonatomic) UILabel *recordingLabel; +@property (nonatomic, nullable) UILabel *recordingLabel; @property (nonatomic) BOOL isRecordingVoiceMemo; @property (nonatomic) CGPoint voiceMemoGestureStartLocation; @@ -69,7 +70,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex [backgroundView autoPinEdgesToSuperviewEdges]; _inputTextView = [ConversationInputTextView new]; - self.inputTextView.delegate = self; + self.inputTextView.textViewToolbarDelegate = self; [self addSubview:self.inputTextView]; // We want to be permissive about taps on the send and attachment buttons, @@ -111,7 +112,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex UIImage *voiceMemoIcon = [UIImage imageNamed:@"voice-memo-button"]; OWSAssert(voiceMemoIcon); - self.voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom]; + _voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom]; [self.voiceMemoButton setImage:[voiceMemoIcon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal]; self.voiceMemoButton.imageView.tintColor = [UIColor ows_materialBlueColor]; @@ -157,7 +158,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex [self ensureShouldShowVoiceMemoButton]; // TODO: Remove this when we remove the delegate method. - [self textViewDidChange:self.inputTextView]; + [self textViewDidChange]; } - (void)clearTextMessage @@ -495,6 +496,8 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex UIView *oldVoiceMemoUI = self.voiceMemoUI; self.voiceMemoUI = nil; + self.voiceMemoContentView = nil; + self.recordingLabel = nil; NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer; self.voiceMemoUpdateTimer = nil; @@ -572,41 +575,19 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex [self.inputToolbarDelegate attachmentButtonPressed]; } -#pragma mark - UITextViewDelegate - -- (void)textViewDidBeginEditing:(UITextView *)textView -{ - OWSAssert(textView == self.inputTextView); - - [textView becomeFirstResponder]; -} +#pragma mark - ConversationTextViewToolbarDelegate -- (void)textViewDidChange:(UITextView *)textView +- (void)textViewDidChange { OWSAssert(self.inputToolbarDelegate); - OWSAssert(textView == self.inputTextView); [self ensureShouldShowVoiceMemoButton]; [self.inputToolbarDelegate textViewDidChange]; } -- (void)textViewDidEndEditing:(UITextView *)textView -{ - OWSAssert(textView == self.inputTextView); - - [textView resignFirstResponder]; -} - -- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +- (void)textViewReturnPressed { - if (range.length > 0) { - return YES; - } - if ([text isEqualToString:@"\n"]) { - [self sendButtonPressed]; - return NO; - } - return YES; + [self sendButtonPressed]; } #pragma mark - Text Input Sizing diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0455faf9b..aba1b48ee 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3491,6 +3491,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { message:errorMessage]; } +// TODO: Is this necessary? It seems redundant with observing changes to +// the collection view's layout. - (void)textViewDidChangeLayout { OWSAssert([NSThread isMainThread]);