MessageInputBar.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. /*
  2. MIT License
  3. Copyright (c) 2017-2018 MessageKit
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. */
  20. import UIKit
  21. /// A powerful InputAccessoryView ideal for messaging applications
  22. open class MessageInputBar: UIView {
  23. // MARK: - Properties
  24. /// A delegate to broadcast notifications from the MessageInputBar
  25. open weak var delegate: MessageInputBarDelegate?
  26. /// The background UIView anchored to the bottom, left, and right of the MessageInputBar
  27. /// with a top anchor equal to the bottom of the top InputStackView
  28. open var backgroundView: UIView = {
  29. let view = UIView()
  30. view.translatesAutoresizingMaskIntoConstraints = false
  31. view.backgroundColor = .inputBarGray
  32. return view
  33. }()
  34. /// A content UIView that holds the left/right/bottom InputStackViews and InputTextView. Anchored to the bottom of the
  35. /// topStackView and inset by the padding UIEdgeInsets
  36. open var contentView: UIView = {
  37. let view = UIView()
  38. view.translatesAutoresizingMaskIntoConstraints = false
  39. return view
  40. }()
  41. /**
  42. A UIVisualEffectView that adds a blur effect to make the view appear transparent.
  43. ## Important Notes ##
  44. 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
  45. */
  46. open var blurView: UIVisualEffectView = {
  47. let blurEffect = UIBlurEffect(style: .light)
  48. let view = UIVisualEffectView(effect: blurEffect)
  49. view.translatesAutoresizingMaskIntoConstraints = false
  50. return view
  51. }()
  52. /// Determines if the MessageInputBar should have a translucent effect
  53. open var isTranslucent: Bool = false {
  54. didSet {
  55. if isTranslucent && blurView.superview == nil {
  56. backgroundView.addSubview(blurView)
  57. blurView.fillSuperview()
  58. }
  59. blurView.isHidden = !isTranslucent
  60. let color: UIColor = backgroundView.backgroundColor ?? .inputBarGray
  61. backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color.withAlphaComponent(1.0)
  62. }
  63. }
  64. /// A SeparatorLine that is anchored at the top of the MessageInputBar with a height of 1
  65. public let separatorLine = SeparatorLine()
  66. /**
  67. The InputStackView at the InputStackView.top position
  68. ## Important Notes ##
  69. 1. It's axis is initially set to .vertical
  70. 2. It's alignment is initially set to .fill
  71. */
  72. public let topStackView: InputStackView = {
  73. let stackView = InputStackView(axis: .vertical, spacing: 0)
  74. stackView.alignment = .fill
  75. return stackView
  76. }()
  77. /**
  78. The InputStackView at the InputStackView.left position
  79. ## Important Notes ##
  80. 1. It's axis is initially set to .horizontal
  81. */
  82. public let leftStackView = InputStackView(axis: .horizontal, spacing: 0)
  83. /**
  84. The InputStackView at the InputStackView.right position
  85. ## Important Notes ##
  86. 1. It's axis is initially set to .horizontal
  87. */
  88. public let rightStackView = InputStackView(axis: .horizontal, spacing: 0)
  89. /**
  90. The InputStackView at the InputStackView.bottom position
  91. ## Important Notes ##
  92. 1. It's axis is initially set to .horizontal
  93. 2. It's spacing is initially set to 15
  94. */
  95. public let bottomStackView = InputStackView(axis: .horizontal, spacing: 15)
  96. /// The InputTextView a user can input a message in
  97. open lazy var inputTextView: InputTextView = {
  98. let textView = InputTextView()
  99. textView.translatesAutoresizingMaskIntoConstraints = false
  100. textView.messageInputBar = self
  101. return textView
  102. }()
  103. /// A InputBarButtonItem used as the send button and initially placed in the rightStackView
  104. open var sendButton: InputBarButtonItem = {
  105. return InputBarButtonItem()
  106. .configure {
  107. $0.setSize(CGSize(width: 52, height: 28), animated: false)
  108. $0.isEnabled = false
  109. $0.title = "Send"
  110. $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
  111. }.onTouchUpInside {
  112. $0.messageInputBar?.didSelectSendButton()
  113. }
  114. }()
  115. /// A boolean that determines whether the sendButton's `isEnabled` state should be managed automatically.
  116. open var shouldManageSendButtonEnabledState = true
  117. /**
  118. The anchor constants that inset the contentView
  119. ````
  120. V:|...[InputStackView.top]-(padding.top)-[contentView]-(padding.bottom)-|
  121. H:|-(padding.left)-[contentView]-(padding.right)-|
  122. ````
  123. */
  124. open var padding: UIEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) {
  125. didSet {
  126. updatePadding()
  127. }
  128. }
  129. /**
  130. The anchor constants used by the top InputStackView
  131. ## Important Notes ##
  132. 1. The topStackViewPadding.bottom property is not used. Use padding.top to add separation
  133. ````
  134. V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[InputTextView]-...|
  135. H:|-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-|
  136. ````
  137. */
  138. open var topStackViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) {
  139. didSet {
  140. updateTopStackViewPadding()
  141. }
  142. }
  143. /**
  144. The anchor constants used by the InputStackView
  145. ````
  146. V:|...-(padding.top)-(textViewPadding.top)-[InputTextView]-(textViewPadding.bottom)-[InputStackView.bottom]-...|
  147. H:|...-[InputStackView.left]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[InputStackView.right]-...|
  148. ````
  149. */
  150. open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
  151. didSet {
  152. updateTextViewPadding()
  153. }
  154. }
  155. /// Returns the most recent size calculated by `calculateIntrinsicContentSize()`
  156. open override var intrinsicContentSize: CGSize {
  157. return cachedIntrinsicContentSize
  158. }
  159. /// The intrinsicContentSize can change a lot so the delegate method
  160. /// `inputBar(self, didChangeIntrinsicContentTo: size)` only needs to be called
  161. /// when it's different
  162. public private(set) var previousIntrinsicContentSize: CGSize?
  163. /// The most recent calculation of the intrinsicContentSize
  164. private lazy var cachedIntrinsicContentSize: CGSize = calculateIntrinsicContentSize()
  165. /// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this
  166. /// improves the performance
  167. public private(set) var isOverMaxTextViewHeight = false
  168. /// A boolean that determines if the maxTextViewHeight should be auto updated on device rotation
  169. open var shouldAutoUpdateMaxTextViewHeight = true
  170. /// The maximum height that the InputTextView can reach
  171. open var maxTextViewHeight: CGFloat = 0 {
  172. didSet {
  173. textViewHeightAnchor?.constant = maxTextViewHeight
  174. invalidateIntrinsicContentSize()
  175. }
  176. }
  177. /// The height that will fit the current text in the InputTextView based on its current bounds
  178. public var requiredInputTextViewHeight: CGFloat {
  179. let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
  180. return inputTextView.sizeThatFits(maxTextViewSize).height.rounded(.down)
  181. }
  182. /// The fixed widthAnchor constant of the leftStackView
  183. public private(set) var leftStackViewWidthConstant: CGFloat = 0 {
  184. didSet {
  185. leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant
  186. }
  187. }
  188. /// The fixed widthAnchor constant of the rightStackView
  189. public private(set) var rightStackViewWidthConstant: CGFloat = 52 {
  190. didSet {
  191. rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant
  192. }
  193. }
  194. /// The InputBarItems held in the leftStackView
  195. public private(set) var leftStackViewItems: [InputBarButtonItem] = []
  196. /// The InputBarItems held in the rightStackView
  197. public private(set) var rightStackViewItems: [InputBarButtonItem] = []
  198. /// The InputBarItems held in the bottomStackView
  199. public private(set) var bottomStackViewItems: [InputBarButtonItem] = []
  200. /// The InputBarItems held in the topStackView
  201. public private(set) var topStackViewItems: [InputBarButtonItem] = []
  202. /// The InputBarItems held to make use of their hooks but they are not automatically added to a UIStackView
  203. open var nonStackViewItems: [InputBarButtonItem] = []
  204. /// Returns a flatMap of all the items in each of the UIStackViews
  205. public var items: [InputBarButtonItem] {
  206. return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, nonStackViewItems].flatMap { $0 }
  207. }
  208. // MARK: - Auto-Layout Management
  209. private var textViewLayoutSet: NSLayoutConstraintSet?
  210. private var textViewHeightAnchor: NSLayoutConstraint?
  211. private var topStackViewLayoutSet: NSLayoutConstraintSet?
  212. private var leftStackViewLayoutSet: NSLayoutConstraintSet?
  213. private var rightStackViewLayoutSet: NSLayoutConstraintSet?
  214. private var bottomStackViewLayoutSet: NSLayoutConstraintSet?
  215. private var contentViewLayoutSet: NSLayoutConstraintSet?
  216. private var windowAnchor: NSLayoutConstraint?
  217. private var backgroundViewBottomAnchor: NSLayoutConstraint?
  218. // MARK: - Initialization
  219. public convenience init() {
  220. self.init(frame: .zero)
  221. }
  222. public override init(frame: CGRect) {
  223. super.init(frame: frame)
  224. setup()
  225. }
  226. required public init?(coder aDecoder: NSCoder) {
  227. super.init(coder: aDecoder)
  228. setup()
  229. }
  230. deinit {
  231. NotificationCenter.default.removeObserver(self)
  232. }
  233. open override func didMoveToWindow() {
  234. super.didMoveToWindow()
  235. setupConstraints(to: window)
  236. }
  237. // MARK: - Setup
  238. /// Sets up the default properties
  239. open func setup() {
  240. autoresizingMask = [.flexibleHeight]
  241. setupSubviews()
  242. setupConstraints()
  243. setupObservers()
  244. }
  245. /// Adds the required notification observers
  246. private func setupObservers() {
  247. NotificationCenter.default.addObserver(self,
  248. selector: #selector(MessageInputBar.textViewDidChange),
  249. name: UITextView.textDidChangeNotification, object: inputTextView)
  250. NotificationCenter.default.addObserver(self,
  251. selector: #selector(MessageInputBar.textViewDidBeginEditing),
  252. name: UITextView.textDidBeginEditingNotification, object: inputTextView)
  253. NotificationCenter.default.addObserver(self,
  254. selector: #selector(MessageInputBar.textViewDidEndEditing),
  255. name: UITextView.textDidEndEditingNotification, object: inputTextView)
  256. }
  257. /// Adds all of the subviews
  258. private func setupSubviews() {
  259. addSubview(backgroundView)
  260. addSubview(topStackView)
  261. addSubview(contentView)
  262. addSubview(separatorLine)
  263. contentView.addSubview(inputTextView)
  264. contentView.addSubview(leftStackView)
  265. contentView.addSubview(rightStackView)
  266. contentView.addSubview(bottomStackView)
  267. setStackViewItems([sendButton], forStack: .right, animated: false)
  268. }
  269. // swiftlint:disable function_body_length colon
  270. /// Sets up the initial constraints of each subview
  271. private func setupConstraints() {
  272. // The constraints within the MessageInputBar
  273. separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor)
  274. backgroundViewBottomAnchor = backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
  275. backgroundViewBottomAnchor?.isActive = true
  276. backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, right: rightAnchor)
  277. topStackViewLayoutSet = NSLayoutConstraintSet(
  278. top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top),
  279. bottom: topStackView.bottomAnchor.constraint(equalTo: contentView.topAnchor, constant: -padding.top),
  280. left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left),
  281. right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -topStackViewPadding.right)
  282. )
  283. contentViewLayoutSet = NSLayoutConstraintSet(
  284. top: contentView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.top),
  285. bottom: contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding.bottom),
  286. left: contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left),
  287. right: contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -padding.right)
  288. )
  289. if #available(iOS 11.0, *) {
  290. // Switch to safeAreaLayoutGuide
  291. contentViewLayoutSet?.bottom = contentView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -padding.bottom)
  292. contentViewLayoutSet?.left = contentView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left)
  293. contentViewLayoutSet?.right = contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right)
  294. topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left)
  295. topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -topStackViewPadding.right)
  296. }
  297. // Constraints Within the contentView
  298. textViewLayoutSet = NSLayoutConstraintSet(
  299. top: inputTextView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: textViewPadding.top),
  300. bottom: inputTextView.bottomAnchor.constraint(equalTo: bottomStackView.topAnchor, constant: -textViewPadding.bottom),
  301. left: inputTextView.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: textViewPadding.left),
  302. right: inputTextView.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -textViewPadding.right)
  303. )
  304. maxTextViewHeight = calculateMaxTextViewHeight()
  305. textViewHeightAnchor = inputTextView.heightAnchor.constraint(equalToConstant: maxTextViewHeight)
  306. leftStackViewLayoutSet = NSLayoutConstraintSet(
  307. top: leftStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
  308. bottom: leftStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0),
  309. left: leftStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
  310. width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant)
  311. )
  312. rightStackViewLayoutSet = NSLayoutConstraintSet(
  313. top: rightStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
  314. bottom: rightStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0),
  315. right: rightStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0),
  316. width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant)
  317. )
  318. bottomStackViewLayoutSet = NSLayoutConstraintSet(
  319. top: bottomStackView.topAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: textViewPadding.bottom),
  320. bottom: bottomStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0),
  321. left: bottomStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
  322. right: bottomStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0)
  323. )
  324. activateConstraints()
  325. }
  326. // swiftlint:enable function_body_length colon
  327. /// Respect iPhone X safeAreaInsets
  328. /// Adds a constraint to anchor the bottomAnchor of the contentView to the window's safeAreaLayoutGuide.bottomAnchor
  329. ///
  330. /// - Parameter window: The window to anchor to
  331. private func setupConstraints(to window: UIWindow?) {
  332. if #available(iOS 11.0, *) {
  333. guard UIScreen.main.nativeBounds.height == 2436 else { return }
  334. if let window = window {
  335. windowAnchor?.isActive = false
  336. windowAnchor = contentView.bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1)
  337. windowAnchor?.constant = -padding.bottom
  338. windowAnchor?.priority = UILayoutPriority(rawValue: 750)
  339. windowAnchor?.isActive = true
  340. backgroundViewBottomAnchor?.constant = 34
  341. }
  342. }
  343. }
  344. // MARK: - Constraint Layout Updates
  345. /// Updates the constraint constants that correspond to the padding UIEdgeInsets
  346. private func updatePadding() {
  347. topStackViewLayoutSet?.bottom?.constant = -padding.top
  348. contentViewLayoutSet?.top?.constant = padding.top
  349. contentViewLayoutSet?.left?.constant = padding.left
  350. contentViewLayoutSet?.right?.constant = -padding.right
  351. contentViewLayoutSet?.bottom?.constant = -padding.bottom
  352. windowAnchor?.constant = -padding.bottom
  353. }
  354. /// Updates the constraint constants that correspond to the textViewPadding UIEdgeInsets
  355. private func updateTextViewPadding() {
  356. textViewLayoutSet?.top?.constant = textViewPadding.top
  357. textViewLayoutSet?.left?.constant = textViewPadding.left
  358. textViewLayoutSet?.right?.constant = -textViewPadding.right
  359. textViewLayoutSet?.bottom?.constant = -textViewPadding.bottom
  360. bottomStackViewLayoutSet?.top?.constant = textViewPadding.bottom
  361. }
  362. /// Updates the constraint constants that correspond to the topStackViewPadding UIEdgeInsets
  363. private func updateTopStackViewPadding() {
  364. topStackViewLayoutSet?.top?.constant = topStackViewPadding.top
  365. topStackViewLayoutSet?.left?.constant = topStackViewPadding.left
  366. topStackViewLayoutSet?.right?.constant = -topStackViewPadding.right
  367. }
  368. /// Invalidates the view’s intrinsic content size
  369. open override func invalidateIntrinsicContentSize() {
  370. super.invalidateIntrinsicContentSize()
  371. cachedIntrinsicContentSize = calculateIntrinsicContentSize()
  372. if previousIntrinsicContentSize != cachedIntrinsicContentSize {
  373. delegate?.messageInputBar(self, didChangeIntrinsicContentTo: cachedIntrinsicContentSize)
  374. previousIntrinsicContentSize = cachedIntrinsicContentSize
  375. }
  376. }
  377. // MARK: - Layout Helper Methods
  378. /// Calculates the correct intrinsicContentSize of the MessageInputBar. This takes into account the various padding edge
  379. /// insets, InputTextView's height and top/bottom InputStackView's heights.
  380. ///
  381. /// - Returns: The required intrinsicContentSize
  382. open func calculateIntrinsicContentSize() -> CGSize {
  383. var inputTextViewHeight = requiredInputTextViewHeight
  384. if inputTextViewHeight >= maxTextViewHeight {
  385. if !isOverMaxTextViewHeight {
  386. textViewHeightAnchor?.isActive = true
  387. inputTextView.isScrollEnabled = true
  388. isOverMaxTextViewHeight = true
  389. }
  390. inputTextViewHeight = maxTextViewHeight
  391. } else {
  392. if isOverMaxTextViewHeight {
  393. textViewHeightAnchor?.isActive = false
  394. inputTextView.isScrollEnabled = false
  395. isOverMaxTextViewHeight = false
  396. inputTextView.invalidateIntrinsicContentSize()
  397. }
  398. }
  399. // Calculate the required height
  400. let totalPadding = padding.top + padding.bottom + topStackViewPadding.top + textViewPadding.top + textViewPadding.bottom
  401. let topStackViewHeight = topStackView.arrangedSubviews.count > 0 ? topStackView.bounds.height : 0
  402. let bottomStackViewHeight = bottomStackView.arrangedSubviews.count > 0 ? bottomStackView.bounds.height : 0
  403. let verticalStackViewHeight = topStackViewHeight + bottomStackViewHeight
  404. let requiredHeight = inputTextViewHeight + totalPadding + verticalStackViewHeight
  405. return CGSize(width: bounds.width, height: requiredHeight)
  406. }
  407. /// Returns the max height the InputTextView can grow to based on the UIScreen
  408. ///
  409. /// - Returns: Max Height
  410. open func calculateMaxTextViewHeight() -> CGFloat {
  411. if traitCollection.verticalSizeClass == .regular {
  412. return (UIScreen.main.bounds.height / 3).rounded(.down)
  413. }
  414. return (UIScreen.main.bounds.height / 5).rounded(.down)
  415. }
  416. /// Layout the given InputStackView's
  417. ///
  418. /// - Parameter positions: The UIStackView's to layout
  419. public func layoutStackViews(_ positions: [InputStackView.Position] = [.left, .right, .bottom, .top]) {
  420. guard superview != nil else { return }
  421. for position in positions {
  422. switch position {
  423. case .left:
  424. leftStackView.setNeedsLayout()
  425. leftStackView.layoutIfNeeded()
  426. case .right:
  427. rightStackView.setNeedsLayout()
  428. rightStackView.layoutIfNeeded()
  429. case .bottom:
  430. bottomStackView.setNeedsLayout()
  431. bottomStackView.layoutIfNeeded()
  432. case .top:
  433. topStackView.setNeedsLayout()
  434. topStackView.layoutIfNeeded()
  435. }
  436. }
  437. }
  438. /// Performs layout changes over the main thread
  439. ///
  440. /// - Parameters:
  441. /// - animated: If the layout should be animated
  442. /// - animations: Code
  443. internal func performLayout(_ animated: Bool, _ animations: @escaping () -> Void) {
  444. deactivateConstraints()
  445. if animated {
  446. DispatchQueue.main.async {
  447. UIView.animate(withDuration: 0.3, animations: animations)
  448. }
  449. } else {
  450. UIView.performWithoutAnimation { animations() }
  451. }
  452. activateConstraints()
  453. }
  454. /// Activates the NSLayoutConstraintSet's
  455. private func activateConstraints() {
  456. contentViewLayoutSet?.activate()
  457. textViewLayoutSet?.activate()
  458. leftStackViewLayoutSet?.activate()
  459. rightStackViewLayoutSet?.activate()
  460. bottomStackViewLayoutSet?.activate()
  461. topStackViewLayoutSet?.activate()
  462. }
  463. /// Deactivates the NSLayoutConstraintSet's
  464. private func deactivateConstraints() {
  465. contentViewLayoutSet?.deactivate()
  466. textViewLayoutSet?.deactivate()
  467. leftStackViewLayoutSet?.deactivate()
  468. rightStackViewLayoutSet?.deactivate()
  469. bottomStackViewLayoutSet?.deactivate()
  470. topStackViewLayoutSet?.deactivate()
  471. }
  472. // MARK: - UIStackView InputBarItem Methods
  473. // swiftlint:disable function_body_length
  474. /// Removes all of the arranged subviews from the UIStackView and adds the given items. Sets the messageInputBar property of the InputBarButtonItem
  475. ///
  476. /// - Parameters:
  477. /// - items: New UIStackView arranged views
  478. /// - position: The targeted UIStackView
  479. /// - animated: If the layout should be animated
  480. open func setStackViewItems(_ items: [InputBarButtonItem], forStack position: InputStackView.Position, animated: Bool) {
  481. func setNewItems() {
  482. switch position {
  483. case .left:
  484. leftStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  485. leftStackViewItems = items
  486. leftStackViewItems.forEach {
  487. $0.messageInputBar = self
  488. $0.parentStackViewPosition = position
  489. leftStackView.addArrangedSubview($0)
  490. }
  491. guard superview != nil else { return }
  492. leftStackView.layoutIfNeeded()
  493. case .right:
  494. rightStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  495. rightStackViewItems = items
  496. rightStackViewItems.forEach {
  497. $0.messageInputBar = self
  498. $0.parentStackViewPosition = position
  499. rightStackView.addArrangedSubview($0)
  500. }
  501. guard superview != nil else { return }
  502. rightStackView.layoutIfNeeded()
  503. case .bottom:
  504. bottomStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  505. bottomStackViewItems = items
  506. bottomStackViewItems.forEach {
  507. $0.messageInputBar = self
  508. $0.parentStackViewPosition = position
  509. bottomStackView.addArrangedSubview($0)
  510. }
  511. guard superview != nil else { return }
  512. bottomStackView.layoutIfNeeded()
  513. case .top:
  514. topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  515. topStackViewItems = items
  516. topStackViewItems.forEach {
  517. $0.messageInputBar = self
  518. $0.parentStackViewPosition = position
  519. topStackView.addArrangedSubview($0)
  520. }
  521. guard superview != nil else { return }
  522. topStackView.layoutIfNeeded()
  523. }
  524. invalidateIntrinsicContentSize()
  525. }
  526. performLayout(animated) {
  527. setNewItems()
  528. }
  529. }
  530. // swiftlint:enable function_body_length
  531. /// Sets the leftStackViewWidthConstant
  532. ///
  533. /// - Parameters:
  534. /// - newValue: New widthAnchor constant
  535. /// - animated: If the layout should be animated
  536. open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
  537. performLayout(animated) {
  538. self.leftStackViewWidthConstant = newValue
  539. self.layoutStackViews([.left])
  540. guard self.superview != nil else { return }
  541. self.layoutIfNeeded()
  542. }
  543. }
  544. /// Sets the rightStackViewWidthConstant
  545. ///
  546. /// - Parameters:
  547. /// - newValue: New widthAnchor constant
  548. /// - animated: If the layout should be animated
  549. open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
  550. performLayout(animated) {
  551. self.rightStackViewWidthConstant = newValue
  552. self.layoutStackViews([.right])
  553. guard self.superview != nil else { return }
  554. self.layoutIfNeeded()
  555. }
  556. }
  557. // MARK: - Notifications/Hooks
  558. /// Invalidates the intrinsicContentSize
  559. open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  560. super.traitCollectionDidChange(previousTraitCollection)
  561. if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass || traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
  562. if shouldAutoUpdateMaxTextViewHeight {
  563. maxTextViewHeight = calculateMaxTextViewHeight()
  564. }
  565. invalidateIntrinsicContentSize()
  566. }
  567. }
  568. /// Enables/Disables the sendButton based on the InputTextView's text being empty
  569. /// Calls each items `textViewDidChangeAction` method
  570. /// Calls the delegates `textViewTextDidChangeTo` method
  571. /// Invalidates the intrinsicContentSize
  572. @objc
  573. open func textViewDidChange() {
  574. let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
  575. if shouldManageSendButtonEnabledState {
  576. sendButton.isEnabled = !trimmedText.isEmpty || inputTextView.images.count > 0
  577. }
  578. inputTextView.placeholderLabel.isHidden = !inputTextView.text.isEmpty
  579. items.forEach { $0.textViewDidChangeAction(with: inputTextView) }
  580. delegate?.messageInputBar(self, textViewTextDidChangeTo: trimmedText)
  581. if requiredInputTextViewHeight != inputTextView.bounds.height {
  582. // Prevent un-needed content size invalidation
  583. invalidateIntrinsicContentSize()
  584. }
  585. }
  586. /// Calls each items `keyboardEditingBeginsAction` method
  587. /// Invalidates the intrinsicContentSize so that the keyboard does not overlap the view
  588. @objc
  589. open func textViewDidBeginEditing() {
  590. items.forEach { $0.keyboardEditingBeginsAction() }
  591. }
  592. /// Calls each items `keyboardEditingEndsAction` method
  593. @objc
  594. open func textViewDidEndEditing() {
  595. items.forEach { $0.keyboardEditingEndsAction() }
  596. }
  597. // MARK: - User Actions
  598. /// Calls the delegates `didPressSendButtonWith` method
  599. /// Assumes that the InputTextView's text has been set to empty and calls `inputTextViewDidChange()`
  600. /// Invalidates each of the inputManagers
  601. open func didSelectSendButton() {
  602. delegate?.messageInputBar(self, didPressSendButtonWith: inputTextView.text)
  603. }
  604. }