From 415c1219f6bca24e16545befdb801e14c5939523 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Mon, 29 Jul 2019 17:09:56 +0100 Subject: [PATCH 01/18] ChatItemProtocol is extended with "func isEqual(to otherItem: ChatItemProtocol) -> Bool" --- .../Chat Items/ChatItemProtocolDefinitions.swift | 1 + .../BaseChatViewControllerTestHelpers.swift | 3 +++ .../Chat Items/BaseMessage/BaseMessageModel.swift | 4 ++++ .../Chat Items/PhotoMessages/PhotoMessageModel.swift | 6 +++++- .../Chat Items/TextMessages/TextMessageModel.swift | 4 ++++ .../Compound Messages/DemoCompoundMessageModel.swift | 12 ++++++++---- .../Sending status/SendingStatusPresenter.swift | 4 ++++ .../Source/Time Separator/TimeSeparatorModel.swift | 4 ++++ 8 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift index 6ea4eb1fe..cd72e30f4 100644 --- a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift +++ b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift @@ -28,6 +28,7 @@ public typealias ChatItemType = String public protocol ChatItemProtocol: AnyObject, UniqueIdentificable { var type: ChatItemType { get } + func isEqual(to otherItem: ChatItemProtocol) -> Bool } public protocol ChatItemDecorationAttributesProtocol { diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift index 199eef34a..204ba25d2 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift @@ -124,6 +124,9 @@ final class FakeChatItem: ChatItemProtocol { self.uid = uid self.type = type } + func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type + } } final class FakeChatItemPresenter: ChatItemPresenterProtocol { diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift index bc8bcf5b2..8091ba725 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift @@ -84,4 +84,8 @@ open class MessageModel: MessageModelProtocol { self.date = date self.status = status } + + public func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type + } } diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift index cf652bb91..d1e6f6240 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift @@ -23,6 +23,7 @@ */ import UIKit +import Chatto public protocol PhotoMessageModelProtocol: DecoratedMessageModelProtocol { var image: UIImage { get } @@ -33,7 +34,7 @@ open class PhotoMessageModel: PhotoMessageM public var messageModel: MessageModelProtocol { return self._messageModel } - public let _messageModel: MessageModelT // Can't make messasgeModel: MessageModelT: https://gist.github.com/diegosanchezr/5a66c7af862e1117b556 + public let _messageModel: MessageModelT // Can't make messageModel: MessageModelT: https://gist.github.com/diegosanchezr/5a66c7af862e1117b556 public let image: UIImage public let imageSize: CGSize public init(messageModel: MessageModelT, imageSize: CGSize, image: UIImage) { @@ -41,4 +42,7 @@ open class PhotoMessageModel: PhotoMessageM self.imageSize = imageSize self.image = image } + public func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type + } } diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift index 4db235b2f..6522c3bed 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift @@ -23,6 +23,7 @@ */ import Foundation +import Chatto public protocol TextMessageModelProtocol: DecoratedMessageModelProtocol { var text: String { get } @@ -38,4 +39,7 @@ open class TextMessageModel: TextMessageMod self._messageModel = messageModel self.text = text } + public func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type + } } diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift b/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift index 5297827dc..53e51f8d4 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift @@ -21,6 +21,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import Chatto import ChattoAdditions final class DemoCompoundMessageModel: Equatable, DecoratedMessageModelProtocol, DemoMessageModelProtocol { @@ -50,9 +51,12 @@ final class DemoCompoundMessageModel: Equatable, DecoratedMessageModelProtocol, // MARK: - Equatable static func == (lhs: DemoCompoundMessageModel, rhs: DemoCompoundMessageModel) -> Bool { - return lhs.text == rhs.text - && lhs.image == rhs.image - && lhs.messageModel.uid == rhs.messageModel.uid - && lhs.status == rhs.status + return lhs.isEqual(to: rhs) + } + + // MARK: - ChatItemProtocol + + func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type } } diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift index bbc7a3405..6dc421c30 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift @@ -42,6 +42,10 @@ class SendingStatusModel: ChatItemProtocol { self.uid = uid self.status = status } + + func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type + } } public class SendingStatusPresenterBuilder: ChatItemPresenterBuilderProtocol { diff --git a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift index 5340180ed..38cc7ee7e 100644 --- a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift +++ b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift @@ -39,6 +39,10 @@ class TimeSeparatorModel: ChatItemProtocol { self.date = date self.uid = uid } + + func isEqual(to otherItem: ChatItemProtocol) -> Bool { + return self.uid == otherItem.uid && self.type == otherItem.type + } } extension Date { From 4b7dc7f8868ca9661e012897f041d81539518409 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Mon, 29 Jul 2019 17:12:40 +0100 Subject: [PATCH 02/18] BaseChatViewController test items for equality with "isEqual(to:)" to reuse item presenters --- .../BaseChatViewController+Changes.swift | 31 ++++++++----- .../BaseChatViewControllerTests.swift | 45 ++++++++++++++++++- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift index 56d551f3b..3a45d3322 100644 --- a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift +++ b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift @@ -301,18 +301,25 @@ extension BaseChatViewController { private func createCompanionCollection(fromChatItems newItems: [DecoratedChatItem], previousCompanionCollection oldItems: ChatItemCompanionCollection) -> ChatItemCompanionCollection { return ChatItemCompanionCollection(items: newItems.map { (decoratedChatItem) -> ChatItemCompanion in - let chatItem = decoratedChatItem.chatItem - var presenter: ChatItemPresenterProtocol! - // We assume that a same messageId can't mutate from one cell class to a different one. - // If we ever need to support that then generation of changes needs to suppport reloading items. - // Oherwise updateVisibleCells may try to update existing cell with a new presenter which is working with a different type of cell - - // Optimization: reuse presenter if it's the same instance. - if let oldChatItemCompanion = oldItems[decoratedChatItem.uid], oldChatItemCompanion.chatItem === chatItem { - presenter = oldChatItemCompanion.presenter - } else { - presenter = self.createPresenterForChatItem(decoratedChatItem.chatItem) - } + + /* + 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] else { + return self.createPresenterForChatItem(decoratedChatItem.chatItem) + } + + guard oldChatItemCompanion.chatItem.isEqual(to: decoratedChatItem.chatItem) else { + return self.createPresenterForChatItem(decoratedChatItem.chatItem) + } + + return oldChatItemCompanion.presenter + }() + return ChatItemCompanion(uid: decoratedChatItem.uid, chatItem: decoratedChatItem.chatItem, presenter: presenter, decorationAttributes: decoratedChatItem.decorationAttributes) }) } diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift index 1b69cb204..cac6364ff 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift @@ -268,10 +268,53 @@ class ChatViewControllerTests: XCTestCase { // MARK: helpers - private func fakeDidAppearAndLayout(controller: TesteableChatViewController) { + fileprivate func fakeDidAppearAndLayout(controller: TesteableChatViewController) { controller.view.frame = CGRect(x: 0, y: 0, width: 400, height: 900) controller.viewWillAppear(true) controller.viewDidAppear(true) controller.view.layoutIfNeeded() } } + +extension ChatViewControllerTests { + + func testThat_WhenDataSourceIsUpdatedWithOneNewItem_ThenOneNewItemPresenterIsCreated() { + let presenterBuilder = FakePresenterBuilder() + let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) + let fakeDataSource = FakeDataSource() + fakeDataSource.chatItems = createFakeChatItems(count: 2) + controller.chatDataSource = fakeDataSource + self.fakeDidAppearAndLayout(controller: controller) + XCTAssertEqual(presenterBuilder.presentersCreatedCount, 2) + + fakeDataSource.chatItems = createFakeChatItems(count: 3) + let asyncExpectation = expectation(description: "update") + controller.enqueueModelUpdate(updateType: .normal) { + asyncExpectation.fulfill() + } + + self.waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(presenterBuilder.presentersCreatedCount, 3) + } + } + + func testThat_WhenDataSourceIsUpdatedWithTheSameItems_ThenNoNewItemPresentersAreCreated() { + let presenterBuilder = FakePresenterBuilder() + let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) + let fakeDataSource = FakeDataSource() + fakeDataSource.chatItems = createFakeChatItems(count: 2) + controller.chatDataSource = fakeDataSource + self.fakeDidAppearAndLayout(controller: controller) + XCTAssertEqual(presenterBuilder.presentersCreatedCount, 2) + + fakeDataSource.chatItems = createFakeChatItems(count: 2) + let asyncExpectation = expectation(description: "update") + controller.enqueueModelUpdate(updateType: .normal) { + asyncExpectation.fulfill() + } + + self.waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(presenterBuilder.presentersCreatedCount, 2) + } + } +} From e4de8db49b1d3b3680553e8312598993694bb9ff Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Mon, 29 Jul 2019 17:13:29 +0100 Subject: [PATCH 03/18] Test example in ChattoApp for items reloading and presenters reusage --- ChattoApp/ChattoApp.xcodeproj/project.pbxproj | 4 ++ .../TestItemsReloadingViewController.swift | 57 +++++++++++++++++++ .../Source/ChatExamplesViewController.swift | 9 ++- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift diff --git a/ChattoApp/ChattoApp.xcodeproj/project.pbxproj b/ChattoApp/ChattoApp.xcodeproj/project.pbxproj index 6bbfdac01..5da9e9e71 100644 --- a/ChattoApp/ChattoApp.xcodeproj/project.pbxproj +++ b/ChattoApp/ChattoApp.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ DD49AA1095C478DD42524839 /* Pods_ChattoApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13A796C853501DB82BA5DC27 /* Pods_ChattoApp.framework */; }; DECD501A21AEEAF100988729 /* ContentAwareInputItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DECD501921AEEAF100988729 /* ContentAwareInputItem.swift */; }; DECD501C21AEEC5A00988729 /* CustomInput.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DECD501B21AEEC5A00988729 /* CustomInput.xcassets */; }; + EAE6345722EB0CAD0053E21A /* TestItemsReloadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE6345622EB0CAD0053E21A /* TestItemsReloadingViewController.swift */; }; FE2D050B1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */; }; /* End PBXBuildFile section */ @@ -119,6 +120,7 @@ C3F91DCB1C75EFE300D461D2 /* SendingStatusPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendingStatusPresenter.swift; sourceTree = ""; }; DECD501921AEEAF100988729 /* ContentAwareInputItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentAwareInputItem.swift; sourceTree = ""; }; DECD501B21AEEC5A00988729 /* CustomInput.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = CustomInput.xcassets; sourceTree = ""; }; + EAE6345622EB0CAD0053E21A /* TestItemsReloadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestItemsReloadingViewController.swift; sourceTree = ""; }; FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseMessageCollectionViewCellAvatarStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -198,6 +200,7 @@ C3F91DAB1C75EF9E00D461D2 /* DemoChatMessageSender.swift */, C3F91DA81C75EF9E00D461D2 /* DemoChatViewController.swift */, 26D6D523220CAAE2006232B4 /* UpdateItemTypeViewController.swift */, + EAE6345622EB0CAD0053E21A /* TestItemsReloadingViewController.swift */, ); path = "Chat View Controllers"; sourceTree = ""; @@ -504,6 +507,7 @@ 268CD578220336EB00DEE2C2 /* DemoTextMessageContentFactory.swift in Sources */, C3F91DCC1C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift in Sources */, C3F91DB71C75EF9E00D461D2 /* DemoChatItemsDecorator.swift in Sources */, + EAE6345722EB0CAD0053E21A /* TestItemsReloadingViewController.swift in Sources */, 0997CD2F2042E42100D7BDF9 /* CellsViewController.swift in Sources */, 0997CD312042E58400D7BDF9 /* ChatWithTabBarExamplesViewController.swift in Sources */, 558730341FCD8891005BC2EC /* AddRandomMessageChatViewController.swift in Sources */, diff --git a/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift b/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift new file mode 100644 index 000000000..c0febab9f --- /dev/null +++ b/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift @@ -0,0 +1,57 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 UIKit +import Chatto + +final class TestItemsReloadingViewController: DemoChatViewController { + + // MARK: - UIViewController + + override func viewDidLoad() { + self.dataSource = DemoChatDataSource(messages: self.remakeItems(), pageSize: 50) + super.viewDidLoad() + self.navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "Update", + style: .plain, + target: self, + action: #selector(self.didPressUpdateItemType) + ) + } + + // MARK: - Private methods + + @objc + private func didPressUpdateItemType() { + self.dataSource = DemoChatDataSource(messages: self.remakeItems(), pageSize: 50) + } + + private func remakeItems() -> [DemoTextMessageModel] { + return [ + DemoChatMessageFactory.makeTextMessage("1", text: "Hello", isIncoming: true), + DemoChatMessageFactory.makeTextMessage("2", text: "Hi!", isIncoming: false), + DemoChatMessageFactory.makeTextMessage("3", text: "How are you doing?", isIncoming: true), + DemoChatMessageFactory.makeTextMessage("4", text: "I'm fine, thanks!", isIncoming: false) + ] + } +} diff --git a/ChattoApp/ChattoApp/Source/ChatExamplesViewController.swift b/ChattoApp/ChattoApp/Source/ChatExamplesViewController.swift index 7447c1168..98ce92bdc 100644 --- a/ChattoApp/ChattoApp/Source/ChatExamplesViewController.swift +++ b/ChattoApp/ChattoApp/Source/ChatExamplesViewController.swift @@ -40,7 +40,8 @@ class ChatExamplesViewController: CellsViewController { self.makeOpenWithTabBarCellItem(), self.makeScrollToBottomCellItem(), self.makeCompoundDemoViewController(), - self.makeUpdateItemTypeViewController() + self.makeUpdateItemTypeViewController(), + self.makeTestItemsReloadingCellItem() ] } @@ -117,6 +118,12 @@ class ChatExamplesViewController: CellsViewController { } } + private func makeTestItemsReloadingCellItem() -> CellItem { + return CellItem(title: "Test items reloading") { [unowned self] in + self.navigationController?.pushViewController(TestItemsReloadingViewController(), animated: true) + } + } + @objc private func dismissPresentedController() { self.dismiss(animated: true, completion: nil) From cdbc86bab779f099234f544e8b0d9f2385555d47 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 1 Aug 2019 11:22:09 +0100 Subject: [PATCH 04/18] BaseChatViewController calls update for reused presenters --- .../Chat Items/BaseChatItemPresenter.swift | 9 ++++ .../ChatItemProtocolDefinitions.swift | 4 ++ .../Chat Items/DummyChatItemPresenter.swift | 10 +++- .../BaseChatViewController+Changes.swift | 5 ++ .../BaseChatViewControllerTestHelpers.swift | 24 ++++++++-- .../BaseChatViewControllerTests.swift | 47 +++++++++++++++---- 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/Chatto/Source/Chat Items/BaseChatItemPresenter.swift b/Chatto/Source/Chat Items/BaseChatItemPresenter.swift index a81c0b348..666b48b39 100644 --- a/Chatto/Source/Chat Items/BaseChatItemPresenter.swift +++ b/Chatto/Source/Chat Items/BaseChatItemPresenter.swift @@ -35,6 +35,15 @@ open class BaseChatItemPresenter: ChatItemPresenter public init() {} + open var isItemUpdateSupported: Bool { + assertionFailure("Implement in subclass") + return false + } + + open func update(with chatItem: ChatItemProtocol) { + assertionFailure("Implement in subclass") + } + open class func registerCells(_ collectionView: UICollectionView) { assert(false, "Implement in subclass") } diff --git a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift index cd72e30f4..79ad387e7 100644 --- a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift +++ b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift @@ -43,6 +43,10 @@ public protocol ChatItemMenuPresenterProtocol { public protocol ChatItemPresenterProtocol: AnyObject, ChatItemMenuPresenterProtocol { static func registerCells(_ collectionView: UICollectionView) + + var isItemUpdateSupported: Bool { get } + func update(with chatItem: ChatItemProtocol) + var canCalculateHeightInBackground: Bool { get } // Default is false func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell diff --git a/Chatto/Source/Chat Items/DummyChatItemPresenter.swift b/Chatto/Source/Chat Items/DummyChatItemPresenter.swift index 71463e582..7eaece15a 100644 --- a/Chatto/Source/Chat Items/DummyChatItemPresenter.swift +++ b/Chatto/Source/Chat Items/DummyChatItemPresenter.swift @@ -24,13 +24,21 @@ import Foundation -// Handles messages that aren't supported so they appear as invisible +// Handles messages which aren't supported. So, they appear as invisible. class DummyChatItemPresenter: ChatItemPresenterProtocol { class func registerCells(_ collectionView: UICollectionView) { collectionView.register(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message") } + var isItemUpdateSupported: Bool { + return true + } + + func update(with chatItem: ChatItemProtocol) { + // Does nothing + } + var canCalculateHeightInBackground: Bool { return true } diff --git a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift index 3a45d3322..b052df55c 100644 --- a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift +++ b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift @@ -317,6 +317,11 @@ extension BaseChatViewController { return self.createPresenterForChatItem(decoratedChatItem.chatItem) } + guard oldChatItemCompanion.presenter.isItemUpdateSupported else { + return self.createPresenterForChatItem(decoratedChatItem.chatItem) + } + + oldChatItemCompanion.presenter.update(with: decoratedChatItem.chatItem) return oldChatItemCompanion.presenter }() diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift index 204ba25d2..87586da70 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift @@ -83,14 +83,16 @@ class FakeDataSource: ChatDataSourceProtocol { class FakeCell: UICollectionViewCell {} class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { - var presentersCreatedCount: Int = 0 + private(set) var createdPresenters: [ChatItemPresenterProtocol] = [] + func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool { return chatItem.type == "fake-type" } func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { - self.presentersCreatedCount += 1 - return FakePresenter() + let presenter = FakePresenter() + self.createdPresenters.append(presenter) + return presenter } var presenterType: ChatItemPresenterProtocol.Type { @@ -99,6 +101,20 @@ class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { } class FakePresenter: BaseChatItemPresenter { + + var _isItemUpdateSupportedReturnValue: Bool = false + override var isItemUpdateSupported: Bool { + return self._isItemUpdateSupportedReturnValue + } + + private var _updateWithChatItemCalls: [(ChatItemProtocol)] = [] + var _updateWithChatItemIsCalled: Bool { return self._updateWithChatItemCallsCount > 0 } + var _updateWithChatItemCallsCount: Int { return self._updateWithChatItemCalls.count } + var _updateWithChatItemLastCallParams: ChatItemProtocol? { return self._updateWithChatItemCalls.last } + override func update(with chatItem: ChatItemProtocol) { + self._updateWithChatItemCalls.append((chatItem)) + } + override class func registerCells(_ collectionView: UICollectionView) { collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell") } @@ -131,6 +147,8 @@ final class FakeChatItem: ChatItemProtocol { final class FakeChatItemPresenter: ChatItemPresenterProtocol { init() {} + var isItemUpdateSupported: Bool { return false } + func update(with chatItem: ChatItemProtocol) {} static func registerCells(_ collectionView: UICollectionView) {} func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { return 0 } func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return UICollectionViewCell() } diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift index cac6364ff..3b200b807 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift @@ -67,7 +67,7 @@ class ChatViewControllerTests: XCTestCase { fakeDataSource.chatItems = createFakeChatItems(count: 2) controller.chatDataSource = fakeDataSource self.fakeDidAppearAndLayout(controller: controller) - XCTAssertEqual(2, presenterBuilder.presentersCreatedCount) + XCTAssertEqual(2, presenterBuilder.createdPresenters.count) } func testThat_WhenDataSourceChanges_ThenCollectionViewUpdatesAsynchronously() { @@ -285,7 +285,8 @@ extension ChatViewControllerTests { fakeDataSource.chatItems = createFakeChatItems(count: 2) controller.chatDataSource = fakeDataSource self.fakeDidAppearAndLayout(controller: controller) - XCTAssertEqual(presenterBuilder.presentersCreatedCount, 2) + presenterBuilder.createdPresenters.forEach { ($0 as! FakePresenter)._isItemUpdateSupportedReturnValue = true } + let numberOfPresentersBeforeUpdate = presenterBuilder.createdPresenters.count fakeDataSource.chatItems = createFakeChatItems(count: 3) let asyncExpectation = expectation(description: "update") @@ -294,27 +295,57 @@ extension ChatViewControllerTests { } self.waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(presenterBuilder.presentersCreatedCount, 3) + let numberOfPresenterAfterUpdate = presenterBuilder.createdPresenters.count + XCTAssertEqual(numberOfPresenterAfterUpdate - numberOfPresentersBeforeUpdate, 1) } } - func testThat_WhenDataSourceIsUpdatedWithTheSameItems_ThenNoNewItemPresentersAreCreated() { + func testThat_GivenPresenterSupportsUpdates_WhenDataSourceIsUpdatedWithTheSameItem_ThendNewPresenterIsNotCreated_AndPresenterIsUpdatedOnce() { let presenterBuilder = FakePresenterBuilder() let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let fakeDataSource = FakeDataSource() - fakeDataSource.chatItems = createFakeChatItems(count: 2) + fakeDataSource.chatItems = createFakeChatItems(count: 1) controller.chatDataSource = fakeDataSource self.fakeDidAppearAndLayout(controller: controller) - XCTAssertEqual(presenterBuilder.presentersCreatedCount, 2) + let presenter = presenterBuilder.createdPresenters.last! as! FakePresenter + presenter._isItemUpdateSupportedReturnValue = true + XCTAssertEqual(presenter._updateWithChatItemCallsCount, 0) + XCTAssertEqual(presenterBuilder.createdPresenters.count, 1) - fakeDataSource.chatItems = createFakeChatItems(count: 2) + fakeDataSource.chatItems = createFakeChatItems(count: 1) let asyncExpectation = expectation(description: "update") controller.enqueueModelUpdate(updateType: .normal) { asyncExpectation.fulfill() } self.waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(presenterBuilder.presentersCreatedCount, 2) + XCTAssertEqual(presenterBuilder.createdPresenters.count, 1) + XCTAssertEqual(presenter._updateWithChatItemCallsCount, 1) + XCTAssert(presenter._updateWithChatItemLastCallParams! === fakeDataSource.chatItems.first!) } } + + func testThat_GivenPresenterDoesntSupportUpdates_WhenDataSourceIsUpdatedWithTheSameItem_ThenPresenterIsNotUpdated_AndNewPresenterIsCreated() { + let presenterBuilder = FakePresenterBuilder() + let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) + let fakeDataSource = FakeDataSource() + fakeDataSource.chatItems = createFakeChatItems(count: 1) + controller.chatDataSource = fakeDataSource + self.fakeDidAppearAndLayout(controller: controller) + let presenter = presenterBuilder.createdPresenters.last! as! FakePresenter + presenter._isItemUpdateSupportedReturnValue = false + XCTAssertEqual(presenter._updateWithChatItemCallsCount, 0) + XCTAssertEqual(presenterBuilder.createdPresenters.count, 1) + + fakeDataSource.chatItems = createFakeChatItems(count: 1) + let asyncExpectation = expectation(description: "update") + controller.enqueueModelUpdate(updateType: .normal) { + asyncExpectation.fulfill() + } + + self.waitForExpectations(timeout: 1) { _ in + XCTAssertFalse(presenter._updateWithChatItemIsCalled) + XCTAssertEqual(presenterBuilder.createdPresenters.count, 2) + } + } } From 19808ed22483e58415434fc6070c79f9f33a27df Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 1 Aug 2019 11:22:50 +0100 Subject: [PATCH 05/18] Text, photo and compound bubble presenters support updating --- .../BaseMessage/BaseMessagePresenter.swift | 8 ++-- .../CompoundMessagePresenter.swift | 40 ++++++++++++++----- .../PhotoMessages/PhotoMessagePresenter.swift | 10 +++++ .../TextMessages/TextMessagePresenter.swift | 9 +++++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift index f898edab8..5a500c96d 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift @@ -68,15 +68,15 @@ open class BaseMessagePresenter public let viewModelBuilder: ViewModelBuilderT public let interactionHandler: InteractionHandlerT? public let cellStyle: BaseMessageCollectionViewCellStyleProtocol - public private(set) final lazy var messageViewModel: ViewModelT = { - return self.createViewModel() - }() + public private(set) final lazy var messageViewModel: ViewModelT = self.createViewModel() open func createViewModel() -> ViewModelT { let viewModel = self.viewModelBuilder.createViewModel(self.messageModel) diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index 56ff79cfa..d84605fcb 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -35,14 +35,15 @@ open class CompoundMessagePresenter public typealias ViewModelT = ViewModelBuilderT.ViewModelT public let compoundCellStyle: CompoundBubbleViewStyleProtocol - private let contentFactories: [AnyMessageContentFactory] - private let compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions + private let compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions private let cache: Cache private let accessibilityIdentifier: String? - private let menuPresenter: ChatItemMenuPresenterProtocol? - private var contentPresenters: [MessageContentPresenterProtocol] = [] + private let initialContentFactories: [AnyMessageContentFactory] + private var contentFactories: [AnyMessageContentFactory]! + private var contentPresenters: [MessageContentPresenterProtocol]! + private var menuPresenter: ChatItemMenuPresenterProtocol? public init( messageModel: ModelT, @@ -58,10 +59,9 @@ open class CompoundMessagePresenter ) { self.compoundCellStyle = compoundCellStyle self.compoundCellDimensions = compoundCellDimensions - self.contentFactories = contentFactories.filter { $0.canCreateMessageContent(forModel: messageModel) } + self.initialContentFactories = contentFactories self.cache = cache self.accessibilityIdentifier = accessibilityIdentifier - self.menuPresenter = self.contentFactories.lazy.compactMap { $0.createMenuPresenter(forModel: messageModel) }.first super.init( messageModel: messageModel, viewModelBuilder: viewModelBuilder, @@ -69,11 +69,7 @@ open class CompoundMessagePresenter sizingCell: sizingCell, cellStyle: baseCellStyle ) - self.contentPresenters = self.contentFactories.map { factory in - var presenter = factory.createContentPresenter(forModel: self.messageModel) - presenter.delegate = self - return presenter - } + self.updateContent() } open override var canCalculateHeightInBackground: Bool { @@ -84,6 +80,28 @@ open class CompoundMessagePresenter // Cell registration is happening lazily, right before the moment when a cell is dequeued. } + open override var isItemUpdateSupported: Bool { + return true + } + + open override func update(with chatItem: ChatItemProtocol) { + guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } + self.messageModel = newMessageModel + self.updateContent() + } + + private func updateContent() { + self.contentFactories = self.initialContentFactories.filter { $0.canCreateMessageContent(forModel: self.messageModel) } + + self.contentPresenters = self.contentFactories.compactMap { + var presenter = $0.createContentPresenter(forModel: self.messageModel) + presenter.delegate = self + return presenter + } + + self.menuPresenter = self.contentFactories.lazy.compactMap { $0.createMenuPresenter(forModel: self.messageModel) }.first + } + open override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cellReuseIdentifier = self.compoundCellReuseId collectionView.register(CompoundMessageCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier) diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift index 30f521bb0..1b8575abf 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift @@ -23,6 +23,7 @@ */ import Foundation +import Chatto open class PhotoMessagePresenter : BaseMessagePresenter where @@ -56,6 +57,15 @@ open class PhotoMessagePresenter collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message") } + open override var isItemUpdateSupported: Bool { + return true + } + + open override func update(with chatItem: ChatItemProtocol) { + guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } + self.messageModel = newMessageModel + } + public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell(withReuseIdentifier: "photo-message", for: indexPath) } diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift index 23f83d877..08b36a780 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift @@ -63,6 +63,15 @@ open class TextMessagePresenter collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming") } + open override var isItemUpdateSupported: Bool { + return true + } + + open override func update(with chatItem: ChatItemProtocol) { + guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } + self.messageModel = newMessageModel + } + public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let identifier = self.messageViewModel.isIncoming ? "text-message-incoming" : "text-message-outcoming" return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) From 158b8c77d54703bb14dccee6263dcd04a19357ec Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 1 Aug 2019 11:23:20 +0100 Subject: [PATCH 06/18] Test page for updated messages --- .../Sending status/SendingStatusPresenter.swift | 8 ++++++++ .../DemoChatMessageFactory.swift | 8 ++++---- .../TestItemsReloadingViewController.swift | 15 ++++++++++++--- .../Time Separator/TimeSeparatorPresenter.swift | 8 ++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift index 6dc421c30..c095cde3e 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift @@ -73,6 +73,14 @@ class SendingStatusPresenter: ChatItemPresenterProtocol { self.statusModel = statusModel } + var isItemUpdateSupported: Bool { + return false + } + + func update(with chatItem: ChatItemProtocol) { + assertionFailure("SendingStatusPresenter update is not supported yet.") + } + static func registerCells(_ collectionView: UICollectionView) { collectionView.register(UINib(nibName: "SendingStatusCollectionViewCell", bundle: Bundle(for: self)), forCellWithReuseIdentifier: "SendingStatusCollectionViewCell") } diff --git a/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageFactory.swift b/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageFactory.swift index f2cfce609..e82211a76 100644 --- a/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageFactory.swift +++ b/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageFactory.swift @@ -55,12 +55,12 @@ class DemoChatMessageFactory { return photoMessageModel } - static func makeCompoundMessage(isIncoming: Bool) -> DemoCompoundMessageModel { - let messageModel = self.makeMessageModel(UUID().uuidString, + static func makeCompoundMessage(uid: String = UUID().uuidString, text: String? = nil, imageName: String? = nil, isIncoming: Bool) -> DemoCompoundMessageModel { + let messageModel = self.makeMessageModel(uid, isIncoming: isIncoming, type: .compoundItemType) - let text = isIncoming ? "Hello, how are you" : "I'm good, thanks, how about yourself?" - let imageName = isIncoming ? "pic-test-1" : "pic-test-2" + let text = text ?? (isIncoming ? "Hello, how are you" : "I'm good, thanks, how about yourself?") + let imageName = imageName ?? (isIncoming ? "pic-test-1" : "pic-test-2") let image = UIImage(named: imageName)! return DemoCompoundMessageModel(text: text, image: image, diff --git a/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift b/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift index c0febab9f..055afb8a0 100644 --- a/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift +++ b/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift @@ -46,12 +46,21 @@ final class TestItemsReloadingViewController: DemoChatViewController { self.dataSource = DemoChatDataSource(messages: self.remakeItems(), pageSize: 50) } - private func remakeItems() -> [DemoTextMessageModel] { + private func remakeItems() -> [ChatItemProtocol] { + let randomGoodAnswer = ["Nice!", "Great!", "Amazing!", "Brilliant!", "Another very very long answer to test how cell resizing works."].randomElement()! + let randomImageName = "pic-test-\((1...3).randomElement()!)" + let randomImage = UIImage(named: randomImageName)! + let randomImageSize = CGSize(width: [300, 400].randomElement()!, height: [300, 400].randomElement()!) + return [ DemoChatMessageFactory.makeTextMessage("1", text: "Hello", isIncoming: true), DemoChatMessageFactory.makeTextMessage("2", text: "Hi!", isIncoming: false), DemoChatMessageFactory.makeTextMessage("3", text: "How are you doing?", isIncoming: true), - DemoChatMessageFactory.makeTextMessage("4", text: "I'm fine, thanks!", isIncoming: false) - ] + DemoChatMessageFactory.makeTextMessage("4", text: "I'm fine, thanks!", isIncoming: false), + DemoChatMessageFactory.makeTextMessage("5", text: randomGoodAnswer, isIncoming: true), + DemoChatMessageFactory.makePhotoMessage("6", image: randomImage, size: randomImageSize, isIncoming: false), + DemoChatMessageFactory.makeTextMessage("7", text: "Cool, bye!", isIncoming: true), + DemoChatMessageFactory.makeCompoundMessage(uid: "8", text: randomGoodAnswer, imageName: randomImageName, isIncoming: false) + ].reversed() } } diff --git a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift index 8ac49ebc1..97e689660 100644 --- a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift @@ -44,6 +44,14 @@ public class TimeSeparatorPresenterBuilder: ChatItemPresenterBuilderProtocol { class TimeSeparatorPresenter: ChatItemPresenterProtocol { + var isItemUpdateSupported: Bool { + return false + } + + func update(with chatItem: ChatItemProtocol) { + assertionFailure("TimeSeparatorPresenter update is not supported yet.") + } + let timeSeparatorModel: TimeSeparatorModel init (timeSeparatorModel: TimeSeparatorModel) { self.timeSeparatorModel = timeSeparatorModel From 2179ea813851617089715c5c78c61df4c6c7d098 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Fri, 2 Aug 2019 12:59:09 +0100 Subject: [PATCH 07/18] UpdateType is made CaseIterable --- .../ChatController/Collaborators/ChatDataSourceProtocol.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chatto/Source/ChatController/Collaborators/ChatDataSourceProtocol.swift b/Chatto/Source/ChatController/Collaborators/ChatDataSourceProtocol.swift index 3e3fc4a7e..74e4a2b48 100644 --- a/Chatto/Source/ChatController/Collaborators/ChatDataSourceProtocol.swift +++ b/Chatto/Source/ChatController/Collaborators/ChatDataSourceProtocol.swift @@ -24,7 +24,7 @@ import Foundation -public enum UpdateType { +public enum UpdateType: CaseIterable { case normal case firstLoad case pagination From 8ab0b79cdcd14605d6dc628a419f15432a5faeec Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Fri, 2 Aug 2019 13:01:20 +0100 Subject: [PATCH 08/18] There is no unbind/bind calls if the cell stays the same --- .../CompoundMessage/CompoundMessagePresenter.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index d84605fcb..46e667511 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -45,6 +45,8 @@ open class CompoundMessagePresenter private var contentPresenters: [MessageContentPresenterProtocol]! private var menuPresenter: ChatItemMenuPresenterProtocol? + private weak var compoundCell: CompoundMessageCollectionViewCell? + public init( messageModel: ModelT, viewModelBuilder: ViewModelBuilderT, @@ -148,12 +150,16 @@ open class CompoundMessagePresenter 3. CompoundCell's views bound with a current compound message presenters. */ + guard sSelf.compoundCell != compoundCell else { return } + sSelf.contentPresenters.forEach { $0.unbindFromView() } compoundCell.viewReferences = zip(sSelf.contentPresenters, bubbleView.decoratedContentViews!.map({ $0.view })).map { presenter, view in let viewReference = ViewReference(to: view) presenter.bindToView(with: viewReference) return viewReference } + + sSelf.compoundCell = compoundCell } } From d2f0f6feb5d7c06e9fb35f92d7b48345d01b2936 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Fri, 2 Aug 2019 16:39:45 +0100 Subject: [PATCH 09/18] Content is updated only when it's needed --- .../CompoundMessage/CompoundMessagePresenter.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index 46e667511..42298e663 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -45,8 +45,6 @@ open class CompoundMessagePresenter private var contentPresenters: [MessageContentPresenterProtocol]! private var menuPresenter: ChatItemMenuPresenterProtocol? - private weak var compoundCell: CompoundMessageCollectionViewCell? - public init( messageModel: ModelT, viewModelBuilder: ViewModelBuilderT, @@ -88,8 +86,9 @@ open class CompoundMessagePresenter open override func update(with chatItem: ChatItemProtocol) { guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } + let isUpdateNeeded = !self.messageModel.isEqual(to: newMessageModel) self.messageModel = newMessageModel - self.updateContent() + if isUpdateNeeded { self.updateContent() } } private func updateContent() { @@ -150,16 +149,12 @@ open class CompoundMessagePresenter 3. CompoundCell's views bound with a current compound message presenters. */ - guard sSelf.compoundCell != compoundCell else { return } - sSelf.contentPresenters.forEach { $0.unbindFromView() } compoundCell.viewReferences = zip(sSelf.contentPresenters, bubbleView.decoratedContentViews!.map({ $0.view })).map { presenter, view in let viewReference = ViewReference(to: view) presenter.bindToView(with: viewReference) return viewReference } - - sSelf.compoundCell = compoundCell } } From b5b063c0546c3f73b145f5fad1da932509d05f8d Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Tue, 6 Aug 2019 17:52:42 +0100 Subject: [PATCH 10/18] Switching from isEqual to hasSameContent --- .../Chat Items/ChatItemProtocolDefinitions.swift | 2 +- .../BaseChatViewController+Changes.swift | 4 ++-- .../BaseChatViewControllerTestHelpers.swift | 4 ++-- .../Chat Items/BaseMessage/BaseMessageModel.swift | 5 +++-- .../CompoundMessage/CompoundMessagePresenter.swift | 2 +- .../Chat Items/PhotoMessages/PhotoMessageModel.swift | 6 ++++-- .../Chat Items/TextMessages/TextMessageModel.swift | 5 +++-- .../Compound Messages/DemoCompoundMessageModel.swift | 10 +++++++--- .../Sending status/SendingStatusPresenter.swift | 5 +++-- .../TestItemsReloadingViewController.swift | 3 +-- .../Source/Time Separator/TimeSeparatorModel.swift | 5 +++-- 11 files changed, 30 insertions(+), 21 deletions(-) diff --git a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift index 79ad387e7..f21f16d9d 100644 --- a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift +++ b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift @@ -28,7 +28,7 @@ public typealias ChatItemType = String public protocol ChatItemProtocol: AnyObject, UniqueIdentificable { var type: ChatItemType { get } - func isEqual(to otherItem: ChatItemProtocol) -> Bool + func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool } public protocol ChatItemDecorationAttributesProtocol { diff --git a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift index b052df55c..f9f3148e5 100644 --- a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift +++ b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift @@ -309,11 +309,11 @@ extension BaseChatViewController { */ let presenter: ChatItemPresenterProtocol = { - guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] else { + guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] ?? oldItems[decoratedChatItem.chatItem.uid] else { return self.createPresenterForChatItem(decoratedChatItem.chatItem) } - guard oldChatItemCompanion.chatItem.isEqual(to: decoratedChatItem.chatItem) else { + guard oldChatItemCompanion.chatItem.type == decoratedChatItem.chatItem.type else { return self.createPresenterForChatItem(decoratedChatItem.chatItem) } diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift index 87586da70..0fb467fea 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift @@ -140,8 +140,8 @@ final class FakeChatItem: ChatItemProtocol { self.uid = uid self.type = type } - func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + return self.type == anotherItem.type } } diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift index 8091ba725..f6d4448d2 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift @@ -85,7 +85,8 @@ open class MessageModel: MessageModelProtocol { self.status = status } - public func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + public func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + assertionFailure("Should be implemented in a subclass.") + return false } } diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index 42298e663..d5a69d59f 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -86,7 +86,7 @@ open class CompoundMessagePresenter open override func update(with chatItem: ChatItemProtocol) { guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } - let isUpdateNeeded = !self.messageModel.isEqual(to: newMessageModel) + let isUpdateNeeded = !self.messageModel.hasSameContent(as: newMessageModel) self.messageModel = newMessageModel if isUpdateNeeded { self.updateContent() } } diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift index d1e6f6240..8e6123afb 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift @@ -42,7 +42,9 @@ open class PhotoMessageModel: PhotoMessageM self.imageSize = imageSize self.image = image } - public func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + public func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + guard let anotherMessageModel = anotherItem as? PhotoMessageModel else { return false } + return self.image == anotherMessageModel.image + && self.imageSize == anotherMessageModel.imageSize } } diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift index 6522c3bed..f30ead415 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift @@ -39,7 +39,8 @@ open class TextMessageModel: TextMessageMod self._messageModel = messageModel self.text = text } - public func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + public func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + guard let anotherMessageModel = anotherItem as? TextMessageModel else { return false } + return self.text == anotherMessageModel.text } } diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift b/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift index 53e51f8d4..9fd4336eb 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Compound Messages/DemoCompoundMessageModel.swift @@ -51,12 +51,16 @@ final class DemoCompoundMessageModel: Equatable, DecoratedMessageModelProtocol, // MARK: - Equatable static func == (lhs: DemoCompoundMessageModel, rhs: DemoCompoundMessageModel) -> Bool { - return lhs.isEqual(to: rhs) + return lhs.messageModel.uid == rhs.messageModel.uid + && lhs.status == rhs.status + && lhs.hasSameContent(as: rhs) } // MARK: - ChatItemProtocol - func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + guard let anotherModel = anotherItem as? DemoCompoundMessageModel else { return false } + return self.text == anotherModel.text + && self.image == anotherModel.image } } diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift index c095cde3e..a58ec2e63 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift @@ -43,8 +43,9 @@ class SendingStatusModel: ChatItemProtocol { self.status = status } - func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + guard let anotherModel = anotherItem as? SendingStatusModel else { return false } + return self.status == anotherModel.status } } diff --git a/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift b/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift index 055afb8a0..5e22d4436 100644 --- a/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift +++ b/ChattoApp/ChattoApp/Source/Chat View Controllers/TestItemsReloadingViewController.swift @@ -50,7 +50,6 @@ final class TestItemsReloadingViewController: DemoChatViewController { let randomGoodAnswer = ["Nice!", "Great!", "Amazing!", "Brilliant!", "Another very very long answer to test how cell resizing works."].randomElement()! let randomImageName = "pic-test-\((1...3).randomElement()!)" let randomImage = UIImage(named: randomImageName)! - let randomImageSize = CGSize(width: [300, 400].randomElement()!, height: [300, 400].randomElement()!) return [ DemoChatMessageFactory.makeTextMessage("1", text: "Hello", isIncoming: true), @@ -58,7 +57,7 @@ final class TestItemsReloadingViewController: DemoChatViewController { DemoChatMessageFactory.makeTextMessage("3", text: "How are you doing?", isIncoming: true), DemoChatMessageFactory.makeTextMessage("4", text: "I'm fine, thanks!", isIncoming: false), DemoChatMessageFactory.makeTextMessage("5", text: randomGoodAnswer, isIncoming: true), - DemoChatMessageFactory.makePhotoMessage("6", image: randomImage, size: randomImageSize, isIncoming: false), + DemoChatMessageFactory.makePhotoMessage("6", image: randomImage, size: randomImage.size, isIncoming: false), DemoChatMessageFactory.makeTextMessage("7", text: "Cool, bye!", isIncoming: true), DemoChatMessageFactory.makeCompoundMessage(uid: "8", text: randomGoodAnswer, imageName: randomImageName, isIncoming: false) ].reversed() diff --git a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift index 38cc7ee7e..c76064192 100644 --- a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift +++ b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift @@ -40,8 +40,9 @@ class TimeSeparatorModel: ChatItemProtocol { self.uid = uid } - func isEqual(to otherItem: ChatItemProtocol) -> Bool { - return self.uid == otherItem.uid && self.type == otherItem.type + func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + guard let anotherModel = anotherItem as? TimeSeparatorModel else { return false } + return self.date == anotherModel.date } } From 4acecc5b94b281ac7c88f7b1c67738d621b54f9f Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 8 Aug 2019 15:47:27 +0100 Subject: [PATCH 11/18] UpdatableChatItemPresenterProtocol & ContentEquatableChatItemProtocol --- .../Chat Items/BaseChatItemPresenter.swift | 7 +- .../ChatItemProtocolDefinitions.swift | 14 ++- .../Chat Items/DummyChatItemPresenter.swift | 6 +- .../BaseChatViewControllerTestHelpers.swift | 87 ++++++++++---- .../BaseChatViewControllerTests.swift | 107 +++++++++++------- .../BaseMessage/BaseMessageModel.swift | 5 - .../CompoundMessagePresenter.swift | 10 +- .../PhotoMessages/PhotoMessageModel.swift | 2 +- .../PhotoMessages/PhotoMessagePresenter.swift | 4 - .../TextMessages/TextMessageModel.swift | 2 +- .../TextMessages/TextMessagePresenter.swift | 4 - .../SendingStatusPresenter.swift | 13 --- .../DemoChatMessageSender.swift | 2 +- .../Time Separator/TimeSeparatorModel.swift | 5 - .../TimeSeparatorPresenter.swift | 8 -- 15 files changed, 151 insertions(+), 125 deletions(-) diff --git a/Chatto/Source/Chat Items/BaseChatItemPresenter.swift b/Chatto/Source/Chat Items/BaseChatItemPresenter.swift index 666b48b39..053bdf0a7 100644 --- a/Chatto/Source/Chat Items/BaseChatItemPresenter.swift +++ b/Chatto/Source/Chat Items/BaseChatItemPresenter.swift @@ -30,16 +30,11 @@ public enum ChatItemVisibility { case visible } -open class BaseChatItemPresenter: ChatItemPresenterProtocol { +open class BaseChatItemPresenter: UpdatableChatItemPresenterProtocol { public final weak var cell: CellT? public init() {} - open var isItemUpdateSupported: Bool { - assertionFailure("Implement in subclass") - return false - } - open func update(with chatItem: ChatItemProtocol) { assertionFailure("Implement in subclass") } diff --git a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift index f21f16d9d..48c84cb6e 100644 --- a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift +++ b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift @@ -28,7 +28,6 @@ public typealias ChatItemType = String public protocol ChatItemProtocol: AnyObject, UniqueIdentificable { var type: ChatItemType { get } - func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool } public protocol ChatItemDecorationAttributesProtocol { @@ -44,9 +43,6 @@ public protocol ChatItemMenuPresenterProtocol { public protocol ChatItemPresenterProtocol: AnyObject, ChatItemMenuPresenterProtocol { static func registerCells(_ collectionView: UICollectionView) - var isItemUpdateSupported: Bool { get } - func update(with chatItem: ChatItemProtocol) - var canCalculateHeightInBackground: Bool { get } // Default is false func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell @@ -69,3 +65,13 @@ public protocol ChatItemPresenterBuilderProtocol { func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol var presenterType: ChatItemPresenterProtocol.Type { get } } + +// MARK: - Updatable Chat Items + +public protocol UpdatableChatItemPresenterProtocol: ChatItemPresenterProtocol { + func update(with chatItem: ChatItemProtocol) +} + +public protocol ContentEquatableChatItemProtocol: ChatItemProtocol { + func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool +} diff --git a/Chatto/Source/Chat Items/DummyChatItemPresenter.swift b/Chatto/Source/Chat Items/DummyChatItemPresenter.swift index 7eaece15a..2e7aab9c4 100644 --- a/Chatto/Source/Chat Items/DummyChatItemPresenter.swift +++ b/Chatto/Source/Chat Items/DummyChatItemPresenter.swift @@ -25,16 +25,12 @@ import Foundation // Handles messages which aren't supported. So, they appear as invisible. -class DummyChatItemPresenter: ChatItemPresenterProtocol { +class DummyChatItemPresenter: UpdatableChatItemPresenterProtocol { class func registerCells(_ collectionView: UICollectionView) { collectionView.register(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message") } - var isItemUpdateSupported: Bool { - return true - } - func update(with chatItem: ChatItemProtocol) { // Does nothing } diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift index 0fb467fea..139c7e6b8 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift @@ -32,7 +32,7 @@ func createFakeChatItems(count: Int) -> [ChatItemProtocol] { return items } -class TesteableChatViewController: BaseChatViewController { +final class TesteableChatViewController: BaseChatViewController { let presenterBuilders: [ChatItemType: [ChatItemPresenterBuilderProtocol]] let chatInputView = UIView() init(presenterBuilders: [ChatItemType: [ChatItemPresenterBuilderProtocol]] = [ChatItemType: [ChatItemPresenterBuilderProtocol]]()) { @@ -53,7 +53,7 @@ class TesteableChatViewController: BaseChatViewController { } } -class FakeDataSource: ChatDataSourceProtocol { +final class FakeDataSource: ChatDataSourceProtocol { var hasMoreNext = false var hasMorePrevious = false var wasRequestedForPrevious = false @@ -80,9 +80,9 @@ class FakeDataSource: ChatDataSourceProtocol { } } -class FakeCell: UICollectionViewCell {} +final class FakeCell: UICollectionViewCell {} -class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { +final class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { private(set) var createdPresenters: [ChatItemPresenterProtocol] = [] func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool { @@ -100,34 +100,21 @@ class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { } } -class FakePresenter: BaseChatItemPresenter { +final class FakePresenter: ChatItemPresenterProtocol { - var _isItemUpdateSupportedReturnValue: Bool = false - override var isItemUpdateSupported: Bool { - return self._isItemUpdateSupportedReturnValue - } - - private var _updateWithChatItemCalls: [(ChatItemProtocol)] = [] - var _updateWithChatItemIsCalled: Bool { return self._updateWithChatItemCallsCount > 0 } - var _updateWithChatItemCallsCount: Int { return self._updateWithChatItemCalls.count } - var _updateWithChatItemLastCallParams: ChatItemProtocol? { return self._updateWithChatItemCalls.last } - override func update(with chatItem: ChatItemProtocol) { - self._updateWithChatItemCalls.append((chatItem)) - } - - override class func registerCells(_ collectionView: UICollectionView) { + class func registerCells(_ collectionView: UICollectionView) { collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell") } - override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { + func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { return 10 } - override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { + func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell(withReuseIdentifier: "fake-cell", for: indexPath as IndexPath) } - override func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { + func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { let fakeCell = cell as! FakeCell fakeCell.backgroundColor = UIColor.red } @@ -147,8 +134,6 @@ final class FakeChatItem: ChatItemProtocol { final class FakeChatItemPresenter: ChatItemPresenterProtocol { init() {} - var isItemUpdateSupported: Bool { return false } - func update(with chatItem: ChatItemProtocol) {} static func registerCells(_ collectionView: UICollectionView) {} func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { return 0 } func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return UICollectionViewCell() } @@ -200,3 +185,57 @@ final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol { } } } + +// MARK: - Updatable + +final class FakeUpdatablePresenterBuilder: ChatItemPresenterBuilderProtocol { + + private(set) var createdPresenters: [ChatItemPresenterProtocol] = [] + + var updatedPresentersCount: Int { + return self.createdPresenters.reduce(0) { return $0 + ($1 as! FakeUpdatablePresenter)._updateWithChatItemCallsCount } + } + + func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool { + return chatItem.type == "fake-type" + } + + func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { + let presenter = FakeUpdatablePresenter() + self.createdPresenters.append(presenter) + return presenter + } + + var presenterType: ChatItemPresenterProtocol.Type { + return FakeUpdatablePresenter.self + } +} + +final class FakeUpdatablePresenter: UpdatableChatItemPresenterProtocol { + + static func registerCells(_ collectionView: UICollectionView) { + collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell") + } + + func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { + return 10 + } + + func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { + return collectionView.dequeueReusableCell(withReuseIdentifier: "fake-cell", for: indexPath as IndexPath) + } + + func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { + let fakeCell = cell as! FakeCell + fakeCell.backgroundColor = UIColor.red + } + + private var _updateWithChatItemCalls: [(ChatItemProtocol)] = [] + var _updateWithChatItemIsCalled: Bool { return self._updateWithChatItemCallsCount > 0 } + var _updateWithChatItemCallsCount: Int { return self._updateWithChatItemCalls.count } + var _updateWithChatItemLastCallParams: ChatItemProtocol? { return self._updateWithChatItemCalls.last } + + func update(with chatItem: ChatItemProtocol) { + self._updateWithChatItemCalls.append((chatItem)) + } +} diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift index 3b200b807..3ad766141 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTests.swift @@ -278,74 +278,105 @@ class ChatViewControllerTests: XCTestCase { extension ChatViewControllerTests { - func testThat_WhenDataSourceIsUpdatedWithOneNewItem_ThenOneNewItemPresenterIsCreated() { + // MARK: Same Items + + func testThat_GivenDataSourceWithNotUpdatableItemPresenters_AndTwoItems_WhenItIsUpdatedWithSameItems_ThenTwoPresentersAreCreated() { let presenterBuilder = FakePresenterBuilder() let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let fakeDataSource = FakeDataSource() fakeDataSource.chatItems = createFakeChatItems(count: 2) controller.chatDataSource = fakeDataSource self.fakeDidAppearAndLayout(controller: controller) - presenterBuilder.createdPresenters.forEach { ($0 as! FakePresenter)._isItemUpdateSupportedReturnValue = true } - let numberOfPresentersBeforeUpdate = presenterBuilder.createdPresenters.count + let numberOfCreatedPresentersBeforeUpdate = presenterBuilder.createdPresenters.count - fakeDataSource.chatItems = createFakeChatItems(count: 3) + fakeDataSource.chatItems = createFakeChatItems(count: 2) + let asyncExpectation = expectation(description: "update") + controller.enqueueModelUpdate(updateType: .normal) { + asyncExpectation.fulfill() + } + + self.waitForExpectations(timeout: 1) { _ in + let numberOfCreatedPresentersAfterUpdate = presenterBuilder.createdPresenters.count + let numberOfCreatedPresenters = numberOfCreatedPresentersAfterUpdate - numberOfCreatedPresentersBeforeUpdate + XCTAssertEqual(numberOfCreatedPresenters, 2) + } + } + + func testThat_GivenDataSourceWithUpdatableItemPresenters_AndTwoItems_WhenItIsUpdatedWithSameItems_ThenTwoPresentersAreUpdated() { + let presenterBuilder = FakeUpdatablePresenterBuilder() + let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) + let fakeDataSource = FakeDataSource() + fakeDataSource.chatItems = createFakeChatItems(count: 2) + controller.chatDataSource = fakeDataSource + self.fakeDidAppearAndLayout(controller: controller) + let numberOfUpdatedPresentersBeforeUpdate = presenterBuilder.updatedPresentersCount + let numberOfCreatedPresentersBeforeUpdate = presenterBuilder.createdPresenters.count + + fakeDataSource.chatItems = createFakeChatItems(count: 2) let asyncExpectation = expectation(description: "update") controller.enqueueModelUpdate(updateType: .normal) { asyncExpectation.fulfill() } self.waitForExpectations(timeout: 1) { _ in - let numberOfPresenterAfterUpdate = presenterBuilder.createdPresenters.count - XCTAssertEqual(numberOfPresenterAfterUpdate - numberOfPresentersBeforeUpdate, 1) + let numberOfUpdatedPresentersAfterUpdate = presenterBuilder.updatedPresentersCount + let numberOfUpdatedPresenters = numberOfUpdatedPresentersAfterUpdate - numberOfUpdatedPresentersBeforeUpdate + XCTAssertEqual(numberOfUpdatedPresenters, 2) + + let numberOfCreatedPresentersAfterUpdate = presenterBuilder.createdPresenters.count + let numberOfCreatedPresenters = numberOfCreatedPresentersAfterUpdate - numberOfCreatedPresentersBeforeUpdate + XCTAssertEqual(numberOfCreatedPresenters, 0) } } - func testThat_GivenPresenterSupportsUpdates_WhenDataSourceIsUpdatedWithTheSameItem_ThendNewPresenterIsNotCreated_AndPresenterIsUpdatedOnce() { + // MARK: New Items + + func testThat_GivenDataSourceWithNotUpdatableItemPresenters_AndTwoItems_WhenItIsUpdatedWithOneNewItem_ThenThreePresentersAreCreated() { let presenterBuilder = FakePresenterBuilder() let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let fakeDataSource = FakeDataSource() - fakeDataSource.chatItems = createFakeChatItems(count: 1) + fakeDataSource.chatItems = createFakeChatItems(count: 2) controller.chatDataSource = fakeDataSource self.fakeDidAppearAndLayout(controller: controller) - let presenter = presenterBuilder.createdPresenters.last! as! FakePresenter - presenter._isItemUpdateSupportedReturnValue = true - XCTAssertEqual(presenter._updateWithChatItemCallsCount, 0) - XCTAssertEqual(presenterBuilder.createdPresenters.count, 1) + let numberOfCreatedPresentersBeforeUpdate = presenterBuilder.createdPresenters.count - fakeDataSource.chatItems = createFakeChatItems(count: 1) + fakeDataSource.chatItems = createFakeChatItems(count: 3) let asyncExpectation = expectation(description: "update") controller.enqueueModelUpdate(updateType: .normal) { asyncExpectation.fulfill() } self.waitForExpectations(timeout: 1) { _ in - XCTAssertEqual(presenterBuilder.createdPresenters.count, 1) - XCTAssertEqual(presenter._updateWithChatItemCallsCount, 1) - XCTAssert(presenter._updateWithChatItemLastCallParams! === fakeDataSource.chatItems.first!) + let numberOfCreatedPresentersAfterUpdate = presenterBuilder.createdPresenters.count + let numberOfCreatedPresenters = numberOfCreatedPresentersAfterUpdate - numberOfCreatedPresentersBeforeUpdate + XCTAssertEqual(numberOfCreatedPresenters, 3) } } - func testThat_GivenPresenterDoesntSupportUpdates_WhenDataSourceIsUpdatedWithTheSameItem_ThenPresenterIsNotUpdated_AndNewPresenterIsCreated() { - let presenterBuilder = FakePresenterBuilder() - let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) - let fakeDataSource = FakeDataSource() - fakeDataSource.chatItems = createFakeChatItems(count: 1) - controller.chatDataSource = fakeDataSource - self.fakeDidAppearAndLayout(controller: controller) - let presenter = presenterBuilder.createdPresenters.last! as! FakePresenter - presenter._isItemUpdateSupportedReturnValue = false - XCTAssertEqual(presenter._updateWithChatItemCallsCount, 0) - XCTAssertEqual(presenterBuilder.createdPresenters.count, 1) - - fakeDataSource.chatItems = createFakeChatItems(count: 1) - let asyncExpectation = expectation(description: "update") - controller.enqueueModelUpdate(updateType: .normal) { - asyncExpectation.fulfill() - } - - self.waitForExpectations(timeout: 1) { _ in - XCTAssertFalse(presenter._updateWithChatItemIsCalled) - XCTAssertEqual(presenterBuilder.createdPresenters.count, 2) - } + func testThat_GivenDataSourceWithUpdatableItemPresenters_AndTwoItems_WhenItIsUpdatedWithOneNewItem_ThenTwoPresentersAreUpdated_AndOnePresenterIsCreated() { + let presenterBuilder = FakeUpdatablePresenterBuilder() + let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) + let fakeDataSource = FakeDataSource() + fakeDataSource.chatItems = createFakeChatItems(count: 2) + controller.chatDataSource = fakeDataSource + self.fakeDidAppearAndLayout(controller: controller) + let numberOfUpdatedPresentersBeforeUpdate = presenterBuilder.updatedPresentersCount + let numberOfCreatedPresentersBeforeUpdate = presenterBuilder.createdPresenters.count + + fakeDataSource.chatItems = createFakeChatItems(count: 3) + let asyncExpectation = expectation(description: "update") + controller.enqueueModelUpdate(updateType: .normal) { + asyncExpectation.fulfill() } + + self.waitForExpectations(timeout: 1) { _ in + let numberOfUpdatedPresentersAfterUpdate = presenterBuilder.updatedPresentersCount + let numberOfUpdatedPresenters = numberOfUpdatedPresentersAfterUpdate - numberOfUpdatedPresentersBeforeUpdate + XCTAssertEqual(numberOfUpdatedPresenters, 2) + + let numberOfCreatedPresentersAfterUpdate = presenterBuilder.createdPresenters.count + let numberOfCreatedPresenters = numberOfCreatedPresentersAfterUpdate - numberOfCreatedPresentersBeforeUpdate + XCTAssertEqual(numberOfCreatedPresenters, 1) + } + } } diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift index f6d4448d2..bc8bcf5b2 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageModel.swift @@ -84,9 +84,4 @@ open class MessageModel: MessageModelProtocol { self.date = date self.status = status } - - public func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { - assertionFailure("Should be implemented in a subclass.") - return false - } } diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index d5a69d59f..ed4697ad2 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -80,13 +80,11 @@ open class CompoundMessagePresenter // Cell registration is happening lazily, right before the moment when a cell is dequeued. } - open override var isItemUpdateSupported: Bool { - return true - } - open override func update(with chatItem: ChatItemProtocol) { - guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } - let isUpdateNeeded = !self.messageModel.hasSameContent(as: newMessageModel) + guard let oldChatItem = self.messageModel as? ContentEquatableChatItemProtocol else { assertionFailure("Unexpected type of the message: \(type(of: self.messageModel))."); return } + guard let newChatItem = chatItem as? ContentEquatableChatItemProtocol, let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } + + let isUpdateNeeded = !oldChatItem.hasSameContent(as: newChatItem) self.messageModel = newMessageModel if isUpdateNeeded { self.updateContent() } } diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift index 8e6123afb..d4589c7e7 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessageModel.swift @@ -25,7 +25,7 @@ import UIKit import Chatto -public protocol PhotoMessageModelProtocol: DecoratedMessageModelProtocol { +public protocol PhotoMessageModelProtocol: DecoratedMessageModelProtocol, ContentEquatableChatItemProtocol { var image: UIImage { get } var imageSize: CGSize { get } } diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift index 1b8575abf..7495bdb22 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift @@ -57,10 +57,6 @@ open class PhotoMessagePresenter collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message") } - open override var isItemUpdateSupported: Bool { - return true - } - open override func update(with chatItem: ChatItemProtocol) { guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } self.messageModel = newMessageModel diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift index f30ead415..94b8b582a 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessageModel.swift @@ -25,7 +25,7 @@ import Foundation import Chatto -public protocol TextMessageModelProtocol: DecoratedMessageModelProtocol { +public protocol TextMessageModelProtocol: DecoratedMessageModelProtocol, ContentEquatableChatItemProtocol { var text: String { get } } diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift index 08b36a780..01a22d5d4 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift @@ -63,10 +63,6 @@ open class TextMessagePresenter collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming") } - open override var isItemUpdateSupported: Bool { - return true - } - open override func update(with chatItem: ChatItemProtocol) { guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } self.messageModel = newMessageModel diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift index a58ec2e63..bbc7a3405 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift @@ -42,11 +42,6 @@ class SendingStatusModel: ChatItemProtocol { self.uid = uid self.status = status } - - func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { - guard let anotherModel = anotherItem as? SendingStatusModel else { return false } - return self.status == anotherModel.status - } } public class SendingStatusPresenterBuilder: ChatItemPresenterBuilderProtocol { @@ -74,14 +69,6 @@ class SendingStatusPresenter: ChatItemPresenterProtocol { self.statusModel = statusModel } - var isItemUpdateSupported: Bool { - return false - } - - func update(with chatItem: ChatItemProtocol) { - assertionFailure("SendingStatusPresenter update is not supported yet.") - } - static func registerCells(_ collectionView: UICollectionView) { collectionView.register(UINib(nibName: "SendingStatusCollectionViewCell", bundle: Bundle(for: self)), forCellWithReuseIdentifier: "SendingStatusCollectionViewCell") } diff --git a/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageSender.swift b/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageSender.swift index 3d7e63a1d..5228f9d02 100644 --- a/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageSender.swift +++ b/ChattoApp/ChattoApp/Source/Chat View Controllers/DemoChatMessageSender.swift @@ -26,7 +26,7 @@ import Foundation import Chatto import ChattoAdditions -public protocol DemoMessageModelProtocol: MessageModelProtocol { +public protocol DemoMessageModelProtocol: MessageModelProtocol, ContentEquatableChatItemProtocol { var status: MessageStatus { get set } } diff --git a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift index c76064192..5340180ed 100644 --- a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift +++ b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorModel.swift @@ -39,11 +39,6 @@ class TimeSeparatorModel: ChatItemProtocol { self.date = date self.uid = uid } - - func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { - guard let anotherModel = anotherItem as? TimeSeparatorModel else { return false } - return self.date == anotherModel.date - } } extension Date { diff --git a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift index 97e689660..8ac49ebc1 100644 --- a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift @@ -44,14 +44,6 @@ public class TimeSeparatorPresenterBuilder: ChatItemPresenterBuilderProtocol { class TimeSeparatorPresenter: ChatItemPresenterProtocol { - var isItemUpdateSupported: Bool { - return false - } - - func update(with chatItem: ChatItemProtocol) { - assertionFailure("TimeSeparatorPresenter update is not supported yet.") - } - let timeSeparatorModel: TimeSeparatorModel init (timeSeparatorModel: TimeSeparatorModel) { self.timeSeparatorModel = timeSeparatorModel From 0a92e7e33b09f70f964a82d85361f8866261086c Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 8 Aug 2019 15:48:07 +0100 Subject: [PATCH 12/18] guards are squeezed --- .../BaseChatViewController+Changes.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift index f9f3148e5..f879e282a 100644 --- a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift +++ b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift @@ -309,20 +309,14 @@ extension BaseChatViewController { */ let presenter: ChatItemPresenterProtocol = { - guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] ?? oldItems[decoratedChatItem.chatItem.uid] else { - return self.createPresenterForChatItem(decoratedChatItem.chatItem) + guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] ?? oldItems[decoratedChatItem.chatItem.uid], + oldChatItemCompanion.chatItem.type == decoratedChatItem.chatItem.type, + let existingPresenter = oldChatItemCompanion.presenter as? UpdatableChatItemPresenterProtocol else { + return self.createPresenterForChatItem(decoratedChatItem.chatItem) } - guard oldChatItemCompanion.chatItem.type == decoratedChatItem.chatItem.type else { - return self.createPresenterForChatItem(decoratedChatItem.chatItem) - } - - guard oldChatItemCompanion.presenter.isItemUpdateSupported else { - return self.createPresenterForChatItem(decoratedChatItem.chatItem) - } - - oldChatItemCompanion.presenter.update(with: decoratedChatItem.chatItem) - return oldChatItemCompanion.presenter + existingPresenter.update(with: decoratedChatItem.chatItem) + return existingPresenter }() return ChatItemCompanion(uid: decoratedChatItem.uid, chatItem: decoratedChatItem.chatItem, presenter: presenter, decorationAttributes: decoratedChatItem.decorationAttributes) From 4bd750235b02712ad8ebfbb7c2976fffe1072718 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 8 Aug 2019 17:52:07 +0100 Subject: [PATCH 13/18] Redundant hasSameContent is removed --- .../ChatController/BaseChatViewControllerTestHelpers.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift index 139c7e6b8..4ed9dbf3d 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift @@ -127,9 +127,6 @@ final class FakeChatItem: ChatItemProtocol { self.uid = uid self.type = type } - func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { - return self.type == anotherItem.type - } } final class FakeChatItemPresenter: ChatItemPresenterProtocol { From 8d6e119a055de245ca64f0aabdf4db967fe4e1fb Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 8 Aug 2019 17:52:50 +0100 Subject: [PATCH 14/18] CompoundMessagePresenter.Model conforms to ContentEquatableChatItemProtocol by default --- .../CompoundMessage/CompoundMessagePresenter.swift | 10 ++++++---- .../CompoundMessagePresenterBuilder.swift | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index ed4697ad2..d11ce0971 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -27,7 +27,7 @@ import Chatto open class CompoundMessagePresenter : BaseMessagePresenter, MessageContentPresenterDelegate where ViewModelBuilderT: ViewModelBuilderProtocol, - ViewModelBuilderT.ModelT: Equatable, + ViewModelBuilderT.ModelT: Equatable & ContentEquatableChatItemProtocol, InteractionHandlerT: BaseMessageInteractionHandlerProtocol, InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT { @@ -81,10 +81,12 @@ open class CompoundMessagePresenter } open override func update(with chatItem: ChatItemProtocol) { - guard let oldChatItem = self.messageModel as? ContentEquatableChatItemProtocol else { assertionFailure("Unexpected type of the message: \(type(of: self.messageModel))."); return } - guard let newChatItem = chatItem as? ContentEquatableChatItemProtocol, let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } + guard let newMessageModel = chatItem as? ModelT else { + assertionFailure("Unexpected type of the message: \(type(of: chatItem)).") + return + } - let isUpdateNeeded = !oldChatItem.hasSameContent(as: newChatItem) + let isUpdateNeeded = !self.messageModel.hasSameContent(as: newMessageModel) self.messageModel = newMessageModel if isUpdateNeeded { self.updateContent() } } diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenterBuilder.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenterBuilder.swift index f17e67403..e5ff4eebe 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenterBuilder.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenterBuilder.swift @@ -26,7 +26,7 @@ import Chatto @available(iOS 11, *) public final class CompoundMessagePresenterBuilder: ChatItemPresenterBuilderProtocol where ViewModelBuilderT: ViewModelBuilderProtocol, - ViewModelBuilderT.ModelT: Equatable, + ViewModelBuilderT.ModelT: Equatable & ContentEquatableChatItemProtocol, InteractionHandlerT: BaseMessageInteractionHandlerProtocol, InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT { public typealias ModelT = ViewModelBuilderT.ModelT From 3f7daf530865abc14183a3abe27ebef0546236d0 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Mon, 12 Aug 2019 17:05:23 +0100 Subject: [PATCH 15/18] Support of the message update if uid is changed --- .../CompoundMessagePresenter.swift | 19 ++++++++++++++++++- .../DefaultMessageContentPresenter.swift | 8 ++++++++ .../MessageContentPresenterProtocol.swift | 2 ++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index d11ce0971..48c90ba44 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -88,7 +88,18 @@ open class CompoundMessagePresenter let isUpdateNeeded = !self.messageModel.hasSameContent(as: newMessageModel) self.messageModel = newMessageModel - if isUpdateNeeded { self.updateContent() } + + guard !isUpdateNeeded else { + self.updateContent() + return + } + + let isUidUpdateNeeded = self.messageModel.uid != newMessageModel.uid + + guard !isUidUpdateNeeded else { + self.updateContentPresenters(with: newMessageModel.uid) + return + } } private func updateContent() { @@ -103,6 +114,12 @@ open class CompoundMessagePresenter self.menuPresenter = self.contentFactories.lazy.compactMap { $0.createMenuPresenter(forModel: self.messageModel) }.first } + private func updateContentPresenters(with newMessage: Any) { + self.contentPresenters.forEach { + $0.updateMessage(newMessage) + } + } + open override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cellReuseIdentifier = self.compoundCellReuseId collectionView.register(CompoundMessageCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier) diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift index 1fc1f1cb8..efe289bce 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift @@ -76,4 +76,12 @@ public final class DefaultMessageContentPresenter public func unbindFromView() { self.onUnbinding?(self.view) } + + public func updateMessage(_ newMessage: Any) { + guard let message = newMessage as? MessageType else { + assertionFailure("Unexpected message type: \(type(of: newMessage))") + return + } + self.message = message + } } diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/MessageContentPresenterProtocol.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/MessageContentPresenterProtocol.swift index 39134eb71..705a25c25 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/MessageContentPresenterProtocol.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/MessageContentPresenterProtocol.swift @@ -51,4 +51,6 @@ public protocol MessageContentPresenterProtocol { func bindToView(with viewReference: ViewReference) func unbindFromView() + + func updateMessage(_ newMessage: Any) } From 8d3b23c07d3bb52e39da8557294655365ab36cc5 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Wed, 14 Aug 2019 15:22:18 +0100 Subject: [PATCH 16/18] Message model update logic is fixed --- .../ChattoAdditions.xcodeproj/project.pbxproj | 36 +++++++ .../CompoundMessagePresenter.swift | 16 +-- .../DefaultMessageContentPresenter.swift | 9 +- .../CompoundMessagePresenterTests.swift | 64 +++++++++++ .../DefaultMessageContentPresenterTests.swift | 44 ++++++++ .../FakeMessageInteractionHandler.swift | 101 ++++++++++++++++++ .../BaseMessage/FakeViewModelBuilder.swift | 56 ++++++++++ .../BaseMessage/MessageModel+Helpers.swift | 53 +++++++++ .../StubCompoundBubbleViewStyle.swift | 48 +++++++++ .../StubMessageCollectionViewCellStyle.swift | 60 +++++++++++ .../Chat Items/BaseMessage/TestHelpers.swift | 65 +++++++++++ .../TestableCompoundMessagePresenter.swift | 64 +++++++++++ 12 files changed, 607 insertions(+), 9 deletions(-) create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/CompoundMessagePresenterTests.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/DefaultMessageContentPresenterTests.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/FakeMessageInteractionHandler.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/FakeViewModelBuilder.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/MessageModel+Helpers.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/StubCompoundBubbleViewStyle.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/StubMessageCollectionViewCellStyle.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/TestHelpers.swift create mode 100644 ChattoAdditions/Tests/Chat Items/BaseMessage/TestableCompoundMessagePresenter.swift diff --git a/ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj b/ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj index 6918602e6..af3dcfd29 100644 --- a/ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj +++ b/ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj @@ -116,6 +116,15 @@ EA13CAF2229EE8C8009340C5 /* CircleIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA13CAF1229EE8C8009340C5 /* CircleIconView.swift */; }; EA13CAF4229EE985009340C5 /* CircleProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA13CAF3229EE985009340C5 /* CircleProgressView.swift */; }; EA13CAF6229EE9A6009340C5 /* CircleProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA13CAF5229EE9A6009340C5 /* CircleProgressIndicatorView.swift */; }; + EA7525502302E94B0069D1AF /* MessageModel+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA75254F2302E94B0069D1AF /* MessageModel+Helpers.swift */; }; + EA7525522302FB930069D1AF /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7525512302FB930069D1AF /* TestHelpers.swift */; }; + EA7527982302FD790069D1AF /* StubCompoundBubbleViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7527972302FD790069D1AF /* StubCompoundBubbleViewStyle.swift */; }; + EA75279A2302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7527992302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift */; }; + EA75279C23031FE60069D1AF /* TestableCompoundMessagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA75279B23031FE60069D1AF /* TestableCompoundMessagePresenter.swift */; }; + EAAFBCEB2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAFBCEA2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift */; }; + EAAFBCED2302D8C8004F553D /* CompoundMessagePresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAFBCEC2302D8C8004F553D /* CompoundMessagePresenterTests.swift */; }; + EAAFBCEF2302DB22004F553D /* FakeViewModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAFBCEE2302DB22004F553D /* FakeViewModelBuilder.swift */; }; + EAAFBCF12302DBAF004F553D /* FakeMessageInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAFBCF02302DBAF004F553D /* FakeMessageInteractionHandler.swift */; }; EAF2678E22BD1524006B3455 /* MessageContentPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2678D22BD1524006B3455 /* MessageContentPresenterProtocol.swift */; }; EAF2679022BD2625006B3455 /* DefaultMessageContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2678F22BD2625006B3455 /* DefaultMessageContentPresenter.swift */; }; F6D04BA71CA46C0200E803FA /* PhotosInputPlaceholderDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D04BA61CA46C0200E803FA /* PhotosInputPlaceholderDataProviderTests.swift */; }; @@ -245,6 +254,15 @@ EA13CAF1229EE8C8009340C5 /* CircleIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleIconView.swift; sourceTree = ""; }; EA13CAF3229EE985009340C5 /* CircleProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleProgressView.swift; sourceTree = ""; }; EA13CAF5229EE9A6009340C5 /* CircleProgressIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleProgressIndicatorView.swift; sourceTree = ""; }; + EA75254F2302E94B0069D1AF /* MessageModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageModel+Helpers.swift"; sourceTree = ""; }; + EA7525512302FB930069D1AF /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; + EA7527972302FD790069D1AF /* StubCompoundBubbleViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubCompoundBubbleViewStyle.swift; sourceTree = ""; }; + EA7527992302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubMessageCollectionViewCellStyle.swift; sourceTree = ""; }; + EA75279B23031FE60069D1AF /* TestableCompoundMessagePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableCompoundMessagePresenter.swift; sourceTree = ""; }; + EAAFBCEA2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultMessageContentPresenterTests.swift; sourceTree = ""; }; + EAAFBCEC2302D8C8004F553D /* CompoundMessagePresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundMessagePresenterTests.swift; sourceTree = ""; }; + EAAFBCEE2302DB22004F553D /* FakeViewModelBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeViewModelBuilder.swift; sourceTree = ""; }; + EAAFBCF02302DBAF004F553D /* FakeMessageInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeMessageInteractionHandler.swift; sourceTree = ""; }; EAF2678D22BD1524006B3455 /* MessageContentPresenterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageContentPresenterProtocol.swift; sourceTree = ""; }; EAF2678F22BD2625006B3455 /* DefaultMessageContentPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultMessageContentPresenter.swift; sourceTree = ""; }; F6D04BA61CA46C0200E803FA /* PhotosInputPlaceholderDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosInputPlaceholderDataProviderTests.swift; sourceTree = ""; }; @@ -581,6 +599,15 @@ isa = PBXGroup; children = ( C3EFA6AF1C03607A0063CE22 /* BaseMessagePresenterTests.swift */, + EAAFBCEC2302D8C8004F553D /* CompoundMessagePresenterTests.swift */, + EAAFBCEA2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift */, + EAAFBCF02302DBAF004F553D /* FakeMessageInteractionHandler.swift */, + EAAFBCEE2302DB22004F553D /* FakeViewModelBuilder.swift */, + EA75254F2302E94B0069D1AF /* MessageModel+Helpers.swift */, + EA7527972302FD790069D1AF /* StubCompoundBubbleViewStyle.swift */, + EA7527992302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift */, + EA75279B23031FE60069D1AF /* TestableCompoundMessagePresenter.swift */, + EA7525512302FB930069D1AF /* TestHelpers.swift */, ); path = BaseMessage; sourceTree = ""; @@ -846,15 +873,24 @@ files = ( C3C0CC861BFE49700052747C /* ChatInputItemTests.swift in Sources */, CDC6100B1FD8268200C2588E /* FakePhotosInputDataProvider.swift in Sources */, + EAAFBCEB2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift in Sources */, + EAAFBCEF2302DB22004F553D /* FakeViewModelBuilder.swift in Sources */, + EA75279A2302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift in Sources */, 55ABA5771FC74F8D00923302 /* ObservableTests.swift in Sources */, + EA7527982302FD790069D1AF /* StubCompoundBubbleViewStyle.swift in Sources */, C3C0CC881BFE49700052747C /* ChatInputPresenterTests.swift in Sources */, C3815D001C036B3000DF95CA /* PhotoMessagePresenterBuilderTests.swift in Sources */, C363C33F1D2405F800599FF5 /* LiveCameraCellPresenterTests.swift in Sources */, F6D04BA91CA46D5000E803FA /* PhotosInputWithPlaceholdersDataProviderTests.swift in Sources */, + EA75279C23031FE60069D1AF /* TestableCompoundMessagePresenter.swift in Sources */, + EA7525522302FB930069D1AF /* TestHelpers.swift in Sources */, C35FE3C51C0331CF00D42980 /* TextMessagePresenterTests.swift in Sources */, 55ABA5731FC74E0400923302 /* UIEdgeInets+AdditionsTests.swift in Sources */, C3C0CC8A1BFE49700052747C /* PhotosChatInputItemTests.swift in Sources */, + EA7525502302E94B0069D1AF /* MessageModel+Helpers.swift in Sources */, + EAAFBCED2302D8C8004F553D /* CompoundMessagePresenterTests.swift in Sources */, 55ABA5691FC7498700923302 /* CGRect+AdditionsTests.swift in Sources */, + EAAFBCF12302DBAF004F553D /* FakeMessageInteractionHandler.swift in Sources */, C3C0CC8B1BFE49700052747C /* PhotosInputViewItemSizeCalculatorTests.swift in Sources */, CDE15F43205993FB005D86DD /* PhotosInputCameraPickerTests.swift in Sources */, F6D04BA71CA46C0200E803FA /* PhotosInputPlaceholderDataProviderTests.swift in Sources */, diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index 48c90ba44..5815ea966 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -86,23 +86,23 @@ open class CompoundMessagePresenter return } - let isUpdateNeeded = !self.messageModel.hasSameContent(as: newMessageModel) + let isContentChanged = !self.messageModel.hasSameContent(as: newMessageModel) + let isMessageUidChanged = self.messageModel.uid != newMessageModel.uid + self.messageModel = newMessageModel - guard !isUpdateNeeded else { + guard !isContentChanged else { self.updateContent() return } - let isUidUpdateNeeded = self.messageModel.uid != newMessageModel.uid - - guard !isUidUpdateNeeded else { - self.updateContentPresenters(with: newMessageModel.uid) + guard !isMessageUidChanged else { + self.updateExistingContentPresenters(with: newMessageModel) return } } - private func updateContent() { + open func updateContent() { self.contentFactories = self.initialContentFactories.filter { $0.canCreateMessageContent(forModel: self.messageModel) } self.contentPresenters = self.contentFactories.compactMap { @@ -114,7 +114,7 @@ open class CompoundMessagePresenter self.menuPresenter = self.contentFactories.lazy.compactMap { $0.createMenuPresenter(forModel: self.messageModel) }.first } - private func updateContentPresenters(with newMessage: Any) { + open func updateExistingContentPresenters(with newMessage: Any) { self.contentPresenters.forEach { $0.updateMessage(newMessage) } diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift index efe289bce..eb8312b75 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/Content/DefaultMessageContentPresenter.swift @@ -28,6 +28,7 @@ public final class DefaultMessageContentPresenter public typealias ActionHandler = (_ message: MessageType, _ view: ViewType?) -> Void public typealias BindingClosure = (_ message: MessageType, _ view: ViewType?) -> Void public typealias UnbindingClosure = (_ view: ViewType?) -> Void + public typealias MessageUpdateClosure = (_ newMessage: MessageType) -> Void public init(message: MessageType, showBorder: Bool, @@ -35,7 +36,8 @@ public final class DefaultMessageContentPresenter onUnbinding: UnbindingClosure? = nil, onContentWillBeShown: ActionHandler? = nil, onContentWasHidden: ActionHandler? = nil, - onContentWasTapped_deprecated: ActionHandler? = nil) { + onContentWasTapped_deprecated: ActionHandler? = nil, + onMessageUpdate: MessageUpdateClosure? = nil) { self.message = message self.onBinding = onBinding @@ -45,6 +47,8 @@ public final class DefaultMessageContentPresenter self.onContentWillBeShown = onContentWillBeShown self.onContentWasHidden = onContentWasHidden self.onContentWasTapped_deprecated = onContentWasTapped_deprecated + + self.onMessageUpdate = onMessageUpdate } private var message: MessageType @@ -58,6 +62,8 @@ public final class DefaultMessageContentPresenter private let onContentWasHidden: ActionHandler? private let onContentWasTapped_deprecated: ActionHandler? + private let onMessageUpdate: MessageUpdateClosure? + // MARK: - MessageContentPresenterProtocol public weak var delegate: MessageContentPresenterDelegate? @@ -83,5 +89,6 @@ public final class DefaultMessageContentPresenter return } self.message = message + self.onMessageUpdate?(self.message) } } diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/CompoundMessagePresenterTests.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/CompoundMessagePresenterTests.swift new file mode 100644 index 000000000..540e0c2d3 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/CompoundMessagePresenterTests.swift @@ -0,0 +1,64 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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. + +@testable import ChattoAdditions +import XCTest + +@available(iOS 11, *) +final class CompoundMessagePresenterTests: XCTestCase { + + func test_WhenPresenterIsUpdatedWithTheSameMessage_ThenUpdateContentNotCalled_AndUpdateExistingContentPresentersNotCalled() { + let message = TestHelpers.makeMessage(withId: "123") + let sameMessage = message + let presenter = TestHelpers.makeTestableCompoundMessagePresenter(with: message) + + presenter.update(with: sameMessage) + + XCTAssertEqual(0, presenter.invokedUpdateContentCount) + XCTAssertEqual(0, presenter.invokedUpdateExistingContentPresentersCount) + } + + func test_WhenPresenterIsUpdatedWithMessageWithAnotherContent_ThenUpdateContentCalled_ButUpdateExistingContentPresentersNotCalled() { + let date = Date() + let anotherDate = date.addingTimeInterval(1) + let message = TestHelpers.makeMessage(withId: "123", date: date) + let sameMessageWithAnotherId = TestHelpers.makeMessage(withId: "123", date: anotherDate) + let presenter = TestHelpers.makeTestableCompoundMessagePresenter(with: message) + + presenter.update(with: sameMessageWithAnotherId) + + XCTAssertEqual(1, presenter.invokedUpdateContentCount) + XCTAssertEqual(0, presenter.invokedUpdateExistingContentPresentersCount) + } + + func test_WhenPresenterIsUpdatedWithSameMessageWithAnotherId_ThenUpdateContentNotCalled_ButUpdateExistingContentPresentersCalled() { + let message = TestHelpers.makeMessage(withId: "123") + let sameMessageWithAnotherId = message.makeSameMessage(butAnotherId: "456") + let presenter = TestHelpers.makeTestableCompoundMessagePresenter(with: message) + + presenter.update(with: sameMessageWithAnotherId) + + XCTAssertEqual(0, presenter.invokedUpdateContentCount) + XCTAssertEqual(1, presenter.invokedUpdateExistingContentPresentersCount) + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/DefaultMessageContentPresenterTests.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/DefaultMessageContentPresenterTests.swift new file mode 100644 index 000000000..8334ac562 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/DefaultMessageContentPresenterTests.swift @@ -0,0 +1,44 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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. + +@testable import ChattoAdditions +import XCTest + +final class DefaultMessageContentPresenterTests: XCTestCase { + + func test_WhenPresenterIsUpdatedWithTheSameMessageWithAnotherId_ThenOnMessageUpdateClosureIsCalled_AndItIsCalledWithUpdatedMessage() { + let message = TestHelpers.makeMessage(withId: "123") + let sameMessageWithAnotherId = message.makeSameMessage(butAnotherId: "456") + var isOnMessageUpdateCallsCount = 0 + var newMessageOnUpdate: MessageModel! + let presenter = TestHelpers.makeDefaultMessageContentPresenter(with: message, onMessageUpdate: { newMessage in + isOnMessageUpdateCallsCount += 1 + newMessageOnUpdate = newMessage + }) + + presenter.updateMessage(sameMessageWithAnotherId) + + XCTAssertEqual(1, isOnMessageUpdateCallsCount) + XCTAssertEqual(sameMessageWithAnotherId.uid, newMessageOnUpdate!.uid) + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/FakeMessageInteractionHandler.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/FakeMessageInteractionHandler.swift new file mode 100644 index 000000000..3e5e280bc --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/FakeMessageInteractionHandler.swift @@ -0,0 +1,101 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 ChattoAdditions +import Foundation + +final class FakeMessageInteractionHandler: BaseMessageInteractionHandlerProtocol { + + typealias ViewModelT = MessageViewModel + + var invokedUserDidTapOnFailIcon = false + var invokedUserDidTapOnFailIconCount = 0 + var invokedUserDidTapOnFailIconParameters: (viewModel: ViewModelT, failIconView: UIView)? + var invokedUserDidTapOnFailIconParametersList = [(viewModel: ViewModelT, failIconView: UIView)]() + func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView) { + invokedUserDidTapOnFailIcon = true + invokedUserDidTapOnFailIconCount += 1 + invokedUserDidTapOnFailIconParameters = (viewModel, failIconView) + invokedUserDidTapOnFailIconParametersList.append((viewModel, failIconView)) + } + var invokedUserDidTapOnAvatar = false + var invokedUserDidTapOnAvatarCount = 0 + var invokedUserDidTapOnAvatarParameters: (viewModel: ViewModelT, Void)? + var invokedUserDidTapOnAvatarParametersList = [(viewModel: ViewModelT, Void)]() + func userDidTapOnAvatar(viewModel: ViewModelT) { + invokedUserDidTapOnAvatar = true + invokedUserDidTapOnAvatarCount += 1 + invokedUserDidTapOnAvatarParameters = (viewModel, ()) + invokedUserDidTapOnAvatarParametersList.append((viewModel, ())) + } + var invokedUserDidTapOnBubble = false + var invokedUserDidTapOnBubbleCount = 0 + var invokedUserDidTapOnBubbleParameters: (viewModel: ViewModelT, Void)? + var invokedUserDidTapOnBubbleParametersList = [(viewModel: ViewModelT, Void)]() + func userDidTapOnBubble(viewModel: ViewModelT) { + invokedUserDidTapOnBubble = true + invokedUserDidTapOnBubbleCount += 1 + invokedUserDidTapOnBubbleParameters = (viewModel, ()) + invokedUserDidTapOnBubbleParametersList.append((viewModel, ())) + } + var invokedUserDidBeginLongPressOnBubble = false + var invokedUserDidBeginLongPressOnBubbleCount = 0 + var invokedUserDidBeginLongPressOnBubbleParameters: (viewModel: ViewModelT, Void)? + var invokedUserDidBeginLongPressOnBubbleParametersList = [(viewModel: ViewModelT, Void)]() + func userDidBeginLongPressOnBubble(viewModel: ViewModelT) { + invokedUserDidBeginLongPressOnBubble = true + invokedUserDidBeginLongPressOnBubbleCount += 1 + invokedUserDidBeginLongPressOnBubbleParameters = (viewModel, ()) + invokedUserDidBeginLongPressOnBubbleParametersList.append((viewModel, ())) + } + var invokedUserDidEndLongPressOnBubble = false + var invokedUserDidEndLongPressOnBubbleCount = 0 + var invokedUserDidEndLongPressOnBubbleParameters: (viewModel: ViewModelT, Void)? + var invokedUserDidEndLongPressOnBubbleParametersList = [(viewModel: ViewModelT, Void)]() + func userDidEndLongPressOnBubble(viewModel: ViewModelT) { + invokedUserDidEndLongPressOnBubble = true + invokedUserDidEndLongPressOnBubbleCount += 1 + invokedUserDidEndLongPressOnBubbleParameters = (viewModel, ()) + invokedUserDidEndLongPressOnBubbleParametersList.append((viewModel, ())) + } + var invokedUserDidSelectMessage = false + var invokedUserDidSelectMessageCount = 0 + var invokedUserDidSelectMessageParameters: (viewModel: ViewModelT, Void)? + var invokedUserDidSelectMessageParametersList = [(viewModel: ViewModelT, Void)]() + func userDidSelectMessage(viewModel: ViewModelT) { + invokedUserDidSelectMessage = true + invokedUserDidSelectMessageCount += 1 + invokedUserDidSelectMessageParameters = (viewModel, ()) + invokedUserDidSelectMessageParametersList.append((viewModel, ())) + } + var invokedUserDidDeselectMessage = false + var invokedUserDidDeselectMessageCount = 0 + var invokedUserDidDeselectMessageParameters: (viewModel: ViewModelT, Void)? + var invokedUserDidDeselectMessageParametersList = [(viewModel: ViewModelT, Void)]() + func userDidDeselectMessage(viewModel: ViewModelT) { + invokedUserDidDeselectMessage = true + invokedUserDidDeselectMessageCount += 1 + invokedUserDidDeselectMessageParameters = (viewModel, ()) + invokedUserDidDeselectMessageParametersList.append((viewModel, ())) + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/FakeViewModelBuilder.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/FakeViewModelBuilder.swift new file mode 100644 index 000000000..50fcd6c5f --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/FakeViewModelBuilder.swift @@ -0,0 +1,56 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 ChattoAdditions +import Foundation + +final class FakeViewModelBuilder: ViewModelBuilderProtocol { + + typealias ModelT = MessageModel + typealias ViewModelT = MessageViewModel + + var invokedCanCreateViewModel = false + var invokedCanCreateViewModelCount = 0 + var invokedCanCreateViewModelParameters: (model: Any, Void)? + var invokedCanCreateViewModelParametersList = [(model: Any, Void)]() + var stubbedCanCreateViewModelResult: Bool! = false + func canCreateViewModel(fromModel model: Any) -> Bool { + invokedCanCreateViewModel = true + invokedCanCreateViewModelCount += 1 + invokedCanCreateViewModelParameters = (model, ()) + invokedCanCreateViewModelParametersList.append((model, ())) + return stubbedCanCreateViewModelResult + } + var invokedCreateViewModel = false + var invokedCreateViewModelCount = 0 + var invokedCreateViewModelParameters: (model: ModelT, Void)? + var invokedCreateViewModelParametersList = [(model: ModelT, Void)]() + var stubbedCreateViewModelResult: ViewModelT! + func createViewModel(_ model: ModelT) -> ViewModelT { + invokedCreateViewModel = true + invokedCreateViewModelCount += 1 + invokedCreateViewModelParameters = (model, ()) + invokedCreateViewModelParametersList.append((model, ())) + return stubbedCreateViewModelResult + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/MessageModel+Helpers.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/MessageModel+Helpers.swift new file mode 100644 index 000000000..63c88c217 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/MessageModel+Helpers.swift @@ -0,0 +1,53 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 Chatto +import ChattoAdditions +import Foundation + +extension MessageModel: Equatable, ContentEquatableChatItemProtocol { + + public static func == (lhs: MessageModel, rhs: MessageModel) -> Bool { + return lhs.uid == rhs.uid && lhs.hasSameContent(as: rhs) + } + + public func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool { + guard let anotherMessageModel = anotherItem as? MessageModel else { return false } + return self.senderId == anotherMessageModel.senderId + && self.type == anotherMessageModel.type + && self.isIncoming == anotherMessageModel.isIncoming + && self.date == anotherMessageModel.date + && self.status == anotherMessageModel.status + } + + func makeSameMessage(butAnotherId anotherId: String) -> MessageModel { + return MessageModel( + uid: anotherId, + senderId: self.senderId, + type: self.type, + isIncoming: self.isIncoming, + date: self.date, + status: self.status + ) + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/StubCompoundBubbleViewStyle.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/StubCompoundBubbleViewStyle.swift new file mode 100644 index 000000000..732ce0b40 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/StubCompoundBubbleViewStyle.swift @@ -0,0 +1,48 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 ChattoAdditions +import Foundation + +final class StubCompoundBubbleViewStyle: CompoundBubbleViewStyleProtocol { + var stubbedHideBubbleForSingleContent: Bool! = false + var hideBubbleForSingleContent: Bool { + return stubbedHideBubbleForSingleContent + } + var stubbedBackgroundColorResult: UIColor! + func backgroundColor(forViewModel viewModel: ViewModel) -> UIColor? { + return stubbedBackgroundColorResult + } + var stubbedMaskingImageResult: UIImage! + func maskingImage(forViewModel viewModel: ViewModel) -> UIImage? { + return stubbedMaskingImageResult + } + var stubbedBorderImageResult: UIImage! + func borderImage(forViewModel viewModel: ViewModel) -> UIImage? { + return stubbedBorderImageResult + } + var stubbedTailWidthResult: CGFloat! + func tailWidth(forViewModel viewModel: ViewModel) -> CGFloat { + return stubbedTailWidthResult + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/StubMessageCollectionViewCellStyle.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/StubMessageCollectionViewCellStyle.swift new file mode 100644 index 000000000..6d1bc5f11 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/StubMessageCollectionViewCellStyle.swift @@ -0,0 +1,60 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 ChattoAdditions +import Foundation + +final class StubMessageCollectionViewCellStyle: BaseMessageCollectionViewCellStyleProtocol { + var stubbedFailedIcon: UIImage! + var failedIcon: UIImage { + return stubbedFailedIcon + } + var stubbedFailedIconHighlighted: UIImage! + var failedIconHighlighted: UIImage { + return stubbedFailedIconHighlighted + } + var stubbedSelectionIndicatorMargins: UIEdgeInsets! + var selectionIndicatorMargins: UIEdgeInsets { + return stubbedSelectionIndicatorMargins + } + var stubbedAvatarSizeResult: CGSize! + func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize { + return stubbedAvatarSizeResult + } + var stubbedAvatarVerticalAlignmentResult: VerticalAlignment! + func avatarVerticalAlignment(viewModel: MessageViewModelProtocol) -> VerticalAlignment { + return stubbedAvatarVerticalAlignmentResult + } + var stubbedSelectionIndicatorIconResult: UIImage! + func selectionIndicatorIcon(for viewModel: MessageViewModelProtocol) -> UIImage { + return stubbedSelectionIndicatorIconResult + } + var stubbedAttributedStringForDateResult: NSAttributedString! + func attributedStringForDate(_ date: String) -> NSAttributedString { + return stubbedAttributedStringForDateResult + } + var stubbedLayoutConstantsResult: BaseMessageCollectionViewCellLayoutConstants! + func layoutConstants(viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants { + return stubbedLayoutConstantsResult + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/TestHelpers.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/TestHelpers.swift new file mode 100644 index 000000000..8613328c2 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/TestHelpers.swift @@ -0,0 +1,65 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 ChattoAdditions +import Foundation + +struct TestHelpers { + + static func makeMessage(withId messageId: String, date: Date = Date()) -> MessageModel { + return MessageModel(uid: messageId, senderId: "123", type: "text", isIncoming: true, date: date, status: .success) + } + + static func makeDefaultMessageContentPresenter(with message: MessageModel, onMessageUpdate: ((_ newMessage: MessageModel) -> Void)?) -> DefaultMessageContentPresenter { + return DefaultMessageContentPresenter( + message: message, + showBorder: true, + onBinding: nil, + onUnbinding: nil, + onContentWillBeShown: nil, + onContentWasHidden: nil, + onContentWasTapped_deprecated: nil, + onMessageUpdate: onMessageUpdate + ) + } + + static func makeFakeViewModelBuilder() -> FakeViewModelBuilder { + let builder = FakeViewModelBuilder() + builder.stubbedCreateViewModelResult = MessageViewModel( + dateFormatter: DateFormatter(), + messageModel: self.makeMessage(withId: "default_message"), + avatarImage: nil, + decorationAttributes: BaseMessageDecorationAttributes() + ) + return builder + } + + @available(iOS 11, *) + static func makeTestableCompoundMessagePresenter(with message: MessageModel, + viewModelBuilder: FakeViewModelBuilder? = nil) -> TestableCompoundMessagePresenter { + return TestableCompoundMessagePresenter( + messageModel: message, + viewModelBuilder: viewModelBuilder ?? self.makeFakeViewModelBuilder() + ) + } +} diff --git a/ChattoAdditions/Tests/Chat Items/BaseMessage/TestableCompoundMessagePresenter.swift b/ChattoAdditions/Tests/Chat Items/BaseMessage/TestableCompoundMessagePresenter.swift new file mode 100644 index 000000000..71a552858 --- /dev/null +++ b/ChattoAdditions/Tests/Chat Items/BaseMessage/TestableCompoundMessagePresenter.swift @@ -0,0 +1,64 @@ +// +// The MIT License (MIT) +// +// Copyright (c) 2015-present Badoo Trading Limited. +// +// 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 ChattoAdditions +import Foundation + +@available(iOS 11, *) +final class TestableCompoundMessagePresenter: CompoundMessagePresenter { + + init(messageModel: MessageModel, viewModelBuilder: FakeViewModelBuilder) { + super.init( + messageModel: messageModel, + viewModelBuilder: viewModelBuilder, + interactionHandler: nil, + contentFactories: [], + sizingCell: CompoundMessageCollectionViewCell(frame: .zero), + baseCellStyle: StubMessageCollectionViewCellStyle(), + compoundCellStyle: StubCompoundBubbleViewStyle(), + compoundCellDimensions: CompoundBubbleLayoutProvider.Dimensions(spacing: 0, contentInsets: .zero), + cache: Cache(), + accessibilityIdentifier: nil + ) + self.resetCounters() + } + + var invokedUpdateContentCount = 0 + override func updateContent() { + super.updateContent() + self.invokedUpdateContentCount += 1 + } + + var invokedUpdateExistingContentPresentersCount: Int { return self.invokedUpdateExistingContentPresentersParametersList.count } + var invokedUpdateExistingContentPresentersParametersList: [Any] = [] + + override func updateExistingContentPresenters(with newMessage: Any) { + super.updateExistingContentPresenters(with: newMessage) + self.invokedUpdateExistingContentPresentersParametersList.append(newMessage) + } + + func resetCounters() { + self.invokedUpdateContentCount = 0 + self.invokedUpdateExistingContentPresentersParametersList = [] + } +} From 1c6837418b92e9b51fcbaad2f2f860d462216ee0 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Thu, 15 Aug 2019 15:18:10 +0100 Subject: [PATCH 17/18] A default implementation for a message update is moved to BaseMessagePresenter --- .../BaseMessage/BaseMessagePresenter.swift | 6 ++++++ .../CompoundMessagePresenter.swift | 15 +++++---------- .../PhotoMessages/PhotoMessagePresenter.swift | 5 ----- .../TextMessages/TextMessagePresenter.swift | 5 ----- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift index 5a500c96d..aa1609128 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift @@ -71,6 +71,12 @@ open class BaseMessagePresenter public let viewModelBuilder: ViewModelBuilderT public let interactionHandler: InteractionHandlerT? diff --git a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift index 5815ea966..74967f3b9 100644 --- a/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/CompoundMessage/CompoundMessagePresenter.swift @@ -81,23 +81,18 @@ open class CompoundMessagePresenter } open override func update(with chatItem: ChatItemProtocol) { - guard let newMessageModel = chatItem as? ModelT else { - assertionFailure("Unexpected type of the message: \(type(of: chatItem)).") - return - } - - let isContentChanged = !self.messageModel.hasSameContent(as: newMessageModel) - let isMessageUidChanged = self.messageModel.uid != newMessageModel.uid - - self.messageModel = newMessageModel + let oldMessageModel = self.messageModel + super.update(with: chatItem) + let isContentChanged = !oldMessageModel.hasSameContent(as: chatItem) guard !isContentChanged else { self.updateContent() return } + let isMessageUidChanged = oldMessageModel.uid != chatItem.uid guard !isMessageUidChanged else { - self.updateExistingContentPresenters(with: newMessageModel) + self.updateExistingContentPresenters(with: chatItem) return } } diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift index 7495bdb22..e8db52d99 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift @@ -57,11 +57,6 @@ open class PhotoMessagePresenter collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message") } - open override func update(with chatItem: ChatItemProtocol) { - guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } - self.messageModel = newMessageModel - } - public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell(withReuseIdentifier: "photo-message", for: indexPath) } diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift index 01a22d5d4..23f83d877 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift @@ -63,11 +63,6 @@ open class TextMessagePresenter collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming") } - open override func update(with chatItem: ChatItemProtocol) { - guard let newMessageModel = chatItem as? ModelT else { assertionFailure("Unexpected type of the message: \(type(of: chatItem))."); return } - self.messageModel = newMessageModel - } - public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let identifier = self.messageViewModel.isIncoming ? "text-message-incoming" : "text-message-outcoming" return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) From e9d4a3ccd103d1e9d8640fe82aeeeb255891d7d3 Mon Sep 17 00:00:00 2001 From: Mikhail Gasanov Date: Mon, 19 Aug 2019 16:20:14 +0100 Subject: [PATCH 18/18] Rollback to the solution with "isItemUpdateSupported" --- .../Chat Items/BaseChatItemPresenter.swift | 14 +++-- .../ChatItemProtocolDefinitions.swift | 7 +-- .../Chat Items/DummyChatItemPresenter.swift | 4 +- .../BaseChatViewController+Changes.swift | 6 +- .../BaseChatViewControllerTestHelpers.swift | 60 ++++++------------- .../ChatItemCompanionCollectionTests.swift | 2 +- .../BaseMessage/BaseMessagePresenter.swift | 10 ++++ .../CompoundMessagePresenter.swift | 4 ++ .../PhotoMessages/PhotoMessagePresenter.swift | 4 ++ .../TextMessages/TextMessagePresenter.swift | 4 ++ .../SendingStatusPresenter.swift | 4 ++ .../TimeSeparatorPresenter.swift | 4 ++ ChattoApp/Podfile.lock | 2 +- ChattoApp/Pods/Manifest.lock | 2 +- ChattoApp/Pods/Pods.xcodeproj/project.pbxproj | 18 +++--- 15 files changed, 79 insertions(+), 66 deletions(-) diff --git a/Chatto/Source/Chat Items/BaseChatItemPresenter.swift b/Chatto/Source/Chat Items/BaseChatItemPresenter.swift index 053bdf0a7..6a73d6ed1 100644 --- a/Chatto/Source/Chat Items/BaseChatItemPresenter.swift +++ b/Chatto/Source/Chat Items/BaseChatItemPresenter.swift @@ -30,19 +30,23 @@ public enum ChatItemVisibility { case visible } -open class BaseChatItemPresenter: UpdatableChatItemPresenterProtocol { +open class BaseChatItemPresenter: ChatItemPresenterProtocol { public final weak var cell: CellT? public init() {} - open func update(with chatItem: ChatItemProtocol) { - assertionFailure("Implement in subclass") - } - open class func registerCells(_ collectionView: UICollectionView) { assert(false, "Implement in subclass") } + open var isItemUpdateSupported: Bool { + fatalError("Implement in subclass") + } + + open func update(with chatItem: ChatItemProtocol) { + fatalError("Implement in subclass") + } + open var canCalculateHeightInBackground: Bool { return false } diff --git a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift index 48c84cb6e..6bc85f61c 100644 --- a/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift +++ b/Chatto/Source/Chat Items/ChatItemProtocolDefinitions.swift @@ -43,6 +43,9 @@ public protocol ChatItemMenuPresenterProtocol { public protocol ChatItemPresenterProtocol: AnyObject, ChatItemMenuPresenterProtocol { static func registerCells(_ collectionView: UICollectionView) + var isItemUpdateSupported: Bool { get } + func update(with chatItem: ChatItemProtocol) + var canCalculateHeightInBackground: Bool { get } // Default is false func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell @@ -68,10 +71,6 @@ public protocol ChatItemPresenterBuilderProtocol { // MARK: - Updatable Chat Items -public protocol UpdatableChatItemPresenterProtocol: ChatItemPresenterProtocol { - func update(with chatItem: ChatItemProtocol) -} - public protocol ContentEquatableChatItemProtocol: ChatItemProtocol { func hasSameContent(as anotherItem: ChatItemProtocol) -> Bool } diff --git a/Chatto/Source/Chat Items/DummyChatItemPresenter.swift b/Chatto/Source/Chat Items/DummyChatItemPresenter.swift index 2e7aab9c4..5ee80257d 100644 --- a/Chatto/Source/Chat Items/DummyChatItemPresenter.swift +++ b/Chatto/Source/Chat Items/DummyChatItemPresenter.swift @@ -25,12 +25,14 @@ import Foundation // Handles messages which aren't supported. So, they appear as invisible. -class DummyChatItemPresenter: UpdatableChatItemPresenterProtocol { +class DummyChatItemPresenter: ChatItemPresenterProtocol { class func registerCells(_ collectionView: UICollectionView) { collectionView.register(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message") } + let isItemUpdateSupported = true + func update(with chatItem: ChatItemProtocol) { // Does nothing } diff --git a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift index f879e282a..b128f4bcb 100644 --- a/Chatto/Source/ChatController/BaseChatViewController+Changes.swift +++ b/Chatto/Source/ChatController/BaseChatViewController+Changes.swift @@ -311,12 +311,12 @@ extension BaseChatViewController { let presenter: ChatItemPresenterProtocol = { guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] ?? oldItems[decoratedChatItem.chatItem.uid], oldChatItemCompanion.chatItem.type == decoratedChatItem.chatItem.type, - let existingPresenter = oldChatItemCompanion.presenter as? UpdatableChatItemPresenterProtocol else { + oldChatItemCompanion.presenter.isItemUpdateSupported else { return self.createPresenterForChatItem(decoratedChatItem.chatItem) } - existingPresenter.update(with: decoratedChatItem.chatItem) - return existingPresenter + oldChatItemCompanion.presenter.update(with: decoratedChatItem.chatItem) + return oldChatItemCompanion.presenter }() return ChatItemCompanion(uid: decoratedChatItem.uid, chatItem: decoratedChatItem.chatItem, presenter: presenter, decorationAttributes: decoratedChatItem.decorationAttributes) diff --git a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift index 4ed9dbf3d..9a5b9664e 100644 --- a/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift +++ b/Chatto/Tests/ChatController/BaseChatViewControllerTestHelpers.swift @@ -91,6 +91,7 @@ final class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { let presenter = FakePresenter() + presenter._isItemUpdateSupportedResult = false self.createdPresenters.append(presenter) return presenter } @@ -100,26 +101,6 @@ final class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { } } -final class FakePresenter: ChatItemPresenterProtocol { - - class func registerCells(_ collectionView: UICollectionView) { - collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell") - } - - func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { - return 10 - } - - func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { - return collectionView.dequeueReusableCell(withReuseIdentifier: "fake-cell", for: indexPath as IndexPath) - } - - func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { - let fakeCell = cell as! FakeCell - fakeCell.backgroundColor = UIColor.red - } -} - final class FakeChatItem: ChatItemProtocol { var uid: String var type: ChatItemType @@ -129,14 +110,6 @@ final class FakeChatItem: ChatItemProtocol { } } -final class FakeChatItemPresenter: ChatItemPresenterProtocol { - init() {} - static func registerCells(_ collectionView: UICollectionView) {} - func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { return 0 } - func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return UICollectionViewCell() } - func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {} -} - final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol { var onAllTasksFinished: (() -> Void)? @@ -190,7 +163,7 @@ final class FakeUpdatablePresenterBuilder: ChatItemPresenterBuilderProtocol { private(set) var createdPresenters: [ChatItemPresenterProtocol] = [] var updatedPresentersCount: Int { - return self.createdPresenters.reduce(0) { return $0 + ($1 as! FakeUpdatablePresenter)._updateWithChatItemCallsCount } + return self.createdPresenters.reduce(0) { return $0 + ($1 as! FakePresenter)._updateWithChatItemCallsCount } } func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool { @@ -198,22 +171,36 @@ final class FakeUpdatablePresenterBuilder: ChatItemPresenterBuilderProtocol { } func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { - let presenter = FakeUpdatablePresenter() + let presenter = FakePresenter() + presenter._isItemUpdateSupportedResult = true self.createdPresenters.append(presenter) return presenter } var presenterType: ChatItemPresenterProtocol.Type { - return FakeUpdatablePresenter.self + return FakePresenter.self } } -final class FakeUpdatablePresenter: UpdatableChatItemPresenterProtocol { +final class FakePresenter: ChatItemPresenterProtocol { static func registerCells(_ collectionView: UICollectionView) { collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell") } + var _isItemUpdateSupportedResult: Bool! + var isItemUpdateSupported: Bool { + return self._isItemUpdateSupportedResult + } + + private var _updateWithChatItemCalls: [(ChatItemProtocol)] = [] + var _updateWithChatItemIsCalled: Bool { return self._updateWithChatItemCallsCount > 0 } + var _updateWithChatItemCallsCount: Int { return self._updateWithChatItemCalls.count } + var _updateWithChatItemLastCallParams: ChatItemProtocol? { return self._updateWithChatItemCalls.last } + func update(with chatItem: ChatItemProtocol) { + self._updateWithChatItemCalls.append((chatItem)) + } + func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { return 10 } @@ -226,13 +213,4 @@ final class FakeUpdatablePresenter: UpdatableChatItemPresenterProtocol { let fakeCell = cell as! FakeCell fakeCell.backgroundColor = UIColor.red } - - private var _updateWithChatItemCalls: [(ChatItemProtocol)] = [] - var _updateWithChatItemIsCalled: Bool { return self._updateWithChatItemCallsCount > 0 } - var _updateWithChatItemCallsCount: Int { return self._updateWithChatItemCalls.count } - var _updateWithChatItemLastCallParams: ChatItemProtocol? { return self._updateWithChatItemCalls.last } - - func update(with chatItem: ChatItemProtocol) { - self._updateWithChatItemCalls.append((chatItem)) - } } diff --git a/Chatto/Tests/ChatItemCompanionCollectionTests.swift b/Chatto/Tests/ChatItemCompanionCollectionTests.swift index 4619e90ff..dced1e715 100644 --- a/Chatto/Tests/ChatItemCompanionCollectionTests.swift +++ b/Chatto/Tests/ChatItemCompanionCollectionTests.swift @@ -31,7 +31,7 @@ class ChatItemCompanionCollectionTests: XCTestCase { override func setUp() { super.setUp() - let fakeChatItemPresenter = FakeChatItemPresenter() + let fakeChatItemPresenter = FakePresenter() let items = [ ChatItemCompanion(uid: "3", chatItem: FakeChatItem(uid: "3", type: "type3"), presenter: fakeChatItemPresenter, decorationAttributes: nil), ChatItemCompanion(uid: "1", chatItem: FakeChatItem(uid: "1", type: "type1"), presenter: fakeChatItemPresenter, decorationAttributes: nil), diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift index aa1609128..15e9b3129 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessagePresenter.swift @@ -72,7 +72,17 @@ open class BaseMessagePresenter // Cell registration is happening lazily, right before the moment when a cell is dequeued. } + open override var isItemUpdateSupported: Bool { + return true + } + open override func update(with chatItem: ChatItemProtocol) { let oldMessageModel = self.messageModel super.update(with: chatItem) diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift index e8db52d99..42737de80 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/PhotoMessagePresenter.swift @@ -57,6 +57,10 @@ open class PhotoMessagePresenter collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message") } + open override var isItemUpdateSupported: Bool { + return true + } + public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell(withReuseIdentifier: "photo-message", for: indexPath) } diff --git a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift index 23f83d877..fd527dc1d 100644 --- a/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift +++ b/ChattoAdditions/Source/Chat Items/TextMessages/TextMessagePresenter.swift @@ -63,6 +63,10 @@ open class TextMessagePresenter collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming") } + open override var isItemUpdateSupported: Bool { + return true + } + public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let identifier = self.messageViewModel.isIncoming ? "text-message-incoming" : "text-message-outcoming" return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) diff --git a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift index bbc7a3405..0896cdfb3 100644 --- a/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Chat Items/Sending status/SendingStatusPresenter.swift @@ -73,6 +73,10 @@ class SendingStatusPresenter: ChatItemPresenterProtocol { collectionView.register(UINib(nibName: "SendingStatusCollectionViewCell", bundle: Bundle(for: self)), forCellWithReuseIdentifier: "SendingStatusCollectionViewCell") } + let isItemUpdateSupported = false + + func update(with chatItem: ChatItemProtocol) {} + func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SendingStatusCollectionViewCell", for: indexPath) return cell diff --git a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift index 8ac49ebc1..28d135de1 100644 --- a/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift +++ b/ChattoApp/ChattoApp/Source/Time Separator/TimeSeparatorPresenter.swift @@ -55,6 +55,10 @@ class TimeSeparatorPresenter: ChatItemPresenterProtocol { collectionView.register(TimeSeparatorCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier) } + let isItemUpdateSupported = false + + func update(with chatItem: ChatItemProtocol) {} + func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell(withReuseIdentifier: TimeSeparatorPresenter.cellReuseIdentifier, for: indexPath) } diff --git a/ChattoApp/Podfile.lock b/ChattoApp/Podfile.lock index b4b8eee0b..1c9e21c4d 100644 --- a/ChattoApp/Podfile.lock +++ b/ChattoApp/Podfile.lock @@ -19,4 +19,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7e673449d94d383ef7c7669fdd0f282d4d0e7059 -COCOAPODS: 1.7.0 +COCOAPODS: 1.7.5 diff --git a/ChattoApp/Pods/Manifest.lock b/ChattoApp/Pods/Manifest.lock index b4b8eee0b..1c9e21c4d 100644 --- a/ChattoApp/Pods/Manifest.lock +++ b/ChattoApp/Pods/Manifest.lock @@ -19,4 +19,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7e673449d94d383ef7c7669fdd0f282d4d0e7059 -COCOAPODS: 1.7.0 +COCOAPODS: 1.7.5 diff --git a/ChattoApp/Pods/Pods.xcodeproj/project.pbxproj b/ChattoApp/Pods/Pods.xcodeproj/project.pbxproj index 79b687870..9135bf99d 100644 --- a/ChattoApp/Pods/Pods.xcodeproj/project.pbxproj +++ b/ChattoApp/Pods/Pods.xcodeproj/project.pbxproj @@ -179,7 +179,6 @@ 29BD3214305662ACBC467BCD1ABB209E /* ChattoAdditions.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = ChattoAdditions.modulemap; sourceTree = ""; }; 2B46F5A7097A8D5F85D3281D141699D3 /* ChatItemCompanionCollection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChatItemCompanionCollection.swift; path = Chatto/Source/ChatItemCompanionCollection.swift; sourceTree = ""; }; 2F0B76D2F736C344E13D10C997980B0A /* Chatto.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = Chatto.h; path = Chatto/Source/Chatto.h; sourceTree = ""; }; - 2F29AA142328F2381411938E09BA15BA /* Chatto.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Chatto.framework; path = Chatto.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 324271A49F8E11A4C6DF72FBEA3821C7 /* CompoundBubbleLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CompoundBubbleLayout.swift; sourceTree = ""; }; 3576DA91CD89C6E7604B0AD9A0026DC3 /* BaseChatViewController+Scrolling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "BaseChatViewController+Scrolling.swift"; sourceTree = ""; }; 37B504583281C64D32A4ECC33CDECFEB /* ChattoAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ChattoAdditions.h; path = ChattoAdditions/Source/ChattoAdditions.h; sourceTree = ""; }; @@ -203,7 +202,6 @@ 61CFE007BEBD29AAB7543E6974D96337 /* ExpandableTextView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExpandableTextView.swift; sourceTree = ""; }; 67D6FC127B130691140A3CDE4A4CBD55 /* BaseMessageViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BaseMessageViewModel.swift; sourceTree = ""; }; 6A210D135E0A553455CC4E638627769A /* Chatto.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Chatto.xcconfig; sourceTree = ""; }; - 6AB4A84DD385F7D9EC9EBC2D62DF112A /* Pods_ChattoApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_ChattoApp.framework; path = "Pods-ChattoApp.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA6B249C9BE06C9A111CE06D36678EB /* AccessoryViewRevealer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccessoryViewRevealer.swift; sourceTree = ""; }; 6F284074807EBD02656E1B9678B3FA42 /* Pods-ChattoApp-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-ChattoApp-umbrella.h"; sourceTree = ""; }; 732356F84EDBE1096CE1FB1C5C92D551 /* KeyboardTracker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = KeyboardTracker.swift; sourceTree = ""; }; @@ -236,6 +234,7 @@ 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 9DB17BD0316619AAA5CD83DBEF1AA8A9 /* PhotosInputPlaceholderCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PhotosInputPlaceholderCell.swift; sourceTree = ""; }; 9EB60E8BFF53415DFEA506750AD00DDC /* PhotosChatInputItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PhotosChatInputItem.swift; sourceTree = ""; }; + A3268B957AD18F7B3950012DFE6FFC88 /* Pods_ChattoApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_ChattoApp.framework; path = "Pods-ChattoApp.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; A76EF42E77A7A2C156BF0D942CC73E3D /* HashableRepresentible.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HashableRepresentible.swift; sourceTree = ""; }; A9C7F1F168BDF3AFBAB46F9F058C1505 /* TextMessageCollectionViewCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TextMessageCollectionViewCell.swift; sourceTree = ""; }; AA42A74330676EB2A228F23EF0732594 /* PhotoMessageViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PhotoMessageViewModel.swift; sourceTree = ""; }; @@ -251,7 +250,9 @@ B75F185F9186FCFDD9678D5DBE54417C /* CGRect+Additions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "CGRect+Additions.swift"; sourceTree = ""; }; BD5D3AA17976519CE895E962C3464677 /* SerialTaskQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SerialTaskQueue.swift; path = Chatto/Source/SerialTaskQueue.swift; sourceTree = ""; }; BDF42E93F8186B5248D722A8CA548EF3 /* BaseChatViewController+AccessoryViewRevealer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "BaseChatViewController+AccessoryViewRevealer.swift"; sourceTree = ""; }; + BE3893C08724F3D82CF1D199CF98F274 /* Chatto.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Chatto.framework; path = Chatto.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C31EDB90BA1E733F2706E722923E129A /* Pods-ChattoApp.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-ChattoApp.modulemap"; sourceTree = ""; }; + C3B22A268DB72CE1BC85F17F9ECA20C2 /* ChattoAdditions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = ChattoAdditions.framework; path = ChattoAdditions.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C40974BFDC85D8311DD13B06BFFE5A50 /* ChatCollectionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewLayout.swift; sourceTree = ""; }; C572B372C172D822A78E3E606668E89A /* ChattoAdditions-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ChattoAdditions-prefix.pch"; sourceTree = ""; }; C595FBCAFF3F5E59E4E119A702168AD5 /* ViewDefinitions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ViewDefinitions.swift; sourceTree = ""; }; @@ -272,7 +273,6 @@ DC9B1744ECAC48DE6F275A77E00B36D7 /* ImagePicker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; DDD44D9595EFB2A47A204B63F58F51FC /* PhotoMessageAssets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; path = PhotoMessageAssets.xcassets; sourceTree = ""; }; DE6249248EF4B0EDB20CB914BF069140 /* InputPositionControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InputPositionControlling.swift; sourceTree = ""; }; - E003633729D7EA8319DF1770BFF4AD5D /* ChattoAdditions.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = ChattoAdditions.framework; path = ChattoAdditions.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E231744704984B2CE53B4D6B20451D07 /* Pods-ChattoApp-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ChattoApp-Info.plist"; sourceTree = ""; }; E32EFB3EED4724CE133603CF2DCF1157 /* PhotosInputWithPlaceholdersDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PhotosInputWithPlaceholdersDataProvider.swift; sourceTree = ""; }; E33848132BADA8CC1DA30DFEF190C47E /* CompoundMessagePresenterBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CompoundMessagePresenterBuilder.swift; sourceTree = ""; }; @@ -836,9 +836,9 @@ FC106E74D3276542F24B5DC027CB1A53 /* Products */ = { isa = PBXGroup; children = ( - 2F29AA142328F2381411938E09BA15BA /* Chatto.framework */, - E003633729D7EA8319DF1770BFF4AD5D /* ChattoAdditions.framework */, - 6AB4A84DD385F7D9EC9EBC2D62DF112A /* Pods_ChattoApp.framework */, + BE3893C08724F3D82CF1D199CF98F274 /* Chatto.framework */, + C3B22A268DB72CE1BC85F17F9ECA20C2 /* ChattoAdditions.framework */, + A3268B957AD18F7B3950012DFE6FFC88 /* Pods_ChattoApp.framework */, ); name = Products; sourceTree = ""; @@ -910,7 +910,7 @@ ); name = Chatto; productName = Chatto; - productReference = 2F29AA142328F2381411938E09BA15BA /* Chatto.framework */; + productReference = BE3893C08724F3D82CF1D199CF98F274 /* Chatto.framework */; productType = "com.apple.product-type.framework"; }; 9A88D54DB316ADF1E80363324718D63E /* ChattoAdditions */ = { @@ -929,7 +929,7 @@ ); name = ChattoAdditions; productName = ChattoAdditions; - productReference = E003633729D7EA8319DF1770BFF4AD5D /* ChattoAdditions.framework */; + productReference = C3B22A268DB72CE1BC85F17F9ECA20C2 /* ChattoAdditions.framework */; productType = "com.apple.product-type.framework"; }; B3A80E083011B2BEB0DBECEE9019EEBA /* Pods-ChattoApp */ = { @@ -949,7 +949,7 @@ ); name = "Pods-ChattoApp"; productName = "Pods-ChattoApp"; - productReference = 6AB4A84DD385F7D9EC9EBC2D62DF112A /* Pods_ChattoApp.framework */; + productReference = A3268B957AD18F7B3950012DFE6FFC88 /* Pods_ChattoApp.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */