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

Externalize creation of Chat collection view layout models #741

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Chatto/Chatto.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
A410D4BD26C570B100A48342 /* ChatMessagesViewControllerHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A410D4BC26C570B100A48342 /* ChatMessagesViewControllerHelpers.swift */; };
A42C66DE273BC387006B032A /* TimingChatInputBarAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42C66DD273BC387006B032A /* TimingChatInputBarAnimation.swift */; };
A42C66E0273BC3B1006B032A /* SpringChatInputBarAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42C66DF273BC3B1006B032A /* SpringChatInputBarAnimation.swift */; };
A441C27C2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A441C27B2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift */; };
A441C27E2787446C00988636 /* ChatCollectionItemsDiffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A441C27D2787446C00988636 /* ChatCollectionItemsDiffer.swift */; };
A452A8C12705DAED00DCC8D5 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A452A8BF2705DAED00DCC8D5 /* Observable.swift */; };
A4724CDB27355829006B9562 /* ChatPanGestureRecogniserHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4724CDA27355829006B9562 /* ChatPanGestureRecogniserHandler.swift */; };
A4C03CFE26CE431D00360526 /* ChatInputBarPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C03CFD26CE431D00360526 /* ChatInputBarPresenterProtocol.swift */; };
Expand Down Expand Up @@ -69,6 +71,8 @@
A410D4BC26C570B100A48342 /* ChatMessagesViewControllerHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesViewControllerHelpers.swift; sourceTree = "<group>"; };
A42C66DD273BC387006B032A /* TimingChatInputBarAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingChatInputBarAnimation.swift; sourceTree = "<group>"; };
A42C66DF273BC3B1006B032A /* SpringChatInputBarAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringChatInputBarAnimation.swift; sourceTree = "<group>"; };
A441C27B2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewLayoutModelFactory.swift; sourceTree = "<group>"; };
A441C27D2787446C00988636 /* ChatCollectionItemsDiffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionItemsDiffer.swift; sourceTree = "<group>"; };
A452A8BF2705DAED00DCC8D5 /* Observable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
A4724CDA27355829006B9562 /* ChatPanGestureRecogniserHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanGestureRecogniserHandler.swift; sourceTree = "<group>"; };
A4C03CFD26CE431D00360526 /* ChatInputBarPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputBarPresenterProtocol.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -224,6 +228,8 @@
A4F7666226BD417600008F24 /* ChatMessagesViewController.swift */,
A4F7666526BD4FA400008F24 /* ChatMessageCollectionAdapter.swift */,
A4F7666726BD59EF00008F24 /* ChatMessagesViewModel.swift */,
A441C27B2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift */,
A441C27D2787446C00988636 /* ChatCollectionItemsDiffer.swift */,
);
path = ChatMessages;
sourceTree = "<group>";
Expand Down Expand Up @@ -440,6 +446,8 @@
A42C66DE273BC387006B032A /* TimingChatInputBarAnimation.swift in Sources */,
C36281E71BF0F196004D6BCE /* UICollectionView+Scrolling.swift in Sources */,
26D653F2251043BD007BC13C /* ReplyFeedbackGenerator.swift in Sources */,
A441C27E2787446C00988636 /* ChatCollectionItemsDiffer.swift in Sources */,
A441C27C2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift in Sources */,
3565429D203DB99300B29DA1 /* ChatLayoutConfiguration.swift in Sources */,
C36281EB1BF0F62F004D6BCE /* DummyChatItemPresenter.swift in Sources */,
C3C7C3981CAC4BAC00A49929 /* ChatCollectionViewLayout.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// Copyright (c) Bumble, 2021-present. All rights reserved.
//

import CoreGraphics

final class ChatCollectionItemsDiffer {

struct Diff {
let changes: CollectionChanges
let itemCompanionCollection: ChatItemCompanionCollection
let layoutModel: ChatCollectionViewLayoutModel
}

private let chatItemsDecorator: ChatItemsDecoratorProtocol
private let chatItemPresenterFactory: ChatItemPresenterFactoryProtocol
private let chatCollectionViewLayoutModelFactory: ChatCollectionViewLayoutModelFactoryProtocol

init(
chatItemsDecorator: ChatItemsDecoratorProtocol,
chatItemPresenterFactory: ChatItemPresenterFactoryProtocol,
chatCollectionViewLayoutModelFactory: ChatCollectionViewLayoutModelFactoryProtocol
) {
self.chatItemsDecorator = chatItemsDecorator
self.chatItemPresenterFactory = chatItemPresenterFactory
self.chatCollectionViewLayoutModelFactory = chatCollectionViewLayoutModelFactory
}

func calculateChanges(
newItems: [ChatItemProtocol],
oldItems: ChatItemCompanionCollection,
collectionViewWidth: CGFloat
) -> Diff {

let newDecoratedItems = self.chatItemsDecorator.decorateItems(newItems)
let changes = generateChanges(
oldCollection: oldItems.map(HashableItem.init),
newCollection: newDecoratedItems.map(HashableItem.init)
)
let itemCompanionCollection = self.createCompanionCollection(fromChatItems: newDecoratedItems, previousCompanionCollection: oldItems)
let layoutModel = self.chatCollectionViewLayoutModelFactory.createLayoutModel(itemCompanionCollection, collectionViewWidth: collectionViewWidth)

return Diff(
changes: changes,
itemCompanionCollection: itemCompanionCollection,
layoutModel: layoutModel
)
}

private func createCompanionCollection(fromChatItems newItems: [DecoratedChatItem], previousCompanionCollection oldItems: ChatItemCompanionCollection) -> ChatItemCompanionCollection {
return ChatItemCompanionCollection(items: newItems.map { (decoratedChatItem) -> ChatItemCompanion in

/*
We use an assumption, that message having a specific messageId never changes its type.
If such changes has to be supported, then generation of changes has to suppport reloading items.
Otherwise, updateVisibleCells may try to update the existing cells with new presenters which aren't able to work with another types.
*/

let presenter: ChatItemPresenterProtocol = {
guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] ?? oldItems[decoratedChatItem.chatItem.uid],
oldChatItemCompanion.chatItem.type == decoratedChatItem.chatItem.type,
oldChatItemCompanion.presenter.isItemUpdateSupported else {
return self.chatItemPresenterFactory.createChatItemPresenter(decoratedChatItem.chatItem)
}

oldChatItemCompanion.presenter.update(with: decoratedChatItem.chatItem)
return oldChatItemCompanion.presenter
}()

return ChatItemCompanion(uid: decoratedChatItem.uid, chatItem: decoratedChatItem.chatItem, presenter: presenter, decorationAttributes: decoratedChatItem.decorationAttributes)
})
}
}

private struct HashableItem: Hashable {
private let uid: String
private let type: String

init(_ decoratedChatItem: DecoratedChatItem) {
self.uid = decoratedChatItem.uid
self.type = decoratedChatItem.chatItem.type
}

init(_ chatItemCompanion: ChatItemCompanion) {
self.uid = chatItemCompanion.uid
self.type = chatItemCompanion.chatItem.type
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright (c) Bumble, 2021-present. All rights reserved.
//

import CoreGraphics
import Foundation

public protocol ChatCollectionViewLayoutModelFactoryProtocol {
func createLayoutModel(_ items: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> ChatCollectionViewLayoutModel
}

public final class ChatCollectionViewLayoutModelFactory: ChatCollectionViewLayoutModelFactoryProtocol {

public init() { }

public func createLayoutModel(_ items: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> ChatCollectionViewLayoutModel {
// swiftlint:disable:next nesting
typealias IntermediateItemLayoutData = (height: CGFloat?, bottomMargin: CGFloat)
typealias ItemLayoutData = (height: CGFloat, bottomMargin: CGFloat)
// swiftlint:disable:previous nesting

func createLayoutModel(intermediateLayoutData: [IntermediateItemLayoutData]) -> ChatCollectionViewLayoutModel {
let layoutData = intermediateLayoutData.map { (intermediateLayoutData: IntermediateItemLayoutData) -> ItemLayoutData in
return (height: intermediateLayoutData.height!, bottomMargin: intermediateLayoutData.bottomMargin)
}
return ChatCollectionViewLayoutModel.createModel(collectionViewWidth, itemsLayoutData: layoutData)
}

let isInBackground = !Thread.isMainThread
var intermediateLayoutData = [IntermediateItemLayoutData]()
var itemsForMainThread = [(index: Int, itemCompanion: ChatItemCompanion)]()

for (index, itemCompanion) in items.enumerated() {
var height: CGFloat?
let bottomMargin: CGFloat = itemCompanion.decorationAttributes?.bottomMargin ?? 0
if !isInBackground || itemCompanion.presenter.canCalculateHeightInBackground {
height = itemCompanion.presenter.heightForCell(maximumWidth: collectionViewWidth, decorationAttributes: itemCompanion.decorationAttributes)
} else {
itemsForMainThread.append((index: index, itemCompanion: itemCompanion))
}
intermediateLayoutData.append((height: height, bottomMargin: bottomMargin))
}

if itemsForMainThread.count > 0 {
DispatchQueue.main.sync {
for (index, itemCompanion) in itemsForMainThread {
let height = itemCompanion.presenter.heightForCell(
maximumWidth: collectionViewWidth,
decorationAttributes: itemCompanion.decorationAttributes
)
intermediateLayoutData[index].height = height
}
}
}
return createLayoutModel(intermediateLayoutData: intermediateLayoutData)
}
}
Loading