From 7a1209ecbe860d82d13a37224a0c562ee7848663 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 7 Mar 2018 22:08:59 -0600 Subject: [PATCH 01/70] Remove open methods from extensions --- MessageKit.xcodeproj/project.pbxproj | 16 - ...sCollectionViewFlowLayout+CellSizing.swift | 100 ---- ...sagesCollectionViewFlowLayout+Labels.swift | 279 ----------- ...nViewFlowLayout+MessageContainerView.swift | 158 ------ .../MessagesCollectionViewFlowLayout.swift | 470 ++++++++++++++++-- 5 files changed, 415 insertions(+), 608 deletions(-) delete mode 100644 Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift delete mode 100644 Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift delete mode 100644 Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 6c9f1752..a985a3f5 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -18,11 +18,7 @@ 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; }; 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; - 1FAC3488201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */; }; - 1FAC348A201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */; }; - 1FAC348C201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */; }; 1FAC3490201E50DF000A1504 /* MessageCellLayoutContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */; }; - 1FAC3492201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; @@ -123,11 +119,7 @@ 1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = ""; }; 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; - 1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+Labels.swift"; sourceTree = ""; }; - 1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+Avatar.swift"; sourceTree = ""; }; - 1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+MessageContainerView.swift"; sourceTree = ""; }; 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayoutContext.swift; sourceTree = ""; }; - 1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+CellSizing.swift"; sourceTree = ""; }; 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; @@ -446,10 +438,6 @@ isa = PBXGroup; children = ( B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, - 1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */, - 1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */, - 1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */, - 1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */, B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */, ); @@ -599,7 +587,6 @@ 38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */, B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */, - 1FAC348A201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift in Sources */, B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, @@ -609,7 +596,6 @@ B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, 1FAC3490201E50DF000A1504 /* MessageCellLayoutContext.swift in Sources */, B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */, - 1FAC3488201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift in Sources */, B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, @@ -622,7 +608,6 @@ B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */, B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */, B7A03F261F866895006AEF79 /* MessageKitDateFormatter.swift in Sources */, - 1FAC3492201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift in Sources */, 8962AC9A1F87ABB20030B058 /* MockMessagesDataSource.swift in Sources */, B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */, B7A03F6A1F8669EB006AEF79 /* UICollectionView+Extensions.swift in Sources */, @@ -647,7 +632,6 @@ 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, B7A03F271F866895006AEF79 /* Avatar.swift in Sources */, - 1FAC348C201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift in Sources */, 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */, 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */, ); diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift deleted file mode 100644 index 63c0f35a..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - 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 Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - /// Returns the height of a `MessageCollectionViewCell`'s content at a given `IndexPath` - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarPosition = _avatarPosition(for: message, at: indexPath) - let avatarHeight = _avatarSize(for: message, at: indexPath).height - let messageContainerHeight = _messageContainerSize(for: message, at: indexPath).height - let bottomLabelHeight = _cellBottomLabelSize(for: message, at: indexPath).height - let topLabelHeight = _cellTopLabelSize(for: message, at: indexPath).height - let messageVerticalPadding = _messageContainerPadding(for: message, at: indexPath).vertical - - var cellHeight: CGFloat = 0 - - switch avatarPosition.vertical { - case .cellTop: - cellHeight += max(avatarHeight, topLabelHeight) - cellHeight += bottomLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .cellBottom: - cellHeight += max(avatarHeight, bottomLabelHeight) - cellHeight += topLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .messageTop, .messageCenter, .messageBottom: - cellHeight += max(avatarHeight, messageContainerHeight) - cellHeight += messageVerticalPadding - cellHeight += topLabelHeight - cellHeight += bottomLabelHeight - } - - return cellHeight - } - - /// Returns the size for the `MessageCollectionViewCell` at a given `IndexPath` - /// considering all of the cell's content. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the cell. - open func sizeForItem(at indexPath: IndexPath) -> CGSize { - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let context = cellLayoutContext(for: message, at: indexPath) - guard let itemHeight = context.itemHeight else { - fatalError("Unexpectedly received a nil itemHeight") - } - return CGSize(width: itemWidth, height: itemHeight) - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - internal func _cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedHeight = currentLayoutContext.itemHeight else { - let height = cellContentHeight(for: message, at: indexPath) - currentLayoutContext.itemHeight = height - return height - } - return cachedHeight - } - - internal func _sizeForItem(at indexPath: IndexPath) -> CGSize { - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let itemHeight = cellContentHeight(for: message, at: indexPath) - return CGSize(width: itemWidth, height: itemHeight) - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift deleted file mode 100644 index 04f437d5..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift +++ /dev/null @@ -1,279 +0,0 @@ -/* - 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 Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - // Top Label - - /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `cellTopLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. - open func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - return messagesLayoutDelegate.cellTopLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the size of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method sizes the label to fit. - open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) - guard let topLabelText = text else { return .zero } - let maxWidth = _cellTopLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: topLabelText, considering: maxWidth) - } - - /// Returns the maximum width of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let labelAlignment = _cellTopLabelAlignment(for: message, at: indexPath) - let avatarPosition = _avatarPosition(for: message, at: indexPath) - let avatarWidth = _avatarSize(for: message, at: indexPath).width - let messageContainerSize = _messageContainerSize(for: message, at: indexPath) - let messagePadding = _messageContainerPadding(for: message, at: indexPath) - - let avatarHorizontal = avatarPosition.horizontal - let avatarVertical = avatarPosition.vertical - - switch (labelAlignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = messageContainerSize.width + messagePadding.left - labelAlignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = messageContainerSize.width + messagePadding.right - labelAlignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - labelAlignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - labelAlignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - - // Bottom Label - - /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `cellBottomLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. - open func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - return messagesLayoutDelegate.cellBottomLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the size of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method sizes the label to fit. - open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - guard let bottomLabelText = text else { return .zero } - let maxWidth = _cellBottomLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: bottomLabelText, considering: maxWidth) - } - - /// Returns the maximum width of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - let labelAlignment = _cellBottomLabelAlignment(for: message, at: indexPath) - let avatarWidth = _avatarSize(for: message, at: indexPath).width - let messageContainerSize = _messageContainerSize(for: message, at: indexPath) - let messagePadding = _messageContainerPadding(for: message, at: indexPath) - let avatarPosition = _avatarPosition(for: message, at: indexPath) - - let avatarHorizontal = avatarPosition.horizontal - let avatarVertical = avatarPosition.vertical - - switch (labelAlignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = messageContainerSize.width + messagePadding.left - labelAlignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = messageContainerSize.width + messagePadding.right - labelAlignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - labelAlignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - labelAlignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - // Top Label - - internal func _cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - guard let cachedAlignment = currentLayoutContext.topLabelAlignment else { - let alignment = cellTopLabelAlignment(for: message, at: indexPath) - currentLayoutContext.topLabelAlignment = alignment - return alignment - } - return cachedAlignment - } - - internal func _cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedMaxWidth = currentLayoutContext.topLabelMaxWidth else { - let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.topLabelMaxWidth = maxWidth - return maxWidth - } - return cachedMaxWidth - } - - internal func _cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.topLabelSize else { - let size = cellTopLabelSize(for: message, at: indexPath) - currentLayoutContext.topLabelSize = size - return size - } - return cachedSize - } - - // Bottom Label - - internal func _cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - guard let cachedAlignment = currentLayoutContext.bottomLabelAlignment else { - let alignment = cellBottomLabelAlignment(for: message, at: indexPath) - currentLayoutContext.bottomLabelAlignment = alignment - return alignment - } - return cachedAlignment - } - - internal func _cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedMaxWidth = currentLayoutContext.bottomLabelMaxWidth else { - let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.bottomLabelMaxWidth = maxWidth - return maxWidth - } - return cachedMaxWidth - } - - internal func _cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.bottomLabelSize else { - let size = cellBottomLabelSize(for: message, at: indexPath) - currentLayoutContext.bottomLabelSize = size - return size - } - return cachedSize - } -} - -// MARK: - Helpers - -extension MessagesCollectionViewFlowLayout { - - /// Returns the size required fit a NSAttributedString considering a constrained max width. - /// - /// - Parameters: - /// - attributedText: The `NSAttributedString` used to calculate a size that fits. - /// - maxWidth: The max width available for the label. - internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { - - let estimatedHeight = attributedText.height(considering: maxWidth) - let estimatedWidth = attributedText.width(considering: estimatedHeight) - - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - - return CGSize(width: finalWidth, height: finalHeight) - } - - /// Returns the size required to fit a String considering a constrained max width. - /// - /// - Parameters: - /// - text: The `String` used to calculate a size that fits. - /// - maxWidth: The max width available for the label. - internal func labelSize(for text: String, considering maxWidth: CGFloat, and font: UIFont) -> CGSize { - - let estimatedHeight = text.height(considering: maxWidth, and: font) - let estimatedWidth = text.width(considering: estimatedHeight, and: font) - - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - - return CGSize(width: finalWidth, height: finalHeight) - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift deleted file mode 100644 index 33125f25..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift +++ /dev/null @@ -1,158 +0,0 @@ -/* - 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 Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - /// Returns the insets of the `MessageLabel` in a `TextMessageCell` for - /// the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `messageLabelInset(for:at:in)` in `MessagesLayoutDelegate`. - open func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - return messagesLayoutDelegate.messageLabelInset(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the padding around the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `messagePadding(for:at:in)` in `MessagesLayoutDelegate`. - open func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - return messagesLayoutDelegate.messagePadding(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the maximum width of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarWidth = _avatarSize(for: message, at: indexPath).width - let messagePadding = _messageContainerPadding(for: message, at: indexPath) - - switch message.data { - case .text, .attributedText: - let messageInsets = _messageLabelInsets(for: message, at: indexPath) - return itemWidth - avatarWidth - messagePadding.horizontal - messageInsets.horizontal - default: - return itemWidth - avatarWidth - messagePadding.horizontal - } - } - - /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let maxWidth = _messageContainerMaxWidth(for: message, at: indexPath) - - var messageContainerSize: CGSize = .zero - - switch message.data { - case .text(let text): - let messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth, and: messageLabelFont) - messageContainerSize.width += messageLabelInsets.horizontal - messageContainerSize.height += messageLabelInsets.vertical - case .attributedText(let text): - let messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth) - messageContainerSize.width += messageLabelInsets.horizontal - messageContainerSize.height += messageLabelInsets.vertical - case .emoji(let text): - let messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth, and: emojiLabelFont) - messageContainerSize.width += messageLabelInsets.horizontal - messageContainerSize.height += messageLabelInsets.vertical - case .photo, .video: - let width = messagesLayoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .location: - let width = messagesLayoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .custom: - fatalError(MessageKitError.customDataUnresolvedSize) - } - - return messageContainerSize - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - internal func _messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let cachedInsets = currentLayoutContext.messageLabelInsets else { - let insets = messageLabelInsets(for: message, at: indexPath) - currentLayoutContext.messageLabelInsets = insets - return insets - } - return cachedInsets - } - - internal func _messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let cachedPadding = currentLayoutContext.messageContainerPadding else { - let padding = messageContainerPadding(for: message, at: indexPath) - currentLayoutContext.messageContainerPadding = padding - return padding - } - return cachedPadding - } - - internal func _messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedMaxWidth = currentLayoutContext.messageContainerMaxWidth else { - let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - currentLayoutContext.messageContainerMaxWidth = maxWidth - return maxWidth - } - return cachedMaxWidth - } - - internal func _messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.messageContainerSize else { - let size = messageContainerSize(for: message, at: indexPath) - currentLayoutContext.messageContainerSize = size - return size - } - return cachedSize - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index ba50efed..44fb87da 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -92,6 +92,376 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { deinit { NotificationCenter.default.removeObserver(self) } + + // MARK: - Open + + // MARK: - Attributes + + open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + + guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } + + attributesArray.forEach { attributes in + if attributes.representedElementCategory == UICollectionElementCategory.cell { + configure(attributes: attributes) + } + } + + return attributesArray + } + + open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + + guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } + + if attributes.representedElementCategory == UICollectionElementCategory.cell { + configure(attributes: attributes) + } + + return attributes + + } + + // MARK: - Layout Invalidation + + open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if collectionView?.bounds.width != newBounds.width { + removeAllCachedAttributes() + return true + } else { + return false + } + } + + open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { + let context = super.invalidationContext(forBoundsChange: newBounds) + guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } + flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) + return flowLayoutContext + } + + // MARK: - Avatar Size + + /// Returns the `AvatarPosition` for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// + /// - Note: The default implementation of this method retrieves its value from + /// `avatarPosition(for:at:in)` in `MessagesLayoutDelegate`. + open func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { + var position = messagesLayoutDelegate.avatarPosition(for: message, at: indexPath, in: messagesCollectionView) + + switch position.horizontal { + case .cellTrailing, .cellLeading: + break + case .natural: + position.horizontal = messagesDataSource.isFromCurrentSender(message: message) ? .cellTrailing : .cellLeading + } + return position + } + + /// Returns the `AvatarSize` for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// - Note: The default implementation of this method retrieves its value from + /// `avatarSize(for:at:in)` in `MessagesLayoutDelegate`. + open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + return messagesLayoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView) + } + + // MARK: - Cell Top Label Size + + /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s top label + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// - Note: The default implementation of this method retrieves its value from + /// `cellTopLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. + open func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + return messagesLayoutDelegate.cellTopLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) + } + + /// Returns the size of the `MessageCollectionViewCell`'s top label + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// + /// - Note: The default implementation of this method sizes the label to fit. + open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) + guard let topLabelText = text else { return .zero } + let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) + return labelSize(for: topLabelText, considering: maxWidth) + } + + /// Returns the maximum width of the `MessageCollectionViewCell`'s top label + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + open func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let alignment = cellTopLabelAlignment(for: message, at: indexPath) + let position = avatarPosition(for: message, at: indexPath) + let avatarWidth = avatarSize(for: message, at: indexPath).width + let containerSize = messageContainerSize(for: message, at: indexPath) + let messagePadding = messageContainerPadding(for: message, at: indexPath) + + let avatarHorizontal = position.horizontal + let avatarVertical = position.vertical + + switch (alignment, avatarHorizontal) { + + case (.cellLeading, _), (.cellTrailing, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellTop ? width : width - avatarWidth + + case (.cellCenter, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) + + case (.messageTrailing, .cellLeading): + let width = containerSize.width + messagePadding.left - alignment.insets.horizontal + return avatarVertical == .cellTop ? width : width + avatarWidth + + case (.messageLeading, .cellTrailing): + let width = containerSize.width + messagePadding.right - alignment.insets.horizontal + return avatarVertical == .cellTop ? width : width + avatarWidth + + case (.messageLeading, .cellLeading): + return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal + + case (.messageTrailing, .cellTrailing): + return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal + + case (_, .natural): + fatalError(MessageKitError.avatarPositionUnresolved) + } + } + + // MARK: - Cell Bottom Label Size + + /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s bottom label + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// - Note: The default implementation of this method retrieves its value from + /// `cellBottomLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. + open func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + return messagesLayoutDelegate.cellBottomLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) + } + + /// Returns the size of the `MessageCollectionViewCell`'s bottom label + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// + /// - Note: The default implementation of this method sizes the label to fit. + open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + guard let bottomLabelText = text else { return .zero } + let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) + return labelSize(for: bottomLabelText, considering: maxWidth) + } + + /// Returns the maximum width of the `MessageCollectionViewCell`'s bottom label + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + open func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + + let alignment = cellBottomLabelAlignment(for: message, at: indexPath) + let avatarWidth = avatarSize(for: message, at: indexPath).width + let containerSize = messageContainerSize(for: message, at: indexPath) + let messagePadding = messageContainerPadding(for: message, at: indexPath) + let position = avatarPosition(for: message, at: indexPath) + + let avatarHorizontal = position.horizontal + let avatarVertical = position.vertical + + switch (alignment, avatarHorizontal) { + + case (.cellLeading, _), (.cellTrailing, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellBottom ? width : width - avatarWidth + + case (.cellCenter, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) + + case (.messageTrailing, .cellLeading): + let width = containerSize.width + messagePadding.left - alignment.insets.horizontal + return avatarVertical == .cellBottom ? width : width + avatarWidth + + case (.messageLeading, .cellTrailing): + let width = containerSize.width + messagePadding.right - alignment.insets.horizontal + return avatarVertical == .cellBottom ? width : width + avatarWidth + + case (.messageLeading, .cellLeading): + return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal + + case (.messageTrailing, .cellTrailing): + return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal + + case (_, .natural): + fatalError(MessageKitError.avatarPositionUnresolved) + } + } + + // MARK: - Message Container Size + + /// Returns the insets of the `MessageLabel` in a `TextMessageCell` for + /// the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// + /// - Note: The default implementation of this method retrieves its value from + /// `messageLabelInset(for:at:in)` in `MessagesLayoutDelegate`. + open func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + return messagesLayoutDelegate.messageLabelInset(for: message, at: indexPath, in: messagesCollectionView) + } + + /// Returns the padding around the `MessageContainerView` in a `MessageCollectionViewCell` + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + /// + /// - Note: The default implementation of this method retrieves its value from + /// `messagePadding(for:at:in)` in `MessagesLayoutDelegate`. + open func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + return messagesLayoutDelegate.messagePadding(for: message, at: indexPath, in: messagesCollectionView) + } + + /// Returns the maximum width of the `MessageContainerView` in a `MessageCollectionViewCell` + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let avatarWidth = avatarSize(for: message, at: indexPath).width + let messagePadding = messageContainerPadding(for: message, at: indexPath) + + switch message.data { + case .text, .attributedText: + let messageInsets = messageLabelInsets(for: message, at: indexPath) + return itemWidth - avatarWidth - messagePadding.horizontal - messageInsets.horizontal + default: + return itemWidth - avatarWidth - messagePadding.horizontal + } + } + + /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` + /// for the `MessageType` at a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + open func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + + var messageContainerSize: CGSize = .zero + + switch message.data { + case .text(let text): + let messageInsets = messageLabelInsets(for: message, at: indexPath) + messageContainerSize = labelSize(for: text, considering: maxWidth, and: messageLabelFont) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + case .attributedText(let text): + let messageInsets = messageLabelInsets(for: message, at: indexPath) + messageContainerSize = labelSize(for: text, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + case .emoji(let text): + let messageInsets = messageLabelInsets(for: message, at: indexPath) + messageContainerSize = labelSize(for: text, considering: maxWidth, and: emojiLabelFont) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + case .photo, .video: + let width = messagesLayoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + let height = messagesLayoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + messageContainerSize = CGSize(width: width, height: height) + case .location: + let width = messagesLayoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + messageContainerSize = CGSize(width: width, height: height) + case .custom: + fatalError(MessageKitError.customDataUnresolvedSize) + } + + return messageContainerSize + } + + // MARK: - Cell Size + + /// Returns the height of a `MessageCollectionViewCell`'s content at a given `IndexPath` + /// + /// - Parameters: + /// - message: The `MessageType` for the given `IndexPath`. + /// - indexPath: The `IndexPath` for the given `MessageType`. + open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let avatarVerticalPosition = avatarPosition(for: message, at: indexPath).vertical + let avatarHeight = avatarSize(for: message, at: indexPath).height + let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height + let bottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height + let topLabelHeight = cellTopLabelSize(for: message, at: indexPath).height + let messageVerticalPadding = messageContainerPadding(for: message, at: indexPath).vertical + + var cellHeight: CGFloat = 0 + + switch avatarVerticalPosition { + case .cellTop: + cellHeight += max(avatarHeight, topLabelHeight) + cellHeight += bottomLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .cellBottom: + cellHeight += max(avatarHeight, bottomLabelHeight) + cellHeight += topLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .messageTop, .messageCenter, .messageBottom: + cellHeight += max(avatarHeight, messageContainerHeight) + cellHeight += messageVerticalPadding + cellHeight += topLabelHeight + cellHeight += bottomLabelHeight + } + + return cellHeight + } + + /// Returns the size for the `MessageCollectionViewCell` at a given `IndexPath` + /// considering all of the cell's content. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the cell. + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + let context = cellLayoutContext(for: message, at: indexPath) + guard let itemHeight = context.itemHeight else { + fatalError("Unexpectedly received a nil itemHeight") + } + return CGSize(width: itemWidth, height: itemHeight) + } } // MARK: - Cache Invalidation @@ -119,23 +489,6 @@ extension MessagesCollectionViewFlowLayout { layoutContextCache.removeAllObjects() } - open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - if collectionView?.bounds.width != newBounds.width { - removeAllCachedAttributes() - return true - } else { - return false - } - } - - open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { - let context = super.invalidationContext(forBoundsChange: newBounds) - guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } - flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) - return flowLayoutContext - } - - /// Invalidates the layout and removes all cached attributes on device orientation change @objc private func handleOrientationChange(_ notification: Notification) { removeAllCachedAttributes() @@ -147,31 +500,6 @@ extension MessagesCollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { - open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - - guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } - - attributesArray.forEach { attributes in - if attributes.representedElementCategory == UICollectionElementCategory.cell { - configure(attributes: attributes) - } - } - - return attributesArray - } - - open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - - guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } - - if attributes.representedElementCategory == UICollectionElementCategory.cell { - configure(attributes: attributes) - } - - return attributes - - } - private func configure(attributes: MessagesCollectionViewLayoutAttributes) { let indexPath = attributes.indexPath @@ -224,19 +552,19 @@ extension MessagesCollectionViewFlowLayout { internal func newCellLayoutContext(for message: MessageType, at indexPath: IndexPath) -> MessageCellLayoutContext { currentLayoutContext = MessageCellLayoutContext() - currentLayoutContext.avatarPosition = _avatarPosition(for: message, at: indexPath) - currentLayoutContext.avatarSize = _avatarSize(for: message, at: indexPath) - currentLayoutContext.messageContainerPadding = _messageContainerPadding(for: message, at: indexPath) - currentLayoutContext.messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - currentLayoutContext.messageContainerMaxWidth = _messageContainerMaxWidth(for: message, at: indexPath) - currentLayoutContext.messageContainerSize = _messageContainerSize(for: message, at: indexPath) - currentLayoutContext.topLabelAlignment = _cellTopLabelAlignment(for: message, at: indexPath) - currentLayoutContext.topLabelMaxWidth = _cellTopLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.topLabelSize = _cellTopLabelSize(for: message, at: indexPath) - currentLayoutContext.bottomLabelAlignment = _cellBottomLabelAlignment(for: message, at: indexPath) - currentLayoutContext.bottomLabelMaxWidth = _cellBottomLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.bottomLabelSize = _cellBottomLabelSize(for: message, at: indexPath) - currentLayoutContext.itemHeight = _cellContentHeight(for: message, at: indexPath) + currentLayoutContext.avatarPosition = avatarPosition(for: message, at: indexPath) + currentLayoutContext.avatarSize = avatarSize(for: message, at: indexPath) + currentLayoutContext.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) + currentLayoutContext.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) + currentLayoutContext.messageContainerMaxWidth = messageContainerMaxWidth(for: message, at: indexPath) + currentLayoutContext.messageContainerSize = messageContainerSize(for: message, at: indexPath) + currentLayoutContext.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) + currentLayoutContext.topLabelMaxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) + currentLayoutContext.topLabelSize = cellTopLabelSize(for: message, at: indexPath) + currentLayoutContext.bottomLabelAlignment = cellBottomLabelAlignment(for: message, at: indexPath) + currentLayoutContext.bottomLabelMaxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) + currentLayoutContext.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + currentLayoutContext.itemHeight = cellContentHeight(for: message, at: indexPath) return currentLayoutContext } } @@ -245,6 +573,38 @@ extension MessagesCollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { + /// Returns the size required fit a NSAttributedString considering a constrained max width. + /// + /// - Parameters: + /// - attributedText: The `NSAttributedString` used to calculate a size that fits. + /// - maxWidth: The max width available for the label. + internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + + let estimatedHeight = attributedText.height(considering: maxWidth) + let estimatedWidth = attributedText.width(considering: estimatedHeight) + + let finalHeight = estimatedHeight.rounded(.up) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) + + return CGSize(width: finalWidth, height: finalHeight) + } + + /// Returns the size required to fit a String considering a constrained max width. + /// + /// - Parameters: + /// - text: The `String` used to calculate a size that fits. + /// - maxWidth: The max width available for the label. + internal func labelSize(for text: String, considering maxWidth: CGFloat, and font: UIFont) -> CGSize { + + let estimatedHeight = text.height(considering: maxWidth, and: font) + let estimatedWidth = text.width(considering: estimatedHeight, and: font) + + let finalHeight = estimatedHeight.rounded(.up) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) + + return CGSize(width: finalWidth, height: finalHeight) + } + /// Convenience property for accessing the layout object's `MessagesCollectionView`. internal var messagesCollectionView: MessagesCollectionView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { From f12347f48576455efa4e3d43f3e431c9f1e3c1f2 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 15:21:09 -0500 Subject: [PATCH 02/70] Remove old deprecations --- .../Supporting/MessageKit+Availability.swift | 95 ------------------- 1 file changed, 95 deletions(-) diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index 31fee58f..8b9594fa 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -24,98 +24,3 @@ import Foundation -// MARK: - Deprecated Protocols - -@available(*, deprecated: 0.11.0, message: "LocationMessageDisplayDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias LocationMessageDisplayDelegate = MessagesDisplayDelegate - -@available(*, deprecated: 0.11.0, message: "TextMessageDisplayDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias TextMessageDisplayDelegate = MessagesDisplayDelegate - -@available(*, deprecated: 0.11.0, message: "LocationMessageLayoutDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias LocationMessageLayoutDelegate = MessagesLayoutDelegate - -@available(*, deprecated: 0.11.0, message: "MediaMessageLayoutDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias MediaMessageLayoutDelegate = MessagesLayoutDelegate - -// MARK: - AvatarAlignment - -@available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use AvatarPosition instead.") -public enum AvatarAlignment {} - -// MARK: - MessagesLayoutDelegate - -extension MessagesLayoutDelegate { - - /// Specifies the vertical alignment for the `AvatarView` in a `MessageCollectionViewCell`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is `AvatarAlignment.cellBottom`. - @available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use avatarPosition(for:at:in:) instead.") - public func avatarAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarAlignment { - fatalError("Please use avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition instead.") - } - -} - -// MARK: - MessagesCollectionViewFlowLayout - -extension MessagesCollectionViewFlowLayout { - - /// A Boolean value that determines if the `AvatarView` is always on the leading - /// side of a MessageCollectionViewCell. - /// - /// Setting this property to `true` causes `avatarAlwaysTrailing` to be set to `false`. - /// - /// The default value of this property is `false`. - @available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use the avatarPosition(for:at:in) delegate method.") - public var avatarAlwaysLeading: Bool { - fatalError("Fatal Error: avatarAlwaysLeading is no longer supported") - } - - /// A Boolean value that determines if the `AvatarView` is always on the trailing - /// side of a `MessageCollectionViewCell`. - /// - /// Setting this property to `true` causes `avatarAlwaysLeading` to be set to `false`. - /// - /// The default value of this property is `false`. - @available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use the avatarPosition(for:at:in) delegate method instead.") - public var avatarAlwaysTrailing: Bool { - fatalError("Fatal Error: avatarAlwaysTrailing is no longer supported") - } - -} - -// MARK: - MessagesDataSource - -extension MessagesDataSource { - /// The `Avatar` information to be used by the `AvatarView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is `Avatar()`. - @available(*, deprecated: 0.12.1, message: "Removed in MessageKit 0.12.1.") - func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { - fatalError("Fatal Error: avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) is no longer supported") - } -} - -// MARK: - MessagesViewController - -extension MessagesViewController { - /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the - /// bottom on the view's first layout. - /// - /// The default value of this property is `false`. - @available(*, deprecated: 0.11.1, message: "Removed in MessageKit 0.11.1.") - open var scrollsToBottomOnFirstLayout: Bool { - fatalError("Fatal Error: scrollsToBottomOnFirstLayout is no longer supported") - } -} From e2ede6fe5fe441cde50eebd13655e72fb9d414db Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 15:39:01 -0500 Subject: [PATCH 03/70] Set avatar size through properties on layout object --- .../MessagesCollectionViewFlowLayout.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 44fb87da..9643a879 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -62,6 +62,14 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } } + open var incomingAvatarSize: CGSize { + return CGSize(width: 30, height: 30) + } + + open var outgoingAvatarSize: CGSize { + return CGSize(width: 30, height: 30) + } + typealias MessageID = NSString /// The cache for `MessageCellLayoutContext`. @@ -162,15 +170,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return position } - /// Returns the `AvatarSize` for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `avatarSize(for:at:in)` in `MessagesLayoutDelegate`. - open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - return messagesLayoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView) + internal func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize } // MARK: - Cell Top Label Size From bf3930bd14435f6aaa85283b4f004989a33eebda Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 16:16:44 -0500 Subject: [PATCH 04/70] Set message padding through property on layout object --- .../MessagesCollectionViewFlowLayout.swift | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 9643a879..55cdb114 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -62,13 +62,19 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } } - open var incomingAvatarSize: CGSize { - return CGSize(width: 30, height: 30) - } + // MARK: - Layout - open var outgoingAvatarSize: CGSize { - return CGSize(width: 30, height: 30) - } + open var incomingAvatarSize = CGSize(width: 30, height: 30) + + open var outgoingAvatarSize = CGSize(width: 30, height: 30) + + open var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) + + open var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + + open var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + + open var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) typealias MessageID = NSString @@ -326,30 +332,14 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Message Container Size - /// Returns the insets of the `MessageLabel` in a `TextMessageCell` for - /// the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `messageLabelInset(for:at:in)` in `MessagesLayoutDelegate`. - open func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - return messagesLayoutDelegate.messageLabelInset(for: message, at: indexPath, in: messagesCollectionView) + internal func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets } - /// Returns the padding around the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `messagePadding(for:at:in)` in `MessagesLayoutDelegate`. - open func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - return messagesLayoutDelegate.messagePadding(for: message, at: indexPath, in: messagesCollectionView) + internal func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding } /// Returns the maximum width of the `MessageContainerView` in a `MessageCollectionViewCell` From dd6787d359f1a7f17977e896dcb34b89e627d8d7 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 16:23:41 -0500 Subject: [PATCH 05/70] Set avatarposition as property on layout object --- .../MessagesCollectionViewFlowLayout.swift | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 55cdb114..0f8bd374 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -68,6 +68,10 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var outgoingAvatarSize = CGSize(width: 30, height: 30) + open var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) + + open var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + open var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) open var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) @@ -156,16 +160,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Avatar Size - /// Returns the `AvatarPosition` for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `avatarPosition(for:at:in)` in `MessagesLayoutDelegate`. - open func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - var position = messagesLayoutDelegate.avatarPosition(for: message, at: indexPath, in: messagesCollectionView) + internal func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition switch position.horizontal { case .cellTrailing, .cellLeading: From 30e6b105c75f1d9f254899e202f0d902cea01eac Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 16:38:42 -0500 Subject: [PATCH 06/70] Set label positions as properties of layout object --- .../MessagesCollectionViewFlowLayout.swift | 40 +++-- .../Protocols/MessagesLayoutDelegate.swift | 139 ------------------ .../Supporting/MessageKit+Availability.swift | 37 +++++ 3 files changed, 54 insertions(+), 162 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 0f8bd374..d44c05fb 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -80,6 +80,14 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + open var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) + + open var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + + open var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) + + open var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + typealias MessageID = NSString /// The cache for `MessageCellLayoutContext`. @@ -180,16 +188,10 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Top Label Size - /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `cellTopLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. - open func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - return messagesLayoutDelegate.cellTopLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) + + internal func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment } /// Returns the size of the `MessageCollectionViewCell`'s top label @@ -254,16 +256,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Bottom Label Size - /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `cellBottomLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. - open func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - return messagesLayoutDelegate.cellBottomLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) + internal func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment } /// Returns the size of the `MessageCollectionViewCell`'s bottom label @@ -531,9 +526,8 @@ extension MessagesCollectionViewFlowLayout { guard let cachedContext = layoutContextCache.object(forKey: message.messageId as NSString) else { let newContext = newCellLayoutContext(for: message, at: indexPath) - if messagesLayoutDelegate.shouldCacheLayoutAttributes(for: message) { - layoutContextCache.setObject(newContext, forKey: message.messageId as NSString) - } + layoutContextCache.setObject(newContext, forKey: message.messageId as NSString) + return newContext } return cachedContext diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index f1be3042..8b74f4fb 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -29,71 +29,6 @@ import AVFoundation /// the size and layout of a `MessageCollectionViewCell` and its contents. public protocol MessagesLayoutDelegate: AnyObject { - // MARK: - All Messages - - /// Specifies the padding around the `MessageContainerView` in a `MessageCollectionViewCell`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is determined by the messages `Sender`: - /// - /// Current Sender: `UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)` - /// - /// All other Senders: `UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)` - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets - - /// Specifies the vertical and horizontal alignment for the `AvatarView` in a `MessageCollectionViewCell`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is an `AvatarPosition` with - /// `Horizontal.natural` and `Vertical.messageBottom`. - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition - - /// Specifies the horizontal alignment of a `MessageCollectionViewCell`'s top label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is determined by the messages `Sender`: - /// - /// Current Sender: .messageTrailing(.zero) - /// - /// All other senders: .messageLeading(.zero) - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment - - /// Specifies the horizontal alignment of a `MessageCollectionViewCell`'s bottom label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is determined by the messages `Sender`: - /// - /// Current Sender: .messageLeading(.zero) - /// - /// All other senders: .messageTrailing(.zero) - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment - - /// Specifies the size of the `AvatarView` in a `MessageCollectionViewCell`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is a size of `30 x 30`. - func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - /// Specifies the size to use for a `MessageHeaderView`. /// /// - Parameters: @@ -114,22 +49,6 @@ public protocol MessagesLayoutDelegate: AnyObject { /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - // MARK: - Text Messages - - /// Specifies the insets for the text rect of the `MessageLabel` in a `TextMessageCell`. - /// - /// - Parameters: - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which these insets will apply. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is determined by the messages `Sender`: - /// - /// Current Sender: `UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)` - /// - /// All other Senders: `UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14)` - func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets - // MARK: - Media Messages /// Specifies the width for a `MessageContainerView`. @@ -177,53 +96,12 @@ public protocol MessagesLayoutDelegate: AnyObject { /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - /// Specifies whether the layout attributes for a given `MessageType` - /// should be cached by the `MessagesCollectionViewFlowLayout`. - /// - Parameters: - /// - message: The `MessageType` whose attributes to cache. - /// - /// The default value returned by this method is `false`. - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool - } public extension MessagesLayoutDelegate { // MARK: - All Messages Defaults - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - if dataSource.isFromCurrentSender(message: message) { - return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - } else { - return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - } - } - - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return dataSource.isFromCurrentSender(message: message) ? .messageTrailing(.zero) : .messageLeading(.zero) - } - - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return dataSource.isFromCurrentSender(message: message) ? .messageLeading(.zero) : .messageTrailing(.zero) - } - - func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - return CGSize(width: 30, height: 30) - } - - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - return AvatarPosition(horizontal: .natural, vertical: .messageBottom) - } - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) @@ -236,23 +114,6 @@ public extension MessagesLayoutDelegate { return .zero } - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { - return false - } - - // MARK: - Text Messages Defaults - - func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - if dataSource.isFromCurrentSender(message: message) { - return UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - } else { - return UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - } - } - // MARK: - Media Messages Defaults func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index 8b9594fa..347e0f85 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -24,3 +24,40 @@ import Foundation +public extension MessagesLayoutDelegate { + + @available(*, deprecated: 0.14.0, message: "avatarSize(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarSize and outgoingAvatarSize properties on MessagesCollectionViewFlowLayout.") + func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + fatalError("avatarSize(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarSize and outgoingAvatarSize properties on MessagesCollectionViewFlowLayout.") + } + + @available(*, deprecated: 0.14.0, message: "avatarPosition(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarPosition and outgoingAvatarPosition properties on MessagesCollectionViewFlowLayout.") + func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { + fatalError("avatarPosition(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarPosition and outgoingAvatarPosition properties on MessagesCollectionViewFlowLayout.") + } + + @available(*, deprecated: 0.14.0, message: "messageLabelInset(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessageLabelInset and outgoingMessageLabelInset properties on MessagesCollectionViewFlowLayout.") + func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { + fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessageLabelInset and outgoingMessageLabelInset properties on MessagesCollectionViewFlowLayout.") + } + + @available(*, deprecated: 0.14.0, message: "messagePadding(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessagePadding and outgoingMessagePadding properties on MessagesCollectionViewFlowLayout.") + func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { + fatalError("messagePadding(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessagePadding and outgoingMessagePadding properties on MessagesCollectionViewFlowLayout.") + } + + @available(*, deprecated: 0.14.0, message: "cellTopLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellTopLabelAlignment and outgoingCellTopLabelAlignment properties on MessagesCollectionViewFlowLayout.") + func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { + fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellTopLabelAlignment and outgoingCellTopLabelAlignment properties on MessagesCollectionViewFlowLayout.") + } + + @available(*, deprecated: 0.14.0, message: "cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellBottomLabelAlignment and outgoingCellBottomLabelAlignment properties on MessagesCollectionViewFlowLayout.") + func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { + fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellBottomLabelAlignment and outgoingCellBottomLabelAlignment properties on MessagesCollectionViewFlowLayout.") + } + + @available(*, deprecated: 0.14.0, message: "Removed in MessageKit 0.14.0. MessageKit no longer caches cell attributes as it leads to undefined behavior.") + func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { + fatalError("shouldCacheLayoutAttributes is removed in MessageKit 0.14.0. This deprecation does not require a replacement method.") + } +} From bd1f7e3dbd4f182cbf0376796af29e4bc05135a0 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 16:39:56 -0500 Subject: [PATCH 07/70] Remove deprecations from example app --- .../Sources/ConversationViewController.swift | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 97275ade..40d44376 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -357,34 +357,6 @@ extension ConversationViewController: MessagesDisplayDelegate { extension ConversationViewController: MessagesLayoutDelegate { - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - return AvatarPosition(horizontal: .natural, vertical: .messageBottom) - } - - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - if isFromCurrentSender(message: message) { - return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - } else { - return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - } - } - - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - if isFromCurrentSender(message: message) { - return .messageTrailing(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)) - } else { - return .messageLeading(UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)) - } - } - - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - if isFromCurrentSender(message: message) { - return .messageLeading(UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)) - } else { - return .messageTrailing(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)) - } - } - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return CGSize(width: messagesCollectionView.bounds.width, height: 10) From a0264952c541bedf077a0925cf46538e34e51090 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 19:47:58 -0500 Subject: [PATCH 08/70] Remove cached layout attributes and cache text size --- .../MessagesCollectionViewFlowLayout.swift | 202 +++++++----------- 1 file changed, 77 insertions(+), 125 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index d44c05fb..99a710fa 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -53,47 +53,68 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// Note: The default value of this property is 2x the `messageLabelFont`. internal var emojiLabelFont: UIFont + + // MARK: - Layout + + open var incomingAvatarSize = CGSize(width: 30, height: 30) { + didSet { removeAllCachedAttributes() } + } + + open var outgoingAvatarSize = CGSize(width: 30, height: 30) { + didSet { removeAllCachedAttributes() } + } + + open var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) { + didSet { removeAllCachedAttributes() } + } + + open var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) { + didSet { removeAllCachedAttributes() } + } + + open var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) { + didSet { removeAllCachedAttributes() } + } + + open var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) { + didSet { removeAllCachedAttributes() } + } + + open var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) { + didSet { removeAllCachedAttributes() } + } + + open var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) { + didSet { removeAllCachedAttributes() } + } + + open var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) { + didSet { removeAllCachedAttributes() } + } + + open var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) { + didSet { removeAllCachedAttributes() } + } + + open var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) { + didSet { removeAllCachedAttributes() } + } + + open var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) { + didSet { removeAllCachedAttributes() } + } + + internal var attributedTextSizeCache = NSCache() + /// Determines the maximum number of `MessageCollectionViewCell` attributes to cache. /// /// Note: The default value of this property is 500. open var attributesCacheMaxSize: Int = 500 { didSet { - layoutContextCache.countLimit = attributesCacheMaxSize + attributedTextSizeCache.countLimit = attributesCacheMaxSize } } - // MARK: - Layout - - open var incomingAvatarSize = CGSize(width: 30, height: 30) - - open var outgoingAvatarSize = CGSize(width: 30, height: 30) - - open var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) - - open var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) - - open var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - - open var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - - open var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - - open var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - - open var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) - - open var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) - - open var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) - - open var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) - - typealias MessageID = NSString - - /// The cache for `MessageCellLayoutContext`. - /// The key is the `messageId` of the `MessageType`. - fileprivate var layoutContextCache = NSCache() - /// The `MessageCellLayoutContext` for the current cell. internal var currentLayoutContext: MessageCellLayoutContext! @@ -367,7 +388,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { switch message.data { case .text(let text): let messageInsets = messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth, and: messageLabelFont) + let attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) messageContainerSize.width += messageInsets.horizontal messageContainerSize.height += messageInsets.vertical case .attributedText(let text): @@ -377,7 +399,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { messageContainerSize.height += messageInsets.vertical case .emoji(let text): let messageInsets = messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth, and: emojiLabelFont) + let attributedText = NSAttributedString(string: text, attributes: [.font: emojiLabelFont]) + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) messageContainerSize.width += messageInsets.horizontal messageContainerSize.height += messageInsets.vertical case .photo, .video: @@ -440,10 +463,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// - indexPath: The `IndexPath` of the cell. open func sizeForItem(at indexPath: IndexPath) -> CGSize { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let context = cellLayoutContext(for: message, at: indexPath) - guard let itemHeight = context.itemHeight else { - fatalError("Unexpectedly received a nil itemHeight") - } + let itemHeight = cellContentHeight(for: message, at: indexPath) return CGSize(width: itemWidth, height: itemHeight) } } @@ -452,25 +472,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { - /// Removes the cached layout information for a given `MessageType`. - /// - /// - Parameters: - /// - message: The `MessageType` whose cached layout information is to be removed. - public func removeCachedAttributes(for message: MessageType) { - removeCachedAttributes(for: message.messageId) - } - - /// Removes the cached layout information for a `MessageType` with a given `messageId`. - /// - /// - Parameters: - /// - messageId: The id for the `MessageType` whose cached layout information is to be removed. - public func removeCachedAttributes(for messageId: String) { - layoutContextCache.removeObject(forKey: messageId as NSString) - } - /// Removes the cached layout information for all `MessageType`s. public func removeAllCachedAttributes() { - layoutContextCache.removeAllObjects() + attributedTextSizeCache.removeAllObjects() } @objc @@ -488,20 +492,19 @@ extension MessagesCollectionViewFlowLayout { let indexPath = attributes.indexPath let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let context = cellLayoutContext(for: message, at: indexPath) - attributes.avatarSize = context.avatarSize! - attributes.avatarPosition = context.avatarPosition! + attributes.avatarSize = avatarSize(for: message, at: indexPath) + attributes.avatarPosition = avatarPosition(for: message, at: indexPath) - attributes.messageContainerPadding = context.messageContainerPadding! - attributes.messageContainerSize = context.messageContainerSize! - attributes.messageLabelInsets = context.messageLabelInsets! + attributes.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) + attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - attributes.topLabelAlignment = context.topLabelAlignment! - attributes.topLabelSize = context.topLabelSize! + attributes.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) + attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) - attributes.bottomLabelAlignment = context.bottomLabelAlignment! - attributes.bottomLabelSize = context.bottomLabelSize! + attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: message, at: indexPath) + attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) switch message.data { case .emoji: @@ -518,74 +521,23 @@ extension MessagesCollectionViewFlowLayout { } } -// MARK: - MessageCellLayoutContext - -extension MessagesCollectionViewFlowLayout { - - internal func cellLayoutContext(for message: MessageType, at indexPath: IndexPath) -> MessageCellLayoutContext { - guard let cachedContext = layoutContextCache.object(forKey: message.messageId as NSString) else { - let newContext = newCellLayoutContext(for: message, at: indexPath) - - layoutContextCache.setObject(newContext, forKey: message.messageId as NSString) - - return newContext - } - return cachedContext - } - - internal func newCellLayoutContext(for message: MessageType, at indexPath: IndexPath) -> MessageCellLayoutContext { - currentLayoutContext = MessageCellLayoutContext() - currentLayoutContext.avatarPosition = avatarPosition(for: message, at: indexPath) - currentLayoutContext.avatarSize = avatarSize(for: message, at: indexPath) - currentLayoutContext.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) - currentLayoutContext.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - currentLayoutContext.messageContainerMaxWidth = messageContainerMaxWidth(for: message, at: indexPath) - currentLayoutContext.messageContainerSize = messageContainerSize(for: message, at: indexPath) - currentLayoutContext.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) - currentLayoutContext.topLabelMaxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.topLabelSize = cellTopLabelSize(for: message, at: indexPath) - currentLayoutContext.bottomLabelAlignment = cellBottomLabelAlignment(for: message, at: indexPath) - currentLayoutContext.bottomLabelMaxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) - currentLayoutContext.itemHeight = cellContentHeight(for: message, at: indexPath) - return currentLayoutContext - } -} - // MARK: - Helpers extension MessagesCollectionViewFlowLayout { - /// Returns the size required fit a NSAttributedString considering a constrained max width. - /// - /// - Parameters: - /// - attributedText: The `NSAttributedString` used to calculate a size that fits. - /// - maxWidth: The max width available for the label. internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { - let estimatedHeight = attributedText.height(considering: maxWidth) - let estimatedWidth = attributedText.width(considering: estimatedHeight) + guard let cachedSize = attributedTextSizeCache.object(forKey: attributedText.hash as NSNumber)?.cgSizeValue else { + let estimatedHeight = attributedText.height(considering: maxWidth) + let estimatedWidth = attributedText.width(considering: estimatedHeight) - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) + let finalHeight = estimatedHeight.rounded(.up) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - return CGSize(width: finalWidth, height: finalHeight) - } + return CGSize(width: finalWidth, height: finalHeight) + } - /// Returns the size required to fit a String considering a constrained max width. - /// - /// - Parameters: - /// - text: The `String` used to calculate a size that fits. - /// - maxWidth: The max width available for the label. - internal func labelSize(for text: String, considering maxWidth: CGFloat, and font: UIFont) -> CGSize { - - let estimatedHeight = text.height(considering: maxWidth, and: font) - let estimatedWidth = text.width(considering: estimatedHeight, and: font) - - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - - return CGSize(width: finalWidth, height: finalHeight) + return cachedSize } /// Convenience property for accessing the layout object's `MessagesCollectionView`. From 0cc21797a0fbe12a53b053f40b3b8a1e783b30f0 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 19:50:03 -0500 Subject: [PATCH 09/70] Cache calculated text size --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 99a710fa..2ff19ea4 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -534,7 +534,9 @@ extension MessagesCollectionViewFlowLayout { let finalHeight = estimatedHeight.rounded(.up) let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - return CGSize(width: finalWidth, height: finalHeight) + let size = CGSize(width: finalWidth, height: finalHeight) + attributedTextSizeCache.setObject(size as NSValue, forKey: attributedText.hash as NSNumber) + return size } return cachedSize From 74946712d3f92e630ef4718c707202aa229f908c Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 19:55:53 -0500 Subject: [PATCH 10/70] Revert access control to internal until otherwise --- .../MessagesCollectionViewFlowLayout.swift | 64 +++---------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 2ff19ea4..44331a7e 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -33,7 +33,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return MessagesCollectionViewLayoutAttributes.self } - /// The width of an item in the `MessagesCollectionView`. internal var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } return collectionView.frame.width - sectionInset.left - sectionInset.right @@ -115,9 +114,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } } - /// The `MessageCellLayoutContext` for the current cell. - internal var currentLayoutContext: MessageCellLayoutContext! - // MARK: - Initializers public override init() { @@ -215,28 +211,15 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment } - /// Returns the size of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method sizes the label to fit. - open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + + internal func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) guard let topLabelText = text else { return .zero } let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) return labelSize(for: topLabelText, considering: maxWidth) } - /// Returns the maximum width of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + internal func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let alignment = cellTopLabelAlignment(for: message, at: indexPath) let position = avatarPosition(for: message, at: indexPath) let avatarWidth = avatarSize(for: message, at: indexPath).width @@ -282,28 +265,14 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment } - /// Returns the size of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method sizes the label to fit. - open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + internal func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) guard let bottomLabelText = text else { return .zero } let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) return labelSize(for: bottomLabelText, considering: maxWidth) } - /// Returns the maximum width of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + internal func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let alignment = cellBottomLabelAlignment(for: message, at: indexPath) let avatarWidth = avatarSize(for: message, at: indexPath).width @@ -355,13 +324,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding } - /// Returns the maximum width of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + internal func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let avatarWidth = avatarSize(for: message, at: indexPath).width let messagePadding = messageContainerPadding(for: message, at: indexPath) @@ -374,13 +337,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } } - /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + internal func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) var messageContainerSize: CGSize = .zero @@ -420,12 +377,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Size - /// Returns the height of a `MessageCollectionViewCell`'s content at a given `IndexPath` - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + internal func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let avatarVerticalPosition = avatarPosition(for: message, at: indexPath).vertical let avatarHeight = avatarSize(for: message, at: indexPath).height let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height From 1d8eedf65b5c0ee81de415faf988be7d1edc11ea Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 19:56:45 -0500 Subject: [PATCH 11/70] Remove MessageCellLayoutContext --- MessageKit.xcodeproj/project.pbxproj | 4 -- Sources/Layout/MessageCellLayoutContext.swift | 48 ------------------- 2 files changed, 52 deletions(-) delete mode 100644 Sources/Layout/MessageCellLayoutContext.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index a985a3f5..2aac2a2b 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; }; 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; - 1FAC3490201E50DF000A1504 /* MessageCellLayoutContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; @@ -119,7 +118,6 @@ 1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = ""; }; 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; - 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayoutContext.swift; sourceTree = ""; }; 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; @@ -439,7 +437,6 @@ children = ( B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, - 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */, ); path = Layout; sourceTree = ""; @@ -594,7 +591,6 @@ B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, - 1FAC3490201E50DF000A1504 /* MessageCellLayoutContext.swift in Sources */, B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */, B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, diff --git a/Sources/Layout/MessageCellLayoutContext.swift b/Sources/Layout/MessageCellLayoutContext.swift deleted file mode 100644 index 693c976f..00000000 --- a/Sources/Layout/MessageCellLayoutContext.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - 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 Foundation - -/// An internal type used to store the attributes used to calculate -/// the size of a `MessageCollectionViewCell`. -final class MessageCellLayoutContext { - - var itemHeight: CGFloat? - - var avatarSize: CGSize? - var avatarPosition: AvatarPosition? - - var messageContainerSize: CGSize? - var messageContainerMaxWidth: CGFloat? - var messageContainerPadding: UIEdgeInsets? - var messageLabelInsets: UIEdgeInsets? - - var bottomLabelAlignment: LabelAlignment? - var bottomLabelSize: CGSize? - var bottomLabelMaxWidth: CGFloat? - - var topLabelAlignment: LabelAlignment? - var topLabelSize: CGSize? - var topLabelMaxWidth: CGFloat? -} From 212473c26fc146cc954a9e3b52797aea053227b9 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 22:23:25 -0500 Subject: [PATCH 12/70] Create MessageCellLayout objects --- MessageKit.xcodeproj/project.pbxproj | 4 + Sources/Layout/MessageCellLayout.swift | 85 ++++++ .../MessagesCollectionViewFlowLayout.swift | 262 +++++++++++------- 3 files changed, 255 insertions(+), 96 deletions(-) create mode 100644 Sources/Layout/MessageCellLayout.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 2aac2a2b..aa921e82 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; + 1FD5895C205F5B8F004B5081 /* MessageCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 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 */; }; @@ -119,6 +120,7 @@ 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = ""; }; 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; + 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayout.swift; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; @@ -437,6 +439,7 @@ children = ( B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, + 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */, ); path = Layout; sourceTree = ""; @@ -624,6 +627,7 @@ B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, + 1FD5895C205F5B8F004B5081 /* MessageCellLayout.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageCellLayout.swift new file mode 100644 index 00000000..42fbfba4 --- /dev/null +++ b/Sources/Layout/MessageCellLayout.swift @@ -0,0 +1,85 @@ +/* + 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 + +protocol MessageCellLayout { + + var incomingAvatarSize: CGSize { get set } + var outgoingAvatarSize: CGSize { get set } + + var incomingAvatarPosition: AvatarPosition { get set } + var outgoingAvatarPosition: AvatarPosition { get set } + + var incomingMessagePadding: UIEdgeInsets { get set } + var outgoingMessagePadding: UIEdgeInsets { get set } + + var incomingCellTopLabelAlignment: LabelAlignment { get set } + var outgoingCellTopLabelAlignment: LabelAlignment { get set } + + var incomingCellBottomLabelAlignment: LabelAlignment { get set } + var outgoingCellBottomLabelAlignment: LabelAlignment { get set } + +} + +public struct TextMessageLayout: MessageCellLayout { + + var incomingAvatarSize = CGSize(width: 30, height: 30) + var outgoingAvatarSize = CGSize(width: 30, height: 30) + + var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) + var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + + var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) + var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + + var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + + var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) + var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + + var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) + var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + +} + +public struct MediaMessageLayout: MessageCellLayout { + + var incomingAvatarSize = CGSize(width: 30, height: 30) + var outgoingAvatarSize = CGSize(width: 30, height: 30) + + var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) + var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + + var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + + var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) + var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + + var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) + var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + +} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 44331a7e..e06be86e 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -52,67 +52,38 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// Note: The default value of this property is 2x the `messageLabelFont`. internal var emojiLabelFont: UIFont - // MARK: - Layout - open var incomingAvatarSize = CGSize(width: 30, height: 30) { - didSet { removeAllCachedAttributes() } + open var textMessageLayout = TextMessageLayout() { + didSet { textMessageAttributesCache.removeAllObjects() } } - open var outgoingAvatarSize = CGSize(width: 30, height: 30) { - didSet { removeAllCachedAttributes() } + open var attributedTextMessageLayout = TextMessageLayout() { + didSet { attributedTextMessageAttributesCache.removeAllObjects() } } - open var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) { - didSet { removeAllCachedAttributes() } + open var emojiMessageLayout = TextMessageLayout() { + didSet { emojiMessageAttributesCache.removeAllObjects() } } - open var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) { - didSet { removeAllCachedAttributes() } + open var photoMessageLayout = MediaMessageLayout() { + didSet { photoMessageAttributesCache.removeAllObjects() } } - open var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) { - didSet { removeAllCachedAttributes() } + open var videoMessageLayout = MediaMessageLayout() { + didSet { videoMessageAttributesCache.removeAllObjects() } } - open var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) { - didSet { removeAllCachedAttributes() } + open var locationMessageLayout = MediaMessageLayout() { + didSet { locationMessageAttributesCache.removeAllObjects() } } - open var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) { - didSet { removeAllCachedAttributes() } - } - - open var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) { - didSet { removeAllCachedAttributes() } - } - - open var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) { - didSet { removeAllCachedAttributes() } - } - - open var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) { - didSet { removeAllCachedAttributes() } - } - - open var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) { - didSet { removeAllCachedAttributes() } - } - - open var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) { - didSet { removeAllCachedAttributes() } - } - - internal var attributedTextSizeCache = NSCache() - - /// Determines the maximum number of `MessageCollectionViewCell` attributes to cache. - /// - /// Note: The default value of this property is 500. - open var attributesCacheMaxSize: Int = 500 { - didSet { - attributedTextSizeCache.countLimit = attributesCacheMaxSize - } - } + let textMessageAttributesCache = NSCache() + let attributedTextMessageAttributesCache = NSCache() + let emojiMessageAttributesCache = NSCache() + let photoMessageAttributesCache = NSCache() + let videoMessageAttributesCache = NSCache() + let locationMessageAttributesCache = NSCache() // MARK: - Initializers @@ -136,13 +107,13 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { NotificationCenter.default.removeObserver(self) } - // MARK: - Open - // MARK: - Attributes open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } + guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { + return nil + } attributesArray.forEach { attributes in if attributes.representedElementCategory == UICollectionElementCategory.cell { @@ -154,7 +125,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } if attributes.representedElementCategory == UICollectionElementCategory.cell { @@ -183,35 +153,69 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return flowLayoutContext } + /// Removes the cached layout information for all `MessageType`s. + public func removeAllCachedAttributes() { + textMessageAttributesCache.removeAllObjects() + attributedTextMessageAttributesCache.removeAllObjects() + emojiMessageAttributesCache.removeAllObjects() + photoMessageAttributesCache.removeAllObjects() + videoMessageAttributesCache.removeAllObjects() + locationMessageAttributesCache.removeAllObjects() + } + + public func removeAllCachedAttributes(for messageType: MessageData) { + let cache = attributeCache(for: messageType) + cache.removeAllObjects() + } + + @objc + private func handleOrientationChange(_ notification: Notification) { + removeAllCachedAttributes() + invalidateLayout() + } + + func layoutForMessage(message: MessageType) -> MessageCellLayout { + switch message.data { + case .text: return textMessageLayout + case .attributedText: return attributedTextMessageLayout + case .emoji: return emojiMessageLayout + case .photo: return photoMessageLayout + case .video: return videoMessageLayout + case .location: return locationMessageLayout + case .custom: fatalError() + } + } + // MARK: - Avatar Size internal func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { + let layout = layoutForMessage(message: message) let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition + var position = isFromCurrentSender ? layout.outgoingAvatarPosition : layout.incomingAvatarPosition switch position.horizontal { case .cellTrailing, .cellLeading: break case .natural: - position.horizontal = messagesDataSource.isFromCurrentSender(message: message) ? .cellTrailing : .cellLeading + position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading } return position } internal func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layout = layoutForMessage(message: message) let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize + return isFromCurrentSender ? layout.outgoingAvatarSize : layout.incomingAvatarSize } // MARK: - Cell Top Label Size - internal func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let layout = layoutForMessage(message: message) let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment + return isFromCurrentSender ? layout.outgoingCellTopLabelAlignment : layout.incomingCellTopLabelAlignment } - internal func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) guard let topLabelText = text else { return .zero } @@ -261,8 +265,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Bottom Label Size internal func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let layout = layoutForMessage(message: message) let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment + return isFromCurrentSender ? layout.outgoingCellBottomLabelAlignment : layout.incomingCellBottomLabelAlignment } internal func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { @@ -315,13 +320,17 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Message Container Size internal func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + guard let layout = layoutForMessage(message: message) as? TextMessageLayout else { + fatalError("Layout is not TextMessageLayout") + } let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets + return isFromCurrentSender ? layout.outgoingMessageLabelInsets : layout.incomingMessageLabelInsets } internal func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + let layout = layoutForMessage(message: message) let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding + return isFromCurrentSender ? layout.outgoingMessagePadding : layout.incomingMessagePadding } internal func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { @@ -378,12 +387,13 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Size internal func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarVerticalPosition = avatarPosition(for: message, at: indexPath).vertical - let avatarHeight = avatarSize(for: message, at: indexPath).height - let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height - let bottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height - let topLabelHeight = cellTopLabelSize(for: message, at: indexPath).height - let messageVerticalPadding = messageContainerPadding(for: message, at: indexPath).vertical + let attributes = intermediateLayoutAttributes(for: message, at: indexPath) + let avatarVerticalPosition = attributes.avatarPosition.vertical + let avatarHeight = attributes.avatarSize.height + let messageContainerHeight = attributes.messageContainerSize.height + let bottomLabelHeight = attributes.bottomLabelSize.height + let topLabelHeight = attributes.topLabelSize.height + let messageVerticalPadding = attributes.messageContainerPadding.vertical var cellHeight: CGFloat = 0 @@ -420,37 +430,27 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } } -// MARK: - Cache Invalidation - -extension MessagesCollectionViewFlowLayout { - - /// Removes the cached layout information for all `MessageType`s. - public func removeAllCachedAttributes() { - attributedTextSizeCache.removeAllObjects() - } - - @objc - private func handleOrientationChange(_ notification: Notification) { - removeAllCachedAttributes() - invalidateLayout() - } -} - // MARK: - MessagesCollectionViewLayoutAttributes extension MessagesCollectionViewFlowLayout { - private func configure(attributes: MessagesCollectionViewLayoutAttributes) { + private func intermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { - let indexPath = attributes.indexPath - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + let cache = attributeCache(for: message.data) + let cachedAttributes = cache.object(forKey: message.messageId as NSString) + + return cachedAttributes ?? createIntermediateLayoutAttributes(for: message, at: indexPath) + } + + private func createIntermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { + + let attributes = MessageIntermediateLayoutAttributes() attributes.avatarSize = avatarSize(for: message, at: indexPath) attributes.avatarPosition = avatarPosition(for: message, at: indexPath) attributes.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) attributes.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) @@ -461,9 +461,56 @@ extension MessagesCollectionViewFlowLayout { switch message.data { case .emoji: attributes.messageLabelFont = emojiLabelFont + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) case .text: attributes.messageLabelFont = messageLabelFont + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) case .attributedText(let text): + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) + attributes.messageLabelFont = messageLabelFont + guard !text.string.isEmpty else { return attributes } + guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return attributes } + attributes.messageLabelFont = font + default: + break + } + + let cache = attributeCache(for: message.data) + cache.setObject(attributes, forKey: message.messageId as NSString) + + return attributes + + } + + private func configure(attributes: MessagesCollectionViewLayoutAttributes) { + + let indexPath = attributes.indexPath + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + let intermediateAttributes = intermediateLayoutAttributes(for: message, at: indexPath) + + attributes.avatarSize = intermediateAttributes.avatarSize + attributes.avatarPosition = intermediateAttributes.avatarPosition + + attributes.messageContainerPadding = intermediateAttributes.messageContainerPadding + attributes.messageContainerSize = intermediateAttributes.messageContainerSize + + attributes.topLabelAlignment = intermediateAttributes.topLabelAlignment + attributes.topLabelSize = intermediateAttributes.topLabelSize + + attributes.bottomLabelAlignment = intermediateAttributes.bottomLabelAlignment + attributes.bottomLabelSize = intermediateAttributes.bottomLabelSize + + switch message.data { + case .emoji: + attributes.messageLabelFont = emojiLabelFont + attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets + case .text: + attributes.messageLabelFont = messageLabelFont + attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets + case .attributedText(let text): + attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets + attributes.messageLabelFont = intermediateAttributes.messageLabelFont guard !text.string.isEmpty else { return } guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } attributes.messageLabelFont = font @@ -478,20 +525,25 @@ extension MessagesCollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + let estimatedHeight = attributedText.height(considering: maxWidth) + let estimatedWidth = attributedText.width(considering: estimatedHeight) - guard let cachedSize = attributedTextSizeCache.object(forKey: attributedText.hash as NSNumber)?.cgSizeValue else { - let estimatedHeight = attributedText.height(considering: maxWidth) - let estimatedWidth = attributedText.width(considering: estimatedHeight) + let finalHeight = estimatedHeight.rounded(.up) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) + return CGSize(width: finalWidth, height: finalHeight) + } - let size = CGSize(width: finalWidth, height: finalHeight) - attributedTextSizeCache.setObject(size as NSValue, forKey: attributedText.hash as NSNumber) - return size + internal func attributeCache(for messageData: MessageData) -> NSCache { + switch messageData { + case .text: return textMessageAttributesCache + case .attributedText: return attributedTextMessageAttributesCache + case .emoji: return emojiMessageAttributesCache + case .photo: return photoMessageAttributesCache + case .video: return videoMessageAttributesCache + case .location: return locationMessageAttributesCache + case .custom: fatalError() } - - return cachedSize } /// Convenience property for accessing the layout object's `MessagesCollectionView`. @@ -518,3 +570,21 @@ extension MessagesCollectionViewFlowLayout { return messagesLayoutDelegate } } + +class MessageIntermediateLayoutAttributes { + + var avatarSize: CGSize = .zero + var avatarPosition = AvatarPosition(vertical: .cellBottom) + + var messageContainerSize: CGSize = .zero + var messageContainerPadding: UIEdgeInsets = .zero + var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) + var messageLabelInsets: UIEdgeInsets = .zero + + var topLabelAlignment = LabelAlignment.cellCenter(.zero) + var topLabelSize: CGSize = .zero + + var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) + var bottomLabelSize: CGSize = .zero + +} From 7a30b63661c31af4cc3ea3d83cb9a5b1bfdb7fa6 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 22:46:13 -0500 Subject: [PATCH 13/70] Handle MessageData.custom caching --- Sources/Layout/MessageCellLayout.swift | 47 +++++++------- .../MessagesCollectionViewFlowLayout.swift | 65 +++++++++++++------ 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageCellLayout.swift index 42fbfba4..33e1c682 100644 --- a/Sources/Layout/MessageCellLayout.swift +++ b/Sources/Layout/MessageCellLayout.swift @@ -24,7 +24,7 @@ import UIKit -protocol MessageCellLayout { +public protocol MessageCellLayout { var incomingAvatarSize: CGSize { get set } var outgoingAvatarSize: CGSize { get set } @@ -45,41 +45,42 @@ protocol MessageCellLayout { public struct TextMessageLayout: MessageCellLayout { - var incomingAvatarSize = CGSize(width: 30, height: 30) - var outgoingAvatarSize = CGSize(width: 30, height: 30) - var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) - var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + public var incomingAvatarSize = CGSize(width: 30, height: 30) + public var outgoingAvatarSize = CGSize(width: 30, height: 30) - var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + public var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) + public var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) - var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) + public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) - var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) - var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + public var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) + public var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + + public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) + public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) } public struct MediaMessageLayout: MessageCellLayout { - var incomingAvatarSize = CGSize(width: 30, height: 30) - var outgoingAvatarSize = CGSize(width: 30, height: 30) + public var incomingAvatarSize = CGSize(width: 30, height: 30) + public var outgoingAvatarSize = CGSize(width: 30, height: 30) - var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) - var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + public var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) + public var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) - var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) - var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + public var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) + public var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) - var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) - var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) + public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index e06be86e..5f9c56cd 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -78,12 +78,17 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { didSet { locationMessageAttributesCache.removeAllObjects() } } + open var customMessageLayout: MessageCellLayout? { + didSet { customMessageAttributesCache.removeAllObjects() } + } + let textMessageAttributesCache = NSCache() let attributedTextMessageAttributesCache = NSCache() let emojiMessageAttributesCache = NSCache() let photoMessageAttributesCache = NSCache() let videoMessageAttributesCache = NSCache() let locationMessageAttributesCache = NSCache() + let customMessageAttributesCache = NSCache() // MARK: - Initializers @@ -110,27 +115,25 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Attributes open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } - - attributesArray.forEach { attributes in - if attributes.representedElementCategory == UICollectionElementCategory.cell { + for attributes in attributesArray { + if attributes.representedElementCategory == .cell { configure(attributes: attributes) } } - return attributesArray } open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } + guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { + return nil - if attributes.representedElementCategory == UICollectionElementCategory.cell { + } + if attributes.representedElementCategory == .cell { configure(attributes: attributes) } - return attributes } @@ -161,6 +164,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { photoMessageAttributesCache.removeAllObjects() videoMessageAttributesCache.removeAllObjects() locationMessageAttributesCache.removeAllObjects() + customMessageAttributesCache.removeAllObjects() } public func removeAllCachedAttributes(for messageType: MessageData) { @@ -168,24 +172,27 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { cache.removeAllObjects() } + public func removeCachedAttributes(for message: MessageType) { + let cache = attributeCache(for: message.data) + cache.removeObject(forKey: message.messageId as NSString) + } + + public func removeCachedAttributes(for messageId: String) { + textMessageAttributesCache.removeObject(forKey: messageId as NSString) + attributedTextMessageAttributesCache.removeObject(forKey: messageId as NSString) + emojiMessageAttributesCache.removeObject(forKey: messageId as NSString) + photoMessageAttributesCache.removeObject(forKey: messageId as NSString) + videoMessageAttributesCache.removeObject(forKey: messageId as NSString) + locationMessageAttributesCache.removeObject(forKey: messageId as NSString) + customMessageAttributesCache.removeObject(forKey: messageId as NSString) + } + @objc private func handleOrientationChange(_ notification: Notification) { removeAllCachedAttributes() invalidateLayout() } - func layoutForMessage(message: MessageType) -> MessageCellLayout { - switch message.data { - case .text: return textMessageLayout - case .attributedText: return attributedTextMessageLayout - case .emoji: return emojiMessageLayout - case .photo: return photoMessageLayout - case .video: return videoMessageLayout - case .location: return locationMessageLayout - case .custom: fatalError() - } - } - // MARK: - Avatar Size internal func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { @@ -542,7 +549,23 @@ extension MessagesCollectionViewFlowLayout { case .photo: return photoMessageAttributesCache case .video: return videoMessageAttributesCache case .location: return locationMessageAttributesCache - case .custom: fatalError() + case .custom: return customMessageAttributesCache + } + } + + internal func layoutForMessage(message: MessageType) -> MessageCellLayout { + switch message.data { + case .text: return textMessageLayout + case .attributedText: return attributedTextMessageLayout + case .emoji: return emojiMessageLayout + case .photo: return photoMessageLayout + case .video: return videoMessageLayout + case .location: return locationMessageLayout + case .custom: + guard let customLayout = customMessageLayout else { + fatalError("Missing MessageCellLayout for MessageData.custom. Please set customMessageLayout") + } + return customLayout } } From 9181cd8db49b4fd9a93c1de178f0072408ad9d42 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 22:56:32 -0500 Subject: [PATCH 14/70] Make itemHeight lazy on intermediate attributes --- .../MessagesCollectionViewFlowLayout.swift | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 5f9c56cd..616423c6 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -395,34 +395,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { internal func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let attributes = intermediateLayoutAttributes(for: message, at: indexPath) - let avatarVerticalPosition = attributes.avatarPosition.vertical - let avatarHeight = attributes.avatarSize.height - let messageContainerHeight = attributes.messageContainerSize.height - let bottomLabelHeight = attributes.bottomLabelSize.height - let topLabelHeight = attributes.topLabelSize.height - let messageVerticalPadding = attributes.messageContainerPadding.vertical - - var cellHeight: CGFloat = 0 - - switch avatarVerticalPosition { - case .cellTop: - cellHeight += max(avatarHeight, topLabelHeight) - cellHeight += bottomLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .cellBottom: - cellHeight += max(avatarHeight, bottomLabelHeight) - cellHeight += topLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .messageTop, .messageCenter, .messageBottom: - cellHeight += max(avatarHeight, messageContainerHeight) - cellHeight += messageVerticalPadding - cellHeight += topLabelHeight - cellHeight += bottomLabelHeight - } - - return cellHeight + return attributes.itemHeight } /// Returns the size for the `MessageCollectionViewCell` at a given `IndexPath` @@ -610,4 +583,35 @@ class MessageIntermediateLayoutAttributes { var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) var bottomLabelSize: CGSize = .zero + lazy var itemHeight: CGFloat = { + let avatarVerticalPosition = avatarPosition.vertical + let avatarHeight = avatarSize.height + let messageContainerHeight = messageContainerSize.height + let bottomLabelHeight = bottomLabelSize.height + let topLabelHeight = topLabelSize.height + let messageVerticalPadding = messageContainerPadding.vertical + + var cellHeight: CGFloat = 0 + + switch avatarVerticalPosition { + case .cellTop: + cellHeight += max(avatarHeight, topLabelHeight) + cellHeight += bottomLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .cellBottom: + cellHeight += max(avatarHeight, bottomLabelHeight) + cellHeight += topLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .messageTop, .messageCenter, .messageBottom: + cellHeight += max(avatarHeight, messageContainerHeight) + cellHeight += messageVerticalPadding + cellHeight += topLabelHeight + cellHeight += bottomLabelHeight + } + + return cellHeight + }() + } From 3af73b0939c5b29b7794cf7fb93a37280ce5a6eb Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 23:03:13 -0500 Subject: [PATCH 15/70] Clean up some cache edge cases --- .../MessagesCollectionViewFlowLayout.swift | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 616423c6..00b0ec1a 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -44,12 +44,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var messageLabelFont: UIFont { didSet { emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) + textMessageAttributesCache.removeAllObjects() + attributedTextMessageAttributesCache.removeAllObjects() + emojiMessageAttributesCache.removeAllObjects() } } - /// The font to be used by `TextMessageCell` for `MessageData.emoji(String)` case. - /// - /// Note: The default value of this property is 2x the `messageLabelFont`. internal var emojiLabelFont: UIFont // MARK: - Layout @@ -82,13 +82,13 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { didSet { customMessageAttributesCache.removeAllObjects() } } - let textMessageAttributesCache = NSCache() - let attributedTextMessageAttributesCache = NSCache() - let emojiMessageAttributesCache = NSCache() - let photoMessageAttributesCache = NSCache() - let videoMessageAttributesCache = NSCache() - let locationMessageAttributesCache = NSCache() - let customMessageAttributesCache = NSCache() + private let textMessageAttributesCache = NSCache() + private let attributedTextMessageAttributesCache = NSCache() + private let emojiMessageAttributesCache = NSCache() + private let photoMessageAttributesCache = NSCache() + private let videoMessageAttributesCache = NSCache() + private let locationMessageAttributesCache = NSCache() + private let customMessageAttributesCache = NSCache() // MARK: - Initializers @@ -135,7 +135,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { configure(attributes: attributes) } return attributes - } // MARK: - Layout Invalidation From 75807db05bf74372ac0ef791aca91089abb53b82 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 23:43:03 -0500 Subject: [PATCH 16/70] Gardening and open a few methods --- MessageKit.xcodeproj/project.pbxproj | 4 + Sources/Layout/MessageCellLayout.swift | 1 - .../MessageIntermediateLayoutAttributes.swift | 74 +++ .../MessagesCollectionViewFlowLayout.swift | 616 ++++++++---------- 4 files changed, 361 insertions(+), 334 deletions(-) create mode 100644 Sources/Layout/MessageIntermediateLayoutAttributes.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index aa921e82..5276c865 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; 1FD5895C205F5B8F004B5081 /* MessageCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */; }; + 1FD5895E205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 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 */; }; @@ -121,6 +122,7 @@ 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayout.swift; sourceTree = ""; }; + 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIntermediateLayoutAttributes.swift; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; @@ -440,6 +442,7 @@ B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */, + 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */, ); path = Layout; sourceTree = ""; @@ -599,6 +602,7 @@ B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, + 1FD5895E205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift in Sources */, 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, B7A03F331F866940006AEF79 /* MessageDateHeaderView.swift in Sources */, diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageCellLayout.swift index 33e1c682..ac1bdf0d 100644 --- a/Sources/Layout/MessageCellLayout.swift +++ b/Sources/Layout/MessageCellLayout.swift @@ -45,7 +45,6 @@ public protocol MessageCellLayout { public struct TextMessageLayout: MessageCellLayout { - public var incomingAvatarSize = CGSize(width: 30, height: 30) public var outgoingAvatarSize = CGSize(width: 30, height: 30) diff --git a/Sources/Layout/MessageIntermediateLayoutAttributes.swift b/Sources/Layout/MessageIntermediateLayoutAttributes.swift new file mode 100644 index 00000000..73b6347d --- /dev/null +++ b/Sources/Layout/MessageIntermediateLayoutAttributes.swift @@ -0,0 +1,74 @@ +/* + 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 Foundation + +class MessageIntermediateLayoutAttributes { + + var avatarSize: CGSize = .zero + var avatarPosition = AvatarPosition(vertical: .cellBottom) + + var messageContainerSize: CGSize = .zero + var messageContainerPadding: UIEdgeInsets = .zero + var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) + var messageLabelInsets: UIEdgeInsets = .zero + + var topLabelAlignment = LabelAlignment.cellCenter(.zero) + var topLabelSize: CGSize = .zero + + var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) + var bottomLabelSize: CGSize = .zero + + lazy var itemHeight: CGFloat = { + let avatarVerticalPosition = avatarPosition.vertical + let avatarHeight = avatarSize.height + let messageContainerHeight = messageContainerSize.height + let bottomLabelHeight = bottomLabelSize.height + let topLabelHeight = topLabelSize.height + let messageVerticalPadding = messageContainerPadding.vertical + + var cellHeight: CGFloat = 0 + + switch avatarVerticalPosition { + case .cellTop: + cellHeight += max(avatarHeight, topLabelHeight) + cellHeight += bottomLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .cellBottom: + cellHeight += max(avatarHeight, bottomLabelHeight) + cellHeight += topLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .messageTop, .messageCenter, .messageBottom: + cellHeight += max(avatarHeight, messageContainerHeight) + cellHeight += messageVerticalPadding + cellHeight += topLabelHeight + cellHeight += bottomLabelHeight + } + + return cellHeight + }() + +} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 00b0ec1a..cec7b7d4 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -118,10 +118,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } - for attributes in attributesArray { - if attributes.representedElementCategory == .cell { - configure(attributes: attributes) - } + for attributes in attributesArray where attributes.representedElementCategory == .cell { + configure(attributes: attributes) } return attributesArray } @@ -129,7 +127,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil - } if attributes.representedElementCategory == .cell { configure(attributes: attributes) @@ -137,282 +134,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return attributes } - // MARK: - Layout Invalidation - - open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - if collectionView?.bounds.width != newBounds.width { - removeAllCachedAttributes() - return true - } else { - return false - } - } - - open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { - let context = super.invalidationContext(forBoundsChange: newBounds) - guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } - flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) - return flowLayoutContext - } - - /// Removes the cached layout information for all `MessageType`s. - public func removeAllCachedAttributes() { - textMessageAttributesCache.removeAllObjects() - attributedTextMessageAttributesCache.removeAllObjects() - emojiMessageAttributesCache.removeAllObjects() - photoMessageAttributesCache.removeAllObjects() - videoMessageAttributesCache.removeAllObjects() - locationMessageAttributesCache.removeAllObjects() - customMessageAttributesCache.removeAllObjects() - } - - public func removeAllCachedAttributes(for messageType: MessageData) { - let cache = attributeCache(for: messageType) - cache.removeAllObjects() - } - - public func removeCachedAttributes(for message: MessageType) { - let cache = attributeCache(for: message.data) - cache.removeObject(forKey: message.messageId as NSString) - } - - public func removeCachedAttributes(for messageId: String) { - textMessageAttributesCache.removeObject(forKey: messageId as NSString) - attributedTextMessageAttributesCache.removeObject(forKey: messageId as NSString) - emojiMessageAttributesCache.removeObject(forKey: messageId as NSString) - photoMessageAttributesCache.removeObject(forKey: messageId as NSString) - videoMessageAttributesCache.removeObject(forKey: messageId as NSString) - locationMessageAttributesCache.removeObject(forKey: messageId as NSString) - customMessageAttributesCache.removeObject(forKey: messageId as NSString) - } - - @objc - private func handleOrientationChange(_ notification: Notification) { - removeAllCachedAttributes() - invalidateLayout() - } - - // MARK: - Avatar Size - - internal func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - var position = isFromCurrentSender ? layout.outgoingAvatarPosition : layout.incomingAvatarPosition - - switch position.horizontal { - case .cellTrailing, .cellLeading: - break - case .natural: - position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading - } - return position - } - - internal func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingAvatarSize : layout.incomingAvatarSize - } - - // MARK: - Cell Top Label Size - - internal func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingCellTopLabelAlignment : layout.incomingCellTopLabelAlignment - } - - internal func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) - guard let topLabelText = text else { return .zero } - let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: topLabelText, considering: maxWidth) - } - - internal func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let alignment = cellTopLabelAlignment(for: message, at: indexPath) - let position = avatarPosition(for: message, at: indexPath) - let avatarWidth = avatarSize(for: message, at: indexPath).width - let containerSize = messageContainerSize(for: message, at: indexPath) - let messagePadding = messageContainerPadding(for: message, at: indexPath) - - let avatarHorizontal = position.horizontal - let avatarVertical = position.vertical - - switch (alignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = containerSize.width + messagePadding.left - alignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = containerSize.width + messagePadding.right - alignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - - // MARK: - Cell Bottom Label Size - - internal func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingCellBottomLabelAlignment : layout.incomingCellBottomLabelAlignment - } - - internal func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - guard let bottomLabelText = text else { return .zero } - let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: bottomLabelText, considering: maxWidth) - } - - internal func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - let alignment = cellBottomLabelAlignment(for: message, at: indexPath) - let avatarWidth = avatarSize(for: message, at: indexPath).width - let containerSize = messageContainerSize(for: message, at: indexPath) - let messagePadding = messageContainerPadding(for: message, at: indexPath) - let position = avatarPosition(for: message, at: indexPath) - - let avatarHorizontal = position.horizontal - let avatarVertical = position.vertical - - switch (alignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = containerSize.width + messagePadding.left - alignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = containerSize.width + messagePadding.right - alignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - - // MARK: - Message Container Size - - internal func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let layout = layoutForMessage(message: message) as? TextMessageLayout else { - fatalError("Layout is not TextMessageLayout") - } - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingMessageLabelInsets : layout.incomingMessageLabelInsets - } - - internal func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingMessagePadding : layout.incomingMessagePadding - } - - internal func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarWidth = avatarSize(for: message, at: indexPath).width - let messagePadding = messageContainerPadding(for: message, at: indexPath) - - switch message.data { - case .text, .attributedText: - let messageInsets = messageLabelInsets(for: message, at: indexPath) - return itemWidth - avatarWidth - messagePadding.horizontal - messageInsets.horizontal - default: - return itemWidth - avatarWidth - messagePadding.horizontal - } - } - - internal func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - - var messageContainerSize: CGSize = .zero - - switch message.data { - case .text(let text): - let messageInsets = messageLabelInsets(for: message, at: indexPath) - let attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - case .attributedText(let text): - let messageInsets = messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - case .emoji(let text): - let messageInsets = messageLabelInsets(for: message, at: indexPath) - let attributedText = NSAttributedString(string: text, attributes: [.font: emojiLabelFont]) - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - case .photo, .video: - let width = messagesLayoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .location: - let width = messagesLayoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .custom: - fatalError(MessageKitError.customDataUnresolvedSize) - } - - return messageContainerSize - } - - // MARK: - Cell Size - - internal func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let attributes = intermediateLayoutAttributes(for: message, at: indexPath) - return attributes.itemHeight - } - - /// Returns the size for the `MessageCollectionViewCell` at a given `IndexPath` - /// considering all of the cell's content. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the cell. - open func sizeForItem(at indexPath: IndexPath) -> CGSize { - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let itemHeight = cellContentHeight(for: message, at: indexPath) - return CGSize(width: itemWidth, height: itemHeight) - } -} - -// MARK: - MessagesCollectionViewLayoutAttributes - -extension MessagesCollectionViewFlowLayout { - private func intermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { let cache = attributeCache(for: message.data) @@ -497,6 +218,287 @@ extension MessagesCollectionViewFlowLayout { break } } + + // MARK: - Layout Invalidation + + open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if collectionView?.bounds.width != newBounds.width { + removeAllCachedAttributes() + return true + } else { + return false + } + } + + open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { + let context = super.invalidationContext(forBoundsChange: newBounds) + guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } + flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) + return flowLayoutContext + } + + /// Removes the cached layout information for all `MessageType`s. + public func removeAllCachedAttributes() { + textMessageAttributesCache.removeAllObjects() + attributedTextMessageAttributesCache.removeAllObjects() + emojiMessageAttributesCache.removeAllObjects() + photoMessageAttributesCache.removeAllObjects() + videoMessageAttributesCache.removeAllObjects() + locationMessageAttributesCache.removeAllObjects() + customMessageAttributesCache.removeAllObjects() + } + + public func removeAllCachedAttributes(for messageType: MessageData) { + let cache = attributeCache(for: messageType) + cache.removeAllObjects() + } + + public func removeCachedAttributes(for message: MessageType) { + let cache = attributeCache(for: message.data) + cache.removeObject(forKey: message.messageId as NSString) + } + + public func removeCachedAttributes(for messageId: String) { + textMessageAttributesCache.removeObject(forKey: messageId as NSString) + attributedTextMessageAttributesCache.removeObject(forKey: messageId as NSString) + emojiMessageAttributesCache.removeObject(forKey: messageId as NSString) + photoMessageAttributesCache.removeObject(forKey: messageId as NSString) + videoMessageAttributesCache.removeObject(forKey: messageId as NSString) + locationMessageAttributesCache.removeObject(forKey: messageId as NSString) + customMessageAttributesCache.removeObject(forKey: messageId as NSString) + } + + @objc + private func handleOrientationChange(_ notification: Notification) { + removeAllCachedAttributes() + invalidateLayout() + } + + // MARK: - Avatar Size + + internal func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { + let layout = layoutForMessage(message: message) + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + var position = isFromCurrentSender ? layout.outgoingAvatarPosition : layout.incomingAvatarPosition + + switch position.horizontal { + case .cellTrailing, .cellLeading: + break + case .natural: + position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading + } + return position + } + + internal func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layout = layoutForMessage(message: message) + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? layout.outgoingAvatarSize : layout.incomingAvatarSize + } + + // MARK: - Cell Top Label Sizing + + /// Returns the size of the `cellTopLabel` in a `MessageCollectionViewCell` + /// for a given `MessageType` and `IndexPath`. + /// - Parameters: + /// - message: The `MessageType` object for the cell. + /// - indexPath: The `IndexPath` for the cell. + open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) + guard let topLabelText = text else { return .zero } + let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) + return labelSize(for: topLabelText, considering: maxWidth) + } + + internal func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let layout = layoutForMessage(message: message) + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? layout.outgoingCellTopLabelAlignment : layout.incomingCellTopLabelAlignment + } + + internal func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let alignment = cellTopLabelAlignment(for: message, at: indexPath) + let position = avatarPosition(for: message, at: indexPath) + let avatarWidth = avatarSize(for: message, at: indexPath).width + let containerSize = messageContainerSize(for: message, at: indexPath) + let messagePadding = messageContainerPadding(for: message, at: indexPath) + + let avatarHorizontal = position.horizontal + let avatarVertical = position.vertical + + switch (alignment, avatarHorizontal) { + + case (.cellLeading, _), (.cellTrailing, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellTop ? width : width - avatarWidth + + case (.cellCenter, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) + + case (.messageTrailing, .cellLeading): + let width = containerSize.width + messagePadding.left - alignment.insets.horizontal + return avatarVertical == .cellTop ? width : width + avatarWidth + + case (.messageLeading, .cellTrailing): + let width = containerSize.width + messagePadding.right - alignment.insets.horizontal + return avatarVertical == .cellTop ? width : width + avatarWidth + + case (.messageLeading, .cellLeading): + return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal + + case (.messageTrailing, .cellTrailing): + return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal + + case (_, .natural): + fatalError(MessageKitError.avatarPositionUnresolved) + } + } + + // MARK: - Cell Bottom Label Sizing + + /// Returns the size of the `cellBottomLabel` in a `MessageCollectionViewCell` + /// for a given `MessageType` and `IndexPath`. + /// - Parameters: + /// - message: The `MessageType` object for the cell. + /// - indexPath: The `IndexPath` for the cell. + open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + guard let bottomLabelText = text else { return .zero } + let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) + return labelSize(for: bottomLabelText, considering: maxWidth) + } + + internal func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let layout = layoutForMessage(message: message) + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? layout.outgoingCellBottomLabelAlignment : layout.incomingCellBottomLabelAlignment + } + + internal func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + + let alignment = cellBottomLabelAlignment(for: message, at: indexPath) + let avatarWidth = avatarSize(for: message, at: indexPath).width + let containerSize = messageContainerSize(for: message, at: indexPath) + let messagePadding = messageContainerPadding(for: message, at: indexPath) + let position = avatarPosition(for: message, at: indexPath) + + let avatarHorizontal = position.horizontal + let avatarVertical = position.vertical + + switch (alignment, avatarHorizontal) { + + case (.cellLeading, _), (.cellTrailing, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellBottom ? width : width - avatarWidth + + case (.cellCenter, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) + + case (.messageTrailing, .cellLeading): + let width = containerSize.width + messagePadding.left - alignment.insets.horizontal + return avatarVertical == .cellBottom ? width : width + avatarWidth + + case (.messageLeading, .cellTrailing): + let width = containerSize.width + messagePadding.right - alignment.insets.horizontal + return avatarVertical == .cellBottom ? width : width + avatarWidth + + case (.messageLeading, .cellLeading): + return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal + + case (.messageTrailing, .cellTrailing): + return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal + + case (_, .natural): + fatalError(MessageKitError.avatarPositionUnresolved) + } + } + + // MARK: - Message Container Sizing + + /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` + /// for a given `MessageType` and `IndexPath`. + /// - Parameters: + /// - message: The `MessageType` object for the cell. + /// - indexPath: The `IndexPath` for the cell. + open func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + + var messageContainerSize: CGSize = .zero + + switch message.data { + case .text(let text): + let messageInsets = messageLabelInsets(for: message, at: indexPath) + let attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + case .attributedText(let text): + let messageInsets = messageLabelInsets(for: message, at: indexPath) + messageContainerSize = labelSize(for: text, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + case .emoji(let text): + let messageInsets = messageLabelInsets(for: message, at: indexPath) + let attributedText = NSAttributedString(string: text, attributes: [.font: emojiLabelFont]) + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + case .photo, .video: + let width = messagesLayoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + let height = messagesLayoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + messageContainerSize = CGSize(width: width, height: height) + case .location: + let width = messagesLayoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) + messageContainerSize = CGSize(width: width, height: height) + case .custom: + fatalError(MessageKitError.customDataUnresolvedSize) + } + + return messageContainerSize + } + + internal func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + guard let layout = layoutForMessage(message: message) as? TextMessageLayout else { + fatalError("Layout is not TextMessageLayout") + } + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? layout.outgoingMessageLabelInsets : layout.incomingMessageLabelInsets + } + + internal func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { + let layout = layoutForMessage(message: message) + let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? layout.outgoingMessagePadding : layout.incomingMessagePadding + } + + internal func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let avatarWidth = avatarSize(for: message, at: indexPath).width + let messagePadding = messageContainerPadding(for: message, at: indexPath) + + switch message.data { + case .text, .attributedText: + let messageInsets = messageLabelInsets(for: message, at: indexPath) + return itemWidth - avatarWidth - messagePadding.horizontal - messageInsets.horizontal + default: + return itemWidth - avatarWidth - messagePadding.horizontal + } + } + + // MARK: - Cell Sizing + + /// Returns the size of a `MessageCollectionViewCell` at a given `IndexPath` + /// considering all of the cell's content. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` for the cell. + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + let attributes = intermediateLayoutAttributes(for: message, at: indexPath) + return CGSize(width: itemWidth, height: attributes.itemHeight) + } } // MARK: - Helpers @@ -541,7 +543,6 @@ extension MessagesCollectionViewFlowLayout { } } - /// Convenience property for accessing the layout object's `MessagesCollectionView`. internal var messagesCollectionView: MessagesCollectionView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.layoutUsedOnForeignType) @@ -549,7 +550,6 @@ extension MessagesCollectionViewFlowLayout { return messagesCollectionView } - /// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesDataSource`. internal var messagesDataSource: MessagesDataSource { guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError(MessageKitError.nilMessagesDataSource) @@ -557,7 +557,6 @@ extension MessagesCollectionViewFlowLayout { return messagesDataSource } - /// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesLayoutDelegate`. internal var messagesLayoutDelegate: MessagesLayoutDelegate { guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) @@ -565,52 +564,3 @@ extension MessagesCollectionViewFlowLayout { return messagesLayoutDelegate } } - -class MessageIntermediateLayoutAttributes { - - var avatarSize: CGSize = .zero - var avatarPosition = AvatarPosition(vertical: .cellBottom) - - var messageContainerSize: CGSize = .zero - var messageContainerPadding: UIEdgeInsets = .zero - var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - var messageLabelInsets: UIEdgeInsets = .zero - - var topLabelAlignment = LabelAlignment.cellCenter(.zero) - var topLabelSize: CGSize = .zero - - var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) - var bottomLabelSize: CGSize = .zero - - lazy var itemHeight: CGFloat = { - let avatarVerticalPosition = avatarPosition.vertical - let avatarHeight = avatarSize.height - let messageContainerHeight = messageContainerSize.height - let bottomLabelHeight = bottomLabelSize.height - let topLabelHeight = topLabelSize.height - let messageVerticalPadding = messageContainerPadding.vertical - - var cellHeight: CGFloat = 0 - - switch avatarVerticalPosition { - case .cellTop: - cellHeight += max(avatarHeight, topLabelHeight) - cellHeight += bottomLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .cellBottom: - cellHeight += max(avatarHeight, bottomLabelHeight) - cellHeight += topLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .messageTop, .messageCenter, .messageBottom: - cellHeight += max(avatarHeight, messageContainerHeight) - cellHeight += messageVerticalPadding - cellHeight += topLabelHeight - cellHeight += bottomLabelHeight - } - - return cellHeight - }() - -} From 3c3b0a052fc815ac27f3c2cb89ab7e3f135e6400 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Mon, 19 Mar 2018 02:32:30 -0500 Subject: [PATCH 17/70] Add back shouldCacheAttributes --- .../MessagesCollectionViewFlowLayout.swift | 81 +++++++++++-------- .../Protocols/MessagesLayoutDelegate.swift | 11 +++ .../Supporting/MessageKit+Availability.swift | 5 -- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index cec7b7d4..60e32291 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -137,49 +137,47 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { private func intermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { let cache = attributeCache(for: message.data) - let cachedAttributes = cache.object(forKey: message.messageId as NSString) + guard let cachedAttributes = cache.object(forKey: message.messageId as NSString) else { + let attributes = MessageIntermediateLayoutAttributes() - return cachedAttributes ?? createIntermediateLayoutAttributes(for: message, at: indexPath) - } + attributes.avatarSize = avatarSize(for: message, at: indexPath) + attributes.avatarPosition = avatarPosition(for: message, at: indexPath) - private func createIntermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { + attributes.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) + attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) - let attributes = MessageIntermediateLayoutAttributes() + attributes.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) + attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) - attributes.avatarSize = avatarSize(for: message, at: indexPath) - attributes.avatarPosition = avatarPosition(for: message, at: indexPath) + attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: message, at: indexPath) + attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) - attributes.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) - attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) + switch message.data { + case .emoji: + attributes.messageLabelFont = emojiLabelFont + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) + case .text: + attributes.messageLabelFont = messageLabelFont + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) + case .attributedText(let text): + attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) + attributes.messageLabelFont = messageLabelFont + guard !text.string.isEmpty else { return attributes } + guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return attributes } + attributes.messageLabelFont = font + default: + break + } - attributes.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) - attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) + if messagesLayoutDelegate.shouldCacheLayoutAttributes(for: message) { + let cache = attributeCache(for: message.data) + cache.setObject(attributes, forKey: message.messageId as NSString) + } - attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: message, at: indexPath) - attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) - - switch message.data { - case .emoji: - attributes.messageLabelFont = emojiLabelFont - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - case .text: - attributes.messageLabelFont = messageLabelFont - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - case .attributedText(let text): - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - attributes.messageLabelFont = messageLabelFont - guard !text.string.isEmpty else { return attributes } - guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return attributes } - attributes.messageLabelFont = font - default: - break + return attributes } - let cache = attributeCache(for: message.data) - cache.setObject(attributes, forKey: message.messageId as NSString) - - return attributes - + return cachedAttributes } private func configure(attributes: MessagesCollectionViewLayoutAttributes) { @@ -454,12 +452,25 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) messageContainerSize = CGSize(width: width, height: height) case .custom: - fatalError(MessageKitError.customDataUnresolvedSize) + return customMessageContainerSize(for: message, at: indexPath) } return messageContainerSize } + /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` + /// for a given `IndexPath` and `MessageType` where the `MessageData` case is `.custom(Any?)`. + /// + /// - Note: + /// Override this method to size the `MessageContainerView` for messages with `.custom` `MessageData`. + /// The default implementation of this method returns `CGSize.zero`. + /// - Parameters: + /// - message: The `MessageType` object for the cell. + /// - indexPath: The `IndexPath` for the cell. + open func customMessageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + return .zero + } + internal func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { guard let layout = layoutForMessage(message: message) as? TextMessageLayout else { fatalError("Layout is not TextMessageLayout") diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 8b74f4fb..4c6464f1 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -96,6 +96,13 @@ public protocol MessagesLayoutDelegate: AnyObject { /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat + /// Specifies whether the layout attributes for a given `MessageType` + /// should be cached by the `MessagesCollectionViewFlowLayout` object. + /// - Parameters: + /// - message: The `MessageType` whose attributes to cache. + /// The default value returned by this method is `false`. + func shouldCacheLayoutAttributes(for message: MessageType) -> Bool + } public extension MessagesLayoutDelegate { @@ -114,6 +121,10 @@ public extension MessagesLayoutDelegate { return .zero } + func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { + return false + } + // MARK: - Media Messages Defaults func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index 347e0f85..e55b603b 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -55,9 +55,4 @@ public extension MessagesLayoutDelegate { func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellBottomLabelAlignment and outgoingCellBottomLabelAlignment properties on MessagesCollectionViewFlowLayout.") } - - @available(*, deprecated: 0.14.0, message: "Removed in MessageKit 0.14.0. MessageKit no longer caches cell attributes as it leads to undefined behavior.") - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { - fatalError("shouldCacheLayoutAttributes is removed in MessageKit 0.14.0. This deprecation does not require a replacement method.") - } } From 1432c02278129693dc56cbf4eeb0e1dbd1fcb090 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 21 Mar 2018 01:15:43 -0500 Subject: [PATCH 18/70] Handle layout logic using CellSizeCalculator --- Sources/Layout/MessageCellLayout.swift | 393 +++++++++++++-- .../MessageIntermediateLayoutAttributes.swift | 2 +- .../MessagesCollectionViewFlowLayout.swift | 469 ++---------------- 3 files changed, 390 insertions(+), 474 deletions(-) diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageCellLayout.swift index ac1bdf0d..c55eb638 100644 --- a/Sources/Layout/MessageCellLayout.swift +++ b/Sources/Layout/MessageCellLayout.swift @@ -24,26 +24,25 @@ import UIKit -public protocol MessageCellLayout { +open class CellSizeCalculator { - var incomingAvatarSize: CGSize { get set } - var outgoingAvatarSize: CGSize { get set } + open weak var layout: UICollectionViewFlowLayout? - var incomingAvatarPosition: AvatarPosition { get set } - var outgoingAvatarPosition: AvatarPosition { get set } + public init(layout: UICollectionViewFlowLayout? = nil) { + self.layout = layout + } - var incomingMessagePadding: UIEdgeInsets { get set } - var outgoingMessagePadding: UIEdgeInsets { get set } + open func configure(attributes: UICollectionViewLayoutAttributes) { + // No-op default + } - var incomingCellTopLabelAlignment: LabelAlignment { get set } - var outgoingCellTopLabelAlignment: LabelAlignment { get set } - - var incomingCellBottomLabelAlignment: LabelAlignment { get set } - var outgoingCellBottomLabelAlignment: LabelAlignment { get set } + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + return .zero + } } -public struct TextMessageLayout: MessageCellLayout { +open class MessageSizeCalculator: CellSizeCalculator { public var incomingAvatarSize = CGSize(width: 30, height: 30) public var outgoingAvatarSize = CGSize(width: 30, height: 30) @@ -51,35 +50,361 @@ public struct TextMessageLayout: MessageCellLayout { public var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) public var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + + public var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) + public var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + + public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) + public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + + internal var messagesLayout: MessagesCollectionViewFlowLayout { + guard let layout = layout as? MessagesCollectionViewFlowLayout else { + fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") + } + return layout + } + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + attributes.avatarSize = avatarSize(for: message) + attributes.avatarPosition = avatarPosition(for: message) + + attributes.messageContainerPadding = messageContainerPadding(for: message) + attributes.messageContainerSize = messageContainerSize(for: message) + attributes.topLabelAlignment = cellTopLabelAlignment(for: message) + attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) + + attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: message) + attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + } + + open override func sizeForItem(at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + let itemHeight = cellContentHeight(for: message, at: indexPath) + return CGSize(width: messagesLayout.itemWidth, height: itemHeight) + } + + internal func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + + let avatarVerticalPosition = avatarPosition(for: message).vertical + let avatarHeight = avatarSize(for: message).height + let messageContainerHeight = messageContainerSize(for: message).height + let bottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height + let topLabelHeight = cellTopLabelSize(for: message, at: indexPath).height + let messageVerticalPadding = messageContainerPadding(for: message).vertical + + var cellHeight: CGFloat = 0 + + switch avatarVerticalPosition { + case .cellTop: + cellHeight += max(avatarHeight, topLabelHeight) + cellHeight += bottomLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .cellBottom: + cellHeight += max(avatarHeight, bottomLabelHeight) + cellHeight += topLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .messageTop, .messageCenter, .messageBottom: + cellHeight += max(avatarHeight, messageContainerHeight) + cellHeight += messageVerticalPadding + cellHeight += topLabelHeight + cellHeight += bottomLabelHeight + } + + return cellHeight + } + + // MARK: - Avatar + + internal func avatarPosition(for message: MessageType) -> AvatarPosition { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition + + switch position.horizontal { + case .cellTrailing, .cellLeading: + break + case .natural: + position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading + } + return position + } + + internal func avatarSize(for message: MessageType) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize + } + + // MARK: - Top Label + + internal func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let text = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) + guard let topLabelText = text else { return .zero } + let maxWidth = cellTopLabelMaxWidth(for: message) + return labelSize(for: topLabelText, considering: maxWidth) + } + + internal func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment + } + + internal func cellTopLabelMaxWidth(for message: MessageType) -> CGFloat { + let alignment = cellTopLabelAlignment(for: message) + let position = avatarPosition(for: message) + let avatarWidth = avatarSize(for: message).width + let containerSize = messageContainerSize(for: message) + let messagePadding = messageContainerPadding(for: message) + + let avatarHorizontal = position.horizontal + let avatarVertical = position.vertical + + let itemWidth = messagesLayout.itemWidth + + switch (alignment, avatarHorizontal) { + + case (.cellLeading, _), (.cellTrailing, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellTop ? width : width - avatarWidth + + case (.cellCenter, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) + + case (.messageTrailing, .cellLeading): + let width = containerSize.width + messagePadding.left - alignment.insets.horizontal + return avatarVertical == .cellTop ? width : width + avatarWidth + + case (.messageLeading, .cellTrailing): + let width = containerSize.width + messagePadding.right - alignment.insets.horizontal + return avatarVertical == .cellTop ? width : width + avatarWidth + + case (.messageLeading, .cellLeading): + return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal + + case (.messageTrailing, .cellTrailing): + return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal + + case (_, .natural): + fatalError(MessageKitError.avatarPositionUnresolved) + } + } + + // MARK: - Bottom Label + + open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let text = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + guard let bottomLabelText = text else { return .zero } + let maxWidth = cellBottomLabelMaxWidth(for: message) + return labelSize(for: bottomLabelText, considering: maxWidth) + } + + internal func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment + } + + internal func cellBottomLabelMaxWidth(for message: MessageType) -> CGFloat { + + let alignment = cellBottomLabelAlignment(for: message) + let avatarWidth = avatarSize(for: message).width + let containerSize = messageContainerSize(for: message) + let messagePadding = messageContainerPadding(for: message) + let position = avatarPosition(for: message) + + let avatarHorizontal = position.horizontal + let avatarVertical = position.vertical + + let itemWidth = messagesLayout.itemWidth + + switch (alignment, avatarHorizontal) { + + case (.cellLeading, _), (.cellTrailing, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellBottom ? width : width - avatarWidth + + case (.cellCenter, _): + let width = itemWidth - alignment.insets.horizontal + return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) + + case (.messageTrailing, .cellLeading): + let width = containerSize.width + messagePadding.left - alignment.insets.horizontal + return avatarVertical == .cellBottom ? width : width + avatarWidth + + case (.messageLeading, .cellTrailing): + let width = containerSize.width + messagePadding.right - alignment.insets.horizontal + return avatarVertical == .cellBottom ? width : width + avatarWidth + + case (.messageLeading, .cellLeading): + return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal + + case (.messageTrailing, .cellTrailing): + return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal + + case (_, .natural): + fatalError(MessageKitError.avatarPositionUnresolved) + } + } + + // MARK: - MessageContainer + + internal func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding + } + + open func messageContainerSize(for message: MessageType) -> CGSize { + // Returns .zero by default + return .zero + } + + internal func messageContainerMaxWidth(for message: MessageType) -> CGFloat { + let avatarWidth = avatarSize(for: message).width + let messagePadding = messageContainerPadding(for: message) + return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal + } + + internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + let estimatedHeight = attributedText.height(considering: maxWidth) + let estimatedWidth = attributedText.width(considering: estimatedHeight) + + let finalHeight = estimatedHeight.rounded(.up) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) + + return CGSize(width: finalWidth, height: finalHeight) + } +} + +open class TextMessageSizeCalculator: MessageSizeCalculator { + public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) { + didSet { + emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) + } + } - public var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) - public var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + internal var emojiLabelFont: UIFont - public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) - public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + public init() { + emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) + } + internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets + } + + internal override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { + let maxWidth = super.messageContainerMaxWidth(for: message) + let textInsets = messageLabelInsets(for: message) + return maxWidth - textInsets.horizontal + } + + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + + var messageContainerSize: CGSize + let attributedText: NSAttributedString + + switch message.data { + case .text(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + case .attributedText(let text): + attributedText = text + case .emoji(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: emojiLabelFont]) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + + let messageInsets = messageLabelInsets(for: message) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + + return messageContainerSize + } + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + attributes.messageLabelInsets = messageLabelInsets(for: message) + attributes.messageLabelFont = messageLabelFont + + switch message.data { + case .attributedText(let text): + guard !text.string.isEmpty else { return } + guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } + attributes.messageLabelFont = font + case .emoji: + attributes.messageLabelFont = emojiLabelFont + default: + break + } + } } -public struct MediaMessageLayout: MessageCellLayout { +open class MediaMessageSizeCalculator: MessageSizeCalculator { - public var incomingAvatarSize = CGSize(width: 30, height: 30) - public var outgoingAvatarSize = CGSize(width: 30, height: 30) - - public var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) - public var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) - - public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - - public var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) - public var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) - - public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) - public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + // TODO: - Figure out how we are going to size these + public var incomingMediaSize: CGSize = .zero + public var outgoingMediaSize: CGSize = .zero + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + switch message.data { + case .photo(_): + // TODO: - Implement actual sizing + return CGSize(width: maxWidth, height: 240) + case .video(_, _): + // TODO: - Implement actual sizing + return CGSize(width: maxWidth, height: 240) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + } } + +open class LocationMessageSizeCalculator: MessageSizeCalculator { + + // TODO: - Figure out how we are going to size these + public var incomingLocationSize: CGSize = .zero + public var outgoingLocationSize: CGSize = .zero + + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + switch message.data { + case .location(_): + // TODO: - Implement actual sizing + return CGSize(width: maxWidth, height: 240) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + } +} \ No newline at end of file diff --git a/Sources/Layout/MessageIntermediateLayoutAttributes.swift b/Sources/Layout/MessageIntermediateLayoutAttributes.swift index 73b6347d..f0cc3fec 100644 --- a/Sources/Layout/MessageIntermediateLayoutAttributes.swift +++ b/Sources/Layout/MessageIntermediateLayoutAttributes.swift @@ -24,7 +24,7 @@ import Foundation -class MessageIntermediateLayoutAttributes { +public class MessageIntermediateLayoutAttributes { var avatarSize: CGSize = .zero var avatarPosition = AvatarPosition(vertical: .cellBottom) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 60e32291..43089679 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -38,67 +38,16 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return collectionView.frame.width - sectionInset.left - sectionInset.right } - /// The font to be used by `TextMessageCell` for `MessageData.text(String)` case. - /// - /// Note: The default value of this property is `UIFont.preferredFont(forTextStyle: .body)` - open var messageLabelFont: UIFont { - didSet { - emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) - textMessageAttributesCache.removeAllObjects() - attributedTextMessageAttributesCache.removeAllObjects() - emojiMessageAttributesCache.removeAllObjects() - } - } - - internal var emojiLabelFont: UIFont - - // MARK: - Layout - - open var textMessageLayout = TextMessageLayout() { - didSet { textMessageAttributesCache.removeAllObjects() } - } - - open var attributedTextMessageLayout = TextMessageLayout() { - didSet { attributedTextMessageAttributesCache.removeAllObjects() } - } - - open var emojiMessageLayout = TextMessageLayout() { - didSet { emojiMessageAttributesCache.removeAllObjects() } - } - - open var photoMessageLayout = MediaMessageLayout() { - didSet { photoMessageAttributesCache.removeAllObjects() } - } - - open var videoMessageLayout = MediaMessageLayout() { - didSet { videoMessageAttributesCache.removeAllObjects() } - } - - open var locationMessageLayout = MediaMessageLayout() { - didSet { locationMessageAttributesCache.removeAllObjects() } - } - - open var customMessageLayout: MessageCellLayout? { - didSet { customMessageAttributesCache.removeAllObjects() } - } - - private let textMessageAttributesCache = NSCache() - private let attributedTextMessageAttributesCache = NSCache() - private let emojiMessageAttributesCache = NSCache() - private let photoMessageAttributesCache = NSCache() - private let videoMessageAttributesCache = NSCache() - private let locationMessageAttributesCache = NSCache() - private let customMessageAttributesCache = NSCache() // MARK: - Initializers public override init() { - - messageLabelFont = UIFont.preferredFont(forTextStyle: .body) - emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) - super.init() + mediaMessageSizeCalculator.layout = self + textMessageSizeCalculator.layout = self + locationMessageSizeCalculator.layout = self + sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: .UIDeviceOrientationDidChange, object: nil) @@ -119,7 +68,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return nil } for attributes in attributesArray where attributes.representedElementCategory == .cell { - configure(attributes: attributes) + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) } return attributesArray } @@ -129,94 +79,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return nil } if attributes.representedElementCategory == .cell { - configure(attributes: attributes) + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) } return attributes } - private func intermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { - - let cache = attributeCache(for: message.data) - guard let cachedAttributes = cache.object(forKey: message.messageId as NSString) else { - let attributes = MessageIntermediateLayoutAttributes() - - attributes.avatarSize = avatarSize(for: message, at: indexPath) - attributes.avatarPosition = avatarPosition(for: message, at: indexPath) - - attributes.messageContainerPadding = messageContainerPadding(for: message, at: indexPath) - attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) - - attributes.topLabelAlignment = cellTopLabelAlignment(for: message, at: indexPath) - attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) - - attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: message, at: indexPath) - attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) - - switch message.data { - case .emoji: - attributes.messageLabelFont = emojiLabelFont - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - case .text: - attributes.messageLabelFont = messageLabelFont - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - case .attributedText(let text): - attributes.messageLabelInsets = messageLabelInsets(for: message, at: indexPath) - attributes.messageLabelFont = messageLabelFont - guard !text.string.isEmpty else { return attributes } - guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return attributes } - attributes.messageLabelFont = font - default: - break - } - - if messagesLayoutDelegate.shouldCacheLayoutAttributes(for: message) { - let cache = attributeCache(for: message.data) - cache.setObject(attributes, forKey: message.messageId as NSString) - } - - return attributes - } - - return cachedAttributes - } - - private func configure(attributes: MessagesCollectionViewLayoutAttributes) { - - let indexPath = attributes.indexPath - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - let intermediateAttributes = intermediateLayoutAttributes(for: message, at: indexPath) - - attributes.avatarSize = intermediateAttributes.avatarSize - attributes.avatarPosition = intermediateAttributes.avatarPosition - - attributes.messageContainerPadding = intermediateAttributes.messageContainerPadding - attributes.messageContainerSize = intermediateAttributes.messageContainerSize - - attributes.topLabelAlignment = intermediateAttributes.topLabelAlignment - attributes.topLabelSize = intermediateAttributes.topLabelSize - - attributes.bottomLabelAlignment = intermediateAttributes.bottomLabelAlignment - attributes.bottomLabelSize = intermediateAttributes.bottomLabelSize - - switch message.data { - case .emoji: - attributes.messageLabelFont = emojiLabelFont - attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets - case .text: - attributes.messageLabelFont = messageLabelFont - attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets - case .attributedText(let text): - attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets - attributes.messageLabelFont = intermediateAttributes.messageLabelFont - guard !text.string.isEmpty else { return } - guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } - attributes.messageLabelFont = font - default: - break - } - } - // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { @@ -235,35 +103,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return flowLayoutContext } - /// Removes the cached layout information for all `MessageType`s. + // TODO: Invalidate attributes public func removeAllCachedAttributes() { - textMessageAttributesCache.removeAllObjects() - attributedTextMessageAttributesCache.removeAllObjects() - emojiMessageAttributesCache.removeAllObjects() - photoMessageAttributesCache.removeAllObjects() - videoMessageAttributesCache.removeAllObjects() - locationMessageAttributesCache.removeAllObjects() - customMessageAttributesCache.removeAllObjects() - } - public func removeAllCachedAttributes(for messageType: MessageData) { - let cache = attributeCache(for: messageType) - cache.removeAllObjects() - } - - public func removeCachedAttributes(for message: MessageType) { - let cache = attributeCache(for: message.data) - cache.removeObject(forKey: message.messageId as NSString) - } - - public func removeCachedAttributes(for messageId: String) { - textMessageAttributesCache.removeObject(forKey: messageId as NSString) - attributedTextMessageAttributesCache.removeObject(forKey: messageId as NSString) - emojiMessageAttributesCache.removeObject(forKey: messageId as NSString) - photoMessageAttributesCache.removeObject(forKey: messageId as NSString) - videoMessageAttributesCache.removeObject(forKey: messageId as NSString) - locationMessageAttributesCache.removeObject(forKey: messageId as NSString) - customMessageAttributesCache.removeObject(forKey: messageId as NSString) } @objc @@ -272,243 +114,29 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { invalidateLayout() } - // MARK: - Avatar Size - - internal func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - var position = isFromCurrentSender ? layout.outgoingAvatarPosition : layout.incomingAvatarPosition - - switch position.horizontal { - case .cellTrailing, .cellLeading: - break - case .natural: - position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading - } - return position - } - - internal func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingAvatarSize : layout.incomingAvatarSize - } - - // MARK: - Cell Top Label Sizing - - /// Returns the size of the `cellTopLabel` in a `MessageCollectionViewCell` - /// for a given `MessageType` and `IndexPath`. - /// - Parameters: - /// - message: The `MessageType` object for the cell. - /// - indexPath: The `IndexPath` for the cell. - open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) - guard let topLabelText = text else { return .zero } - let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: topLabelText, considering: maxWidth) - } - - internal func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingCellTopLabelAlignment : layout.incomingCellTopLabelAlignment - } - - internal func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let alignment = cellTopLabelAlignment(for: message, at: indexPath) - let position = avatarPosition(for: message, at: indexPath) - let avatarWidth = avatarSize(for: message, at: indexPath).width - let containerSize = messageContainerSize(for: message, at: indexPath) - let messagePadding = messageContainerPadding(for: message, at: indexPath) - - let avatarHorizontal = position.horizontal - let avatarVertical = position.vertical - - switch (alignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = containerSize.width + messagePadding.left - alignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = containerSize.width + messagePadding.right - alignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - - // MARK: - Cell Bottom Label Sizing - - /// Returns the size of the `cellBottomLabel` in a `MessageCollectionViewCell` - /// for a given `MessageType` and `IndexPath`. - /// - Parameters: - /// - message: The `MessageType` object for the cell. - /// - indexPath: The `IndexPath` for the cell. - open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - guard let bottomLabelText = text else { return .zero } - let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: bottomLabelText, considering: maxWidth) - } - - internal func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingCellBottomLabelAlignment : layout.incomingCellBottomLabelAlignment - } - - internal func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - let alignment = cellBottomLabelAlignment(for: message, at: indexPath) - let avatarWidth = avatarSize(for: message, at: indexPath).width - let containerSize = messageContainerSize(for: message, at: indexPath) - let messagePadding = messageContainerPadding(for: message, at: indexPath) - let position = avatarPosition(for: message, at: indexPath) - - let avatarHorizontal = position.horizontal - let avatarVertical = position.vertical - - switch (alignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = containerSize.width + messagePadding.left - alignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = containerSize.width + messagePadding.right - alignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - - // MARK: - Message Container Sizing - - /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for a given `MessageType` and `IndexPath`. - /// - Parameters: - /// - message: The `MessageType` object for the cell. - /// - indexPath: The `IndexPath` for the cell. - open func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - - var messageContainerSize: CGSize = .zero - - switch message.data { - case .text(let text): - let messageInsets = messageLabelInsets(for: message, at: indexPath) - let attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - case .attributedText(let text): - let messageInsets = messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - case .emoji(let text): - let messageInsets = messageLabelInsets(for: message, at: indexPath) - let attributedText = NSAttributedString(string: text, attributes: [.font: emojiLabelFont]) - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - case .photo, .video: - let width = messagesLayoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .location: - let width = messagesLayoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .custom: - return customMessageContainerSize(for: message, at: indexPath) - } - - return messageContainerSize - } - - /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for a given `IndexPath` and `MessageType` where the `MessageData` case is `.custom(Any?)`. - /// - /// - Note: - /// Override this method to size the `MessageContainerView` for messages with `.custom` `MessageData`. - /// The default implementation of this method returns `CGSize.zero`. - /// - Parameters: - /// - message: The `MessageType` object for the cell. - /// - indexPath: The `IndexPath` for the cell. - open func customMessageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - return .zero - } - - internal func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let layout = layoutForMessage(message: message) as? TextMessageLayout else { - fatalError("Layout is not TextMessageLayout") - } - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingMessageLabelInsets : layout.incomingMessageLabelInsets - } - - internal func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - let layout = layoutForMessage(message: message) - let isFromCurrentSender = messagesDataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? layout.outgoingMessagePadding : layout.incomingMessagePadding - } - - internal func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarWidth = avatarSize(for: message, at: indexPath).width - let messagePadding = messageContainerPadding(for: message, at: indexPath) - - switch message.data { - case .text, .attributedText: - let messageInsets = messageLabelInsets(for: message, at: indexPath) - return itemWidth - avatarWidth - messagePadding.horizontal - messageInsets.horizontal - default: - return itemWidth - avatarWidth - messagePadding.horizontal - } - } - // MARK: - Cell Sizing - /// Returns the size of a `MessageCollectionViewCell` at a given `IndexPath` - /// considering all of the cell's content. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` for the cell. - open func sizeForItem(at indexPath: IndexPath) -> CGSize { + var textMessageSizeCalculator = TextMessageSizeCalculator() + var mediaMessageSizeCalculator = MediaMessageSizeCalculator() + var locationMessageSizeCalculator = LocationMessageSizeCalculator() + + open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> MessageSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let attributes = intermediateLayoutAttributes(for: message, at: indexPath) - return CGSize(width: itemWidth, height: attributes.itemHeight) + switch message.data { + case .text, .attributedText, .emoji: + return textMessageSizeCalculator + case .photo, .video: + return mediaMessageSizeCalculator + case .location: + return locationMessageSizeCalculator + case .custom: + fatalError("") + } + } + + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let calculator = cellSizeCalculatorForItem(at: indexPath) + return calculator.sizeForItem(at:indexPath) } } @@ -516,44 +144,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { - internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { - let estimatedHeight = attributedText.height(considering: maxWidth) - let estimatedWidth = attributedText.width(considering: estimatedHeight) - - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - - return CGSize(width: finalWidth, height: finalHeight) - } - - internal func attributeCache(for messageData: MessageData) -> NSCache { - switch messageData { - case .text: return textMessageAttributesCache - case .attributedText: return attributedTextMessageAttributesCache - case .emoji: return emojiMessageAttributesCache - case .photo: return photoMessageAttributesCache - case .video: return videoMessageAttributesCache - case .location: return locationMessageAttributesCache - case .custom: return customMessageAttributesCache - } - } - - internal func layoutForMessage(message: MessageType) -> MessageCellLayout { - switch message.data { - case .text: return textMessageLayout - case .attributedText: return attributedTextMessageLayout - case .emoji: return emojiMessageLayout - case .photo: return photoMessageLayout - case .video: return videoMessageLayout - case .location: return locationMessageLayout - case .custom: - guard let customLayout = customMessageLayout else { - fatalError("Missing MessageCellLayout for MessageData.custom. Please set customMessageLayout") - } - return customLayout - } - } - internal var messagesCollectionView: MessagesCollectionView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.layoutUsedOnForeignType) @@ -568,6 +158,7 @@ extension MessagesCollectionViewFlowLayout { return messagesDataSource } + // TODO: - This will become unused internal var messagesLayoutDelegate: MessagesLayoutDelegate { guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) From 5707a3441f3af4ba8645bf3b9497f61ed3c760c9 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Fri, 23 Mar 2018 02:19:54 -0500 Subject: [PATCH 19/70] Add MediaItem protocol --- MessageKit.xcodeproj/project.pbxproj | 12 ++++-- Sources/Layout/MessageCellLayout.swift | 18 ++++---- Sources/Models/MessageData.swift | 4 +- Sources/Protocols/MediaItem.swift | 37 +++++++++++++++++ .../Protocols/MessagesLayoutDelegate.swift | 41 ------------------- Tests/Mocks/MockMessage.swift | 1 + Tests/Mocks/MockMessagesDataSource.swift | 1 + 7 files changed, 56 insertions(+), 58 deletions(-) create mode 100644 Sources/Protocols/MediaItem.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 5276c865..6724fd99 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -21,6 +21,9 @@ 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; 1FD5895C205F5B8F004B5081 /* MessageCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */; }; 1FD5895E205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */; }; + 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895F2064E08A004B5081 /* MediaItem.swift */; }; + 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; + 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 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 */; }; @@ -38,8 +41,6 @@ 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */; }; 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC751F87AB230030B058 /* MessageStyleTests.swift */; }; 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC811F87AB230030B058 /* MessagesDisplayDelegateTests.swift */; }; - 8962AC9A1F87ABB20030B058 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; - 8962AC9C1F87ABC60030B058 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */; }; B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */; }; B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */; }; @@ -123,6 +124,7 @@ 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayout.swift; sourceTree = ""; }; 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIntermediateLayoutAttributes.swift; sourceTree = ""; }; + 1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; @@ -422,6 +424,7 @@ B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */, B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */, B7A03F511F8669C9006AEF79 /* MessageType.swift */, + 1FD5895F2064E08A004B5081 /* MediaItem.swift */, ); path = Protocols; sourceTree = ""; @@ -611,10 +614,9 @@ B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */, B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */, B7A03F261F866895006AEF79 /* MessageKitDateFormatter.swift in Sources */, - 8962AC9A1F87ABB20030B058 /* MockMessagesDataSource.swift in Sources */, B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */, B7A03F6A1F8669EB006AEF79 /* UICollectionView+Extensions.swift in Sources */, - 8962AC9C1F87ABC60030B058 /* MockMessage.swift in Sources */, + 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */, B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */, B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */, B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */, @@ -653,12 +655,14 @@ 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */, 1F066E211FDA3DEA00E11013 /* AvatarSpec.swift in Sources */, 8962AC901F87AB7D0030B058 /* MessageDateHeaderViewTests.swift in Sources */, + 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */, 8962AC8F1F87AB7D0030B058 /* MessageInputBarTests.swift in Sources */, 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */, 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */, 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */, 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */, 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */, + 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */, 8962AC8B1F87AB7D0030B058 /* InputBarItemTests.swift in Sources */, 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */, ); diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageCellLayout.swift index c55eb638..5f5caf5c 100644 --- a/Sources/Layout/MessageCellLayout.swift +++ b/Sources/Layout/MessageCellLayout.swift @@ -372,19 +372,15 @@ open class TextMessageSizeCalculator: MessageSizeCalculator { open class MediaMessageSizeCalculator: MessageSizeCalculator { - // TODO: - Figure out how we are going to size these - public var incomingMediaSize: CGSize = .zero - public var outgoingMediaSize: CGSize = .zero - open override func messageContainerSize(for message: MessageType) -> CGSize { let maxWidth = messageContainerMaxWidth(for: message) switch message.data { - case .photo(_): - // TODO: - Implement actual sizing - return CGSize(width: maxWidth, height: 240) - case .video(_, _): - // TODO: - Implement actual sizing - return CGSize(width: maxWidth, height: 240) + case .photo(let item): + let width = min(item.size.width, maxWidth) + return CGSize(width: width, height: item.size.height) + case .video(let item): + let width = min(item.size.width, maxWidth) + return CGSize(width: width, height: item.size.height) default: fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") } @@ -407,4 +403,4 @@ open class LocationMessageSizeCalculator: MessageSizeCalculator { fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") } } -} \ No newline at end of file +} diff --git a/Sources/Models/MessageData.swift b/Sources/Models/MessageData.swift index 2bb72bae..b229742c 100644 --- a/Sources/Models/MessageData.swift +++ b/Sources/Models/MessageData.swift @@ -41,10 +41,10 @@ public enum MessageData { case attributedText(NSAttributedString) /// A photo message. - case photo(UIImage) + case photo(MediaItem) /// A video message. - case video(file: URL, thumbnail: UIImage) + case video(MediaItem) /// A location message. case location(CLLocation) diff --git a/Sources/Protocols/MediaItem.swift b/Sources/Protocols/MediaItem.swift new file mode 100644 index 00000000..68d0c2de --- /dev/null +++ b/Sources/Protocols/MediaItem.swift @@ -0,0 +1,37 @@ +/* + 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 Foundation + +public protocol MediaItem { + + var url: URL? { get set } + + var image: UIImage? { get set } + + var placeholderImage: UIImage { get set } + + var size: CGSize { get set } + +} diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 4c6464f1..97d7d845 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -49,31 +49,6 @@ public protocol MessagesLayoutDelegate: AnyObject { /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - // MARK: - Media Messages - - /// Specifies the width for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is the `maxWidth`. - func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies the height for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method uses `AVMakeRect(aspectRatio:insideRect:)` with a bounding - /// rect using the `maxWidth` and `.greatestFiniteMagnitude` for the height. - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - // MARK: - Location Messages /// Specifies the width for a `MessageContainerView`. @@ -125,22 +100,6 @@ public extension MessagesLayoutDelegate { return false } - // MARK: - Media Messages Defaults - - func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return maxWidth - } - - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - switch message.data { - case .photo(let image), .video(_, let image): - let boundingRect = CGRect(origin: .zero, size: CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) - return AVMakeRect(aspectRatio: image.size, insideRect: boundingRect).height - default: - return 0 - } - } - // MARK: - Location Messages Defaults func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { diff --git a/Tests/Mocks/MockMessage.swift b/Tests/Mocks/MockMessage.swift index a3e0dc32..70f747f2 100644 --- a/Tests/Mocks/MockMessage.swift +++ b/Tests/Mocks/MockMessage.swift @@ -24,6 +24,7 @@ import Foundation import CoreLocation +@testable import MessageKit struct MockMessage: MessageType { diff --git a/Tests/Mocks/MockMessagesDataSource.swift b/Tests/Mocks/MockMessagesDataSource.swift index 31e4a0cf..9af1596c 100644 --- a/Tests/Mocks/MockMessagesDataSource.swift +++ b/Tests/Mocks/MockMessagesDataSource.swift @@ -23,6 +23,7 @@ */ import Foundation +@testable import MessageKit class MockMessagesDataSource: MessagesDataSource { From d342bc4b564b12a8acde45d043754b22be63e972 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Fri, 23 Mar 2018 23:32:53 -0500 Subject: [PATCH 20/70] Add LocationItem protocol --- MessageKit.xcodeproj/project.pbxproj | 4 +++ .../Controllers/MessagesViewController.swift | 4 +-- Sources/Layout/MessageCellLayout.swift | 31 ++++++++++------- .../MessagesCollectionViewFlowLayout.swift | 21 +----------- Sources/Models/MessageData.swift | 2 +- Sources/Protocols/LocationItem.swift | 33 +++++++++++++++++++ Sources/Protocols/MessageType.swift | 3 -- Sources/Views/Cells/LocationMessageCell.swift | 6 ++-- Sources/Views/Cells/MediaMessageCell.swift | 8 ++--- 9 files changed, 67 insertions(+), 45 deletions(-) create mode 100644 Sources/Protocols/LocationItem.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 6724fd99..f316b204 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895F2064E08A004B5081 /* MediaItem.swift */; }; 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; + 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5896320660C1C004B5081 /* LocationItem.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 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 */; }; @@ -125,6 +126,7 @@ 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayout.swift; sourceTree = ""; }; 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIntermediateLayoutAttributes.swift; sourceTree = ""; }; 1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; + 1FD5896320660C1C004B5081 /* LocationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItem.swift; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; @@ -425,6 +427,7 @@ B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */, B7A03F511F8669C9006AEF79 /* MessageType.swift */, 1FD5895F2064E08A004B5081 /* MediaItem.swift */, + 1FD5896320660C1C004B5081 /* LocationItem.swift */, ); path = Protocols; sourceTree = ""; @@ -622,6 +625,7 @@ B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */, B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */, B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */, + 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */, B7A03F291F866895006AEF79 /* Sender.swift in Sources */, B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */, B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 4f4f9ad5..45c3ba0d 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -298,8 +298,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { pasteBoard.string = text case .attributedText(let attributedText): pasteBoard.string = attributedText.string - case .photo(let image): - pasteBoard.image = image + case .photo(let mediaItem): + pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage default: break } diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageCellLayout.swift index 5f5caf5c..f806e749 100644 --- a/Sources/Layout/MessageCellLayout.swift +++ b/Sources/Layout/MessageCellLayout.swift @@ -374,13 +374,19 @@ open class MediaMessageSizeCalculator: MessageSizeCalculator { open override func messageContainerSize(for message: MessageType) -> CGSize { let maxWidth = messageContainerMaxWidth(for: message) + let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + } switch message.data { case .photo(let item): - let width = min(item.size.width, maxWidth) - return CGSize(width: width, height: item.size.height) + return sizeForMediaItem(maxWidth, item) case .video(let item): - let width = min(item.size.width, maxWidth) - return CGSize(width: width, height: item.size.height) + return sizeForMediaItem(maxWidth, item) default: fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") } @@ -389,18 +395,19 @@ open class MediaMessageSizeCalculator: MessageSizeCalculator { open class LocationMessageSizeCalculator: MessageSizeCalculator { - // TODO: - Figure out how we are going to size these - public var incomingLocationSize: CGSize = .zero - public var outgoingLocationSize: CGSize = .zero - open override func messageContainerSize(for message: MessageType) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message) switch message.data { - case .location(_): - // TODO: - Implement actual sizing - return CGSize(width: maxWidth, height: 240) + case .location(let item): + let maxWidth = messageContainerMaxWidth(for: message) + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size default: fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") } } } + diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 43089679..e3441b70 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -88,12 +88,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - if collectionView?.bounds.width != newBounds.width { - removeAllCachedAttributes() - return true - } else { - return false - } + return collectionView?.bounds.width != newBounds.width } open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { @@ -103,14 +98,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return flowLayoutContext } - // TODO: Invalidate attributes - public func removeAllCachedAttributes() { - - } - @objc private func handleOrientationChange(_ notification: Notification) { - removeAllCachedAttributes() invalidateLayout() } @@ -157,12 +146,4 @@ extension MessagesCollectionViewFlowLayout { } return messagesDataSource } - - // TODO: - This will become unused - internal var messagesLayoutDelegate: MessagesLayoutDelegate { - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return messagesLayoutDelegate - } } diff --git a/Sources/Models/MessageData.swift b/Sources/Models/MessageData.swift index b229742c..b9b8f77b 100644 --- a/Sources/Models/MessageData.swift +++ b/Sources/Models/MessageData.swift @@ -47,7 +47,7 @@ public enum MessageData { case video(MediaItem) /// A location message. - case location(CLLocation) + case location(LocationItem) /// An emoji message. case emoji(String) diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift new file mode 100644 index 00000000..fe037932 --- /dev/null +++ b/Sources/Protocols/LocationItem.swift @@ -0,0 +1,33 @@ +/* + 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 CoreLocation + +public protocol LocationItem { + + var location: CLLocation { get set } + + var size: CGSize { get set } + +} diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift index 5a8b8471..71adc853 100644 --- a/Sources/Protocols/MessageType.swift +++ b/Sources/Protocols/MessageType.swift @@ -32,9 +32,6 @@ public protocol MessageType { var sender: Sender { get } /// The unique identifier for the message. - /// - /// NOTE: This value must be unique for all messages as it is - /// used to cache layout information. var messageId: String { get } /// The date the message was sent. diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index ad4aad7a..0534cb45 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -64,12 +64,12 @@ open class LocationMessageCell: MessageCollectionViewCell { let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView) let animationBlock = displayDelegate.animationBlockForLocation(message: message, at: indexPath, in: messagesCollectionView) - guard case let .location(location) = message.data else { fatalError("") } + guard case let .location(locationItem) = message.data else { fatalError("") } activityIndicator.startAnimating() let snapshotOptions = MKMapSnapshotOptions() - snapshotOptions.region = MKCoordinateRegion(center: location.coordinate, span: options.span) + snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span) snapshotOptions.showsBuildings = options.showsBuildings snapshotOptions.showsPointsOfInterest = options.showsPointsOfInterest @@ -93,7 +93,7 @@ open class LocationMessageCell: MessageCollectionViewCell { snapshot.image.draw(at: .zero) - var point = snapshot.point(for: location.coordinate) + var point = snapshot.point(for: locationItem.location.coordinate) //Move point to reflect annotation anchor point.x -= annotationView.bounds.size.width / 2 point.y -= annotationView.bounds.size.height / 2 diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index 3a915b86..fc093ab0 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -55,11 +55,11 @@ open class MediaMessageCell: MessageCollectionViewCell { open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { super.configure(with: message, at: indexPath, and: messagesCollectionView) switch message.data { - case .photo(let image): - imageView.image = image + case .photo(let mediaItem): + imageView.image = mediaItem.image ?? mediaItem.placeholderImage playButtonView.isHidden = true - case .video(_, let image): - imageView.image = image + case .video(let mediaItem): + imageView.image = mediaItem.image ?? mediaItem.placeholderImage playButtonView.isHidden = false default: break From caefdbc22be8d6d1d99b356185cff0ddb3ed3c61 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Fri, 23 Mar 2018 23:44:21 -0500 Subject: [PATCH 21/70] Fix tests --- Tests/Mocks/MockMessage.swift | 37 +++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/Tests/Mocks/MockMessage.swift b/Tests/Mocks/MockMessage.swift index 70f747f2..cdcb6f08 100644 --- a/Tests/Mocks/MockMessage.swift +++ b/Tests/Mocks/MockMessage.swift @@ -26,6 +26,33 @@ import Foundation import CoreLocation @testable import MessageKit +struct MockLocationItem: LocationItem { + + var location: CLLocation + var size: CGSize + + init(location: CLLocation) { + self.location = location + self.size = CGSize(width: 240, height: 240) + } + +} + +struct MockMediaItem: MediaItem { + + var url: URL? + var image: UIImage? + var placeholderImage: UIImage + var size: CGSize + + init(image: UIImage) { + self.image = image + self.size = CGSize(width: 240, height: 240) + self.placeholderImage = UIImage() + } + +} + struct MockMessage: MessageType { var messageId: String @@ -49,16 +76,18 @@ struct MockMessage: MessageType { } init(image: UIImage, sender: Sender, messageId: String) { - self.init(data: .photo(image), sender: sender, messageId: messageId) + let mediaItem = MockMediaItem(image: image) + self.init(data: .photo(mediaItem), sender: sender, messageId: messageId) } init(thumbnail: UIImage, sender: Sender, messageId: String) { - let url = URL(fileURLWithPath: "") - self.init(data: .video(file: url, thumbnail: thumbnail), sender: sender, messageId: messageId) + let mediaItem = MockMediaItem(image: thumbnail) + self.init(data: .video(mediaItem), sender: sender, messageId: messageId) } init(location: CLLocation, sender: Sender, messageId: String) { - self.init(data: .location(location), sender: sender, messageId: messageId) + let locationItem = MockLocationItem(location: location) + self.init(data: .location(locationItem), sender: sender, messageId: messageId) } init(emoji: String, sender: Sender, messageId: String) { From f589debbd9d55c65c738b79287274b8b3e713bcb Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 00:24:20 -0500 Subject: [PATCH 22/70] Fix example app --- .../Sources/ConversationViewController.swift | 8 -- Example/Sources/MockMessage.swift | 97 +++++++++++++++---- .../Protocols/MessagesLayoutDelegate.swift | 68 ++++++------- .../Supporting/MessageKit+Availability.swift | 39 +++++--- Sources/Views/Cells/MediaMessageCell.swift | 6 +- 5 files changed, 139 insertions(+), 79 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 40d44376..8ec00a95 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -358,16 +358,8 @@ extension ConversationViewController: MessagesDisplayDelegate { extension ConversationViewController: MessagesLayoutDelegate { func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - return CGSize(width: messagesCollectionView.bounds.width, height: 10) } - - // MARK: - Location Messages - - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 200 - } - } // MARK: - MessageCellDelegate diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/MockMessage.swift index de49c2a8..8532af06 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/MockMessage.swift @@ -25,40 +25,96 @@ import Foundation import MessageKit import CoreLocation +/* + 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 Foundation +import CoreLocation +import MessageKit + +struct MockLocationItem: LocationItem { + + var location: CLLocation + var size: CGSize + + init(location: CLLocation) { + self.location = location + self.size = CGSize(width: 240, height: 240) + } + +} + +struct MockMediaItem: MediaItem { + + var url: URL? + var image: UIImage? + var placeholderImage: UIImage + var size: CGSize + + init(image: UIImage) { + self.image = image + self.size = CGSize(width: 240, height: 240) + self.placeholderImage = UIImage() + } + +} struct MockMessage: MessageType { - - var messageId: String - var sender: Sender - var sentDate: Date - var data: MessageData - - init(data: MessageData, sender: Sender, messageId: String, date: Date) { - self.data = data - self.sender = sender - self.messageId = messageId - self.sentDate = date - } - + + var messageId: String + var sender: Sender + var sentDate: Date + var data: MessageData + + private init(data: MessageData, sender: Sender, messageId: String, date: Date) { + self.data = data + self.sender = sender + self.messageId = messageId + self.sentDate = date + } + init(text: String, sender: Sender, messageId: String, date: Date) { self.init(data: .text(text), sender: sender, messageId: messageId, date: date) - } - + } + init(attributedText: NSAttributedString, sender: Sender, messageId: String, date: Date) { self.init(data: .attributedText(attributedText), sender: sender, messageId: messageId, date: date) - } + } init(image: UIImage, sender: Sender, messageId: String, date: Date) { - self.init(data: .photo(image), sender: sender, messageId: messageId, date: date) + let mediaItem = MockMediaItem(image: image) + self.init(data: .photo(mediaItem), sender: sender, messageId: messageId, date: date) } init(thumbnail: UIImage, sender: Sender, messageId: String, date: Date) { - let url = URL(fileURLWithPath: "") - self.init(data: .video(file: url, thumbnail: thumbnail), sender: sender, messageId: messageId, date: date) + let mediaItem = MockMediaItem(image: thumbnail) + self.init(data: .video(mediaItem), sender: sender, messageId: messageId, date: date) } init(location: CLLocation, sender: Sender, messageId: String, date: Date) { - self.init(data: .location(location), sender: sender, messageId: messageId, date: date) + let locationItem = MockLocationItem(location: location) + self.init(data: .location(locationItem), sender: sender, messageId: messageId, date: date) } init(emoji: String, sender: Sender, messageId: String, date: Date) { @@ -66,3 +122,4 @@ struct MockMessage: MessageType { } } + diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 97d7d845..cacb56ee 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -49,41 +49,43 @@ public protocol MessagesLayoutDelegate: AnyObject { /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - // MARK: - Location Messages - /// Specifies the width for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is the `maxWidth`. - func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies the height for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies whether the layout attributes for a given `MessageType` - /// should be cached by the `MessagesCollectionViewFlowLayout` object. - /// - Parameters: - /// - message: The `MessageType` whose attributes to cache. - /// The default value returned by this method is `false`. + @available(*, deprecated: 1.0, message: "avatarSize(for:at:in) has been removed in MessageKit 1.0") func shouldCacheLayoutAttributes(for message: MessageType) -> Bool + @available(*, deprecated: 1.0, message: "avatarSize(for:at:in) has been removed in MessageKit 1.0") + func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + + @available(*, deprecated: 1.0, message: "avatarPosition(for:at:in) has been removed in MessageKit 1.0") + func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition + + @available(*, deprecated: 1.0, message: "messageLabelInset(for:at:in) has been removed in MessageKit 1.0.") + func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets + + @available(*, deprecated: 1.0, message: "messagePadding(for:at:in) has been removed in MessageKit 1.0.") + func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets + + @available(*, deprecated: 1.0, message: "cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") + func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment + + @available(*, deprecated: 1.0, message: "cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") + func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment + + @available(*, deprecated: 1.0, message: "widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + @available(*, deprecated: 1.0, message: "heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + @available(*, deprecated: 1.0, message: "widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + @available(*, deprecated: 1.0, message: "heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat } public extension MessagesLayoutDelegate { - // MARK: - All Messages Defaults - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) @@ -95,14 +97,4 @@ public extension MessagesLayoutDelegate { func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return .zero } - - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { - return false - } - - // MARK: - Location Messages Defaults - - func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return maxWidth - } } diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index e55b603b..13db71db 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -26,33 +26,48 @@ import Foundation public extension MessagesLayoutDelegate { - @available(*, deprecated: 0.14.0, message: "avatarSize(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarSize and outgoingAvatarSize properties on MessagesCollectionViewFlowLayout.") func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - fatalError("avatarSize(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarSize and outgoingAvatarSize properties on MessagesCollectionViewFlowLayout.") + fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.") } - @available(*, deprecated: 0.14.0, message: "avatarPosition(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarPosition and outgoingAvatarPosition properties on MessagesCollectionViewFlowLayout.") func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - fatalError("avatarPosition(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingAvatarPosition and outgoingAvatarPosition properties on MessagesCollectionViewFlowLayout.") + fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.") } - @available(*, deprecated: 0.14.0, message: "messageLabelInset(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessageLabelInset and outgoingMessageLabelInset properties on MessagesCollectionViewFlowLayout.") func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessageLabelInset and outgoingMessageLabelInset properties on MessagesCollectionViewFlowLayout.") + fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0") } - @available(*, deprecated: 0.14.0, message: "messagePadding(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessagePadding and outgoingMessagePadding properties on MessagesCollectionViewFlowLayout.") func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - fatalError("messagePadding(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingMessagePadding and outgoingMessagePadding properties on MessagesCollectionViewFlowLayout.") + fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.") } - @available(*, deprecated: 0.14.0, message: "cellTopLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellTopLabelAlignment and outgoingCellTopLabelAlignment properties on MessagesCollectionViewFlowLayout.") func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellTopLabelAlignment and outgoingCellTopLabelAlignment properties on MessagesCollectionViewFlowLayout.") + fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") } - @available(*, deprecated: 0.14.0, message: "cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellBottomLabelAlignment and outgoingCellBottomLabelAlignment properties on MessagesCollectionViewFlowLayout.") func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 0.14.0. Please use the incomingCellBottomLabelAlignment and outgoingCellBottomLabelAlignment properties on MessagesCollectionViewFlowLayout.") + fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") + } + + func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { + fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.") } } + diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index fc093ab0..c23eb414 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -35,7 +35,11 @@ open class MediaMessageCell: MessageCollectionViewCell { return playButtonView }() - open var imageView = UIImageView() + open var imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() // MARK: - Methods From a9af5290c84469ca7ad2768df1983ec9841265b1 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 00:27:44 -0500 Subject: [PATCH 23/70] Remove double import --- Example/Sources/MockMessage.swift | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/MockMessage.swift index 8532af06..99267958 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/MockMessage.swift @@ -1,30 +1,3 @@ -/* - 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 Foundation -import MessageKit -import CoreLocation /* MIT License From 8fc9cd60a8bf36ae3ab02c3e197d663f0fd5396f Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 01:02:31 -0500 Subject: [PATCH 24/70] Add CHANGELOG entry for #587 --- CHANGELOG.md | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f80f0f3..c417c79a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,36 +8,22 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Added -- **Breaking Change** Adds new `DetectorType` called `.transitInformation` to message label. +- **Breaking Change** Added new protocol `MediaItem` as the associated value for the +`MessageData.video` and `MessageData.photo` cases. +[#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Added new protocol `LocationItem` as the associated value for +the `MessageData.location` case. +[#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Added new `DetectorType` called `.transitInformation` to message label. [#520](https://github.com/MessageKit/MessageKit/pull/520) by [@nosarj](https://github.com/nosarj). -- **Breaking Change** Adds `.custom(Any?)` case to `MessageData`. +- **Breaking Change** Added `.custom(Any?)` case to `MessageData`. [#498](https://github.com/MessageKit/MessageKit/pull/498) by [@SD10](https://github.com/SD10). -- Adds the following methods to `MessagesCollectionViewFlowLayout`: - - `messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets` - - `messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets` - - `messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat` - - `messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment` - - `cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat` - - `cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment` - - `cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat` - - `avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition` - - `avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat` -[#491](https://github.com/MessageKit/MessageKit/pull/491) by [@SD10](https://github.com/SD10). - -- Adds the following methods to `MessageCollectionViewCell`: - - `layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes)` - - `layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes)` - - `layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes)` - - `layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes)` -[#491](https://github.com/MessageKit/MessageKit/pull/491) by [@SD10](https://github.com/SD10). - -- Added `shouldManageSendButtonEnabledState` to `MessageInputBar` to disable automatically managing `MessageInputBar.sendButton`'s `isEnabled` state when text changes. (Default value is `true`). +- Added `shouldManageSendButtonEnabledState` to `MessageInputBar` to disable automatically managing `MessageInputBar.sendButton`'s + `isEnabled` state when text changes. (Default value is `true`). [#530](https://github.com/MessageKit/MessageKit/pull/530) by [@clayellis](https://github.com/clayellis). ## [[Prerelease] 0.13.1](https://github.com/MessageKit/MessageKit/releases/tag/0.13.1) From 599f9afc27042d17921870f7ba72e73cf3e925d1 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 01:40:58 -0500 Subject: [PATCH 25/70] Remove unused String extension --- Sources/Extensions/String+Extensions.swift | 44 ---------------------- Sources/Views/AvatarView.swift | 6 ++- 2 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 Sources/Extensions/String+Extensions.swift diff --git a/Sources/Extensions/String+Extensions.swift b/Sources/Extensions/String+Extensions.swift deleted file mode 100644 index 205db79d..00000000 --- a/Sources/Extensions/String+Extensions.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - 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 Foundation - -extension String { - - func height(considering width: CGFloat, and font: UIFont) -> CGFloat { - - let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) - let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil) - return rect.height - - } - - func width(considering height: CGFloat, and font: UIFont) -> CGFloat { - - let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) - let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil) - return rect.width - - } -} diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index be28e739..f88af355 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -96,8 +96,9 @@ open class AvatarView: UIImageView { //// Text Drawing let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height) + let initialsText = NSAttributedString(string: initials, attributes: [.font: font]) if adjustsFontSizeToFitWidth, - initials.width(considering: textRect.height, and: font) > textRect.width { + initialsText.width(considering: textRect.height) > textRect.width { let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height) font = placeholderFont.withSize(newFontSize) } @@ -119,7 +120,8 @@ open class AvatarView: UIImageView { Recursively find the biggest size to fit the text with a given width and height */ private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat { - if text.width(considering: height, and: font) > width { + let attributedText = NSAttributedString(string: text, attributes: [.font: font]) + if attributedText.width(considering: height) > width { let newFont = font.withSize(font.pointSize - 1) if newFont.pointSize > minimumFontSize { return font.pointSize From 4e7b1b4eed5cb362af0b855869ffd626b7925b97 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 01:41:35 -0500 Subject: [PATCH 26/70] Make CellSizeCalculator a protocol --- Sources/Protocols/CellSizeCalculator.swift | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Sources/Protocols/CellSizeCalculator.swift diff --git a/Sources/Protocols/CellSizeCalculator.swift b/Sources/Protocols/CellSizeCalculator.swift new file mode 100644 index 00000000..2aca310f --- /dev/null +++ b/Sources/Protocols/CellSizeCalculator.swift @@ -0,0 +1,34 @@ +/* + 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 + +public protocol CellSizeCalculator { + + weak var layout: UICollectionViewFlowLayout? { get set } + + func configure(attributes: UICollectionViewLayoutAttributes) + + func sizeForItem(at indexPath: IndexPath) -> CGSize +} From 601a35007a0e70300a5255d04d00ae113dd70e22 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 01:42:24 -0500 Subject: [PATCH 27/70] Remove MessageIntermediateLayoutAttributes --- .../MessageIntermediateLayoutAttributes.swift | 74 ------------------- 1 file changed, 74 deletions(-) delete mode 100644 Sources/Layout/MessageIntermediateLayoutAttributes.swift diff --git a/Sources/Layout/MessageIntermediateLayoutAttributes.swift b/Sources/Layout/MessageIntermediateLayoutAttributes.swift deleted file mode 100644 index f0cc3fec..00000000 --- a/Sources/Layout/MessageIntermediateLayoutAttributes.swift +++ /dev/null @@ -1,74 +0,0 @@ -/* - 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 Foundation - -public class MessageIntermediateLayoutAttributes { - - var avatarSize: CGSize = .zero - var avatarPosition = AvatarPosition(vertical: .cellBottom) - - var messageContainerSize: CGSize = .zero - var messageContainerPadding: UIEdgeInsets = .zero - var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - var messageLabelInsets: UIEdgeInsets = .zero - - var topLabelAlignment = LabelAlignment.cellCenter(.zero) - var topLabelSize: CGSize = .zero - - var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) - var bottomLabelSize: CGSize = .zero - - lazy var itemHeight: CGFloat = { - let avatarVerticalPosition = avatarPosition.vertical - let avatarHeight = avatarSize.height - let messageContainerHeight = messageContainerSize.height - let bottomLabelHeight = bottomLabelSize.height - let topLabelHeight = topLabelSize.height - let messageVerticalPadding = messageContainerPadding.vertical - - var cellHeight: CGFloat = 0 - - switch avatarVerticalPosition { - case .cellTop: - cellHeight += max(avatarHeight, topLabelHeight) - cellHeight += bottomLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .cellBottom: - cellHeight += max(avatarHeight, bottomLabelHeight) - cellHeight += topLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .messageTop, .messageCenter, .messageBottom: - cellHeight += max(avatarHeight, messageContainerHeight) - cellHeight += messageVerticalPadding - cellHeight += topLabelHeight - cellHeight += bottomLabelHeight - } - - return cellHeight - }() - -} From 86f9372d44db1d844456c5ab4c9b0913e75113f9 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 01:42:53 -0500 Subject: [PATCH 28/70] Add size calculators --- MessageKit.xcodeproj/project.pbxproj | 32 ++-- .../LocationMessageSizeCalculator.swift | 43 +++++ .../Layout/MediaMessageSizeCalculator.swift | 48 +++++ ...yout.swift => MessageSizeCalculator.swift} | 171 ++---------------- .../MessagesCollectionViewFlowLayout.swift | 27 ++- .../Layout/TextMessageSizeCalculator.swift | 90 +++++++++ .../Protocols/MessagesLayoutDelegate.swift | 1 - .../Supporting/MessageKit+Availability.swift | 1 - 8 files changed, 240 insertions(+), 173 deletions(-) create mode 100644 Sources/Layout/LocationMessageSizeCalculator.swift create mode 100644 Sources/Layout/MediaMessageSizeCalculator.swift rename Sources/Layout/{MessageCellLayout.swift => MessageSizeCalculator.swift} (69%) create mode 100644 Sources/Layout/TextMessageSizeCalculator.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index f316b204..f11ff957 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -19,12 +19,15 @@ 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; - 1FD5895C205F5B8F004B5081 /* MessageCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */; }; - 1FD5895E205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */; }; 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895F2064E08A004B5081 /* MediaItem.swift */; }; 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5896320660C1C004B5081 /* LocationItem.swift */; }; + 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */; }; + 1FE783A02066286A007FA024 /* CellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7839F2066286A007FA024 /* CellSizeCalculator.swift */; }; + 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */; }; + 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */; }; + 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 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 */; }; @@ -81,7 +84,6 @@ B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */; }; B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */; }; B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */; }; - B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F691F8669EB006AEF79 /* String+Extensions.swift */; }; B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */; }; B7A03F751F866A06006AEF79 /* MessageKit.h in Headers */ = {isa = PBXBuildFile; fileRef = B7A03F721F866A06006AEF79 /* MessageKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */; }; @@ -123,10 +125,13 @@ 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = ""; }; 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; - 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayout.swift; sourceTree = ""; }; - 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIntermediateLayoutAttributes.swift; sourceTree = ""; }; 1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; 1FD5896320660C1C004B5081 /* LocationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItem.swift; sourceTree = ""; }; + 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSizeCalculator.swift; sourceTree = ""; }; + 1FE7839F2066286A007FA024 /* CellSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellSizeCalculator.swift; sourceTree = ""; }; + 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaMessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessageSizeCalculator.swift; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; @@ -187,7 +192,6 @@ B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; - B7A03F691F8669EB006AEF79 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageKit+Availability.swift"; sourceTree = ""; }; B7A03F711F866A06006AEF79 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B7A03F721F866A06006AEF79 /* MessageKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageKit.h; sourceTree = ""; }; @@ -370,7 +374,6 @@ 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */, B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */, B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */, - B7A03F691F8669EB006AEF79 /* String+Extensions.swift */, B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */, B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */, B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */, @@ -428,6 +431,7 @@ B7A03F511F8669C9006AEF79 /* MessageType.swift */, 1FD5895F2064E08A004B5081 /* MediaItem.swift */, 1FD5896320660C1C004B5081 /* LocationItem.swift */, + 1FE7839F2066286A007FA024 /* CellSizeCalculator.swift */, ); path = Protocols; sourceTree = ""; @@ -447,8 +451,10 @@ children = ( B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, - 1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */, - 1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */, + 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */, + 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */, + 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */, + 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */, ); path = Layout; sourceTree = ""; @@ -595,9 +601,9 @@ 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */, 38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */, B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, - B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */, B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, + 1FE783A02066286A007FA024 /* CellSizeCalculator.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, @@ -608,9 +614,9 @@ B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, - 1FD5895E205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift in Sources */, 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, + 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */, B7A03F331F866940006AEF79 /* MessageDateHeaderView.swift in Sources */, B7A03F2E1F866895006AEF79 /* MessageData.swift in Sources */, B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */, @@ -636,8 +642,10 @@ B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */, + 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, - 1FD5895C205F5B8F004B5081 /* MessageCellLayout.swift in Sources */, + 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */, + 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, diff --git a/Sources/Layout/LocationMessageSizeCalculator.swift b/Sources/Layout/LocationMessageSizeCalculator.swift new file mode 100644 index 00000000..75048212 --- /dev/null +++ b/Sources/Layout/LocationMessageSizeCalculator.swift @@ -0,0 +1,43 @@ +/* + 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 Foundation + +open class LocationMessageSizeCalculator: MessageSizeCalculator { + + open override func messageContainerSize(for message: MessageType) -> CGSize { + switch message.data { + case .location(let item): + let maxWidth = messageContainerMaxWidth(for: message) + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + } +} diff --git a/Sources/Layout/MediaMessageSizeCalculator.swift b/Sources/Layout/MediaMessageSizeCalculator.swift new file mode 100644 index 00000000..82f03445 --- /dev/null +++ b/Sources/Layout/MediaMessageSizeCalculator.swift @@ -0,0 +1,48 @@ +/* + 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 Foundation + +open class MediaMessageSizeCalculator: MessageSizeCalculator { + + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + } + switch message.data { + case .photo(let item): + return sizeForMediaItem(maxWidth, item) + case .video(let item): + return sizeForMediaItem(maxWidth, item) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + } +} diff --git a/Sources/Layout/MessageCellLayout.swift b/Sources/Layout/MessageSizeCalculator.swift similarity index 69% rename from Sources/Layout/MessageCellLayout.swift rename to Sources/Layout/MessageSizeCalculator.swift index f806e749..64247179 100644 --- a/Sources/Layout/MessageCellLayout.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -22,28 +22,16 @@ SOFTWARE. */ -import UIKit - -open class CellSizeCalculator { - - open weak var layout: UICollectionViewFlowLayout? - - public init(layout: UICollectionViewFlowLayout? = nil) { - self.layout = layout - } - - open func configure(attributes: UICollectionViewLayoutAttributes) { - // No-op default - } - - open func sizeForItem(at indexPath: IndexPath) -> CGSize { - return .zero - } - -} +import Foundation open class MessageSizeCalculator: CellSizeCalculator { + public var layout: UICollectionViewFlowLayout? + + public init(layout: MessagesCollectionViewFlowLayout? = nil) { + self.layout = layout + } + public var incomingAvatarSize = CGSize(width: 30, height: 30) public var outgoingAvatarSize = CGSize(width: 30, height: 30) @@ -59,14 +47,7 @@ open class MessageSizeCalculator: CellSizeCalculator { public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) - internal var messagesLayout: MessagesCollectionViewFlowLayout { - guard let layout = layout as? MessagesCollectionViewFlowLayout else { - fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") - } - return layout - } - - open override func configure(attributes: UICollectionViewLayoutAttributes) { + open func configure(attributes: UICollectionViewLayoutAttributes) { guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } let dataSource = messagesLayout.messagesDataSource @@ -85,14 +66,14 @@ open class MessageSizeCalculator: CellSizeCalculator { attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) } - open override func sizeForItem(at indexPath: IndexPath) -> CGSize { + open func sizeForItem(at indexPath: IndexPath) -> CGSize { let dataSource = messagesLayout.messagesDataSource let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) let itemHeight = cellContentHeight(for: message, at: indexPath) return CGSize(width: messagesLayout.itemWidth, height: itemHeight) } - internal func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let avatarVerticalPosition = avatarPosition(for: message).vertical let avatarHeight = avatarSize(for: message).height @@ -274,12 +255,21 @@ open class MessageSizeCalculator: CellSizeCalculator { return .zero } - internal func messageContainerMaxWidth(for message: MessageType) -> CGFloat { + open func messageContainerMaxWidth(for message: MessageType) -> CGFloat { let avatarWidth = avatarSize(for: message).width let messagePadding = messageContainerPadding(for: message) return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal } + // MARK: - Helpers + + internal var messagesLayout: MessagesCollectionViewFlowLayout { + guard let layout = layout as? MessagesCollectionViewFlowLayout else { + fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") + } + return layout + } + internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { let estimatedHeight = attributedText.height(considering: maxWidth) let estimatedWidth = attributedText.width(considering: estimatedHeight) @@ -290,124 +280,3 @@ open class MessageSizeCalculator: CellSizeCalculator { return CGSize(width: finalWidth, height: finalHeight) } } - -open class TextMessageSizeCalculator: MessageSizeCalculator { - - public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - - public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) { - didSet { - emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) - } - } - - internal var emojiLabelFont: UIFont - - public init() { - emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) - } - - internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets - } - - internal override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { - let maxWidth = super.messageContainerMaxWidth(for: message) - let textInsets = messageLabelInsets(for: message) - return maxWidth - textInsets.horizontal - } - - open override func messageContainerSize(for message: MessageType) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message) - - var messageContainerSize: CGSize - let attributedText: NSAttributedString - - switch message.data { - case .text(let text): - attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) - case .attributedText(let text): - attributedText = text - case .emoji(let text): - attributedText = NSAttributedString(string: text, attributes: [.font: emojiLabelFont]) - default: - fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") - } - - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - - let messageInsets = messageLabelInsets(for: message) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - - return messageContainerSize - } - - open override func configure(attributes: UICollectionViewLayoutAttributes) { - super.configure(attributes: attributes) - guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } - - let dataSource = messagesLayout.messagesDataSource - let indexPath = attributes.indexPath - let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - - attributes.messageLabelInsets = messageLabelInsets(for: message) - attributes.messageLabelFont = messageLabelFont - - switch message.data { - case .attributedText(let text): - guard !text.string.isEmpty else { return } - guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } - attributes.messageLabelFont = font - case .emoji: - attributes.messageLabelFont = emojiLabelFont - default: - break - } - } -} - -open class MediaMessageSizeCalculator: MessageSizeCalculator { - - open override func messageContainerSize(for message: MessageType) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message) - let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in - if maxWidth < item.size.width { - // Maintain the ratio if width is too great - let height = maxWidth * item.size.height / item.size.width - return CGSize(width: maxWidth, height: height) - } - return item.size - } - switch message.data { - case .photo(let item): - return sizeForMediaItem(maxWidth, item) - case .video(let item): - return sizeForMediaItem(maxWidth, item) - default: - fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") - } - } -} - -open class LocationMessageSizeCalculator: MessageSizeCalculator { - - open override func messageContainerSize(for message: MessageType) -> CGSize { - switch message.data { - case .location(let item): - let maxWidth = messageContainerMaxWidth(for: message) - if maxWidth < item.size.width { - // Maintain the ratio if width is too great - let height = maxWidth * item.size.height / item.size.width - return CGSize(width: maxWidth, height: height) - } - return item.size - default: - fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") - } - } -} - diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index e3441b70..93e9477b 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -38,14 +38,16 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return collectionView.frame.width - sectionInset.left - sectionInset.right } - // MARK: - Initializers public override init() { super.init() - mediaMessageSizeCalculator.layout = self textMessageSizeCalculator.layout = self + attributedTextMessageSizeCalculator.layout = self + emojiMessageSizeCalculator.layout = self + photoMessageSizeCalculator.layout = self + videoMessageSizeCalculator.layout = self locationMessageSizeCalculator.layout = self sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) @@ -106,26 +108,35 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Sizing var textMessageSizeCalculator = TextMessageSizeCalculator() - var mediaMessageSizeCalculator = MediaMessageSizeCalculator() + var attributedTextMessageSizeCalculator = TextMessageSizeCalculator() + var emojiMessageSizeCalculator = TextMessageSizeCalculator() + var photoMessageSizeCalculator = MediaMessageSizeCalculator() + var videoMessageSizeCalculator = MediaMessageSizeCalculator() var locationMessageSizeCalculator = LocationMessageSizeCalculator() open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> MessageSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.data { - case .text, .attributedText, .emoji: + case .text: return textMessageSizeCalculator - case .photo, .video: - return mediaMessageSizeCalculator + case .attributedText: + return attributedTextMessageSizeCalculator + case .emoji: + return emojiMessageSizeCalculator + case .photo: + return photoMessageSizeCalculator + case .video: + return videoMessageSizeCalculator case .location: return locationMessageSizeCalculator case .custom: - fatalError("") + fatalError("Must return a CellSizeCalculator for MessageData.custom(Any?)") } } open func sizeForItem(at indexPath: IndexPath) -> CGSize { let calculator = cellSizeCalculatorForItem(at: indexPath) - return calculator.sizeForItem(at:indexPath) + return calculator.sizeForItem(at: indexPath) } } diff --git a/Sources/Layout/TextMessageSizeCalculator.swift b/Sources/Layout/TextMessageSizeCalculator.swift new file mode 100644 index 00000000..0d24c6b4 --- /dev/null +++ b/Sources/Layout/TextMessageSizeCalculator.swift @@ -0,0 +1,90 @@ +/* + 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 Foundation + +open class TextMessageSizeCalculator: MessageSizeCalculator { + + public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) + public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + + public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + + internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets + } + + open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { + let maxWidth = super.messageContainerMaxWidth(for: message) + let textInsets = messageLabelInsets(for: message) + return maxWidth - textInsets.horizontal + } + + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + + var messageContainerSize: CGSize + let attributedText: NSAttributedString + + switch message.data { + case .attributedText(let text): + attributedText = text + case .text(let text), .emoji(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + + let messageInsets = messageLabelInsets(for: message) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + + return messageContainerSize + } + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + attributes.messageLabelInsets = messageLabelInsets(for: message) + attributes.messageLabelFont = messageLabelFont + + switch message.data { + case .attributedText(let text): + guard !text.string.isEmpty else { return } + guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } + attributes.messageLabelFont = font + default: + break + } + } +} diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index cacb56ee..59704738 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -49,7 +49,6 @@ public protocol MessagesLayoutDelegate: AnyObject { /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - @available(*, deprecated: 1.0, message: "avatarSize(for:at:in) has been removed in MessageKit 1.0") func shouldCacheLayoutAttributes(for message: MessageType) -> Bool diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index 13db71db..735409e3 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -70,4 +70,3 @@ public extension MessagesLayoutDelegate { fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.") } } - From 8c16892ca9ef6d256042d25d13a8e35d7bf449e4 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 02:04:30 -0500 Subject: [PATCH 29/70] Add CHANGELOG entry for #579 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c417c79a..2debcb1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,32 @@ the `MessageData.location` case. - **Breaking Change** Added `.custom(Any?)` case to `MessageData`. [#498](https://github.com/MessageKit/MessageKit/pull/498) by [@SD10](https://github.com/SD10). +- Added `CellSizeCalculator` protocol that is responsible for sizing and configuring attributes of a `MessageCollectionViewCell`. +[#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/SD10). + +- Added `MessageSizeCalculator`, `MediaMessageSizeCalculator`, `TextMessageSizeCalculator`, and `LocationMessageSizeCalculator` + classes that are responsible for sizing the `MessagesCollectionViewCell` types provided by MessageKit. +[#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). + - Added `shouldManageSendButtonEnabledState` to `MessageInputBar` to disable automatically managing `MessageInputBar.sendButton`'s `isEnabled` state when text changes. (Default value is `true`). [#530](https://github.com/MessageKit/MessageKit/pull/530) by [@clayellis](https://github.com/clayellis). +### Changed + +- The `MessageData.emoji` case no longer uses a default font of 2x the `messageLabelFont` size. +You must now set this font explicitly through the `emojiMessageSizeCalculator` on `MessagesCollectionViewFlowLayout`. +[#530](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). + +- Changed the `contentMode` of the `UIImageView` for `MediaMessageCell` to be `.scaleAspectFill`. +[#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). + +### Removed + +- **Breaking Change** Removed the `messageLabelFont` property from `MessagesCollectionViewFlowLayout`. +You can now set this property through `textMessageSizeCalculator` property. +[#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). + ## [[Prerelease] 0.13.1](https://github.com/MessageKit/MessageKit/releases/tag/0.13.1) ### Fixed From 4f2fd524658272d5cda6911fd22fc5588c094c12 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 02:18:52 -0500 Subject: [PATCH 30/70] Stop sizing cell labels to fit text --- .../Sources/ConversationViewController.swift | 2 +- Sources/Layout/MessageSizeCalculator.swift | 113 +++--------------- .../MessagesCollectionViewFlowLayout.swift | 7 ++ ...ssagesCollectionViewLayoutAttributes.swift | 4 +- Sources/Models/LabelAlignment.swift | 35 +----- .../Protocols/MessagesLayoutDelegate.swift | 17 +++ .../Cells/MessageCollectionViewCell.swift | 58 ++------- Sources/Views/MessageLabel.swift | 13 ++ 8 files changed, 70 insertions(+), 179 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 8ec00a95..99b278af 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -37,7 +37,7 @@ class ConversationViewController: MessagesViewController { override func viewDidLoad() { super.viewDidLoad() - let messagesToFetch = UserDefaults.standard.mockMessagesCount() + let messagesToFetch = 10000 //UserDefaults.standard.mockMessagesCount() DispatchQueue.global(qos: .userInitiated).async { SampleData.shared.getMessages(count: messagesToFetch) { messages in diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 64247179..3f3a9e0e 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -41,11 +41,11 @@ open class MessageSizeCalculator: CellSizeCalculator { public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - public var incomingCellTopLabelAlignment = LabelAlignment.messageLeading(.zero) - public var outgoingCellTopLabelAlignment = LabelAlignment.messageTrailing(.zero) + public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: .zero) + public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: .zero) - public var incomingCellBottomLabelAlignment = LabelAlignment.messageTrailing(.zero) - public var outgoingCellBottomLabelAlignment = LabelAlignment.messageLeading(.zero) + public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: .zero) + public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: .zero) open func configure(attributes: UICollectionViewLayoutAttributes) { guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } @@ -130,11 +130,10 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top Label internal func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let dataSource = messagesLayout.messagesDataSource - let text = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) - guard let topLabelText = text else { return .zero } - let maxWidth = cellTopLabelMaxWidth(for: message) - return labelSize(for: topLabelText, considering: maxWidth) + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) } internal func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { @@ -143,55 +142,13 @@ open class MessageSizeCalculator: CellSizeCalculator { return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment } - internal func cellTopLabelMaxWidth(for message: MessageType) -> CGFloat { - let alignment = cellTopLabelAlignment(for: message) - let position = avatarPosition(for: message) - let avatarWidth = avatarSize(for: message).width - let containerSize = messageContainerSize(for: message) - let messagePadding = messageContainerPadding(for: message) - - let avatarHorizontal = position.horizontal - let avatarVertical = position.vertical - - let itemWidth = messagesLayout.itemWidth - - switch (alignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = containerSize.width + messagePadding.left - alignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = containerSize.width + messagePadding.right - alignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - // MARK: - Bottom Label open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let dataSource = messagesLayout.messagesDataSource - let text = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - guard let bottomLabelText = text else { return .zero } - let maxWidth = cellBottomLabelMaxWidth(for: message) - return labelSize(for: bottomLabelText, considering: maxWidth) + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) } internal func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { @@ -200,48 +157,6 @@ open class MessageSizeCalculator: CellSizeCalculator { return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment } - internal func cellBottomLabelMaxWidth(for message: MessageType) -> CGFloat { - - let alignment = cellBottomLabelAlignment(for: message) - let avatarWidth = avatarSize(for: message).width - let containerSize = messageContainerSize(for: message) - let messagePadding = messageContainerPadding(for: message) - let position = avatarPosition(for: message) - - let avatarHorizontal = position.horizontal - let avatarVertical = position.vertical - - let itemWidth = messagesLayout.itemWidth - - switch (alignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - alignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = containerSize.width + messagePadding.left - alignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = containerSize.width + messagePadding.right - alignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - alignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - alignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - // MARK: - MessageContainer internal func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { @@ -274,8 +189,8 @@ open class MessageSizeCalculator: CellSizeCalculator { let estimatedHeight = attributedText.height(considering: maxWidth) let estimatedWidth = attributedText.width(considering: estimatedHeight) - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) + let finalHeight = ceil(estimatedHeight) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : ceil(estimatedWidth) return CGSize(width: finalWidth, height: finalHeight) } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 93e9477b..381aec4b 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -157,4 +157,11 @@ extension MessagesCollectionViewFlowLayout { } return messagesDataSource } + + internal var messagesLayoutDelegate: MessagesLayoutDelegate { + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) + } + return messagesLayoutDelegate + } } diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index 60944c73..c7589734 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -37,10 +37,10 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) var messageLabelInsets: UIEdgeInsets = .zero - var topLabelAlignment = LabelAlignment.cellCenter(.zero) + var topLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) var topLabelSize: CGSize = .zero - var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) + var bottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) var bottomLabelSize: CGSize = .zero // MARK: - Methods diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift index b2d66361..c1566380 100644 --- a/Sources/Models/LabelAlignment.swift +++ b/Sources/Models/LabelAlignment.swift @@ -24,38 +24,9 @@ import UIKit -/// An enum represnting the horizontal alignment of a `MessageCollectionViewCell`'s top and bottom labels. -public enum LabelAlignment { +public struct LabelAlignment { - /// Aligns the label's trailing edge to the cell's trailing edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case cellTrailing(UIEdgeInsets) - - /// Aligns the label's leading edge to the cell's leading edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case cellLeading(UIEdgeInsets) - - /// Aligns the label's center to the cell's center. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case cellCenter(UIEdgeInsets) - - /// Aligns the label's trailing edge to the `MessageContainerView`'s trailing edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case messageTrailing(UIEdgeInsets) - - /// Aligns the label's leading edge to the `MessageContainerView`'s leading edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case messageLeading(UIEdgeInsets) - - /// Returns the `UIEdgeInsets` associated value for the `LabelAlignment` case. - public var insets: UIEdgeInsets { - switch self { - case .cellTrailing(let insets): return insets - case .cellLeading(let insets): return insets - case .cellCenter(let insets): return insets - case .messageTrailing(let insets): return insets - case .messageLeading(let insets): return insets - } - } + public var textAlignment: NSTextAlignment + public var textInsets: UIEdgeInsets } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 59704738..f3d8b9c3 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -81,10 +81,27 @@ public protocol MessagesLayoutDelegate: AnyObject { @available(*, deprecated: 1.0, message: "heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat + } public extension MessagesLayoutDelegate { + // MARK: - All Messages Defaults + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + // TODO: - More sensible default, this is just for testing + return 10 + } + + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + // TODO: - More sensible default, this is just for testing + return 10 + } + func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 2507402f..d4dc506f 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -39,14 +39,14 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab return containerView }() - open var cellTopLabel: UILabel = { - let label = UILabel() + open var cellTopLabel: InsetLabel = { + let label = InsetLabel() label.numberOfLines = 0 return label }() - open var cellBottomLabel: UILabel = { - let label = UILabel() + open var cellBottomLabel: InsetLabel = { + let label = InsetLabel() label.numberOfLines = 0 return label }() @@ -185,7 +185,7 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab guard attributes.messageContainerSize != .zero else { return } var origin: CGPoint = .zero - origin.y = attributes.topLabelSize.height + attributes.messageContainerPadding.top + attributes.topLabelAlignment.insets.vertical + origin.y = attributes.topLabelSize.height + attributes.messageContainerPadding.top switch attributes.avatarPosition.horizontal { case .cellLeading: @@ -204,28 +204,10 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab open func layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { guard attributes.topLabelSize != .zero else { return } - let topLabelAlignment = attributes.topLabelAlignment - let topLabelPadding = topLabelAlignment.insets - let topLabelSize = attributes.topLabelSize + cellTopLabel.textAlignment = attributes.topLabelAlignment.textAlignment + cellTopLabel.textInsets = attributes.topLabelAlignment.textInsets - var origin: CGPoint = .zero - - origin.y = topLabelPadding.top - - switch attributes.topLabelAlignment { - case .cellLeading: - origin.x = topLabelPadding.left - case .cellCenter: - origin.x = (attributes.frame.width/2) + topLabelPadding.left - topLabelPadding.right - case .cellTrailing: - origin.x = attributes.frame.width - topLabelSize.width - topLabelPadding.right - case .messageLeading: // Needs messageContainerView frame to be set - origin.x = messageContainerView.frame.minX + topLabelPadding.left - case .messageTrailing: // Needs messageContainerView frame to be set - origin.x = messageContainerView.frame.maxX - topLabelSize.width - topLabelPadding.right - } - - cellTopLabel.frame = CGRect(origin: origin, size: topLabelSize) + cellTopLabel.frame = CGRect(origin: .zero, size: attributes.topLabelSize) } /// Positions the cell's bottom label. @@ -233,27 +215,13 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { guard attributes.bottomLabelSize != .zero else { return } - let bottomLabelAlignment = attributes.bottomLabelAlignment - let bottomLabelPadding = bottomLabelAlignment.insets - let bottomLabelSize = attributes.bottomLabelSize + cellBottomLabel.textAlignment = attributes.bottomLabelAlignment.textAlignment + cellBottomLabel.textInsets = attributes.bottomLabelAlignment.textInsets - var origin: CGPoint = .zero + let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom + let origin = CGPoint(x: 0, y: y) - origin.y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom + bottomLabelPadding.top - switch bottomLabelAlignment { - case .cellLeading: - origin.x = bottomLabelPadding.left - case .cellCenter: - origin.x = (attributes.frame.width/2) + bottomLabelPadding.left - bottomLabelPadding.right - case .cellTrailing: - origin.x = attributes.frame.width - bottomLabelSize.width - bottomLabelPadding.right - case .messageLeading: - origin.x = messageContainerView.frame.minX + bottomLabelPadding.left - case .messageTrailing: - origin.x = messageContainerView.frame.maxX - bottomLabelSize.width - bottomLabelPadding.right - } - - cellBottomLabel.frame = CGRect(origin: origin, size: bottomLabelSize) + cellBottomLabel.frame = CGRect(origin: origin, size: attributes.bottomLabelSize) } } diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index e46c1e59..6363211a 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -24,6 +24,19 @@ import UIKit +open class InsetLabel: UILabel { + + open var textInsets: UIEdgeInsets = .zero { + didSet { setNeedsDisplay() } + } + + open override func drawText(in rect: CGRect) { + let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) + super.drawText(in: insetRect) + } + +} + open class MessageLabel: UILabel { // MARK: - Private Properties From c3bb03bd02c779faa481d51170591cebfc2981e9 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 02:21:29 -0500 Subject: [PATCH 31/70] Add InsetLabel class --- MessageKit.xcodeproj/project.pbxproj | 4 +++ Sources/Views/InsetLabel.swift | 38 ++++++++++++++++++++++++++++ Sources/Views/MessageLabel.swift | 13 ---------- 3 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 Sources/Views/InsetLabel.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index f11ff957..0cf4ac78 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */; }; 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */; }; 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */; }; + 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A7206633C0007FA024 /* InsetLabel.swift */; }; 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 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 */; }; @@ -132,6 +133,7 @@ 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSizeCalculator.swift; sourceTree = ""; }; 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaMessageSizeCalculator.swift; sourceTree = ""; }; 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A7206633C0007FA024 /* InsetLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLabel.swift; sourceTree = ""; }; 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; @@ -409,6 +411,7 @@ B7A03F411F86694F006AEF79 /* InputBarItem.swift */, B7A03F401F86694F006AEF79 /* InputTextView.swift */, 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */, + 1FE783A7206633C0007FA024 /* InsetLabel.swift */, B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, B7A03F421F86694F006AEF79 /* MessageInputBar.swift */, B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */, @@ -603,6 +606,7 @@ B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, + 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, 1FE783A02066286A007FA024 /* CellSizeCalculator.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, diff --git a/Sources/Views/InsetLabel.swift b/Sources/Views/InsetLabel.swift new file mode 100644 index 00000000..b543a8ff --- /dev/null +++ b/Sources/Views/InsetLabel.swift @@ -0,0 +1,38 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class InsetLabel: UILabel { + + open var textInsets: UIEdgeInsets = .zero { + didSet { setNeedsDisplay() } + } + + open override func drawText(in rect: CGRect) { + let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) + super.drawText(in: insetRect) + } + +} diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index 6363211a..e46c1e59 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -24,19 +24,6 @@ import UIKit -open class InsetLabel: UILabel { - - open var textInsets: UIEdgeInsets = .zero { - didSet { setNeedsDisplay() } - } - - open override func drawText(in rect: CGRect) { - let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) - super.drawText(in: insetRect) - } - -} - open class MessageLabel: UILabel { // MARK: - Private Properties From 66ed7a0817ad884551c3650f19fb6a79b4bea899 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 02:52:34 -0500 Subject: [PATCH 32/70] Modify default behavior for insets and height --- .../Sources/ConversationViewController.swift | 10 ++++- Sources/Layout/MessageSizeCalculator.swift | 14 ++++-- .../Protocols/MessagesLayoutDelegate.swift | 45 ++++++++++++------- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 99b278af..915d7ebf 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -37,7 +37,7 @@ class ConversationViewController: MessagesViewController { override func viewDidLoad() { super.viewDidLoad() - let messagesToFetch = 10000 //UserDefaults.standard.mockMessagesCount() + let messagesToFetch = UserDefaults.standard.mockMessagesCount() DispatchQueue.global(qos: .userInitiated).async { SampleData.shared.getMessages(count: messagesToFetch) { messages in @@ -357,6 +357,14 @@ extension ConversationViewController: MessagesDisplayDelegate { extension ConversationViewController: MessagesLayoutDelegate { + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 16 + } + + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 16 + } + func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return CGSize(width: messagesCollectionView.bounds.width, height: 10) } diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 3f3a9e0e..c7a99a21 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -41,11 +41,11 @@ open class MessageSizeCalculator: CellSizeCalculator { public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: .zero) - public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: .zero) + public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: .zero) - public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: .zero) + public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) open func configure(attributes: UICollectionViewLayoutAttributes) { guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } @@ -195,3 +195,9 @@ open class MessageSizeCalculator: CellSizeCalculator { return CGSize(width: finalWidth, height: finalHeight) } } + +fileprivate extension UIEdgeInsets { + init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) { + self.init(top: top, left: left, bottom: bottom, right: right) + } +} diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index f3d8b9c3..0e4a380d 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -23,7 +23,6 @@ */ import Foundation -import AVFoundation /// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine /// the size and layout of a `MessageCollectionViewCell` and its contents. @@ -49,6 +48,26 @@ public protocol MessagesLayoutDelegate: AnyObject { /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + /// Specifies the height for the `MessageCollectionViewCell`'s top label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is zero. + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + /// Specifies the height for the `MessageCollectionViewCell`'s bottom label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is zero. + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat + @available(*, deprecated: 1.0, message: "avatarSize(for:at:in) has been removed in MessageKit 1.0") func shouldCacheLayoutAttributes(for message: MessageType) -> Bool @@ -82,26 +101,10 @@ public protocol MessagesLayoutDelegate: AnyObject { @available(*, deprecated: 1.0, message: "heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - } public extension MessagesLayoutDelegate { - // MARK: - All Messages Defaults - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - // TODO: - More sensible default, this is just for testing - return 10 - } - - func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - // TODO: - More sensible default, this is just for testing - return 10 - } - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) @@ -113,4 +116,12 @@ public extension MessagesLayoutDelegate { func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return .zero } + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 0 + } + + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 0 + } } From b9c626d405076b6b73c93b205418e4fb74e83ed0 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 24 Mar 2018 03:08:19 -0500 Subject: [PATCH 33/70] Add CHANGELOG entry for #580 --- CHANGELOG.md | 13 +++++++++++++ Sources/Layout/MessageSizeCalculator.swift | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2debcb1c..ec1e639b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,21 @@ the `MessageData.location` case. `isEnabled` state when text changes. (Default value is `true`). [#530](https://github.com/MessageKit/MessageKit/pull/530) by [@clayellis](https://github.com/clayellis). +- Added two new methods `cellTopLabelHeight(for:at:in)` and `cellBottomLabelHeight(for:at:in)` to `MessagesLayoutDelegate` +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + +- Added new class `InsetLabel`. +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + ### Changed +- **Breaking Change** Changed `LabelAlignment` to be a `struct` with properties of +`textAlignment: NSTextAlignment` and `textInsets: UIEdgeInsets` to position the text in the `cellTopLabel` and `cellBottomLabel`. +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + +- **Breaking Change** The type of `cellTopLabel` and `cellBottomLabel` has been changed to `InsetLabel`. +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + - The `MessageData.emoji` case no longer uses a default font of 2x the `messageLabelFont` size. You must now set this font explicitly through the `emojiMessageSizeCalculator` on `MessagesCollectionViewFlowLayout`. [#530](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index c7a99a21..a3a2422b 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -129,7 +129,7 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top Label - internal func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) From f70e1c82aafea80d7eb30af5a4e5072b4cd09395 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 27 Mar 2018 01:11:26 -0500 Subject: [PATCH 34/70] Remove deleted file --- ...sagesCollectionViewFlowLayout+Avatar.swift | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift deleted file mode 100644 index c9299456..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - 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 Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - /// Returns the `AvatarPosition` for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `avatarPosition(for:at:in)` in `MessagesLayoutDelegate`. - open func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - var position = messagesLayoutDelegate.avatarPosition(for: message, at: indexPath, in: messagesCollectionView) - - switch position.horizontal { - case .cellTrailing, .cellLeading: - break - case .natural: - position.horizontal = messagesDataSource.isFromCurrentSender(message: message) ? .cellTrailing : .cellLeading - } - return position - } - - /// Returns the `AvatarSize` for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `avatarSize(for:at:in)` in `MessagesLayoutDelegate`. - open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - return messagesLayoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView) - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - internal func _avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - - guard let cachedPosition = currentLayoutContext.avatarPosition else { - let position = avatarPosition(for: message, at: indexPath) - currentLayoutContext.avatarPosition = position - return position - } - return cachedPosition - } - - internal func _avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.avatarSize else { - let size = avatarSize(for: message, at: indexPath) - currentLayoutContext.avatarSize = size - return size - } - return cachedSize - } -} From 17398a2cb2e1240e73ad559deb61438c12ac67b2 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 27 Mar 2018 01:28:14 -0500 Subject: [PATCH 35/70] Add hook to configure async media on cell --- .../Protocols/MessagesDisplayDelegate.swift | 20 +++++++++++++++++++ Sources/Views/Cells/MediaMessageCell.swift | 7 +++++++ 2 files changed, 27 insertions(+) diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 75d3e72c..3e7dcb4f 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -164,6 +164,20 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - messagesCollectionView: The collection view requesting the information /// - Returns: Your customized animation block. func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? + + // MARK: - Media Messages + + /// Configure the `MediaMessageCell`s `UIImageView`. + /// + /// - Parameters: + /// - imageView: The `UIImageView` of the cell. + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// - Note: + /// The default implementation of this method does nothing. + func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + } public extension MessagesDisplayDelegate { @@ -240,4 +254,10 @@ public extension MessagesDisplayDelegate { return nil } + // MARK: - Media Message Defaults + + func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + + /* No-op Default */ + } } diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index c23eb414..5e922c4a 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -58,6 +58,11 @@ open class MediaMessageCell: MessageCollectionViewCell { open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } + switch message.data { case .photo(let mediaItem): imageView.image = mediaItem.image ?? mediaItem.placeholderImage @@ -68,5 +73,7 @@ open class MediaMessageCell: MessageCollectionViewCell { default: break } + + displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView) } } From 28199f6687127c2bf8f652f8159c5f0d4c57dee6 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 27 Mar 2018 01:35:46 -0500 Subject: [PATCH 36/70] Add CHANGELOG entry for #592 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec1e639b..f925e884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,10 @@ the `MessageData.location` case. - Added new class `InsetLabel`. [#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). +- Added new method `configureMediaMessageImageView(_:for:at:in)` to configure the `UIImageView` of a +`MediaMessageCell` asynchronously. +[#592](https://github.com/MessageKit/MessageKit/pull/592) by [@zhongwuzw](https://github.com/zhongwuzw), [@SD10](https://github.com/sd10) + ### Changed - **Breaking Change** Changed `LabelAlignment` to be a `struct` with properties of From 577e1bdc2163109ee7ea2e47cbf095fc2b588fa4 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 27 Mar 2018 01:50:13 -0500 Subject: [PATCH 37/70] Improve equality checking on layout attributes --- CHANGELOG.md | 3 +++ .../MessagesCollectionViewLayoutAttributes.swift | 16 +++++++++++++--- Sources/Models/AvatarPosition.swift | 10 ++++++++++ Sources/Models/LabelAlignment.swift | 10 ++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec1e639b..ff6e0315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,9 @@ You can now set this property through `textMessageSizeCalculator` property. - Fixed message bubble tail orientation invalidation in `iOS9`. [#469](https://github.com/MessageKit/MessageKit/pull/469) by [@zhongwuzw](https://github.com/zhongwuzw). +- Fixed equality checking on `MessagesCollectionViewLayoutAttributes`. +[#593](https://github.com/MessageKit/MessageKit/pull/593) by [@zhongwuzw](https://github.com/zhongwuzw), [@SD10](https://github.com/sd10) + ## [[Prerelease] 0.13.0](https://github.com/MessageKit/MessageKit/releases/tag/0.13.0) ### Fixed diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index c7589734..b80f780b 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -63,14 +63,24 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu } open override func isEqual(_ object: Any?) -> Bool { - // MARK: - LEAVE this as is // swiftlint:disable unused_optional_binding - if let _ = object as? MessagesCollectionViewLayoutAttributes { - return super.isEqual(object) + if let attributes = object as? MessagesCollectionViewLayoutAttributes { + return super.isEqual(object) && attributes.avatarSize == avatarSize + && attributes.avatarPosition == attributes.avatarPosition + && attributes.messageContainerSize == messageContainerSize + && attributes.messageContainerPadding == messageContainerPadding + && attributes.messageLabelFont == messageLabelFont + && attributes.messageLabelInsets == messageLabelInsets + && attributes.topLabelAlignment == topLabelAlignment + && attributes.topLabelSize == topLabelSize + && attributes.bottomLabelAlignment == bottomLabelAlignment + && attributes.bottomLabelSize == bottomLabelSize } else { return false } // swiftlint:enable unused_optional_binding } } + + diff --git a/Sources/Models/AvatarPosition.swift b/Sources/Models/AvatarPosition.swift index 35ea380c..4ea54f24 100644 --- a/Sources/Models/AvatarPosition.swift +++ b/Sources/Models/AvatarPosition.swift @@ -82,3 +82,13 @@ public struct AvatarPosition { } } + +// MARK: - Equatable Conformance + +extension AvatarPosition: Equatable { + + public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool { + return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal + } + +} diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift index c1566380..b75838de 100644 --- a/Sources/Models/LabelAlignment.swift +++ b/Sources/Models/LabelAlignment.swift @@ -30,3 +30,13 @@ public struct LabelAlignment { public var textInsets: UIEdgeInsets } + +// MARK: - Equatable Conformance + +extension LabelAlignment: Equatable { + + public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool { + return lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets + } + +} From 82dcf562d5962da6217e48b971b75cfc9477874b Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 27 Mar 2018 02:37:53 -0500 Subject: [PATCH 38/70] Refactor cell inheritance hierarchy --- Sources/Views/Cells/LocationMessageCell.swift | 6 +- Sources/Views/Cells/MediaMessageCell.swift | 6 +- .../Cells/MessageCollectionViewCell.swift | 188 +-------------- Sources/Views/Cells/MessageContentCell.swift | 227 ++++++++++++++++++ Sources/Views/Cells/TextMessageCell.swift | 6 +- 5 files changed, 241 insertions(+), 192 deletions(-) create mode 100644 Sources/Views/Cells/MessageContentCell.swift diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index 0534cb45..59c770a7 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -25,9 +25,11 @@ import UIKit import MapKit -open class LocationMessageCell: MessageCollectionViewCell { +open class LocationMessageCell: MessageContentCell { - open override class func reuseIdentifier() -> String { return "messagekit.cell.location" } + open override class func reuseIdentifier() -> String { + return "messagekit.cell.location" + } // MARK: - Properties diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index 5e922c4a..4278e70b 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -24,9 +24,11 @@ import UIKit -open class MediaMessageCell: MessageCollectionViewCell { +open class MediaMessageCell: MessageContentCell { - open override class func reuseIdentifier() -> String { return "messagekit.cell.mediamessage" } + open override class func reuseIdentifier() -> String { + return "messagekit.cell.mediamessage" + } // MARK: - Properties diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index d4dc506f..882adeff 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -30,198 +30,14 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab return "messagekit.cell.base-cell" } - open var avatarView = AvatarView() - - open var messageContainerView: MessageContainerView = { - let containerView = MessageContainerView() - containerView.clipsToBounds = true - containerView.layer.masksToBounds = true - return containerView - }() - - open var cellTopLabel: InsetLabel = { - let label = InsetLabel() - label.numberOfLines = 0 - return label - }() - - open var cellBottomLabel: InsetLabel = { - let label = InsetLabel() - label.numberOfLines = 0 - return label - }() - - open weak var delegate: MessageCellDelegate? + // MARK: - Initializers public override init(frame: CGRect) { super.init(frame: frame) - contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - setupSubviews() } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - open func setupSubviews() { - contentView.addSubview(messageContainerView) - contentView.addSubview(avatarView) - contentView.addSubview(cellTopLabel) - contentView.addSubview(cellBottomLabel) - } - - open override func prepareForReuse() { - super.prepareForReuse() - cellTopLabel.text = nil - cellTopLabel.attributedText = nil - cellBottomLabel.text = nil - cellBottomLabel.attributedText = nil - } - - // MARK: - Configuration - - open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { - super.apply(layoutAttributes) - guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - // Call this before other laying out other subviews - layoutMessageContainerView(with: attributes) - layoutAvatarView(with: attributes) - layoutBottomLabel(with: attributes) - layoutTopLabel(with: attributes) - } - - open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - - delegate = messagesCollectionView.messageCellDelegate - - let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) - let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView) - - displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView) - - messageContainerView.backgroundColor = messageColor - messageContainerView.style = messageStyle - - let topText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) - let bottomText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - - cellTopLabel.attributedText = topText - cellBottomLabel.attributedText = bottomText - } - - /// Handle tap gesture on contentView and its subviews like messageContainerView, cellTopLabel, cellBottomLabel, avatarView .... - open func handleTapGesture(_ gesture: UIGestureRecognizer) { - let touchLocation = gesture.location(in: self) - - switch true { - case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): - delegate?.didTapMessage(in: self) - case avatarView.frame.contains(touchLocation): - delegate?.didTapAvatar(in: self) - case cellTopLabel.frame.contains(touchLocation): - delegate?.didTapTopLabel(in: self) - case cellBottomLabel.frame.contains(touchLocation): - delegate?.didTapBottomLabel(in: self) - default: - break - } - } - - /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame - open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let touchPoint = gestureRecognizer.location(in: self) - guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false } - return messageContainerView.frame.contains(touchPoint) - } - - /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture - open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { - return false - } - - // MARK: - Origin Calculations - - /// Positions the cell's `AvatarView`. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.avatarSize != .zero else { return } - - var origin: CGPoint = .zero - - switch attributes.avatarPosition.horizontal { - case .cellLeading: - break - case .cellTrailing: - origin.x = attributes.frame.width - attributes.avatarSize.width - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - switch attributes.avatarPosition.vertical { - case .cellTop: - break - case .cellBottom: - origin.y = attributes.frame.height - attributes.avatarSize.height - case .messageTop: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.minY - case .messageBottom: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height - case .messageCenter: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2) - } - - avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize) - } - - /// Positions the cell's `MessageContainerView`. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.messageContainerSize != .zero else { return } - - var origin: CGPoint = .zero - origin.y = attributes.topLabelSize.height + attributes.messageContainerPadding.top - - switch attributes.avatarPosition.horizontal { - case .cellLeading: - origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left - case .cellTrailing: - origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) - } - - /// Positions the cell's top label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.topLabelSize != .zero else { return } - - cellTopLabel.textAlignment = attributes.topLabelAlignment.textAlignment - cellTopLabel.textInsets = attributes.topLabelAlignment.textInsets - - cellTopLabel.frame = CGRect(origin: .zero, size: attributes.topLabelSize) - } - - /// Positions the cell's bottom label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.bottomLabelSize != .zero else { return } - - cellBottomLabel.textAlignment = attributes.bottomLabelAlignment.textAlignment - cellBottomLabel.textInsets = attributes.bottomLabelAlignment.textInsets - - let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom - let origin = CGPoint(x: 0, y: y) - - - cellBottomLabel.frame = CGRect(origin: origin, size: attributes.bottomLabelSize) - } } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift new file mode 100644 index 00000000..43f04c57 --- /dev/null +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -0,0 +1,227 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class MessageContentCell: MessageCollectionViewCell { + + open override class func reuseIdentifier() -> String { + return "messagekit.cell.contentcell" + } + + open var avatarView = AvatarView() + + open var messageContainerView: MessageContainerView = { + let containerView = MessageContainerView() + containerView.clipsToBounds = true + containerView.layer.masksToBounds = true + return containerView + }() + + open var cellTopLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + return label + }() + + open var cellBottomLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + return label + }() + + open weak var delegate: MessageCellDelegate? + + public override init(frame: CGRect) { + super.init(frame: frame) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open func setupSubviews() { + contentView.addSubview(messageContainerView) + contentView.addSubview(avatarView) + contentView.addSubview(cellTopLabel) + contentView.addSubview(cellBottomLabel) + } + + open override func prepareForReuse() { + super.prepareForReuse() + cellTopLabel.text = nil + cellTopLabel.attributedText = nil + cellBottomLabel.text = nil + cellBottomLabel.attributedText = nil + } + + // MARK: - Configuration + + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } + // Call this before other laying out other subviews + layoutMessageContainerView(with: attributes) + layoutAvatarView(with: attributes) + layoutBottomLabel(with: attributes) + layoutTopLabel(with: attributes) + } + + open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } + + delegate = messagesCollectionView.messageCellDelegate + + let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) + let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView) + + displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView) + + messageContainerView.backgroundColor = messageColor + messageContainerView.style = messageStyle + + let topText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) + let bottomText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + + cellTopLabel.attributedText = topText + cellBottomLabel.attributedText = bottomText + } + + /// Handle tap gesture on contentView and its subviews like messageContainerView, cellTopLabel, cellBottomLabel, avatarView .... + open func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + + switch true { + case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): + delegate?.didTapMessage(in: self) + case avatarView.frame.contains(touchLocation): + delegate?.didTapAvatar(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapTopLabel(in: self) + case cellBottomLabel.frame.contains(touchLocation): + delegate?.didTapBottomLabel(in: self) + default: + break + } + } + + /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame + open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let touchPoint = gestureRecognizer.location(in: self) + guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false } + return messageContainerView.frame.contains(touchPoint) + } + + /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture + open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { + return false + } + + // MARK: - Origin Calculations + + /// Positions the cell's `AvatarView`. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.avatarSize != .zero else { return } + + var origin: CGPoint = .zero + + switch attributes.avatarPosition.horizontal { + case .cellLeading: + break + case .cellTrailing: + origin.x = attributes.frame.width - attributes.avatarSize.width + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) + } + + switch attributes.avatarPosition.vertical { + case .cellTop: + break + case .cellBottom: + origin.y = attributes.frame.height - attributes.avatarSize.height + case .messageTop: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.minY + case .messageBottom: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height + case .messageCenter: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2) + } + + avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize) + } + + /// Positions the cell's `MessageContainerView`. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.messageContainerSize != .zero else { return } + + var origin: CGPoint = .zero + origin.y = attributes.topLabelSize.height + attributes.messageContainerPadding.top + + switch attributes.avatarPosition.horizontal { + case .cellLeading: + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + case .cellTrailing: + origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) + } + + messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) + } + + /// Positions the cell's top label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.topLabelSize != .zero else { return } + + cellTopLabel.textAlignment = attributes.topLabelAlignment.textAlignment + cellTopLabel.textInsets = attributes.topLabelAlignment.textInsets + + cellTopLabel.frame = CGRect(origin: .zero, size: attributes.topLabelSize) + } + + /// Positions the cell's bottom label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.bottomLabelSize != .zero else { return } + + cellBottomLabel.textAlignment = attributes.bottomLabelAlignment.textAlignment + cellBottomLabel.textInsets = attributes.bottomLabelAlignment.textInsets + + let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom + let origin = CGPoint(x: 0, y: y) + + cellBottomLabel.frame = CGRect(origin: origin, size: attributes.bottomLabelSize) + } + +} diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 242d713e..84e24826 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -24,9 +24,11 @@ import UIKit -open class TextMessageCell: MessageCollectionViewCell { +open class TextMessageCell: MessageContentCell { - open override class func reuseIdentifier() -> String { return "messagekit.cell.text" } + open override class func reuseIdentifier() -> String { + return "messagekit.cell.text" + } // MARK: - Properties From 800396c13e797ae8dc4ae9d46c02b3f18baedec1 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 27 Mar 2018 02:38:07 -0500 Subject: [PATCH 39/70] Refactor reusable view inheritance hierarchy --- MessageKit.xcodeproj/project.pbxproj | 8 ++++ .../MessagesViewController+Menu.swift | 2 +- ...ssagesCollectionViewLayoutAttributes.swift | 4 -- .../MessageDateHeaderView.swift | 5 ++- .../Headers & Footers/MessageFooterView.swift | 7 ++- .../Headers & Footers/MessageHeaderView.swift | 7 ++- .../MessageReusableView.swift | 43 +++++++++++++++++++ Sources/Views/MessagesCollectionView.swift | 2 +- 8 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 Sources/Views/Headers & Footers/MessageReusableView.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 0cf4ac78..0905ab08 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */; }; 1F087D451FD274FD00E95A45 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1F087D461FD274FD00E95A45 /* Quick.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */; }; + 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */; }; 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; }; 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; @@ -120,6 +122,8 @@ 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderSpec.swift; sourceTree = ""; }; 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSpec.swift; sourceTree = ""; }; 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorTypeSpec.swift; sourceTree = ""; }; + 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentCell.swift; sourceTree = ""; }; + 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReusableView.swift; sourceTree = ""; }; 1F7FC8C31FD26F22006CC979 /* Carthage */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Carthage; sourceTree = ""; }; 1F7FC8C51FD26F33006CC979 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; 1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; @@ -237,6 +241,7 @@ B7A03F381F866946006AEF79 /* LocationMessageCell.swift */, B7A03F391F866946006AEF79 /* MediaMessageCell.swift */, B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */, + 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */, B7A03F361F866946006AEF79 /* TextMessageCell.swift */, ); path = Cells; @@ -248,6 +253,7 @@ B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */, B7A03F311F866940006AEF79 /* MessageFooterView.swift */, B7A03F321F866940006AEF79 /* MessageHeaderView.swift */, + 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); path = "Headers & Footers"; sourceTree = ""; @@ -611,6 +617,7 @@ B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, + 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */, @@ -652,6 +659,7 @@ 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, + 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, B7A03F271F866895006AEF79 /* Avatar.swift in Sources */, 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index a1fa9de6..e9a944e1 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -57,7 +57,7 @@ extension MessagesViewController { currentMenuController.setMenuVisible(false, animated: false) - guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return } + guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else { return } let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) var messageInputBarFrame: CGRect = .zero diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index b80f780b..f9709b35 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -64,7 +64,6 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu open override func isEqual(_ object: Any?) -> Bool { // MARK: - LEAVE this as is - // swiftlint:disable unused_optional_binding if let attributes = object as? MessagesCollectionViewLayoutAttributes { return super.isEqual(object) && attributes.avatarSize == avatarSize && attributes.avatarPosition == attributes.avatarPosition @@ -79,8 +78,5 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu } else { return false } - // swiftlint:enable unused_optional_binding } } - - diff --git a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift index 9f54583e..7ea470b7 100644 --- a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift +++ b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift @@ -25,7 +25,10 @@ import UIKit open class MessageDateHeaderView: MessageHeaderView { - open override class func reuseIdentifier() -> String { return "messagekit.header.date" } + + open override class func reuseIdentifier() -> String { + return "messagekit.header.date" + } // MARK: - Properties diff --git a/Sources/Views/Headers & Footers/MessageFooterView.swift b/Sources/Views/Headers & Footers/MessageFooterView.swift index e7df9b56..df466368 100644 --- a/Sources/Views/Headers & Footers/MessageFooterView.swift +++ b/Sources/Views/Headers & Footers/MessageFooterView.swift @@ -24,8 +24,11 @@ import UIKit -open class MessageFooterView: UICollectionReusableView, CollectionViewReusable { - open class func reuseIdentifier() -> String { return "messagekit.footer.base" } +open class MessageFooterView: MessageReusableView { + + open override class func reuseIdentifier() -> String { + return "messagekit.footer.base" + } // MARK: - Initializers diff --git a/Sources/Views/Headers & Footers/MessageHeaderView.swift b/Sources/Views/Headers & Footers/MessageHeaderView.swift index 789efdb7..fea13acb 100644 --- a/Sources/Views/Headers & Footers/MessageHeaderView.swift +++ b/Sources/Views/Headers & Footers/MessageHeaderView.swift @@ -24,8 +24,11 @@ import UIKit -open class MessageHeaderView: UICollectionReusableView, CollectionViewReusable { - open class func reuseIdentifier() -> String { return "messagekit.header.base" } +open class MessageHeaderView: MessageReusableView { + + open override class func reuseIdentifier() -> String { + return "messagekit.header.base" + } // MARK: - Properties diff --git a/Sources/Views/Headers & Footers/MessageReusableView.swift b/Sources/Views/Headers & Footers/MessageReusableView.swift new file mode 100644 index 00000000..cba573b3 --- /dev/null +++ b/Sources/Views/Headers & Footers/MessageReusableView.swift @@ -0,0 +1,43 @@ +/* + 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 Foundation + +open class MessageReusableView: UICollectionReusableView, CollectionViewReusable { + + open class func reuseIdentifier() -> String { + return "messagekit.reusable.base" + } + + // MARK: - Initializers + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 44a503d8..6184277c 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -77,7 +77,7 @@ open class MessagesCollectionView: UICollectionView { let touchLocation = gesture.location(in: self) guard let indexPath = indexPathForItem(at: touchLocation) else { return } - let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell + let cell = cellForItem(at: indexPath) as? MessageContentCell cell?.handleTapGesture(gesture) } From a79149e6cd6096004d6cfa75e39ed62f5648bc78 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 28 Mar 2018 06:18:57 -0500 Subject: [PATCH 40/70] Fix failing unit tests --- Tests/ViewsTests/MessageCollectionViewCellTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ViewsTests/MessageCollectionViewCellTests.swift b/Tests/ViewsTests/MessageCollectionViewCellTests.swift index b0583725..da92e331 100644 --- a/Tests/ViewsTests/MessageCollectionViewCellTests.swift +++ b/Tests/ViewsTests/MessageCollectionViewCellTests.swift @@ -25,14 +25,14 @@ import XCTest @testable import MessageKit -class MessageCollectionViewCellTests: XCTestCase { +final class MessageContentCellTests: XCTestCase { - var cell: MessageCollectionViewCell! + var cell: MessageContentCell! let frame = CGRect(x: 0, y: 0, width: 100, height: 100) override func setUp() { super.setUp() - cell = MessageCollectionViewCell(frame: frame) + cell = MessageContentCell(frame: frame) } override func tearDown() { @@ -74,7 +74,7 @@ class MessageCollectionViewCellTests: XCTestCase { } -extension MessageCollectionViewCellTests { +extension MessageContentCellTests { fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate { From 0e94329b97d0310f012e295d1dc3a2dc9220cc71 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 28 Mar 2018 06:23:46 -0500 Subject: [PATCH 41/70] Add CHANGELOG entry for #596 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db4f9f0..05303404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,8 +43,18 @@ the `MessageData.location` case. `MediaMessageCell` asynchronously. [#592](https://github.com/MessageKit/MessageKit/pull/592) by [@zhongwuzw](https://github.com/zhongwuzw), [@SD10](https://github.com/sd10) +- Added new `MessageContentCell` class and `MessageReusableView` class. +[#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). + ### Changed +- **Breaking Change** `MessageHeaderView` and `MessageFooterView` now subclass `MessageReusableView` class. +[#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Renamed `MessageCollectionViewCell` to `MessageContentCell`. +The `MessageCollectionViewCell` class is now a bare bones subclass. +[#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). + - **Breaking Change** Changed `LabelAlignment` to be a `struct` with properties of `textAlignment: NSTextAlignment` and `textInsets: UIEdgeInsets` to position the text in the `cellTopLabel` and `cellBottomLabel`. [#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). From 07c0e5859759b64095226a4f7c6606f8357e655e Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 29 Mar 2018 00:16:32 -0500 Subject: [PATCH 42/70] Remove the limitation of 1 cell to 1 section (#601) * Add numberOfSections delegate method * Remove numberOfMessages(in) and add other two substitution methods * Fix tests failure --- CHANGELOG.md | 6 ++++++ .../Sources/ConversationViewController.swift | 4 ++-- .../Controllers/MessagesViewController.swift | 7 ++----- Sources/Protocols/MessagesDataSource.swift | 17 +++++++++++++++-- .../MessagesViewControllerTests.swift | 2 +- Tests/Mocks/MockMessagesDataSource.swift | 2 +- 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05303404..729263af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Added +- **Breaking Change** Added new `MessagesDataSource` delegate methods, `numberOfSections(in)` and `numberOfItems(inSection:in)`. +[#601](https://github.com/MessageKit/MessageKit/pull/601) by [@SD10](https://github.com/sd10), [@zhongwuzw](https://github.com/zhongwuzw). + - **Breaking Change** Added new protocol `MediaItem` as the associated value for the `MessageData.video` and `MessageData.photo` cases. [#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). @@ -71,6 +74,9 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o ### Removed +- **Breaking Change** Removed `numberOfMessages(in)` method of `MessagesDataSource`, instead please use `numberOfSections(in)` and `numberOfItems(inSection:in)`. +[#601](https://github.com/MessageKit/MessageKit/pull/601) by [@SD10](https://github.com/sd10), [@zhongwuzw](https://github.com/zhongwuzw). + - **Breaking Change** Removed the `messageLabelFont` property from `MessagesCollectionViewFlowLayout`. You can now set this property through `textMessageSizeCalculator` property. [#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 915d7ebf..98793a84 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -260,8 +260,8 @@ extension ConversationViewController: MessagesDataSource { func currentSender() -> Sender { return SampleData.shared.currentSender } - - func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { + + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { return messageList.count } diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 45c3ba0d..a7a875ee 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -160,17 +160,14 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { guard let collectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } - // Each message is its own section - return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 + return collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 } open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard let collectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } - let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 - // There will only ever be 1 message per section - return messageCount > 0 ? 1 : 0 + return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 486510eb..a8751178 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -45,11 +45,20 @@ public protocol MessagesDataSource: AnyObject { /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed. func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType - /// The number of messages to be displayed in the `MessagesCollectionView`. + /// The number of sections to be displayed in the `MessagesCollectionView`. /// /// - Parameters: /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. - func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int + + /// The number of cells to be displayed in the `MessagesCollectionView`. + /// + /// - Parameters: + /// - section: The number of the section in which the cells will be displayed. + /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. + /// - Note: + /// The default implementation of this method returns 1. Putting each message in its own section. + func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int /// The attributed text to be used for cell's top label. /// @@ -79,6 +88,10 @@ public extension MessagesDataSource { return message.sender == currentSender() } + func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int { + return 1 + } + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } diff --git a/Tests/ControllersTest/MessagesViewControllerTests.swift b/Tests/ControllersTest/MessagesViewControllerTests.swift index 302fd999..520667be 100644 --- a/Tests/ControllersTest/MessagesViewControllerTests.swift +++ b/Tests/ControllersTest/MessagesViewControllerTests.swift @@ -87,7 +87,7 @@ class MessagesViewControllerTests: XCTestCase { sut.messagesCollectionView.reloadData() let count = sut.messagesCollectionView.numberOfSections - let expectedCount = messagesDataSource.numberOfMessages(in: sut.messagesCollectionView) + let expectedCount = messagesDataSource.numberOfSections(in: sut.messagesCollectionView) XCTAssertEqual(count, expectedCount) } diff --git a/Tests/Mocks/MockMessagesDataSource.swift b/Tests/Mocks/MockMessagesDataSource.swift index 9af1596c..566d8d27 100644 --- a/Tests/Mocks/MockMessagesDataSource.swift +++ b/Tests/Mocks/MockMessagesDataSource.swift @@ -35,7 +35,7 @@ class MockMessagesDataSource: MessagesDataSource { return senders[0] } - func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { return messages.count } From 7ac9ede82937d67576c30da39473551d097645b3 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 29 Mar 2018 15:38:45 +0800 Subject: [PATCH 43/70] make calculator of layour open --- .../Layout/MessagesCollectionViewFlowLayout.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 381aec4b..4a42d762 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -107,12 +107,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Sizing - var textMessageSizeCalculator = TextMessageSizeCalculator() - var attributedTextMessageSizeCalculator = TextMessageSizeCalculator() - var emojiMessageSizeCalculator = TextMessageSizeCalculator() - var photoMessageSizeCalculator = MediaMessageSizeCalculator() - var videoMessageSizeCalculator = MediaMessageSizeCalculator() - var locationMessageSizeCalculator = LocationMessageSizeCalculator() + open var textMessageSizeCalculator = TextMessageSizeCalculator() + open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator() + open var emojiMessageSizeCalculator = TextMessageSizeCalculator() + open var photoMessageSizeCalculator = MediaMessageSizeCalculator() + open var videoMessageSizeCalculator = MediaMessageSizeCalculator() + open var locationMessageSizeCalculator = LocationMessageSizeCalculator() open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> MessageSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) From 87a0baa31b01ea2ee5867bf266cf22892f4b0188 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 29 Mar 2018 23:13:08 +0800 Subject: [PATCH 44/70] Add explicit access level && some comments --- ...ssagesCollectionViewLayoutAttributes.swift | 20 +++++++++---------- Sources/Models/MessageData.swift | 3 +-- Sources/Protocols/LocationItem.swift | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index f9709b35..a9d7cb37 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -29,19 +29,19 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu // MARK: - Properties - var avatarSize: CGSize = .zero - var avatarPosition = AvatarPosition(vertical: .cellBottom) + internal var avatarSize: CGSize = .zero + internal var avatarPosition = AvatarPosition(vertical: .cellBottom) - var messageContainerSize: CGSize = .zero - var messageContainerPadding: UIEdgeInsets = .zero - var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - var messageLabelInsets: UIEdgeInsets = .zero + internal var messageContainerSize: CGSize = .zero + internal var messageContainerPadding: UIEdgeInsets = .zero + internal var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) + internal var messageLabelInsets: UIEdgeInsets = .zero - var topLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - var topLabelSize: CGSize = .zero + internal var topLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + internal var topLabelSize: CGSize = .zero - var bottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - var bottomLabelSize: CGSize = .zero + internal var bottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + internal var bottomLabelSize: CGSize = .zero // MARK: - Methods diff --git a/Sources/Models/MessageData.swift b/Sources/Models/MessageData.swift index b9b8f77b..0d3f8b91 100644 --- a/Sources/Models/MessageData.swift +++ b/Sources/Models/MessageData.swift @@ -23,7 +23,6 @@ */ import Foundation -import class CoreLocation.CLLocation /// An enum representing the kind of message and its underlying data. public enum MessageData { @@ -55,7 +54,7 @@ public enum MessageData { /// A custom message. /// - Note: Using this case requires that you override the following methods and handle this case: /// - `collectionView(_:cellForItemAt indexPath: IndexPath) -> UICollectionViewCell` - /// - `messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` + /// - `cellSizeCalculatorForItem(at indexPath: IndexPath) -> MessageSizeCalculator` case custom(Any?) // MARK: - Not supported yet diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift index fe037932..747c9c19 100644 --- a/Sources/Protocols/LocationItem.swift +++ b/Sources/Protocols/LocationItem.swift @@ -22,7 +22,7 @@ SOFTWARE. */ -import CoreLocation +import class CoreLocation.CLLocation public protocol LocationItem { From 5971523b41f5fe72a6c04028a6bc622b425d9634 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 29 Mar 2018 23:33:27 +0800 Subject: [PATCH 45/70] Support custom CellSizeCalculator not restrict to MessageSizeCalculator --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 2 +- Sources/Models/MessageData.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 4a42d762..5b2aa124 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -114,7 +114,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var videoMessageSizeCalculator = MediaMessageSizeCalculator() open var locationMessageSizeCalculator = LocationMessageSizeCalculator() - open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> MessageSizeCalculator { + open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.data { case .text: diff --git a/Sources/Models/MessageData.swift b/Sources/Models/MessageData.swift index 0d3f8b91..25f619ad 100644 --- a/Sources/Models/MessageData.swift +++ b/Sources/Models/MessageData.swift @@ -54,7 +54,7 @@ public enum MessageData { /// A custom message. /// - Note: Using this case requires that you override the following methods and handle this case: /// - `collectionView(_:cellForItemAt indexPath: IndexPath) -> UICollectionViewCell` - /// - `cellSizeCalculatorForItem(at indexPath: IndexPath) -> MessageSizeCalculator` + /// - `cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator` case custom(Any?) // MARK: - Not supported yet From aaae67ec65ba16593d1e4fa9fded93e9290e8b10 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 30 Mar 2018 10:09:46 +0800 Subject: [PATCH 46/70] Make sizeCalculator lazy && fix access level --- .../MessagesViewController+Keyboard.swift | 6 ++--- .../MessagesViewController+Menu.swift | 4 +-- .../Controllers/MessagesViewController.swift | 4 +-- Sources/Extensions/Bundle+Extensions.swift | 2 +- .../NSAttributedString+Extensions.swift | 4 +-- .../UICollectionView+Extensions.swift | 12 ++++----- Sources/Extensions/UIColor+Extensions.swift | 10 +++---- .../Extensions/UIEdgeInsets+Extensions.swift | 4 +-- Sources/Extensions/UIView+Extensions.swift | 8 +++--- .../MessagesCollectionViewFlowLayout.swift | 19 +++++--------- Sources/Models/DetectorType.swift | 2 +- Sources/Models/MessageKitError.swift | 26 +++++++++---------- Sources/Models/NSConstraintLayoutSet.swift | 4 +-- Sources/Views/MessagesCollectionView.swift | 2 +- 14 files changed, 50 insertions(+), 57 deletions(-) diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index 2fdbdba6..ab82e31d 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -28,13 +28,13 @@ extension MessagesViewController { // MARK: - Register / Unregister Observers - func addKeyboardObservers() { + internal func addKeyboardObservers() { NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: .UIKeyboardWillChangeFrame, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: .UITextViewTextDidBeginEditing, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil) } - func removeKeyboardObservers() { + internal func removeKeyboardObservers() { NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil) NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidBeginEditing, object: nil) NotificationCenter.default.removeObserver(self, name: .UIDeviceOrientationDidChange, object: nil) @@ -86,7 +86,7 @@ extension MessagesViewController { // MARK: - Helpers - var keyboardOffsetFrame: CGRect { + internal var keyboardOffsetFrame: CGRect { guard let inputFrame = inputAccessoryView?.frame else { return .zero } return CGRect(origin: inputFrame.origin, size: CGSize(width: inputFrame.width, height: inputFrame.height - iPhoneXBottomInset)) } diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index e9a944e1..26d1d65f 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -29,12 +29,12 @@ extension MessagesViewController { // MARK: - Register / Unregister Observers /// Add observer for `UIMenuControllerWillShowMenu` notification - func addMenuControllerObservers() { + internal func addMenuControllerObservers() { NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil) } /// Remove observer for `UIMenuControllerWillShowMenu` notification - func removeMenuControllerObservers() { + internal func removeMenuControllerObservers() { NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) } diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index a7a875ee..08477617 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -63,9 +63,9 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { private var isFirstLayout: Bool = true /// Indicated selected indexPath when handle menu action - var selectedIndexPathForMenu: IndexPath? + internal var selectedIndexPathForMenu: IndexPath? - var messageCollectionViewBottomInset: CGFloat = 0 { + internal var messageCollectionViewBottomInset: CGFloat = 0 { didSet { messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index 9d6f641e..c51036c6 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension Bundle { - static func messageKitAssetBundle() -> Bundle { + public static func messageKitAssetBundle() -> Bundle { let podBundle = Bundle(for: MessagesViewController.self) guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else { diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift index 77fb7093..bae07bd7 100644 --- a/Sources/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/Extensions/NSAttributedString+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension NSAttributedString { - func height(considering width: CGFloat) -> CGFloat { + public func height(considering width: CGFloat) -> CGFloat { let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) @@ -34,7 +34,7 @@ extension NSAttributedString { } - func width(considering height: CGFloat) -> CGFloat { + public func width(considering height: CGFloat) -> CGFloat { let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) diff --git a/Sources/Extensions/UICollectionView+Extensions.swift b/Sources/Extensions/UICollectionView+Extensions.swift index 9b624064..c5b42b01 100644 --- a/Sources/Extensions/UICollectionView+Extensions.swift +++ b/Sources/Extensions/UICollectionView+Extensions.swift @@ -29,21 +29,21 @@ public protocol CollectionViewReusable: AnyObject { static func reuseIdentifier() -> String } -public extension MessagesCollectionView { +extension MessagesCollectionView { /// Registers a particular cell using its reuse-identifier - func register(_ cellClass: CellType.Type) { + public func register(_ cellClass: CellType.Type) { register(cellClass, forCellWithReuseIdentifier: CellType.reuseIdentifier()) } /// Registers a reusable view for a specific SectionKind - func register(_ headerFooterClass: ViewType.Type, forSupplementaryViewOfKind kind: String) { + public func register(_ headerFooterClass: ViewType.Type, forSupplementaryViewOfKind kind: String) { register(headerFooterClass, forSupplementaryViewOfKind: kind, withReuseIdentifier: ViewType.reuseIdentifier()) } /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - func dequeueReusableCell(_ cellClass: CellType.Type, for indexPath: IndexPath) -> CellType { + public func dequeueReusableCell(_ cellClass: CellType.Type, for indexPath: IndexPath) -> CellType { guard let cell = dequeueReusableCell(withReuseIdentifier: cellClass.reuseIdentifier(), for: indexPath) as? CellType else { fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(cellClass.reuseIdentifier())") } @@ -51,7 +51,7 @@ public extension MessagesCollectionView { } /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - func dequeueReusableHeaderView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { + public func dequeueReusableHeaderView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: viewClass.reuseIdentifier(), for: indexPath) guard let viewType = view as? ViewType else { fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(viewClass.reuseIdentifier())") @@ -60,7 +60,7 @@ public extension MessagesCollectionView { } /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - func dequeueReusableFooterView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { + public func dequeueReusableFooterView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: viewClass.reuseIdentifier(), for: indexPath) guard let viewType = view as? ViewType else { fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(viewClass.reuseIdentifier())") diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift index 0295a63a..bf441676 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -26,14 +26,14 @@ import Foundation extension UIColor { - static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) + public static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) - static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) + public static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) - static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) + public static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) - static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) + public static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) - static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) + public static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) } diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift index 019b4343..2ec4b0f7 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -26,11 +26,11 @@ import Foundation extension UIEdgeInsets { - var vertical: CGFloat { + public var vertical: CGFloat { return top + bottom } - var horizontal: CGFloat { + public var horizontal: CGFloat { return left + right } diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift index f1519695..ec368f5a 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -26,7 +26,7 @@ import UIKit extension UIView { - func fillSuperview() { + public func fillSuperview() { guard let superview = self.superview else { return } @@ -41,7 +41,7 @@ extension UIView { NSLayoutConstraint.activate(constraints) } - func centerInSuperview() { + public func centerInSuperview() { guard let superview = self.superview else { return } @@ -53,7 +53,7 @@ extension UIView { NSLayoutConstraint.activate(constraints) } - func constraint(equalTo size: CGSize) { + public func constraint(equalTo size: CGSize) { guard superview != nil else { return } translatesAutoresizingMaskIntoConstraints = false let constraints: [NSLayoutConstraint] = [ @@ -65,7 +65,7 @@ extension UIView { } @discardableResult - func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { + public func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { if self.superview == nil { return [] diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 5b2aa124..08e43e00 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -43,13 +43,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { public override init() { super.init() - textMessageSizeCalculator.layout = self - attributedTextMessageSizeCalculator.layout = self - emojiMessageSizeCalculator.layout = self - photoMessageSizeCalculator.layout = self - videoMessageSizeCalculator.layout = self - locationMessageSizeCalculator.layout = self - sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: .UIDeviceOrientationDidChange, object: nil) @@ -107,12 +100,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Cell Sizing - open var textMessageSizeCalculator = TextMessageSizeCalculator() - open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator() - open var emojiMessageSizeCalculator = TextMessageSizeCalculator() - open var photoMessageSizeCalculator = MediaMessageSizeCalculator() - open var videoMessageSizeCalculator = MediaMessageSizeCalculator() - open var locationMessageSizeCalculator = LocationMessageSizeCalculator() + lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var emojiMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) + lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) + lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index 20ea43f7..a475cad6 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -38,7 +38,7 @@ public enum DetectorType { //case hashtag //case custom - var textCheckingType: NSTextCheckingResult.CheckingType { + public var textCheckingType: NSTextCheckingResult.CheckingType { switch self { case .address: return .address case .date: return .date diff --git a/Sources/Models/MessageKitError.swift b/Sources/Models/MessageKitError.swift index 5d2760ba..d4f3a5a4 100644 --- a/Sources/Models/MessageKitError.swift +++ b/Sources/Models/MessageKitError.swift @@ -22,17 +22,17 @@ SOFTWARE. */ -enum MessageKitError { - static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved." - static let nilMessagesDataSource = "MessagesDataSource has not been set." - static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set." - static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set." - static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView." - static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type." - static let unrecognizedSectionKind = "Received unrecognized element kind:" - static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType" - static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle" - static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle." - static let customDataUnresolvedCell = "Did not return a cell for MessageData.custom(Any)." - static let customDataUnresolvedSize = "Did not return a size for MessageData.custom(Any)." +internal enum MessageKitError { + internal static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved." + internal static let nilMessagesDataSource = "MessagesDataSource has not been set." + internal static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set." + internal static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set." + internal static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView." + internal static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type." + internal static let unrecognizedSectionKind = "Received unrecognized element kind:" + internal static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType" + internal static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle" + internal static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle." + internal static let customDataUnresolvedCell = "Did not return a cell for MessageData.custom(Any)." + internal static let customDataUnresolvedSize = "Did not return a size for MessageData.custom(Any)." } diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift index ff5b4679..f1efa5a0 100644 --- a/Sources/Models/NSConstraintLayoutSet.swift +++ b/Sources/Models/NSConstraintLayoutSet.swift @@ -59,7 +59,7 @@ public class NSLayoutConstraintSet { /// /// - Returns: Self @discardableResult - func activate() -> Self { + public func activate() -> Self { NSLayoutConstraint.activate(availableConstraints) return self } @@ -68,7 +68,7 @@ public class NSLayoutConstraintSet { /// /// - Returns: Self @discardableResult - func deactivate() -> Self { + public func deactivate() -> Self { NSLayoutConstraint.deactivate(availableConstraints) return self } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 6184277c..0f9f245d 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -64,7 +64,7 @@ open class MessagesCollectionView: UICollectionView { // MARK: - Methods - func setupGestureRecognizers() { + private func setupGestureRecognizers() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) tapGesture.delaysTouchesBegan = true addGestureRecognizer(tapGesture) From c82a7c0d1e5f98c254e8107c18f5aef63ed2ac8b Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 30 Mar 2018 12:49:31 +0800 Subject: [PATCH 47/70] update access control --- .swiftlint.yml | 8 ++++++-- .../Controllers/MessagesViewController+Keyboard.swift | 2 +- Sources/Extensions/Bundle+Extensions.swift | 2 +- Sources/Extensions/CGRect+Extensions.swift | 2 +- Sources/Extensions/NSAttributedString+Extensions.swift | 4 ++-- Sources/Extensions/UIColor+Extensions.swift | 10 +++++----- Sources/Extensions/UIEdgeInsets+Extensions.swift | 4 ++-- Sources/Extensions/UIView+Extensions.swift | 8 ++++---- Sources/Models/DetectorType.swift | 2 +- Sources/Models/MessageStyle.swift | 4 ++-- Sources/Views/InputStackView.swift | 2 +- Sources/Views/MessageLabel.swift | 2 +- 12 files changed, 27 insertions(+), 23 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 0189737e..8bf8a3da 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,6 +11,10 @@ custom_rules: regex: "override (open|public|private|internal|fileprivate)" # matching pattern message: "Use like open override or public override instead" # violation message. optional. severity: warning # violation severity. optional. +opt_in_rules: + - explicit_acl + - explicit_top_level_acl +explicit_acl: error +explicit_top_level_acl: error included: - - Sources - - Tests \ No newline at end of file + - Sources \ No newline at end of file diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index ab82e31d..1e5f9ff4 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -72,7 +72,7 @@ extension MessagesViewController { } @objc - func adjustScrollViewInset() { + internal func adjustScrollViewInset() { if #available(iOS 11.0, *) { // No need to add to the top contentInset } else { diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index c51036c6..0cda64b9 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension Bundle { - public static func messageKitAssetBundle() -> Bundle { + internal static func messageKitAssetBundle() -> Bundle { let podBundle = Bundle(for: MessagesViewController.self) guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else { diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift index 3b68368c..1a1177b5 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension CGRect { - init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { + internal init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { self.init(x: x, y: y, width: w, height: h) } diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift index bae07bd7..d27f0d3a 100644 --- a/Sources/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/Extensions/NSAttributedString+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension NSAttributedString { - public func height(considering width: CGFloat) -> CGFloat { + internal func height(considering width: CGFloat) -> CGFloat { let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) @@ -34,7 +34,7 @@ extension NSAttributedString { } - public func width(considering height: CGFloat) -> CGFloat { + internal func width(considering height: CGFloat) -> CGFloat { let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift index bf441676..79c4f4fc 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -26,14 +26,14 @@ import Foundation extension UIColor { - public static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) + internal static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) - public static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) + internal static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) - public static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) + internal static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) - public static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) + internal static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) - public static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) + internal static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) } diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift index 2ec4b0f7..4d99e081 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -26,11 +26,11 @@ import Foundation extension UIEdgeInsets { - public var vertical: CGFloat { + internal var vertical: CGFloat { return top + bottom } - public var horizontal: CGFloat { + internal var horizontal: CGFloat { return left + right } diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift index ec368f5a..b38d39a5 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -26,7 +26,7 @@ import UIKit extension UIView { - public func fillSuperview() { + internal func fillSuperview() { guard let superview = self.superview else { return } @@ -41,7 +41,7 @@ extension UIView { NSLayoutConstraint.activate(constraints) } - public func centerInSuperview() { + internal func centerInSuperview() { guard let superview = self.superview else { return } @@ -53,7 +53,7 @@ extension UIView { NSLayoutConstraint.activate(constraints) } - public func constraint(equalTo size: CGSize) { + internal func constraint(equalTo size: CGSize) { guard superview != nil else { return } translatesAutoresizingMaskIntoConstraints = false let constraints: [NSLayoutConstraint] = [ @@ -65,7 +65,7 @@ extension UIView { } @discardableResult - public func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { + internal func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { if self.superview == nil { return [] diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index a475cad6..a49b7b6d 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -38,7 +38,7 @@ public enum DetectorType { //case hashtag //case custom - public var textCheckingType: NSTextCheckingResult.CheckingType { + internal var textCheckingType: NSTextCheckingResult.CheckingType { switch self { case .address: return .address case .date: return .date diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift index 6c8e2451..22e9c684 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -35,7 +35,7 @@ public enum MessageStyle { case topRight case bottomRight - var imageOrientation: UIImageOrientation { + internal var imageOrientation: UIImageOrientation { switch self { case .bottomRight: return .up case .bottomLeft: return .upMirrored @@ -52,7 +52,7 @@ public enum MessageStyle { case curved case pointedEdge - var imageNameSuffix: String { + internal var imageNameSuffix: String { switch self { case .curved: return "_tail_v2" diff --git a/Sources/Views/InputStackView.swift b/Sources/Views/InputStackView.swift index f5ea3856..eb750906 100644 --- a/Sources/Views/InputStackView.swift +++ b/Sources/Views/InputStackView.swift @@ -46,7 +46,7 @@ open class InputStackView: UIStackView { // MARK: Initialization - convenience init(axis: UILayoutConstraintAxis, spacing: CGFloat) { + public convenience init(axis: UILayoutConstraintAxis, spacing: CGFloat) { self.init(frame: .zero) self.axis = axis self.spacing = spacing diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index e46c1e59..d1adada5 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -371,7 +371,7 @@ open class MessageLabel: UILabel { } - func handleGesture(_ touchLocation: CGPoint) -> Bool { + internal func handleGesture(_ touchLocation: CGPoint) -> Bool { guard let index = stringIndex(at: touchLocation) else { return false } From 9ce067ffdfbc049de48395c8548a5640c1df89ad Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 30 Mar 2018 12:56:45 +0800 Subject: [PATCH 48/70] make NSConstraintLayoutSet internal --- Sources/Models/NSConstraintLayoutSet.swift | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift index f1efa5a0..14922ee8 100644 --- a/Sources/Models/NSConstraintLayoutSet.swift +++ b/Sources/Models/NSConstraintLayoutSet.swift @@ -24,18 +24,18 @@ import UIKit -public class NSLayoutConstraintSet { +internal class NSLayoutConstraintSet { - public var top: NSLayoutConstraint? - public var bottom: NSLayoutConstraint? - public var left: NSLayoutConstraint? - public var right: NSLayoutConstraint? - public var centerX: NSLayoutConstraint? - public var centerY: NSLayoutConstraint? - public var width: NSLayoutConstraint? - public var height: NSLayoutConstraint? + internal var top: NSLayoutConstraint? + internal var bottom: NSLayoutConstraint? + internal var left: NSLayoutConstraint? + internal var right: NSLayoutConstraint? + internal var centerX: NSLayoutConstraint? + internal var centerY: NSLayoutConstraint? + internal var width: NSLayoutConstraint? + internal var height: NSLayoutConstraint? - public init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, + internal init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil, centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil, width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) { @@ -59,7 +59,7 @@ public class NSLayoutConstraintSet { /// /// - Returns: Self @discardableResult - public func activate() -> Self { + internal func activate() -> Self { NSLayoutConstraint.activate(availableConstraints) return self } @@ -68,7 +68,7 @@ public class NSLayoutConstraintSet { /// /// - Returns: Self @discardableResult - public func deactivate() -> Self { + internal func deactivate() -> Self { NSLayoutConstraint.deactivate(availableConstraints) return self } From 537756e652662569c0cfbb351fc5688d928eda0b Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 30 Mar 2018 16:01:49 +0800 Subject: [PATCH 49/70] Add CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 729263af..cfc41a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,9 @@ the `MessageData.location` case. ### Changed +- **Breaking Change** Change `NSLayoutConstraintSet` class from `public` to `internal`. +[#607](https://github.com/MessageKit/MessageKit/pull/607) by [@zhongwuzw](https://github.com/zhongwuzw). + - **Breaking Change** `MessageHeaderView` and `MessageFooterView` now subclass `MessageReusableView` class. [#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). From 04bfb7c5d794de2d506fb9589ec83c2ec5eb3f51 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Fri, 30 Mar 2018 10:43:41 -0500 Subject: [PATCH 50/70] Document all protocols --- .../MessagesViewController+Menu.swift | 2 - .../Controllers/MessagesViewController.swift | 15 ++-- Sources/Protocols/CellSizeCalculator.swift | 12 +++ Sources/Protocols/LocationItem.swift | 3 + Sources/Protocols/MediaItem.swift | 5 ++ Sources/Protocols/MessageCellDelegate.swift | 19 +++-- .../Protocols/MessageInputBarDelegate.swift | 21 ++--- Sources/Protocols/MessageLabelDelegate.swift | 23 ++++- Sources/Protocols/MessageType.swift | 4 +- Sources/Protocols/MessagesDataSource.swift | 11 ++- .../Protocols/MessagesDisplayDelegate.swift | 85 ++++++++++--------- .../Protocols/MessagesLayoutDelegate.swift | 50 +++-------- 12 files changed, 131 insertions(+), 119 deletions(-) diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index 26d1d65f..57317584 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -28,12 +28,10 @@ extension MessagesViewController { // MARK: - Register / Unregister Observers - /// Add observer for `UIMenuControllerWillShowMenu` notification internal func addMenuControllerObservers() { NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil) } - /// Remove observer for `UIMenuControllerWillShowMenu` notification internal func removeMenuControllerObservers() { NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) } diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 08477617..5617add3 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -24,10 +24,10 @@ import UIKit +/// A subclass of `UIViewController` with a `MessagesCollectionView` object +/// that is used to display conversation interfaces. open class MessagesViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { - - // MARK: - Properties [Public] /// The `MessagesCollectionView` managed by the messages view controller object. open var messagesCollectionView = MessagesCollectionView() @@ -59,10 +59,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return false } - /// A Boolean value used to determine if `viewDidLayoutSubviews()` has been called. private var isFirstLayout: Bool = true - - /// Indicated selected indexPath when handle menu action + internal var selectedIndexPathForMenu: IndexPath? internal var messageCollectionViewBottomInset: CGFloat = 0 { @@ -106,7 +104,6 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { // MARK: - Methods [Private] - /// Sets the default values for the MessagesViewController private func setupDefaults() { extendedLayoutIncludesOpaqueBars = true automaticallyAdjustsScrollViewInsets = false @@ -115,18 +112,15 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { messagesCollectionView.alwaysBounceVertical = true } - /// Sets the delegate and dataSource of the messagesCollectionView property. private func setupDelegates() { messagesCollectionView.delegate = self messagesCollectionView.dataSource = self } - /// Adds the messagesCollectionView to the controllers root view. private func setupSubviews() { view.addSubview(messagesCollectionView) } - /// Registers all cells and supplementary views of the messagesCollectionView property. private func registerReusableViews() { messagesCollectionView.register(TextMessageCell.self) messagesCollectionView.register(MediaMessageCell.self) @@ -137,7 +131,6 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { messagesCollectionView.register(MessageDateHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) } - /// Sets the constraints of the `MessagesCollectionView`. private func setupConstraints() { messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false @@ -301,6 +294,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { break } } + + // MARK: - Helpers private func addObservers() { NotificationCenter.default.addObserver( diff --git a/Sources/Protocols/CellSizeCalculator.swift b/Sources/Protocols/CellSizeCalculator.swift index 2aca310f..893ad8ed 100644 --- a/Sources/Protocols/CellSizeCalculator.swift +++ b/Sources/Protocols/CellSizeCalculator.swift @@ -24,11 +24,23 @@ import UIKit +/// An object that adopts the `CellSizeCalculator` protocol is responsible for +/// sizing and configuring cells for given `IndexPath`s. public protocol CellSizeCalculator { + /// The layout object for which the cell size calculator is used. weak var layout: UICollectionViewFlowLayout? { get set } + /// Used to configure the layout attributes for a given cell. + /// + /// - Parameters: + /// - attributes: The attributes of the cell. func configure(attributes: UICollectionViewLayoutAttributes) + /// Used to size an item at a given `IndexPath`. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the item to be displayed. func sizeForItem(at indexPath: IndexPath) -> CGSize + } diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift index 747c9c19..5cb12b5d 100644 --- a/Sources/Protocols/LocationItem.swift +++ b/Sources/Protocols/LocationItem.swift @@ -24,10 +24,13 @@ import class CoreLocation.CLLocation +/// A protocol used to represent the data for a location message. public protocol LocationItem { + /// The location. var location: CLLocation { get set } + /// The size of the location item. var size: CGSize { get set } } diff --git a/Sources/Protocols/MediaItem.swift b/Sources/Protocols/MediaItem.swift index 68d0c2de..10acfe12 100644 --- a/Sources/Protocols/MediaItem.swift +++ b/Sources/Protocols/MediaItem.swift @@ -24,14 +24,19 @@ import Foundation +/// A protocol used to represent the data for a media message. public protocol MediaItem { + /// The url where the media is located. var url: URL? { get set } + /// The image. var image: UIImage? { get set } + /// A placeholder image for when the image is obtained asychronously. var placeholderImage: UIImage { get set } + /// The size of the media item. var size: CGSize { get set } } diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift index 8a4e686f..80b4432b 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -24,43 +24,44 @@ import Foundation -/// A protocol used by `MessageCollectionViewCell` subclasses to detect taps in the cell's contents. +/// A protocol used by `MessageContentCell` subclasses to detect taps in the cell's subviews. public protocol MessageCellDelegate: MessageLabelDelegate { - /// Triggered when a touch occurs in the `MessageContainerView`. + /// Triggered when a tap occurs in the `MessageContainerView`. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - cell: The cell where the tap occurred. /// + /// - Note: /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` /// method `messageForItem(at:indexPath:messagesCollectionView)`. func didTapMessage(in cell: MessageCollectionViewCell) - /// Triggered when a touch occurs in the `AvatarView`. + /// Triggered when a tap occurs in the `AvatarView`. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - cell: The cell where the tap occurred. /// /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` /// method `messageForItem(at:indexPath:messagesCollectionView)`. func didTapAvatar(in cell: MessageCollectionViewCell) - /// Triggered when a touch occurs in the cellBottomLabel. + /// Triggered when a tap occurs in the cellBottomLabel. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - cell: The cell where the tap occurred. /// /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` /// method `messageForItem(at:indexPath:messagesCollectionView)`. func didTapBottomLabel(in cell: MessageCollectionViewCell) - /// Triggered when a touch occurs in the cellTopLabel. + /// Triggered when a tap occurs in the cellTopLabel. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - cell: The cell tap the touch occurred. /// /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` diff --git a/Sources/Protocols/MessageInputBarDelegate.swift b/Sources/Protocols/MessageInputBarDelegate.swift index 8c680f27..39c5b180 100644 --- a/Sources/Protocols/MessageInputBarDelegate.swift +++ b/Sources/Protocols/MessageInputBarDelegate.swift @@ -24,26 +24,27 @@ import Foundation -/// MessageInputBarDelegate is a protocol that can recieve notifications from the MessageInputBar +/// A protocol that can receive different event notifications from the MessageInputBar. public protocol MessageInputBarDelegate: AnyObject { - /// Called when the default send button has been selected + /// Called when the default send button has been selected. /// /// - Parameters: - /// - inputBar: The MessageInputBar - /// - text: The current text in the MessageInputBar's InputTextView + /// - inputBar: The `MessageInputBar`. + /// - text: The current text in the `InputTextView` of the `MessageInputBar`. func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) - /// Called when the instrinsicContentSize of the MessageInputBar has changed. Can be used for adjusting content insets - /// on other views to make sure the MessageInputBar does not cover up any other view + /// Called when the instrinsicContentSize of the MessageInputBar has changed. + /// Can be used for adjusting content insets on other views to make sure + /// the MessageInputBar does not cover up any other view. /// /// - Parameters: - /// - inputBar: The MessageInputBar - /// - size: The new instrinsicContentSize + /// - inputBar: The `MessageInputBar`. + /// - size: The new instrinsic content size. func messageInputBar(_ inputBar: MessageInputBar, didChangeIntrinsicContentTo size: CGSize) - /// Called when the MessageInputBar's InputTextView's text has changed. Useful for adding your own logic without the - /// need of assigning a delegate or notification + /// Called when the `MessageInputBar`'s `InputTextView`'s text has changed. + /// Useful for adding your own logic without the need of assigning a delegate or notification. /// /// - Parameters: /// - inputBar: The MessageInputBar diff --git a/Sources/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift index e19c9cf1..e84ea622 100644 --- a/Sources/Protocols/MessageLabelDelegate.swift +++ b/Sources/Protocols/MessageLabelDelegate.swift @@ -24,16 +24,37 @@ import Foundation +/// A protocol used to handle tap events on detected text. public protocol MessageLabelDelegate: AnyObject { + /// Triggered when a tap occurs on a detected address. + /// + /// - Parameters: + /// - addressComponents: The components of the selected address. func didSelectAddress(_ addressComponents: [String: String]) + /// Triggered when a tap occurs on a detected date. + /// + /// - Parameters: + /// - date: The selected date. func didSelectDate(_ date: Date) + /// Triggered when a tap occurs on a detected phone number. + /// + /// - Parameters: + /// - phoneNumber: The selected phone number. func didSelectPhoneNumber(_ phoneNumber: String) + /// Triggered when a tap occurs on a detected URL. + /// + /// - Parameters: + /// - url: The selected URL. func didSelectURL(_ url: URL) - + + /// Triggered when a tap occurs on detected transit information. + /// + /// - Parameters: + /// - transitInformation: The selected transit information. func didSelectTransitInformation(_ transitInformation: [String: String]) } diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift index 71adc853..7f687a5b 100644 --- a/Sources/Protocols/MessageType.swift +++ b/Sources/Protocols/MessageType.swift @@ -24,8 +24,8 @@ import Foundation -/// A standard protocol representing a message. Use this protocol to create -/// your own message object to be used by MessageKit. +/// A standard protocol representing a message. +/// Use this protocol to create your own message object to be used by MessageKit. public protocol MessageType { /// The sender of the message. diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index a8751178..0b1f65a9 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -24,18 +24,21 @@ import UIKit +/// An object that adopts the `MessagesDataSource` protocol is responsible for providing +/// the data required by a `MessagesCollectionView`. public protocol MessagesDataSource: AnyObject { /// The `Sender` of new messages in the `MessagesCollectionView`. func currentSender() -> Sender - /// A helper method to determine if a given message is from the current sender. + /// A helper method to determine if a given message is from the current `Sender`. /// /// - Parameters: - /// - message: The message to check if it was sent by the current Sender. + /// - message: The message to check if it was sent by the current `Sender`. /// - /// The default implementation of this method checks for equality between the message's `Sender` - /// and the current Sender. + /// - Note: + /// The default implementation of this method checks for equality between + /// the message's `Sender` and the current `Sender`. func isFromCurrentSender(message: MessageType) -> Bool /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`. diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 3e7dcb4f..034753a7 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -25,7 +25,7 @@ import Foundation import MapKit -/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageCollectionViewCell`. +/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageContentCell`. public protocol MessagesDisplayDelegate: AnyObject { // MARK: - All Messages @@ -37,7 +37,8 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is `MessageStyle.bubble`. + /// - Note: + /// The default value returned by this method is `MessageStyle.bubble`. func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle /// Specifies the background color of the `MessageContainerView`. @@ -47,11 +48,13 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value is `UIColor.clear` for emoji messages. For all other `MessageData` cases, the color depends on the `Sender`: + /// - Note: + /// The default value is `UIColor.clear` for emoji messages. + /// For all other `MessageData` cases, the color depends on the `Sender`. /// - /// Current Sender: Green + /// Current sender: Green /// - /// All other Senders: Gray + /// All other senders: Gray func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor /// The section header to use for a given `MessageType`. @@ -61,17 +64,20 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. /// - /// The default value returned by this method is a `MessageDateHeaderView`. + /// - Note: + /// The default value returned by this method is a `MessageDateHeaderView`. func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView /// Used by the `MessageLayoutDelegate` method `headerViewSize(_:_:_:)` to determine if a header should be displayed. - /// This method checks `MessageCollectionView`'s `showsDateHeaderAfterTimeInterval` property and returns true if - /// the current messages sent date occurs after the specified time interval when compared to the previous message. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this header. /// - indexPath: The `IndexPath` of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. + /// + /// - Note: + /// This method checks `MessageCollectionView`'s `showsDateHeaderAfterTimeInterval` property and returns true + /// if the current messages sent date occurs after the specified time interval when compared to the previous message. func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool /// The section footer to use for a given `MessageType`. @@ -81,10 +87,11 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. /// - /// The default value returned by this method is a `MessageFooterView`. + /// - Note: + /// The default value returned by this method is a `MessageFooterView`. func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView - /// Configure `AvatarView`‘s image. + /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class. /// /// - Parameters: /// - avatarView: The `AvatarView` of the cell. @@ -92,7 +99,8 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default image configured by this method is `?`. + /// - Note: + /// The default image configured by this method is `?`. func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) // MARK: - Text Messages @@ -104,11 +112,12 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is determined by the messages `Sender`: + /// - Note: + /// The default value returned by this method is determined by the messages `Sender`. /// - /// Current Sender: UIColor.white + /// Current sender: UIColor.white /// - /// All other Senders: UIColor.darkText + /// All other senders: UIColor.darkText func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor /// Specifies the `DetectorType`s to check for the `MessageType`'s text against. @@ -118,64 +127,57 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is all available detector types. + /// - Note: + /// This method returns an empty array by default. func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] /// Specifies the attributes for a given `DetectorType` /// /// - Parameters: /// - detector: The `DetectorType` for the applied attributes. - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` - /// to which the detectors will apply. + /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the detectors will apply. /// - indexPath: The `IndexPath` of the cell. func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] // MARK: - Location Messages - /// Ask the delegate for a LocationMessageSnapshotOptions instance to customize the MapView on the given message + /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message. /// /// - Parameters: - /// - message: The location message to be customized - /// - indexPath: Message's index path - /// - messagesCollectionView: The collection view requesting the information - /// - Returns: Your LocationMessageSnapshotOptions instance with the options to customize map style + /// - message: A `MessageType` with a `MessageData` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style. func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions - /// Ask the delegate for a custom MKAnnotationView to show on the given message. - /// You can return nil if you don't want to show any annotation. - /// - /// default: MKPinAnnotationView + /// Used to configure the annoation view of the map image on the given location message. /// /// - Parameters: - /// - message: The location message with the annotation to customize - /// - indexPath: Message's index path - /// - messageCollectionView: The collection view requesting the information - /// - Returns: Your customized MKAnnotationView or nil to not show any. + /// - message: A `MessageType` with a `MessageData` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The `MKAnnotationView` to use as the annotation view. func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? - /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message - /// The animation block is called with the image view to be animated. You can animate it with CoreAnimation, UIView.animate or any library you prefer. - /// - /// default: nil + /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message. + /// The animation block is called with the `UIImageView` to be animated. /// /// - Parameters: - /// - message: The location message with the map to animate - /// - indexPath: Message's index path - /// - messagesCollectionView: The collection view requesting the information - /// - Returns: Your customized animation block. + /// - message: A `MessageType` with a `MessageData` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The animation block to use to apply the location image. func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? // MARK: - Media Messages - /// Configure the `MediaMessageCell`s `UIImageView`. + /// Used to configure the `UIImageView` of a `MediaMessageCell. /// /// - Parameters: /// - imageView: The `UIImageView` of the cell. /// - message: The `MessageType` that will be displayed by this cell. /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - Note: - /// The default implementation of this method does nothing. func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) } @@ -258,6 +260,5 @@ public extension MessagesDisplayDelegate { func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - /* No-op Default */ } } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 0e4a380d..b6ef3e3c 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -35,7 +35,9 @@ public protocol MessagesLayoutDelegate: AnyObject { /// - indexPath: The `IndexPath` of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. /// - /// The default value returned by this method is the width of the `MessagesCollectionView` and a height of 12. + /// - Note: + /// The default value returned by this method is the width of the `MessagesCollectionView` + /// and a height of 12. func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize /// Specifies the size to use for a `MessageFooterView`. @@ -45,62 +47,32 @@ public protocol MessagesLayoutDelegate: AnyObject { /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. /// - /// The default value returned by this method is a size of `GGSize.zero`. + /// - Note: + /// The default value returned by this method is a size of `GGSize.zero`. func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - /// Specifies the height for the `MessageCollectionViewCell`'s top label. + /// Specifies the height for the `MessageContentCell`'s top label. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this cell. /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is zero. + /// - Note: + /// The default value returned by this method is zero. func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - /// Specifies the height for the `MessageCollectionViewCell`'s bottom label. + /// Specifies the height for the `MessageContentCell`'s bottom label. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this cell. /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is zero. + /// - Note: + /// The default value returned by this method is zero. func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - @available(*, deprecated: 1.0, message: "avatarSize(for:at:in) has been removed in MessageKit 1.0") - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool - - @available(*, deprecated: 1.0, message: "avatarSize(for:at:in) has been removed in MessageKit 1.0") - func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - - @available(*, deprecated: 1.0, message: "avatarPosition(for:at:in) has been removed in MessageKit 1.0") - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition - - @available(*, deprecated: 1.0, message: "messageLabelInset(for:at:in) has been removed in MessageKit 1.0.") - func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets - - @available(*, deprecated: 1.0, message: "messagePadding(for:at:in) has been removed in MessageKit 1.0.") - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets - - @available(*, deprecated: 1.0, message: "cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment - - @available(*, deprecated: 1.0, message: "cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment - - @available(*, deprecated: 1.0, message: "widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.") - func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - @available(*, deprecated: 1.0, message: "heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.") - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - @available(*, deprecated: 1.0, message: "widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.") - func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - @available(*, deprecated: 1.0, message: "heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - } public extension MessagesLayoutDelegate { From 1abcf9757678d3d70671906cae468820fe7a479b Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Fri, 30 Mar 2018 11:18:11 -0500 Subject: [PATCH 51/70] Add documentation for cells --- Sources/Views/Cells/LocationMessageCell.swift | 6 ++++-- Sources/Views/Cells/MediaMessageCell.swift | 6 ++++-- Sources/Views/Cells/MessageCollectionViewCell.swift | 2 ++ Sources/Views/Cells/MessageContentCell.swift | 12 ++++++++++++ Sources/Views/Cells/TextMessageCell.swift | 7 ++++++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index 59c770a7..994cf945 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -25,16 +25,17 @@ import UIKit import MapKit +/// A subclass of `MessageContentCell` used to display location messages. open class LocationMessageCell: MessageContentCell { open override class func reuseIdentifier() -> String { return "messagekit.cell.location" } - // MARK: - Properties - + /// The activity indicator to be displayed while the map image is loading. open var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + /// The image view holding the map image. open var imageView = UIImageView() private weak var snapShotter: MKMapSnapshotter? @@ -47,6 +48,7 @@ open class LocationMessageCell: MessageContentCell { setupConstraints() } + /// Responsible for setting up the constraints of the cell's subviews. open func setupConstraints() { imageView.fillSuperview() activityIndicator.centerInSuperview() diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index 4278e70b..0d02b342 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -24,19 +24,20 @@ import UIKit +/// A subclass of `MessageContentCell` used to display video and audio messages. open class MediaMessageCell: MessageContentCell { open override class func reuseIdentifier() -> String { return "messagekit.cell.mediamessage" } - // MARK: - Properties - + /// The play button view to display on video messages. open lazy var playButtonView: PlayButtonView = { let playButtonView = PlayButtonView() return playButtonView }() + /// The image view display the media content. open var imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill @@ -45,6 +46,7 @@ open class MediaMessageCell: MessageContentCell { // MARK: - Methods + /// Responsible for setting up the constraints of the cell's subviews. open func setupConstraints() { imageView.fillSuperview() playButtonView.centerInSuperview() diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 882adeff..1a842047 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -24,8 +24,10 @@ import UIKit +/// A subclass of `UICollectionViewCell` to be used inside of a `MessagesCollectionView`. open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusable { + /// The reuse identifier of the cell. open class func reuseIdentifier() -> String { return "messagekit.cell.base-cell" } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index 43f04c57..dd599958 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -24,14 +24,17 @@ import UIKit +/// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages. open class MessageContentCell: MessageCollectionViewCell { open override class func reuseIdentifier() -> String { return "messagekit.cell.contentcell" } + /// The image view displaying the avatar. open var avatarView = AvatarView() + /// The container used for styling and holding the message's content view. open var messageContainerView: MessageContainerView = { let containerView = MessageContainerView() containerView.clipsToBounds = true @@ -39,18 +42,21 @@ open class MessageContentCell: MessageCollectionViewCell { return containerView }() + /// The top label of the cell. open var cellTopLabel: InsetLabel = { let label = InsetLabel() label.numberOfLines = 0 return label }() + /// The bottom label of the cell. open var cellBottomLabel: InsetLabel = { let label = InsetLabel() label.numberOfLines = 0 return label }() + /// The `MessageCellDelegate` for the cell. open weak var delegate: MessageCellDelegate? public override init(frame: CGRect) { @@ -90,6 +96,12 @@ open class MessageContentCell: MessageCollectionViewCell { layoutTopLabel(with: attributes) } + /// Used to configure the cell. + /// + /// - Parameters: + /// - message: The `MessageType` this cell displays. + /// - indexPath: The `IndexPath` for this cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained. open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { guard let dataSource = messagesCollectionView.messagesDataSource else { fatalError(MessageKitError.nilMessagesDataSource) diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 84e24826..8102e64f 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -24,6 +24,7 @@ import UIKit +/// A subclass of `MessageContentCell` used to display text messages. open class TextMessageCell: MessageContentCell { open override class func reuseIdentifier() -> String { @@ -32,12 +33,14 @@ open class TextMessageCell: MessageContentCell { // MARK: - Properties + /// The `MessageCellDelegate` for the cell. open override weak var delegate: MessageCellDelegate? { didSet { messageLabel.delegate = delegate } } + /// The label used to display the message's text. open var messageLabel = MessageLabel() // MARK: - Methods @@ -91,8 +94,10 @@ open class TextMessageCell: MessageContentCell { } } - /// Handle `ContentView`'s tap gesture, return false when `ContentView` don't needs to handle gesture + /// Used to handle the cell's contentView's tap gesture. + /// Return false when the contentView does not need to handle the gesture. open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { return messageLabel.handleGesture(touchPoint) } + } From d2ce5877f927760d7b9af9b8c5ff9f1811bf4c7e Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Sat, 31 Mar 2018 16:03:35 +0800 Subject: [PATCH 52/70] Transform CellSizeCalculator from protocol to class --- .../CellSizeCalculator.swift | 16 +++++++++------- Sources/Layout/MessageSizeCalculator.swift | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) rename Sources/{Protocols => Layout}/CellSizeCalculator.swift (75%) diff --git a/Sources/Protocols/CellSizeCalculator.swift b/Sources/Layout/CellSizeCalculator.swift similarity index 75% rename from Sources/Protocols/CellSizeCalculator.swift rename to Sources/Layout/CellSizeCalculator.swift index 893ad8ed..09e356d1 100644 --- a/Sources/Protocols/CellSizeCalculator.swift +++ b/Sources/Layout/CellSizeCalculator.swift @@ -24,23 +24,25 @@ import UIKit -/// An object that adopts the `CellSizeCalculator` protocol is responsible for +/// An object is responsible for /// sizing and configuring cells for given `IndexPath`s. -public protocol CellSizeCalculator { +open class CellSizeCalculator { /// The layout object for which the cell size calculator is used. - weak var layout: UICollectionViewFlowLayout? { get set } + public weak var layout: UICollectionViewFlowLayout? /// Used to configure the layout attributes for a given cell. /// /// - Parameters: - /// - attributes: The attributes of the cell. - func configure(attributes: UICollectionViewLayoutAttributes) + /// - attributes: The attributes of the cell. + /// The default does nothing + open func configure(attributes: UICollectionViewLayoutAttributes) {} /// Used to size an item at a given `IndexPath`. /// /// - Parameters: - /// - indexPath: The `IndexPath` of the item to be displayed. - func sizeForItem(at indexPath: IndexPath) -> CGSize + /// - indexPath: The `IndexPath` of the item to be displayed. + /// The default return .zero + open func sizeForItem(at indexPath: IndexPath) -> CGSize { return .zero } } diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index a3a2422b..0cb5fa2d 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -26,9 +26,9 @@ import Foundation open class MessageSizeCalculator: CellSizeCalculator { - public var layout: UICollectionViewFlowLayout? - public init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + self.layout = layout } @@ -47,7 +47,7 @@ open class MessageSizeCalculator: CellSizeCalculator { public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - open func configure(attributes: UICollectionViewLayoutAttributes) { + open override func configure(attributes: UICollectionViewLayoutAttributes) { guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } let dataSource = messagesLayout.messagesDataSource @@ -66,7 +66,7 @@ open class MessageSizeCalculator: CellSizeCalculator { attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) } - open func sizeForItem(at indexPath: IndexPath) -> CGSize { + open override func sizeForItem(at indexPath: IndexPath) -> CGSize { let dataSource = messagesLayout.messagesDataSource let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) let itemHeight = cellContentHeight(for: message, at: indexPath) From 762e684ac636df0d42d750c1cc8981b543ffb63a Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Sat, 31 Mar 2018 16:31:13 +0800 Subject: [PATCH 53/70] Fix tests --- MessageKit.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 0905ab08..165be274 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */; }; + 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */; }; 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */; }; 1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */; }; 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */; }; @@ -26,7 +27,6 @@ 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5896320660C1C004B5081 /* LocationItem.swift */; }; 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */; }; - 1FE783A02066286A007FA024 /* CellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7839F2066286A007FA024 /* CellSizeCalculator.swift */; }; 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */; }; 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */; }; 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */; }; @@ -118,6 +118,7 @@ /* Begin PBXFileReference section */ 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = ""; }; + 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellSizeCalculator.swift; sourceTree = ""; }; 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabelSpec.swift; sourceTree = ""; }; 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderSpec.swift; sourceTree = ""; }; 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSpec.swift; sourceTree = ""; }; @@ -133,7 +134,6 @@ 1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; 1FD5896320660C1C004B5081 /* LocationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItem.swift; sourceTree = ""; }; 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSizeCalculator.swift; sourceTree = ""; }; - 1FE7839F2066286A007FA024 /* CellSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellSizeCalculator.swift; sourceTree = ""; }; 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSizeCalculator.swift; sourceTree = ""; }; 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaMessageSizeCalculator.swift; sourceTree = ""; }; 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessageSizeCalculator.swift; sourceTree = ""; }; @@ -440,7 +440,6 @@ B7A03F511F8669C9006AEF79 /* MessageType.swift */, 1FD5895F2064E08A004B5081 /* MediaItem.swift */, 1FD5896320660C1C004B5081 /* LocationItem.swift */, - 1FE7839F2066286A007FA024 /* CellSizeCalculator.swift */, ); path = Protocols; sourceTree = ""; @@ -464,6 +463,7 @@ 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */, 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */, 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */, + 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */, ); path = Layout; sourceTree = ""; @@ -613,7 +613,6 @@ B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, - 1FE783A02066286A007FA024 /* CellSizeCalculator.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, @@ -652,6 +651,7 @@ B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, + 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */, B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */, 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, From 3697048cb3218ae1cf0223e847724359bddd8d36 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 3 Apr 2018 12:11:26 -0500 Subject: [PATCH 54/70] Remove MessageHeaderView and MessageFooterView --- CHANGELOG.md | 6 +++ MessageKit.xcodeproj/project.pbxproj | 7 --- .../Controllers/MessagesViewController.swift | 3 +- .../MessageDateHeaderView.swift | 2 +- .../Headers & Footers/MessageFooterView.swift | 43 ------------------- .../Headers & Footers/MessageHeaderView.swift | 43 ------------------- 6 files changed, 8 insertions(+), 96 deletions(-) delete mode 100644 Sources/Views/Headers & Footers/MessageFooterView.swift delete mode 100644 Sources/Views/Headers & Footers/MessageHeaderView.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc41a4b..c6031cc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,9 @@ the `MessageData.location` case. ### Changed +- **Breaking Change** Changed the superclass of `MessageDateHeaderView` from `MessageHeaderView` to `MessageReusableView`. +[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). + - **Breaking Change** Change `NSLayoutConstraintSet` class from `public` to `internal`. [#607](https://github.com/MessageKit/MessageKit/pull/607) by [@zhongwuzw](https://github.com/zhongwuzw). @@ -77,6 +80,9 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o ### Removed +- **Breaking Change** Removed `MessageHeaderView` and `MessageFooterView` in favor of `MessageReusableView`. +[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). + - **Breaking Change** Removed `numberOfMessages(in)` method of `MessagesDataSource`, instead please use `numberOfSections(in)` and `numberOfItems(inSection:in)`. [#601](https://github.com/MessageKit/MessageKit/pull/601) by [@SD10](https://github.com/sd10), [@zhongwuzw](https://github.com/zhongwuzw). diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 165be274..65caf690 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -61,8 +61,6 @@ B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F221F866895006AEF79 /* LabelAlignment.swift */; }; B7A03F2E1F866895006AEF79 /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F231F866895006AEF79 /* MessageData.swift */; }; B7A03F331F866940006AEF79 /* MessageDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */; }; - B7A03F341F866940006AEF79 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F311F866940006AEF79 /* MessageFooterView.swift */; }; - B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F321F866940006AEF79 /* MessageHeaderView.swift */; }; B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F361F866946006AEF79 /* TextMessageCell.swift */; }; B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F381F866946006AEF79 /* LocationMessageCell.swift */; }; B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F391F866946006AEF79 /* MediaMessageCell.swift */; }; @@ -172,8 +170,6 @@ B7A03F221F866895006AEF79 /* LabelAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAlignment.swift; sourceTree = ""; }; B7A03F231F866895006AEF79 /* MessageData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = ""; }; B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDateHeaderView.swift; sourceTree = ""; }; - B7A03F311F866940006AEF79 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = ""; }; - B7A03F321F866940006AEF79 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = ""; }; B7A03F361F866946006AEF79 /* TextMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = ""; }; B7A03F381F866946006AEF79 /* LocationMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageCell.swift; sourceTree = ""; }; B7A03F391F866946006AEF79 /* MediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageCell.swift; sourceTree = ""; }; @@ -251,8 +247,6 @@ isa = PBXGroup; children = ( B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */, - B7A03F311F866940006AEF79 /* MessageFooterView.swift */, - B7A03F321F866940006AEF79 /* MessageHeaderView.swift */, 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); path = "Headers & Footers"; @@ -645,7 +639,6 @@ B7A03F291F866895006AEF79 /* Sender.swift in Sources */, B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */, B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, - B7A03F341F866940006AEF79 /* MessageFooterView.swift in Sources */, B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */, 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 5617add3..1e75e603 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -126,8 +126,7 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { messagesCollectionView.register(MediaMessageCell.self) messagesCollectionView.register(LocationMessageCell.self) - messagesCollectionView.register(MessageFooterView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter) - messagesCollectionView.register(MessageHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) + messagesCollectionView.register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) messagesCollectionView.register(MessageDateHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) } diff --git a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift index 7ea470b7..edf9d5b9 100644 --- a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift +++ b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift @@ -24,7 +24,7 @@ import UIKit -open class MessageDateHeaderView: MessageHeaderView { +open class MessageDateHeaderView: MessageReusableView { open override class func reuseIdentifier() -> String { return "messagekit.header.date" diff --git a/Sources/Views/Headers & Footers/MessageFooterView.swift b/Sources/Views/Headers & Footers/MessageFooterView.swift deleted file mode 100644 index df466368..00000000 --- a/Sources/Views/Headers & Footers/MessageFooterView.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -open class MessageFooterView: MessageReusableView { - - open override class func reuseIdentifier() -> String { - return "messagekit.footer.base" - } - - // MARK: - Initializers - - public override init(frame: CGRect) { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/Sources/Views/Headers & Footers/MessageHeaderView.swift b/Sources/Views/Headers & Footers/MessageHeaderView.swift deleted file mode 100644 index fea13acb..00000000 --- a/Sources/Views/Headers & Footers/MessageHeaderView.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -open class MessageHeaderView: MessageReusableView { - - open override class func reuseIdentifier() -> String { - return "messagekit.header.base" - } - - // MARK: - Properties - - public override init(frame: CGRect) { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} From 9b8f0ba9151bb15d76f889e62dc66bd66a704ac5 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 3 Apr 2018 12:21:36 -0500 Subject: [PATCH 55/70] Remove CollectionViewReusable protocol --- CHANGELOG.md | 11 ++++++ .../UICollectionView+Extensions.swift | 35 ++++++++----------- .../Protocols/MessagesDisplayDelegate.swift | 32 ++++------------- .../Protocols/MessagesLayoutDelegate.swift | 4 +-- Sources/Views/Cells/LocationMessageCell.swift | 4 --- Sources/Views/Cells/MediaMessageCell.swift | 4 --- .../Cells/MessageCollectionViewCell.swift | 7 +--- Sources/Views/Cells/MessageContentCell.swift | 4 --- Sources/Views/Cells/TextMessageCell.swift | 4 --- .../MessageDateHeaderView.swift | 4 --- .../MessageReusableView.swift | 6 +--- 11 files changed, 36 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6031cc7..d170746a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,10 @@ the `MessageData.location` case. ### Changed +- **Breaking Change** The reuse identifiers of all `MessageReusableView`s and `MessageCollectionViewCell`s have been +changed to match their class name exactly. +[#](https://github.com/MessageKit/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). + - **Breaking Change** Changed the superclass of `MessageDateHeaderView` from `MessageHeaderView` to `MessageReusableView`. [#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). @@ -80,6 +84,13 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o ### Removed +- **Breaking Change** Removed the `reuseIdentifer` method from `MessageCollectionViewCell`, `TextMessageCell`, +`LocationMessageCell`, `MediaMessageCell`, and `MessageContentCell`. +[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Removed the `CollectionViewReusable` protocol. +[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10).) + - **Breaking Change** Removed `MessageHeaderView` and `MessageFooterView` in favor of `MessageReusableView`. [#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). diff --git a/Sources/Extensions/UICollectionView+Extensions.swift b/Sources/Extensions/UICollectionView+Extensions.swift index c5b42b01..ebf7e49e 100644 --- a/Sources/Extensions/UICollectionView+Extensions.swift +++ b/Sources/Extensions/UICollectionView+Extensions.swift @@ -24,46 +24,41 @@ import Foundation -/// Optional Cell Protocol to Simplify registration/cell type loading in a generic way -public protocol CollectionViewReusable: AnyObject { - static func reuseIdentifier() -> String -} - extension MessagesCollectionView { /// Registers a particular cell using its reuse-identifier - public func register(_ cellClass: CellType.Type) { - register(cellClass, forCellWithReuseIdentifier: CellType.reuseIdentifier()) + 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: ViewType.Type, forSupplementaryViewOfKind kind: String) { + public func register(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { register(headerFooterClass, forSupplementaryViewOfKind: kind, - withReuseIdentifier: ViewType.reuseIdentifier()) + withReuseIdentifier: String(describing: T.self)) } /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableCell(_ cellClass: CellType.Type, for indexPath: IndexPath) -> CellType { - guard let cell = dequeueReusableCell(withReuseIdentifier: cellClass.reuseIdentifier(), for: indexPath) as? CellType else { - fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(cellClass.reuseIdentifier())") + public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else { + fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))") } return cell } /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableHeaderView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: viewClass.reuseIdentifier(), for: indexPath) - guard let viewType = view as? ViewType else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(viewClass.reuseIdentifier())") + public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) + guard let viewType = view as? T else { + fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") } return viewType } /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableFooterView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: viewClass.reuseIdentifier(), for: indexPath) - guard let viewType = view as? ViewType else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(viewClass.reuseIdentifier())") + public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, 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/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 034753a7..cb0a2107 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -63,22 +63,7 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - message: The `MessageType` that will be displayed for this header. /// - indexPath: The `IndexPath` of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - /// - /// - Note: - /// The default value returned by this method is a `MessageDateHeaderView`. - func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView - - /// Used by the `MessageLayoutDelegate` method `headerViewSize(_:_:_:)` to determine if a header should be displayed. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this header. - /// - indexPath: The `IndexPath` of the header. - /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - /// - /// - Note: - /// This method checks `MessageCollectionView`'s `showsDateHeaderAfterTimeInterval` property and returns true - /// if the current messages sent date occurs after the specified time interval when compared to the previous message. - func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool + func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView /// The section footer to use for a given `MessageType`. /// @@ -86,10 +71,7 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - message: The `MessageType` that will be displayed for this footer. /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. - /// - /// - Note: - /// The default value returned by this method is a `MessageFooterView`. - func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView + func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class. /// @@ -201,10 +183,8 @@ public extension MessagesDisplayDelegate { } } - func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView { - let header = messagesCollectionView.dequeueReusableHeaderView(MessageDateHeaderView.self, for: indexPath) - header.dateLabel.text = MessageKitDateFormatter.shared.string(from: message.sentDate) - return header + func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + } func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool { @@ -217,8 +197,8 @@ public extension MessagesDisplayDelegate { return timeIntervalSinceLastMessage >= messagesCollectionView.showsDateHeaderAfterTimeInterval } - func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView { - return messagesCollectionView.dequeueReusableFooterView(MessageFooterView.self, for: indexPath) + func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index b6ef3e3c..3a3a3e21 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -28,7 +28,7 @@ import Foundation /// the size and layout of a `MessageCollectionViewCell` and its contents. public protocol MessagesLayoutDelegate: AnyObject { - /// Specifies the size to use for a `MessageHeaderView`. + /// Specifies the size to use for a header view. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this header. @@ -40,7 +40,7 @@ public protocol MessagesLayoutDelegate: AnyObject { /// and a height of 12. func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - /// Specifies the size to use for a `MessageFooterView`. + /// Specifies the size to use for a footer view. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this footer. diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index 994cf945..a767f31c 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -28,10 +28,6 @@ import MapKit /// A subclass of `MessageContentCell` used to display location messages. open class LocationMessageCell: MessageContentCell { - open override class func reuseIdentifier() -> String { - return "messagekit.cell.location" - } - /// The activity indicator to be displayed while the map image is loading. open var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index 0d02b342..5414b406 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -27,10 +27,6 @@ import UIKit /// A subclass of `MessageContentCell` used to display video and audio messages. open class MediaMessageCell: MessageContentCell { - open override class func reuseIdentifier() -> String { - return "messagekit.cell.mediamessage" - } - /// The play button view to display on video messages. open lazy var playButtonView: PlayButtonView = { let playButtonView = PlayButtonView() diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 1a842047..8d99ab90 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -25,12 +25,7 @@ import UIKit /// A subclass of `UICollectionViewCell` to be used inside of a `MessagesCollectionView`. -open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusable { - - /// The reuse identifier of the cell. - open class func reuseIdentifier() -> String { - return "messagekit.cell.base-cell" - } +open class MessageCollectionViewCell: UICollectionViewCell { // MARK: - Initializers diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index dd599958..89794d10 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -27,10 +27,6 @@ import UIKit /// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages. open class MessageContentCell: MessageCollectionViewCell { - open override class func reuseIdentifier() -> String { - return "messagekit.cell.contentcell" - } - /// The image view displaying the avatar. open var avatarView = AvatarView() diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 8102e64f..2ae74e56 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -27,10 +27,6 @@ import UIKit /// A subclass of `MessageContentCell` used to display text messages. open class TextMessageCell: MessageContentCell { - open override class func reuseIdentifier() -> String { - return "messagekit.cell.text" - } - // MARK: - Properties /// The `MessageCellDelegate` for the cell. diff --git a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift index edf9d5b9..11e4e3d6 100644 --- a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift +++ b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift @@ -26,10 +26,6 @@ import UIKit open class MessageDateHeaderView: MessageReusableView { - open override class func reuseIdentifier() -> String { - return "messagekit.header.date" - } - // MARK: - Properties open let dateLabel = UILabel() diff --git a/Sources/Views/Headers & Footers/MessageReusableView.swift b/Sources/Views/Headers & Footers/MessageReusableView.swift index cba573b3..f8c37c36 100644 --- a/Sources/Views/Headers & Footers/MessageReusableView.swift +++ b/Sources/Views/Headers & Footers/MessageReusableView.swift @@ -24,11 +24,7 @@ import Foundation -open class MessageReusableView: UICollectionReusableView, CollectionViewReusable { - - open class func reuseIdentifier() -> String { - return "messagekit.reusable.base" - } +open class MessageReusableView: UICollectionReusableView { // MARK: - Initializers From 911387edc1bfeb84bfb25b4f77e55f2188ed6d2b Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 3 Apr 2018 13:13:04 -0500 Subject: [PATCH 56/70] Refactor header/footer API to remove indexPath --- MessageKit.xcodeproj/project.pbxproj | 4 -- .../Controllers/MessagesViewController.swift | 26 ++------ .../UICollectionView+Extensions.swift | 65 ------------------- .../Protocols/MessagesDisplayDelegate.swift | 17 +++-- .../Protocols/MessagesLayoutDelegate.swift | 23 +++---- Sources/Views/MessagesCollectionView.swift | 38 +++++++++++ 6 files changed, 58 insertions(+), 115 deletions(-) delete mode 100644 Sources/Extensions/UICollectionView+Extensions.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 65caf690..ce879f8f 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -80,7 +80,6 @@ B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */; }; B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */; }; B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */; }; - B7A03F6A1F8669EB006AEF79 /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */; }; B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */; }; B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */; }; B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */; }; @@ -189,7 +188,6 @@ B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegate.swift; sourceTree = ""; }; B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = ""; }; - B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = ""; }; B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; @@ -376,7 +374,6 @@ 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */, B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */, B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */, - B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */, B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */, B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */, 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */, @@ -628,7 +625,6 @@ B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */, B7A03F261F866895006AEF79 /* MessageKitDateFormatter.swift in Sources */, B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */, - B7A03F6A1F8669EB006AEF79 /* UICollectionView+Extensions.swift in Sources */, 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */, B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */, B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 1e75e603..02b81e86 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -198,21 +198,15 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { fatalError(MessageKitError.notMessagesCollectionView) } - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) } - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - switch kind { case UICollectionElementKindSectionHeader: - return displayDelegate.messageHeaderView(for: message, at: indexPath, in: messagesCollectionView) + return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) case UICollectionElementKindSectionFooter: - return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView) + return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) default: fatalError(MessageKitError.unrecognizedSectionKind) } @@ -230,32 +224,20 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) } - // Could pose a problem if subclass behaviors allows more than one item per section - let indexPath = IndexPath(item: 0, section: section) - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return layoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView) + return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) } - // Could pose a problem if subclass behaviors allows more than one item per section - let indexPath = IndexPath(item: 0, section: section) - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return layoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) + return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { diff --git a/Sources/Extensions/UICollectionView+Extensions.swift b/Sources/Extensions/UICollectionView+Extensions.swift deleted file mode 100644 index ebf7e49e..00000000 --- a/Sources/Extensions/UICollectionView+Extensions.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - 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 Foundation - -extension MessagesCollectionView { - /// 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, - forSupplementaryViewOfKind: kind, - withReuseIdentifier: String(describing: T.self)) - } - - /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { - guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else { - fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))") - } - return cell - } - - /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) - guard let viewType = view as? T else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") - } - return viewType - } - - /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, 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/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index cb0a2107..1700f351 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -57,21 +57,20 @@ public protocol MessagesDisplayDelegate: AnyObject { /// All other senders: Gray func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - /// The section header to use for a given `MessageType`. + /// The section header to use for a given `IndexPath`. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this header. /// - indexPath: The `IndexPath` of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView + func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView - /// The section footer to use for a given `MessageType`. + /// The section footer to use for a given `IndexPath`. /// /// - Parameters: - /// - message: The `MessageType` that will be displayed for this footer. /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. - func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView + func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class. /// @@ -183,8 +182,8 @@ public extension MessagesDisplayDelegate { } } - func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { - + func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath) } func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool { @@ -197,8 +196,8 @@ public extension MessagesDisplayDelegate { return timeIntervalSinceLastMessage >= messagesCollectionView.showsDateHeaderAfterTimeInterval } - func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { - + func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 3a3a3e21..2b3022f1 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -31,25 +31,22 @@ public protocol MessagesLayoutDelegate: AnyObject { /// Specifies the size to use for a header view. /// /// - Parameters: - /// - message: The `MessageType` that will be displayed for this header. - /// - indexPath: The `IndexPath` of the header. + /// - section: The section number of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. /// /// - Note: - /// The default value returned by this method is the width of the `MessagesCollectionView` - /// and a height of 12. - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + /// The default value returned by this method is a size of `GGSize.zero`. + func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize /// Specifies the size to use for a footer view. /// /// - Parameters: - /// - message: The `MessageType` that will be displayed for this footer. - /// - indexPath: The `IndexPath` of the footer. + /// - section: The section number of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. /// /// - Note: /// The default value returned by this method is a size of `GGSize.zero`. - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize /// Specifies the height for the `MessageContentCell`'s top label. /// @@ -77,15 +74,11 @@ public protocol MessagesLayoutDelegate: AnyObject { public extension MessagesLayoutDelegate { - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - let shouldDisplay = displayDelegate.shouldDisplayHeader(for: message, at: indexPath, in: messagesCollectionView) - return shouldDisplay ? CGSize(width: messagesCollectionView.bounds.width, height: 12) : .zero + func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize { + return .zero } - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize { return .zero } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 0f9f245d..25f9910f 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -106,4 +106,42 @@ open class MessagesCollectionView: UICollectionView { setContentOffset(newOffset, animated: false) } + /// 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, + forSupplementaryViewOfKind: kind, + withReuseIdentifier: String(describing: T.self)) + } + + /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else { + fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))") + } + return cell + } + + /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) + guard let viewType = view as? T else { + fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") + } + return viewType + } + + /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, 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 1dc97fc38f0a89bde35770ad67f53c4d6723327e Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 3 Apr 2018 13:20:06 -0500 Subject: [PATCH 57/70] Remove shouldDisplayHeader method --- Example/Sources/ConversationViewController.swift | 2 +- Sources/Protocols/MessagesDisplayDelegate.swift | 10 ---------- Sources/Views/MessagesCollectionView.swift | 4 ---- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 98793a84..a74fe92f 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -365,7 +365,7 @@ extension ConversationViewController: MessagesLayoutDelegate { return 16 } - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + func footerViewSize(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return CGSize(width: messagesCollectionView.bounds.width, height: 10) } } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 1700f351..766f8ba8 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -186,16 +186,6 @@ public extension MessagesDisplayDelegate { return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath) } - func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool { - guard let dataSource = messagesCollectionView.messagesDataSource else { return false } - if indexPath.section == 0 { return false } - let previousSection = indexPath.section - 1 - let previousIndexPath = IndexPath(item: 0, section: previousSection) - let previousMessage = dataSource.messageForItem(at: previousIndexPath, in: messagesCollectionView) - let timeIntervalSinceLastMessage = message.sentDate.timeIntervalSince(previousMessage.sentDate) - return timeIntervalSinceLastMessage >= messagesCollectionView.showsDateHeaderAfterTimeInterval - } - func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 25f9910f..323cd76e 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -36,14 +36,10 @@ open class MessagesCollectionView: UICollectionView { open weak var messageCellDelegate: MessageCellDelegate? - open var showsDateHeaderAfterTimeInterval: TimeInterval = 3600 - private var indexPathForLastItem: IndexPath? { - let lastSection = numberOfSections - 1 guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil } return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) - } // MARK: - Initializers From cbab51948b0bdae557e8e5ae6c16be8038c859ba Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 3 Apr 2018 13:34:56 -0500 Subject: [PATCH 58/70] Update CHANGELOG.md --- CHANGELOG.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d170746a..3309bfb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,12 +51,20 @@ the `MessageData.location` case. ### Changed +- **Breaking Change** Changed the `messageFooterView(for:in)` and `messageHeaderView(for:in)` methods of +`MessagesDisplayDelegate` by removing the `message` parameter. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Changed the `footerViewSize(for:in)` and `headerViewSize(for:in)` methods of `MessagesLayoutDelegate` +by removing the `MessageType` and `IndexPath` parameters and replacing them with a new `section: Int` parameter. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + - **Breaking Change** The reuse identifiers of all `MessageReusableView`s and `MessageCollectionViewCell`s have been changed to match their class name exactly. -[#](https://github.com/MessageKit/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). - **Breaking Change** Changed the superclass of `MessageDateHeaderView` from `MessageHeaderView` to `MessageReusableView`. -[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). - **Breaking Change** Change `NSLayoutConstraintSet` class from `public` to `internal`. [#607](https://github.com/MessageKit/MessageKit/pull/607) by [@zhongwuzw](https://github.com/zhongwuzw). @@ -84,15 +92,18 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o ### Removed +- **Breaking Change** Removed the `showsDateHeaderAfterTimeInterval` property of `MessagesCollectionView`. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + - **Breaking Change** Removed the `reuseIdentifer` method from `MessageCollectionViewCell`, `TextMessageCell`, `LocationMessageCell`, `MediaMessageCell`, and `MessageContentCell`. -[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). - **Breaking Change** Removed the `CollectionViewReusable` protocol. -[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10).) +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). - **Breaking Change** Removed `MessageHeaderView` and `MessageFooterView` in favor of `MessageReusableView`. -[#](https://github.com/MessageKit/MessageKit/pull/) by [@SD10](https://github.com/sd10). +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). - **Breaking Change** Removed `numberOfMessages(in)` method of `MessagesDataSource`, instead please use `numberOfSections(in)` and `numberOfItems(inSection:in)`. [#601](https://github.com/MessageKit/MessageKit/pull/601) by [@SD10](https://github.com/sd10), [@zhongwuzw](https://github.com/zhongwuzw). From bb1ea1d67ea43a7afc6b30d6c2655fb1cbde7003 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 3 Apr 2018 13:45:52 -0500 Subject: [PATCH 59/70] Remove failing tests --- .../Protocols/MessagesDisplayDelegate.swift | 1 - .../MessagesDisplayDelegateTests.swift | 61 +------------------ 2 files changed, 2 insertions(+), 60 deletions(-) diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 766f8ba8..9cea97fd 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -228,6 +228,5 @@ public extension MessagesDisplayDelegate { // MARK: - Media Message Defaults func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - } } diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift index 89a575dd..15d6b774 100644 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift @@ -111,69 +111,12 @@ class MessagesDisplayDelegateTests: XCTestCase { XCTAssertTrue(result) } - func testShouldDisplayHeaderWithoutDataSource_returnsFalseForDefault() { - sut.messagesCollectionView.messagesDataSource = nil - - XCTAssertFalse(sut.shouldDisplayHeader(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForFirstMessage_returnsFalseForDefault() { - XCTAssertFalse(sut.shouldDisplayHeader(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForMessageWithTimeIntervalSinceLastMessageGreatherThanScheduled_returnsTrue() { - var message = MockMessage(text: "Test", sender: sut.dataProvider.currentSender(), messageId: "003") - let scheduledInterval = sut.messagesCollectionView.showsDateHeaderAfterTimeInterval - message.sentDate = Date(timeIntervalSinceNow: scheduledInterval + 300) - sut.dataProvider.messages.append(message) - - XCTAssertTrue(sut.shouldDisplayHeader(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForMessageWithTimeIntervalSinceLastMessageEqualThanScheduled_returnsTrue() { - var message = MockMessage(text: "Test", sender: sut.dataProvider.currentSender(), messageId: "003") - let scheduledInterval = sut.messagesCollectionView.showsDateHeaderAfterTimeInterval - message.sentDate = Date(timeIntervalSinceNow: scheduledInterval) - sut.dataProvider.messages.append(message) - - XCTAssertTrue(sut.shouldDisplayHeader(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForMessageWithTimeIntervalSinceLastMessageLessThanScheduled_returnsFalse() { - var message = MockMessage(text: "Test", sender: sut.dataProvider.currentSender(), messageId: "003") - let scheduledInterval = sut.messagesCollectionView.showsDateHeaderAfterTimeInterval - message.sentDate = Date(timeIntervalSinceNow: scheduledInterval - 300) - sut.dataProvider.messages.append(message) - - XCTAssertFalse(sut.shouldDisplayHeader(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView)) - } - func testMessageHeaderView_isNotNil() { - let headerView = sut.messageHeaderView(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView) - + let indexPath = IndexPath(item: 0, section: 1) + let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView) XCTAssertNotNil(headerView) } - func testMessageFooterView_isNotNil() { - let footerView = sut.messageFooterView(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView) - - XCTAssertNotNil(footerView) - } - } class TextMessageDisplayDelegateTests: XCTestCase { From 63784c7010d8ff354e0f2fd6a269ae3cb616126d Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Tue, 3 Apr 2018 14:59:06 -0500 Subject: [PATCH 60/70] Make computed properties of MessagesCollectionViewFlowLayout public This allows cellSizeCalculatorForItem to be overridden and still call super. --- Sources/Layout/MessagesCollectionViewFlowLayout.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 08e43e00..9c0f45e6 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -137,21 +137,21 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { - internal var messagesCollectionView: MessagesCollectionView { + public var messagesCollectionView: MessagesCollectionView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.layoutUsedOnForeignType) } return messagesCollectionView } - internal var messagesDataSource: MessagesDataSource { + public var messagesDataSource: MessagesDataSource { guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError(MessageKitError.nilMessagesDataSource) } return messagesDataSource } - internal var messagesLayoutDelegate: MessagesLayoutDelegate { + public var messagesLayoutDelegate: MessagesLayoutDelegate { guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) } From 3dc6979a7853dcc911f0f39a4e395ee4aeea2be9 Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Tue, 3 Apr 2018 15:16:06 -0500 Subject: [PATCH 61/70] Move computed properties out of extension and add documentation --- .../MessagesCollectionViewFlowLayout.swift | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 9c0f45e6..cee7b38f 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -32,6 +32,30 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open override class var layoutAttributesClass: AnyClass { return MessagesCollectionViewLayoutAttributes.self } + + /** The `MessagesCollectionView` that owns this layout object. */ + public var messagesCollectionView: MessagesCollectionView { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.layoutUsedOnForeignType) + } + return messagesCollectionView + } + + /** The `MessagesDataSource` for the layout's collection view. */ + public var messagesDataSource: MessagesDataSource { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + return messagesDataSource + } + + /** The `MessagesLayoutDelegate` for the layout's collection view. */ + public var messagesLayoutDelegate: MessagesLayoutDelegate { + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) + } + return messagesLayoutDelegate + } internal var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } @@ -132,29 +156,3 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return calculator.sizeForItem(at: indexPath) } } - -// MARK: - Helpers - -extension MessagesCollectionViewFlowLayout { - - public var messagesCollectionView: MessagesCollectionView { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.layoutUsedOnForeignType) - } - return messagesCollectionView - } - - public var messagesDataSource: MessagesDataSource { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return messagesDataSource - } - - public var messagesLayoutDelegate: MessagesLayoutDelegate { - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return messagesLayoutDelegate - } -} From f48cd5685121f166c4caf3a4c827182e4a577d23 Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Thu, 5 Apr 2018 13:00:25 -0500 Subject: [PATCH 62/70] Only apply textColor for .text case of MessageData --- Sources/Protocols/MessagesDisplayDelegate.swift | 2 +- Sources/Views/Cells/TextMessageCell.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 9cea97fd..288b1cda 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -89,7 +89,7 @@ public protocol MessagesDisplayDelegate: AnyObject { /// Specifies the color of the text for a `TextMessageCell`. /// /// - Parameters: - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the color will apply. + /// - message: A `MessageType` with a `MessageData` case of `.text` to which the color will apply. /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 2ae74e56..530dcb3f 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -80,13 +80,12 @@ open class TextMessageCell: MessageContentCell { switch message.data { case .text(let text), .emoji(let text): messageLabel.text = text + messageLabel.textColor = textColor case .attributedText(let text): messageLabel.attributedText = text default: break } - // Needs to be set after the attributedText because it takes precedence - messageLabel.textColor = textColor } } From 771194fbf265c851bd62c506c7ba9b485c870cda Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Thu, 5 Apr 2018 13:10:24 -0500 Subject: [PATCH 63/70] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3309bfb1..7f38f4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,8 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o - Changed the `contentMode` of the `UIImageView` for `MediaMessageCell` to be `.scaleAspectFill`. [#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). +- The result of the `MessagesDisplayDelegate` method `textColor(for message:...)` no longer applies to `.attributedText` case of `MessageData`. + ### Removed - **Breaking Change** Removed the `showsDateHeaderAfterTimeInterval` property of `MessagesCollectionView`. From 9b5ba97b53af8349599d9901f742a7293ce72008 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 5 Apr 2018 13:26:01 -0500 Subject: [PATCH 64/70] Add name and PR # --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f38f4ab..18dfeca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o [#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). - The result of the `MessagesDisplayDelegate` method `textColor(for message:...)` no longer applies to `.attributedText` case of `MessageData`. +[#625](https://github.com/MessageKit/MessageKit/pull/625) by [@cwalo](https://github.com/cwalo). ### Removed From 6fcd2d6aa24df02fa5d7847a0aa474f70dc41271 Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Thu, 5 Apr 2018 21:27:01 -0500 Subject: [PATCH 65/70] Only apply textColor for .text case of MessageData (#625) * Only apply textColor for .text case of MessageData * Update CHANGELOG * Add name and PR # --- CHANGELOG.md | 3 +++ Sources/Protocols/MessagesDisplayDelegate.swift | 2 +- Sources/Views/Cells/TextMessageCell.swift | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3309bfb1..18dfeca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,9 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o - Changed the `contentMode` of the `UIImageView` for `MediaMessageCell` to be `.scaleAspectFill`. [#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). +- The result of the `MessagesDisplayDelegate` method `textColor(for message:...)` no longer applies to `.attributedText` case of `MessageData`. +[#625](https://github.com/MessageKit/MessageKit/pull/625) by [@cwalo](https://github.com/cwalo). + ### Removed - **Breaking Change** Removed the `showsDateHeaderAfterTimeInterval` property of `MessagesCollectionView`. diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 9cea97fd..288b1cda 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -89,7 +89,7 @@ public protocol MessagesDisplayDelegate: AnyObject { /// Specifies the color of the text for a `TextMessageCell`. /// /// - Parameters: - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the color will apply. + /// - message: A `MessageType` with a `MessageData` case of `.text` to which the color will apply. /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 2ae74e56..530dcb3f 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -80,13 +80,12 @@ open class TextMessageCell: MessageContentCell { switch message.data { case .text(let text), .emoji(let text): messageLabel.text = text + messageLabel.textColor = textColor case .attributedText(let text): messageLabel.attributedText = text default: break } - // Needs to be set after the attributedText because it takes precedence - messageLabel.textColor = textColor } } From bdfb9885904ebbe292cfe83cc4f5357102399497 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 6 Apr 2018 11:00:42 +0800 Subject: [PATCH 66/70] Fixed font invalidation of attributedString in MessageLabel --- Sources/Views/Cells/TextMessageCell.swift | 5 ++++- Sources/Views/MessageLabel.swift | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 530dcb3f..0497e001 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -45,7 +45,7 @@ open class TextMessageCell: MessageContentCell { super.apply(layoutAttributes) if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes { messageLabel.textInsets = attributes.messageLabelInsets - messageLabel.font = attributes.messageLabelFont + messageLabel.messageLabelFont = attributes.messageLabelFont messageLabel.frame = messageContainerView.bounds } } @@ -81,6 +81,9 @@ open class TextMessageCell: MessageContentCell { case .text(let text), .emoji(let text): messageLabel.text = text messageLabel.textColor = textColor + if let font = messageLabel.messageLabelFont { + messageLabel.font = font + } case .attributedText(let text): messageLabel.attributedText = text default: diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index d1adada5..bd4e3cdb 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -112,6 +112,8 @@ open class MessageLabel: UILabel { if !isConfiguring { setNeedsDisplay() } } } + + internal var messageLabelFont: UIFont? private var attributesNeedUpdate = false From 1038b96d01c74cace2bad6fc8be0868b1b049a8a Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Fri, 6 Apr 2018 16:15:52 -0500 Subject: [PATCH 67/70] Update access control These are necessary to create custom cells that rely on the existing layout mechanisms. --- Sources/Layout/MessageSizeCalculator.swift | 14 ++++++------- .../MessagesCollectionViewFlowLayout.swift | 2 +- ...ssagesCollectionViewLayoutAttributes.swift | 20 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 0cb5fa2d..a17c9efc 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -107,7 +107,7 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Avatar - internal func avatarPosition(for message: MessageType) -> AvatarPosition { + open func avatarPosition(for message: MessageType) -> AvatarPosition { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition @@ -121,7 +121,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return position } - internal func avatarSize(for message: MessageType) -> CGSize { + open func avatarSize(for message: MessageType) -> CGSize { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize @@ -136,7 +136,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return CGSize(width: messagesLayout.itemWidth, height: height) } - internal func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { + open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment @@ -151,7 +151,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return CGSize(width: messagesLayout.itemWidth, height: height) } - internal func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + open func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment @@ -159,7 +159,7 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - MessageContainer - internal func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding @@ -178,14 +178,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Helpers - internal var messagesLayout: MessagesCollectionViewFlowLayout { + public var messagesLayout: MessagesCollectionViewFlowLayout { guard let layout = layout as? MessagesCollectionViewFlowLayout else { fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") } return layout } - internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + public func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { let estimatedHeight = attributedText.height(considering: maxWidth) let estimatedWidth = attributedText.width(considering: estimatedHeight) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index cee7b38f..7612e957 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -57,7 +57,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return messagesLayoutDelegate } - internal var itemWidth: CGFloat { + public var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } return collectionView.frame.width - sectionInset.left - sectionInset.right } diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index a9d7cb37..16a90399 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -29,19 +29,19 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu // MARK: - Properties - internal var avatarSize: CGSize = .zero - internal var avatarPosition = AvatarPosition(vertical: .cellBottom) + public var avatarSize: CGSize = .zero + public var avatarPosition = AvatarPosition(vertical: .cellBottom) - internal var messageContainerSize: CGSize = .zero - internal var messageContainerPadding: UIEdgeInsets = .zero - internal var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - internal var messageLabelInsets: UIEdgeInsets = .zero + public var messageContainerSize: CGSize = .zero + public var messageContainerPadding: UIEdgeInsets = .zero + public var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) + public var messageLabelInsets: UIEdgeInsets = .zero - internal var topLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - internal var topLabelSize: CGSize = .zero + public var topLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var topLabelSize: CGSize = .zero - internal var bottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - internal var bottomLabelSize: CGSize = .zero + public var bottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var bottomLabelSize: CGSize = .zero // MARK: - Methods From b646060bc571126a25c23956d742fe62e8917e9d Mon Sep 17 00:00:00 2001 From: Corey Walo Date: Sat, 7 Apr 2018 18:23:43 -0500 Subject: [PATCH 68/70] Updated access control from review --- Sources/Layout/MessageSizeCalculator.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index a17c9efc..f107d42b 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -107,7 +107,7 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Avatar - open func avatarPosition(for message: MessageType) -> AvatarPosition { + public func avatarPosition(for message: MessageType) -> AvatarPosition { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition @@ -121,7 +121,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return position } - open func avatarSize(for message: MessageType) -> CGSize { + public func avatarSize(for message: MessageType) -> CGSize { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize @@ -129,14 +129,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top Label - open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + public func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { + public func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment @@ -144,14 +144,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Bottom Label - open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + public func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - open func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + public func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment @@ -159,7 +159,7 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - MessageContainer - open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + public func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding @@ -185,7 +185,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return layout } - public func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { let estimatedHeight = attributedText.height(considering: maxWidth) let estimatedWidth = attributedText.width(considering: estimatedHeight) From 943ed2d899dfcaf93691e9056ec003fd0fcfc388 Mon Sep 17 00:00:00 2001 From: Changnam Hong Date: Mon, 9 Apr 2018 23:51:08 +0900 Subject: [PATCH 69/70] Fix ChatExample for SwiftLint 1. Set .swiftlint.yml for ChatExample 2. Fix Access Control for Explicit Top Level ACL Violation 3. Fix SwiftLint Warning: Colon Violation, Function Body Length 4. Fix Forced Unwrapping style --- Example/.swiftlint.yml | 20 +++++++++++++ Example/ChatExample.xcodeproj/project.pbxproj | 14 +++++++++ .../xcschemes/ChatExample.xcscheme | 8 ++--- .../xcschemes/ChatExampleUITests.xcscheme | 4 +-- Example/Podfile.lock | 4 +-- Example/Sources/AppDelegate.swift | 4 +-- .../Sources/ConversationViewController.swift | 30 ++++++++----------- Example/Sources/IMessageViewController.swift | 3 +- Example/Sources/InboxViewController.swift | 4 +-- Example/Sources/MockMessage.swift | 7 ++--- Example/Sources/SampleData.swift | 2 +- Example/Sources/SettingsViewController.swift | 24 ++++++++------- Example/Sources/TableViewCells.swift | 2 +- 13 files changed, 75 insertions(+), 51 deletions(-) create mode 100644 Example/.swiftlint.yml diff --git a/Example/.swiftlint.yml b/Example/.swiftlint.yml new file mode 100644 index 00000000..8bf8a3da --- /dev/null +++ b/Example/.swiftlint.yml @@ -0,0 +1,20 @@ + +disabled_rules: + - identifier_name + - trailing_whitespace + - line_length + - type_body_length + - file_length +custom_rules: + override_func: # rule identifier + name: "override in func" # rule name. optional. + regex: "override (open|public|private|internal|fileprivate)" # matching pattern + message: "Use like open override or public override instead" # violation message. optional. + severity: warning # violation severity. optional. +opt_in_rules: + - explicit_acl + - explicit_top_level_acl +explicit_acl: error +explicit_top_level_acl: error +included: + - Sources \ No newline at end of file diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 56d35379..7d32b277 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ 882B5EA41CF7D8D100B6E160 /* Embed Frameworks */, 3C8DFCD3063DA0DF099EC7B6 /* [CP] Embed Pods Frameworks */, CD3F8EE3AFF2313DF2D62568 /* [CP] Copy Pods Resources */, + 7E0AFF9D207BB460004DFD4C /* ShellScript */, ); buildRules = ( ); @@ -464,6 +465,19 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 7E0AFF9D207BB460004DFD4C /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; CD3F8EE3AFF2313DF2D62568 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme index 56543ffd..cebf89c0 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -71,7 +70,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme index 6d3eb900..78acc96f 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme @@ -1,6 +1,6 @@ Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { UIApplication.shared.statusBarStyle = .lightContent window = UIWindow(frame: UIScreen.main.bounds) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index a74fe92f..a4820889 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -26,7 +26,7 @@ import UIKit import MessageKit import MapKit -class ConversationViewController: MessagesViewController { +internal class ConversationViewController: MessagesViewController { let refreshControl = UIRefreshControl() @@ -91,8 +91,6 @@ class ConversationViewController: MessagesViewController { label.text = "nathan.tannar is typing..." label.font = UIFont.boldSystemFont(ofSize: 16) messageInputBar.topStackView.addArrangedSubview(label) - - messageInputBar.topStackViewPadding.top = 6 messageInputBar.topStackViewPadding.left = 12 @@ -156,11 +154,9 @@ class ConversationViewController: MessagesViewController { }, makeButton(named: "ic_at").onSelected { $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - print("@ Selected") }, makeButton(named: "ic_hashtag").onSelected { $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - print("# Selected") }, .flexibleSpace, makeButton(named: "ic_library").onTextViewDidChange { button, textView in @@ -275,15 +271,13 @@ extension ConversationViewController: MessagesDataSource { } func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - - struct ConversationDateFormatter { - static let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - return formatter - }() - } - let formatter = ConversationDateFormatter.formatter + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() + let dateString = formatter.string(from: message.sentDate) return NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)]) } @@ -300,7 +294,7 @@ extension ConversationViewController: MessagesDisplayDelegate { return isFromCurrentSender(message: message) ? .white : .darkText } - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey : Any] { + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] { return MessageLabel.defaultAttributes } @@ -365,7 +359,7 @@ extension ConversationViewController: MessagesLayoutDelegate { return 16 } - func footerViewSize(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + private func footerViewSize(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return CGSize(width: messagesCollectionView.bounds.width, height: 10) } } @@ -396,7 +390,7 @@ extension ConversationViewController: MessageCellDelegate { extension ConversationViewController: MessageLabelDelegate { - func didSelectAddress(_ addressComponents: [String : String]) { + func didSelectAddress(_ addressComponents: [String: String]) { print("Address Selected: \(addressComponents)") } @@ -412,7 +406,7 @@ extension ConversationViewController: MessageLabelDelegate { print("URL Selected: \(url)") } - func didSelectTransitInformation(_ transitInformation: [String : String]) { + func didSelectTransitInformation(_ transitInformation: [String: String]) { print("TransitInformation Selected: \(transitInformation)") } diff --git a/Example/Sources/IMessageViewController.swift b/Example/Sources/IMessageViewController.swift index 77a8dcaa..4e29233c 100644 --- a/Example/Sources/IMessageViewController.swift +++ b/Example/Sources/IMessageViewController.swift @@ -8,8 +8,7 @@ import MessageKit -class IMessageViewController: MessagesViewController { - +internal class IMessageViewController: MessagesViewController { override func viewDidLoad() { super.viewDidLoad() diff --git a/Example/Sources/InboxViewController.swift b/Example/Sources/InboxViewController.swift index 37f478bc..559437ad 100644 --- a/Example/Sources/InboxViewController.swift +++ b/Example/Sources/InboxViewController.swift @@ -26,7 +26,7 @@ import UIKit import MessageKit import SafariServices -final class InboxViewController: UITableViewController { +final internal class InboxViewController: UITableViewController { let cells = ["Example", "Settings", "Source Code", "Contributors"] @@ -37,7 +37,7 @@ final class InboxViewController: UITableViewController { title = "MessageKit" navigationController?.navigationBar.tintColor = .white navigationController?.navigationBar.barTintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white, NSAttributedStringKey.font : UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.bold) ] + navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white, NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.bold) ] tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() } diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/MockMessage.swift index 99267958..f7077a82 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/MockMessage.swift @@ -26,7 +26,7 @@ import Foundation import CoreLocation import MessageKit -struct MockLocationItem: LocationItem { +private struct MockLocationItem: LocationItem { var location: CLLocation var size: CGSize @@ -38,7 +38,7 @@ struct MockLocationItem: LocationItem { } -struct MockMediaItem: MediaItem { +private struct MockMediaItem: MediaItem { var url: URL? var image: UIImage? @@ -53,7 +53,7 @@ struct MockMediaItem: MediaItem { } -struct MockMessage: MessageType { +internal struct MockMessage: MessageType { var messageId: String var sender: Sender @@ -95,4 +95,3 @@ struct MockMessage: MessageType { } } - diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index f33d5e8c..10619a8e 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -25,7 +25,7 @@ import MessageKit import CoreLocation -final class SampleData { +final internal class SampleData { static let shared = SampleData() diff --git a/Example/Sources/SettingsViewController.swift b/Example/Sources/SettingsViewController.swift index 61bb813d..dae638b2 100644 --- a/Example/Sources/SettingsViewController.swift +++ b/Example/Sources/SettingsViewController.swift @@ -25,7 +25,7 @@ import UIKit import MessageKit -final class SettingsViewController: UITableViewController { +final internal class SettingsViewController: UITableViewController { // MARK: - Properties @@ -100,16 +100,18 @@ final class SettingsViewController: UITableViewController { // MARK: - Helper private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.identifier, for: indexPath) as! TextFieldTableViewCell - cell.mainLabel.text = "Mock messages count:" - - let messagesCount = UserDefaults.standard.mockMessagesCount() - cell.textField.text = "\(messagesCount)" - - cell.textField.inputView = messagesPicker - cell.textField.inputAccessoryView = messagesToolbar - - return cell + if let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.identifier, for: indexPath) as? TextFieldTableViewCell { + cell.mainLabel.text = "Mock messages count:" + + let messagesCount = UserDefaults.standard.mockMessagesCount() + cell.textField.text = "\(messagesCount)" + + cell.textField.inputView = messagesPicker + cell.textField.inputAccessoryView = messagesToolbar + + return cell + } + return TextFieldTableViewCell() } } diff --git a/Example/Sources/TableViewCells.swift b/Example/Sources/TableViewCells.swift index b3e114e5..6b88a523 100644 --- a/Example/Sources/TableViewCells.swift +++ b/Example/Sources/TableViewCells.swift @@ -24,7 +24,7 @@ import UIKit -class TextFieldTableViewCell: UITableViewCell { +internal class TextFieldTableViewCell: UITableViewCell { static let identifier = "TextFieldTableViewCellIdentifier" From cda394a297af782a5b38f9a4eed7609df37802f4 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 11 Apr 2018 23:43:41 +0800 Subject: [PATCH 70/70] Update Example swiftlint config --- Example/.swiftlint.yml | 5 ----- Example/ChatExample.xcodeproj/project.pbxproj | 6 +++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Example/.swiftlint.yml b/Example/.swiftlint.yml index 8bf8a3da..821c4a93 100644 --- a/Example/.swiftlint.yml +++ b/Example/.swiftlint.yml @@ -11,10 +11,5 @@ custom_rules: regex: "override (open|public|private|internal|fileprivate)" # matching pattern message: "Use like open override or public override instead" # violation message. optional. severity: warning # violation severity. optional. -opt_in_rules: - - explicit_acl - - explicit_top_level_acl -explicit_acl: error -explicit_top_level_acl: error included: - Sources \ No newline at end of file diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 7d32b277..1efe577a 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -267,7 +267,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = MessageKit; TargetAttributes = { 882B5E321CF7D4B900B6E160 = { @@ -570,12 +570,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -625,12 +627,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;