InputBarAccessoryView.swift 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. //
  2. // InputBarAccessoryView.swift
  3. // InputBarAccessoryView
  4. //
  5. // Copyright © 2017-2019 Nathan Tannar.
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in all
  15. // copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. // SOFTWARE.
  24. //
  25. // Created by Nathan Tannar on 8/18/17.
  26. //
  27. import UIKit
  28. /// A powerful InputAccessoryView ideal for messaging applications
  29. open class InputBarAccessoryView: UIView {
  30. // MARK: - Properties
  31. /// A delegate to broadcast notifications from the `InputBarAccessoryView`
  32. open weak var delegate: InputBarAccessoryViewDelegate?
  33. /// The background UIView anchored to the bottom, left, and right of the InputBarAccessoryView
  34. /// with a top anchor equal to the bottom of the top InputStackView
  35. open var backgroundView: UIView = {
  36. let view = UIView()
  37. view.translatesAutoresizingMaskIntoConstraints = false
  38. view.backgroundColor = .white
  39. return view
  40. }()
  41. /// A content UIView that holds the left/right/bottom InputStackViews
  42. /// and the middleContentView. Anchored to the bottom of the
  43. /// topStackView and inset by the padding UIEdgeInsets
  44. open var contentView: UIView = {
  45. let view = UIView()
  46. view.translatesAutoresizingMaskIntoConstraints = false
  47. return view
  48. }()
  49. /**
  50. A UIVisualEffectView that adds a blur effect to make the view appear transparent.
  51. ## Important Notes ##
  52. 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
  53. */
  54. open var blurView: UIVisualEffectView = {
  55. let blurEffect = UIBlurEffect(style: .light)
  56. let view = UIVisualEffectView(effect: blurEffect)
  57. view.translatesAutoresizingMaskIntoConstraints = false
  58. return view
  59. }()
  60. /// Determines if the InputBarAccessoryView should have a translucent effect
  61. open var isTranslucent: Bool = false {
  62. didSet {
  63. if isTranslucent && blurView.superview == nil {
  64. backgroundView.addSubview(blurView)
  65. blurView.fillSuperview()
  66. }
  67. blurView.isHidden = !isTranslucent
  68. let color: UIColor = backgroundView.backgroundColor ?? .white
  69. backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color
  70. }
  71. }
  72. /// A SeparatorLine that is anchored at the top of the InputBarAccessoryView
  73. public let separatorLine = SeparatorLine()
  74. /**
  75. The InputStackView at the InputStackView.top position
  76. ## Important Notes ##
  77. 1. It's axis is initially set to .vertical
  78. 2. It's alignment is initially set to .fill
  79. */
  80. public let topStackView: InputStackView = {
  81. let stackView = InputStackView(axis: .vertical, spacing: 0)
  82. stackView.alignment = .fill
  83. return stackView
  84. }()
  85. /**
  86. The InputStackView at the InputStackView.left position
  87. ## Important Notes ##
  88. 1. It's axis is initially set to .horizontal
  89. */
  90. public let leftStackView = InputStackView(axis: .horizontal, spacing: 0)
  91. /**
  92. The InputStackView at the InputStackView.right position
  93. ## Important Notes ##
  94. 1. It's axis is initially set to .horizontal
  95. */
  96. public let rightStackView = InputStackView(axis: .horizontal, spacing: 0)
  97. /**
  98. The InputStackView at the InputStackView.bottom position
  99. ## Important Notes ##
  100. 1. It's axis is initially set to .horizontal
  101. 2. It's spacing is initially set to 15
  102. */
  103. public let bottomStackView = InputStackView(axis: .horizontal, spacing: 15)
  104. /**
  105. The main view component of the InputBarAccessoryView
  106. The default value is the `InputTextView`.
  107. ## Important Notes ##
  108. 1. This view should self-size with constraints or an
  109. intrinsicContentSize to auto-size the InputBarAccessoryView
  110. 2. Override with `setMiddleContentView(view: UIView?, animated: Bool)`
  111. */
  112. public private(set) weak var middleContentView: UIView?
  113. /// A view to wrap the `middleContentView` inside
  114. private let middleContentViewWrapper: UIView = {
  115. let view = UIView()
  116. view.translatesAutoresizingMaskIntoConstraints = false
  117. return view
  118. }()
  119. /// The InputTextView a user can input a message in
  120. open lazy var inputTextView: InputTextView = { [weak self] in
  121. let inputTextView = InputTextView()
  122. inputTextView.translatesAutoresizingMaskIntoConstraints = false
  123. inputTextView.inputBarAccessoryView = self
  124. return inputTextView
  125. }()
  126. /// A InputBarButtonItem used as the send button and initially placed in the rightStackView
  127. open var sendButton: InputBarSendButton = {
  128. return InputBarSendButton()
  129. .configure {
  130. $0.setSize(CGSize(width: 52, height: 36), animated: false)
  131. $0.isEnabled = false
  132. $0.title = "Send"
  133. $0.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .bold)
  134. }.onTouchUpInside {
  135. $0.inputBarAccessoryView?.didSelectSendButton()
  136. }
  137. }()
  138. /**
  139. The anchor contants used to add horizontal inset from the InputBarAccessoryView and the
  140. window. By default, an `inputAccessoryView` spans the entire width of the UIWindow. You
  141. can manage these insets if you wish to implement designs that do not have the bar spanning
  142. the entire width.
  143. ## Important Notes ##
  144. USE AT YOUR OWN RISK
  145. ````
  146. H:|-(frameInsets.left)-[InputBarAccessoryView]-(frameInsets.right)-|
  147. ````
  148. */
  149. open var frameInsets: HorizontalEdgePadding = .zero {
  150. didSet {
  151. updateFrameInsets()
  152. }
  153. }
  154. /**
  155. The anchor constants used by the InputStackView's and InputTextView to create padding
  156. within the InputBarAccessoryView
  157. ## Important Notes ##
  158. ````
  159. V:|...[InputStackView.top]-(padding.top)-[contentView]-(padding.bottom)-|
  160. H:|-(frameInsets.left)-(padding.left)-[contentView]-(padding.right)-(frameInsets.right)-|
  161. ````
  162. */
  163. open var padding: UIEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) {
  164. didSet {
  165. updatePadding()
  166. }
  167. }
  168. /**
  169. The anchor constants used by the top InputStackView
  170. ## Important Notes ##
  171. 1. The topStackViewPadding.bottom property is not used. Use padding.top
  172. ````
  173. V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[middleContentView]-...|
  174. H:|-(frameInsets.left)-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-(frameInsets.right)-|
  175. ````
  176. */
  177. open var topStackViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) {
  178. didSet {
  179. updateTopStackViewPadding()
  180. }
  181. }
  182. /**
  183. The anchor constants used by the middleContentView
  184. ````
  185. V:|...-(padding.top)-(middleContentViewPadding.top)-[middleContentView]-(middleContentViewPadding.bottom)-[InputStackView.bottom]-...|
  186. H:|...-[InputStackView.left]-(middleContentViewPadding.left)-[middleContentView]-(middleContentViewPadding.right)-[InputStackView.right]-...|
  187. ````
  188. */
  189. open var middleContentViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
  190. didSet {
  191. updateMiddleContentViewPadding()
  192. }
  193. }
  194. /// Returns the most recent size calculated by `calculateIntrinsicContentSize()`
  195. open override var intrinsicContentSize: CGSize {
  196. return cachedIntrinsicContentSize
  197. }
  198. /// The intrinsicContentSize can change a lot so the delegate method
  199. /// `inputBar(self, didChangeIntrinsicContentTo: size)` only needs to be called
  200. /// when it's different
  201. public private(set) var previousIntrinsicContentSize: CGSize?
  202. /// The most recent calculation of the intrinsicContentSize
  203. private lazy var cachedIntrinsicContentSize: CGSize = calculateIntrinsicContentSize()
  204. /// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this
  205. /// improves the performance
  206. public private(set) var isOverMaxTextViewHeight = false
  207. /// A boolean that when set as `TRUE` will always enable the `InputTextView` to be anchored to the
  208. /// height of `maxTextViewHeight`
  209. /// The default value is `FALSE`
  210. public private(set) var shouldForceTextViewMaxHeight = false
  211. /// A boolean that determines if the `maxTextViewHeight` should be maintained automatically.
  212. /// To control the maximum height of the view yourself, set this to `false`.
  213. open var shouldAutoUpdateMaxTextViewHeight = true
  214. /// The maximum height that the InputTextView can reach.
  215. /// This is set automatically when `shouldAutoUpdateMaxTextViewHeight` is true.
  216. /// To control the height yourself, make sure to set `shouldAutoUpdateMaxTextViewHeight` to false.
  217. open var maxTextViewHeight: CGFloat = 0 {
  218. didSet {
  219. textViewHeightAnchor?.constant = maxTextViewHeight
  220. }
  221. }
  222. /// A boolean that determines whether the sendButton's `isEnabled` state should be managed automatically.
  223. open var shouldManageSendButtonEnabledState = true
  224. /// The height that will fit the current text in the InputTextView based on its current bounds
  225. public var requiredInputTextViewHeight: CGFloat {
  226. guard middleContentView == inputTextView else {
  227. return middleContentView?.intrinsicContentSize.height ?? 0
  228. }
  229. let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
  230. return inputTextView.sizeThatFits(maxTextViewSize).height.rounded(.down)
  231. }
  232. /// The fixed widthAnchor constant of the leftStackView
  233. public private(set) var leftStackViewWidthConstant: CGFloat = 0 {
  234. didSet {
  235. leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant
  236. }
  237. }
  238. /// The fixed widthAnchor constant of the rightStackView
  239. public private(set) var rightStackViewWidthConstant: CGFloat = 52 {
  240. didSet {
  241. rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant
  242. }
  243. }
  244. /// Holds the InputPlugin plugins that can be used to extend the functionality of the InputBarAccessoryView
  245. open var inputPlugins = [InputPlugin]()
  246. /// The InputBarItems held in the leftStackView
  247. public private(set) var leftStackViewItems: [InputItem] = []
  248. /// The InputBarItems held in the rightStackView
  249. public private(set) var rightStackViewItems: [InputItem] = []
  250. /// The InputBarItems held in the bottomStackView
  251. public private(set) var bottomStackViewItems: [InputItem] = []
  252. /// The InputBarItems held in the topStackView
  253. public private(set) var topStackViewItems: [InputItem] = []
  254. /// The InputBarItems held to make use of their hooks but they are not automatically added to a UIStackView
  255. open var nonStackViewItems: [InputItem] = []
  256. /// Returns a flatMap of all the items in each of the UIStackViews
  257. public var items: [InputItem] {
  258. return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, topStackViewItems, nonStackViewItems].flatMap { $0 }
  259. }
  260. // MARK: - Auto-Layout Constraint Sets
  261. private var middleContentViewLayoutSet: NSLayoutConstraintSet?
  262. private var textViewHeightAnchor: NSLayoutConstraint?
  263. private var topStackViewLayoutSet: NSLayoutConstraintSet?
  264. private var leftStackViewLayoutSet: NSLayoutConstraintSet?
  265. private var rightStackViewLayoutSet: NSLayoutConstraintSet?
  266. private var bottomStackViewLayoutSet: NSLayoutConstraintSet?
  267. private var contentViewLayoutSet: NSLayoutConstraintSet?
  268. private var windowAnchor: NSLayoutConstraint?
  269. private var backgroundViewLayoutSet: NSLayoutConstraintSet?
  270. // MARK: - Initialization
  271. public convenience init() {
  272. self.init(frame: .zero)
  273. }
  274. public override init(frame: CGRect) {
  275. super.init(frame: frame)
  276. setup()
  277. }
  278. required public init?(coder aDecoder: NSCoder) {
  279. super.init(coder: aDecoder)
  280. setup()
  281. }
  282. deinit {
  283. NotificationCenter.default.removeObserver(self)
  284. }
  285. open override func willMove(toSuperview newSuperview: UIView?) {
  286. super.willMove(toSuperview: newSuperview)
  287. guard newSuperview != nil else {
  288. deactivateConstraints()
  289. return
  290. }
  291. activateConstraints()
  292. }
  293. open override func didMoveToWindow() {
  294. super.didMoveToWindow()
  295. setupConstraints(to: window)
  296. }
  297. // MARK: - Setup
  298. /// Sets up the default properties
  299. open func setup() {
  300. backgroundColor = .white
  301. autoresizingMask = [.flexibleHeight]
  302. setupSubviews()
  303. setupConstraints()
  304. setupObservers()
  305. setupGestureRecognizers()
  306. }
  307. /// Adds the required notification observers
  308. private func setupObservers() {
  309. NotificationCenter.default.addObserver(self,
  310. selector: #selector(InputBarAccessoryView.orientationDidChange),
  311. name: UIDevice.orientationDidChangeNotification, object: nil)
  312. NotificationCenter.default.addObserver(self,
  313. selector: #selector(InputBarAccessoryView.inputTextViewDidChange),
  314. name: UITextView.textDidChangeNotification, object: inputTextView)
  315. NotificationCenter.default.addObserver(self,
  316. selector: #selector(InputBarAccessoryView.inputTextViewDidBeginEditing),
  317. name: UITextView.textDidBeginEditingNotification, object: inputTextView)
  318. NotificationCenter.default.addObserver(self,
  319. selector: #selector(InputBarAccessoryView.inputTextViewDidEndEditing),
  320. name: UITextView.textDidEndEditingNotification, object: inputTextView)
  321. }
  322. /// Adds a UISwipeGestureRecognizer for each direction to the InputTextView
  323. private func setupGestureRecognizers() {
  324. let directions: [UISwipeGestureRecognizer.Direction] = [.left, .right]
  325. for direction in directions {
  326. let gesture = UISwipeGestureRecognizer(target: self,
  327. action: #selector(InputBarAccessoryView.didSwipeTextView(_:)))
  328. gesture.direction = direction
  329. inputTextView.addGestureRecognizer(gesture)
  330. }
  331. }
  332. /// Adds all of the subviews
  333. private func setupSubviews() {
  334. addSubview(backgroundView)
  335. addSubview(topStackView)
  336. addSubview(contentView)
  337. addSubview(separatorLine)
  338. contentView.addSubview(middleContentViewWrapper)
  339. contentView.addSubview(leftStackView)
  340. contentView.addSubview(rightStackView)
  341. contentView.addSubview(bottomStackView)
  342. middleContentViewWrapper.addSubview(inputTextView)
  343. middleContentView = inputTextView
  344. setStackViewItems([sendButton], forStack: .right, animated: false)
  345. }
  346. /// Sets up the initial constraints of each subview
  347. private func setupConstraints() {
  348. // The constraints within the InputBarAccessoryView
  349. separatorLine.addConstraints(topAnchor, left: backgroundView.leftAnchor, right: backgroundView.rightAnchor, heightConstant: separatorLine.height)
  350. backgroundViewLayoutSet = NSLayoutConstraintSet(
  351. top: backgroundView.topAnchor.constraint(equalTo: topStackView.bottomAnchor),
  352. bottom: backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
  353. left: backgroundView.leftAnchor.constraint(equalTo: leftAnchor, constant: frameInsets.left),
  354. right: backgroundView.rightAnchor.constraint(equalTo: rightAnchor, constant: -frameInsets.right)
  355. )
  356. topStackViewLayoutSet = NSLayoutConstraintSet(
  357. top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top),
  358. bottom: topStackView.bottomAnchor.constraint(equalTo: contentView.topAnchor, constant: -padding.top),
  359. left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left + frameInsets.left),
  360. right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -(topStackViewPadding.right + frameInsets.right))
  361. )
  362. contentViewLayoutSet = NSLayoutConstraintSet(
  363. top: contentView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.top),
  364. bottom: contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding.bottom),
  365. left: contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left + frameInsets.left),
  366. right: contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -(padding.right + frameInsets.right))
  367. )
  368. if #available(iOS 11.0, *) {
  369. // Switch to safeAreaLayoutGuide
  370. contentViewLayoutSet?.bottom = contentView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -padding.bottom)
  371. contentViewLayoutSet?.left = contentView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left + frameInsets.left)
  372. contentViewLayoutSet?.right = contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -(padding.right + frameInsets.right))
  373. topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left + frameInsets.left)
  374. topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -(topStackViewPadding.right + frameInsets.right))
  375. }
  376. // Constraints Within the contentView
  377. middleContentViewLayoutSet = NSLayoutConstraintSet(
  378. top: middleContentViewWrapper.topAnchor.constraint(equalTo: contentView.topAnchor, constant: middleContentViewPadding.top),
  379. bottom: middleContentViewWrapper.bottomAnchor.constraint(equalTo: bottomStackView.topAnchor, constant: -middleContentViewPadding.bottom),
  380. left: middleContentViewWrapper.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: middleContentViewPadding.left),
  381. right: middleContentViewWrapper.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -middleContentViewPadding.right)
  382. )
  383. inputTextView.fillSuperview()
  384. maxTextViewHeight = calculateMaxTextViewHeight()
  385. textViewHeightAnchor = inputTextView.heightAnchor.constraint(equalToConstant: maxTextViewHeight)
  386. leftStackViewLayoutSet = NSLayoutConstraintSet(
  387. top: leftStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
  388. bottom: leftStackView.bottomAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: 0),
  389. left: leftStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
  390. width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant)
  391. )
  392. rightStackViewLayoutSet = NSLayoutConstraintSet(
  393. top: rightStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0),
  394. bottom: rightStackView.bottomAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: 0),
  395. right: rightStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0),
  396. width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant)
  397. )
  398. bottomStackViewLayoutSet = NSLayoutConstraintSet(
  399. top: bottomStackView.topAnchor.constraint(equalTo: middleContentViewWrapper.bottomAnchor, constant: middleContentViewPadding.bottom),
  400. bottom: bottomStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0),
  401. left: bottomStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
  402. right: bottomStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0)
  403. )
  404. }
  405. /// Respect window safeAreaInsets
  406. /// Adds a constraint to anchor the bottomAnchor of the contentView to the window's safeAreaLayoutGuide.bottomAnchor
  407. ///
  408. /// - Parameter window: The window to anchor to
  409. private func setupConstraints(to window: UIWindow?) {
  410. if #available(iOS 11.0, *) {
  411. if let window = window {
  412. guard window.safeAreaInsets.bottom > 0 else { return }
  413. windowAnchor?.isActive = false
  414. windowAnchor = contentView.bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1)
  415. windowAnchor?.constant = -padding.bottom
  416. windowAnchor?.priority = UILayoutPriority(rawValue: 750)
  417. windowAnchor?.isActive = true
  418. backgroundViewLayoutSet?.bottom?.constant = window.safeAreaInsets.bottom
  419. }
  420. }
  421. }
  422. // MARK: - Constraint Layout Updates
  423. private func updateFrameInsets() {
  424. backgroundViewLayoutSet?.left?.constant = frameInsets.left
  425. backgroundViewLayoutSet?.right?.constant = -frameInsets.right
  426. updatePadding()
  427. updateTopStackViewPadding()
  428. }
  429. /// Updates the constraint constants that correspond to the padding UIEdgeInsets
  430. private func updatePadding() {
  431. topStackViewLayoutSet?.bottom?.constant = -padding.top
  432. contentViewLayoutSet?.top?.constant = padding.top
  433. contentViewLayoutSet?.left?.constant = padding.left + frameInsets.left
  434. contentViewLayoutSet?.right?.constant = -(padding.right + frameInsets.right)
  435. contentViewLayoutSet?.bottom?.constant = -padding.bottom
  436. windowAnchor?.constant = -padding.bottom
  437. }
  438. /// Updates the constraint constants that correspond to the middleContentViewPadding UIEdgeInsets
  439. private func updateMiddleContentViewPadding() {
  440. middleContentViewLayoutSet?.top?.constant = middleContentViewPadding.top
  441. middleContentViewLayoutSet?.left?.constant = middleContentViewPadding.left
  442. middleContentViewLayoutSet?.right?.constant = -middleContentViewPadding.right
  443. middleContentViewLayoutSet?.bottom?.constant = -middleContentViewPadding.bottom
  444. bottomStackViewLayoutSet?.top?.constant = middleContentViewPadding.bottom
  445. }
  446. /// Updates the constraint constants that correspond to the topStackViewPadding UIEdgeInsets
  447. private func updateTopStackViewPadding() {
  448. topStackViewLayoutSet?.top?.constant = topStackViewPadding.top
  449. topStackViewLayoutSet?.left?.constant = topStackViewPadding.left + frameInsets.left
  450. topStackViewLayoutSet?.right?.constant = -(topStackViewPadding.right + frameInsets.right)
  451. }
  452. /// Invalidates the view’s intrinsic content size
  453. open override func invalidateIntrinsicContentSize() {
  454. super.invalidateIntrinsicContentSize()
  455. cachedIntrinsicContentSize = calculateIntrinsicContentSize()
  456. if previousIntrinsicContentSize != cachedIntrinsicContentSize {
  457. delegate?.inputBar(self, didChangeIntrinsicContentTo: cachedIntrinsicContentSize)
  458. previousIntrinsicContentSize = cachedIntrinsicContentSize
  459. }
  460. }
  461. /// Calculates the correct intrinsicContentSize of the InputBarAccessoryView
  462. ///
  463. /// - Returns: The required intrinsicContentSize
  464. open func calculateIntrinsicContentSize() -> CGSize {
  465. var inputTextViewHeight = requiredInputTextViewHeight
  466. if inputTextViewHeight >= maxTextViewHeight {
  467. if !isOverMaxTextViewHeight {
  468. textViewHeightAnchor?.isActive = true
  469. inputTextView.isScrollEnabled = true
  470. isOverMaxTextViewHeight = true
  471. }
  472. inputTextViewHeight = maxTextViewHeight
  473. } else {
  474. if isOverMaxTextViewHeight {
  475. textViewHeightAnchor?.isActive = false || shouldForceTextViewMaxHeight
  476. inputTextView.isScrollEnabled = false
  477. isOverMaxTextViewHeight = false
  478. inputTextView.invalidateIntrinsicContentSize()
  479. }
  480. }
  481. // Calculate the required height
  482. let totalPadding = padding.top + padding.bottom + topStackViewPadding.top + middleContentViewPadding.top + middleContentViewPadding.bottom
  483. let topStackViewHeight = topStackView.arrangedSubviews.count > 0 ? topStackView.bounds.height : 0
  484. let bottomStackViewHeight = bottomStackView.arrangedSubviews.count > 0 ? bottomStackView.bounds.height : 0
  485. let verticalStackViewHeight = topStackViewHeight + bottomStackViewHeight
  486. let requiredHeight = inputTextViewHeight + totalPadding + verticalStackViewHeight
  487. return CGSize(width: UIView.noIntrinsicMetric, height: requiredHeight)
  488. }
  489. open override func layoutIfNeeded() {
  490. super.layoutIfNeeded()
  491. inputTextView.layoutIfNeeded()
  492. }
  493. open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  494. guard frameInsets.left != 0 || frameInsets.right != 0 else {
  495. return super.point(inside: point, with: event)
  496. }
  497. // Allow touches to pass through base view
  498. return subviews.contains {
  499. !$0.isHidden && $0.point(inside: convert(point, to: $0), with: event)
  500. }
  501. }
  502. /// Returns the max height the InputTextView can grow to based on the UIScreen
  503. ///
  504. /// - Returns: Max Height
  505. open func calculateMaxTextViewHeight() -> CGFloat {
  506. if traitCollection.verticalSizeClass == .regular {
  507. return (UIScreen.main.bounds.height / 3).rounded(.down)
  508. }
  509. return (UIScreen.main.bounds.height / 5).rounded(.down)
  510. }
  511. // MARK: - Layout Helper Methods
  512. /// Layout the given InputStackView's
  513. ///
  514. /// - Parameter positions: The InputStackView's to layout
  515. public func layoutStackViews(_ positions: [InputStackView.Position] = [.left, .right, .bottom, .top]) {
  516. guard superview != nil else { return }
  517. for position in positions {
  518. switch position {
  519. case .left:
  520. leftStackView.setNeedsLayout()
  521. leftStackView.layoutIfNeeded()
  522. case .right:
  523. rightStackView.setNeedsLayout()
  524. rightStackView.layoutIfNeeded()
  525. case .bottom:
  526. bottomStackView.setNeedsLayout()
  527. bottomStackView.layoutIfNeeded()
  528. case .top:
  529. topStackView.setNeedsLayout()
  530. topStackView.layoutIfNeeded()
  531. }
  532. }
  533. }
  534. /// Performs a layout over the main thread
  535. ///
  536. /// - Parameters:
  537. /// - animated: If the layout should be animated
  538. /// - animations: Animation logic
  539. internal func performLayout(_ animated: Bool, _ animations: @escaping () -> Void) {
  540. deactivateConstraints()
  541. if animated {
  542. DispatchQueue.main.async {
  543. UIView.animate(withDuration: 0.3, animations: animations)
  544. }
  545. } else {
  546. UIView.performWithoutAnimation { animations() }
  547. }
  548. activateConstraints()
  549. }
  550. /// Activates the NSLayoutConstraintSet's
  551. public func activateConstraints() {
  552. backgroundViewLayoutSet?.activate()
  553. contentViewLayoutSet?.activate()
  554. middleContentViewLayoutSet?.activate()
  555. leftStackViewLayoutSet?.activate()
  556. rightStackViewLayoutSet?.activate()
  557. bottomStackViewLayoutSet?.activate()
  558. topStackViewLayoutSet?.activate()
  559. }
  560. /// Deactivates the NSLayoutConstraintSet's
  561. public func deactivateConstraints() {
  562. backgroundViewLayoutSet?.deactivate()
  563. contentViewLayoutSet?.deactivate()
  564. middleContentViewLayoutSet?.deactivate()
  565. leftStackViewLayoutSet?.deactivate()
  566. rightStackViewLayoutSet?.deactivate()
  567. bottomStackViewLayoutSet?.deactivate()
  568. topStackViewLayoutSet?.deactivate()
  569. }
  570. /// Removes the current `middleContentView` and assigns a new one.
  571. ///
  572. /// WARNING: This will remove the `InputTextView`
  573. ///
  574. /// - Parameters:
  575. /// - view: New view
  576. /// - animated: If the layout should be animated
  577. open func setMiddleContentView(_ view: UIView?, animated: Bool) {
  578. middleContentView?.removeFromSuperview()
  579. middleContentView = view
  580. guard let view = view else { return }
  581. middleContentViewWrapper.addSubview(view)
  582. view.fillSuperview()
  583. performLayout(animated) { [weak self] in
  584. guard self?.superview != nil else { return }
  585. self?.middleContentViewWrapper.layoutIfNeeded()
  586. self?.invalidateIntrinsicContentSize()
  587. }
  588. }
  589. /// Removes all of the arranged subviews from the InputStackView and adds the given items.
  590. /// Sets the inputBarAccessoryView property of the InputBarButtonItem
  591. ///
  592. /// - Parameters:
  593. /// - items: New InputStackView arranged views
  594. /// - position: The targeted InputStackView
  595. /// - animated: If the layout should be animated
  596. open func setStackViewItems(_ items: [InputItem], forStack position: InputStackView.Position, animated: Bool) {
  597. func setNewItems() {
  598. switch position {
  599. case .left:
  600. leftStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  601. leftStackViewItems = items
  602. leftStackViewItems.forEach {
  603. $0.inputBarAccessoryView = self
  604. $0.parentStackViewPosition = position
  605. if let view = $0 as? UIView {
  606. leftStackView.addArrangedSubview(view)
  607. }
  608. }
  609. guard superview != nil else { return }
  610. leftStackView.layoutIfNeeded()
  611. case .right:
  612. rightStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  613. rightStackViewItems = items
  614. rightStackViewItems.forEach {
  615. $0.inputBarAccessoryView = self
  616. $0.parentStackViewPosition = position
  617. if let view = $0 as? UIView {
  618. rightStackView.addArrangedSubview(view)
  619. }
  620. }
  621. guard superview != nil else { return }
  622. rightStackView.layoutIfNeeded()
  623. case .bottom:
  624. bottomStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  625. bottomStackViewItems = items
  626. bottomStackViewItems.forEach {
  627. $0.inputBarAccessoryView = self
  628. $0.parentStackViewPosition = position
  629. if let view = $0 as? UIView {
  630. bottomStackView.addArrangedSubview(view)
  631. }
  632. }
  633. guard superview != nil else { return }
  634. bottomStackView.layoutIfNeeded()
  635. case .top:
  636. topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
  637. topStackViewItems = items
  638. topStackViewItems.forEach {
  639. $0.inputBarAccessoryView = self
  640. $0.parentStackViewPosition = position
  641. if let view = $0 as? UIView {
  642. topStackView.addArrangedSubview(view)
  643. }
  644. }
  645. guard superview != nil else { return }
  646. topStackView.layoutIfNeeded()
  647. }
  648. invalidateIntrinsicContentSize()
  649. }
  650. performLayout(animated) {
  651. setNewItems()
  652. }
  653. }
  654. /// Sets the leftStackViewWidthConstant
  655. ///
  656. /// - Parameters:
  657. /// - newValue: New widthAnchor constant
  658. /// - animated: If the layout should be animated
  659. open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
  660. performLayout(animated) {
  661. self.leftStackViewWidthConstant = newValue
  662. self.layoutStackViews([.left])
  663. guard self.superview?.superview != nil else { return }
  664. self.superview?.superview?.layoutIfNeeded()
  665. }
  666. }
  667. /// Sets the rightStackViewWidthConstant
  668. ///
  669. /// - Parameters:
  670. /// - newValue: New widthAnchor constant
  671. /// - animated: If the layout should be animated
  672. open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
  673. performLayout(animated) {
  674. self.rightStackViewWidthConstant = newValue
  675. self.layoutStackViews([.right])
  676. guard self.superview?.superview != nil else { return }
  677. self.superview?.superview?.layoutIfNeeded()
  678. }
  679. }
  680. /// Sets the `shouldForceTextViewMaxHeight` property
  681. ///
  682. /// - Parameters:
  683. /// - newValue: New boolean value
  684. /// - animated: If the layout should be animated
  685. open func setShouldForceMaxTextViewHeight(to newValue: Bool, animated: Bool) {
  686. performLayout(animated) {
  687. self.shouldForceTextViewMaxHeight = newValue
  688. self.textViewHeightAnchor?.isActive = newValue
  689. guard self.superview?.superview != nil else { return }
  690. self.superview?.superview?.layoutIfNeeded()
  691. }
  692. }
  693. // MARK: - Notifications/Hooks
  694. /// Invalidates the intrinsicContentSize
  695. open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  696. super.traitCollectionDidChange(previousTraitCollection)
  697. if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass || traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
  698. if shouldAutoUpdateMaxTextViewHeight {
  699. maxTextViewHeight = calculateMaxTextViewHeight()
  700. } else {
  701. invalidateIntrinsicContentSize()
  702. }
  703. }
  704. }
  705. /// Invalidates the intrinsicContentSize
  706. @objc
  707. open func orientationDidChange() {
  708. if shouldAutoUpdateMaxTextViewHeight {
  709. maxTextViewHeight = calculateMaxTextViewHeight()
  710. }
  711. invalidateIntrinsicContentSize()
  712. }
  713. /// Enables/Disables the sendButton based on the InputTextView's text being empty
  714. /// Calls each items `textViewDidChangeAction` method
  715. /// Calls the delegates `textViewTextDidChangeTo` method
  716. /// Invalidates the intrinsicContentSize
  717. @objc
  718. open func inputTextViewDidChange() {
  719. let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
  720. if shouldManageSendButtonEnabledState {
  721. var isEnabled = !trimmedText.isEmpty
  722. if !isEnabled {
  723. // The images property is more resource intensive so only use it if needed
  724. isEnabled = inputTextView.images.count > 0
  725. }
  726. sendButton.isEnabled = isEnabled
  727. }
  728. // Capture change before iterating over the InputItem's
  729. let shouldInvalidateIntrinsicContentSize = requiredInputTextViewHeight != inputTextView.bounds.height
  730. items.forEach { $0.textViewDidChangeAction(with: self.inputTextView) }
  731. delegate?.inputBar(self, textViewTextDidChangeTo: trimmedText)
  732. if shouldInvalidateIntrinsicContentSize {
  733. // Prevent un-needed content size invalidation
  734. invalidateIntrinsicContentSize()
  735. }
  736. }
  737. /// Calls each items `keyboardEditingBeginsAction` method
  738. @objc
  739. open func inputTextViewDidBeginEditing() {
  740. items.forEach { $0.keyboardEditingBeginsAction() }
  741. }
  742. /// Calls each items `keyboardEditingEndsAction` method
  743. @objc
  744. open func inputTextViewDidEndEditing() {
  745. items.forEach { $0.keyboardEditingEndsAction() }
  746. }
  747. // MARK: - Plugins
  748. /// Reloads each of the plugins
  749. open func reloadPlugins() {
  750. inputPlugins.forEach { $0.reloadData() }
  751. }
  752. /// Invalidates each of the plugins
  753. open func invalidatePlugins() {
  754. inputPlugins.forEach { $0.invalidate() }
  755. }
  756. // MARK: - User Actions
  757. /// Calls each items `keyboardSwipeGestureAction` method
  758. /// Calls the delegates `didSwipeTextViewWith` method
  759. @objc
  760. open func didSwipeTextView(_ gesture: UISwipeGestureRecognizer) {
  761. items.forEach { $0.keyboardSwipeGestureAction(with: gesture) }
  762. delegate?.inputBar(self, didSwipeTextViewWith: gesture)
  763. }
  764. /// Calls the delegates `didPressSendButtonWith` method
  765. /// Assumes that the InputTextView's text has been set to empty and calls `inputTextViewDidChange()`
  766. /// Invalidates each of the InputPlugins
  767. open func didSelectSendButton() {
  768. delegate?.inputBar(self, didPressSendButtonWith: inputTextView.text)
  769. }
  770. }