Add .top InputStackView and Docs

This commit is contained in:
Nathan Tannar committed 2017-11-02 20:21:15 -07:00
1 parent a86bc8e6b1
commit eea80108f7
2 files changed
+210 -68

No files matched your search

+1 -1
View File
@@ -40,7 +40,7 @@ open class InputStackView: UIStackView {
/// - right: Bottom Stack View
/// - bottom: Left Stack View
public enum Position {
case left, right, bottom
case left, right, bottom, top
}
// MARK: Initialization
+209 -67
View File
@@ -24,6 +24,7 @@
import UIKit
/// A powerful InputAccessoryView ideal for messaging applications
open class MessageInputBar: UIView {
public enum UIStackViewPosition {
@@ -32,28 +33,61 @@ open class MessageInputBar: UIView {
// MARK: - Properties
/// A delegate to broadcast notifications from the MessageInputBar
open weak var delegate: MessageInputBarDelegate?
/// A background view that adds a blur effect. Shown when 'isTransparent' is set to TRUE. Hidden by default.
open let blurView: UIView = {
let blurEffect = UIBlurEffect(style: .extraLight)
let view = UIVisualEffectView(effect: blurEffect)
/// 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.isHidden = true
view.backgroundColor = .white
return view
}()
/// When set to true, the blurView in the background is shown and the backgroundColor is set to .clear. Default is FALSE
open var isTranslucent: Bool = false {
/// Also sets the backgroundView's backgroundColor to the newValue
open override var backgroundColor: UIColor? {
didSet {
blurView.isHidden = !isTranslucent
backgroundColor = isTranslucent ? .clear : .white
backgroundView.backgroundColor = backgroundColor
}
}
/// A boarder line anchored to the top of the 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
backgroundView.backgroundColor = isTranslucent ? (backgroundView.backgroundColor?.withAlphaComponent(0.7) ?? UIColor.white.withAlphaComponent(0.7)) : .white
}
}
/// A SeparatorLine that is initially placed in the topStackView
open let separatorLine = SeparatorLine()
/**
The InputStackView at the InputStackView.top position
## Important Notes ##
1. It's axis is initially set to .vertical
*/
open let topStackView = InputStackView(axis: .vertical, spacing: 0)
/**
The InputStackView at the InputStackView.left position
@@ -78,24 +112,16 @@ open class MessageInputBar: UIView {
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 = { [weak self] in
let textView = InputTextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.messageInputBar = self
return textView
}()
/// The padding around the textView that separates it from the stackViews
open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
didSet {
textViewLayoutSet?.bottom?.constant = -textViewPadding.bottom
textViewLayoutSet?.left?.constant = textViewPadding.left
textViewLayoutSet?.right?.constant = -textViewPadding.right
bottomStackViewLayoutSet?.top?.constant = textViewPadding.bottom
}
}
/// A InputBarButtonItem used as the send button and initially placed in the rightStackView
open var sendButton: InputBarButtonItem = {
return InputBarButtonItem()
.configure {
@@ -108,46 +134,82 @@ open class MessageInputBar: UIView {
}
}()
/// The anchor contants used by the UIStackViews and InputTextView to create padding within the InputBarAccessoryView
/**
The anchor contants used by the InputStackView's and InputTextView to create padding
within the MessageInputBar
## Important Notes ##
````
V:|...-[InputStackView.top]-(padding.top)-[InputTextView]-(textViewPadding.bottom)-[InputStackView.bottom]-(padding.bottom)-|
H:|-(padding.left)-[InputStackView.left(leftStackViewWidthConstant)]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[InputStackView.right(rightStackViewWidthConstant)]-(padding.right)-|
````
*/
open var padding: UIEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) {
didSet {
updateViewContraints()
updatePadding()
}
}
/**
The anchor constants used by the top InputStackView
## Important Notes ##
1. The topStackViewPadding.bottom property is not used. Use padding.top
````
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
## Important Notes ##
1. The inputTextViewPadding.top property is not used. Use padding.top
````
V:|...-(padding.top)-[InputTextView]-(inputTextViewPadding.bottom)-[InputStackView.bottom]-...|
H:|...-[InputStackView.left]-(inputTextViewPadding.left)-[InputTextView]-(inputTextViewPadding.left)-[InputStackView.left.right]-...|
````
*/
open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
didSet {
updateTextViewPadding()
}
}
open override var intrinsicContentSize: CGSize {
let maxSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
let sizeToFit = inputTextView.sizeThatFits(maxSize)
var heightToFit = sizeToFit.height.rounded() + padding.top + padding.bottom
if heightToFit >= maxHeight {
if !isOverMaxHeight {
textViewHeightAnchor?.isActive = true
inputTextView.isScrollEnabled = true
isOverMaxHeight = true
}
heightToFit = maxHeight
} else {
if isOverMaxHeight {
textViewHeightAnchor?.isActive = false
inputTextView.isScrollEnabled = false
isOverMaxHeight = false
}
}
let size = CGSize(width: bounds.width, height: heightToFit)
let size = calculateIntrinsicContentSize()
if previousIntrinsicContentSize != size {
delegate?.messageInputBar(self, didChangeIntrinsicContentTo: size)
previousIntrinsicContentSize = size
}
return size
}
private(set) var isOverMaxHeight = false
/// The intrinsicContentSize can change a lot so the delegate method
/// `inputBar(self, didChangeIntrinsicContentTo: size)` only needs to be called
/// when it's different
private var previousIntrinsicContentSize: CGSize?
/// The maximum intrinsicContentSize height. When reached the delegate 'didChangeIntrinsicContentTo' will be called.
/// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this
/// improves the performance
private(set) public var isOverMaxTextViewHeight = false
/// The maximum height that the InputTextView can reach
open var maxHeight: CGFloat = UIScreen.main.bounds.height / 3 {
didSet {
textViewHeightAnchor?.constant = maxHeight
@@ -156,27 +218,30 @@ open class MessageInputBar: UIView {
}
/// The fixed widthAnchor constant of the leftStackView
private(set) var leftStackViewWidthContant: CGFloat = 0 {
private(set) var leftStackViewWidthConstant: CGFloat = 0 {
didSet {
leftStackViewLayoutSet?.width?.constant = leftStackViewWidthContant
leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant
}
}
/// The fixed widthAnchor constant of the rightStackView
private(set) var rightStackViewWidthContant: CGFloat = 52 {
private(set) var rightStackViewWidthConstant: CGFloat = 52 {
didSet {
rightStackViewLayoutSet?.width?.constant = rightStackViewWidthContant
rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant
}
}
/// The InputBarItems held in the leftStackView
private(set) var leftStackViewItems: [InputBarButtonItem] = []
private(set) public var leftStackViewItems: [InputBarButtonItem] = []
/// The InputBarItems held in the rightStackView
private(set) var rightStackViewItems: [InputBarButtonItem] = []
private(set) public var rightStackViewItems: [InputBarButtonItem] = []
/// The InputBarItems held in the bottomStackView
private(set) var bottomStackViewItems: [InputBarButtonItem] = []
private(set) public var bottomStackViewItems: [InputBarButtonItem] = []
/// The InputBarItems held in the topStackView
private(set) public 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] = []
@@ -190,10 +255,11 @@ open class MessageInputBar: UIView {
private var textViewLayoutSet: NSLayoutConstraintSet?
private var textViewHeightAnchor: NSLayoutConstraint?
private var topStackViewHeightAnchor: NSLayoutConstraint?
private var topStackViewLayoutSet: NSLayoutConstraintSet?
private var leftStackViewLayoutSet: NSLayoutConstraintSet?
private var rightStackViewLayoutSet: NSLayoutConstraintSet?
private var bottomStackViewLayoutSet: NSLayoutConstraintSet?
private var previousIntrinsicContentSize: CGSize?
// MARK: - Initialization
@@ -217,6 +283,7 @@ open class MessageInputBar: UIView {
// MARK: - Setup
/// Sets up the default properties
open func setup() {
backgroundColor = .inputBarGray
@@ -226,24 +293,32 @@ open class MessageInputBar: UIView {
setupObservers()
}
/// Adds all of the subviews
private func setupSubviews() {
addSubview(blurView)
addSubview(backgroundView)
addSubview(topStackView)
addSubview(inputTextView)
addSubview(leftStackView)
addSubview(rightStackView)
addSubview(bottomStackView)
addSubview(separatorLine)
topStackView.addArrangedSubview(separatorLine)
setStackViewItems([sendButton], forStack: .right, animated: false)
}
/// Sets up the initial constraints of each subview
private func setupConstraints() {
separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor, heightConstant: 0.5)
blurView.fillSuperview()
topStackViewLayoutSet = NSLayoutConstraintSet(
top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top),
bottom: topStackView.bottomAnchor.constraint(equalTo: inputTextView.topAnchor, constant: -padding.top),
left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left),
right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -topStackViewPadding.right)
)
backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor)
textViewLayoutSet = NSLayoutConstraintSet(
top: inputTextView.topAnchor.constraint(equalTo: topAnchor, constant: padding.top),
top: inputTextView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.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)
@@ -254,14 +329,14 @@ open class MessageInputBar: UIView {
top: leftStackView.topAnchor.constraint(equalTo: topAnchor, constant: padding.top),
bottom: leftStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0),
left: leftStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left),
width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthContant)
width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant)
)
rightStackViewLayoutSet = NSLayoutConstraintSet(
top: rightStackView.topAnchor.constraint(equalTo: topAnchor, constant: padding.top),
bottom: rightStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0),
right: rightStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -padding.right),
width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthContant)
width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant)
)
bottomStackViewLayoutSet = NSLayoutConstraintSet(
@@ -273,19 +348,22 @@ open class MessageInputBar: UIView {
if #available(iOS 11.0, *) {
// Switch to safeAreaLayoutGuide
topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left)
topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: topStackViewPadding.right)
leftStackViewLayoutSet?.left = leftStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left)
rightStackViewLayoutSet?.right = rightStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right)
bottomStackViewLayoutSet?.bottom = bottomStackView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -padding.bottom)
bottomStackViewLayoutSet?.left = bottomStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left)
bottomStackViewLayoutSet?.right = bottomStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right)
}
topStackViewLayoutSet?.activate()
leftStackViewLayoutSet?.activate()
rightStackViewLayoutSet?.activate()
bottomStackViewLayoutSet?.activate()
}
private func updateViewContraints() {
/// Updates the constraint constants that correspond to the padding UIEdgeInsets
private func updatePadding() {
textViewLayoutSet?.top?.constant = padding.top
leftStackViewLayoutSet?.top?.constant = padding.top
leftStackViewLayoutSet?.left?.constant = padding.left
@@ -296,6 +374,22 @@ open class MessageInputBar: UIView {
bottomStackViewLayoutSet?.bottom?.constant = -padding.bottom
}
/// Updates the constraint constants that correspond to the inputTextViewPadding UIEdgeInsets
private func updateTextViewPadding() {
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
}
/// Adds the required notification observers
private func setupObservers() {
NotificationCenter.default.addObserver(self,
@@ -312,6 +406,30 @@ open class MessageInputBar: UIView {
name: .UITextViewTextDidEndEditing, object: nil)
}
/// Calculates the correct intrinsicContentSize of the MessageInputBar
///
/// - Returns: The required intrinsicContentSize
open func calculateIntrinsicContentSize() -> CGSize {
let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
var heightToFit = inputTextView.sizeThatFits(maxTextViewSize).height.rounded()
if heightToFit >= maxHeight {
if !isOverMaxTextViewHeight {
textViewHeightAnchor?.isActive = true
inputTextView.isScrollEnabled = true
isOverMaxTextViewHeight = true
}
heightToFit = maxHeight
} else {
if isOverMaxTextViewHeight {
textViewHeightAnchor?.isActive = false
inputTextView.isScrollEnabled = false
isOverMaxTextViewHeight = false
}
}
return CGSize(width: bounds.width, height: heightToFit)
}
// MARK: - Layout Helper Methods
/// Layout the given UIStackView's
@@ -330,6 +448,9 @@ open class MessageInputBar: UIView {
case .bottom:
bottomStackView.setNeedsLayout()
bottomStackView.layoutIfNeeded()
case .top:
bottomStackView.setNeedsLayout()
bottomStackView.layoutIfNeeded()
}
}
}
@@ -345,6 +466,7 @@ open class MessageInputBar: UIView {
leftStackViewLayoutSet?.deactivate()
rightStackViewLayoutSet?.deactivate()
bottomStackViewLayoutSet?.deactivate()
topStackViewLayoutSet?.deactivate()
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 0.3, animations: animations)
@@ -356,11 +478,12 @@ open class MessageInputBar: UIView {
leftStackViewLayoutSet?.activate()
rightStackViewLayoutSet?.activate()
bottomStackViewLayoutSet?.activate()
topStackViewLayoutSet?.activate()
}
// MARK: - UIStackView InputBarItem Methods
/// Removes all of the arranged subviews from the UIStackView and adds the given items. Sets the inputBarAccessoryView property of the InputBarButtonItem
/// 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
@@ -397,6 +520,15 @@ open class MessageInputBar: UIView {
bottomStackView.addArrangedSubview($0)
}
bottomStackView.layoutIfNeeded()
case .top:
topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
topStackViewItems = items
topStackViewItems.forEach {
$0.messageInputBar = self
$0.parentStackViewPosition = position
topStackView.addArrangedSubview($0)
}
topStackView.layoutIfNeeded()
}
}
@@ -412,7 +544,7 @@ open class MessageInputBar: UIView {
/// - animated: If the layout should be animated
open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
performLayout(animated) {
self.leftStackViewWidthContant = newValue
self.leftStackViewWidthConstant = newValue
self.layoutStackViews([.left])
self.layoutIfNeeded()
}
@@ -425,7 +557,7 @@ open class MessageInputBar: UIView {
/// - animated: If the layout should be animated
open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
performLayout(animated) {
self.rightStackViewWidthContant = newValue
self.rightStackViewWidthConstant = newValue
self.layoutStackViews([.right])
self.layoutIfNeeded()
}
@@ -433,11 +565,16 @@ open class MessageInputBar: UIView {
// MARK: - Notifications/Hooks
/// Invalidates the intrinsicContentSize
@objc
open func orientationDidChange() {
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)
@@ -451,11 +588,13 @@ open class MessageInputBar: UIView {
invalidateIntrinsicContentSize()
}
/// Calls each items `keyboardEditingBeginsAction` method
@objc
open func textViewDidBeginEditing() {
self.items.forEach { $0.keyboardEditingBeginsAction() }
}
/// Calls each items `keyboardEditingEndsAction` method
@objc
open func textViewDidEndEditing() {
self.items.forEach { $0.keyboardEditingEndsAction() }
@@ -463,6 +602,9 @@ open class MessageInputBar: UIView {
// 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)
textViewDidChange()