@@ -180,6 +180,7 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
180180 static let cachePreviousWidth = PrepareActions ( rawValue: 1 << 2 )
181181 static let cachePreviousContentInsets = PrepareActions ( rawValue: 1 << 3 )
182182 static let switchStates = PrepareActions ( rawValue: 1 << 4 )
183+ static let updatePinnedInfo = PrepareActions ( rawValue: 1 << 5 )
183184 }
184185
185186 private struct InvalidationActions : OptionSet {
@@ -219,8 +220,6 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
219220
220221 private var _supportSelfSizingInvalidation : Bool = false
221222
222- var hasPinnedHeaderOrFooter : Bool = false
223-
224223 // MARK: IOS 15.1 fix flags
225224
226225 private var needsIOS15_1IssueFix : Bool {
@@ -331,6 +330,11 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
331330 reconfigureItemsIndexPaths = indexPaths
332331 }
333332
333+ /// Returns index path of currently pinned item.
334+ open func indexPathForItePinnedAt( _ pinningType: ChatItemPinningType ) -> IndexPath ? {
335+ controller. pinnedIndexPaths [ pinningType] ? . current
336+ }
337+
334338 // MARK: Providing Layout Attributes
335339
336340 /// Tells the layout object to update the current layout.
@@ -350,10 +354,6 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
350354 contentOffsetBeforeUpdate = nil
351355 }
352356
353- if prepareActions. contains ( . updateLayoutMetrics) || prepareActions. contains ( . recreateSectionModels) {
354- hasPinnedHeaderOrFooter = false
355- }
356-
357357 if prepareActions. contains ( . recreateSectionModels) {
358358 var sections : ContiguousArray < SectionModel < CollectionViewChatLayout > > = [ ]
359359 for sectionIndex in 0 ..< collectionView. numberOfSections {
@@ -386,8 +386,6 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
386386 footer: footer,
387387 items: items,
388388 collectionLayout: self )
389- section. set ( shouldPinHeaderToVisibleBounds: shouldPinHeaderToVisibleBounds ( at: sectionIndex) )
390- section. set ( shouldPinFooterToVisibleBounds: shouldPinFooterToVisibleBounds ( at: sectionIndex) )
391389 section. assembleLayout ( )
392390 sections. append ( section)
393391 }
@@ -438,6 +436,12 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
438436 cachedCollectionViewSize = collectionView. bounds. size
439437 }
440438
439+ if prepareActions. contains ( . updatePinnedInfo) ||
440+ prepareActions. contains ( . updateLayoutMetrics) ||
441+ prepareActions. contains ( . recreateSectionModels) {
442+ controller. updatePinnedInfo ( at: state)
443+ }
444+
441445 prepareActions = [ ]
442446 }
443447
@@ -478,7 +482,7 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
478482 guard !dontReturnAttributes else {
479483 return nil
480484 }
481- let attributes = controller. itemAttributes ( for: indexPath. itemPath, kind: . cell, at: state)
485+ let attributes = controller. itemAttributes ( for: indexPath. itemPath, kind: . cell, at: state, withPinnning : true )
482486 return attributes
483487 }
484488
@@ -490,7 +494,7 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
490494 }
491495
492496 let kind = ItemKind ( elementKind)
493- let attributes = controller. itemAttributes ( for: indexPath. itemPath, kind: kind, at: state)
497+ let attributes = controller. itemAttributes ( for: indexPath. itemPath, kind: kind, at: state, withPinnning : true )
494498
495499 return attributes
496500 }
@@ -512,6 +516,7 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
512516 let newBounds = collectionView. bounds
513517 let heightDifference = oldBounds. height - newBounds. height
514518 controller. proposedCompensatingOffset += heightDifference + ( oldBounds. origin. y - newBounds. origin. y)
519+ controller. updatePinnedInfo ( at: state)
515520 }
516521
517522 /// Cleans up after any animated changes to the view’s bounds or after the insertion or deletion of items.
@@ -542,6 +547,7 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
542547 || ( _supportSelfSizingInvalidation ? ( item. size. height - preferredMessageAttributes. size. height) . rounded ( ) != 0 : false )
543548 || item. alignment != preferredMessageAttributes. alignment
544549 || item. interItemSpacing != preferredMessageAttributes. interItemSpacing
550+ || item. pinningType != preferredMessageAttributes. pinningType
545551
546552 return shouldInvalidateLayout
547553 }
@@ -566,9 +572,11 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
566572 let newItemSize = itemSize ( with: preferredMessageAttributes)
567573 let newItemAlignment = alignment ( for: preferredMessageAttributes. kind, at: preferredMessageAttributes. indexPath)
568574 let newInterItemSpacing = interItemSpacing ( for: preferredMessageAttributes. kind, at: preferredMessageAttributes. indexPath)
575+ let newPinningType = pinningType ( for: preferredMessageAttributes. kind, at: preferredMessageAttributes. indexPath)
569576 controller. update ( preferredSize: newItemSize,
570577 alignment: newItemAlignment,
571578 interItemSpacing: newInterItemSpacing,
579+ pinningType: newPinningType,
572580 for: preferredAttributesItemPath,
573581 kind: preferredMessageAttributes. kind,
574582 at: state)
@@ -578,7 +586,9 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
578586 let heightDifference = newItemSize. height - originalAttributes. size. height
579587 let isAboveBottomEdge = originalAttributes. frame. minY. rounded ( ) <= visibleBounds. maxY. rounded ( )
580588
589+ let shouldApplyCompensations = !controller. isPinnedItem ( indexPath: preferredMessageAttributes. indexPath, kind: preferredMessageAttributes. kind)
581590 if heightDifference != 0 ,
591+ shouldApplyCompensations,
582592 ( keepContentOffsetAtBottomOnBatchUpdates && controller. contentHeight ( at: state) . rounded ( ) + heightDifference > visibleBounds. height. rounded ( ) ) || isUserInitiatedScrolling,
583593 isAboveBottomEdge {
584594 let offsetCompensation : CGFloat = min ( controller. contentHeight ( at: state) - collectionView!. frame. height + adjustedContentInset. bottom + adjustedContentInset. top, heightDifference)
@@ -588,7 +598,8 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
588598
589599 if let attributes = controller. itemAttributes ( for: preferredAttributesItemPath, kind: preferredMessageAttributes. kind, at: state) ? . typedCopy ( ) {
590600 layoutAttributesForPendingAnimation? . frame = attributes. frame
591- if state == . afterUpdate {
601+ if state == . afterUpdate,
602+ shouldApplyCompensations {
592603 controller. totalProposedCompensatingOffset += heightDifference
593604 controller. offsetByTotalCompensation ( attributes: layoutAttributesForPendingAnimation, for: state, backward: true )
594605 if controller. insertedIndexes. contains ( preferredMessageAttributes. indexPath) ||
@@ -626,10 +637,11 @@ open class CollectionViewChatLayout: UICollectionViewLayout {
626637 let shouldInvalidateLayout = cachedCollectionViewSize != . some( newBounds. size) ||
627638 cachedCollectionViewInset != . some( adjustedContentInset) ||
628639 invalidationActions. contains ( . shouldInvalidateOnBoundsChange)
629- || ( isUserInitiatedScrolling && state == . beforeUpdate)
640+ || ( ( isUserInitiatedScrolling || !controller . pinnedIndexPaths . isEmpty ) && state == . beforeUpdate)
630641
631642 invalidationActions. remove ( . shouldInvalidateOnBoundsChange)
632- return shouldInvalidateLayout || hasPinnedHeaderOrFooter
643+ prepareActions. insert ( . updatePinnedInfo)
644+ return shouldInvalidateLayout
633645 }
634646
635647 /// Retrieves a context object that defines the portions of the layout that should change when a bounds change occurs.
@@ -967,7 +979,13 @@ extension CollectionViewChatLayout {
967979 } else {
968980 interItemSpacing = 0
969981 }
970- return ItemModel . Configuration ( alignment: alignment ( for: element, at: indexPath) , preferredSize: itemSize. estimated, calculatedSize: itemSize. exact, interItemSpacing: interItemSpacing)
982+ return ItemModel . Configuration (
983+ alignment: alignment ( for: element, at: indexPath) ,
984+ pinningType: pinningType ( for: element, at: indexPath) ,
985+ preferredSize: itemSize. estimated,
986+ calculatedSize: itemSize. exact,
987+ interItemSpacing: interItemSpacing
988+ )
971989 }
972990
973991 private func estimatedSize( for element: ItemKind , at indexPath: IndexPath ) -> ( estimated: CGSize , exact: CGSize ? ) {
@@ -1016,6 +1034,30 @@ extension CollectionViewChatLayout {
10161034 return delegate. alignmentForItem ( self , of: element, at: indexPath)
10171035 }
10181036
1037+ private func pinningType( for kind: ItemKind , at indexPath: IndexPath ) -> ChatItemPinningType ? {
1038+ guard let delegate else {
1039+ return nil
1040+ }
1041+ let pinningType : ChatItemPinningType ?
1042+ if kind == . cell,
1043+ settings. pinnableItems == . cells {
1044+ pinningType = delegate. pinningTypeForItem ( self , at: indexPath)
1045+ } else if settings. pinnableItems == . supplementaryViews {
1046+ if kind == . header,
1047+ delegate. shouldPinHeaderToVisibleBounds ( self , at: indexPath. section) {
1048+ pinningType = . top
1049+ } else if kind == . footer,
1050+ delegate. shouldPinFooterToVisibleBounds ( self , at: indexPath. section) {
1051+ pinningType = . bottom
1052+ } else {
1053+ pinningType = nil
1054+ }
1055+ } else {
1056+ pinningType = nil
1057+ }
1058+ return pinningType
1059+ }
1060+
10191061 private var estimatedItemSize : CGSize {
10201062 guard let estimatedItemSize = settings. estimatedItemSize else {
10211063 guard collectionView != nil else {
@@ -1056,14 +1098,6 @@ extension CollectionViewChatLayout: ChatLayoutRepresentation {
10561098 delegate? . shouldPresentFooter ( self , at: sectionIndex) ?? false
10571099 }
10581100
1059- func shouldPinHeaderToVisibleBounds( at sectionIndex: Int ) -> Bool {
1060- delegate? . shouldPinHeaderToVisibleBounds ( self , at: sectionIndex) ?? false
1061- }
1062-
1063- func shouldPinFooterToVisibleBounds( at sectionIndex: Int ) -> Bool {
1064- delegate? . shouldPinFooterToVisibleBounds ( self , at: sectionIndex) ?? false
1065- }
1066-
10671101 func interSectionSpacing( at sectionIndex: Int ) -> CGFloat {
10681102 let interItemSpacing : CGFloat
10691103 if let delegate,
0 commit comments