Restore the input toolbar's placeholder text.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-10-17 07:07:57 -07:00
parent 7d3df0bf0a
commit 4a94d039e8
5 changed files with 164 additions and 47 deletions

View File

@ -18,10 +18,22 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark -
@protocol ConversationTextViewToolbarDelegate <NSObject>
- (void)textViewDidChange;
- (void)textViewReturnPressed;
@end
#pragma mark -
@interface ConversationInputTextView : UITextView
@property (weak, nonatomic) id<ConversationInputTextViewDelegate> inputTextViewDelegate;
@property (weak, nonatomic) id<ConversationTextViewToolbarDelegate> textViewToolbarDelegate;
- (NSString *)trimmedText;
@end

View File

@ -8,6 +8,16 @@
NS_ASSUME_NONNULL_BEGIN
@interface ConversationInputTextView () <UITextViewDelegate>
@property (nonatomic) UILabel *placeholderView;
@property (nonatomic) NSArray<NSLayoutConstraint *> *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

View File

@ -20,9 +20,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)textViewDidChange;
// TODO: Is this necessary.
//- (void)textViewDidBeginEditing;
@end
#pragma mark -

View File

@ -14,25 +14,26 @@ NS_ASSUME_NONNULL_BEGIN
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, UITextViewDelegate>
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>
@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<NSLayoutConstraint *> *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
#pragma mark - ConversationTextViewToolbarDelegate
- (void)textViewDidBeginEditing:(UITextView *)textView
{
OWSAssert(textView == self.inputTextView);
[textView becomeFirstResponder];
}
- (void)textViewDidChange:(UITextView *)textView
- (void)textViewDidChange
{
OWSAssert(self.inputToolbarDelegate);
OWSAssert(textView == self.inputTextView);
[self ensureShouldShowVoiceMemoButton];
[self.inputToolbarDelegate textViewDidChange];
}
- (void)textViewDidEndEditing:(UITextView *)textView
- (void)textViewReturnPressed
{
OWSAssert(textView == self.inputTextView);
[textView resignFirstResponder];
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if (range.length > 0) {
return YES;
}
if ([text isEqualToString:@"\n"]) {
[self sendButtonPressed];
return NO;
}
return YES;
[self sendButtonPressed];
}
#pragma mark - Text Input Sizing

View File

@ -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]);