mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
717 lines
30 KiB
Swift
717 lines
30 KiB
Swift
/*
|
||
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
|
||
open 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
|
||
*/
|
||
open 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
|
||
*/
|
||
open 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
|
||
*/
|
||
open 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
|
||
*/
|
||
open 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: .UITextViewTextDidChange, object: inputTextView)
|
||
NotificationCenter.default.addObserver(self,
|
||
selector: #selector(MessageInputBar.textViewDidBeginEditing),
|
||
name: .UITextViewTextDidBeginEditing, object: inputTextView)
|
||
NotificationCenter.default.addObserver(self,
|
||
selector: #selector(MessageInputBar.textViewDidEndEditing),
|
||
name: .UITextViewTextDidEndEditing, 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, heightConstant: 1)
|
||
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.constraintLessThanOrEqualToSystemSpacingBelow(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)
|
||
}
|
||
}
|