123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- /*
- MIT License
-
- Copyright (c) 2017-2018 MessageKit
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
- import UIKit
- open class MessagesCollectionView: UICollectionView {
- // MARK: - Properties
- open weak var messagesDataSource: MessagesDataSource?
- open weak var messagesDisplayDelegate: MessagesDisplayDelegate?
- open weak var messagesLayoutDelegate: MessagesLayoutDelegate?
- open weak var messageCellDelegate: MessageCellDelegate?
- private var indexPathForLastItem: IndexPath? {
- let lastSection = numberOfSections - 1
- guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil }
- return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection)
- }
- // MARK: - Initializers
- public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
- super.init(frame: frame, collectionViewLayout: layout)
- backgroundColor = .white
- registerReusableViews()
- setupGestureRecognizers()
- }
-
- required public init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- public convenience init() {
- self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout())
- }
- // MARK: - Methods
-
- private func registerReusableViews() {
- register(TextMessageCell.self)
- register(MediaMessageCell.self)
- register(LocationMessageCell.self)
- register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader)
- register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter)
- }
-
- private func setupGestureRecognizers() {
- let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
- tapGesture.delaysTouchesBegan = true
- addGestureRecognizer(tapGesture)
- }
-
- @objc
- open func handleTapGesture(_ gesture: UIGestureRecognizer) {
- guard gesture.state == .ended else { return }
-
- let touchLocation = gesture.location(in: self)
- guard let indexPath = indexPathForItem(at: touchLocation) else { return }
-
- let cell = cellForItem(at: indexPath) as? MessageContentCell
- cell?.handleTapGesture(gesture)
- }
- public func scrollToBottom(animated: Bool = false) {
- let collectionViewContentHeight = collectionViewLayout.collectionViewContentSize.height
- performBatchUpdates(nil) { _ in
- self.scrollRectToVisible(CGRect(0.0, collectionViewContentHeight - 1.0, 1.0, 1.0), animated: animated)
- }
- }
-
- public func reloadDataAndKeepOffset() {
- // stop scrolling
- setContentOffset(contentOffset, animated: false)
-
- // calculate the offset and reloadData
- let beforeContentSize = contentSize
- reloadData()
- layoutIfNeeded()
- let afterContentSize = contentSize
-
- // reset the contentOffset after data is updated
- let newOffset = CGPoint(
- x: contentOffset.x + (afterContentSize.width - beforeContentSize.width),
- y: contentOffset.y + (afterContentSize.height - beforeContentSize.height))
- setContentOffset(newOffset, animated: false)
- }
- /// Registers a particular cell using its reuse-identifier
- public func register<T: UICollectionViewCell>(_ cellClass: T.Type) {
- register(cellClass, forCellWithReuseIdentifier: String(describing: T.self))
- }
- /// Registers a reusable view for a specific SectionKind
- public func register<T: UICollectionReusableView>(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) {
- register(headerFooterClass,
- forSupplementaryViewOfKind: kind,
- withReuseIdentifier: String(describing: T.self))
- }
- /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
- public func dequeueReusableCell<T: UICollectionViewCell>(_ cellClass: T.Type, for indexPath: IndexPath) -> T {
- guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else {
- fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))")
- }
- return cell
- }
- /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
- public func dequeueReusableHeaderView<T: UICollectionReusableView>(_ viewClass: T.Type, for indexPath: IndexPath) -> T {
- let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath)
- guard let viewType = view as? T else {
- fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))")
- }
- return viewType
- }
- /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal
- public func dequeueReusableFooterView<T: UICollectionReusableView>(_ viewClass: T.Type, for indexPath: IndexPath) -> T {
- let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: T.self), for: indexPath)
- guard let viewType = view as? T else {
- fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))")
- }
- return viewType
- }
- }
|