Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compound bubble updates #587

Merged
merged 12 commits into from
Jul 9, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>

public let compoundCellStyle: CompoundBubbleViewStyleProtocol
private let contentFactories: [AnyMessageContentFactory<ModelT>]
private let compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions

private let cache: Cache<CompoundBubbleLayoutProvider.Configuration, CompoundBubbleLayoutProvider>
private let accessibilityIdentifier: String?
Expand All @@ -51,10 +52,12 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
sizingCell: CompoundMessageCollectionViewCell,
baseCellStyle: BaseMessageCollectionViewCellStyleProtocol,
compoundCellStyle: CompoundBubbleViewStyleProtocol,
compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions,
cache: Cache<CompoundBubbleLayoutProvider.Configuration, CompoundBubbleLayoutProvider>,
accessibilityIdentifier: String?
) {
self.compoundCellStyle = compoundCellStyle
self.compoundCellDimensions = compoundCellDimensions
self.contentFactories = contentFactories.filter { $0.canCreateMessageContent(forModel: messageModel) }
self.cache = cache
self.accessibilityIdentifier = accessibilityIdentifier
Expand Down Expand Up @@ -164,7 +167,8 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
return CompoundBubbleLayoutProvider.Configuration(
layoutProviders: contentLayoutProviders,
tailWidth: tailWidth,
isIncoming: viewModel.isIncoming
isIncoming: viewModel.isIncoming,
dimensions: self.compoundCellDimensions
)
}()
defer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,23 @@ public final class CompoundMessagePresenterBuilder<ViewModelBuilderT, Interactio
accessibilityIdentifier: String?,
contentFactories: [AnyMessageContentFactory<ModelT>],
compoundCellStyle: CompoundBubbleViewStyleProtocol = DefaultCompoundBubbleViewStyle(),
compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions,
baseCellStyle: BaseMessageCollectionViewCellStyleProtocol = BaseMessageCollectionViewCellDefaultStyle()) {
self.viewModelBuilder = viewModelBuilder
self.interactionHandler = interactionHandler
self.contentFactories = contentFactories
self.accessibilityIdentifier = accessibilityIdentifier
self.compoundCellStyle = compoundCellStyle
self.baseCellStyle = baseCellStyle
self.compoundCellDimensions = compoundCellDimensions
}

public let viewModelBuilder: ViewModelBuilderT
public let interactionHandler: InteractionHandlerT?
private let contentFactories: [AnyMessageContentFactory<ModelT>]
public let sizingCell: CompoundMessageCollectionViewCell = CompoundMessageCollectionViewCell()
private let compoundCellStyle: CompoundBubbleViewStyleProtocol
private let compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions
private let baseCellStyle: BaseMessageCollectionViewCellStyleProtocol
private let cache = Cache<CompoundBubbleLayoutProvider.Configuration, CompoundBubbleLayoutProvider>()
private let accessibilityIdentifier: String?
Expand All @@ -70,6 +73,7 @@ public final class CompoundMessagePresenterBuilder<ViewModelBuilderT, Interactio
sizingCell: self.sizingCell,
baseCellStyle: self.baseCellStyle,
compoundCellStyle: self.compoundCellStyle,
compoundCellDimensions: self.compoundCellDimensions,
cache: self.cache,
accessibilityIdentifier: self.accessibilityIdentifier
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import UIKit

public protocol MessageManualLayoutProviderProtocol: HashableRepresentible {
var ignoreContentInsets: Bool { get }
func sizeThatFits(size: CGSize, safeAreaInsets: UIEdgeInsets) -> CGSize
}

Expand All @@ -43,6 +44,8 @@ public struct TextMessageLayoutProvider: Hashable, MessageManualLayoutProviderPr
self.numberOfLines = numberOfLines
}

public let ignoreContentInsets: Bool = false

public func sizeThatFits(size: CGSize, safeAreaInsets: UIEdgeInsets) -> CGSize {
var sizeWithInset = size
sizeWithInset.substract(insets: safeAreaInsets)
Expand Down Expand Up @@ -77,6 +80,8 @@ public struct ImageMessageLayoutProvider: Hashable, MessageManualLayoutProviderP
self.imageSize = imageSize
}

public let ignoreContentInsets: Bool = false

public func sizeThatFits(size: CGSize, safeAreaInsets _: UIEdgeInsets) -> CGSize {
let ratio = self.imageSize.width / self.imageSize.height
return CGSize(width: size.width, height: size.width / ratio).bma_round()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,47 @@ public struct CompoundBubbleLayout {

public struct CompoundBubbleLayoutProvider {

public struct Dimensions: Hashable {
public let spacing: CGFloat
public let contentInsets: UIEdgeInsets

public init(spacing: CGFloat,
contentInsets: UIEdgeInsets) {
self.spacing = spacing
self.contentInsets = contentInsets
}
}

public struct Configuration: Hashable {

fileprivate let layoutProviders: [MessageManualLayoutProviderProtocol]
fileprivate let tailWidth: CGFloat
fileprivate let isIncoming: Bool
fileprivate let dimensions: Dimensions

public init(layoutProviders: [MessageManualLayoutProviderProtocol],
tailWidth: CGFloat,
isIncoming: Bool) {
isIncoming: Bool,
dimensions: Dimensions) {
self.layoutProviders = layoutProviders
self.tailWidth = tailWidth
self.isIncoming = isIncoming
self.dimensions = dimensions
}

public func hash(into hasher: inout Hasher) {
hasher.combine(self.layoutProviders.map { $0.asHashable })
hasher.combine(self.tailWidth)
hasher.combine(self.isIncoming)
hasher.combine(self.dimensions)
}

public static func == (lhs: CompoundBubbleLayoutProvider.Configuration,
rhs: CompoundBubbleLayoutProvider.Configuration) -> Bool {
return lhs.layoutProviders.map { $0.asHashable } == rhs.layoutProviders.map { $0.asHashable }
&& lhs.tailWidth == rhs.tailWidth
&& lhs.isIncoming == rhs.isIncoming
&& lhs.dimensions == rhs.dimensions
}
}

Expand All @@ -73,25 +89,61 @@ public struct CompoundBubbleLayoutProvider {
return layout
}

private typealias RectWithLayoutProvider = (frame: CGRect, provider: MessageManualLayoutProviderProtocol)

private func makeLayout(forMaxWidth width: CGFloat) -> CompoundBubbleLayout {
var subviewsFrames: [CGRect] = []
subviewsFrames.reserveCapacity(self.configuration.layoutProviders.count)
var maxY: CGFloat = 0
var resultWidth: CGFloat = 0
let sizeToFit = CGSize(width: width,
height: .greatestFiniteMagnitude)
var subviewsFramesWithProviders: [RectWithLayoutProvider] = []
subviewsFramesWithProviders.reserveCapacity(self.configuration.layoutProviders.count)
let contentInsets = self.configuration.dimensions.contentInsets
let safeAreaInsets = self.safeAreaInsets()
for layoutProvider in self.configuration.layoutProviders {
let size = layoutProvider.sizeThatFits(size: sizeToFit, safeAreaInsets: safeAreaInsets)
let viewWidth = max(size.width, resultWidth)
resultWidth = min(viewWidth, width)
let frame = CGRect(x: 0, y: maxY, width: viewWidth, height: size.height)
subviewsFrames.append(frame)
let totalInsets = UIEdgeInsets(top: safeAreaInsets.top + contentInsets.top,
left: safeAreaInsets.left + contentInsets.left,
bottom: safeAreaInsets.bottom + contentInsets.bottom,
right: safeAreaInsets.right + contentInsets.right)

var resultWidth: CGFloat = 0
let fittingSizeWithInsets = CGSize(width: width - contentInsets.bma_horziontalInset, height: .greatestFiniteMagnitude)
let shouldAddTopInset = self.configuration.layoutProviders.first?.ignoreContentInsets == false
let shouldAddBottomInset = self.configuration.layoutProviders.last?.ignoreContentInsets == false

var maxY: CGFloat = shouldAddTopInset ? totalInsets.top : 0
for (i, layoutProvider) in self.configuration.layoutProviders.enumerated() {
let frame: CGRect
if layoutProvider.ignoreContentInsets {
let size = layoutProvider.sizeThatFits(size: CGSize(width: width, height: .greatestFiniteMagnitude), safeAreaInsets: safeAreaInsets)
let viewWidth = max(size.width, resultWidth)
resultWidth = min(viewWidth, width)
frame = CGRect(x: 0, y: maxY, width: viewWidth, height: size.height)
} else {
let size = layoutProvider.sizeThatFits(size: fittingSizeWithInsets, safeAreaInsets: safeAreaInsets)
let viewWidth = max(size.width, resultWidth)
resultWidth = min(viewWidth + totalInsets.bma_horziontalInset, width)
frame = CGRect(x: totalInsets.left, y: maxY, width: viewWidth, height: size.height)
}
subviewsFramesWithProviders.append((frame, layoutProvider))
maxY = frame.maxY
if i != self.configuration.layoutProviders.count - 1 {
maxY += self.configuration.dimensions.spacing
}
}

subviewsFramesWithProviders = subviewsFramesWithProviders.map({ frameWithProvider in
var newFrame = frameWithProvider.frame
if frameWithProvider.provider.ignoreContentInsets {
newFrame.size.width = resultWidth
} else {
newFrame.size.width = resultWidth - totalInsets.bma_horziontalInset
}
return (newFrame, frameWithProvider.provider)
})

if shouldAddBottomInset {
maxY += totalInsets.bottom
}

return CompoundBubbleLayout(
size: CGSize(width: resultWidth, height: maxY).bma_round(),
subviewsFrames: subviewsFrames,
subviewsFrames: subviewsFramesWithProviders.map({ $0.frame }),
safeAreaInsets: safeAreaInsets
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class DemoChatViewController: BaseChatViewController {
.init(DemoImageMessageContentFactory()),
.init(DemoDateMessageContentFactory())
],
compoundCellDimensions: .defaultDimensions,
baseCellStyle: BaseMessageCollectionViewCellAvatarStyle()
)

Expand Down Expand Up @@ -152,3 +153,9 @@ extension DemoChatViewController: MessagesSelectorDelegate {
self.enqueueModelUpdate(updateType: .normal)
}
}

extension CompoundBubbleLayoutProvider.Dimensions {
static var defaultDimensions: CompoundBubbleLayoutProvider.Dimensions {
return .init(spacing: 8, contentInsets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8))
}
}