From 75807db05bf74372ac0ef791aca91089abb53b82 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sun, 18 Mar 2018 23:43:03 -0500 Subject: [PATCH] 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 - }() - -}