JCChatView.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. //
  2. // JCChatView.swift
  3. // JChat
  4. //
  5. // Created by deng on 2017/2/28.
  6. // Copyright © 2017年 HXHG. All rights reserved.
  7. //
  8. import UIKit
  9. public protocol JCChatViewDataSource: class {
  10. func numberOfItems(in chatView: JCChatView)
  11. func chatView(_ chatView: JCChatView, itemAtIndexPath: IndexPath)
  12. }
  13. var isWait = false
  14. @objc public protocol JCChatViewDelegate: NSObjectProtocol {
  15. @objc optional func chatView(_ chatView: JCChatView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool
  16. @objc optional func chatView(_ chatView: JCChatView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool
  17. @objc optional func chatView(_ chatView: JCChatView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?)
  18. @objc optional func refershChatView(chatView: JCChatView)
  19. @objc optional func tapImageMessage(image: UIImage?, indexPath: IndexPath)
  20. @objc optional func deleteMessage(message: JCMessageType)
  21. @objc optional func copyMessage(message: JCMessageType)
  22. @objc optional func forwardMessage(message: JCMessageType)
  23. @objc optional func withdrawMessage(message: JCMessageType)
  24. @objc optional func indexPathsForVisibleItems(chatView: JCChatView, items: [IndexPath])
  25. }
  26. @objc open class JCChatView: UIView {
  27. public init(frame: CGRect, chatViewLayout: JCChatViewLayout) {
  28. _chatViewData = JCChatViewData()
  29. _chatViewLayout = chatViewLayout
  30. let containerViewFrame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
  31. _chatContainerView = JCChatContainerView(frame: containerViewFrame, collectionViewLayout: chatViewLayout)
  32. super.init(frame: frame)
  33. _commonInit()
  34. }
  35. public required init?(coder aDecoder: NSCoder) {
  36. // decode layout
  37. guard let chatViewLayout = JCChatViewLayout(coder: aDecoder) else {
  38. return nil
  39. }
  40. // decode container view
  41. guard let chatContainerView = JCChatContainerView(coder: aDecoder) else {
  42. return nil
  43. }
  44. // init data
  45. _chatViewData = JCChatViewData()
  46. // init to layout & container view
  47. _chatViewLayout = chatViewLayout
  48. _chatContainerView = chatContainerView
  49. // init super
  50. super.init(coder: aDecoder)
  51. // init other data
  52. _commonInit()
  53. }
  54. open weak var delegate: JCChatViewDelegate?
  55. open weak var dataSource: JCChatViewDataSource?
  56. open weak var messageDelegate: JCMessageDelegate?
  57. func insert(_ newMessage: JCMessageType, at index: Int) {
  58. _batchBegin()
  59. _batchItems.append(.insert(newMessage, at: index))
  60. _batchCommit()
  61. }
  62. func insert(contentsOf newMessages: Array<JCMessageType>, at index: Int) {
  63. _batchBegin()
  64. _batchItems.append(contentsOf: newMessages.map({ .insert($0, at: index) }))
  65. _batchCommit(true)
  66. }
  67. func update(_ newMessage: JCMessageType, at index: Int) {
  68. _batchBegin()
  69. _batchItems.append(.update(newMessage, at: index))
  70. _batchCommit()
  71. }
  72. func removeAll() {
  73. _batchBegin()
  74. for index in 0..<_chatViewData.count {
  75. _batchItems.append(.remove(at: index))
  76. }
  77. _batchCommit()
  78. }
  79. func remove(at index: Int) {
  80. _batchBegin()
  81. _batchItems.append(.remove(at: index))
  82. _batchCommit()
  83. }
  84. func remove(contentOf indexs: Array<Int>) {
  85. _batchBegin()
  86. _batchItems.append(contentsOf: indexs.map({ .remove(at: $0) }))
  87. _batchCommit()
  88. }
  89. func move(at index1: Int, to index2: Int) {
  90. _batchBegin()
  91. _batchItems.append(.move(at: index1, to: index2))
  92. _batchCommit()
  93. }
  94. func append(_ newMessage: JCMessageType) {
  95. insert(newMessage, at: _chatViewData.count)
  96. }
  97. func append(contentsOf newMessages: Array<JCMessageType>) {
  98. insert(contentsOf: newMessages, at: _chatViewData.count)
  99. }
  100. fileprivate func _batchBegin() {
  101. _chatContainerView.messageDelegate = self.messageDelegate
  102. objc_sync_enter(_batchItems)
  103. _batchRequiredCount = max(_batchRequiredCount + 1, 1)
  104. objc_sync_exit(_batchItems)
  105. }
  106. fileprivate func _batchCommit(_ isInsert: Bool = false) {
  107. objc_sync_enter(_batchItems)
  108. _batchRequiredCount = max(_batchRequiredCount - 1, 0)
  109. guard _batchRequiredCount == 0 else {
  110. objc_sync_exit(_batchItems)
  111. return
  112. }
  113. let oldData = _chatViewData
  114. let newData = JCChatViewData()
  115. let updateItems = _batchItems
  116. _batchItems.removeAll()
  117. objc_sync_exit(_batchItems)
  118. _ = _chatContainerView.numberOfItems(inSection: 0)
  119. let update = JCChatViewUpdate(newData: newData, oldData: oldData, updateItems: updateItems)
  120. // exec
  121. _chatViewData = newData
  122. _chatContainerView.performBatchUpdates(with: update, isInsert, completion: nil)
  123. }
  124. fileprivate lazy var _batchItems: Array<JCChatViewUpdateChangeItem> = []
  125. fileprivate lazy var _batchRequiredCount: Int = 0
  126. private func _commonInit() {
  127. backgroundColor = UIColor(netHex: 0xe8edf3)
  128. let header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(_onPullToFresh))
  129. header?.stateLabel.isHidden = true
  130. _chatContainerView.mj_header = header
  131. _chatContainerView.allowsSelection = false
  132. _chatContainerView.allowsMultipleSelection = false
  133. _chatContainerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  134. _chatContainerView.keyboardDismissMode = .onDrag
  135. _chatContainerView.backgroundColor = UIColor(netHex: 0xE8EDF3)
  136. _chatContainerView.dataSource = self
  137. _chatContainerView.delegate = self
  138. addSubview(_chatContainerView)
  139. #if READ_VERSION
  140. _chatContainerView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
  141. #endif
  142. }
  143. fileprivate var _chatViewData: JCChatViewData
  144. fileprivate var _chatViewLayout: JCChatViewLayout
  145. fileprivate var _chatContainerView: JCChatContainerView
  146. fileprivate lazy var _chatContainerRegistedTypes: Set<String> = []
  147. @objc func _onPullToFresh() {
  148. delegate?.refershChatView?(chatView: self)
  149. }
  150. func stopRefresh() {
  151. _chatContainerView.mj_header.endRefreshing()
  152. }
  153. func scrollToLast(animated: Bool) {
  154. let count = _chatContainerView.numberOfItems(inSection: 0)
  155. if count > 0 {
  156. _chatContainerView.scrollToItem(at: IndexPath(row: count - 1, section: 0), at: .bottom, animated: animated)
  157. }
  158. }
  159. deinit {
  160. #if READ_VERSION
  161. _chatContainerView.removeObserver(self, forKeyPath: "contentOffset")
  162. #endif
  163. }
  164. }
  165. internal class JCChatContainerView: UICollectionView {
  166. weak var messageDelegate: JCMessageDelegate?
  167. var currentUpdate: JCChatViewUpdate? {
  168. return _currentUpdate
  169. }
  170. func performBatchUpdates(with update: JCChatViewUpdate, _ isInsert: Bool = false, completion:((Bool) -> Void)?) {
  171. // read changes
  172. guard let changes = update.updateChanges else {
  173. return
  174. }
  175. _currentUpdate = update
  176. // TODO: 不是最优
  177. if update.updateItems.count > 0 {
  178. for item in update.updateItems {
  179. switch item {
  180. case .update:
  181. self.performBatchUpdates({
  182. self.reloadItems(at: [IndexPath(row: item.at, section: 0)])
  183. }, completion: nil)
  184. return
  185. default:
  186. break
  187. }
  188. }
  189. }
  190. var oldContent = self.contentSize
  191. // self.contentSize = CGSize(width: self.contentSize.width, height: 3725)
  192. // self.setContentOffset(CGPoint(x: 0, y: 3725 - oldContent.height), animated: false)
  193. // self.layoutIfNeeded()
  194. // self.setContentOffset(CGPoint(x: 0, y: 250232320), animated: false)
  195. // self.layoutIfNeeded()
  196. // commit changes
  197. UIView.animate(withDuration: 0) {
  198. if isInsert {
  199. self.isHidden = true
  200. }
  201. self.performBatchUpdates({
  202. // apply move
  203. changes.filter({ $0.isMove }).forEach({
  204. self.moveItem(at: .init(item: max($0.from, 0), section: 0),
  205. to: .init(item: max($0.to, 0), section: 0))
  206. })
  207. // print(oldContent)
  208. // apply insert/remove/update
  209. self.insertItems(at: changes.filter({ $0.isInsert }).map({ .init(item: max($0.to, 0), section: 0) }))
  210. self.reloadItems(at: changes.filter({ $0.isUpdate }).map({ .init(item: max($0.from, 0), section: 0) }))
  211. self.deleteItems(at: changes.filter({ $0.isRemove }).map({ .init(item: max($0.from, 0), section: 0) }))
  212. }, completion: { finished in
  213. if isInsert {
  214. UIView.animate(withDuration: 0, animations: {
  215. if self.contentSize.height > oldContent.height && oldContent.height != 0 {
  216. self.setContentOffset(CGPoint(x: 0, y: self.contentSize.height - oldContent.height), animated: false)
  217. self.layoutIfNeeded()
  218. oldContent = self.contentSize
  219. }
  220. })
  221. self.isHidden = false
  222. }
  223. completion?(finished)
  224. })
  225. }
  226. _currentUpdate = nil
  227. }
  228. private var _currentUpdate: JCChatViewUpdate?
  229. }
  230. extension JCChatView: UICollectionViewDataSource, JCChatViewLayoutDelegate {
  231. open var isRoll: Bool {
  232. return _chatContainerView.isDragging || _chatContainerView.isDecelerating
  233. }
  234. open dynamic var indexPathsForVisibleItems: [IndexPath] {
  235. return _chatContainerView.indexPathsForVisibleItems
  236. }
  237. open dynamic var contentSize: CGSize {
  238. set { return _chatContainerView.contentSize = newValue }
  239. get { return _chatContainerView.contentSize }
  240. }
  241. open dynamic var contentOffset: CGPoint {
  242. set { return _chatContainerView.contentOffset = newValue }
  243. get { return _chatContainerView.contentOffset }
  244. }
  245. open dynamic var contentInset: UIEdgeInsets {
  246. set { return _chatContainerView.contentInset = newValue }
  247. get { return _chatContainerView.contentInset }
  248. }
  249. open dynamic var scrollIndicatorInsets: UIEdgeInsets {
  250. set { return _chatContainerView.scrollIndicatorInsets = newValue }
  251. get { return _chatContainerView.scrollIndicatorInsets }
  252. }
  253. open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  254. return _chatViewData.count
  255. }
  256. open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  257. let message = _chatViewData[indexPath.item]
  258. // let options = (message.options.showsCard.hashValue << 0) | (message.options.showsAvatar.hashValue << 1)
  259. let alignment = message.options.alignment.rawValue
  260. let identifier = NSStringFromClass(type(of: message.content)) + ".\(alignment)"
  261. if !_chatContainerRegistedTypes.contains(identifier) {
  262. _chatContainerRegistedTypes.insert(identifier)
  263. _chatContainerView.register(JCChatViewCell.self, forCellWithReuseIdentifier: identifier)
  264. }
  265. let cell = _chatContainerView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! JCChatViewCell
  266. cell.delegate = messageDelegate
  267. cell.updateView()
  268. return cell
  269. }
  270. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, itemAt indexPath: IndexPath) -> JCMessageType {
  271. return _chatViewData[indexPath.item]
  272. }
  273. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  274. guard let collectionViewLayout = collectionViewLayout as? JCChatViewLayout else {
  275. return .zero
  276. }
  277. guard let layoutAttributesInfo = collectionViewLayout.layoutAttributesInfoForItem(at: indexPath) else {
  278. return .zero
  279. }
  280. let size = layoutAttributesInfo.layoutedBoxRect(with: .all).size
  281. return .init(width: collectionView.frame.width, height: size.height)
  282. }
  283. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAvatarOf style: JCMessageStyle) -> CGSize {
  284. // 78 * 78
  285. return .init(width: 40, height: 40)
  286. }
  287. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemCardOf style: JCMessageStyle) -> CGSize {
  288. return .init(width: 0, height: 18)
  289. }
  290. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemTipsOf style: JCMessageStyle) -> CGSize {
  291. return .init(width: 100, height: 21)
  292. }
  293. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemOf style: JCMessageStyle) -> UIEdgeInsets {
  294. switch style {
  295. case .bubble:
  296. // bubble content edg, 2x
  297. // +----12--+-+---+
  298. // | | | |
  299. // 16 4 40 16
  300. // | | | |
  301. // +----12--+-+---+
  302. return .init(top: 6, left: 8, bottom: 6, right: 2 + 20 + 8)
  303. case .notice:
  304. // default edg
  305. // +----10----+
  306. // 20 20
  307. // +----10----+
  308. return .init(top: 10, left: 20, bottom: 10, right: 20)
  309. // default:
  310. // // default edg
  311. // // +----10----+
  312. // // 10 10
  313. // // +----10----+
  314. // return .init(top: 10, left: 10, bottom: 10, right: 10)
  315. }
  316. }
  317. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemCardOf style: JCMessageStyle) -> UIEdgeInsets {
  318. return .init(top: 0, left: 8, bottom: 2, right: 8)
  319. }
  320. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemAvatarOf style: JCMessageStyle) -> UIEdgeInsets {
  321. return .init(top: 0, left: 2, bottom: 2, right: 2)
  322. }
  323. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemBubbleOf style: JCMessageStyle) -> UIEdgeInsets {
  324. // return .init(top: -2, left: 0, bottom: -2, right: 0)
  325. return .init(top: 0, left: 8, bottom: 0, right: 0)
  326. }
  327. open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemContentOf style: JCMessageStyle) -> UIEdgeInsets {
  328. switch style {
  329. case .bubble:
  330. // bubble image edg, scale: 2x, radius: 15
  331. // /--------16-------\
  332. // | +-----04-----+ |
  333. // 20 04 04 20
  334. // | +-----04-----+ |
  335. // \--------16-------/
  336. // return .init(top: 8 + 2, left: 10 + 2, bottom: 8 + 2, right: 10 + 2)
  337. return .init(top: 2, left: 5 + 2, bottom: 2, right: 2)
  338. case .notice:
  339. // notice edg
  340. // /------4-------\
  341. // 10 10
  342. // \------4-------/
  343. return .init(top: 4, left: 10, bottom: 4, right: 10)
  344. }
  345. }
  346. open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
  347. return true
  348. }
  349. open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
  350. let message = _chatViewData[indexPath.item]
  351. if message.content is JCMessageNoticeContent || message.content is JCMessageTimeLineContent {
  352. return false
  353. }
  354. if let _ = message.content as? JCMessageTextContent {
  355. if action == #selector(copyMessage(_:)) {
  356. return true
  357. }
  358. }
  359. if action == #selector(deleteMessage(_:)) {
  360. return true
  361. }
  362. if action == #selector(forwardMessage(_:)) {
  363. return true
  364. }
  365. if action == #selector(withdrawMessage(_:)) {
  366. if let sender = message.sender {
  367. if sender.isEqual(to: JMSGUser.myInfo()) {
  368. return true
  369. }
  370. }
  371. return false
  372. }
  373. return false
  374. }
  375. @objc func copyMessage(_ sender: Any) {}
  376. @objc func deleteMessage(_ sender: Any) {}
  377. @objc func forwardMessage(_ sender: Any) {}
  378. @objc func withdrawMessage(_ sender: Any) {}
  379. open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
  380. let message = _chatViewData[indexPath.item]
  381. if action == #selector(copyMessage(_:)) {
  382. if let content = message.content as? JCMessageTextContent {
  383. let pas = UIPasteboard.general
  384. pas.string = content.text.string
  385. }
  386. }
  387. if action == #selector(deleteMessage(_:)) {
  388. remove(at: indexPath.item)
  389. delegate?.deleteMessage?(message: message)
  390. }
  391. // if action == #selector(paste(_:)) {
  392. // move(at: indexPath.item, to: _chatViewData.count - 1)
  393. // }
  394. if action == #selector(forwardMessage(_:)) {
  395. delegate?.forwardMessage?(message: message)
  396. }
  397. if action == #selector(withdrawMessage(_:)) {
  398. delegate?.withdrawMessage?(message: message)
  399. }
  400. }
  401. }
  402. extension JCChatView {
  403. override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  404. if keyPath == "contentOffset" {
  405. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
  406. if !isWait {
  407. isWait = true
  408. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
  409. self.delegate?.indexPathsForVisibleItems?(chatView: self, items: self._chatContainerView.indexPathsForVisibleItems)
  410. isWait = false
  411. }
  412. }
  413. }
  414. }
  415. }
  416. }
  417. extension JCChatView: SAIInputBarScrollViewType {
  418. }