/* MIT License Copyright (c) 2017-2018 MessageKit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import UIKit /// A powerful InputAccessoryView ideal for messaging applications open class MessageInputBar: UIView { // MARK: - Properties /// A delegate to broadcast notifications from the MessageInputBar open weak var delegate: MessageInputBarDelegate? /// The background UIView anchored to the bottom, left, and right of the MessageInputBar /// with a top anchor equal to the bottom of the top InputStackView open var backgroundView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .inputBarGray return view }() /// A content UIView that holds the left/right/bottom InputStackViews and InputTextView. Anchored to the bottom of the /// topStackView and inset by the padding UIEdgeInsets open var contentView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() /** A UIVisualEffectView that adds a blur effect to make the view appear transparent. ## Important Notes ## 1. The blurView is initially not added to the backgroundView to improve performance when not needed. When `isTranslucent` is set to TRUE for the first time the blurView is added and anchored to the `backgroundView`s edge anchors */ open var blurView: UIVisualEffectView = { let blurEffect = UIBlurEffect(style: .light) let view = UIVisualEffectView(effect: blurEffect) view.translatesAutoresizingMaskIntoConstraints = false return view }() /// Determines if the MessageInputBar should have a translucent effect open var isTranslucent: Bool = false { didSet { if isTranslucent && blurView.superview == nil { backgroundView.addSubview(blurView) blurView.fillSuperview() } blurView.isHidden = !isTranslucent let color: UIColor = backgroundView.backgroundColor ?? .inputBarGray backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color.withAlphaComponent(1.0) } } /// A SeparatorLine that is anchored at the top of the MessageInputBar with a height of 1 public let separatorLine = SeparatorLine() /** The InputStackView at the InputStackView.top position ## Important Notes ## 1. It's axis is initially set to .vertical 2. It's alignment is initially set to .fill */ public let topStackView: InputStackView = { let stackView = InputStackView(axis: .vertical, spacing: 0) stackView.alignment = .fill return stackView }() /** The InputStackView at the InputStackView.left position ## Important Notes ## 1. It's axis is initially set to .horizontal */ public let leftStackView = InputStackView(axis: .horizontal, spacing: 0) /** The InputStackView at the InputStackView.right position ## Important Notes ## 1. It's axis is initially set to .horizontal */ public let rightStackView = InputStackView(axis: .horizontal, spacing: 0) /** The InputStackView at the InputStackView.bottom position ## Important Notes ## 1. It's axis is initially set to .horizontal 2. It's spacing is initially set to 15 */ public let bottomStackView = InputStackView(axis: .horizontal, spacing: 15) /// The InputTextView a user can input a message in open lazy var inputTextView: InputTextView = { let textView = InputTextView() textView.translatesAutoresizingMaskIntoConstraints = false textView.messageInputBar = self return textView }() /// A InputBarButtonItem used as the send button and initially placed in the rightStackView open var sendButton: InputBarButtonItem = { return InputBarButtonItem() .configure { $0.setSize(CGSize(width: 52, height: 28), animated: false) $0.isEnabled = false $0.title = "Send" $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline) }.onTouchUpInside { $0.messageInputBar?.didSelectSendButton() } }() /// A boolean that determines whether the sendButton's `isEnabled` state should be managed automatically. open var shouldManageSendButtonEnabledState = true /** The anchor constants that inset the contentView ```` V:|...[InputStackView.top]-(padding.top)-[contentView]-(padding.bottom)-| H:|-(padding.left)-[contentView]-(padding.right)-| ```` */ open var padding: UIEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) { didSet { updatePadding() } } /** The anchor constants used by the top InputStackView ## Important Notes ## 1. The topStackViewPadding.bottom property is not used. Use padding.top to add separation ```` V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[InputTextView]-...| H:|-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-| ```` */ open var topStackViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) { didSet { updateTopStackViewPadding() } } /** The anchor constants used by the InputStackView ```` V:|...-(padding.top)-(textViewPadding.top)-[InputTextView]-(textViewPadding.bottom)-[InputStackView.bottom]-...| H:|...-[InputStackView.left]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[InputStackView.right]-...| ```` */ open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) { didSet { updateTextViewPadding() } } /// Returns the most recent size calculated by `calculateIntrinsicContentSize()` open override var intrinsicContentSize: CGSize { return cachedIntrinsicContentSize } /// The intrinsicContentSize can change a lot so the delegate method /// `inputBar(self, didChangeIntrinsicContentTo: size)` only needs to be called /// when it's different public private(set) var previousIntrinsicContentSize: CGSize? /// The most recent calculation of the intrinsicContentSize private lazy var cachedIntrinsicContentSize: CGSize = calculateIntrinsicContentSize() /// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this /// improves the performance public private(set) var isOverMaxTextViewHeight = false /// A boolean that determines if the maxTextViewHeight should be auto updated on device rotation open var shouldAutoUpdateMaxTextViewHeight = true /// The maximum height that the InputTextView can reach open var maxTextViewHeight: CGFloat = 0 { didSet { textViewHeightAnchor?.constant = maxTextViewHeight invalidateIntrinsicContentSize() } } /// The height that will fit the current text in the InputTextView based on its current bounds public var requiredInputTextViewHeight: CGFloat { let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude) return inputTextView.sizeThatFits(maxTextViewSize).height.rounded(.down) } /// The fixed widthAnchor constant of the leftStackView public private(set) var leftStackViewWidthConstant: CGFloat = 0 { didSet { leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant } } /// The fixed widthAnchor constant of the rightStackView public private(set) var rightStackViewWidthConstant: CGFloat = 52 { didSet { rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant } } /// The InputBarItems held in the leftStackView public private(set) var leftStackViewItems: [InputBarButtonItem] = [] /// The InputBarItems held in the rightStackView public private(set) var rightStackViewItems: [InputBarButtonItem] = [] /// The InputBarItems held in the bottomStackView public private(set) var bottomStackViewItems: [InputBarButtonItem] = [] /// The InputBarItems held in the topStackView public private(set) var topStackViewItems: [InputBarButtonItem] = [] /// The InputBarItems held to make use of their hooks but they are not automatically added to a UIStackView open var nonStackViewItems: [InputBarButtonItem] = [] /// Returns a flatMap of all the items in each of the UIStackViews public var items: [InputBarButtonItem] { return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, nonStackViewItems].flatMap { $0 } } // MARK: - Auto-Layout Management private var textViewLayoutSet: NSLayoutConstraintSet? private var textViewHeightAnchor: NSLayoutConstraint? private var topStackViewLayoutSet: NSLayoutConstraintSet? private var leftStackViewLayoutSet: NSLayoutConstraintSet? private var rightStackViewLayoutSet: NSLayoutConstraintSet? private var bottomStackViewLayoutSet: NSLayoutConstraintSet? private var contentViewLayoutSet: NSLayoutConstraintSet? private var windowAnchor: NSLayoutConstraint? private var backgroundViewBottomAnchor: NSLayoutConstraint? // MARK: - Initialization public convenience init() { self.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: frame) setup() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } deinit { NotificationCenter.default.removeObserver(self) } open override func didMoveToWindow() { super.didMoveToWindow() setupConstraints(to: window) } // MARK: - Setup /// Sets up the default properties open func setup() { autoresizingMask = [.flexibleHeight] setupSubviews() setupConstraints() setupObservers() } /// Adds the required notification observers private func setupObservers() { NotificationCenter.default.addObserver(self, selector: #selector(MessageInputBar.textViewDidChange), name: UITextView.textDidChangeNotification, object: inputTextView) NotificationCenter.default.addObserver(self, selector: #selector(MessageInputBar.textViewDidBeginEditing), name: UITextView.textDidBeginEditingNotification, object: inputTextView) NotificationCenter.default.addObserver(self, selector: #selector(MessageInputBar.textViewDidEndEditing), name: UITextView.textDidEndEditingNotification, object: inputTextView) } /// Adds all of the subviews private func setupSubviews() { addSubview(backgroundView) addSubview(topStackView) addSubview(contentView) addSubview(separatorLine) contentView.addSubview(inputTextView) contentView.addSubview(leftStackView) contentView.addSubview(rightStackView) contentView.addSubview(bottomStackView) setStackViewItems([sendButton], forStack: .right, animated: false) } // swiftlint:disable function_body_length colon /// Sets up the initial constraints of each subview private func setupConstraints() { // The constraints within the MessageInputBar separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor) backgroundViewBottomAnchor = backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor) backgroundViewBottomAnchor?.isActive = true backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, right: rightAnchor) topStackViewLayoutSet = NSLayoutConstraintSet( top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top), bottom: topStackView.bottomAnchor.constraint(equalTo: contentView.topAnchor, constant: -padding.top), left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left), right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -topStackViewPadding.right) ) contentViewLayoutSet = NSLayoutConstraintSet( top: contentView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.top), bottom: contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding.bottom), left: contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left), right: contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -padding.right) ) if #available(iOS 11.0, *) { // Switch to safeAreaLayoutGuide contentViewLayoutSet?.bottom = contentView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -padding.bottom) contentViewLayoutSet?.left = contentView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left) contentViewLayoutSet?.right = contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right) topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left) topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -topStackViewPadding.right) } // Constraints Within the contentView textViewLayoutSet = NSLayoutConstraintSet( top: inputTextView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: textViewPadding.top), bottom: inputTextView.bottomAnchor.constraint(equalTo: bottomStackView.topAnchor, constant: -textViewPadding.bottom), left: inputTextView.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: textViewPadding.left), right: inputTextView.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -textViewPadding.right) ) maxTextViewHeight = calculateMaxTextViewHeight() textViewHeightAnchor = inputTextView.heightAnchor.constraint(equalToConstant: maxTextViewHeight) leftStackViewLayoutSet = NSLayoutConstraintSet( top: leftStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), bottom: leftStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0), left: leftStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant) ) rightStackViewLayoutSet = NSLayoutConstraintSet( top: rightStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), bottom: rightStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0), right: rightStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0), width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant) ) bottomStackViewLayoutSet = NSLayoutConstraintSet( top: bottomStackView.topAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: textViewPadding.bottom), bottom: bottomStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0), left: bottomStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), right: bottomStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0) ) activateConstraints() } // swiftlint:enable function_body_length colon /// Respect iPhone X safeAreaInsets /// Adds a constraint to anchor the bottomAnchor of the contentView to the window's safeAreaLayoutGuide.bottomAnchor /// /// - Parameter window: The window to anchor to private func setupConstraints(to window: UIWindow?) { if #available(iOS 11.0, *) { guard UIScreen.main.nativeBounds.height == 2436 else { return } if let window = window { windowAnchor?.isActive = false windowAnchor = contentView.bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1) windowAnchor?.constant = -padding.bottom windowAnchor?.priority = UILayoutPriority(rawValue: 750) windowAnchor?.isActive = true backgroundViewBottomAnchor?.constant = 34 } } } // MARK: - Constraint Layout Updates /// Updates the constraint constants that correspond to the padding UIEdgeInsets private func updatePadding() { topStackViewLayoutSet?.bottom?.constant = -padding.top contentViewLayoutSet?.top?.constant = padding.top contentViewLayoutSet?.left?.constant = padding.left contentViewLayoutSet?.right?.constant = -padding.right contentViewLayoutSet?.bottom?.constant = -padding.bottom windowAnchor?.constant = -padding.bottom } /// Updates the constraint constants that correspond to the textViewPadding UIEdgeInsets private func updateTextViewPadding() { textViewLayoutSet?.top?.constant = textViewPadding.top textViewLayoutSet?.left?.constant = textViewPadding.left textViewLayoutSet?.right?.constant = -textViewPadding.right textViewLayoutSet?.bottom?.constant = -textViewPadding.bottom bottomStackViewLayoutSet?.top?.constant = textViewPadding.bottom } /// Updates the constraint constants that correspond to the topStackViewPadding UIEdgeInsets private func updateTopStackViewPadding() { topStackViewLayoutSet?.top?.constant = topStackViewPadding.top topStackViewLayoutSet?.left?.constant = topStackViewPadding.left topStackViewLayoutSet?.right?.constant = -topStackViewPadding.right } /// Invalidates the view’s intrinsic content size open override func invalidateIntrinsicContentSize() { super.invalidateIntrinsicContentSize() cachedIntrinsicContentSize = calculateIntrinsicContentSize() if previousIntrinsicContentSize != cachedIntrinsicContentSize { delegate?.messageInputBar(self, didChangeIntrinsicContentTo: cachedIntrinsicContentSize) previousIntrinsicContentSize = cachedIntrinsicContentSize } } // MARK: - Layout Helper Methods /// Calculates the correct intrinsicContentSize of the MessageInputBar. This takes into account the various padding edge /// insets, InputTextView's height and top/bottom InputStackView's heights. /// /// - Returns: The required intrinsicContentSize open func calculateIntrinsicContentSize() -> CGSize { var inputTextViewHeight = requiredInputTextViewHeight if inputTextViewHeight >= maxTextViewHeight { if !isOverMaxTextViewHeight { textViewHeightAnchor?.isActive = true inputTextView.isScrollEnabled = true isOverMaxTextViewHeight = true } inputTextViewHeight = maxTextViewHeight } else { if isOverMaxTextViewHeight { textViewHeightAnchor?.isActive = false inputTextView.isScrollEnabled = false isOverMaxTextViewHeight = false inputTextView.invalidateIntrinsicContentSize() } } // Calculate the required height let totalPadding = padding.top + padding.bottom + topStackViewPadding.top + textViewPadding.top + textViewPadding.bottom let topStackViewHeight = topStackView.arrangedSubviews.count > 0 ? topStackView.bounds.height : 0 let bottomStackViewHeight = bottomStackView.arrangedSubviews.count > 0 ? bottomStackView.bounds.height : 0 let verticalStackViewHeight = topStackViewHeight + bottomStackViewHeight let requiredHeight = inputTextViewHeight + totalPadding + verticalStackViewHeight return CGSize(width: bounds.width, height: requiredHeight) } /// Returns the max height the InputTextView can grow to based on the UIScreen /// /// - Returns: Max Height open func calculateMaxTextViewHeight() -> CGFloat { if traitCollection.verticalSizeClass == .regular { return (UIScreen.main.bounds.height / 3).rounded(.down) } return (UIScreen.main.bounds.height / 5).rounded(.down) } /// Layout the given InputStackView's /// /// - Parameter positions: The UIStackView's to layout public func layoutStackViews(_ positions: [InputStackView.Position] = [.left, .right, .bottom, .top]) { guard superview != nil else { return } for position in positions { switch position { case .left: leftStackView.setNeedsLayout() leftStackView.layoutIfNeeded() case .right: rightStackView.setNeedsLayout() rightStackView.layoutIfNeeded() case .bottom: bottomStackView.setNeedsLayout() bottomStackView.layoutIfNeeded() case .top: topStackView.setNeedsLayout() topStackView.layoutIfNeeded() } } } /// Performs layout changes over the main thread /// /// - Parameters: /// - animated: If the layout should be animated /// - animations: Code internal func performLayout(_ animated: Bool, _ animations: @escaping () -> Void) { deactivateConstraints() if animated { DispatchQueue.main.async { UIView.animate(withDuration: 0.3, animations: animations) } } else { UIView.performWithoutAnimation { animations() } } activateConstraints() } /// Activates the NSLayoutConstraintSet's private func activateConstraints() { contentViewLayoutSet?.activate() textViewLayoutSet?.activate() leftStackViewLayoutSet?.activate() rightStackViewLayoutSet?.activate() bottomStackViewLayoutSet?.activate() topStackViewLayoutSet?.activate() } /// Deactivates the NSLayoutConstraintSet's private func deactivateConstraints() { contentViewLayoutSet?.deactivate() textViewLayoutSet?.deactivate() leftStackViewLayoutSet?.deactivate() rightStackViewLayoutSet?.deactivate() bottomStackViewLayoutSet?.deactivate() topStackViewLayoutSet?.deactivate() } // MARK: - UIStackView InputBarItem Methods // swiftlint:disable function_body_length /// Removes all of the arranged subviews from the UIStackView and adds the given items. Sets the messageInputBar property of the InputBarButtonItem /// /// - Parameters: /// - items: New UIStackView arranged views /// - position: The targeted UIStackView /// - animated: If the layout should be animated open func setStackViewItems(_ items: [InputBarButtonItem], forStack position: InputStackView.Position, animated: Bool) { func setNewItems() { switch position { case .left: leftStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } leftStackViewItems = items leftStackViewItems.forEach { $0.messageInputBar = self $0.parentStackViewPosition = position leftStackView.addArrangedSubview($0) } guard superview != nil else { return } leftStackView.layoutIfNeeded() case .right: rightStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } rightStackViewItems = items rightStackViewItems.forEach { $0.messageInputBar = self $0.parentStackViewPosition = position rightStackView.addArrangedSubview($0) } guard superview != nil else { return } rightStackView.layoutIfNeeded() case .bottom: bottomStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } bottomStackViewItems = items bottomStackViewItems.forEach { $0.messageInputBar = self $0.parentStackViewPosition = position bottomStackView.addArrangedSubview($0) } guard superview != nil else { return } bottomStackView.layoutIfNeeded() case .top: topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } topStackViewItems = items topStackViewItems.forEach { $0.messageInputBar = self $0.parentStackViewPosition = position topStackView.addArrangedSubview($0) } guard superview != nil else { return } topStackView.layoutIfNeeded() } invalidateIntrinsicContentSize() } performLayout(animated) { setNewItems() } } // swiftlint:enable function_body_length /// Sets the leftStackViewWidthConstant /// /// - Parameters: /// - newValue: New widthAnchor constant /// - animated: If the layout should be animated open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) { performLayout(animated) { self.leftStackViewWidthConstant = newValue self.layoutStackViews([.left]) guard self.superview != nil else { return } self.layoutIfNeeded() } } /// Sets the rightStackViewWidthConstant /// /// - Parameters: /// - newValue: New widthAnchor constant /// - animated: If the layout should be animated open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) { performLayout(animated) { self.rightStackViewWidthConstant = newValue self.layoutStackViews([.right]) guard self.superview != nil else { return } self.layoutIfNeeded() } } // MARK: - Notifications/Hooks /// Invalidates the intrinsicContentSize open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass || traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass { if shouldAutoUpdateMaxTextViewHeight { maxTextViewHeight = calculateMaxTextViewHeight() } invalidateIntrinsicContentSize() } } /// Enables/Disables the sendButton based on the InputTextView's text being empty /// Calls each items `textViewDidChangeAction` method /// Calls the delegates `textViewTextDidChangeTo` method /// Invalidates the intrinsicContentSize @objc open func textViewDidChange() { let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) if shouldManageSendButtonEnabledState { sendButton.isEnabled = !trimmedText.isEmpty || inputTextView.images.count > 0 } inputTextView.placeholderLabel.isHidden = !inputTextView.text.isEmpty items.forEach { $0.textViewDidChangeAction(with: inputTextView) } delegate?.messageInputBar(self, textViewTextDidChangeTo: trimmedText) if requiredInputTextViewHeight != inputTextView.bounds.height { // Prevent un-needed content size invalidation invalidateIntrinsicContentSize() } } /// Calls each items `keyboardEditingBeginsAction` method /// Invalidates the intrinsicContentSize so that the keyboard does not overlap the view @objc open func textViewDidBeginEditing() { items.forEach { $0.keyboardEditingBeginsAction() } } /// Calls each items `keyboardEditingEndsAction` method @objc open func textViewDidEndEditing() { items.forEach { $0.keyboardEditingEndsAction() } } // MARK: - User Actions /// Calls the delegates `didPressSendButtonWith` method /// Assumes that the InputTextView's text has been set to empty and calls `inputTextViewDidChange()` /// Invalidates each of the inputManagers open func didSelectSendButton() { delegate?.messageInputBar(self, didPressSendButtonWith: inputTextView.text) } }