Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Source/SLKTextInputbar.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ UIKIT_EXTERN NSString * const SLKTextInputbarContentSizeDidChangeNotification;
/** The inner padding to use when laying out content in the view. Default is {5, 8, 5, 8}. */
@property (nonatomic, assign) UIEdgeInsets contentInset;

/** The minimum size of the left and right button, so they keep an accessible touch target even with a small image. Default is {44, 44}. */
@property (nonatomic, assign) CGSize minimumButtonSize;

/** YES if the left and right button stay in place while editing, instead of being replaced by the buttons of the
editor content view. Use this together with an editorContentViewHeight of 0 to edit in the input bar itself. Default is NO. */
@property (nonatomic, assign) BOOL keepsButtonsWhileEditing;

/** The minimum height based on the intrinsic content size's. */
@property (nonatomic, readonly) CGFloat minimumInputbarHeight;

Expand All @@ -87,6 +94,16 @@ UIKIT_EXTERN NSString * const SLKTextInputbarContentSizeDidChangeNotification;
- (instancetype)initWithTextViewClass:(Class)textViewClass withTypingIndicatorViewClass:(Class)typingIndicatorClass;


/**
Recalculates the size and position of the left and right button.

Their image is observed to do this automatically, but that observation is bound to the imageView of the button
at the time it was set up. UIKit recreates that imageView when the button is given a UIButtonConfiguration, so
call this after changing the image of such a button.
*/
- (void)invalidateButtonSizes;


#pragma mark - Text Editing
///------------------------------------------------
/// @name Text Editing
Expand Down
110 changes: 77 additions & 33 deletions Source/SLKTextInputbar.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
NSString * const SLKTextInputbarDidMoveNotification = @"SLKTextInputbarDidMoveNotification";
NSString * const SLKTextInputbarContentSizeDidChangeNotification = @"SLKTextInputbarContentSizeDidChangeNotification";

CGFloat const SLKTextInputbarMinButtonWidth = 44.0;
CGFloat const SLKTextInputbarMinButtonHeight = 44.0;
CGFloat const SLKTextInputbarTypingIndicatorHeight = 24.0;

@interface SLKTextInputbar ()
Expand All @@ -40,6 +38,7 @@ @interface SLKTextInputbar ()
@property (nonatomic, strong) NSLayoutConstraint *typingIndicatorViewHC;
@property (nonatomic, strong) NSLayoutConstraint *typingIndicatorViewTextViewPaddingConstraint;
@property (nonatomic, strong) NSArray *charCountLabelVCs;
@property (nonatomic, strong) NSArray *slk_layoutConstraints;

@property (nonatomic, assign) UIEdgeInsets defaultInsets;

Expand All @@ -50,6 +49,11 @@ @interface SLKTextInputbar ()
@property (nonatomic, strong) Class textViewClass;
@property (nonatomic, strong) Class typingIndicatorClass;

// The objects we actually did register as an observer to. UIKit recreates these helper views for example when
// assigning a UIButtonConfiguration, so we can't rely on asking the buttons for them again when unregistering.
@property (nonatomic, weak) UIImageView *observedLeftButtonImageView;
@property (nonatomic, weak) UILabel *observedRightButtonTitleLabel;

@property (nonatomic, getter=isHidden) BOOL hidden; // Required override

@end
Expand Down Expand Up @@ -104,6 +108,7 @@ - (void)slk_commonInit

self.autoHideRightButton = YES;
self.editorContentViewHeight = 38.0;
self.minimumButtonSize = CGSizeMake(44.0, 44.0);
self.defaultInsets = UIEdgeInsetsMake(5.0, 8.0, 5.0, 8.0);
self.contentInset = _defaultInsets;

Expand All @@ -127,9 +132,12 @@ - (void)slk_commonInit

[self slk_registerNotifications];

self.observedLeftButtonImageView = self.leftButton.imageView;
self.observedRightButtonTitleLabel = self.rightButton.titleLabel;

[self slk_registerTo:self.layer forSelector:@selector(position)];
[self slk_registerTo:self.leftButton.imageView forSelector:@selector(image)];
[self slk_registerTo:self.rightButton.titleLabel forSelector:@selector(font)];
[self slk_registerTo:self.observedLeftButtonImageView forSelector:@selector(image)];
[self slk_registerTo:self.observedRightButtonTitleLabel forSelector:@selector(font)];

self.accessibilityIdentifier = @"SLKTextInputbar";
}
Expand Down Expand Up @@ -436,7 +444,7 @@ - (CGFloat)slk_appropriateRightButtonWidth
}

CGFloat width = [self.rightButton intrinsicContentSize].width;
width = (width >= SLKTextInputbarMinButtonWidth) ? width : SLKTextInputbarMinButtonWidth;
width = (width >= self.minimumButtonSize.width) ? width : self.minimumButtonSize.width;
return width;
}

Expand Down Expand Up @@ -483,6 +491,18 @@ - (void)setAutoHideRightButton:(BOOL)hide
[self layoutIfNeeded];
}

- (void)setMinimumButtonSize:(CGSize)minimumButtonSize
{
if (CGSizeEqualToSize(self.minimumButtonSize, minimumButtonSize)) {
return;
}

_minimumButtonSize = minimumButtonSize;

[self slk_updateConstraintConstants];
[self setNeedsLayout];
}

- (void)setContentInset:(UIEdgeInsets)insets
{
if (UIEdgeInsetsEqualToEdgeInsets(self.contentInset, insets)) {
Expand All @@ -495,9 +515,10 @@ - (void)setContentInset:(UIEdgeInsets)insets
}

_contentInset = insets;

// Add new constraints
[self removeConstraints:self.constraints];

// Add new constraints. Only remove the constraints we created ourselves, others might have been added
// by the owner of this view (e.g. to position a background view behind the textView or the buttons).
[self removeConstraints:self.slk_layoutConstraints ? : self.constraints];
[self.editorContentView removeConstraints:self.editorContentView.constraints];
[self slk_setupViewConstraints];
[self setCounterPosition:_counterPosition];
Expand Down Expand Up @@ -569,6 +590,15 @@ - (void)setCounterPosition:(SLKCounterPosition)counterPosition
}


#pragma mark - Button sizing

- (void)invalidateButtonSizes
{
[self slk_updateConstraintConstants];
[self setNeedsLayout];
}


#pragma mark - Text Editing

- (BOOL)canEditText:(NSString *)text
Expand Down Expand Up @@ -701,6 +731,8 @@ - (void)slk_didChangeContentSizeCategory:(NSNotification *)notification

- (void)slk_setupViewConstraints
{
NSArray *foreignConstraints = self.constraints;

NSDictionary *metrics = @{
@"top" : @(self.contentInset.top),
@"left" : @(self.contentInset.left),
Expand Down Expand Up @@ -752,26 +784,36 @@ - (void)slk_setupViewConstraints
self.leftButtonHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.leftButton secondItem:nil];
self.leftButtonBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.leftButton];

self.leftMarginWC = [[self slk_constraintsForAttribute:NSLayoutAttributeLeading] firstObject];

self.rightButtonWC = [self slk_constraintForAttribute:NSLayoutAttributeWidth firstItem:self.rightButton secondItem:nil];
self.rightButtonHC = [self slk_constraintForAttribute:NSLayoutAttributeHeight firstItem:self.rightButton secondItem:nil];
self.rightMarginWC = [[self slk_constraintsForAttribute:NSLayoutAttributeTrailing] firstObject];


self.rightButtonTopMarginC = [self slk_constraintForAttribute:NSLayoutAttributeTop firstItem:self.rightButton secondItem:self];
self.rightButtonBottomMarginC = [self slk_constraintForAttribute:NSLayoutAttributeBottom firstItem:self secondItem:self.rightButton];

// Remember the constraints we own, so -setContentInset: can rebuild them without touching foreign ones
NSMutableArray *layoutConstraints = [self.constraints mutableCopy];
[layoutConstraints removeObjectsInArray:foreignConstraints];
self.slk_layoutConstraints = layoutConstraints;

// The margins are looked up by attribute only, so search our own constraints instead of all of them.
// Otherwise constraints added by the owner of this view (e.g. to position a background view behind the
// textView or the buttons) could be picked up here instead.
self.leftMarginWC = [[layoutConstraints filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"firstAttribute = %d", NSLayoutAttributeLeading]] firstObject];
self.rightMarginWC = [[layoutConstraints filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"firstAttribute = %d", NSLayoutAttributeTrailing]] firstObject];
}

- (void)slk_updateConstraintConstants
{
CGFloat zero = 0.0;

self.textViewBottomMarginC.constant = self.slk_bottomMargin;
self.editorContentViewHC.constant = self.isEditing ? self.editorContentViewHeight : zero;

// While editing, the buttons are replaced by the ones of the editor content view
BOOL hidesButtons = (self.isEditing && !self.keepsButtonsWhileEditing) || self->_hidden;

if (self.isEditing)
if (hidesButtons)
{
self.editorContentViewHC.constant = self.editorContentViewHeight;

self.leftButtonWC.constant = zero;
self.leftButtonHC.constant = zero;
self.leftMarginWC.constant = zero;
Expand All @@ -780,25 +822,17 @@ - (void)slk_updateConstraintConstants
self.rightButtonWC.constant = zero;
self.rightButtonHC.constant = zero;
self.rightMarginWC.constant = zero;

[self slk_updateButtonVisibility];
}
else {
self.editorContentViewHC.constant = zero;

// When the inputbar is hidden, we need to hide the buttons as well
if (self->_hidden) {
self.leftButtonHC.constant = zero;
self.rightButtonHC.constant = zero;

return;
}

CGSize leftButtonSize = [self.leftButton imageForState:self.leftButton.state].size;
CGSize rightButtonSize = [self.rightButton imageForState:self.rightButton.state].size;

if (leftButtonSize.width > 0) {
leftButtonSize.width = (leftButtonSize.width >= SLKTextInputbarMinButtonWidth) ? leftButtonSize.width : SLKTextInputbarMinButtonWidth;
leftButtonSize.width = (leftButtonSize.width >= self.minimumButtonSize.width) ? leftButtonSize.width : self.minimumButtonSize.width;

float leftButtonHeight = (leftButtonSize.height >= SLKTextInputbarMinButtonHeight) ? leftButtonSize.height : SLKTextInputbarMinButtonHeight;
float leftButtonHeight = (leftButtonSize.height >= self.minimumButtonSize.height) ? leftButtonSize.height : self.minimumButtonSize.height;
self.leftButtonHC.constant = roundf(leftButtonHeight);
self.leftButtonBottomMarginC.constant = roundf((self.intrinsicContentSize.height - leftButtonHeight) / 2.0) + self.slk_textViewHeight / 2.0;
}
Expand All @@ -809,12 +843,22 @@ - (void)slk_updateConstraintConstants
self.rightButtonWC.constant = [self slk_appropriateRightButtonWidth];
self.rightMarginWC.constant = [self slk_appropriateRightButtonMargin];

float rightButtonHeight = (rightButtonSize.height >= SLKTextInputbarMinButtonHeight) ? rightButtonSize.height : SLKTextInputbarMinButtonHeight;
float rightButtonHeight = (rightButtonSize.height >= self.minimumButtonSize.height) ? rightButtonSize.height : self.minimumButtonSize.height;
self.rightButtonHC.constant = roundf(rightButtonHeight);
self.rightButtonBottomMarginC.constant = roundf((self.intrinsicContentSize.height - rightButtonHeight) / 2.0) + self.slk_textViewHeight / 2.0;

[self slk_updateButtonVisibility];
}
}

- (void)slk_updateButtonVisibility
{
// Sizing a button to zero does not necessarily hide it: a UIButtonConfiguration draws its background
// (a glass capsule for example) outside of the button's bounds.
self.leftButton.hidden = (self.leftButtonWC.constant <= 0.0 || self.leftButtonHC.constant <= 0.0);
self.rightButton.hidden = (self.rightButtonWC.constant <= 0.0 || self.rightButtonHC.constant <= 0.0);
}


#pragma mark - Observers

Expand All @@ -841,7 +885,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
[[NSNotificationCenter defaultCenter] postNotificationName:SLKTextInputbarDidMoveNotification object:self userInfo:@{@"origin": [NSValue valueWithCGPoint:self.previousOrigin]}];
}
}
else if ([object isEqual:self.leftButton.imageView] && [keyPath isEqualToString:NSStringFromSelector(@selector(image))]) {
else if ([object isEqual:self.observedLeftButtonImageView] && [keyPath isEqualToString:NSStringFromSelector(@selector(image))]) {

UIImage *newImage = change[NSKeyValueChangeNewKey];
UIImage *oldImage = change[NSKeyValueChangeOldKey];
Expand All @@ -850,7 +894,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
[self slk_updateConstraintConstants];
}
}
else if ([object isEqual:self.rightButton.titleLabel] && [keyPath isEqualToString:NSStringFromSelector(@selector(font))]) {
else if ([object isEqual:self.observedRightButtonTitleLabel] && [keyPath isEqualToString:NSStringFromSelector(@selector(font))]) {

[self slk_updateConstraintConstants];
}
Expand Down Expand Up @@ -900,8 +944,8 @@ - (void)dealloc
[self slk_unregisterNotifications];

[self slk_unregisterFrom:self.layer forSelector:@selector(position)];
[self slk_unregisterFrom:self.leftButton.imageView forSelector:@selector(image)];
[self slk_unregisterFrom:self.rightButton.titleLabel forSelector:@selector(font)];
[self slk_unregisterFrom:self.observedLeftButtonImageView forSelector:@selector(image)];
[self slk_unregisterFrom:self.observedRightButtonTitleLabel forSelector:@selector(font)];

[self.typingView removeObserver:self forKeyPath:@"visible"];
}
Expand Down
8 changes: 8 additions & 0 deletions Source/SLKTextViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ NS_CLASS_AVAILABLE_IOS(7_0) @interface SLKTextViewController : UIViewController
*/
@property (nonatomic, assign, getter = isInverted) BOOL inverted;

/**
YES if the scrollView should extend behind the text input bar (and the reply view) instead of ending at its top edge. Default is NO.
When enabled, the scrollView keeps its full height (minus the keyboard) and the area covered by the text input bar is
reserved using the scrollView's bottom content inset instead. Use this for a translucent/glass text input bar,
so the content scrolls underneath it.
*/
@property (nonatomic, assign) BOOL scrollViewExtendsBehindTextInputbar;

/** YES if the view controller is presented inside of a popover controller. If YES, the keyboard won't move the text input bar and tapping on the tableView/collectionView will not cause the keyboard to be dismissed. This property is compatible only with iPad. */
@property (nonatomic, assign, getter = isPresentedInPopover) BOOL presentedInPopover;

Expand Down
Loading