|
@@ -1,47 +1,51 @@
|
|
|
-/*
|
|
|
- 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.
|
|
|
- */
|
|
|
+//
|
|
|
+// InputBarAccessoryView.swift
|
|
|
+// InputBarAccessoryView
|
|
|
+//
|
|
|
+// Copyright © 2017-2019 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
|
|
|
+// 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.
|
|
|
+//
|
|
|
+// Created by Nathan Tannar on 8/18/17.
|
|
|
+//
|
|
|
|
|
|
import UIKit
|
|
|
|
|
|
/// A powerful InputAccessoryView ideal for messaging applications
|
|
|
-open class MessageInputBar: UIView {
|
|
|
+open class InputBarAccessoryView: UIView {
|
|
|
|
|
|
// MARK: - Properties
|
|
|
|
|
|
- /// A delegate to broadcast notifications from the `MessageInputBar`
|
|
|
- open weak var delegate: MessageInputBarDelegate?
|
|
|
+ /// A delegate to broadcast notifications from the `InputBarAccessoryView`
|
|
|
+ open weak var delegate: InputBarAccessoryViewDelegate?
|
|
|
|
|
|
- /// The background UIView anchored to the bottom, left, and right of the MessageInputBar
|
|
|
+ /// The background UIView anchored to the bottom, left, and right of the InputBarAccessoryView
|
|
|
/// with a top anchor equal to the bottom of the top InputStackView
|
|
|
open var backgroundView: UIView = {
|
|
|
let view = UIView()
|
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- view.backgroundColor = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0)
|
|
|
+ view.backgroundColor = .white
|
|
|
return view
|
|
|
}()
|
|
|
|
|
|
- /// A content UIView that holds the left/right/bottom InputStackViews and InputTextView. Anchored to the bottom of the
|
|
|
+ /// A content UIView that holds the left/right/bottom InputStackViews
|
|
|
+ /// and the middleContentView. Anchored to the bottom of the
|
|
|
/// topStackView and inset by the padding UIEdgeInsets
|
|
|
open var contentView: UIView = {
|
|
|
let view = UIView()
|
|
@@ -54,7 +58,7 @@ open class MessageInputBar: UIView {
|
|
|
|
|
|
## 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)
|
|
@@ -62,7 +66,7 @@ open class MessageInputBar: UIView {
|
|
|
return view
|
|
|
}()
|
|
|
|
|
|
- /// Determines if the MessageInputBar should have a translucent effect
|
|
|
+ /// Determines if the InputBarAccessoryView should have a translucent effect
|
|
|
open var isTranslucent: Bool = false {
|
|
|
didSet {
|
|
|
if isTranslucent && blurView.superview == nil {
|
|
@@ -70,12 +74,12 @@ open class MessageInputBar: UIView {
|
|
|
blurView.fillSuperview()
|
|
|
}
|
|
|
blurView.isHidden = !isTranslucent
|
|
|
- let color: UIColor = backgroundView.backgroundColor ?? UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0)
|
|
|
- backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color.withAlphaComponent(1.0)
|
|
|
+ let color: UIColor = backgroundView.backgroundColor ?? .white
|
|
|
+ backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /// A SeparatorLine that is anchored at the top of the MessageInputBar with a height of 1
|
|
|
+
|
|
|
+ /// A SeparatorLine that is anchored at the top of the InputBarAccessoryView
|
|
|
public let separatorLine = SeparatorLine()
|
|
|
|
|
|
/**
|
|
@@ -115,38 +119,78 @@ open class MessageInputBar: UIView {
|
|
|
2. It's spacing is initially set to 15
|
|
|
*/
|
|
|
public let bottomStackView = InputStackView(axis: .horizontal, spacing: 15)
|
|
|
+
|
|
|
+ /**
|
|
|
+ The main view component of the InputBarAccessoryView
|
|
|
+
|
|
|
+ The default value is the `InputTextView`.
|
|
|
+
|
|
|
+ ## Important Notes ##
|
|
|
+ 1. This view should self-size with constraints or an
|
|
|
+ intrinsicContentSize to auto-size the InputBarAccessoryView
|
|
|
+ 2. Override with `setMiddleContentView(view: UIView?, animated: Bool)`
|
|
|
+ */
|
|
|
+ public private(set) weak var middleContentView: UIView?
|
|
|
+
|
|
|
+ /// A view to wrap the `middleContentView` inside
|
|
|
+ private let middleContentViewWrapper: UIView = {
|
|
|
+ let view = UIView()
|
|
|
+ view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ return view
|
|
|
+ }()
|
|
|
|
|
|
/// The InputTextView a user can input a message in
|
|
|
open lazy var inputTextView: InputTextView = { [weak self] in
|
|
|
let inputTextView = InputTextView()
|
|
|
inputTextView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- inputTextView.messageInputBar = self
|
|
|
+ inputTextView.inputBarAccessoryView = self
|
|
|
return inputTextView
|
|
|
}()
|
|
|
-
|
|
|
+
|
|
|
/// A InputBarButtonItem used as the send button and initially placed in the rightStackView
|
|
|
- open var sendButton: InputBarButtonItem = {
|
|
|
- return InputBarButtonItem()
|
|
|
+ open var sendButton: InputBarSendButton = {
|
|
|
+ return InputBarSendButton()
|
|
|
.configure {
|
|
|
$0.setSize(CGSize(width: 52, height: 36), animated: false)
|
|
|
$0.isEnabled = false
|
|
|
$0.title = "Send"
|
|
|
$0.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold)
|
|
|
}.onTouchUpInside {
|
|
|
- $0.messageInputBar?.didSelectSendButton()
|
|
|
+ $0.inputBarAccessoryView?.didSelectSendButton()
|
|
|
}
|
|
|
}()
|
|
|
+
|
|
|
+ /**
|
|
|
+ The anchor contants used to add horizontal inset from the InputBarAccessoryView and the
|
|
|
+ window. By default, an `inputAccessoryView` spans the entire width of the UIWindow. You
|
|
|
+ can manage these insets if you wish to implement designs that do not have the bar spanning
|
|
|
+ the entire width.
|
|
|
+
|
|
|
+ ## Important Notes ##
|
|
|
+
|
|
|
+ USE AT YOUR OWN RISK
|
|
|
+
|
|
|
+ ````
|
|
|
+ H:|-(frameInsets.left)-[InputBarAccessoryView]-(frameInsets.right)-|
|
|
|
+ ````
|
|
|
+
|
|
|
+ */
|
|
|
+ open var frameInsets: HorizontalEdgePadding = .zero {
|
|
|
+ didSet {
|
|
|
+ updateFrameInsets()
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
- The anchor contants used by the InputStackView's and InputTextView to create padding
|
|
|
- within the MessageInputBar
|
|
|
+ The anchor constants used by the InputStackView's and InputTextView to create padding
|
|
|
+ within the InputBarAccessoryView
|
|
|
|
|
|
## Important Notes ##
|
|
|
|
|
|
````
|
|
|
V:|...[InputStackView.top]-(padding.top)-[contentView]-(padding.bottom)-|
|
|
|
|
|
|
- H:|-(padding.left)-[contentView]-(padding.right)-|
|
|
|
+ H:|-(frameInsets.left)-(padding.left)-[contentView]-(padding.right)-(frameInsets.right)-|
|
|
|
````
|
|
|
|
|
|
*/
|
|
@@ -163,9 +207,9 @@ open class MessageInputBar: UIView {
|
|
|
1. The topStackViewPadding.bottom property is not used. Use padding.top
|
|
|
|
|
|
````
|
|
|
- V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[InputTextView]-...|
|
|
|
+ V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[middleContentView]-...|
|
|
|
|
|
|
- H:|-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-|
|
|
|
+ H:|-(frameInsets.left)-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-(frameInsets.right)-|
|
|
|
````
|
|
|
|
|
|
*/
|
|
@@ -176,18 +220,18 @@ open class MessageInputBar: UIView {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- The anchor constants used by the InputStackView
|
|
|
+ The anchor constants used by the middleContentView
|
|
|
|
|
|
````
|
|
|
- V:|...-(padding.top)-(textViewPadding.top)-[InputTextView]-(textViewPadding.bottom)-[InputStackView.bottom]-...|
|
|
|
+ V:|...-(padding.top)-(middleContentViewPadding.top)-[middleContentView]-(middleContentViewPadding.bottom)-[InputStackView.bottom]-...|
|
|
|
|
|
|
- H:|...-[InputStackView.left]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[InputStackView.right]-...|
|
|
|
+ H:|...-[InputStackView.left]-(middleContentViewPadding.left)-[middleContentView]-(middleContentViewPadding.right)-[InputStackView.right]-...|
|
|
|
````
|
|
|
|
|
|
*/
|
|
|
- open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
|
|
|
+ open var middleContentViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
|
|
|
didSet {
|
|
|
- updateTextViewPadding()
|
|
|
+ updateMiddleContentViewPadding()
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -216,14 +260,13 @@ open class MessageInputBar: 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`.
|
|
|
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.
|
|
|
open var maxTextViewHeight: CGFloat = 0 {
|
|
|
didSet {
|
|
|
textViewHeightAnchor?.constant = maxTextViewHeight
|
|
|
- invalidateIntrinsicContentSize()
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -232,6 +275,9 @@ open class MessageInputBar: UIView {
|
|
|
|
|
|
/// The height that will fit the current text in the InputTextView based on its current bounds
|
|
|
public var requiredInputTextViewHeight: CGFloat {
|
|
|
+ guard middleContentView == inputTextView else {
|
|
|
+ return middleContentView?.intrinsicContentSize.height ?? 0
|
|
|
+ }
|
|
|
let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
|
|
|
return inputTextView.sizeThatFits(maxTextViewSize).height.rounded(.down)
|
|
|
}
|
|
@@ -250,9 +296,9 @@ open class MessageInputBar: UIView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// Holds the InputPlugin plugins that can be used to extend the functionality of the MessageInputBar
|
|
|
- open var plugins = [InputPlugin]()
|
|
|
-
|
|
|
+ /// Holds the InputPlugin plugins that can be used to extend the functionality of the InputBarAccessoryView
|
|
|
+ open var inputPlugins = [InputPlugin]()
|
|
|
+
|
|
|
/// The InputBarItems held in the leftStackView
|
|
|
public private(set) var leftStackViewItems: [InputItem] = []
|
|
|
|
|
@@ -272,10 +318,10 @@ open class MessageInputBar: UIView {
|
|
|
public var items: [InputItem] {
|
|
|
return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, topStackViewItems, nonStackViewItems].flatMap { $0 }
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// MARK: - Auto-Layout Constraint Sets
|
|
|
|
|
|
- private var textViewLayoutSet: NSLayoutConstraintSet?
|
|
|
+ private var middleContentViewLayoutSet: NSLayoutConstraintSet?
|
|
|
private var textViewHeightAnchor: NSLayoutConstraint?
|
|
|
private var topStackViewLayoutSet: NSLayoutConstraintSet?
|
|
|
private var leftStackViewLayoutSet: NSLayoutConstraintSet?
|
|
@@ -283,7 +329,7 @@ open class MessageInputBar: UIView {
|
|
|
private var bottomStackViewLayoutSet: NSLayoutConstraintSet?
|
|
|
private var contentViewLayoutSet: NSLayoutConstraintSet?
|
|
|
private var windowAnchor: NSLayoutConstraint?
|
|
|
- private var backgroundViewBottomAnchor: NSLayoutConstraint?
|
|
|
+ private var backgroundViewLayoutSet: NSLayoutConstraintSet?
|
|
|
|
|
|
// MARK: - Initialization
|
|
|
|
|
@@ -304,7 +350,16 @@ open class MessageInputBar: UIView {
|
|
|
deinit {
|
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
+ open override func willMove(toSuperview newSuperview: UIView?) {
|
|
|
+ super.willMove(toSuperview: newSuperview)
|
|
|
+ guard newSuperview != nil else {
|
|
|
+ deactivateConstraints()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ activateConstraints()
|
|
|
+ }
|
|
|
+
|
|
|
open override func didMoveToWindow() {
|
|
|
super.didMoveToWindow()
|
|
|
setupConstraints(to: window)
|
|
@@ -314,29 +369,42 @@ open class MessageInputBar: UIView {
|
|
|
|
|
|
/// Sets up the default properties
|
|
|
open func setup() {
|
|
|
-
|
|
|
+
|
|
|
+ backgroundColor = .white
|
|
|
autoresizingMask = [.flexibleHeight]
|
|
|
setupSubviews()
|
|
|
setupConstraints()
|
|
|
setupObservers()
|
|
|
+ setupGestureRecognizers()
|
|
|
}
|
|
|
|
|
|
/// Adds the required notification observers
|
|
|
private func setupObservers() {
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
- selector: #selector(MessageInputBar.orientationDidChange),
|
|
|
+ selector: #selector(InputBarAccessoryView.orientationDidChange),
|
|
|
name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
- selector: #selector(MessageInputBar.inputTextViewDidChange),
|
|
|
+ selector: #selector(InputBarAccessoryView.inputTextViewDidChange),
|
|
|
name: UITextView.textDidChangeNotification, object: inputTextView)
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
- selector: #selector(MessageInputBar.inputTextViewDidBeginEditing),
|
|
|
+ selector: #selector(InputBarAccessoryView.inputTextViewDidBeginEditing),
|
|
|
name: UITextView.textDidBeginEditingNotification, object: inputTextView)
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
- selector: #selector(MessageInputBar.inputTextViewDidEndEditing),
|
|
|
+ selector: #selector(InputBarAccessoryView.inputTextViewDidEndEditing),
|
|
|
name: UITextView.textDidEndEditingNotification, object: inputTextView)
|
|
|
}
|
|
|
|
|
|
+ /// Adds a UISwipeGestureRecognizer for each direction to the InputTextView
|
|
|
+ private func setupGestureRecognizers() {
|
|
|
+ let directions: [UISwipeGestureRecognizer.Direction] = [.left, .right]
|
|
|
+ for direction in directions {
|
|
|
+ let gesture = UISwipeGestureRecognizer(target: self,
|
|
|
+ action: #selector(InputBarAccessoryView.didSwipeTextView(_:)))
|
|
|
+ gesture.direction = direction
|
|
|
+ inputTextView.addGestureRecognizer(gesture)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/// Adds all of the subviews
|
|
|
private func setupSubviews() {
|
|
|
|
|
@@ -344,136 +412,150 @@ open class MessageInputBar: UIView {
|
|
|
addSubview(topStackView)
|
|
|
addSubview(contentView)
|
|
|
addSubview(separatorLine)
|
|
|
- contentView.addSubview(inputTextView)
|
|
|
+ contentView.addSubview(middleContentViewWrapper)
|
|
|
contentView.addSubview(leftStackView)
|
|
|
contentView.addSubview(rightStackView)
|
|
|
contentView.addSubview(bottomStackView)
|
|
|
+ middleContentViewWrapper.addSubview(inputTextView)
|
|
|
+ middleContentView = inputTextView
|
|
|
setStackViewItems([sendButton], forStack: .right, animated: false)
|
|
|
}
|
|
|
|
|
|
/// Sets up the initial constraints of each subview
|
|
|
private func setupConstraints() {
|
|
|
|
|
|
- // The constraints within the MessageInputBar
|
|
|
- separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor, heightConstant: separatorLine.height)
|
|
|
- backgroundViewBottomAnchor = backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
|
- backgroundViewBottomAnchor?.isActive = true
|
|
|
- backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, right: rightAnchor)
|
|
|
+ // The constraints within the InputBarAccessoryView
|
|
|
+ separatorLine.addConstraints(topAnchor, left: backgroundView.leftAnchor, right: backgroundView.rightAnchor, heightConstant: separatorLine.height)
|
|
|
+
|
|
|
+ backgroundViewLayoutSet = NSLayoutConstraintSet(
|
|
|
+ top: backgroundView.topAnchor.constraint(equalTo: topStackView.bottomAnchor),
|
|
|
+ bottom: backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
+ left: backgroundView.leftAnchor.constraint(equalTo: leftAnchor, constant: frameInsets.left),
|
|
|
+ right: backgroundView.rightAnchor.constraint(equalTo: rightAnchor, constant: -frameInsets.right)
|
|
|
+ )
|
|
|
|
|
|
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)
|
|
|
+ left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left + frameInsets.left),
|
|
|
+ right: topStackView.rightAnchor.constraint(equalTo: 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),
|
|
|
- right: contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -padding.right)
|
|
|
+ left: contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left + frameInsets.left),
|
|
|
+ right: contentView.rightAnchor.constraint(equalTo: 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)
|
|
|
- contentViewLayoutSet?.right = contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right)
|
|
|
+ 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)
|
|
|
- topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -topStackViewPadding.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
|
|
|
- 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)
|
|
|
+ middleContentViewLayoutSet = NSLayoutConstraintSet(
|
|
|
+ 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)
|
|
|
)
|
|
|
+
|
|
|
+ inputTextView.fillSuperview()
|
|
|
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),
|
|
|
+ bottom: leftStackView.bottomAnchor.constraint(equalTo: middleContentViewWrapper.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),
|
|
|
+ bottom: rightStackView.bottomAnchor.constraint(equalTo: middleContentViewWrapper.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),
|
|
|
+ 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)
|
|
|
)
|
|
|
- activateConstraints()
|
|
|
}
|
|
|
|
|
|
- /// Respect iPhone X safeAreaInsets
|
|
|
+ /// Respect window 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 {
|
|
|
+ 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
|
|
|
- backgroundViewBottomAnchor?.constant = 34
|
|
|
+ backgroundViewLayoutSet?.bottom?.constant = window.safeAreaInsets.bottom
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - Constraint Layout Updates
|
|
|
+
|
|
|
+ private func updateFrameInsets() {
|
|
|
+ backgroundViewLayoutSet?.left?.constant = frameInsets.left
|
|
|
+ backgroundViewLayoutSet?.right?.constant = -frameInsets.right
|
|
|
+ updatePadding()
|
|
|
+ updateTopStackViewPadding()
|
|
|
+ }
|
|
|
|
|
|
/// 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?.left?.constant = padding.left + frameInsets.left
|
|
|
+ contentViewLayoutSet?.right?.constant = -(padding.right + frameInsets.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 middleContentViewPadding UIEdgeInsets
|
|
|
+ private func updateMiddleContentViewPadding() {
|
|
|
+ middleContentViewLayoutSet?.top?.constant = middleContentViewPadding.top
|
|
|
+ middleContentViewLayoutSet?.left?.constant = middleContentViewPadding.left
|
|
|
+ middleContentViewLayoutSet?.right?.constant = -middleContentViewPadding.right
|
|
|
+ middleContentViewLayoutSet?.bottom?.constant = -middleContentViewPadding.bottom
|
|
|
+ bottomStackViewLayoutSet?.top?.constant = middleContentViewPadding.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
|
|
|
+ topStackViewLayoutSet?.left?.constant = topStackViewPadding.left + frameInsets.left
|
|
|
+ topStackViewLayoutSet?.right?.constant = -(topStackViewPadding.right + frameInsets.right)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/// Invalidates the view’s intrinsic content size
|
|
|
open override func invalidateIntrinsicContentSize() {
|
|
|
super.invalidateIntrinsicContentSize()
|
|
|
cachedIntrinsicContentSize = calculateIntrinsicContentSize()
|
|
|
if previousIntrinsicContentSize != cachedIntrinsicContentSize {
|
|
|
- delegate?.messageInputBar(self, didChangeIntrinsicContentTo: cachedIntrinsicContentSize)
|
|
|
+ delegate?.inputBar(self, didChangeIntrinsicContentTo: cachedIntrinsicContentSize)
|
|
|
previousIntrinsicContentSize = cachedIntrinsicContentSize
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// Calculates the correct intrinsicContentSize of the MessageInputBar
|
|
|
+ /// Calculates the correct intrinsicContentSize of the InputBarAccessoryView
|
|
|
///
|
|
|
/// - Returns: The required intrinsicContentSize
|
|
|
open func calculateIntrinsicContentSize() -> CGSize {
|
|
@@ -484,7 +566,6 @@ open class MessageInputBar: UIView {
|
|
|
textViewHeightAnchor?.isActive = true
|
|
|
inputTextView.isScrollEnabled = true
|
|
|
isOverMaxTextViewHeight = true
|
|
|
- inputTextView.layoutIfNeeded()
|
|
|
}
|
|
|
inputTextViewHeight = maxTextViewHeight
|
|
|
} else {
|
|
@@ -497,14 +578,28 @@ open class MessageInputBar: UIView {
|
|
|
}
|
|
|
|
|
|
// Calculate the required height
|
|
|
- let totalPadding = padding.top + padding.bottom + topStackViewPadding.top + textViewPadding.top + textViewPadding.bottom
|
|
|
+ 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 verticalStackViewHeight = topStackViewHeight + bottomStackViewHeight
|
|
|
let requiredHeight = inputTextViewHeight + totalPadding + verticalStackViewHeight
|
|
|
- return CGSize(width: bounds.width, height: requiredHeight)
|
|
|
+ return CGSize(width: UIView.noIntrinsicMetric, height: requiredHeight)
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func layoutIfNeeded() {
|
|
|
+ super.layoutIfNeeded()
|
|
|
+ inputTextView.layoutIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
|
+ guard frameInsets.left != 0 || frameInsets.right != 0 else {
|
|
|
+ return super.point(inside: point, with: event)
|
|
|
+ }
|
|
|
+ // Allow touches to pass through base view
|
|
|
+ return subviews.contains {
|
|
|
+ !$0.isHidden && $0.point(inside: convert(point, to: $0), with: event)
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
|
|
|
/// Returns the max height the InputTextView can grow to based on the UIScreen
|
|
|
///
|
|
@@ -542,7 +637,7 @@ open class MessageInputBar: UIView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// Performs layout changes over the main thread
|
|
|
+ /// Performs a layout over the main thread
|
|
|
///
|
|
|
/// - Parameters:
|
|
|
/// - animated: If the layout should be animated
|
|
@@ -561,8 +656,9 @@ open class MessageInputBar: UIView {
|
|
|
|
|
|
/// Activates the NSLayoutConstraintSet's
|
|
|
private func activateConstraints() {
|
|
|
+ backgroundViewLayoutSet?.activate()
|
|
|
contentViewLayoutSet?.activate()
|
|
|
- textViewLayoutSet?.activate()
|
|
|
+ middleContentViewLayoutSet?.activate()
|
|
|
leftStackViewLayoutSet?.activate()
|
|
|
rightStackViewLayoutSet?.activate()
|
|
|
bottomStackViewLayoutSet?.activate()
|
|
@@ -571,16 +667,38 @@ open class MessageInputBar: UIView {
|
|
|
|
|
|
/// Deactivates the NSLayoutConstraintSet's
|
|
|
private func deactivateConstraints() {
|
|
|
+ backgroundViewLayoutSet?.deactivate()
|
|
|
contentViewLayoutSet?.deactivate()
|
|
|
- textViewLayoutSet?.deactivate()
|
|
|
+ middleContentViewLayoutSet?.deactivate()
|
|
|
leftStackViewLayoutSet?.deactivate()
|
|
|
rightStackViewLayoutSet?.deactivate()
|
|
|
bottomStackViewLayoutSet?.deactivate()
|
|
|
topStackViewLayoutSet?.deactivate()
|
|
|
}
|
|
|
+
|
|
|
+ /// Removes the current `middleContentView` and assigns a new one.
|
|
|
+ ///
|
|
|
+ /// WARNING: This will remove the `InputTextView`
|
|
|
+ ///
|
|
|
+ /// - Parameters:
|
|
|
+ /// - view: New view
|
|
|
+ /// - animated: If the layout should be animated
|
|
|
+ open func setMiddleContentView(_ view: UIView?, animated: Bool) {
|
|
|
+ middleContentView?.removeFromSuperview()
|
|
|
+ middleContentView = view
|
|
|
+ guard let view = view else { return }
|
|
|
+ middleContentViewWrapper.addSubview(view)
|
|
|
+ view.fillSuperview()
|
|
|
+
|
|
|
+ performLayout(animated) { [weak self] in
|
|
|
+ guard self?.superview != nil else { return }
|
|
|
+ self?.middleContentViewWrapper.layoutIfNeeded()
|
|
|
+ self?.invalidateIntrinsicContentSize()
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
/// Removes all of the arranged subviews from the InputStackView and adds the given items.
|
|
|
- /// Sets the messageInputBar property of the InputBarButtonItem
|
|
|
+ /// Sets the inputBarAccessoryView property of the InputBarButtonItem
|
|
|
///
|
|
|
/// - Parameters:
|
|
|
/// - items: New InputStackView arranged views
|
|
@@ -594,7 +712,7 @@ open class MessageInputBar: UIView {
|
|
|
leftStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
|
leftStackViewItems = items
|
|
|
leftStackViewItems.forEach {
|
|
|
- $0.messageInputBar = self
|
|
|
+ $0.inputBarAccessoryView = self
|
|
|
$0.parentStackViewPosition = position
|
|
|
if let view = $0 as? UIView {
|
|
|
leftStackView.addArrangedSubview(view)
|
|
@@ -606,7 +724,7 @@ open class MessageInputBar: UIView {
|
|
|
rightStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
|
rightStackViewItems = items
|
|
|
rightStackViewItems.forEach {
|
|
|
- $0.messageInputBar = self
|
|
|
+ $0.inputBarAccessoryView = self
|
|
|
$0.parentStackViewPosition = position
|
|
|
if let view = $0 as? UIView {
|
|
|
rightStackView.addArrangedSubview(view)
|
|
@@ -618,7 +736,7 @@ open class MessageInputBar: UIView {
|
|
|
bottomStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
|
bottomStackViewItems = items
|
|
|
bottomStackViewItems.forEach {
|
|
|
- $0.messageInputBar = self
|
|
|
+ $0.inputBarAccessoryView = self
|
|
|
$0.parentStackViewPosition = position
|
|
|
if let view = $0 as? UIView {
|
|
|
bottomStackView.addArrangedSubview(view)
|
|
@@ -630,7 +748,7 @@ open class MessageInputBar: UIView {
|
|
|
topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
|
topStackViewItems = items
|
|
|
topStackViewItems.forEach {
|
|
|
- $0.messageInputBar = self
|
|
|
+ $0.inputBarAccessoryView = self
|
|
|
$0.parentStackViewPosition = position
|
|
|
if let view = $0 as? UIView {
|
|
|
topStackView.addArrangedSubview(view)
|
|
@@ -653,7 +771,7 @@ open class MessageInputBar: UIView {
|
|
|
/// - newValue: New widthAnchor constant
|
|
|
/// - animated: If the layout should be animated
|
|
|
open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
|
|
|
- performLayout(animated) {
|
|
|
+ performLayout(animated) {
|
|
|
self.leftStackViewWidthConstant = newValue
|
|
|
self.layoutStackViews([.left])
|
|
|
guard self.superview?.superview != nil else { return }
|
|
@@ -667,7 +785,7 @@ open class MessageInputBar: UIView {
|
|
|
/// - newValue: New widthAnchor constant
|
|
|
/// - animated: If the layout should be animated
|
|
|
open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
|
|
|
- performLayout(animated) {
|
|
|
+ performLayout(animated) {
|
|
|
self.rightStackViewWidthConstant = newValue
|
|
|
self.layoutStackViews([.right])
|
|
|
guard self.superview?.superview != nil else { return }
|
|
@@ -711,7 +829,7 @@ open class MessageInputBar: UIView {
|
|
|
}
|
|
|
invalidateIntrinsicContentSize()
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/// Enables/Disables the sendButton based on the InputTextView's text being empty
|
|
|
/// Calls each items `textViewDidChangeAction` method
|
|
|
/// Calls the delegates `textViewTextDidChangeTo` method
|
|
@@ -734,7 +852,7 @@ open class MessageInputBar: UIView {
|
|
|
let shouldInvalidateIntrinsicContentSize = requiredInputTextViewHeight != inputTextView.bounds.height
|
|
|
|
|
|
items.forEach { $0.textViewDidChangeAction(with: self.inputTextView) }
|
|
|
- delegate?.messageInputBar(self, textViewTextDidChangeTo: trimmedText)
|
|
|
+ delegate?.inputBar(self, textViewTextDidChangeTo: trimmedText)
|
|
|
|
|
|
if shouldInvalidateIntrinsicContentSize {
|
|
|
// Prevent un-needed content size invalidation
|
|
@@ -758,20 +876,28 @@ open class MessageInputBar: UIView {
|
|
|
|
|
|
/// Reloads each of the plugins
|
|
|
open func reloadPlugins() {
|
|
|
- plugins.forEach { $0.reloadData() }
|
|
|
+ inputPlugins.forEach { $0.reloadData() }
|
|
|
}
|
|
|
|
|
|
/// Invalidates each of the plugins
|
|
|
open func invalidatePlugins() {
|
|
|
- plugins.forEach { $0.invalidate() }
|
|
|
+ inputPlugins.forEach { $0.invalidate() }
|
|
|
}
|
|
|
|
|
|
// MARK: - User Actions
|
|
|
-
|
|
|
+
|
|
|
+ /// Calls each items `keyboardSwipeGestureAction` method
|
|
|
+ /// Calls the delegates `didSwipeTextViewWith` method
|
|
|
+ @objc
|
|
|
+ open func didSwipeTextView(_ gesture: UISwipeGestureRecognizer) {
|
|
|
+ items.forEach { $0.keyboardSwipeGestureAction(with: gesture) }
|
|
|
+ delegate?.inputBar(self, didSwipeTextViewWith: gesture)
|
|
|
+ }
|
|
|
+
|
|
|
/// Calls the delegates `didPressSendButtonWith` method
|
|
|
/// Assumes that the InputTextView's text has been set to empty and calls `inputTextViewDidChange()`
|
|
|
/// Invalidates each of the InputPlugins
|
|
|
open func didSelectSendButton() {
|
|
|
- delegate?.messageInputBar(self, didPressSendButtonWith: inputTextView.text)
|
|
|
+ delegate?.inputBar(self, didPressSendButtonWith: inputTextView.text)
|
|
|
}
|
|
|
}
|