mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
Gardening and open a few methods
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = "<group>"; };
|
||||
1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayout.swift; sourceTree = "<group>"; };
|
||||
1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIntermediateLayoutAttributes.swift; sourceTree = "<group>"; };
|
||||
1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = "<group>"; };
|
||||
1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = "<group>"; };
|
||||
1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = "<group>"; };
|
||||
@@ -440,6 +442,7 @@
|
||||
B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */,
|
||||
B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */,
|
||||
1FD5895B205F5B8F004B5081 /* MessageCellLayout.swift */,
|
||||
1FD5895D205F6E77004B5081 /* MessageIntermediateLayoutAttributes.swift */,
|
||||
);
|
||||
path = Layout;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user