From fa06818e748e39fa88b5bdb922f68117dfdc46b0 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sun, 14 Oct 2018 22:04:48 -0700 Subject: [PATCH 01/13] Typing Indicator as a Supplementary View --- .../AdvancedExampleViewController.swift | 22 +-- MessageKit.xcodeproj/project.pbxproj | 16 ++ .../Controllers/MessagesViewController.swift | 17 ++ .../MessagesCollectionViewFlowLayout.swift | 81 ++++++++- .../Protocols/MessagesDisplayDelegate.swift | 13 ++ .../Protocols/MessagesLayoutDelegate.swift | 27 +++ Sources/Views/BubbleCircle.swift | 49 ++++++ .../TypingIndicatorView.swift | 66 +++++++ Sources/Views/MessagesCollectionView.swift | 40 ++++- Sources/Views/TypingBubble.swift | 155 ++++++++++++++++ Sources/Views/TypingIndicator.swift | 165 ++++++++++++++++++ 11 files changed, 632 insertions(+), 19 deletions(-) create mode 100644 Sources/Views/BubbleCircle.swift create mode 100644 Sources/Views/Headers & Footers/TypingIndicatorView.swift create mode 100644 Sources/Views/TypingBubble.swift create mode 100644 Sources/Views/TypingIndicator.swift diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index d9274957..603d81cc 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -37,10 +37,6 @@ final class AdvancedExampleViewController: ChatViewController { super.viewDidLoad() updateTitleView(title: "MessageKit", subtitle: "2 Online") - - // Customize the typing bubble! These are the default values -// typingBubbleBackgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) -// typingBubbleDotColor = .lightGray } override func viewDidAppear(_ animated: Bool) { @@ -48,12 +44,11 @@ final class AdvancedExampleViewController: ChatViewController { MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) .onTypingStatus { [weak self] in - self?.setTypingIndicatorHidden(false) + self?.setTypingIndicatorViewHidden(false) }.onNewMessage { [weak self] message in - self?.setTypingIndicatorHidden(true, performUpdates: { + self?.setTypingIndicatorViewHidden(true, performUpdates: { // self?.insertMessage(message) }) - self?.insertMessage(message) } } @@ -186,14 +181,13 @@ final class AdvancedExampleViewController: ChatViewController { return messageList[indexPath.section].user == messageList[indexPath.section + 1].user } - func setTypingIndicatorHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { + func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") -// setTypingBubbleHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in -// if self?.isLastSectionVisible() == true { -// self?.messagesCollectionView.scrollToBottom(animated: true) -// } -// } -// messagesCollectionView.scrollToBottom(animated: true) + setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in + if self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToBottom(animated: true) + } + } } private func makeButton(named: String) -> InputBarButtonItem { diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 6ed27369..9b277bfa 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -45,6 +45,10 @@ 5073C1192175BE960040EAD5 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */; }; 5073C11D2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */; }; 5073C1232175C1980040EAD5 /* sound1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5073C1222175C1980040EAD5 /* sound1.m4a */; }; + 38F8063221740D9E00CDB9DB /* TypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */; }; + 38F8063321740D9E00CDB9DB /* BubbleCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */; }; + 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063421740DAD00CDB9DB /* TypingBubble.swift */; }; + 38F8063721740DD500CDB9DB /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063621740DD500CDB9DB /* TypingIndicatorView.swift */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */; }; 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC851F87AB230030B058 /* AvatarViewTests.swift */; }; @@ -149,6 +153,10 @@ 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = ""; }; 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageSizeCalculator.swift; sourceTree = ""; }; 5073C1222175C1980040EAD5 /* sound1.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = sound1.m4a; sourceTree = ""; }; + 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = ""; }; + 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleCircle.swift; sourceTree = ""; }; + 38F8063421740DAD00CDB9DB /* TypingBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingBubble.swift; sourceTree = ""; }; + 38F8063621740DD500CDB9DB /* TypingIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatterTests.swift; sourceTree = ""; }; @@ -248,6 +256,7 @@ 2EB618F11F846899007FBA0E /* Headers & Footers */ = { isa = PBXGroup; children = ( + 38F8063621740DD500CDB9DB /* TypingIndicatorView.swift */, 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); path = "Headers & Footers"; @@ -416,11 +425,14 @@ 2EB618F01F84676A007FBA0E /* Cells */, 2EB618F11F846899007FBA0E /* Headers & Footers */, B7A03F3E1F86694F006AEF79 /* AvatarView.swift */, + 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */, + 38F8063421740DAD00CDB9DB /* TypingBubble.swift */, 1FE783A7206633C0007FA024 /* InsetLabel.swift */, B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */, B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */, B7A03F441F86694F006AEF79 /* PlayButtonView.swift */, + 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */, ); path = Views; sourceTree = ""; @@ -613,6 +625,7 @@ B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, + 38F8063321740D9E00CDB9DB /* BubbleCircle.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 5073C11D2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, @@ -627,6 +640,7 @@ B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, + 38F8063721740DD500CDB9DB /* TypingIndicatorView.swift in Sources */, 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */, B7A03F2E1F866895006AEF79 /* MessageKind.swift in Sources */, B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */, @@ -648,6 +662,7 @@ B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, + 38F8063221740D9E00CDB9DB /* TypingIndicator.swift in Sources */, B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */, @@ -655,6 +670,7 @@ 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */, + 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */, 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 706b9567..2851d529 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -171,6 +171,21 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } } + // MARK: - Typing Indicator API + + /// Sets the typing indicator sate by inserting/deleting the `TypingBubbleCell` + /// + /// - Parameters: + /// - isHidden: A Boolean value that is to be the new state of the typing indicator + /// - animated: A Boolean value determining if the insertion is to be animated + /// - updates: A block of code that will be executed during `performBatchUpdates` + /// when `animated` is `TRUE` or before the `completion` block executes + /// when `animated` is `FALSE` + /// - completion: A completion block to execute after the insertion/deletion + open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { + messagesCollectionView.setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates, completion: completion) + } + // MARK: - UICollectionViewDataSource open func numberOfSections(in collectionView: UICollectionView) -> Int { @@ -238,6 +253,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) case UICollectionView.elementKindSectionFooter: return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) + case MessagesCollectionView.elementKindTypingIndicator: + return displayDelegate.typingIndicatorView(for: indexPath, in: messagesCollectionView) default: fatalError(MessageKitError.unrecognizedSectionKind) } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 45b8e11b..7c42825e 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -62,18 +62,18 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return collectionView.frame.width - sectionInset.left - sectionInset.right } + public private(set) var isTypingIndicatorViewHidden: Bool = true + // MARK: - Initializers public override init() { super.init() - setupView() setupObserver() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - setupView() setupObserver() } @@ -92,13 +92,59 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) } + // MARK: - Typing Indicator API + + /// Sets the typing indicator sate by inserting/deleting the `TypingIndicatorView` + /// + /// - Parameters: + /// - isHidden: A Boolean value that is to be the new state of the typing indicator + /// - animated: A Boolean value determining if the insertion is to be animated + /// - updates: A block of code that will be executed during `performBatchUpdates` + /// when `animated` is `TRUE` or before the `completion` block executes + /// when `animated` is `FALSE` + /// - completion: A completion block to execute after the insertion/deletion + open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { + + guard isTypingIndicatorViewHidden != isHidden else { return } + isTypingIndicatorViewHidden = isHidden + + if animated { + messagesCollectionView.performBatchUpdates({ [weak self] in + self?.invalidateLayout() + updates?() + }, completion: { [weak self] success in + if success { + self?.adjustBottomInsetForTypingIndicatorView() + } + completion?(success) + }) + } else { + updates?() + invalidateLayout() + adjustBottomInsetForTypingIndicatorView() + completion?(true) + } + } + + private func adjustBottomInsetForTypingIndicatorView() { + guard let delegate = messagesCollectionView.messagesLayoutDelegate else { return } + let height = delegate.typingIndicatorViewSize(in: messagesCollectionView).height + let inset = delegate.typingIndicatorViewTopInset(in: messagesCollectionView) + let totalHeight = height + inset + let delta = isTypingIndicatorViewHidden ? -totalHeight : totalHeight + messagesCollectionView.contentInset.bottom += delta + } + // MARK: - Attributes open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { + guard var attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } for attributes in attributesArray where attributes.representedElementCategory == .cell { + if let supplementaryAttributes = layoutAttributesForSupplementaryView(ofKind: MessagesCollectionView.elementKindTypingIndicator, at: attributes.indexPath) as? MessagesCollectionViewLayoutAttributes { + attributesArray.append(supplementaryAttributes) + } let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) cellSizeCalculator.configure(attributes: attributes) } @@ -116,6 +162,35 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return attributes } + open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + switch elementKind { + case MessagesCollectionView.elementKindTypingIndicator: + + guard shouldDisplayTypingIndicatorView(at: indexPath) else { return nil } + guard let delegate = messagesCollectionView.messagesLayoutDelegate else { return nil } + let size = delegate.typingIndicatorViewSize(in: messagesCollectionView) + guard size != .zero else { return nil } + let inset = delegate.typingIndicatorViewTopInset(in: messagesCollectionView) + let attributes = MessagesCollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath) + + if let itemAttributes = layoutAttributesForItem(at: indexPath) { + attributes.frame = CGRect(x: itemAttributes.frame.origin.x, + y: itemAttributes.frame.maxY + inset, + width: size.width, + height: size.height) + attributes.zIndex = 1 + } + return attributes + default: + return super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) + } + } + + public func shouldDisplayTypingIndicatorView(at indexPath: IndexPath) -> Bool { + let isLastIndexPath = indexPath.section == messagesCollectionView.numberOfSections - 1 + return isLastIndexPath && !isTypingIndicatorViewHidden + } + // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 29d43e80..5cdcb2f4 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -71,6 +71,13 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView + + /// The section footer to use for a given `IndexPath`. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the footer. + /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. + func typingIndicatorView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class. /// @@ -241,6 +248,12 @@ public extension MessagesDisplayDelegate { func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) } + + func typingIndicatorView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + let view = messagesCollectionView.dequeueReusableTypingIndicatorView(TypingIndicatorView.self, for: indexPath) + view.typingBubble.startAnimating() + return view + } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { avatarView.initials = "?" diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 57c4ec33..41a2cf3c 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -48,6 +48,25 @@ public protocol MessagesLayoutDelegate: AnyObject { /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize + /// Specifies the size to use for a typing indicator view. + /// + /// - Parameters: + /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed. + /// + /// - Note: + /// The default value returned by this method is the width of the `messagesCollectionView` and + /// a height of 52. + func typingIndicatorViewSize(in messagesCollectionView: MessagesCollectionView) -> CGSize + + /// Specifies the top inset to use for a typing indicator view. + /// + /// - Parameters: + /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed. + /// + /// - Note: + /// The default value returned by this method is a top inset of 15. + func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat + /// Specifies the height for the `MessageContentCell`'s top label. /// /// - Parameters: @@ -114,6 +133,14 @@ public extension MessagesLayoutDelegate { return .zero } + func typingIndicatorViewSize(in messagesCollectionView: MessagesCollectionView) -> CGSize { + return CGSize(width: messagesCollectionView.bounds.width, height: 52) + } + + func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 15 + } + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return 0 } diff --git a/Sources/Views/BubbleCircle.swift b/Sources/Views/BubbleCircle.swift new file mode 100644 index 00000000..48c0dad9 --- /dev/null +++ b/Sources/Views/BubbleCircle.swift @@ -0,0 +1,49 @@ +/* + 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 + +/// A `UIView` subclass that maintains a mask to keep it fully circular +open class BubbleCircle: UIView { + + /// Lays out subviews and applys a circular mask to the layer + open override func layoutSubviews() { + super.layoutSubviews() + layer.mask = roundedMask(corners: .allCorners, radius: bounds.height / 2) + } + + /// Returns a rounded mask of the view + /// + /// - Parameters: + /// - corners: The corners to round + /// - radius: The radius of curve + /// - Returns: A mask + open func roundedMask(corners: UIRectCorner, radius: CGFloat) -> CAShapeLayer { + let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + return mask + } + +} diff --git a/Sources/Views/Headers & Footers/TypingIndicatorView.swift b/Sources/Views/Headers & Footers/TypingIndicatorView.swift new file mode 100644 index 00000000..6a22611c --- /dev/null +++ b/Sources/Views/Headers & Footers/TypingIndicatorView.swift @@ -0,0 +1,66 @@ +/* + 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 + +/// A subclass of `MessageReusableView` used to display the typing indicator. +open class TypingIndicatorView: MessageReusableView { + + // MARK: - Subviews + + open var typingBubble = TypingBubble() + + // MARK: - Initialization + + public override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSubviews() + } + + open func setupSubviews() { + autoresizingMask = [.flexibleWidth, .flexibleHeight] + addSubview(typingBubble) + } + + open override func prepareForReuse() { + super.prepareForReuse() + if typingBubble.isAnimating { + typingBubble.stopAnimating() + typingBubble.startAnimating() + } + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + typingBubble.frame = bounds + } + +} diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 0fae30db..3cb68247 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -28,6 +28,8 @@ open class MessagesCollectionView: UICollectionView { // MARK: - Properties + public static var elementKindTypingIndicator = "TypingIndicatorElementKind" + open weak var messagesDataSource: MessagesDataSource? open weak var messagesDisplayDelegate: MessagesDisplayDelegate? @@ -42,6 +44,13 @@ open class MessagesCollectionView: UICollectionView { return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) } + open var messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout { + guard let layout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { + fatalError(MessageKitError.layoutUsedOnForeignType) + } + return layout + } + // MARK: - Initializers public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { @@ -68,6 +77,7 @@ open class MessagesCollectionView: UICollectionView { register(AudioMessageCell.self) register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter) + register(TypingIndicatorView.self, forSupplementaryViewOfKind: MessagesCollectionView.elementKindTypingIndicator) } private func setupGestureRecognizers() { @@ -112,14 +122,31 @@ open class MessagesCollectionView: UICollectionView { setContentOffset(newOffset, animated: false) } + // MARK: - Typing Indicator API + + /// Sets the typing indicator sate by inserting/deleting the `TypingIndicatorView` + /// + /// - Parameters: + /// - isHidden: A Boolean value that is to be the new state of the typing indicator + /// - animated: A Boolean value determining if the insertion is to be animated + /// - updates: A block of code that will be executed during `performBatchUpdates` + /// when `animated` is `TRUE` or before the `completion` block executes + /// when `animated` is `FALSE` + /// - completion: A completion block to execute after the insertion/deletion + open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { + messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates, completion: completion) + } + + // MARK: View Register/Dequeue + /// Registers a particular cell using its reuse-identifier public func register(_ cellClass: T.Type) { register(cellClass, forCellWithReuseIdentifier: String(describing: T.self)) } /// Registers a reusable view for a specific SectionKind - public func register(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { - register(headerFooterClass, + public func register(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) { + register(reusableViewClass, forSupplementaryViewOfKind: kind, withReuseIdentifier: String(describing: T.self)) } @@ -157,4 +184,13 @@ open class MessagesCollectionView: UICollectionView { return viewType } + /// Generically dequeues a typing indicator of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableTypingIndicatorView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: MessagesCollectionView.elementKindTypingIndicator, 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 + } + } diff --git a/Sources/Views/TypingBubble.swift b/Sources/Views/TypingBubble.swift new file mode 100644 index 00000000..a668631b --- /dev/null +++ b/Sources/Views/TypingBubble.swift @@ -0,0 +1,155 @@ +/* + 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 + +/// A subclass of `UIView` that mimics the iMessage typing bubble +open class TypingBubble: UIView { + + // MARK: - Properties + + open var isPulseEnabled: Bool = true + + public private(set) var isAnimating: Bool = false + + open override var backgroundColor: UIColor? { + set { + [contentBubble, cornerBubble, tinyBubble].forEach { $0.backgroundColor = newValue } + } + get { + return contentBubble.backgroundColor + } + } + + private struct AnimationKeys { + static let pulse = "typingBubble.pulse" + } + + // MARK: - Subviews + + /// The indicator used to display the typing animation. + public let typingIndicator = TypingIndicator() + + public let contentBubble = UIView() + + public let cornerBubble = BubbleCircle() + + public let tinyBubble = BubbleCircle() + + // MARK: - Animation Layers + + open var contentPulseAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.scale") + animation.fromValue = 1 + animation.toValue = 1.04 + animation.duration = 1 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + open var circlePulseAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.scale") + animation.fromValue = 1 + animation.toValue = 1.1 + animation.duration = 0.5 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + public override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSubviews() + } + + open func setupSubviews() { + addSubview(tinyBubble) + addSubview(cornerBubble) + addSubview(contentBubble) + contentBubble.addSubview(typingIndicator) + backgroundColor = .incomingGray + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + + // To maintain the iMessage like bubble the width:height ratio of the frame + // must be close to 1.65 + let ratio = bounds.width / bounds.height + let extraRightInset = bounds.width - 1.65/ratio*bounds.width + + let tinyBubbleRadius: CGFloat = bounds.height / 6 + tinyBubble.frame = CGRect(x: 0, + y: bounds.height - tinyBubbleRadius, + width: tinyBubbleRadius, + height: tinyBubbleRadius) + + let cornerBubbleRadius = tinyBubbleRadius * 2 + let offset: CGFloat = tinyBubbleRadius / 6 + cornerBubble.frame = CGRect(x: tinyBubbleRadius - offset, + y: bounds.height - (1.5 * cornerBubbleRadius) + offset, + width: cornerBubbleRadius, + height: cornerBubbleRadius) + + let contentBubbleFrame = CGRect(x: tinyBubbleRadius + offset, + y: 0, + width: bounds.width - (tinyBubbleRadius + offset) - extraRightInset, + height: bounds.height - (tinyBubbleRadius + offset)) + let contentBubbleFrameCornerRadius = contentBubbleFrame.height / 2 + + contentBubble.frame = contentBubbleFrame + contentBubble.layer.cornerRadius = contentBubbleFrameCornerRadius + + let insets = UIEdgeInsets(top: offset, left: contentBubbleFrameCornerRadius / 1.25, bottom: offset, right: contentBubbleFrameCornerRadius / 1.25) + typingIndicator.frame = contentBubble.bounds.inset(by: insets) + } + + // MARK: - Animation API + + open func startAnimating() { + defer { isAnimating = true } + guard !isAnimating else { return } + typingIndicator.startAnimating() + if isPulseEnabled { + contentBubble.layer.add(contentPulseAnimationLayer, forKey: AnimationKeys.pulse) + [cornerBubble, tinyBubble].forEach { $0.layer.add(circlePulseAnimationLayer, forKey: AnimationKeys.pulse) } + } + } + + open func stopAnimating() { + defer { isAnimating = false } + guard isAnimating else { return } + typingIndicator.stopAnimating() + [contentBubble, cornerBubble, tinyBubble].forEach { $0.layer.removeAnimation(forKey: AnimationKeys.pulse) } + } + +} diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift new file mode 100644 index 00000000..2f4d336f --- /dev/null +++ b/Sources/Views/TypingIndicator.swift @@ -0,0 +1,165 @@ +/* + 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 + +/// A `UIView` subclass that holds 3 dots which can be animated +open class TypingIndicator: UIView { + + // MARK: - Properties + + /// The offset that each dot will transform by during the bounce animation + open var bounceOffset: CGFloat = 2.5 + + /// A convenience accessor for the `backgroundColor` of each dot + open var dotColor: UIColor = UIColor.lightGray { + didSet { + dots.forEach { $0.backgroundColor = dotColor } + } + } + + /// A flag that determines if the bounce animation is added in `startAnimating()` + open var isBounceEnabled: Bool = false + + /// A flag that determines if the opacity animation is added in `startAnimating()` + open var isFadeEnabled: Bool = true + + /// A flag indicating the animation state + public private(set) var isAnimating: Bool = false + + /// Keys for each animation layer + private struct AnimationKeys { + static let offset = "typingIndicator.offset" + static let bounce = "typingIndicator.bounce" + static let opacity = "typingIndicator.opacity" + } + + /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE to move the dot to the correct + /// initial offset + open var initialOffsetAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.translation.y") + animation.byValue = -bounceOffset + animation.duration = 0.5 + animation.isRemovedOnCompletion = true + return animation + } + + /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE + open var bounceAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.translation.y") + animation.toValue = -bounceOffset + animation.fromValue = bounceOffset + animation.duration = 0.5 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + /// The `CABasicAnimation` applied when `isFadeEnabled` is TRUE + open var opacityAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = 1 + animation.toValue = 0.5 + animation.duration = 0.5 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + // MARK: - Subviews + + public let stackView = UIStackView() + + public let dots: [BubbleCircle] = { + return [BubbleCircle(), BubbleCircle(), BubbleCircle()] + }() + + // MARK: - Initialization + + public override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + /// Sets up the view + private func setupView() { + dots.forEach { + $0.backgroundColor = dotColor + $0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true + stackView.addArrangedSubview($0) + } + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .fillEqually + addSubview(stackView) + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + stackView.frame = bounds + stackView.spacing = bounds.width > 0 ? 5 : 0 + } + + // MARK: - Animation API + + /// Sets the state of the `TypingIndicator` to animating and applies animation layers + open func startAnimating() { + defer { isAnimating = true } + guard !isAnimating else { return } + var delay: TimeInterval = 0 + for dot in dots { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let `self` = self else { return } + if self.isBounceEnabled { + dot.layer.add(self.initialOffsetAnimationLayer, forKey: AnimationKeys.offset) + let bounceLayer = self.bounceAnimationLayer + bounceLayer.timeOffset = delay + 0.33 + dot.layer.add(bounceLayer, forKey: AnimationKeys.bounce) + } + if self.isFadeEnabled { + dot.layer.add(self.opacityAnimationLayer, forKey: AnimationKeys.opacity) + } + } + delay += 0.33 + } + } + + /// Sets the state of the `TypingIndicator` to not animating and removes animation layers + open func stopAnimating() { + defer { isAnimating = false } + guard isAnimating else { return } + dots.forEach { + $0.layer.removeAnimation(forKey: AnimationKeys.bounce) + $0.layer.removeAnimation(forKey: AnimationKeys.opacity) + } + } + +} From 085f563a912a8196401ed5dfddcf6f72ec99509c Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 1 Mar 2019 00:01:43 -0800 Subject: [PATCH 02/13] Crunch some bugs --- Example/Sources/AppDelegate.swift | 2 +- .../BasicAudioController.swift | 2 +- Example/Sources/Data Generation/Lorem.swift | 2 +- .../Sources/Data Generation/SampleData.swift | 2 +- .../Extensions/UIColor+Extensions.swift | 2 +- .../UIViewController+Extensions.swift | 2 +- .../Layout/CustomMessageFlowLayout.swift | 2 +- Example/Sources/Models/MockMessage.swift | 2 +- Example/Sources/Models/MockSocket.swift | 2 +- Example/Sources/Models/MockUser.swift | 2 +- Example/Sources/Settings+UserDefaults.swift | 2 +- .../AdvancedExampleViewController.swift | 4 +-- .../BasicExampleViewController.swift | 2 +- .../View Controllers/ChatViewController.swift | 6 ++++- .../LaunchViewController.swift | 2 +- .../MessageContainerController.swift | 2 +- .../NavigationController.swift | 2 +- .../SettingsViewController.swift | 2 +- Example/Sources/Views/CustomCell.swift | 2 +- Example/Sources/Views/TableViewCells.swift | 2 +- LICENSE.md | 2 +- .../MessagesCollectionViewFlowLayout.swift | 27 ++++++++++++++++--- Sources/Models/AccessoryPosition.swift | 2 +- .../Protocols/MessagesLayoutDelegate.swift | 2 +- Sources/Views/BubbleCircle.swift | 2 +- .../TypingIndicatorView.swift | 4 +-- Sources/Views/TypingBubble.swift | 2 +- Sources/Views/TypingIndicator.swift | 8 +++--- 28 files changed, 60 insertions(+), 35 deletions(-) diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index ea28caaa..d5efe9f4 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/AudioController/BasicAudioController.swift b/Example/Sources/AudioController/BasicAudioController.swift index 4ff00d2b..3924dce7 100644 --- a/Example/Sources/AudioController/BasicAudioController.swift +++ b/Example/Sources/AudioController/BasicAudioController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Data Generation/Lorem.swift b/Example/Sources/Data Generation/Lorem.swift index 61b7df48..47597730 100755 --- a/Example/Sources/Data Generation/Lorem.swift +++ b/Example/Sources/Data Generation/Lorem.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 46e8f3d7..bbe9c211 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Extensions/UIColor+Extensions.swift b/Example/Sources/Extensions/UIColor+Extensions.swift index f951c17c..8711dbbc 100644 --- a/Example/Sources/Extensions/UIColor+Extensions.swift +++ b/Example/Sources/Extensions/UIColor+Extensions.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Extensions/UIViewController+Extensions.swift b/Example/Sources/Extensions/UIViewController+Extensions.swift index 2d734549..eff46fa3 100644 --- a/Example/Sources/Extensions/UIViewController+Extensions.swift +++ b/Example/Sources/Extensions/UIViewController+Extensions.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift index 004ec703..70df0376 100644 --- a/Example/Sources/Layout/CustomMessageFlowLayout.swift +++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Models/MockMessage.swift b/Example/Sources/Models/MockMessage.swift index e4f95d28..5706c2d6 100644 --- a/Example/Sources/Models/MockMessage.swift +++ b/Example/Sources/Models/MockMessage.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Models/MockSocket.swift b/Example/Sources/Models/MockSocket.swift index c3bc2996..b23c5bcf 100644 --- a/Example/Sources/Models/MockSocket.swift +++ b/Example/Sources/Models/MockSocket.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Models/MockUser.swift b/Example/Sources/Models/MockUser.swift index de17c097..19f6fe79 100644 --- a/Example/Sources/Models/MockUser.swift +++ b/Example/Sources/Models/MockUser.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Settings+UserDefaults.swift b/Example/Sources/Settings+UserDefaults.swift index edddcc72..f638cf4c 100644 --- a/Example/Sources/Settings+UserDefaults.swift +++ b/Example/Sources/Settings+UserDefaults.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 603d81cc..d8c87507 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 @@ -47,7 +47,7 @@ final class AdvancedExampleViewController: ChatViewController { self?.setTypingIndicatorViewHidden(false) }.onNewMessage { [weak self] message in self?.setTypingIndicatorViewHidden(true, performUpdates: { -// self?.insertMessage(message) + self?.insertMessage(message) }) } } diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift index d4445831..9cc5390e 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index 7975afa1..f87101c5 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2017-2018 MessageKit +Copyright (c) 2017-2019 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 @@ -112,6 +112,10 @@ class ChatViewController: MessagesViewController, MessagesDataSource { messageInputBar.delegate = self messageInputBar.inputTextView.tintColor = .primaryColor messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal) + messageInputBar.sendButton.setTitleColor( + UIColor.primaryColor.withAlphaComponent(0.3), + for: .highlighted + ) } // MARK: - Helpers diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index 109db593..fbfb71d2 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/View Controllers/MessageContainerController.swift b/Example/Sources/View Controllers/MessageContainerController.swift index bf170616..e48d6584 100644 --- a/Example/Sources/View Controllers/MessageContainerController.swift +++ b/Example/Sources/View Controllers/MessageContainerController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/View Controllers/NavigationController.swift b/Example/Sources/View Controllers/NavigationController.swift index cb41b0cb..cc2d7eb2 100644 --- a/Example/Sources/View Controllers/NavigationController.swift +++ b/Example/Sources/View Controllers/NavigationController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index 7c39887e..aa8291ea 100644 --- a/Example/Sources/View Controllers/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Views/CustomCell.swift b/Example/Sources/Views/CustomCell.swift index c23b0205..a229f344 100644 --- a/Example/Sources/Views/CustomCell.swift +++ b/Example/Sources/Views/CustomCell.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Example/Sources/Views/TableViewCells.swift b/Example/Sources/Views/TableViewCells.swift index 615ffc13..c03081d8 100644 --- a/Example/Sources/Views/TableViewCells.swift +++ b/Example/Sources/Views/TableViewCells.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/LICENSE.md b/LICENSE.md index edefe0f8..21660f30 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2018 MessageKit +Copyright (c) 2017-2019 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 diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 7c42825e..3d561a57 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -108,9 +108,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { guard isTypingIndicatorViewHidden != isHidden else { return } isTypingIndicatorViewHidden = isHidden + let ctx = UICollectionViewFlowLayoutInvalidationContext() + ctx.invalidateSupplementaryElements(ofKind: MessagesCollectionView.elementKindTypingIndicator, at: [indexPathForTypingIndicatorView()]) + if animated { messagesCollectionView.performBatchUpdates({ [weak self] in - self?.invalidateLayout() + self?.invalidateLayout(with: ctx) updates?() }, completion: { [weak self] success in if success { @@ -120,7 +123,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { }) } else { updates?() - invalidateLayout() + invalidateLayout(with: ctx) adjustBottomInsetForTypingIndicatorView() completion?(true) } @@ -178,7 +181,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { y: itemAttributes.frame.maxY + inset, width: size.width, height: size.height) - attributes.zIndex = 1 } return attributes default: @@ -191,6 +193,11 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return isLastIndexPath && !isTypingIndicatorViewHidden } + private func indexPathForTypingIndicatorView() -> IndexPath { + let section = messagesCollectionView.numberOfSections - 2 + return IndexPath(row: 0, section: max(section, 0)) + } + // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { @@ -209,6 +216,20 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { invalidateLayout() } + open override func indexPathsToInsertForSupplementaryView(ofKind elementKind: String) -> [IndexPath] { + guard elementKind == MessagesCollectionView.elementKindTypingIndicator else { + return super.indexPathsToInsertForSupplementaryView(ofKind: elementKind) + } + return [indexPathForTypingIndicatorView()] + } + + open override func indexPathsToDeleteForSupplementaryView(ofKind elementKind: String) -> [IndexPath] { + guard elementKind == MessagesCollectionView.elementKindTypingIndicator else { + return super.indexPathsToDeleteForSupplementaryView(ofKind: elementKind) + } + return [indexPathForTypingIndicatorView()] + } + // MARK: - Cell Sizing lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) diff --git a/Sources/Models/AccessoryPosition.swift b/Sources/Models/AccessoryPosition.swift index d0496220..04092026 100644 --- a/Sources/Models/AccessoryPosition.swift +++ b/Sources/Models/AccessoryPosition.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 41a2cf3c..3bdcb363 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -134,7 +134,7 @@ public extension MessagesLayoutDelegate { } func typingIndicatorViewSize(in messagesCollectionView: MessagesCollectionView) -> CGSize { - return CGSize(width: messagesCollectionView.bounds.width, height: 52) + return CGSize(width: messagesCollectionView.bounds.width, height: 48) } func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat { diff --git a/Sources/Views/BubbleCircle.swift b/Sources/Views/BubbleCircle.swift index 48c0dad9..c0c276ad 100644 --- a/Sources/Views/BubbleCircle.swift +++ b/Sources/Views/BubbleCircle.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Sources/Views/Headers & Footers/TypingIndicatorView.swift b/Sources/Views/Headers & Footers/TypingIndicatorView.swift index 6a22611c..a6a4c3bd 100644 --- a/Sources/Views/Headers & Footers/TypingIndicatorView.swift +++ b/Sources/Views/Headers & Footers/TypingIndicatorView.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 @@ -29,7 +29,7 @@ open class TypingIndicatorView: MessageReusableView { // MARK: - Subviews - open var typingBubble = TypingBubble() + public let typingBubble = TypingBubble() // MARK: - Initialization diff --git a/Sources/Views/TypingBubble.swift b/Sources/Views/TypingBubble.swift index a668631b..82cfb239 100644 --- a/Sources/Views/TypingBubble.swift +++ b/Sources/Views/TypingBubble.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift index 2f4d336f..60872b37 100644 --- a/Sources/Views/TypingIndicator.swift +++ b/Sources/Views/TypingIndicator.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + Copyright (c) 2017-2019 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 @@ -30,7 +30,7 @@ open class TypingIndicator: UIView { // MARK: - Properties /// The offset that each dot will transform by during the bounce animation - open var bounceOffset: CGFloat = 2.5 + public var bounceOffset: CGFloat = 2.5 /// A convenience accessor for the `backgroundColor` of each dot open var dotColor: UIColor = UIColor.lightGray { @@ -40,10 +40,10 @@ open class TypingIndicator: UIView { } /// A flag that determines if the bounce animation is added in `startAnimating()` - open var isBounceEnabled: Bool = false + public var isBounceEnabled: Bool = false /// A flag that determines if the opacity animation is added in `startAnimating()` - open var isFadeEnabled: Bool = true + public var isFadeEnabled: Bool = true /// A flag indicating the animation state public private(set) var isAnimating: Bool = false From 54ceaa8c33ecf6b3e8bafb8031a65a27ab2152a3 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 1 Mar 2019 00:29:08 -0800 Subject: [PATCH 03/13] Fix content size --- .../MessagesCollectionViewFlowLayout.swift | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 3d561a57..cacf40f2 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -62,6 +62,18 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return collectionView.frame.width - sectionInset.left - sectionInset.right } + open override var collectionViewContentSize: CGSize { + let size = super.collectionViewContentSize + + guard !isTypingIndicatorViewHidden, let delegate = messagesCollectionView.messagesLayoutDelegate else { return size } + let typingIndicatorSize = delegate.typingIndicatorViewSize(in: messagesCollectionView) + let inset = delegate.typingIndicatorViewTopInset(in: messagesCollectionView) + 5 + return CGSize( + width: size.width, + height: size.height + typingIndicatorSize.height + inset + ) + } + public private(set) var isTypingIndicatorViewHidden: Bool = true // MARK: - Initializers @@ -109,22 +121,19 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { isTypingIndicatorViewHidden = isHidden let ctx = UICollectionViewFlowLayoutInvalidationContext() - ctx.invalidateSupplementaryElements(ofKind: MessagesCollectionView.elementKindTypingIndicator, at: [indexPathForTypingIndicatorView()]) + ctx.invalidateSupplementaryElements( + ofKind: MessagesCollectionView.elementKindTypingIndicator, + at: [indexPathForTypingIndicatorView()] + ) if animated { messagesCollectionView.performBatchUpdates({ [weak self] in - self?.invalidateLayout(with: ctx) updates?() - }, completion: { [weak self] success in - if success { - self?.adjustBottomInsetForTypingIndicatorView() - } - completion?(success) - }) + self?.invalidateLayout(with: ctx) + }, completion: completion) } else { updates?() invalidateLayout(with: ctx) - adjustBottomInsetForTypingIndicatorView() completion?(true) } } From 8621566ccfc020b8faa30dcb27334faa4ed3bf7f Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 1 Mar 2019 00:33:42 -0800 Subject: [PATCH 04/13] Changelog --- CHANGELOG.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0395d8..5acf47f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,17 +10,23 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Changed -- **Breaking Change** Deprecated the Sender struct in favor of the `SenderType` protocol. [#909](https://github.com/MessageKit/MessageKit/pull/909) by [@nathantannar4](https://github.com/nathantannar4) +- **Breaking Change** Deprecated the Sender struct in favor of the `SenderType` protocol. +[#909](https://github.com/MessageKit/MessageKit/pull/909) by [@nathantannar4](https://github.com/nathantannar4) -- **Breaking Change** Add support for audio messages [#892](https://github.com/MessageKit/MessageKit/pull/892) by [@moldovaniosif](https://github.com/moldovaniosif). Added new protocols `AudioControllerDelegate`, `AudioItem` a new cell `AudioMessageCell` and a new controller `BasicAudioController`. +- **Breaking Change** Add support for audio messages. Added new protocols `AudioControllerDelegate`, `AudioItem` a new cell `AudioMessageCell` and a new controller `BasicAudioController`. +[#892](https://github.com/MessageKit/MessageKit/pull/892) by [@moldovaniosif](https://github.com/moldovaniosif). -- **Breaking Change** Moved `handleTapGesture` method to `MessageCollectionViewCell` [#950](https://github.com/MessageKit/MessageKit/pull/950) by [@nathantannar4](https://github.com/nathantannar4) +- **Breaking Change** Moved `handleTapGesture` method to `MessageCollectionViewCell` +[#950](https://github.com/MessageKit/MessageKit/pull/950) by [@nathantannar4](https://github.com/nathantannar4) - **Breaking Change** Renamed function `layoutBottomLabel(with:)` to `layoutMessageBottomLabel(with:)` in `MessageContentCell` class. [#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) ### Added +- Added typing indicator support, `func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil)`. Return a custom typing view by conforming to `MessagesDisplayDelegate` or use the [default appearance](https://github.com/nathantannar4/TypingIndicator). Customize the size with `MessagesLayoutDelegate` . +[#989](https://github.com/MessageKit/MessageKit/pull/911) by [@nathantannar4](https://github.com/nathantannar4) + - Added `AccessoryPosition` class. [#989](https://github.com/MessageKit/MessageKit/pull/989) by [@subdiox](https://github.com/subdiox) From 4677ac2b292f516b1108b74b6d1e6f2bc647b100 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 1 Mar 2019 00:39:01 -0800 Subject: [PATCH 05/13] Fix lint/tests --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 2 +- Tests/ControllersTest/MessagesViewControllerSpec.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index cacf40f2..bf5e2a60 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -188,7 +188,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { if let itemAttributes = layoutAttributesForItem(at: indexPath) { attributes.frame = CGRect(x: itemAttributes.frame.origin.x, y: itemAttributes.frame.maxY + inset, - width: size.width, + width: size.width, height: size.height) } return attributes diff --git a/Tests/ControllersTest/MessagesViewControllerSpec.swift b/Tests/ControllersTest/MessagesViewControllerSpec.swift index 59646326..77c4de4c 100644 --- a/Tests/ControllersTest/MessagesViewControllerSpec.swift +++ b/Tests/ControllersTest/MessagesViewControllerSpec.swift @@ -24,7 +24,7 @@ import Quick import Nimble -import MessageInputBar +import InputBarAccessoryView @testable import MessageKit //swiftlint:disable function_body_length From 54bd08e91fa44ba20971cde4190075d2d954b608 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 22 Mar 2019 11:34:12 -0700 Subject: [PATCH 06/13] Protect index path from empty --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index bf5e2a60..2481a5c4 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -198,6 +198,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } public func shouldDisplayTypingIndicatorView(at indexPath: IndexPath) -> Bool { + guard !indexPath.isEmpty else { return false } let isLastIndexPath = indexPath.section == messagesCollectionView.numberOfSections - 1 return isLastIndexPath && !isTypingIndicatorViewHidden } From 8386755c1e508a3ed68c5b177dd22654c556683c Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sat, 23 Mar 2019 23:47:46 -0700 Subject: [PATCH 07/13] Undo hot fix --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 2481a5c4..bf5e2a60 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -198,7 +198,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } public func shouldDisplayTypingIndicatorView(at indexPath: IndexPath) -> Bool { - guard !indexPath.isEmpty else { return false } let isLastIndexPath = indexPath.section == messagesCollectionView.numberOfSections - 1 return isLastIndexPath && !isTypingIndicatorViewHidden } From 488e501ef31fa6045aeaf70798a492f34f6349c5 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Tue, 26 Mar 2019 00:17:16 -0700 Subject: [PATCH 08/13] Invalidate layout before batch updates --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index bf5e2a60..451384e1 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -125,15 +125,14 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { ofKind: MessagesCollectionView.elementKindTypingIndicator, at: [indexPathForTypingIndicatorView()] ) + invalidateLayout(with: ctx) if animated { messagesCollectionView.performBatchUpdates({ [weak self] in updates?() - self?.invalidateLayout(with: ctx) }, completion: completion) } else { updates?() - invalidateLayout(with: ctx) completion?(true) } } From 2138b3ba5d2510ce62fe1c41d7ea94bdad7653d3 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Tue, 26 Mar 2019 12:20:11 -0700 Subject: [PATCH 09/13] Small changes to try and fix crash --- .../AdvancedExampleViewController.swift | 4 +-- MessageKit.podspec~ | 27 ------------------- .../MessagesCollectionViewFlowLayout.swift | 10 ++++--- 3 files changed, 9 insertions(+), 32 deletions(-) delete mode 100644 MessageKit.podspec~ diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index d8c87507..88955154 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -183,8 +183,8 @@ final class AdvancedExampleViewController: ChatViewController { func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") - setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in - if self?.isLastSectionVisible() == true { + setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in + if success, self?.isLastSectionVisible() == true { self?.messagesCollectionView.scrollToBottom(animated: true) } } diff --git a/MessageKit.podspec~ b/MessageKit.podspec~ deleted file mode 100644 index 4a4007e6..00000000 --- a/MessageKit.podspec~ +++ /dev/null @@ -1,27 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'MessageKit' - s.version = '3.0.0' - s.license = { :type => "MIT", :file => "LICENSE.md" } - - s.summary = 'An elegant messages UI library for iOS.' - s.homepage = 'https://github.com/MessageKit/MessageKit' - s.social_media_url = 'https://twitter.com/_SD10_' - s.author = { "Steven Deutsch" => "stevensdeutsch@yahoo.com" } - - s.source = { :git => 'https://github.com/MessageKit/MessageKit.git', :tag => s.version } - s.source_files = 'Sources/**/*.swift' - - s.pod_target_xcconfig = { - "SWIFT_VERSION" => "4.2", - } - - s.swift_version = '4.2' - - s.ios.deployment_target = '9.0' - s.ios.resource_bundle = { 'MessageKitAssets' => 'Assets/MessageKitAssets.bundle/Images' } - - s.requires_arc = true - - s.dependency 'InputBarAccessoryView' - -end diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 451384e1..9162f90c 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -117,7 +117,10 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// - completion: A completion block to execute after the insertion/deletion open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { - guard isTypingIndicatorViewHidden != isHidden else { return } + guard isTypingIndicatorViewHidden != isHidden, messagesCollectionView.numberOfSections > 0 else { + completion?(false) + return + } isTypingIndicatorViewHidden = isHidden let ctx = UICollectionViewFlowLayoutInvalidationContext() @@ -125,13 +128,14 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { ofKind: MessagesCollectionView.elementKindTypingIndicator, at: [indexPathForTypingIndicatorView()] ) - invalidateLayout(with: ctx) if animated { messagesCollectionView.performBatchUpdates({ [weak self] in + self?.invalidateLayout(with: ctx) updates?() }, completion: completion) } else { + invalidateLayout(with: ctx) updates?() completion?(true) } @@ -203,7 +207,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { private func indexPathForTypingIndicatorView() -> IndexPath { let section = messagesCollectionView.numberOfSections - 2 - return IndexPath(row: 0, section: max(section, 0)) + return IndexPath(item: 0, section: max(section, 0)) } // MARK: - Layout Invalidation From 5f405f2d14bd92136715d9f754898785ea5d0340 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Tue, 26 Mar 2019 12:29:43 -0700 Subject: [PATCH 10/13] Add indexpath empty check --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 9162f90c..be28c353 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -201,6 +201,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } public func shouldDisplayTypingIndicatorView(at indexPath: IndexPath) -> Bool { + guard !indexPath.isEmpty else { return false } let isLastIndexPath = indexPath.section == messagesCollectionView.numberOfSections - 1 return isLastIndexPath && !isTypingIndicatorViewHidden } From d81430c78de44d970df8bd6a7a6b3b07da4251e0 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Mon, 1 Apr 2019 00:40:04 -0700 Subject: [PATCH 11/13] Work around for crashes some users are reporting --- .../MessagesCollectionViewFlowLayout.swift | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index be28c353..b1a1a03a 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -29,6 +29,22 @@ import AVFoundation /// framework provided `MessageCollectionViewCell` subclasses. open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { + /// There is a known issue where the layout invalidation + /// causes a fatal crash when setting the typing indicator + /// view to hidden. The cause has been isolated to + /// `UICollectionViewFlowLayoutInvalidationContext` which + /// causes an `IndexPath` with 0 indices to be passed into + /// `layoutAttributesForSupplementaryView` when accessing + /// `.section`. The current work around is to not use + /// `invalidateLayout(with: context)` for the case of + /// setting the typing indicator to hidden but rather + /// `invalidateLayout()`. This however is not efficent + /// and thus will not be the default behaviour. Instead, + /// if you experience the crash set this value to TRUE. + /// + /// The default value is FALSE + public var invalidateLayoutOnTypingIndicatorHidden: Bool = false + open override class var layoutAttributesClass: AnyClass { return MessagesCollectionViewLayoutAttributes.self } @@ -123,33 +139,18 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } isTypingIndicatorViewHidden = isHidden - let ctx = UICollectionViewFlowLayoutInvalidationContext() - ctx.invalidateSupplementaryElements( - ofKind: MessagesCollectionView.elementKindTypingIndicator, - at: [indexPathForTypingIndicatorView()] - ) - if animated { messagesCollectionView.performBatchUpdates({ [weak self] in - self?.invalidateLayout(with: ctx) + self?.invalidateLayoutForTypingIndicatorChange() updates?() }, completion: completion) } else { - invalidateLayout(with: ctx) + invalidateLayoutForTypingIndicatorChange() updates?() completion?(true) } } - private func adjustBottomInsetForTypingIndicatorView() { - guard let delegate = messagesCollectionView.messagesLayoutDelegate else { return } - let height = delegate.typingIndicatorViewSize(in: messagesCollectionView).height - let inset = delegate.typingIndicatorViewTopInset(in: messagesCollectionView) - let totalHeight = height + inset - let delta = isTypingIndicatorViewHidden ? -totalHeight : totalHeight - messagesCollectionView.contentInset.bottom += delta - } - // MARK: - Attributes open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { @@ -201,7 +202,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } public func shouldDisplayTypingIndicatorView(at indexPath: IndexPath) -> Bool { - guard !indexPath.isEmpty else { return false } + guard indexPath.count > 0 else { + fatalError("`indexPath` contained 0 indices, set `invalidateLayoutOnTypingIndicatorHidden` to `TRUE`") + } let isLastIndexPath = indexPath.section == messagesCollectionView.numberOfSections - 1 return isLastIndexPath && !isTypingIndicatorViewHidden } @@ -211,6 +214,19 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return IndexPath(item: 0, section: max(section, 0)) } + private func invalidateLayoutForTypingIndicatorChange() { + if !isTypingIndicatorViewHidden || !invalidateLayoutOnTypingIndicatorHidden { + let ctx = UICollectionViewFlowLayoutInvalidationContext() + ctx.invalidateSupplementaryElements( + ofKind: MessagesCollectionView.elementKindTypingIndicator, + at: [indexPathForTypingIndicatorView()] + ) + invalidateLayout(with: ctx) + } else { + invalidateLayout() + } + } + // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { From d19e7df997da734996ad652adcf771902dc20009 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Tue, 2 Apr 2019 22:43:09 -0700 Subject: [PATCH 12/13] Go back to using a reserved section method --- .../Layout/CustomMessageFlowLayout.swift | 6 +- .../AdvancedExampleViewController.swift | 8 +- MessageKit.xcodeproj/project.pbxproj | 29 ++-- .../Controllers/MessagesViewController.swift | 85 ++++++++++- .../MessagesCollectionViewFlowLayout.swift | 140 +++--------------- .../TypingIndicatorCellSizeCalculator.swift | 43 ++++++ Sources/Protocols/MessagesDataSource.swift | 12 ++ .../Protocols/MessagesDisplayDelegate.swift | 13 -- .../TypingIndicatorCell.swift} | 10 +- Sources/Views/MessagesCollectionView.swift | 38 +++-- 10 files changed, 201 insertions(+), 183 deletions(-) create mode 100644 Sources/Layout/TypingIndicatorCellSizeCalculator.swift rename Sources/Views/{Headers & Footers/TypingIndicatorView.swift => Cells/TypingIndicatorCell.swift} (86%) diff --git a/Example/Sources/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift index 70df0376..74b0633e 100644 --- a/Example/Sources/Layout/CustomMessageFlowLayout.swift +++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift @@ -30,9 +30,9 @@ open class CustomMessagesFlowLayout: MessagesCollectionViewFlowLayout { open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self) open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { -// if isSectionReservedForTypingBubble(indexPath.section) { -// return typingMessageSizeCalculator -// } + if isSectionReservedForTypingIndicator(indexPath.section) { + return typingIndicatorSizeCalculator + } let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) if case .custom = message.kind { return customMessageSizeCalculator diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 88955154..3eb41451 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -222,10 +222,10 @@ final class AdvancedExampleViewController: ChatViewController { fatalError("Ouch. nil data source for messages") } -// guard !isSectionReservedForTypingBubble(indexPath.section) else { -// return super.collectionView(collectionView, cellForItemAt: indexPath) -// } - + guard !isSectionReservedForTypingIndicator(indexPath.section) else { + return super.collectionView(collectionView, cellForItemAt: indexPath) + } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) if case .custom = message.kind { let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath) diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 9b277bfa..64c83376 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -35,20 +35,20 @@ 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; }; 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */; }; - 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; 383B9EB121728BAD008AB91A /* SenderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383B9EB021728BAD008AB91A /* SenderType.swift */; }; + 388119462253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */; }; 38A2230F221FB8A300D14DAF /* MessageInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A2230E221FB8A300D14DAF /* MessageInputBar.swift */; }; 38A223112223493500D14DAF /* InputBarAccessoryView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38A223102223493400D14DAF /* InputBarAccessoryView.framework */; }; - 38C2AE7C20D4878D00F8079E /* MessageInputBar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */; }; 38F8062F2173CD8F00CDB9DB /* MockUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8062D2173CD4300CDB9DB /* MockUser.swift */; }; + 38F8063221740D9E00CDB9DB /* TypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */; }; + 38F8063321740D9E00CDB9DB /* BubbleCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */; }; + 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063421740DAD00CDB9DB /* TypingBubble.swift */; }; + 38F8063721740DD500CDB9DB /* TypingIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */; }; + 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; 5073C1152175BE750040EAD5 /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1142175BE750040EAD5 /* AudioItem.swift */; }; 5073C1192175BE960040EAD5 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */; }; 5073C11D2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */; }; 5073C1232175C1980040EAD5 /* sound1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5073C1222175C1980040EAD5 /* sound1.m4a */; }; - 38F8063221740D9E00CDB9DB /* TypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */; }; - 38F8063321740D9E00CDB9DB /* BubbleCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */; }; - 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063421740DAD00CDB9DB /* TypingBubble.swift */; }; - 38F8063721740DD500CDB9DB /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063621740DD500CDB9DB /* TypingIndicatorView.swift */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */; }; 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC851F87AB230030B058 /* AvatarViewTests.swift */; }; @@ -144,19 +144,20 @@ 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalEdgeInsets.swift; sourceTree = ""; }; 383B9EB021728BAD008AB91A /* SenderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderType.swift; sourceTree = ""; }; + 388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCellSizeCalculator.swift; sourceTree = ""; }; 38A2230E221FB8A300D14DAF /* MessageInputBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = ""; }; 38A223102223493400D14DAF /* InputBarAccessoryView.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputBarAccessoryView.framework; path = Carthage/Build/iOS/InputBarAccessoryView.framework; sourceTree = ""; }; 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; sourceTree = ""; }; - 4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = ""; }; 38F8062D2173CD4300CDB9DB /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = ""; }; + 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = ""; }; + 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleCircle.swift; sourceTree = ""; }; + 38F8063421740DAD00CDB9DB /* TypingBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingBubble.swift; sourceTree = ""; }; + 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = ""; }; + 4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = ""; }; 5073C1142175BE750040EAD5 /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioItem.swift; sourceTree = ""; }; 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = ""; }; 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageSizeCalculator.swift; sourceTree = ""; }; 5073C1222175C1980040EAD5 /* sound1.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = sound1.m4a; sourceTree = ""; }; - 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = ""; }; - 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleCircle.swift; sourceTree = ""; }; - 38F8063421740DAD00CDB9DB /* TypingBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingBubble.swift; sourceTree = ""; }; - 38F8063621740DD500CDB9DB /* TypingIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatterTests.swift; sourceTree = ""; }; @@ -249,6 +250,7 @@ B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */, 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */, B7A03F361F866946006AEF79 /* TextMessageCell.swift */, + 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */, ); path = Cells; sourceTree = ""; @@ -256,7 +258,6 @@ 2EB618F11F846899007FBA0E /* Headers & Footers */ = { isa = PBXGroup; children = ( - 38F8063621740DD500CDB9DB /* TypingIndicatorView.swift */, 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); path = "Headers & Footers"; @@ -475,6 +476,7 @@ 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */, 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */, 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */, + 388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */, ); path = Layout; sourceTree = ""; @@ -630,6 +632,7 @@ 5073C11D2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */, + 388119462253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, @@ -640,7 +643,7 @@ B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, - 38F8063721740DD500CDB9DB /* TypingIndicatorView.swift in Sources */, + 38F8063721740DD500CDB9DB /* TypingIndicatorCell.swift in Sources */, 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */, B7A03F2E1F866895006AEF79 /* MessageKind.swift in Sources */, B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 2851d529..f9336f19 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -70,6 +70,10 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } } + public var isTypingIndicatorHidden: Bool { + return messagesCollectionView.isTypingIndicatorHidden + } + public var selectedIndexPathForMenu: IndexPath? private var isFirstLayout: Bool = true @@ -183,7 +187,46 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { /// when `animated` is `FALSE` /// - completion: A completion block to execute after the insertion/deletion open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { - messagesCollectionView.setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates, completion: completion) + + guard isTypingIndicatorHidden != isHidden else { + completion?(false) + return + } + + let section = messagesCollectionView.numberOfSections + messagesCollectionView.setTypingIndicatorViewHidden(isHidden) + + if animated { + messagesCollectionView.performBatchUpdates({ [weak self] in + self?.performUpdatesForTypingIndicatorVisability(at: section) + updates?() + }, completion: completion) + } else { + performUpdatesForTypingIndicatorVisability(at: section) + updates?() + completion?(true) + } + } + + /// Performs a delete or insert on the `MessagesCollectionView` on the provided section + /// + /// - Parameter section: The index to modify + private func performUpdatesForTypingIndicatorVisability(at section: Int) { + if isTypingIndicatorHidden { + messagesCollectionView.deleteSections([section - 1]) + } else { + messagesCollectionView.insertSections([section]) + } + } + + /// A method that by default checks if the section is the last in the + /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` + /// is FALSE + /// + /// - Parameter section + /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section + public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { + return !messagesCollectionView.isTypingIndicatorHidden && section == self.numberOfSections(in: messagesCollectionView) - 1 } // MARK: - UICollectionViewDataSource @@ -192,18 +235,27 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { guard let collectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } - return collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 + let sections = collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 + return collectionView.isTypingIndicatorHidden ? sections : sections + 1 } open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard let collectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } + if isSectionReservedForTypingIndicator(section) { + return 1 + } return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 } - /// Note: - /// If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) for MessageKind.custom messages, if necessary + /// Notes: + /// - If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) + /// for MessageKind.custom messages, if necessary. + /// + /// - If you are using the typing indicator you will need to ensure that the section is not + /// reserved for it with `isSectionReservedForTypingIndicator` defined in + /// `MessagesCollectionViewFlowLayout` open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { @@ -214,6 +266,10 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { fatalError(MessageKitError.nilMessagesDataSource) } + if isSectionReservedForTypingIndicator(indexPath.section) { + return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView) + } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.kind { @@ -253,8 +309,6 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) case UICollectionView.elementKindSectionFooter: return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) - case MessagesCollectionView.elementKindTypingIndicator: - return displayDelegate.typingIndicatorView(for: indexPath, in: messagesCollectionView) default: fatalError(MessageKitError.unrecognizedSectionKind) } @@ -275,9 +329,17 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) } + if isSectionReservedForTypingIndicator(section) { + return .zero + } return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView) } + open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let cell = cell as? TypingIndicatorCell else { return } + cell.typingBubble.startAnimating() + } + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) @@ -285,11 +347,19 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) } + if isSectionReservedForTypingIndicator(section) { + return .zero + } return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } + + if isSectionReservedForTypingIndicator(indexPath.section) { + return false + } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.kind { @@ -302,6 +372,9 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { + if isSectionReservedForTypingIndicator(indexPath.section) { + return false + } return (action == NSSelectorFromString("copy:")) } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index b1a1a03a..fc35fc12 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -29,22 +29,6 @@ import AVFoundation /// framework provided `MessageCollectionViewCell` subclasses. open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { - /// There is a known issue where the layout invalidation - /// causes a fatal crash when setting the typing indicator - /// view to hidden. The cause has been isolated to - /// `UICollectionViewFlowLayoutInvalidationContext` which - /// causes an `IndexPath` with 0 indices to be passed into - /// `layoutAttributesForSupplementaryView` when accessing - /// `.section`. The current work around is to not use - /// `invalidateLayout(with: context)` for the case of - /// setting the typing indicator to hidden but rather - /// `invalidateLayout()`. This however is not efficent - /// and thus will not be the default behaviour. Instead, - /// if you experience the crash set this value to TRUE. - /// - /// The default value is FALSE - public var invalidateLayoutOnTypingIndicatorHidden: Bool = false - open override class var layoutAttributesClass: AnyClass { return MessagesCollectionViewLayoutAttributes.self } @@ -78,18 +62,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return collectionView.frame.width - sectionInset.left - sectionInset.right } - open override var collectionViewContentSize: CGSize { - let size = super.collectionViewContentSize - - guard !isTypingIndicatorViewHidden, let delegate = messagesCollectionView.messagesLayoutDelegate else { return size } - let typingIndicatorSize = delegate.typingIndicatorViewSize(in: messagesCollectionView) - let inset = delegate.typingIndicatorViewTopInset(in: messagesCollectionView) + 5 - return CGSize( - width: size.width, - height: size.height + typingIndicatorSize.height + inset - ) - } - public private(set) var isTypingIndicatorViewHidden: Bool = true // MARK: - Initializers @@ -122,45 +94,31 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Typing Indicator API - /// Sets the typing indicator sate by inserting/deleting the `TypingIndicatorView` + /// Notifies the layout that the typing indicator will change state /// /// - Parameters: /// - isHidden: A Boolean value that is to be the new state of the typing indicator - /// - animated: A Boolean value determining if the insertion is to be animated - /// - updates: A block of code that will be executed during `performBatchUpdates` - /// when `animated` is `TRUE` or before the `completion` block executes - /// when `animated` is `FALSE` - /// - completion: A completion block to execute after the insertion/deletion - open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { - - guard isTypingIndicatorViewHidden != isHidden, messagesCollectionView.numberOfSections > 0 else { - completion?(false) - return - } + open func setTypingIndicatorViewHidden(_ isHidden: Bool) { isTypingIndicatorViewHidden = isHidden + } - if animated { - messagesCollectionView.performBatchUpdates({ [weak self] in - self?.invalidateLayoutForTypingIndicatorChange() - updates?() - }, completion: completion) - } else { - invalidateLayoutForTypingIndicatorChange() - updates?() - completion?(true) - } + /// A method that by default checks if the section is the last in the + /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` + /// is FALSE + /// + /// - Parameter section + /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section + open func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { + return !isTypingIndicatorViewHidden && section == messagesCollectionView.numberOfSections - 1 } // MARK: - Attributes open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard var attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { + guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } for attributes in attributesArray where attributes.representedElementCategory == .cell { - if let supplementaryAttributes = layoutAttributesForSupplementaryView(ofKind: MessagesCollectionView.elementKindTypingIndicator, at: attributes.indexPath) as? MessagesCollectionViewLayoutAttributes { - attributesArray.append(supplementaryAttributes) - } let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) cellSizeCalculator.configure(attributes: attributes) } @@ -178,55 +136,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return attributes } - open override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - switch elementKind { - case MessagesCollectionView.elementKindTypingIndicator: - - guard shouldDisplayTypingIndicatorView(at: indexPath) else { return nil } - guard let delegate = messagesCollectionView.messagesLayoutDelegate else { return nil } - let size = delegate.typingIndicatorViewSize(in: messagesCollectionView) - guard size != .zero else { return nil } - let inset = delegate.typingIndicatorViewTopInset(in: messagesCollectionView) - let attributes = MessagesCollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath) - - if let itemAttributes = layoutAttributesForItem(at: indexPath) { - attributes.frame = CGRect(x: itemAttributes.frame.origin.x, - y: itemAttributes.frame.maxY + inset, - width: size.width, - height: size.height) - } - return attributes - default: - return super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) - } - } - - public func shouldDisplayTypingIndicatorView(at indexPath: IndexPath) -> Bool { - guard indexPath.count > 0 else { - fatalError("`indexPath` contained 0 indices, set `invalidateLayoutOnTypingIndicatorHidden` to `TRUE`") - } - let isLastIndexPath = indexPath.section == messagesCollectionView.numberOfSections - 1 - return isLastIndexPath && !isTypingIndicatorViewHidden - } - - private func indexPathForTypingIndicatorView() -> IndexPath { - let section = messagesCollectionView.numberOfSections - 2 - return IndexPath(item: 0, section: max(section, 0)) - } - - private func invalidateLayoutForTypingIndicatorChange() { - if !isTypingIndicatorViewHidden || !invalidateLayoutOnTypingIndicatorHidden { - let ctx = UICollectionViewFlowLayoutInvalidationContext() - ctx.invalidateSupplementaryElements( - ofKind: MessagesCollectionView.elementKindTypingIndicator, - at: [indexPathForTypingIndicatorView()] - ) - invalidateLayout(with: ctx) - } else { - invalidateLayout() - } - } - // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { @@ -245,20 +154,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { invalidateLayout() } - open override func indexPathsToInsertForSupplementaryView(ofKind elementKind: String) -> [IndexPath] { - guard elementKind == MessagesCollectionView.elementKindTypingIndicator else { - return super.indexPathsToInsertForSupplementaryView(ofKind: elementKind) - } - return [indexPathForTypingIndicatorView()] - } - - open override func indexPathsToDeleteForSupplementaryView(ofKind elementKind: String) -> [IndexPath] { - guard elementKind == MessagesCollectionView.elementKindTypingIndicator else { - return super.indexPathsToDeleteForSupplementaryView(ofKind: elementKind) - } - return [indexPathForTypingIndicatorView()] - } - // MARK: - Cell Sizing lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) @@ -272,10 +167,17 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) lazy open var audioMessageSizeCalculator = AudioMessageSizeCalculator(layout: self) + lazy open var typingIndicatorSizeCalculator = TypingCellSizeCalculator(layout: self) - /// - Note: - /// If you override this method, remember to call MessageLayoutDelegate's customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary + /// Note: + /// - If you override this method, remember to call MessageLayoutDelegate's + /// customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary + /// - If you are using the typing indicator be sure to return the `typingIndicatorSizeCalculator` + /// when the section is reserved for it, indicated by `isSectionReservedForTypingIndicator` open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { + if isSectionReservedForTypingIndicator(indexPath.section) { + return typingIndicatorSizeCalculator + } let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.kind { case .text: diff --git a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift new file mode 100644 index 00000000..3e77663f --- /dev/null +++ b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift @@ -0,0 +1,43 @@ +/* + MIT License + + Copyright (c) 2017-2019 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 TypingCellSizeCalculator: CellSizeCalculator { + + open var height: CGFloat = 62 + + public init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + self.layout = layout + } + + open override func sizeForItem(at indexPath: IndexPath) -> CGSize { + guard let layout = layout else { return .zero } + let collectionViewWidth = layout.collectionView?.bounds.width ?? 0 + let contentInset = layout.collectionView?.contentInset ?? .zero + let inset = layout.sectionInset.horizontal + contentInset.horizontal + return CGSize(width: collectionViewWidth - inset, height: height) + } +} diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index a725459d..40e7edc0 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -113,6 +113,14 @@ public protocol MessagesDataSource: AnyObject { /// - Note: /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages. func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell + + /// Typing indicator cell used when the indicator is set to be shown + /// + /// - Parameters: + /// - indexPath: The index path to dequeue the cell at + /// - messagesCollectionView: The `MessagesCollectionView` the cell is to be rendered in + /// - Returns: A `UICollectionViewCell` that indicates a user is typing + func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell } public extension MessagesDataSource { @@ -144,4 +152,8 @@ public extension MessagesDataSource { func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { fatalError(MessageKitError.customDataUnresolvedCell) } + + func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + return messagesCollectionView.dequeueReusableCell(TypingIndicatorCell.self, for: indexPath) + } } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 5cdcb2f4..29d43e80 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -71,13 +71,6 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView - - /// The section footer to use for a given `IndexPath`. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the footer. - /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. - func typingIndicatorView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class. /// @@ -248,12 +241,6 @@ public extension MessagesDisplayDelegate { func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) } - - func typingIndicatorView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { - let view = messagesCollectionView.dequeueReusableTypingIndicatorView(TypingIndicatorView.self, for: indexPath) - view.typingBubble.startAnimating() - return view - } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { avatarView.initials = "?" diff --git a/Sources/Views/Headers & Footers/TypingIndicatorView.swift b/Sources/Views/Cells/TypingIndicatorCell.swift similarity index 86% rename from Sources/Views/Headers & Footers/TypingIndicatorView.swift rename to Sources/Views/Cells/TypingIndicatorCell.swift index a6a4c3bd..7e8c5d94 100644 --- a/Sources/Views/Headers & Footers/TypingIndicatorView.swift +++ b/Sources/Views/Cells/TypingIndicatorCell.swift @@ -24,10 +24,12 @@ import UIKit -/// A subclass of `MessageReusableView` used to display the typing indicator. -open class TypingIndicatorView: MessageReusableView { +/// A subclass of `MessageCollectionViewCell` used to display the typing indicator. +open class TypingIndicatorCell: MessageCollectionViewCell { // MARK: - Subviews + + public var insets = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0) public let typingBubble = TypingBubble() @@ -44,7 +46,6 @@ open class TypingIndicatorView: MessageReusableView { } open func setupSubviews() { - autoresizingMask = [.flexibleWidth, .flexibleHeight] addSubview(typingBubble) } @@ -52,7 +53,6 @@ open class TypingIndicatorView: MessageReusableView { super.prepareForReuse() if typingBubble.isAnimating { typingBubble.stopAnimating() - typingBubble.startAnimating() } } @@ -60,7 +60,7 @@ open class TypingIndicatorView: MessageReusableView { open override func layoutSubviews() { super.layoutSubviews() - typingBubble.frame = bounds + typingBubble.frame = bounds.inset(by: insets) } } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 3cb68247..bb68687d 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -28,8 +28,6 @@ open class MessagesCollectionView: UICollectionView { // MARK: - Properties - public static var elementKindTypingIndicator = "TypingIndicatorElementKind" - open weak var messagesDataSource: MessagesDataSource? open weak var messagesDisplayDelegate: MessagesDisplayDelegate? @@ -38,6 +36,10 @@ open class MessagesCollectionView: UICollectionView { open weak var messageCellDelegate: MessageCellDelegate? + open var isTypingIndicatorHidden: Bool { + return messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden + } + private var indexPathForLastItem: IndexPath? { let lastSection = numberOfSections - 1 guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil } @@ -75,9 +77,9 @@ open class MessagesCollectionView: UICollectionView { register(MediaMessageCell.self) register(LocationMessageCell.self) register(AudioMessageCell.self) + register(TypingIndicatorCell.self) register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter) - register(TypingIndicatorView.self, forSupplementaryViewOfKind: MessagesCollectionView.elementKindTypingIndicator) } private func setupGestureRecognizers() { @@ -124,17 +126,22 @@ open class MessagesCollectionView: UICollectionView { // MARK: - Typing Indicator API - /// Sets the typing indicator sate by inserting/deleting the `TypingIndicatorView` + /// Notifies the layout that the typing indicator will change state /// /// - Parameters: /// - isHidden: A Boolean value that is to be the new state of the typing indicator - /// - animated: A Boolean value determining if the insertion is to be animated - /// - updates: A block of code that will be executed during `performBatchUpdates` - /// when `animated` is `TRUE` or before the `completion` block executes - /// when `animated` is `FALSE` - /// - completion: A completion block to execute after the insertion/deletion - open func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil) { - messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates, completion: completion) + open func setTypingIndicatorViewHidden(_ isHidden: Bool) { + messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden) + } + + /// A method that by default checks if the section is the last in the + /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` + /// is FALSE + /// + /// - Parameter section + /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section + public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { + return messagesCollectionViewFlowLayout.isSectionReservedForTypingIndicator(section) } // MARK: View Register/Dequeue @@ -184,13 +191,4 @@ open class MessagesCollectionView: UICollectionView { return viewType } - /// Generically dequeues a typing indicator of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableTypingIndicatorView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: MessagesCollectionView.elementKindTypingIndicator, 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 - } - } From 601c393969700418c58d2779515ac1a748f4b8b6 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Wed, 3 Apr 2019 20:09:13 -0700 Subject: [PATCH 13/13] Better access control --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 2 +- Sources/Views/MessagesCollectionView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index fc35fc12..7d8c2d2b 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -98,7 +98,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// /// - Parameters: /// - isHidden: A Boolean value that is to be the new state of the typing indicator - open func setTypingIndicatorViewHidden(_ isHidden: Bool) { + internal func setTypingIndicatorViewHidden(_ isHidden: Bool) { isTypingIndicatorViewHidden = isHidden } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index bb68687d..07dbdcac 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -130,7 +130,7 @@ open class MessagesCollectionView: UICollectionView { /// /// - Parameters: /// - isHidden: A Boolean value that is to be the new state of the typing indicator - open func setTypingIndicatorViewHidden(_ isHidden: Bool) { + internal func setTypingIndicatorViewHidden(_ isHidden: Bool) { messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden) }