123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 |
- /*
- 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)
- }
- }
|