From 992f9004cc2f0cdaa5471e54afa2fcab1492bbb1 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Fri, 20 Oct 2017 02:39:42 -0500 Subject: [PATCH] Add improved MessagesCollectionViewFlowLayout --- .../MessagesCollectionViewFlowLayout.swift | 716 +++++++++--------- 1 file changed, 366 insertions(+), 350 deletions(-) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 83e9ea8b..2e542ee5 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -34,6 +34,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) } } + private var emojiLabelFont: UIFont open var avatarAlwaysLeading: Bool { @@ -52,8 +53,31 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } } - fileprivate var messagesCollectionView: MessagesCollectionView? { - return collectionView as? MessagesCollectionView + open var attributesCacheMaxSize: Int = 500 + + typealias MessageID = String + + fileprivate var intermediateAttributesCache: [MessageID: MessageIntermediateLayoutAttributes] = [:] + + fileprivate var messagesCollectionView: MessagesCollectionView { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError("MessagesCollectionViewFlowLayout is being used on a foreign type.") + } + return messagesCollectionView + } + + fileprivate var messagesDataSource: MessagesDataSource { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError("MessagesDataSource has not been set.") + } + return messagesDataSource + } + + fileprivate var messagesLayoutDelegate: MessagesLayoutDelegate { + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError("MessagesLayoutDeleagte has not been set.") + } + return messagesLayoutDelegate } fileprivate var itemWidth: CGFloat { @@ -115,52 +139,44 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { private func configure(attributes: MessagesCollectionViewLayoutAttributes) { - guard let messagesCollectionView = messagesCollectionView else { return } - guard let dataSource = messagesCollectionView.messagesDataSource else { return } + let intermediateAttributes = messageIntermediateLayoutAttributes(for: attributes.indexPath) - let indexPath = attributes.indexPath - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let messagePadding = messageContainerPadding(for: message, at: indexPath) - let messageInsets = messageLabelInsets(for: message, at: indexPath) - let topLabelInsets = cellTopLabelInsets(for: message, at: indexPath) - let bottomLabelInsets = cellBottomLabelInsets(for: message, at: indexPath) + let messageOrigin = messageContainerOrigin(for: intermediateAttributes, and: attributes.frame) + attributes.messageContainerFrame = CGRect(origin: messageOrigin, size: intermediateAttributes.messageContainerSize) - // First we set all the sizes so we can pass the attributes object around to calculate the origins - attributes.messageContainerFrame = CGRect(origin: .zero, size: messageContainerSize(for: message, at: indexPath)) - attributes.cellTopLabelFrame = CGRect(origin: .zero, size: cellTopLabelSize(for: message, at: indexPath)) - attributes.cellBottomLabelFrame = CGRect(origin: .zero, size: cellBottomLabelSize(for: message, at: indexPath)) - attributes.avatarFrame = CGRect(origin: .zero, size: avatarSize(for: message, at: indexPath)) + let avaOrigin = avatarOrigin(for: intermediateAttributes, and: attributes.frame) + attributes.avatarFrame = CGRect(origin: avaOrigin, size: intermediateAttributes.avatarSize) - switch message.data { + let topLabelOrigin = cellTopLabelOrigin(for: intermediateAttributes, and: attributes.frame) + attributes.cellTopLabelFrame = CGRect(origin: topLabelOrigin, size: intermediateAttributes.cellTopLabelSize) + + let bottomLabelOrigin = cellBottomLabelOrigin(for: intermediateAttributes, and: attributes.frame) + attributes.cellBottomLabelFrame = CGRect(origin: bottomLabelOrigin, size: intermediateAttributes.cellBottomLabelSize) + + switch intermediateAttributes.message.data { case .emoji: attributes.messageLabelFont = emojiLabelFont default: attributes.messageLabelFont = messageLabelFont } - attributes.messagePadding = messagePadding - attributes.messageLabelInsets = messageInsets - attributes.cellTopLabelInsets = topLabelInsets - attributes.cellBottomLabelInsets = bottomLabelInsets - attributes.avatarHorizontalAlignment = avatarHorizontalAlignment(for: message) - - let layoutDelegate = messagesCollectionView.messagesLayoutDelegate - let avatarAlignment = layoutDelegate?.avatarAlignment(for: message, at: indexPath, in: messagesCollectionView) ?? .messageBottom - - // Now we set the origins for the frames using the attributes object that contains the calculated sizes - attributes.messageContainerFrame.origin = messageContainerOrigin(for: attributes) - attributes.cellTopLabelFrame.origin = cellTopLabelOrigin(for: message, and: attributes) - attributes.cellBottomLabelFrame.origin = cellBottomLabelOrigin(for: message, and: attributes) - attributes.avatarFrame.origin = avatarOrigin(for: attributes, with: avatarAlignment) + attributes.avatarHorizontalAlignment = intermediateAttributes.avatarHorizontal + attributes.messagePadding = intermediateAttributes.messageContainerPadding + attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets + attributes.cellTopLabelInsets = intermediateAttributes.cellTopLabelAlignment.insets + attributes.cellBottomLabelInsets = intermediateAttributes.cellBottomLabelAlignment.insets } // MARK: - Invalidation Context open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - - return collectionView?.bounds.width != newBounds.width || collectionView?.bounds.height != newBounds.height - + if collectionView?.bounds.width != newBounds.width { + intermediateAttributesCache = [:] + return true + } else { + return false + } } open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { @@ -170,6 +186,10 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { return flowLayoutContext } + public func clearCachedAttributes(for message: MessageType) { + intermediateAttributesCache.removeValue(forKey: message.messageId) + } + } extension MessagesCollectionViewFlowLayout { @@ -182,62 +202,9 @@ extension MessagesCollectionViewFlowLayout { if avatarAlwaysTrailing { return .cellTrailing } if avatarAlwaysLeading { return .cellLeading } - guard let messagesCollectionView = messagesCollectionView else { return .cellLeading } - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .cellLeading } - return messagesDataSource.isFromCurrentSender(message: message) ? .cellTrailing : .cellLeading } - /// The size of the avatar view. - fileprivate func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - - return layoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView) - } - - /// The origin for the avatar view. - fileprivate func avatarOrigin(for attributes: MessagesCollectionViewLayoutAttributes, with avatarAlignment: AvatarAlignment) -> CGPoint { - - var origin = CGPoint.zero - - let contentWidth = attributes.frame.size.width - let avatarWidth = attributes.avatarFrame.size.width - - switch attributes.avatarHorizontalAlignment { - case .cellLeading: - origin.x = 0 - case .cellTrailing: - origin.x = contentWidth - avatarWidth - } - - let contentHeight = attributes.frame.height - let avatarHeight = attributes.avatarFrame.height - let cellTopLabelHeight = attributes.cellTopLabelFrame.height - let cellBottomLabelHeight = attributes.cellBottomLabelFrame.height - let messageContainerHeight = attributes.messageContainerFrame.height - let messagePadding = attributes.messagePadding - - switch avatarAlignment { - case .cellTop: - origin.y = 0 - case .cellBottom: - origin.y = contentHeight - avatarHeight - case .messageTop: - origin.y = cellTopLabelHeight + messagePadding.top - case .messageBottom: - origin.y = contentHeight - avatarHeight - cellBottomLabelHeight - messagePadding.bottom - case .messageCenter: - let messageMidY = messageContainerHeight / 2 - let avatarMidY = avatarHeight / 2 - origin.y = cellTopLabelHeight + messagePadding.top + messageMidY - avatarMidY - } - - return origin - - } - // MARK: - Label Size Calculations /// The size required fit a NSAttributedString considering a constrained max width. @@ -264,354 +231,403 @@ extension MessagesCollectionViewFlowLayout { return CGSize(width: finalWidth, height: finalHeight) } - // MARK: - Message Container Calculations + // MARK: - Cell Sizing - fileprivate func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - return layoutDelegate.messagePadding(for: message, at: indexPath, in: messagesCollectionView) + /// The size for the cell considering all cell contents. + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let attributes = messageIntermediateLayoutAttributes(for: indexPath) + return CGSize(width: itemWidth, height: attributes.itemHeight) } - /// The insets for the text of the MessageLabel - private func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - return layoutDelegate.messageLabelInset(for: message, at: indexPath, in: messagesCollectionView) + func messageIntermediateLayoutAttributes(for indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { + + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + if let intermediateAttributes = intermediateAttributesCache[message.messageId] { + return intermediateAttributes + } else { + let newAttributes = createMessageIntermediateLayoutAttributes(for: message, at: indexPath) + + if intermediateAttributesCache.count < attributesCacheMaxSize { + intermediateAttributesCache[message.messageId] = newAttributes + } + return newAttributes + } + } - /// The max width available for the message container. - private func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + func createMessageIntermediateLayoutAttributes(for message: MessageType, at indexPath: IndexPath) -> MessageIntermediateLayoutAttributes { - let avatarWidth = avatarSize(for: message, at: indexPath).width - let messagePadding = messageContainerPadding(for: message, at: indexPath) - let messageInsets = messageLabelInsets(for: message, at: indexPath) + let attributes = MessageIntermediateLayoutAttributes(message: message, indexPath: indexPath) - let messageHorizontalPadding = messagePadding.left + messagePadding.right - let messageHorizontalInsets = messageInsets.left + messageInsets.right + // None of these are dependent on other attributes + attributes.avatarVertical = avatarVerticalAlignment(for: attributes) + attributes.avatarHorizontal = avatarHorizontalAlignment(for: attributes) + attributes.avatarSize = avatarSize(for: attributes) + attributes.messageContainerPadding = messageContainerPadding(for: attributes) + attributes.messageLabelInsets = messageLabelInsets(for: attributes) - let availableWidth = itemWidth - avatarWidth - messageHorizontalPadding - messageHorizontalInsets + // MessageContainerView + attributes.messageContainerMaxWidth = messageContainerMaxWidth(for: attributes) + attributes.messageContainerSize = messageContainerSize(for: attributes) - return availableWidth + // Cell Bottom Label + attributes.cellBottomLabelText = cellBottomLabelText(for: attributes) // little concerned about storing text here TODO + attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: attributes) + attributes.cellBottomLabelMaxWidth = cellBottomLabelMaxWidth(for: attributes) + attributes.cellBottomLabelSize = cellBottomLabelSize(for: attributes) + + // Cell Top Label + attributes.cellTopLabelText = cellTopLabelText(for: attributes) // little concerned about storing text here TODO + attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: attributes) + attributes.cellTopLabelMaxWidth = cellTopLabelMaxWidth(for: attributes) + attributes.cellTopLabelSize = cellTopLabelSize(for: attributes) + + // Cell Height + attributes.itemHeight = cellHeight(for: attributes) + + return attributes } - /// The size for the message container. - fileprivate func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + // MARK: - AvatarView Size [ A - C ] - let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - let messageInsets = messageLabelInsets(for: message, at: indexPath) - let messageHorizontalInsets = messageInsets.left + messageInsets.right - let messageVerticalInsets = messageInsets.top + messageInsets.bottom + // A + private func avatarHorizontalAlignment(for attributes: MessageIntermediateLayoutAttributes) -> AvatarHorizontalAlignment { + return avatarHorizontalAlignment(for: attributes.message) + } + // B + private func avatarSize(for attributes: MessageIntermediateLayoutAttributes) -> CGSize { + return messagesLayoutDelegate.avatarSize(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView) + } + // C + private func avatarVerticalAlignment(for attributes: MessageIntermediateLayoutAttributes) -> AvatarAlignment { + return messagesLayoutDelegate.avatarAlignment(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView) + } + + // MARK: - MessageContainerView Size [ D - G ] + + // D + private func messageContainerPadding(for attributes: MessageIntermediateLayoutAttributes) -> UIEdgeInsets { + return messagesLayoutDelegate.messagePadding(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView) + } + + // E + private func messageLabelInsets(for attributes: MessageIntermediateLayoutAttributes) -> UIEdgeInsets { + // Maybe check the message type here since insets only apply to text messages + return messagesLayoutDelegate.messageLabelInset(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView) + } + + // F + private func messageContainerMaxWidth(for attributes: MessageIntermediateLayoutAttributes) -> CGFloat { + + switch attributes.message.data { + case .text, .attributedText: + return itemWidth - attributes.avatarSize.width - attributes.messageHorizontalPadding - attributes.messageLabelHorizontalInsets + default: + return itemWidth - attributes.avatarSize.width - attributes.messageHorizontalPadding + } + + } + + // G + private func messageContainerSize(for attributes: MessageIntermediateLayoutAttributes) -> CGSize { + + let message = attributes.message + let indexPath = attributes.indexPath + let maxWidth = attributes.messageContainerMaxWidth var messageContainerSize: CGSize = .zero - switch message.data { + switch attributes.message.data { case .text(let text): messageContainerSize = labelSize(for: text, considering: maxWidth, and: messageLabelFont) - messageContainerSize.width += messageHorizontalInsets - messageContainerSize.height += messageVerticalInsets + messageContainerSize.width += attributes.messageLabelHorizontalInsets + messageContainerSize.height += attributes.messageLabelVerticalInsets case .attributedText(let text): messageContainerSize = labelSize(for: text, considering: maxWidth) - messageContainerSize.width += messageHorizontalInsets - messageContainerSize.height += messageVerticalInsets + messageContainerSize.width += attributes.messageLabelHorizontalInsets + messageContainerSize.height += attributes.messageLabelVerticalInsets + case .emoji(let text): + messageContainerSize = labelSize(for: text, considering: maxWidth, and: emojiLabelFont) + messageContainerSize.width += attributes.messageLabelHorizontalInsets + messageContainerSize.height += attributes.messageLabelVerticalInsets case .photo, .video: - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate as? MediaMessageLayoutDelegate else { return .zero } + guard let layoutDelegate = messagesLayoutDelegate as? MediaMessageLayoutDelegate else { + fatalError("Layout object attempting to size media message type and MediaMessageLayoutDelegate is not set.") + } let width = layoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) let height = layoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) messageContainerSize = CGSize(width: width, height: height) case .location: - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate as? LocationMessageLayoutDelegate else { return .zero } + guard let layoutDelegate = messagesLayoutDelegate as? LocationMessageLayoutDelegate else { + fatalError("Layout object attempting to size location message type and LocationMessageLayoutDelegate is not set.") + } let width = layoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) let height = layoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) messageContainerSize = CGSize(width: width, height: height) - case .emoji(let text): - messageContainerSize = labelSize(for: text, considering: maxWidth, and: emojiLabelFont) - messageContainerSize.width += messageHorizontalInsets - messageContainerSize.height += messageVerticalInsets } return messageContainerSize - } - - /// The origin for the message container. - fileprivate func messageContainerOrigin(for attributes: MessagesCollectionViewLayoutAttributes) -> CGPoint { - - let avatarWidth = attributes.avatarFrame.width - - var origin = CGPoint.zero - - switch attributes.avatarHorizontalAlignment { - case .cellLeading: - origin.x = avatarWidth + attributes.messagePadding.left - origin.y = attributes.cellTopLabelFrame.height + attributes.messagePadding.top - case .cellTrailing: - origin.x = attributes.frame.width - avatarWidth - attributes.messageContainerFrame.width - attributes.messagePadding.right - origin.y = attributes.cellTopLabelFrame.height + attributes.messagePadding.top - } - - return origin } - // MARK: - Cell Top Label Calculations + // MARK: - Cell Bottom Label Size [ H - K ] - /// The insets for the cell's top label. - private func cellTopLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - let labelAlignment = layoutDelegate.cellTopLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - return labelAlignment.insets + // H + private func cellBottomLabelText(for attributes: MessageIntermediateLayoutAttributes) -> NSAttributedString? { + return messagesDataSource.cellBottomLabelAttributedText(for: attributes.message, at: attributes.indexPath) } - /// The max width available for the cell top label. - private func topLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - guard let messagesCollectionView = messagesCollectionView else { return 0 } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return 0 } - - let labelHorizontal = layoutDelegate.cellTopLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - let avatarVertical = layoutDelegate.avatarAlignment(for: message, at: indexPath, in: messagesCollectionView) - let avatarHorizontal = avatarHorizontalAlignment(for: message) - let topLabelHorizontalInsets = labelHorizontal.insets.left + labelHorizontal.insets.right - - let avatarWidth = layoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView).width - - switch (labelHorizontal, avatarHorizontal, avatarVertical) { - - case (.cellLeading, .cellTrailing, .cellTop), (.cellTrailing, .cellLeading, .cellTop): - return itemWidth - avatarWidth - topLabelHorizontalInsets - - case (.cellLeading, _, _), (.cellTrailing, _, _): - return itemWidth - topLabelHorizontalInsets - - case (.cellCenter, .cellLeading, .cellTop), (.cellCenter, .cellTrailing, .cellTop): - return itemWidth - (avatarWidth * 2) - topLabelHorizontalInsets - - case (.cellCenter, .cellLeading, _), (.cellCenter, .cellTrailing, _): - return itemWidth - topLabelHorizontalInsets - - case (.messageTrailing, .cellLeading, .cellTop), (.messageLeading, .cellTrailing, .cellTop): - return messageContainerSize(for: message, at: indexPath).width - topLabelHorizontalInsets - - case (.messageLeading, .cellLeading, _), (.messageTrailing, .cellTrailing, _): - return itemWidth - avatarWidth - topLabelHorizontalInsets - - case (.messageLeading, .cellTrailing, _), (.messageTrailing, .cellLeading, _): - return messageContainerSize(for: message, at: indexPath).width + avatarWidth - topLabelHorizontalInsets - } + // I + private func cellBottomLabelAlignment(for attributes: MessageIntermediateLayoutAttributes) -> LabelAlignment { + return messagesLayoutDelegate.cellBottomLabelAlignment(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView) } - /// The size of the cell top label. - fileprivate func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + // J + private func cellBottomLabelMaxWidth(for attributes: MessageIntermediateLayoutAttributes) -> CGFloat { - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero } - guard let topLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) else { return .zero } + guard attributes.cellBottomLabelText != nil else { return 0 } - let maxWidth = topLabelMaxWidth(for: message, at: indexPath) - var size = labelSize(for: topLabelText, considering: maxWidth) - - let topLabelInsets = cellTopLabelInsets(for: message, at: indexPath) - let topLabelHorizontalInsets = topLabelInsets.left + topLabelInsets.right - let topLabelVerticalInsets = topLabelInsets.top + topLabelInsets.bottom - - size.width += topLabelHorizontalInsets - size.height += topLabelVerticalInsets - - return size - - } - - /// The origin for the cell top label. - fileprivate func cellTopLabelOrigin(for message: MessageType, and attributes: MessagesCollectionViewLayoutAttributes) -> CGPoint { - - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - - let labelAlignment = layoutDelegate.cellTopLabelAlignment(for: message, at: attributes.indexPath, in: messagesCollectionView) - let avatarAlignment = avatarHorizontalAlignment(for: message) - - var origin = CGPoint.zero - - switch (labelAlignment, avatarAlignment) { - case (.cellLeading, _): - origin.x = 0 - case (.cellCenter, _): - origin.x = attributes.frame.width / 2 - (attributes.cellTopLabelFrame.width / 2) - case (.cellTrailing, _): - origin.x = attributes.frame.width - attributes.cellTopLabelFrame.width - case (.messageLeading, .cellLeading): - origin.x = attributes.avatarFrame.width + attributes.messagePadding.left - case (.messageLeading, .cellTrailing): - origin.x = attributes.frame.width - attributes.avatarFrame.width - attributes.messagePadding.right - attributes.messageContainerFrame.width - case (.messageTrailing, .cellTrailing): - origin.x = attributes.frame.width - attributes.avatarFrame.width - attributes.messagePadding.right - attributes.cellTopLabelFrame.width - case (.messageTrailing, .cellLeading): - origin.x = attributes.frame.width - attributes.messagePadding.right - attributes.cellTopLabelFrame.width - } - - return origin - - } - - // MARK: - Cell Bottom Label Calculations - - /// The insets for the cell's top label. - private func cellBottomLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - let labelAlignment = layoutDelegate.cellBottomLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - return labelAlignment.insets - } - - /// The max with available for the cell bottom label. - private func bottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - guard let messagesCollectionView = messagesCollectionView else { return 0 } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return 0 } - - let labelHorizontal = layoutDelegate.cellBottomLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - let avatarVertical = layoutDelegate.avatarAlignment(for: message, at: indexPath, in: messagesCollectionView) - let avatarHorizontal = avatarHorizontalAlignment(for: message) - let bottomLabelHorizontalInsets = labelHorizontal.insets.left + labelHorizontal.insets.right - - let avatarWidth = layoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView).width + let labelHorizontal = attributes.cellBottomLabelAlignment + let avatarHorizontal = attributes.avatarHorizontal + let avatarVertical = attributes.avatarVertical + let avatarWidth = attributes.avatarSize.width switch (labelHorizontal, avatarHorizontal, avatarVertical) { case (.cellLeading, .cellTrailing, .cellBottom), (.cellTrailing, .cellLeading, .cellBottom): - return itemWidth - avatarWidth - bottomLabelHorizontalInsets + return itemWidth - avatarWidth - attributes.cellBottomLabelHorizontalInsets case (.cellLeading, _, _), (.cellTrailing, _, _): - return itemWidth - bottomLabelHorizontalInsets + return itemWidth - attributes.cellBottomLabelHorizontalInsets case (.cellCenter, .cellLeading, .cellBottom), (.cellCenter, .cellTrailing, .cellBottom): - return itemWidth - (avatarWidth * 2) - bottomLabelHorizontalInsets + return itemWidth - (avatarWidth * 2) - attributes.cellBottomLabelHorizontalInsets case (.cellCenter, .cellLeading, _), (.cellCenter, .cellTrailing, _): - return itemWidth - bottomLabelHorizontalInsets + return itemWidth - attributes.cellBottomLabelHorizontalInsets case (.messageTrailing, .cellLeading, .cellBottom), (.messageLeading, .cellTrailing, .cellBottom): - return messageContainerSize(for: message, at: indexPath).width - bottomLabelHorizontalInsets + return attributes.messageContainerSize.width - attributes.cellBottomLabelHorizontalInsets case (.messageLeading, .cellLeading, _), (.messageTrailing, .cellTrailing, _): - return itemWidth - avatarWidth - bottomLabelHorizontalInsets + return itemWidth - avatarWidth - attributes.cellBottomLabelHorizontalInsets case (.messageLeading, .cellTrailing, _), (.messageTrailing, .cellLeading, _): - return messageContainerSize(for: message, at: indexPath).width + avatarWidth - bottomLabelHorizontalInsets - + return attributes.messageContainerSize.width + avatarWidth - attributes.cellBottomLabelHorizontalInsets } - } - - /// The size for the cell bottom label. - fileprivate func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero } - guard let bottomLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) else { return .zero } - - let maxWidth = bottomLabelMaxWidth(for: message, at: indexPath) - var size = labelSize(for: bottomLabelText, considering: maxWidth) - - let bottomLabelInsets = cellBottomLabelInsets(for: message, at: indexPath) - let bottomLabelHorizontalInsets = bottomLabelInsets.left + bottomLabelInsets.right - let bottomLabelVerticalInsets = bottomLabelInsets.top + bottomLabelInsets.bottom - - size.width += bottomLabelHorizontalInsets - size.height += bottomLabelVerticalInsets - - return size } - /// The origin for the cell bottom label. - fileprivate func cellBottomLabelOrigin(for message: MessageType, and attributes: MessagesCollectionViewLayoutAttributes) -> CGPoint { + // K + private func cellBottomLabelSize(for attributes: MessageIntermediateLayoutAttributes) -> CGSize { + guard let bottomLabelText = attributes.cellBottomLabelText else { return .zero } - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + var bottomLabelSize = labelSize(for: bottomLabelText, considering: attributes.cellBottomLabelMaxWidth) + bottomLabelSize.width += attributes.cellBottomLabelHorizontalInsets + bottomLabelSize.height += attributes.cellBottomLabelVerticalInsets - let labelAlignment = layoutDelegate.cellBottomLabelAlignment(for: message, at: attributes.indexPath, in: messagesCollectionView) - let avatarAlignment = avatarHorizontalAlignment(for: message) + return bottomLabelSize - var origin = CGPoint(x: 0, y: attributes.frame.height - attributes.cellBottomLabelFrame.height) + } - switch (labelAlignment, avatarAlignment) { - case (.cellLeading, _): + // MARK: - Cell Top Label Sizing [ L - O ] + + // L + private func cellTopLabelText(for attributes: MessageIntermediateLayoutAttributes) -> NSAttributedString? { + return messagesDataSource.cellTopLabelAttributedText(for: attributes.message, at: attributes.indexPath) + } + + // M + private func cellTopLabelAlignment(for attributes: MessageIntermediateLayoutAttributes) -> LabelAlignment { + return messagesLayoutDelegate.cellTopLabelAlignment(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView) + } + + // N + private func cellTopLabelMaxWidth(for attributes: MessageIntermediateLayoutAttributes) -> CGFloat { + + guard attributes.cellTopLabelText != nil else { return 0 } + + let labelHorizontal = attributes.cellTopLabelAlignment + let avatarHorizontal = attributes.avatarHorizontal + let avatarVertical = attributes.avatarVertical + let avatarWidth = attributes.avatarSize.width + + switch (labelHorizontal, avatarHorizontal, avatarVertical) { + + case (.cellLeading, .cellTrailing, .cellTop), (.cellTrailing, .cellLeading, .cellTop): + return itemWidth - avatarWidth - attributes.cellTopLabelHorizontalInsets + + case (.cellLeading, _, _), (.cellTrailing, _, _): + return itemWidth - attributes.cellTopLabelHorizontalInsets + + case (.cellCenter, .cellLeading, .cellTop), (.cellCenter, .cellTrailing, .cellTop): + return itemWidth - (avatarWidth * 2) - attributes.cellTopLabelHorizontalInsets + + case (.cellCenter, .cellLeading, _), (.cellCenter, .cellTrailing, _): + return itemWidth - attributes.cellTopLabelHorizontalInsets + + case (.messageTrailing, .cellLeading, .cellTop), (.messageLeading, .cellTrailing, .cellTop): + return attributes.messageContainerSize.width - attributes.cellTopLabelHorizontalInsets + + case (.messageLeading, .cellLeading, _), (.messageTrailing, .cellTrailing, _): + return itemWidth - avatarWidth - attributes.cellTopLabelHorizontalInsets + + case (.messageLeading, .cellTrailing, _), (.messageTrailing, .cellLeading, _): + return attributes.messageContainerSize.width + avatarWidth - attributes.cellTopLabelHorizontalInsets + } + + } + + // O + private func cellTopLabelSize(for attributes: MessageIntermediateLayoutAttributes) -> CGSize { + guard let topLabelText = attributes.cellTopLabelText else { return .zero } + + var topLabelSize = labelSize(for: topLabelText, considering: attributes.cellTopLabelMaxWidth) + topLabelSize.width += attributes.cellTopLabelHorizontalInsets + topLabelSize.height += attributes.cellTopLabelVerticalInsets + + return topLabelSize + } + + // MARK: - Cell Height [ P ] + + // P + private func cellHeight(for attributes: MessageIntermediateLayoutAttributes) -> CGFloat { + + var cellHeight: CGFloat = 0 + + switch attributes.avatarVertical { + case .cellTop: + cellHeight += max(attributes.avatarSize.height, attributes.cellTopLabelSize.height) + cellHeight += attributes.cellBottomLabelSize.height + cellHeight += attributes.messageContainerSize.height + cellHeight += attributes.messageVerticalPadding + case .cellBottom: + cellHeight += max(attributes.avatarSize.height, attributes.cellBottomLabelSize.height) + cellHeight += attributes.cellTopLabelSize.height + cellHeight += attributes.messageContainerSize.height + cellHeight += attributes.messageVerticalPadding + case .messageTop, .messageCenter, .messageBottom: + cellHeight += max(attributes.avatarSize.height, attributes.messageContainerSize.height) + cellHeight += attributes.messageVerticalPadding + cellHeight += attributes.cellTopLabelSize.height + cellHeight += attributes.cellBottomLabelSize.height + } + + return cellHeight + } + + // MARK: - Origins [ Q - T ] + + // Q + private func avatarOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint { + + guard attributes.avatarSize != .zero else { return .zero } + + var origin = CGPoint.zero + + switch attributes.avatarHorizontal { + case .cellLeading: origin.x = 0 - case (.cellCenter, _): - origin.x = attributes.frame.width / 2 - (attributes.cellBottomLabelFrame.width / 2) - case (.cellTrailing, _): - origin.x = attributes.frame.width - attributes.cellBottomLabelFrame.width - case (.messageLeading, .cellLeading): - origin.x = attributes.avatarFrame.width + attributes.messagePadding.left - case (.messageLeading, .cellTrailing): - origin.x = attributes.frame.width - attributes.avatarFrame.width - attributes.messagePadding.right - attributes.messageContainerFrame.width - case (.messageTrailing, .cellTrailing): - origin.x = attributes.frame.width - attributes.avatarFrame.width - attributes.messagePadding.right - attributes.cellBottomLabelFrame.width - case (.messageTrailing, .cellLeading): - origin.x = attributes.avatarFrame.width + attributes.messagePadding.left + attributes.messageContainerFrame.width - attributes.cellBottomLabelFrame.width + case .cellTrailing: + origin.x = contentFrame.width - attributes.avatarSize.width + } + + switch attributes.avatarVertical { + case .cellTop: + origin.y = 0 + case .cellBottom: + origin.y = contentFrame.height - attributes.avatarSize.height + case .messageTop: + origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top + case .messageBottom: + origin.y = contentFrame.height - attributes.avatarSize.height - attributes.cellBottomLabelSize.height - attributes.messageContainerPadding.bottom + case .messageCenter: + let messageMidY = attributes.messageContainerSize.height / 2 + let avatarMidY = attributes.avatarSize.height / 2 + origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top + messageMidY - avatarMidY + } + + return origin + } + + // R + private func messageContainerOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint { + + guard attributes.messageContainerSize != .zero else { return .zero } + + var origin = CGPoint.zero + + switch attributes.avatarHorizontal { + case .cellLeading: + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top + case .cellTrailing: + origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right + origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top } return origin } - // MARK: - Cell Size Calculations + // S + private func cellBottomLabelOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint { - /// The minimum height for the cell. - private func minimumCellHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + guard attributes.cellBottomLabelSize != .zero else { return .zero } - guard let messagesCollectionView = messagesCollectionView else { return 0 } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return 0 } + var origin = CGPoint(x: 0, y: contentFrame.height - attributes.cellBottomLabelSize.height) - let avatarAlignment = layoutDelegate.avatarAlignment(for: message, at: indexPath, in: messagesCollectionView) - - let topLabelHeight = cellTopLabelSize(for: message, at: indexPath).height - let avatarHeight = avatarSize(for: message, at: indexPath).height - let bottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height - let messagePadding = messageContainerPadding(for: message, at: indexPath) - let messagePaddingVertical = messagePadding.top + messagePadding.bottom - - switch avatarAlignment { - case .cellTop: - return max(avatarHeight, topLabelHeight) + bottomLabelHeight + messagePaddingVertical - case .cellBottom: - return max(avatarHeight, bottomLabelHeight) + topLabelHeight + messagePaddingVertical - case .messageTop, .messageCenter, .messageBottom: - return topLabelHeight + avatarHeight + bottomLabelHeight + messagePaddingVertical + switch (attributes.cellBottomLabelAlignment, attributes.avatarHorizontal) { + case (.cellLeading, _): + origin.x = 0 + case (.cellCenter, _): + origin.x = contentFrame.width / 2 - (attributes.cellBottomLabelSize.width / 2) + case (.cellTrailing, _): + origin.x = contentFrame.width - attributes.cellBottomLabelSize.width + case (.messageLeading, .cellLeading): + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + case (.messageLeading, .cellTrailing): + origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.cellBottomLabelSize.width + case (.messageTrailing, .cellTrailing): + origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.cellBottomLabelSize.width + case (.messageTrailing, .cellLeading): + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + attributes.messageContainerSize.width - attributes.cellBottomLabelSize.width } - } - - /// The calculated height of the cell not considering the minimum height. - private func estimatedCellHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height - let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height - let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height - let messagePadding = messageContainerPadding(for: message, at: indexPath) - let messagePaddingVertical = messagePadding.top + messagePadding.bottom - - return messageContainerHeight + cellBottomLabelHeight + cellTopLabelHeight + messagePaddingVertical + return origin } - /// The size for the cell considering all cell contents. - open func sizeForItem(at indexPath: IndexPath) -> CGSize { + // T + fileprivate func cellTopLabelOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint { - guard let messagesCollectionView = messagesCollectionView else { return .zero } - guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard attributes.cellTopLabelSize != .zero else { return .zero } - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) + var origin = CGPoint.zero - let minimumHeight = minimumCellHeight(for: message, at: indexPath) - let estimatedHeight = estimatedCellHeight(for: message, at: indexPath) + switch (attributes.cellTopLabelAlignment, attributes.avatarHorizontal) { + case (.cellLeading, _): + origin.x = 0 + case (.cellCenter, _): + origin.x = contentFrame.width / 2 - (attributes.cellTopLabelSize.width / 2) + case (.cellTrailing, _): + origin.x = contentFrame.width - attributes.cellTopLabelSize.width + case (.messageLeading, .cellLeading): + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + case (.messageLeading, .cellTrailing): + origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.messageContainerSize.width + case (.messageTrailing, .cellTrailing): + origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.cellTopLabelSize.width + case (.messageTrailing, .cellLeading): + origin.x = contentFrame.width - attributes.messageContainerPadding.right - attributes.cellTopLabelSize.width + } - let itemHeight = estimatedHeight > minimumHeight ? estimatedHeight : minimumHeight - - return CGSize(width: itemWidth, height: itemHeight) + return origin } } + +