|
@@ -2,7 +2,7 @@
|
|
|
// InputBarAccessoryView.swift
|
|
|
// InputBarAccessoryView
|
|
|
//
|
|
|
-// Copyright © 2017-2019 Nathan Tannar.
|
|
|
+// Copyright © 2017-2020 Nathan Tannar.
|
|
|
//
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
@@ -26,6 +26,7 @@
|
|
|
//
|
|
|
|
|
|
import UIKit
|
|
|
+import DcCore
|
|
|
|
|
|
/// A powerful InputAccessoryView ideal for messaging applications
|
|
|
open class InputBarAccessoryView: UIView {
|
|
@@ -40,7 +41,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
open var backgroundView: UIView = {
|
|
|
let view = UIView()
|
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- view.backgroundColor = .white
|
|
|
+ view.backgroundColor = InputBarAccessoryView.defaultBackgroundColor
|
|
|
return view
|
|
|
}()
|
|
|
|
|
@@ -55,27 +56,21 @@ open class InputBarAccessoryView: UIView {
|
|
|
|
|
|
/**
|
|
|
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)
|
|
|
+ open lazy var blurView: UIVisualEffectView = {
|
|
|
+ var blurEffect = UIBlurEffect(style: .light)
|
|
|
+ if #available(iOS 13, *) {
|
|
|
+ blurEffect = UIBlurEffect(style: .systemMaterial)
|
|
|
+ }
|
|
|
let view = UIVisualEffectView(effect: blurEffect)
|
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
return view
|
|
|
}()
|
|
|
|
|
|
/// Determines if the InputBarAccessoryView should have a translucent effect
|
|
|
- open var isTranslucent: Bool = false {
|
|
|
+ open var isTranslucent: Bool = true {
|
|
|
didSet {
|
|
|
- if isTranslucent && blurView.superview == nil {
|
|
|
- backgroundView.addSubview(blurView)
|
|
|
- blurView.fillSuperview()
|
|
|
- }
|
|
|
blurView.isHidden = !isTranslucent
|
|
|
- let color: UIColor = backgroundView.backgroundColor ?? .white
|
|
|
- backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -138,10 +133,18 @@ open class InputBarAccessoryView: UIView {
|
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
return view
|
|
|
}()
|
|
|
+
|
|
|
+ private static let defaultBackgroundColor: UIColor = {
|
|
|
+ if #available(iOS 13, *) {
|
|
|
+ return .systemBackground
|
|
|
+ } else {
|
|
|
+ return .white
|
|
|
+ }
|
|
|
+ }()
|
|
|
|
|
|
/// The InputTextView a user can input a message in
|
|
|
- open lazy var inputTextView: InputTextView = { [weak self] in
|
|
|
- let inputTextView = InputTextView()
|
|
|
+ open lazy var inputTextView: ChatInputTextView = {
|
|
|
+ let inputTextView = ChatInputTextView()
|
|
|
inputTextView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
inputTextView.inputBarAccessoryView = self
|
|
|
return inputTextView
|
|
@@ -250,8 +253,13 @@ open class InputBarAccessoryView: UIView {
|
|
|
|
|
|
/// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this
|
|
|
/// improves the performance
|
|
|
+ /// The default value is `FALSE`
|
|
|
public private(set) var isOverMaxTextViewHeight = false
|
|
|
|
|
|
+ /// A boolean that tracks orientation changes to calculate the correct intrinsicContentSize and
|
|
|
+ /// enable/disable NSLayoutContraints accordingly in calculateIntrinsicContentSize
|
|
|
+ private var isInPhoneLandscapeOrientation = false
|
|
|
+
|
|
|
/// A boolean that when set as `TRUE` will always enable the `InputTextView` to be anchored to the
|
|
|
/// height of `maxTextViewHeight`
|
|
|
/// The default value is `FALSE`
|
|
@@ -259,11 +267,13 @@ open class InputBarAccessoryView: UIView {
|
|
|
|
|
|
/// A boolean that determines if the `maxTextViewHeight` should be maintained automatically.
|
|
|
/// To control the maximum height of the view yourself, set this to `false`.
|
|
|
+ /// The default value is `TRUE`
|
|
|
open var shouldAutoUpdateMaxTextViewHeight = true
|
|
|
|
|
|
/// The maximum height that the InputTextView can reach.
|
|
|
/// This is set automatically when `shouldAutoUpdateMaxTextViewHeight` is true.
|
|
|
/// To control the height yourself, make sure to set `shouldAutoUpdateMaxTextViewHeight` to false.
|
|
|
+ /// The default value is `0`
|
|
|
open var maxTextViewHeight: CGFloat = 0 {
|
|
|
didSet {
|
|
|
textViewHeightAnchor?.constant = maxTextViewHeight
|
|
@@ -271,7 +281,13 @@ open class InputBarAccessoryView: UIView {
|
|
|
}
|
|
|
|
|
|
/// A boolean that determines whether the sendButton's `isEnabled` state should be managed automatically.
|
|
|
+ /// The default value is `TRUE`
|
|
|
open var shouldManageSendButtonEnabledState = true
|
|
|
+
|
|
|
+ /// A boolean that determines if the layout required for new or typed text should
|
|
|
+ /// be animated.
|
|
|
+ /// The default value is `FALSE`
|
|
|
+ open var shouldAnimateTextDidChangeLayout = false
|
|
|
|
|
|
/// The height that will fit the current text in the InputTextView based on its current bounds
|
|
|
public var requiredInputTextViewHeight: CGFloat {
|
|
@@ -283,6 +299,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
}
|
|
|
|
|
|
/// The fixed widthAnchor constant of the leftStackView
|
|
|
+ /// The default value is `0`
|
|
|
public private(set) var leftStackViewWidthConstant: CGFloat = 0 {
|
|
|
didSet {
|
|
|
leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant
|
|
@@ -290,6 +307,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
}
|
|
|
|
|
|
/// The fixed widthAnchor constant of the rightStackView
|
|
|
+ /// The default value is `52`
|
|
|
public private(set) var rightStackViewWidthConstant: CGFloat = 52 {
|
|
|
didSet {
|
|
|
rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant
|
|
@@ -319,6 +337,27 @@ open class InputBarAccessoryView: UIView {
|
|
|
return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, topStackViewItems, nonStackViewItems].flatMap { $0 }
|
|
|
}
|
|
|
|
|
|
+ // customization
|
|
|
+ /// indicates if a draft view was added to the InputBarAccessoryView
|
|
|
+ var hasDraft: Bool = false
|
|
|
+
|
|
|
+ /// indicates if a quote view was added to the InputBarAccessoryView
|
|
|
+ var hasQuote: Bool = false
|
|
|
+
|
|
|
+ /// optional callback that gets called if scroll down button was pressed
|
|
|
+ var onScrollDownButtonPressed: (() -> Void)?
|
|
|
+
|
|
|
+ lazy var scrollDownButton: UIButton = {
|
|
|
+ let button = UIButton(frame: .zero)
|
|
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ button.addTarget(self, action: #selector(onScrollDownPressed), for: .touchUpInside)
|
|
|
+ button.isHidden = true
|
|
|
+ return button
|
|
|
+ }()
|
|
|
+
|
|
|
+ private let keyboardManager: KeyboardManager = KeyboardManager()
|
|
|
+ open var keyboardHeight: CGFloat = 0
|
|
|
+
|
|
|
// MARK: - Auto-Layout Constraint Sets
|
|
|
|
|
|
private var middleContentViewLayoutSet: NSLayoutConstraintSet?
|
|
@@ -369,14 +408,29 @@ open class InputBarAccessoryView: UIView {
|
|
|
|
|
|
/// Sets up the default properties
|
|
|
open func setup() {
|
|
|
-
|
|
|
- backgroundColor = .white
|
|
|
+ backgroundColor = .clear
|
|
|
autoresizingMask = [.flexibleHeight]
|
|
|
setupSubviews()
|
|
|
setupConstraints()
|
|
|
setupObservers()
|
|
|
setupGestureRecognizers()
|
|
|
+ setupKeyboardEvents()
|
|
|
+ setupScrollDownButton()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setupKeyboardEvents() {
|
|
|
+ keyboardManager.on(event: .willChangeFrame, do: { [weak self] (notification) in
|
|
|
+ guard let self = self else { return }
|
|
|
+ self.keyboardHeight = notification.endFrame.height - self.intrinsicContentSize.height
|
|
|
+ }).on(event: .didChangeFrame, do: { [weak self] (notification) in
|
|
|
+ guard let self = self else { return }
|
|
|
+ self.keyboardHeight = notification.endFrame.height - self.intrinsicContentSize.height
|
|
|
+ if self.isInPhoneLandscapeOrientation {
|
|
|
+ self.orientationDidChange()
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
+
|
|
|
|
|
|
/// Adds the required notification observers
|
|
|
private func setupObservers() {
|
|
@@ -407,7 +461,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
|
|
|
/// Adds all of the subviews
|
|
|
private func setupSubviews() {
|
|
|
-
|
|
|
+ addSubview(blurView)
|
|
|
addSubview(backgroundView)
|
|
|
addSubview(topStackView)
|
|
|
addSubview(contentView)
|
|
@@ -419,6 +473,8 @@ open class InputBarAccessoryView: UIView {
|
|
|
middleContentViewWrapper.addSubview(inputTextView)
|
|
|
middleContentView = inputTextView
|
|
|
setStackViewItems([sendButton], forStack: .right, animated: false)
|
|
|
+ backgroundView.backgroundColor = DcColors.defaultTransparentBackgroundColor
|
|
|
+ blurView.fillSuperview()
|
|
|
}
|
|
|
|
|
|
/// Sets up the initial constraints of each subview
|
|
@@ -435,35 +491,25 @@ open class InputBarAccessoryView: UIView {
|
|
|
)
|
|
|
|
|
|
topStackViewLayoutSet = NSLayoutConstraintSet(
|
|
|
- top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top),
|
|
|
+ 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 + frameInsets.left),
|
|
|
- right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -(topStackViewPadding.right + frameInsets.right))
|
|
|
+ left: topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left + frameInsets.left),
|
|
|
+ right: topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -(topStackViewPadding.right + frameInsets.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 + frameInsets.left),
|
|
|
- right: contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -(padding.right + frameInsets.right))
|
|
|
+ top: contentView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.top),
|
|
|
+ bottom: contentView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -padding.bottom),
|
|
|
+ left: contentView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left + frameInsets.left),
|
|
|
+ right: contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -(padding.right + frameInsets.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 + frameInsets.left)
|
|
|
- contentViewLayoutSet?.right = contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -(padding.right + frameInsets.right))
|
|
|
-
|
|
|
- topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left + frameInsets.left)
|
|
|
- topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -(topStackViewPadding.right + frameInsets.right))
|
|
|
- }
|
|
|
|
|
|
// Constraints Within the contentView
|
|
|
middleContentViewLayoutSet = NSLayoutConstraintSet(
|
|
|
- top: middleContentViewWrapper.topAnchor.constraint(equalTo: contentView.topAnchor, constant: middleContentViewPadding.top),
|
|
|
+ top: middleContentViewWrapper.topAnchor.constraint(equalTo: contentView.topAnchor, constant: middleContentViewPadding.top),
|
|
|
bottom: middleContentViewWrapper.bottomAnchor.constraint(equalTo: bottomStackView.topAnchor, constant: -middleContentViewPadding.bottom),
|
|
|
- left: middleContentViewWrapper.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: middleContentViewPadding.left),
|
|
|
- right: middleContentViewWrapper.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -middleContentViewPadding.right)
|
|
|
+ left: middleContentViewWrapper.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: middleContentViewPadding.left),
|
|
|
+ right: middleContentViewWrapper.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -middleContentViewPadding.right)
|
|
|
)
|
|
|
|
|
|
inputTextView.fillSuperview()
|
|
@@ -471,24 +517,24 @@ open class InputBarAccessoryView: UIView {
|
|
|
textViewHeightAnchor = inputTextView.heightAnchor.constraint(equalToConstant: maxTextViewHeight)
|
|
|
|
|
|
leftStackViewLayoutSet = NSLayoutConstraintSet(
|
|
|
- top: leftStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
|
|
|
+ top: leftStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
|
|
|
bottom: leftStackView.bottomAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: 0),
|
|
|
- left: leftStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
|
|
|
- width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant)
|
|
|
+ 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),
|
|
|
+ top: rightStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
|
|
|
bottom: rightStackView.bottomAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: 0),
|
|
|
- right: rightStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0),
|
|
|
- width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant)
|
|
|
+ right: rightStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0),
|
|
|
+ width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant)
|
|
|
)
|
|
|
|
|
|
bottomStackViewLayoutSet = NSLayoutConstraintSet(
|
|
|
- top: bottomStackView.topAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: middleContentViewPadding.bottom),
|
|
|
+ top: bottomStackView.topAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: middleContentViewPadding.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)
|
|
|
+ left: bottomStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
|
|
|
+ right: bottomStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0)
|
|
|
)
|
|
|
}
|
|
|
|
|
@@ -497,17 +543,13 @@ open class InputBarAccessoryView: UIView {
|
|
|
///
|
|
|
/// - Parameter window: The window to anchor to
|
|
|
private func setupConstraints(to window: UIWindow?) {
|
|
|
- if #available(iOS 11.0, *) {
|
|
|
- if let window = window {
|
|
|
- guard window.safeAreaInsets.bottom > 0 else { return }
|
|
|
- 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
|
|
|
- backgroundViewLayoutSet?.bottom?.constant = window.safeAreaInsets.bottom
|
|
|
- }
|
|
|
- }
|
|
|
+ guard let window = window, window.safeAreaInsets.bottom > 0 else { return }
|
|
|
+ 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
|
|
|
+ backgroundViewLayoutSet?.bottom?.constant = window.safeAreaInsets.bottom
|
|
|
}
|
|
|
|
|
|
// MARK: - Constraint Layout Updates
|
|
@@ -560,27 +602,35 @@ open class InputBarAccessoryView: UIView {
|
|
|
/// - Returns: The required intrinsicContentSize
|
|
|
open func calculateIntrinsicContentSize() -> CGSize {
|
|
|
|
|
|
+ let isPhoneInLandscape = UIApplication.shared.statusBarOrientation.isLandscape && UIDevice.current.userInterfaceIdiom == .phone
|
|
|
var inputTextViewHeight = requiredInputTextViewHeight
|
|
|
- if inputTextViewHeight >= maxTextViewHeight {
|
|
|
+ let bottomSafeAreaInsetsHeight = UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets.bottom ?? 0
|
|
|
+
|
|
|
+ if isPhoneInLandscape && keyboardHeight > bottomSafeAreaInsetsHeight {
|
|
|
+ textViewHeightAnchor?.isActive = true
|
|
|
+ inputTextView.isScrollEnabled = true
|
|
|
+ inputTextViewHeight = maxTextViewHeight
|
|
|
+ isOverMaxTextViewHeight = inputTextViewHeight >= maxTextViewHeight
|
|
|
+ } else if inputTextViewHeight >= maxTextViewHeight {
|
|
|
if !isOverMaxTextViewHeight {
|
|
|
textViewHeightAnchor?.isActive = true
|
|
|
inputTextView.isScrollEnabled = true
|
|
|
isOverMaxTextViewHeight = true
|
|
|
}
|
|
|
inputTextViewHeight = maxTextViewHeight
|
|
|
- } else {
|
|
|
- if isOverMaxTextViewHeight {
|
|
|
- textViewHeightAnchor?.isActive = false || shouldForceTextViewMaxHeight
|
|
|
- inputTextView.isScrollEnabled = false
|
|
|
- isOverMaxTextViewHeight = false
|
|
|
- inputTextView.invalidateIntrinsicContentSize()
|
|
|
- }
|
|
|
+ } else if isOverMaxTextViewHeight || isInPhoneLandscapeOrientation {
|
|
|
+ textViewHeightAnchor?.isActive = false || shouldForceTextViewMaxHeight
|
|
|
+ inputTextView.isScrollEnabled = false
|
|
|
+ isOverMaxTextViewHeight = false
|
|
|
+ inputTextView.invalidateIntrinsicContentSize()
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ isInPhoneLandscapeOrientation = isPhoneInLandscape
|
|
|
+
|
|
|
// Calculate the required height
|
|
|
let totalPadding = padding.top + padding.bottom + topStackViewPadding.top + middleContentViewPadding.top + middleContentViewPadding.bottom
|
|
|
- let topStackViewHeight = topStackView.arrangedSubviews.count > 0 ? topStackView.bounds.height : 0
|
|
|
- let bottomStackViewHeight = bottomStackView.arrangedSubviews.count > 0 ? bottomStackView.bounds.height : 0
|
|
|
+ let topStackViewHeight = !topStackView.arrangedSubviews.isEmpty ? topStackView.bounds.height : 0
|
|
|
+ let bottomStackViewHeight = !bottomStackView.arrangedSubviews.isEmpty ? bottomStackView.bounds.height : 0
|
|
|
let verticalStackViewHeight = topStackViewHeight + bottomStackViewHeight
|
|
|
let requiredHeight = inputTextViewHeight + totalPadding + verticalStackViewHeight
|
|
|
return CGSize(width: UIView.noIntrinsicMetric, height: requiredHeight)
|
|
@@ -592,6 +642,10 @@ open class InputBarAccessoryView: UIView {
|
|
|
}
|
|
|
|
|
|
open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
|
+ if !scrollDownButton.isHidden && scrollDownButton.point(inside: convert(point, to: scrollDownButton), with: event) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
guard frameInsets.left != 0 || frameInsets.right != 0 else {
|
|
|
return super.point(inside: point, with: event)
|
|
|
}
|
|
@@ -605,10 +659,22 @@ open class InputBarAccessoryView: UIView {
|
|
|
///
|
|
|
/// - Returns: Max Height
|
|
|
open func calculateMaxTextViewHeight() -> CGFloat {
|
|
|
- if traitCollection.verticalSizeClass == .regular {
|
|
|
- return (UIScreen.main.bounds.height / 3).rounded(.down)
|
|
|
+ let bottomSafeAreaInsetsHeight = UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets.bottom ?? 0
|
|
|
+ if UIApplication.shared.statusBarOrientation.isPortrait || UIDevice.current.userInterfaceIdiom == .pad || keyboardHeight == bottomSafeAreaInsetsHeight {
|
|
|
+ let divisor: CGFloat = 3
|
|
|
+ var subtract: CGFloat = 0
|
|
|
+ subtract += hasDraft ? 90 : 0
|
|
|
+ subtract += hasQuote ? 90 : 0
|
|
|
+ let height = (UIScreen.main.bounds.height / divisor).rounded(.down) - subtract
|
|
|
+ if height < 40 {
|
|
|
+ return 40
|
|
|
+ }
|
|
|
+ return height
|
|
|
+ } else {
|
|
|
+ // landscape phone layout with shown keyboard
|
|
|
+ let height = UIScreen.main.bounds.height - keyboardHeight - padding.vertical
|
|
|
+ return height
|
|
|
}
|
|
|
- return (UIScreen.main.bounds.height / 5).rounded(.down)
|
|
|
}
|
|
|
|
|
|
// MARK: - Layout Helper Methods
|
|
@@ -700,6 +766,13 @@ open class InputBarAccessoryView: UIView {
|
|
|
/// Removes all of the arranged subviews from the InputStackView and adds the given items.
|
|
|
/// Sets the inputBarAccessoryView property of the InputBarButtonItem
|
|
|
///
|
|
|
+ /// Note: If you call `animated = true`, the `items` property of the stack view items will not be updated until the
|
|
|
+ /// views are done being animated. If you perform a check for the items after they're set, setting animated to `false`
|
|
|
+ /// will apply the body of the closure immediately.
|
|
|
+ ///
|
|
|
+ /// The send button is attached to `rightStackView` so remember to remove it if you're setting it to a different
|
|
|
+ /// stack.
|
|
|
+ ///
|
|
|
/// - Parameters:
|
|
|
/// - items: New InputStackView arranged views
|
|
|
/// - position: The targeted InputStackView
|
|
@@ -774,8 +847,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
performLayout(animated) {
|
|
|
self.leftStackViewWidthConstant = newValue
|
|
|
self.layoutStackViews([.left])
|
|
|
- guard self.superview?.superview != nil else { return }
|
|
|
- self.superview?.superview?.layoutIfNeeded()
|
|
|
+ self.layoutContainerViewIfNeeded()
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -788,8 +860,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
performLayout(animated) {
|
|
|
self.rightStackViewWidthConstant = newValue
|
|
|
self.layoutStackViews([.right])
|
|
|
- guard self.superview?.superview != nil else { return }
|
|
|
- self.superview?.superview?.layoutIfNeeded()
|
|
|
+ self.layoutContainerViewIfNeeded()
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -802,9 +873,24 @@ open class InputBarAccessoryView: UIView {
|
|
|
performLayout(animated) {
|
|
|
self.shouldForceTextViewMaxHeight = newValue
|
|
|
self.textViewHeightAnchor?.isActive = newValue
|
|
|
- guard self.superview?.superview != nil else { return }
|
|
|
- self.superview?.superview?.layoutIfNeeded()
|
|
|
+ self.layoutContainerViewIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Calls `layoutIfNeeded()` on the `UIInputSetContainerView` that holds the
|
|
|
+ /// `InputBarAccessoryView`, if it exists, else `layoutIfNeeded()` is called
|
|
|
+ /// on the `superview`.
|
|
|
+ /// Use this for invoking a smooth layout of a size change when used as
|
|
|
+ /// an `inputAccessoryView`
|
|
|
+ public func layoutContainerViewIfNeeded() {
|
|
|
+ guard
|
|
|
+ let UIInputSetContainerViewKind: AnyClass = NSClassFromString("UIInputSetContainerView"),
|
|
|
+ let container = superview?.superview,
|
|
|
+ container.isKind(of: UIInputSetContainerViewKind) else {
|
|
|
+ superview?.layoutIfNeeded()
|
|
|
+ return
|
|
|
}
|
|
|
+ superview?.superview?.layoutIfNeeded()
|
|
|
}
|
|
|
|
|
|
// MARK: - Notifications/Hooks
|
|
@@ -812,13 +898,15 @@ open class InputBarAccessoryView: UIView {
|
|
|
/// Invalidates the intrinsicContentSize
|
|
|
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
- if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass || traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
|
|
|
+ if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass ||
|
|
|
+ traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
|
|
|
if shouldAutoUpdateMaxTextViewHeight {
|
|
|
maxTextViewHeight = calculateMaxTextViewHeight()
|
|
|
} else {
|
|
|
invalidateIntrinsicContentSize()
|
|
|
}
|
|
|
}
|
|
|
+ scrollDownButton.layer.borderColor = DcColors.colorDisabled.cgColor
|
|
|
}
|
|
|
|
|
|
/// Invalidates the intrinsicContentSize
|
|
@@ -843,7 +931,7 @@ open class InputBarAccessoryView: UIView {
|
|
|
var isEnabled = !trimmedText.isEmpty
|
|
|
if !isEnabled {
|
|
|
// The images property is more resource intensive so only use it if needed
|
|
|
- isEnabled = inputTextView.images.count > 0
|
|
|
+ isEnabled = !inputTextView.images.isEmpty
|
|
|
}
|
|
|
sendButton.isEnabled = isEnabled
|
|
|
}
|
|
@@ -857,6 +945,12 @@ open class InputBarAccessoryView: UIView {
|
|
|
if shouldInvalidateIntrinsicContentSize {
|
|
|
// Prevent un-needed content size invalidation
|
|
|
invalidateIntrinsicContentSize()
|
|
|
+ if shouldAnimateTextDidChangeLayout {
|
|
|
+ inputTextView.layoutIfNeeded()
|
|
|
+ UIView.animate(withDuration: 0.15) {
|
|
|
+ self.layoutContainerViewIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -900,4 +994,53 @@ open class InputBarAccessoryView: UIView {
|
|
|
open func didSelectSendButton() {
|
|
|
delegate?.inputBar(self, didPressSendButtonWith: inputTextView.text)
|
|
|
}
|
|
|
+
|
|
|
+ // MARK: - Drafts - Customization
|
|
|
+ public func configure(draft: DraftModel) {
|
|
|
+ hasDraft = !draft.isEditing && draft.attachment != nil
|
|
|
+ hasQuote = !draft.isEditing && draft.quoteText != nil
|
|
|
+ leftStackView.isHidden = draft.isEditing
|
|
|
+ rightStackView.isHidden = draft.isEditing
|
|
|
+ maxTextViewHeight = calculateMaxTextViewHeight()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func cancel() {
|
|
|
+ hasDraft = false
|
|
|
+ hasQuote = false
|
|
|
+ maxTextViewHeight = calculateMaxTextViewHeight()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func onScrollDownPressed() {
|
|
|
+ if let callback = onScrollDownButtonPressed {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func setupScrollDownButton() {
|
|
|
+ self.addSubview(scrollDownButton)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ scrollDownButton.constraintAlignTopTo(self, paddingTop: -52),
|
|
|
+ scrollDownButton.constraintAlignTrailingToAnchor(self.safeAreaLayoutGuide.trailingAnchor, paddingTrailing: 12),
|
|
|
+ scrollDownButton.constraintHeightTo(40),
|
|
|
+ scrollDownButton.constraintWidthTo(40)
|
|
|
+ ])
|
|
|
+ scrollDownButton.backgroundColor = DcColors.defaultBackgroundColor
|
|
|
+ scrollDownButton.setImage(UIImage(named: "ic_scrolldown")?.sd_tintedImage(with: .systemBlue), for: .normal)
|
|
|
+ scrollDownButton.layer.cornerRadius = 20
|
|
|
+ scrollDownButton.layer.borderColor = DcColors.colorDisabled.cgColor
|
|
|
+ scrollDownButton.layer.borderWidth = 1
|
|
|
+ scrollDownButton.layer.masksToBounds = true
|
|
|
+ scrollDownButton.accessibilityLabel = String.localized("menu_scroll_to_bottom")
|
|
|
+ }
|
|
|
+
|
|
|
+ public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
|
+ if !scrollDownButton.isHidden {
|
|
|
+ let scrollButtonViewPoint = self.scrollDownButton.convert(point, from: self)
|
|
|
+ if let view = scrollDownButton.hitTest(scrollButtonViewPoint, with: event) {
|
|
|
+ return view
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return super.hitTest(point, with: event)
|
|
|
+ }
|
|
|
+
|
|
|
}
|