Skip to content

Commit 26a8d90

Browse files
committed
Improved the interItemSpacing support and fixed an issue when they new value of the interItemSpacing may not porpagate to the model.
1 parent 5835c3c commit 26a8d90

File tree

7 files changed

+73
-23
lines changed

7 files changed

+73
-23
lines changed

ChatLayout.podspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'ChatLayout'
3-
s.version = '1.3.4'
3+
s.version = '1.3.5'
44
s.summary = 'Chat UI Library. It uses custom UICollectionViewLayout to provide you full control over the presentation.'
55
s.swift_version = '5.8'
66

ChatLayout/Classes/Core/ChatLayoutAttributes.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public final class ChatLayoutAttributes: UICollectionViewLayoutAttributes {
1919
/// Alignment of the current item. Can be changed within `UICollectionViewCell.preferredLayoutAttributesFitting(...)`
2020
public var alignment: ChatItemAlignment = .fullWidth
2121

22+
/// Inter item spacing. Can be changed within `UICollectionViewCell.preferredLayoutAttributesFitting(...)`
23+
public var interItemSpacing: CGFloat = 0
24+
2225
/// `CollectionViewChatLayout`s additional insets setup using `ChatLayoutSettings`. Added for convenience.
2326
public internal(set) var additionalInsets: UIEdgeInsets = .zero
2427

@@ -54,6 +57,7 @@ public final class ChatLayoutAttributes: UICollectionViewLayoutAttributes {
5457
let copy = super.copy(with: zone) as! ChatLayoutAttributes
5558
copy.viewSize = viewSize
5659
copy.alignment = alignment
60+
copy.interItemSpacing = interItemSpacing
5761
copy.layoutFrame = layoutFrame
5862
copy.additionalInsets = additionalInsets
5963
copy.visibleBoundsSize = visibleBoundsSize
@@ -66,7 +70,9 @@ public final class ChatLayoutAttributes: UICollectionViewLayoutAttributes {
6670

6771
/// Returns a Boolean value indicating whether two `ChatLayoutAttributes` are considered equal.
6872
public override func isEqual(_ object: Any?) -> Bool {
69-
super.isEqual(object) && alignment == (object as? ChatLayoutAttributes)?.alignment
73+
super.isEqual(object)
74+
&& alignment == (object as? ChatLayoutAttributes)?.alignment
75+
&& interItemSpacing == (object as? ChatLayoutAttributes)?.interItemSpacing
7076
}
7177

7278
/// `ItemKind` represented by this attributes object.

ChatLayout/Classes/Core/CollectionViewChatLayout.swift

+12-7
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,9 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
304304

305305
/// If you want to use new `UICollectionView.reconfigureItems(..)` api and expect the reconfiguration to happen animated as well
306306
/// - you must call this method next to the `UICollectionView` one. `UIKit` in its classic way uses private API to process it.
307+
///
308+
/// NB: Reconfigure items is not exposed to the layout, it may behave strange and if you experience something like
309+
/// this - move to the `UICollectionView.reloadItems(..)` as a safer option.
307310
public func reconfigureItems(at indexPaths: [IndexPath]) {
308311
reconfigureItemsIndexPaths = indexPaths
309312
}
@@ -514,7 +517,7 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
514517
return true
515518
}
516519

517-
let shouldInvalidateLayout = item.calculatedSize == nil || item.alignment != preferredMessageAttributes.alignment
520+
let shouldInvalidateLayout = item.calculatedSize == nil || item.alignment != preferredMessageAttributes.alignment || item.interItemSpacing != preferredMessageAttributes.interItemSpacing
518521

519522
return shouldInvalidateLayout
520523
}
@@ -535,12 +538,14 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
535538
let layoutAttributesForPendingAnimation = attributesForPendingAnimations[preferredMessageAttributes.kind]?[preferredAttributesItemPath]
536539

537540
let newItemSize = itemSize(with: preferredMessageAttributes)
538-
let newInterItemSpacing = interItemSpacing(for: preferredMessageAttributes.kind, at: preferredMessageAttributes.indexPath)
541+
let newInterItemSpacing: CGFloat
539542
let newItemAlignment: ChatItemAlignment
540-
if controller.reloadedIndexes.contains(preferredMessageAttributes.indexPath) || reconfigureItemsIndexPaths.contains(preferredMessageAttributes.indexPath) {
543+
if controller.reloadedIndexes.contains(preferredMessageAttributes.indexPath) || controller.reconfiguredIndexes.contains(preferredMessageAttributes.indexPath) {
541544
newItemAlignment = alignment(for: preferredMessageAttributes.kind, at: preferredMessageAttributes.indexPath)
545+
newInterItemSpacing = interItemSpacing(for: preferredMessageAttributes.kind, at: preferredMessageAttributes.indexPath)
542546
} else {
543547
newItemAlignment = preferredMessageAttributes.alignment
548+
newInterItemSpacing = preferredMessageAttributes.interItemSpacing
544549
}
545550
controller.update(preferredSize: newItemSize,
546551
alignment: newItemAlignment,
@@ -694,7 +699,8 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
694699

695700
/// Notifies the layout object that the contents of the collection view are about to change.
696701
public override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
697-
let changeItems = updateItems.compactMap { ChangeItem(with: $0) }
702+
var changeItems = updateItems.compactMap { ChangeItem(with: $0) }
703+
changeItems.append(contentsOf: reconfigureItemsIndexPaths.map { .itemReconfigure(itemIndexPath: $0) })
698704
controller.process(changeItems: changeItems)
699705
state = .afterUpdate
700706
dontReturnAttributes = false
@@ -743,7 +749,6 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
743749
}
744750

745751
prepareActions.formUnion(.switchStates)
746-
747752
super.finalizeCollectionViewUpdates()
748753
}
749754

@@ -771,7 +776,7 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
771776
attributes = controller.itemAttributes(for: initialIndexPath, kind: .cell, at: .beforeUpdate)?.typedCopy() ?? ChatLayoutAttributes(forCellWith: itemIndexPath)
772777
attributes?.indexPath = itemIndexPath
773778
if #unavailable(iOS 13.0) {
774-
if controller.reloadedIndexes.contains(itemIndexPath) || controller.reloadedSectionsIndexes.contains(itemPath.section) {
779+
if controller.reloadedIndexes.contains(itemIndexPath) || controller.reconfiguredIndexes.contains(itemIndexPath) || controller.reloadedSectionsIndexes.contains(itemPath.section) {
775780
// It is needed to position the new cell in the middle of the old cell on ios 12
776781
attributesForPendingAnimations[.cell]?[itemPath] = attributes
777782
}
@@ -810,7 +815,7 @@ public final class CollectionViewChatLayout: UICollectionViewLayout {
810815
} else if let itemIdentifier = controller.itemIdentifier(for: itemPath, kind: .cell, at: .beforeUpdate),
811816
let finalIndexPath = controller.itemPath(by: itemIdentifier, kind: .cell, at: .afterUpdate) {
812817
if controller.movedIndexes.contains(itemIndexPath) || controller.movedSectionsIndexes.contains(itemPath.section) ||
813-
controller.reloadedIndexes.contains(itemIndexPath) || controller.reloadedSectionsIndexes.contains(itemPath.section) {
818+
controller.reloadedIndexes.contains(itemIndexPath) || controller.reconfiguredIndexes.contains(itemIndexPath) || controller.reloadedSectionsIndexes.contains(itemPath.section) {
814819
attributes = controller.itemAttributes(for: finalIndexPath, kind: .cell, at: .afterUpdate)?.typedCopy()
815820
} else {
816821
attributes = controller.itemAttributes(for: itemPath, kind: .cell, at: .beforeUpdate)?.typedCopy()

ChatLayout/Classes/Core/Model/ChangeItem.swift

+13-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ enum ChangeItem: Equatable {
3434
/// Reload item at `itemIndexPath`
3535
case itemReload(itemIndexPath: IndexPath)
3636

37+
/// Reconfigure item at `itemIndexPath`
38+
case itemReconfigure(itemIndexPath: IndexPath)
39+
3740
/// Move section from `initialSectionIndex` to `finalSectionIndex`
3841
case sectionMove(initialSectionIndex: Int, finalSectionIndex: Int)
3942

@@ -102,18 +105,20 @@ enum ChangeItem: Equatable {
102105
return 0
103106
case .itemReload:
104107
return 1
105-
case .sectionDelete:
108+
case .itemReconfigure:
106109
return 2
107-
case .itemDelete:
110+
case .sectionDelete:
108111
return 3
109-
case .sectionInsert:
112+
case .itemDelete:
110113
return 4
111-
case .itemInsert:
114+
case .sectionInsert:
112115
return 5
113-
case .sectionMove:
116+
case .itemInsert:
114117
return 6
115-
case .itemMove:
118+
case .sectionMove:
116119
return 7
120+
case .itemMove:
121+
return 8
117122
}
118123
}
119124

@@ -135,6 +140,8 @@ extension ChangeItem: Comparable {
135140
return lIndex < rIndex
136141
case let (.itemReload(itemIndexPath: lIndexPath), .itemReload(itemIndexPath: rIndexPath)):
137142
return lIndexPath < rIndexPath
143+
case let (.itemReconfigure(itemIndexPath: lIndexPath), .itemReconfigure(itemIndexPath: rIndexPath)):
144+
return lIndexPath < rIndexPath
138145
case let (.sectionMove(initialSectionIndex: lInitialSectionIndex, finalSectionIndex: lFinalSectionIndex),
139146
.sectionMove(initialSectionIndex: rInitialSectionIndex, finalSectionIndex: rFinalSectionIndex)):
140147
if lInitialSectionIndex == rInitialSectionIndex {

ChatLayout/Classes/Core/Model/StateController.swift

+32
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ final class StateController<Layout: ChatLayoutRepresentation> {
8989

9090
private(set) var reloadedIndexes: Set<IndexPath> = []
9191

92+
private(set) var reconfiguredIndexes: Set<IndexPath> = []
93+
9294
private(set) var insertedIndexes: Set<IndexPath> = []
9395

9496
private(set) var movedIndexes: Set<IndexPath> = []
@@ -238,6 +240,7 @@ final class StateController<Layout: ChatLayoutRepresentation> {
238240
attributes.indexPath = itemIndexPath
239241
attributes.zIndex = 10
240242
attributes.alignment = item.alignment
243+
attributes.interItemSpacing = item.interItemSpacing
241244
case .footer:
242245
guard itemPath.section < layout.sections.count,
243246
itemPath.item == 0 else {
@@ -265,6 +268,7 @@ final class StateController<Layout: ChatLayoutRepresentation> {
265268
attributes.indexPath = itemIndexPath
266269
attributes.zIndex = 10
267270
attributes.alignment = item.alignment
271+
attributes.interItemSpacing = item.interItemSpacing
268272
case .cell:
269273
guard itemPath.section < layout.sections.count,
270274
itemPath.item < layout.sections[itemPath.section].items.count else {
@@ -292,6 +296,7 @@ final class StateController<Layout: ChatLayoutRepresentation> {
292296
attributes.indexPath = itemIndexPath
293297
attributes.zIndex = 0
294298
attributes.alignment = item.alignment
299+
attributes.interItemSpacing = item.interItemSpacing
295300
}
296301
attributes.viewSize = additionalAttributes.viewSize
297302
attributes.adjustedContentInsets = additionalAttributes.adjustedContentInsets
@@ -489,6 +494,7 @@ final class StateController<Layout: ChatLayoutRepresentation> {
489494
func process(changeItems: [ChangeItem]) {
490495
func applyConfiguration(_ configuration: ItemModel.Configuration, to item: inout ItemModel) {
491496
item.alignment = configuration.alignment
497+
item.interItemSpacing = configuration.interItemSpacing
492498
if let calculatedSize = configuration.calculatedSize {
493499
item.calculatedSize = calculatedSize
494500
item.calculatedOnce = true
@@ -600,6 +606,15 @@ final class StateController<Layout: ChatLayoutRepresentation> {
600606
applyConfiguration(configuration, to: &item)
601607
afterUpdateModel.replaceItem(item, at: indexPath)
602608
reloadedIndexes.insert(indexPath)
609+
case let .itemReconfigure(itemIndexPath: indexPath):
610+
guard var item = item(for: indexPath.itemPath, kind: .cell, at: .beforeUpdate) else {
611+
assertionFailure("Item at index path (\(indexPath.section) - \(indexPath.item)) does not exist.")
612+
return
613+
}
614+
let configuration = layoutRepresentation.configuration(for: .cell, at: indexPath)
615+
applyConfiguration(configuration, to: &item)
616+
afterUpdateModel.replaceItem(item, at: indexPath)
617+
reconfiguredIndexes.insert(indexPath)
603618
case let .sectionMove(initialSectionIndex: initialSectionIndex, finalSectionIndex: finalSectionIndex):
604619
let section = layoutBeforeUpdate.sections[initialSectionIndex]
605620
movedSectionsIndexes.insert(finalSectionIndex)
@@ -671,6 +686,22 @@ final class StateController<Layout: ChatLayoutRepresentation> {
671686
newSpacing: newItem.interItemSpacing),
672687
visibleBounds: visibleBounds)
673688
}
689+
reconfiguredIndexes.sorted(by: { $0 < $1 }).forEach {
690+
let newItemPath = $0.itemPath
691+
guard let oldItem = item(for: newItemPath, kind: .cell, at: .beforeUpdate),
692+
let newItemIndexPath = itemPath(by: oldItem.id, kind: .cell, at: .afterUpdate),
693+
let newItem = item(for: newItemIndexPath, kind: .cell, at: .afterUpdate) else {
694+
assertionFailure("Internal inconsistency.")
695+
return
696+
}
697+
compensateOffsetIfNeeded(for: newItemPath,
698+
kind: .cell,
699+
action: .frameUpdate(previousFrame: oldItem.frame,
700+
newFrame: newItem.frame,
701+
previousSpacing: oldItem.interItemSpacing,
702+
newSpacing: newItem.interItemSpacing),
703+
visibleBounds: visibleBounds)
704+
}
674705
insertedIndexes.sorted(by: { $0 < $1 }).forEach {
675706
let itemPath = $0.itemPath
676707
guard let item = item(for: itemPath, kind: .cell, at: .afterUpdate) else {
@@ -702,6 +733,7 @@ final class StateController<Layout: ChatLayoutRepresentation> {
702733
insertedSectionsIndexes = []
703734

704735
reloadedIndexes = []
736+
reconfiguredIndexes = []
705737
reloadedSectionsIndexes = []
706738

707739
movedIndexes = []

Example/ChatLayout/AppDelegate.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import UIKit
1414

15-
@UIApplicationMain
15+
@main
1616
class AppDelegate: UIResponder, UIApplicationDelegate {
1717

1818
var window: UIWindow?

Example/Podfile.lock

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
PODS:
2-
- ChatLayout (1.3.4):
3-
- ChatLayout/Ultimate (= 1.3.4)
4-
- ChatLayout/Core (1.3.4)
5-
- ChatLayout/Extras (1.3.4):
2+
- ChatLayout (1.3.5):
3+
- ChatLayout/Ultimate (= 1.3.5)
4+
- ChatLayout/Core (1.3.5)
5+
- ChatLayout/Extras (1.3.5):
66
- ChatLayout/Core
7-
- ChatLayout/Ultimate (1.3.4):
7+
- ChatLayout/Ultimate (1.3.5):
88
- ChatLayout/Core
99
- ChatLayout/Extras
1010
- DifferenceKit (1.3.0):
@@ -35,11 +35,11 @@ EXTERNAL SOURCES:
3535
:path: "../"
3636

3737
SPEC CHECKSUMS:
38-
ChatLayout: e07c2fd8ac2ba7a78684e5a8aa32291eb746cab1
38+
ChatLayout: ae545bdb36b4872d776ce800d840cd630a71f979
3939
DifferenceKit: ab185c4d7f9cef8af3fcf593e5b387fb81e999ca
4040
FPSCounter: 884afec377de66637808c4f52ecc3b85a404732b
4141
InputBarAccessoryView: 1d7b0a672b36e370f01f264b3907ef39d03328e3
4242

4343
PODFILE CHECKSUM: 0bbac6c60b293f3e90bba25beda75886a6fbcb05
4444

45-
COCOAPODS: 1.12.1
45+
COCOAPODS: 1.13.0

0 commit comments

Comments
 (0)