Add improved MessagesCollectionViewFlowLayout

This commit is contained in:
Steven Deutsch
2017-10-20 02:39:42 -05:00
parent 6d30313f16
commit 992f9004cc
@@ -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
}
}