Slather logo

Coverage for "StateController.swift" : 83.11%

(620 of 746 relevant lines covered)

ChatLayout/Classes/Core/Model/StateController.swift

1
//
2
// ChatLayout
3
// StateController.swift
4
// https://github.com/ekazaev/ChatLayout
5
//
6
// Created by Eugene Kazaev in 2020-2022.
7
// Distributed under the MIT license.
8
//
9
// Become a sponsor:
10
// https://github.com/sponsors/ekazaev
11
//
12
13
import Foundation
14
import UIKit
15
16
/// This protocol exists only to serve an ability to unit test `StateController`.
17
protocol ChatLayoutRepresentation: AnyObject {
18
19
    var settings: ChatLayoutSettings { get }
20
21
    var viewSize: CGSize { get }
22
23
    var visibleBounds: CGRect { get }
24
25
    var layoutFrame: CGRect { get }
26
27
    var adjustedContentInset: UIEdgeInsets { get }
28
29
    var keepContentOffsetAtBottomOnBatchUpdates: Bool { get }
30
31
    var processOnlyVisibleItemsOnAnimatedBatchUpdates: Bool { get }
32
33
    func numberOfItems(in section: Int) -> Int
34
35
    func configuration(for element: ItemKind, at indexPath: IndexPath) -> ItemModel.Configuration
36
37
    func shouldPresentHeader(at sectionIndex: Int) -> Bool
38
39
    func shouldPresentFooter(at sectionIndex: Int) -> Bool
40
41
}
42
43
final class StateController<Layout: ChatLayoutRepresentation> {
44
45
    private enum CompensatingAction {
46
        case insert
47
        case delete
48
        case frameUpdate(previousFrame: CGRect, newFrame: CGRect)
49
    }
50
51
    private enum TraverseState {
52
        case notFound
53
        case found
54
        case done
55
    }
56
57
    // This thing exists here as `UICollectionView` calls `targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint)` only once at the
58
    // beginning of the animated updates. But we must compensate the other changes that happened during the update.
59
    var batchUpdateCompensatingOffset: CGFloat = 0
60
61
    var proposedCompensatingOffset: CGFloat = 0
62
63
    var totalProposedCompensatingOffset: CGFloat = 0
64
65
    var isAnimatedBoundsChange = false
66
67
    private(set) var reloadedIndexes: Set<IndexPath> = []
24x
68
69
    private(set) var insertedIndexes: Set<IndexPath> = []
24x
70
71
    private(set) var movedIndexes: Set<IndexPath> = []
24x
72
73
    private(set) var deletedIndexes: Set<IndexPath> = []
24x
74
75
    private(set) var reloadedSectionsIndexes: Set<Int> = []
24x
76
77
    private(set) var insertedSectionsIndexes: Set<Int> = []
24x
78
79
    private(set) var deletedSectionsIndexes: Set<Int> = []
24x
80
81
    private(set) var movedSectionsIndexes: Set<Int> = []
24x
82
83
    private var cachedAttributesState: (rect: CGRect, attributes: [ChatLayoutAttributes])?
84
85
    private var cachedAttributeObjects = [ModelState: [ItemKind: [ItemPath: ChatLayoutAttributes]]]()
24x
86
87
    private var layoutBeforeUpdate: LayoutModel<Layout>
88
89
    private var layoutAfterUpdate: LayoutModel<Layout>?
90
91
    private unowned var layoutRepresentation: Layout
92
93
    init(layoutRepresentation: Layout) {
24x
94
        self.layoutRepresentation = layoutRepresentation
24x
95
        layoutBeforeUpdate = LayoutModel(sections: [], collectionLayout: self.layoutRepresentation)
24x
96
        resetCachedAttributeObjects()
24x
97
    }
24x
98
99
    func set(_ sections: ContiguousArray<SectionModel<Layout>>, at state: ModelState) {
24x
100
        let layoutModel = LayoutModel(sections: sections, collectionLayout: layoutRepresentation)
24x
101
        layoutModel.assembleLayout()
24x
102
        switch state {
24x
103
        case .beforeUpdate:
24x
104
            layoutBeforeUpdate = layoutModel
24x
105
        case .afterUpdate:
24x
106
            layoutAfterUpdate = layoutModel
!
107
        }
24x
108
    }
24x
109
110
    func contentHeight(at state: ModelState) -> CGFloat {
111
        let locationHeight: CGFloat?
112
        switch state {
113
        case .beforeUpdate:
114
            locationHeight = layoutBeforeUpdate.sections.withUnsafeBufferPointer { $0.last?.locationHeight }
115
        case .afterUpdate:
116
            locationHeight = layoutAfterUpdate?.sections.withUnsafeBufferPointer { $0.last?.locationHeight }
117
        }
118
119
        guard let locationHeight = locationHeight else {
120
            return 0
10000x
121
        }
122
        return locationHeight + layoutRepresentation.settings.additionalInsets.bottom
123
    }
124
125
    func layoutAttributesForElements(in rect: CGRect,
126
                                     state: ModelState,
127
                                     ignoreCache: Bool = false) -> [ChatLayoutAttributes] {
106x
128
        let predicate: (ChatLayoutAttributes) -> ComparisonResult = { attributes in
656x
129
            if attributes.frame.intersects(rect) {
656x
130
                return .orderedSame
247x
131
            } else if attributes.frame.minY > rect.maxY {
409x
132
                return .orderedDescending
5x
133
            } else if attributes.frame.maxY < rect.minY {
404x
134
                return .orderedAscending
!
135
            }
404x
136
            return .orderedSame
404x
137
        }
404x
138
106x
139
        if !ignoreCache,
106x
140
           let cachedAttributesState = cachedAttributesState,
106x
141
           cachedAttributesState.rect.contains(rect) {
106x
142
            return cachedAttributesState.attributes.withUnsafeBufferPointer { $0.binarySearchRange(predicate: predicate) }
1x
143
        } else {
105x
144
            let totalRect: CGRect
105x
145
            switch state {
105x
146
            case .beforeUpdate:
105x
147
                totalRect = rect.inset(by: UIEdgeInsets(top: -rect.height / 2, left: -rect.width / 2, bottom: -rect.height / 2, right: -rect.width / 2))
105x
148
            case .afterUpdate:
105x
149
                totalRect = rect
!
150
            }
105x
151
            let attributes = allAttributes(at: state, visibleRect: totalRect)
105x
152
            if !ignoreCache {
105x
153
                cachedAttributesState = (rect: totalRect, attributes: attributes)
4x
154
            }
105x
155
            let visibleAttributes = rect != totalRect ? attributes.withUnsafeBufferPointer { $0.binarySearchRange(predicate: predicate) } : attributes
105x
156
            return visibleAttributes
105x
157
        }
105x
158
    }
!
159
160
    func resetCachedAttributes() {
2x
161
        cachedAttributesState = nil
2x
162
    }
2x
163
164
    func resetCachedAttributeObjects() {
67x
165
        ModelState.allCases.forEach { state in
134x
166
            resetCachedAttributeObjects(at: state)
134x
167
        }
134x
168
    }
67x
169
170
    private func resetCachedAttributeObjects(at state: ModelState) {
146x
171
        cachedAttributeObjects[state] = [:]
146x
172
        ItemKind.allCases.forEach { kind in
438x
173
            cachedAttributeObjects[state]?[kind] = [:]
438x
174
        }
438x
175
    }
146x
176
177
    func itemAttributes(for itemPath: ItemPath,
178
                        kind: ItemKind,
179
                        predefinedFrame: CGRect? = nil,
180
                        at state: ModelState,
181
                        additionalAttributes: AdditionalLayoutAttributes? = nil) -> ChatLayoutAttributes? {
772x
182
        let additionalAttributes = additionalAttributes ?? AdditionalLayoutAttributes(layoutRepresentation)
772x
183
772x
184
        let attributes: ChatLayoutAttributes
772x
185
        let itemIndexPath = itemPath.indexPath
772x
186
        let layout = layout(at: state)
772x
187
772x
188
        switch kind {
772x
189
        case .header:
772x
190
            guard itemPath.section < layout.sections.count,
13x
191
                  itemPath.item == 0 else {
13x
192
                // This occurs when getting layout attributes for initial / final animations
!
193
                return nil
!
194
            }
13x
195
            guard let headerFrame = predefinedFrame ?? itemFrame(for: itemPath, kind: kind, at: state, isFinal: true, additionalAttributes: additionalAttributes),
13x
196
                  let item = item(for: itemPath, kind: kind, at: state) else {
13x
197
                return nil
!
198
            }
13x
199
            if let cachedAttributes = cachedAttributeObjects[state]?[.header]?[itemPath] {
13x
200
                attributes = cachedAttributes
3x
201
            } else {
13x
202
                attributes = ChatLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: itemIndexPath)
10x
203
                cachedAttributeObjects[state]?[.header]?[itemPath] = attributes
10x
204
            }
13x
205
            #if DEBUG
13x
206
            attributes.id = item.id
13x
207
            #endif
13x
208
            attributes.frame = headerFrame
13x
209
            attributes.indexPath = itemIndexPath
13x
210
            attributes.zIndex = 10
13x
211
            attributes.alignment = item.alignment
13x
212
        case .footer:
772x
213
            guard itemPath.section < layout.sections.count,
9x
214
                  itemPath.item == 0 else {
9x
215
                // This occurs when getting layout attributes for initial / final animations
!
216
                return nil
!
217
            }
9x
218
            guard let footerFrame = predefinedFrame ?? itemFrame(for: itemPath, kind: kind, at: state, isFinal: true, additionalAttributes: additionalAttributes),
9x
219
                  let item = item(for: itemPath, kind: kind, at: state) else {
9x
220
                return nil
!
221
            }
9x
222
            if let cachedAttributes = cachedAttributeObjects[state]?[.footer]?[itemPath] {
9x
223
                attributes = cachedAttributes
2x
224
            } else {
9x
225
                attributes = ChatLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, with: itemIndexPath)
7x
226
                cachedAttributeObjects[state]?[.footer]?[itemPath] = attributes
7x
227
            }
9x
228
            #if DEBUG
9x
229
            attributes.id = item.id
9x
230
            #endif
9x
231
            attributes.frame = footerFrame
9x
232
            attributes.indexPath = itemIndexPath
9x
233
            attributes.zIndex = 10
9x
234
            attributes.alignment = item.alignment
9x
235
        case .cell:
772x
236
            guard itemPath.section < layout.sections.count,
750x
237
                  itemPath.item < layout.sections[itemPath.section].items.count else {
750x
238
                // This occurs when getting layout attributes for initial / final animations
!
239
                return nil
!
240
            }
750x
241
            guard let itemFrame = predefinedFrame ?? itemFrame(for: itemPath, kind: .cell, at: state, isFinal: true, additionalAttributes: additionalAttributes),
750x
242
                  let item = item(for: itemPath, kind: kind, at: state) else {
750x
243
                return nil
!
244
            }
750x
245
            if let cachedAttributes = cachedAttributeObjects[state]?[.cell]?[itemPath] {
750x
246
                attributes = cachedAttributes
413x
247
            } else {
750x
248
                attributes = ChatLayoutAttributes(forCellWith: itemIndexPath)
337x
249
                cachedAttributeObjects[state]?[.cell]?[itemPath] = attributes
337x
250
            }
750x
251
            #if DEBUG
750x
252
            attributes.id = item.id
750x
253
            #endif
750x
254
            attributes.frame = itemFrame
750x
255
            attributes.indexPath = itemIndexPath
750x
256
            attributes.zIndex = 0
750x
257
            attributes.alignment = item.alignment
750x
258
        }
772x
259
        attributes.viewSize = additionalAttributes.viewSize
772x
260
        attributes.adjustedContentInsets = additionalAttributes.adjustedContentInsets
772x
261
        attributes.visibleBoundsSize = additionalAttributes.visibleBounds.size
772x
262
        attributes.layoutFrame = additionalAttributes.layoutFrame
772x
263
        attributes.additionalInsets = additionalAttributes.additionalInsets
772x
264
        return attributes
772x
265
    }
772x
266
267
    func itemFrame(for itemPath: ItemPath, kind: ItemKind, at state: ModelState, isFinal: Bool = false, additionalAttributes: AdditionalLayoutAttributes? = nil) -> CGRect? {
268
        let additionalAttributes = additionalAttributes ?? AdditionalLayoutAttributes(layoutRepresentation)
269
        let layout = layout(at: state)
270
        guard itemPath.section < layout.sections.count else {
271
            return nil
!
272
        }
273
        guard let item = item(for: itemPath, kind: kind, at: state) else {
274
            // This occurs when getting layout attributes for initial / final animations
!
275
            return nil
!
276
        }
277
278
        let section = layout.sections[itemPath.section]
279
        var itemFrame = item.frame
280
        let dx: CGFloat
281
        let visibleBounds = additionalAttributes.visibleBounds
282
        let additionalInsets = additionalAttributes.additionalInsets
283
284
        switch item.alignment {
285
        case .leading:
286
            dx = additionalInsets.left
2x
287
        case .trailing:
288
            dx = visibleBounds.size.width - itemFrame.width - additionalInsets.right
2x
289
        case .center:
290
            let availableWidth = visibleBounds.size.width - additionalInsets.right - additionalInsets.left
5x
291
            dx = additionalInsets.left + availableWidth / 2 - itemFrame.width / 2
5x
292
        case .fullWidth:
293
            dx = additionalInsets.left
294
            itemFrame.size.width = additionalAttributes.layoutFrame.size.width
295
        }
296
297
        itemFrame.offsettingBy(dx: dx, dy: section.offsetY)
298
        if isFinal {
299
            offsetByCompensation(frame: &itemFrame, at: itemPath, for: state, backward: true)
4310x
300
        }
301
        return itemFrame
302
    }
303
304
    func itemPath(by itemId: UUID, kind: ItemKind, at state: ModelState) -> ItemPath? {
10300x
305
        layout(at: state).itemPath(by: itemId, kind: kind)
10300x
306
    }
10300x
307
308
    func sectionIdentifier(for index: Int, at state: ModelState) -> UUID? {
2x
309
        let layout = layout(at: state)
2x
310
        guard index < layout.sections.count else {
2x
311
            // This occurs when getting layout attributes for initial / final animations
!
312
            return nil
!
313
        }
2x
314
        return layout.sections[index].id
2x
315
    }
2x
316
317
    func sectionIndex(for sectionIdentifier: UUID, at state: ModelState) -> Int? {
5x
318
        guard let sectionIndex = layout(at: state).sectionIndex(by: sectionIdentifier) else {
5x
319
            // This occurs when getting layout attributes for initial / final animations
!
320
            return nil
!
321
        }
5x
322
        return sectionIndex
5x
323
    }
5x
324
325
    func section(at index: Int, at state: ModelState) -> SectionModel<Layout> {
10x
326
        #if DEBUG
10x
327
        guard index < layout(at: state).sections.count else {
10x
328
            preconditionFailure("Section index \(index) is bigger than the amount of sections \(layout(at: state).sections.count).")
!
329
        }
10x
330
        #endif
10x
331
        return layout(at: state).sections[index]
10x
332
    }
10x
333
334
    func itemIdentifier(for itemPath: ItemPath, kind: ItemKind, at state: ModelState) -> UUID? {
335
        let layout = layout(at: state)
336
        guard itemPath.section < layout.sections.count else {
337
            // This occurs when getting layout attributes for initial / final animations
2x
338
            return nil
2x
339
        }
340
        let sectionModel = layout.sections[itemPath.section]
341
        switch kind {
342
        case .cell:
343
            guard itemPath.item < layout.sections[itemPath.section].items.count else {
344
                // This occurs when getting layout attributes for initial / final animations
3x
345
                return nil
3x
346
            }
347
            let rowModel = sectionModel.items[itemPath.item]
348
            return rowModel.id
349
        case .header, .footer:
350
            guard let item = item(for: ItemPath(item: 0, section: itemPath.section), kind: kind, at: state) else {
32x
351
                return nil
4x
352
            }
28x
353
            return item.id
28x
354
        }
355
356
    }
357
358
    func numberOfSections(at state: ModelState) -> Int {
4x
359
        layout(at: state).sections.count
4x
360
    }
4x
361
362
    func numberOfItems(in sectionIndex: Int, at state: ModelState) -> Int {
28x
363
        layout(at: state).sections[sectionIndex].items.count
28x
364
    }
28x
365
366
    func item(for itemPath: ItemPath, kind: ItemKind, at state: ModelState) -> ItemModel? {
367
        let layout = layout(at: state)
368
        switch kind {
369
        case .header:
370
            guard itemPath.section < layout.sections.count,
263x
371
                  itemPath.item == 0 else {
263x
372
                // This occurs when getting layout attributes for initial / final animations
1x
373
                return nil
1x
374
            }
262x
375
            guard let header = layout.sections[itemPath.section].header else {
262x
376
                return nil
3x
377
            }
259x
378
            return header
259x
379
        case .footer:
380
            guard itemPath.section < layout.sections.count,
147x
381
                  itemPath.item == 0 else {
147x
382
                // This occurs when getting layout attributes for initial / final animations
1x
383
                return nil
1x
384
            }
146x
385
            guard let footer = layout.sections[itemPath.section].footer else {
146x
386
                return nil
1x
387
            }
145x
388
            return footer
145x
389
        case .cell:
390
            guard itemPath.section < layout.sections.count,
391
                  itemPath.item < layout.sections[itemPath.section].items.count else {
392
                // This occurs when getting layout attributes for initial / final animations
!
393
                return nil
!
394
            }
395
            return layout.sections[itemPath.section].items[itemPath.item]
396
        }
397
    }
398
399
    func update(preferredSize: CGSize, alignment: ChatItemAlignment, for itemPath: ItemPath, kind: ItemKind, at state: ModelState) {
10000x
400
        guard var item = item(for: itemPath, kind: kind, at: state) else {
10000x
401
            assertionFailure("Item at index path (\(itemPath.section) - \(itemPath.item)) does not exist.")
!
402
            return
!
403
        }
10000x
404
10000x
405
        let previousFrame = item.frame
10000x
406
        cachedAttributesState = nil
10000x
407
        item.alignment = alignment
10000x
408
        item.calculatedSize = preferredSize
10000x
409
        item.calculatedOnce = true
10000x
410
10000x
411
        switch state {
10000x
412
        case .beforeUpdate:
10000x
413
            switch kind {
10000x
414
            case .header:
10000x
415
                layoutBeforeUpdate.setAndAssemble(header: item, sectionIndex: itemPath.section)
3x
416
            case .footer:
10000x
417
                layoutBeforeUpdate.setAndAssemble(footer: item, sectionIndex: itemPath.section)
3x
418
            case .cell:
10000x
419
                layoutBeforeUpdate.setAndAssemble(item: item, sectionIndex: itemPath.section, itemIndex: itemPath.item)
10000x
420
            }
10000x
421
        case .afterUpdate:
10000x
422
            switch kind {
!
423
            case .header:
!
424
                layoutAfterUpdate?.setAndAssemble(header: item, sectionIndex: itemPath.section)
!
425
            case .footer:
!
426
                layoutAfterUpdate?.setAndAssemble(footer: item, sectionIndex: itemPath.section)
!
427
            case .cell:
!
428
                layoutAfterUpdate?.setAndAssemble(item: item, sectionIndex: itemPath.section, itemIndex: itemPath.item)
!
429
            }
10000x
430
        }
10000x
431
10000x
432
        let frameUpdateAction = CompensatingAction.frameUpdate(previousFrame: previousFrame, newFrame: item.frame)
10000x
433
        compensateOffsetIfNeeded(for: itemPath, kind: kind, action: frameUpdateAction)
10000x
434
    }
10000x
435
436
    func process(changeItems: [ChangeItem]) {
42x
437
        func applyConfiguration(_ configuration: ItemModel.Configuration, to item: inout ItemModel) {
10700x
438
            item.alignment = configuration.alignment
10700x
439
            if let calculatedSize = configuration.calculatedSize {
10700x
440
                item.calculatedSize = calculatedSize
10700x
441
                item.calculatedOnce = true
10700x
442
            } else {
10700x
443
                item.resetSize()
!
444
            }
10700x
445
        }
10700x
446
42x
447
        batchUpdateCompensatingOffset = 0
42x
448
        proposedCompensatingOffset = 0
42x
449
        let changeItems = changeItems.sorted()
42x
450
42x
451
        var afterUpdateModel = LayoutModel(sections: layoutBeforeUpdate.sections, collectionLayout: layoutRepresentation)
42x
452
        resetCachedAttributeObjects()
42x
453
42x
454
        changeItems.forEach { updateItem in
455
            switch updateItem {
456
            case let .sectionInsert(sectionIndex: sectionIndex):
457
                let items = (0..<layoutRepresentation.numberOfItems(in: sectionIndex)).map { index -> ItemModel in
350x
458
                    let itemIndexPath = IndexPath(item: index, section: sectionIndex)
350x
459
                    return ItemModel(with: layoutRepresentation.configuration(for: .cell, at: itemIndexPath))
350x
460
                }
350x
461
                let header: ItemModel?
2x
462
                if layoutRepresentation.shouldPresentHeader(at: sectionIndex) == true {
2x
463
                    let headerIndexPath = IndexPath(item: 0, section: sectionIndex)
1x
464
                    header = ItemModel(with: layoutRepresentation.configuration(for: .header, at: headerIndexPath))
1x
465
                } else {
2x
466
                    header = nil
1x
467
                }
2x
468
                let footer: ItemModel?
2x
469
                if layoutRepresentation.shouldPresentFooter(at: sectionIndex) == true {
2x
470
                    let footerIndexPath = IndexPath(item: 0, section: sectionIndex)
2x
471
                    footer = ItemModel(with: layoutRepresentation.configuration(for: .footer, at: footerIndexPath))
2x
472
                } else {
2x
473
                    footer = nil
!
474
                }
2x
475
                let section = SectionModel(header: header, footer: footer, items: ContiguousArray(items), collectionLayout: layoutRepresentation)
2x
476
                afterUpdateModel.insertSection(section, at: sectionIndex)
2x
477
                insertedSectionsIndexes.insert(sectionIndex)
2x
478
            case let .itemInsert(itemIndexPath: indexPath):
479
                let item = ItemModel(with: layoutRepresentation.configuration(for: .cell, at: indexPath))
480
                insertedIndexes.insert(indexPath)
481
                afterUpdateModel.insertItem(item, at: indexPath)
482
            case let .sectionDelete(sectionIndex: sectionIndex):
483
                let section = layoutBeforeUpdate.sections[sectionIndex]
2x
484
                deletedSectionsIndexes.insert(sectionIndex)
2x
485
                afterUpdateModel.removeSection(by: section.id)
2x
486
            case let .itemDelete(itemIndexPath: indexPath):
487
                let itemId = itemIdentifier(for: indexPath.itemPath, kind: .cell, at: .beforeUpdate)!
488
                afterUpdateModel.removeItem(by: itemId)
489
                deletedIndexes.insert(indexPath)
490
            case let .sectionReload(sectionIndex: sectionIndex):
491
                reloadedSectionsIndexes.insert(sectionIndex)
5x
492
                var section = layoutBeforeUpdate.sections[sectionIndex]
5x
493
5x
494
                var header: ItemModel?
5x
495
                if layoutRepresentation.shouldPresentHeader(at: sectionIndex) == true {
5x
496
                    let headerIndexPath = IndexPath(item: 0, section: sectionIndex)
3x
497
                    var newHeader = section.header ?? ItemModel(with: layoutRepresentation.configuration(for: .header, at: headerIndexPath))
3x
498
                    let configuration = layoutRepresentation.configuration(for: .header, at: headerIndexPath)
3x
499
                    applyConfiguration(configuration, to: &newHeader)
3x
500
                    header = newHeader
3x
501
                } else {
5x
502
                    header = nil
2x
503
                }
5x
504
                section.set(header: header)
5x
505
5x
506
                var footer: ItemModel?
5x
507
                if layoutRepresentation.shouldPresentFooter(at: sectionIndex) == true {
5x
508
                    let footerIndexPath = IndexPath(item: 0, section: sectionIndex)
4x
509
                    var newFooter = section.footer ?? ItemModel(with: layoutRepresentation.configuration(for: .footer, at: footerIndexPath))
4x
510
                    let configuration = layoutRepresentation.configuration(for: .footer, at: footerIndexPath)
4x
511
                    applyConfiguration(configuration, to: &newFooter)
4x
512
                    footer = newFooter
4x
513
                } else {
5x
514
                    footer = nil
1x
515
                }
5x
516
                section.set(footer: footer)
5x
517
5x
518
                let oldItems = section.items
5x
519
                let items: [ItemModel] = (0..<layoutRepresentation.numberOfItems(in: sectionIndex)).map { index in
550x
520
                    var newItem: ItemModel
550x
521
                    let itemIndexPath = IndexPath(item: index, section: sectionIndex)
550x
522
                    if index < oldItems.count {
550x
523
                        newItem = oldItems[index]
450x
524
                        let configuration = layoutRepresentation.configuration(for: .cell, at: itemIndexPath)
450x
525
                        applyConfiguration(configuration, to: &newItem)
450x
526
                    } else {
550x
527
                        newItem = ItemModel(with: layoutRepresentation.configuration(for: .cell, at: itemIndexPath))
100x
528
                    }
550x
529
                    return newItem
550x
530
                }
550x
531
                section.set(items: ContiguousArray(items))
5x
532
                afterUpdateModel.removeSection(for: sectionIndex)
5x
533
                afterUpdateModel.insertSection(section, at: sectionIndex)
5x
534
            case let .itemReload(itemIndexPath: indexPath):
535
                guard var item = item(for: indexPath.itemPath, kind: .cell, at: .beforeUpdate) else {
10300x
536
                    assertionFailure("Item at index path (\(indexPath.section) - \(indexPath.item)) does not exist.")
!
537
                    return
!
538
                }
10300x
539
                let configuration = layoutRepresentation.configuration(for: .cell, at: indexPath)
10300x
540
                applyConfiguration(configuration, to: &item)
10300x
541
                afterUpdateModel.replaceItem(item, at: indexPath)
10300x
542
                reloadedIndexes.insert(indexPath)
10300x
543
            case let .sectionMove(initialSectionIndex: initialSectionIndex, finalSectionIndex: finalSectionIndex):
544
                let section = layoutBeforeUpdate.sections[initialSectionIndex]
2x
545
                movedSectionsIndexes.insert(finalSectionIndex)
2x
546
                afterUpdateModel.removeSection(by: section.id)
2x
547
                afterUpdateModel.insertSection(section, at: finalSectionIndex)
2x
548
            case let .itemMove(initialItemIndexPath: initialItemIndexPath, finalItemIndexPath: finalItemIndexPath):
549
                let itemId = itemIdentifier(for: initialItemIndexPath.itemPath, kind: .cell, at: .beforeUpdate)!
4x
550
                let item = layoutBeforeUpdate.sections[initialItemIndexPath.section].items[initialItemIndexPath.item]
4x
551
                movedIndexes.insert(initialItemIndexPath)
4x
552
                afterUpdateModel.removeItem(by: itemId)
4x
553
                afterUpdateModel.insertItem(item, at: finalItemIndexPath)
4x
554
            }
555
        }
556
42x
557
        var afterUpdateModelSections = afterUpdateModel.sections
42x
558
        afterUpdateModelSections.withUnsafeMutableBufferPointer { directlyMutableSections in
42x
559
            for index in 0..<directlyMutableSections.count {
60x
560
                directlyMutableSections[index].assembleLayout()
60x
561
            }
60x
562
        }
42x
563
        afterUpdateModel = LayoutModel(sections: afterUpdateModelSections, collectionLayout: layoutRepresentation)
42x
564
        afterUpdateModel.assembleLayout()
42x
565
42x
566
        layoutAfterUpdate = afterUpdateModel
42x
567
42x
568
        let visibleBounds = layoutRepresentation.visibleBounds
42x
569
42x
570
        // Calculating potential content offset changes after the updates
42x
571
        insertedSectionsIndexes.sorted(by: { $0 < $1 }).forEach {
42x
572
            compensateOffsetOfSectionIfNeeded(for: $0, action: .insert, visibleBounds: visibleBounds)
2x
573
        }
2x
574
        reloadedSectionsIndexes.sorted(by: { $0 < $1 }).forEach {
42x
575
            let oldSection = section(at: $0, at: .beforeUpdate)
5x
576
            guard let newSectionIndex = sectionIndex(for: oldSection.id, at: .afterUpdate) else {
5x
577
                assertionFailure("Section with identifier \(oldSection.id) does not exist.")
!
578
                return
!
579
            }
5x
580
            let newSection = section(at: newSectionIndex, at: .afterUpdate)
5x
581
            compensateOffsetOfSectionIfNeeded(for: $0, action: .frameUpdate(previousFrame: oldSection.frame, newFrame: newSection.frame), visibleBounds: visibleBounds)
5x
582
        }
5x
583
        deletedSectionsIndexes.sorted(by: { $0 < $1 }).forEach {
42x
584
            compensateOffsetOfSectionIfNeeded(for: $0, action: .delete, visibleBounds: visibleBounds)
2x
585
        }
2x
586
42x
587
        reloadedIndexes.sorted(by: { $0 < $1 }).forEach {
588
            guard let oldItem = item(for: $0.itemPath, kind: .cell, at: .beforeUpdate),
10300x
589
                  let newItemIndexPath = itemPath(by: oldItem.id, kind: .cell, at: .afterUpdate),
10300x
590
                  let newItem = item(for: newItemIndexPath, kind: .cell, at: .afterUpdate) else {
10300x
591
                assertionFailure("Internal inconsistency.")
!
592
                return
!
593
            }
10300x
594
            compensateOffsetIfNeeded(for: $0.itemPath, kind: .cell, action: .frameUpdate(previousFrame: oldItem.frame, newFrame: newItem.frame), visibleBounds: visibleBounds)
10300x
595
        }
10300x
596
        insertedIndexes.sorted(by: { $0 < $1 }).forEach {
1850000x
597
            compensateOffsetIfNeeded(for: $0.itemPath, kind: .cell, action: .insert, visibleBounds: visibleBounds)
598
        }
599
        deletedIndexes.sorted(by: { $0 < $1 }).forEach {
1840000x
600
            compensateOffsetIfNeeded(for: $0.itemPath, kind: .cell, action: .delete, visibleBounds: visibleBounds)
601
        }
602
42x
603
        totalProposedCompensatingOffset = proposedCompensatingOffset
42x
604
    }
42x
605
606
    func commitUpdates() {
12x
607
        insertedIndexes = []
12x
608
        insertedSectionsIndexes = []
12x
609
12x
610
        reloadedIndexes = []
12x
611
        reloadedSectionsIndexes = []
12x
612
12x
613
        movedIndexes = []
12x
614
        movedSectionsIndexes = []
12x
615
12x
616
        deletedIndexes = []
12x
617
        deletedSectionsIndexes = []
12x
618
12x
619
        layoutBeforeUpdate = layout(at: .afterUpdate)
12x
620
        layoutAfterUpdate = nil
12x
621
12x
622
        totalProposedCompensatingOffset = 0
12x
623
12x
624
        cachedAttributeObjects[.beforeUpdate] = cachedAttributeObjects[.afterUpdate]
12x
625
        resetCachedAttributeObjects(at: .afterUpdate)
12x
626
    }
12x
627
628
    func contentSize(for state: ModelState) -> CGSize {
1x
629
        let contentHeight = contentHeight(at: state)
1x
630
        guard contentHeight != 0 else {
1x
631
            return .zero
!
632
        }
1x
633
        // This is a workaround for `layoutAttributesForElementsInRect:` not getting invoked enough
1x
634
        // times if `collectionViewContentSize.width` is not smaller than the width of the collection
1x
635
        // view, minus horizontal insets. This results in visual defects when performing batch
1x
636
        // updates. To work around this, we subtract 0.0001 from our content size width calculation;
1x
637
        // this small decrease in `collectionViewContentSize.width` is enough to work around the
1x
638
        // incorrect, internal collection view `CGRect` checks, without introducing any visual
1x
639
        // differences for elements in the collection view.
1x
640
        // See https://openradar.appspot.com/radar?id=5025850143539200 for more details.
1x
641
        let contentSize = CGSize(width: layoutRepresentation.visibleBounds.size.width - 0.0001, height: contentHeight)
1x
642
        return contentSize
1x
643
    }
1x
644
645
    func offsetByTotalCompensation(attributes: UICollectionViewLayoutAttributes?, for state: ModelState, backward: Bool = false) {
!
646
        guard layoutRepresentation.keepContentOffsetAtBottomOnBatchUpdates,
!
647
              state == .afterUpdate,
!
648
              let attributes = attributes else {
!
649
            return
!
650
        }
!
651
        if backward, isLayoutBiggerThanVisibleBounds(at: .afterUpdate) {
!
652
            attributes.frame.offsettingBy(dx: 0, dy: totalProposedCompensatingOffset * -1)
!
653
        } else if !backward, isLayoutBiggerThanVisibleBounds(at: .afterUpdate) {
!
654
            attributes.frame.offsettingBy(dx: 0, dy: totalProposedCompensatingOffset)
!
655
        }
!
656
    }
!
657
658
    func layout(at state: ModelState) -> LayoutModel<Layout> {
659
        switch state {
660
        case .beforeUpdate:
661
            return layoutBeforeUpdate
662
        case .afterUpdate:
663
            guard let layoutAfterUpdate = layoutAfterUpdate else {
664
                assertionFailure("Internal inconsistency. Layout at \(state) is missing.")
!
665
                return LayoutModel(sections: [], collectionLayout: layoutRepresentation)
!
666
            }
667
            return layoutAfterUpdate
668
        }
669
    }
670
671
    func isLayoutBiggerThanVisibleBounds(at state: ModelState, withFullCompensation: Bool = false, visibleBounds: CGRect? = nil) -> Bool {
672
        let visibleBounds = visibleBounds ?? layoutRepresentation.visibleBounds
673
        let visibleBoundsHeight = visibleBounds.height + (withFullCompensation ? batchUpdateCompensatingOffset + proposedCompensatingOffset : 0)
674
        return contentHeight(at: state).rounded() > visibleBoundsHeight.rounded()
675
    }
676
677
    private func allAttributes(at state: ModelState, visibleRect: CGRect? = nil) -> [ChatLayoutAttributes] {
105x
678
        let layout = layout(at: state)
105x
679
        let additionalAttributes = AdditionalLayoutAttributes(layoutRepresentation)
105x
680
105x
681
        if let visibleRect = visibleRect {
105x
682
            var traverseState: TraverseState = .notFound
105x
683
105x
684
            func check(rect: CGRect) -> Bool {
1070x
685
                switch traverseState {
1070x
686
                case .notFound:
1070x
687
                    if visibleRect.intersects(rect) {
307x
688
                        traverseState = .found
105x
689
                        return true
105x
690
                    } else {
202x
691
                        return false
202x
692
                    }
1070x
693
                case .found:
1070x
694
                    if visibleRect.intersects(rect) {
557x
695
                        return true
351x
696
                    } else {
351x
697
                        if rect.minY > visibleRect.maxY + batchUpdateCompensatingOffset + proposedCompensatingOffset {
206x
698
                            traverseState = .done
105x
699
                        }
206x
700
                        return false
206x
701
                    }
1070x
702
                case .done:
1070x
703
                    return false
206x
704
                }
1070x
705
            }
1070x
706
105x
707
            var allRects = ContiguousArray<(frame: CGRect, indexPath: ItemPath, kind: ItemKind)>()
105x
708
            // I dont think there can be more then a 200 elements on the screen simultaneously
105x
709
            allRects.reserveCapacity(200)
105x
710
105x
711
            let comparisonResults = [ComparisonResult.orderedSame, .orderedDescending]
105x
712
105x
713
            for sectionIndex in 0..<layout.sections.count {
214x
714
                let section = layout.sections[sectionIndex]
214x
715
                let sectionPath = ItemPath(item: 0, section: sectionIndex)
214x
716
                if let headerFrame = itemFrame(for: sectionPath, kind: .header, at: state, isFinal: true, additionalAttributes: additionalAttributes),
214x
717
                   check(rect: headerFrame) {
214x
718
                    allRects.append((frame: headerFrame, indexPath: sectionPath, kind: .header))
8x
719
                }
214x
720
                guard traverseState != .done else {
214x
721
                    break
105x
722
                }
109x
723
109x
724
                var startingIndex = 0
109x
725
                // If header is not visible
109x
726
                if traverseState == .notFound, !section.items.isEmpty {
109x
727
                    func predicate(itemIndex: Int) -> ComparisonResult {
2620x
728
                        let itemPath = ItemPath(item: itemIndex, section: sectionIndex)
2620x
729
                        guard let itemFrame = itemFrame(for: itemPath, kind: .cell, at: state, isFinal: true, additionalAttributes: additionalAttributes) else {
2620x
730
                            return .orderedDescending
!
731
                        }
2620x
732
                        if itemFrame.intersects(visibleRect) {
2620x
733
                            return .orderedSame
101x
734
                        } else if itemFrame.minY > visibleRect.maxY {
2520x
735
                            return .orderedDescending
101x
736
                        } else if itemFrame.maxX < visibleRect.minY {
2420x
737
                            return .orderedAscending
2420x
738
                        }
2420x
739
                        return .orderedSame
!
740
                    }
2420x
741
101x
742
                    // Find if any of the items of the section is visible
101x
743
101x
744
                    if comparisonResults.contains(predicate(itemIndex: section.items.count - 1)),
101x
745
                       let firstMatchingIndex = ContiguousArray(0...section.items.count - 1).withUnsafeBufferPointer({ $0.binarySearch(predicate: predicate) }) {
101x
746
                        // Find first item that is visible
101x
747
                        startingIndex = firstMatchingIndex
101x
748
                        for itemIndex in (0..<firstMatchingIndex).reversed() {
303x
749
                            let itemPath = ItemPath(item: itemIndex, section: sectionIndex)
303x
750
                            guard let itemFrame = itemFrame(for: itemPath, kind: .cell, at: state, isFinal: true, additionalAttributes: additionalAttributes) else {
303x
751
                                continue
!
752
                            }
303x
753
                            guard itemFrame.maxY >= visibleRect.minY else {
303x
754
                                break
101x
755
                            }
202x
756
                            startingIndex = itemIndex
202x
757
                        }
202x
758
                    } else {
101x
759
                        // Otherwise we can safely skip all the items in the section and go to footer.
!
760
                        startingIndex = section.items.count
!
761
                    }
101x
762
                }
109x
763
109x
764
                if startingIndex < section.items.count {
109x
765
                    for itemIndex in startingIndex..<section.items.count {
747x
766
                        let itemPath = ItemPath(item: itemIndex, section: sectionIndex)
747x
767
                        if let itemFrame = itemFrame(for: itemPath, kind: .cell, at: state, isFinal: true, additionalAttributes: additionalAttributes),
747x
768
                           check(rect: itemFrame) {
747x
769
                            if state == .beforeUpdate || isAnimatedBoundsChange || !layoutRepresentation.processOnlyVisibleItemsOnAnimatedBatchUpdates {
444x
770
                                allRects.append((frame: itemFrame, indexPath: itemPath, kind: .cell))
444x
771
                            } else {
444x
772
                                var itemWasVisibleBefore: Bool {
!
773
                                    guard let itemIdentifier = itemIdentifier(for: itemPath, kind: .cell, at: .afterUpdate),
!
774
                                          let initialIndexPath = self.itemPath(by: itemIdentifier, kind: .cell, at: .beforeUpdate),
!
775
                                          let item = item(for: initialIndexPath, kind: .cell, at: .beforeUpdate),
!
776
                                          item.calculatedOnce == true,
!
777
                                          let itemFrame = self.itemFrame(for: initialIndexPath, kind: .cell, at: .beforeUpdate, isFinal: false, additionalAttributes: additionalAttributes),
!
778
                                          itemFrame.intersects(additionalAttributes.visibleBounds.offsetBy(dx: 0, dy: -totalProposedCompensatingOffset)) else {
!
779
                                        return false
!
780
                                    }
!
781
                                    return true
!
782
                                }
!
783
                                var itemWillBeVisible: Bool {
!
784
                                    let offsetVisibleBounds = additionalAttributes.visibleBounds.offsetBy(dx: 0, dy: proposedCompensatingOffset + batchUpdateCompensatingOffset)
!
785
                                    if insertedIndexes.contains(itemPath.indexPath),
!
786
                                       let itemFrame = self.itemFrame(for: itemPath, kind: .cell, at: state, isFinal: true, additionalAttributes: additionalAttributes),
!
787
                                       itemFrame.intersects(offsetVisibleBounds) {
!
788
                                        return true
!
789
                                    }
!
790
                                    if let itemIdentifier = itemIdentifier(for: itemPath, kind: .cell, at: .afterUpdate),
!
791
                                       let initialIndexPath = self.itemPath(by: itemIdentifier, kind: .cell, at: .beforeUpdate)?.indexPath,
!
792
                                       movedIndexes.contains(initialIndexPath) || reloadedIndexes.contains(initialIndexPath),
!
793
                                       let itemFrame = self.itemFrame(for: itemPath, kind: .cell, at: state, isFinal: true, additionalAttributes: additionalAttributes),
!
794
                                       itemFrame.intersects(offsetVisibleBounds) {
!
795
                                        return true
!
796
                                    }
!
797
                                    return false
!
798
                                }
!
799
                                if itemWillBeVisible || itemWasVisibleBefore {
!
800
                                    allRects.append((frame: itemFrame, indexPath: itemPath, kind: .cell))
!
801
                                }
!
802
                            }
444x
803
                        }
747x
804
                        guard traverseState != .done else {
747x
805
                            break
101x
806
                        }
646x
807
                    }
646x
808
                }
109x
809
109x
810
                if let footerFrame = itemFrame(for: sectionPath, kind: .footer, at: state, isFinal: true, additionalAttributes: additionalAttributes),
109x
811
                   check(rect: footerFrame) {
109x
812
                    allRects.append((frame: footerFrame, indexPath: sectionPath, kind: .footer))
4x
813
                }
109x
814
            }
109x
815
105x
816
            return allRects.compactMap { frame, path, kind -> ChatLayoutAttributes? in
456x
817
                itemAttributes(for: path, kind: kind, predefinedFrame: frame, at: state, additionalAttributes: additionalAttributes)
456x
818
            }
456x
819
        } else {
105x
820
            // Debug purposes only.
!
821
            var attributes = [ChatLayoutAttributes]()
!
822
            attributes.reserveCapacity(layout.sections.reduce(into: 0) { $0 += $1.items.count })
!
823
            layout.sections.enumerated().forEach { sectionIndex, section in
!
824
                let sectionPath = ItemPath(item: 0, section: sectionIndex)
!
825
                if let headerAttributes = itemAttributes(for: sectionPath, kind: .header, at: state, additionalAttributes: additionalAttributes) {
!
826
                    attributes.append(headerAttributes)
!
827
                }
!
828
                if let footerAttributes = itemAttributes(for: sectionPath, kind: .footer, at: state, additionalAttributes: additionalAttributes) {
!
829
                    attributes.append(footerAttributes)
!
830
                }
!
831
                section.items.enumerated().forEach { itemIndex, _ in
!
832
                    let itemPath = ItemPath(item: itemIndex, section: sectionIndex)
!
833
                    if let itemAttributes = itemAttributes(for: itemPath, kind: .cell, at: state, additionalAttributes: additionalAttributes) {
!
834
                        attributes.append(itemAttributes)
!
835
                    }
!
836
                }
!
837
            }
!
838
!
839
            return attributes
!
840
        }
!
841
    }
!
842
843
    private func compensateOffsetIfNeeded(for itemPath: ItemPath, kind: ItemKind, action: CompensatingAction, visibleBounds: CGRect? = nil) {
844
        guard layoutRepresentation.keepContentOffsetAtBottomOnBatchUpdates else {
845
            return
!
846
        }
847
848
        let visibleBounds = visibleBounds ?? layoutRepresentation.visibleBounds
849
        let minY = (visibleBounds.lowerPoint.y + batchUpdateCompensatingOffset + proposedCompensatingOffset).rounded()
850
        let interItemSpacing = layoutRepresentation.settings.interItemSpacing
851
852
        switch action {
853
        case .insert:
854
            guard isLayoutBiggerThanVisibleBounds(at: .afterUpdate, visibleBounds: visibleBounds),
855
                  let itemFrame = itemFrame(for: itemPath, kind: kind, at: .afterUpdate) else {
856
                return
2x
857
            }
858
            if itemFrame.minY.rounded() - interItemSpacing <= minY {
859
                proposedCompensatingOffset += itemFrame.height + interItemSpacing
860
            }
861
        case let .frameUpdate(previousFrame, newFrame):
862
            guard isLayoutBiggerThanVisibleBounds(at: .afterUpdate, withFullCompensation: true, visibleBounds: visibleBounds) else {
20300x
863
                return
10000x
864
            }
10300x
865
            if newFrame.minY.rounded() <= minY {
10300x
866
                batchUpdateCompensatingOffset += newFrame.height - previousFrame.height
110x
867
            }
868
        case .delete:
869
            guard isLayoutBiggerThanVisibleBounds(at: .beforeUpdate, visibleBounds: visibleBounds),
870
                  let deletedFrame = itemFrame(for: itemPath, kind: kind, at: .beforeUpdate) else {
871
                return
4x
872
            }
873
            if deletedFrame.minY.rounded() <= minY {
874
                // Changing content offset for deleted items using `invalidateLayout(with:) causes UI glitches.
40x
875
                // So we are using targetContentOffset(forProposedContentOffset:) which is going to be called after.
40x
876
                proposedCompensatingOffset -= (deletedFrame.height + interItemSpacing)
40x
877
            }
878
        }
879
880
    }
881
882
    private func compensateOffsetOfSectionIfNeeded(for sectionIndex: Int, action: CompensatingAction, visibleBounds: CGRect? = nil) {
9x
883
        guard layoutRepresentation.keepContentOffsetAtBottomOnBatchUpdates else {
9x
884
            return
!
885
        }
9x
886
9x
887
        let visibleBounds = visibleBounds ?? layoutRepresentation.visibleBounds
9x
888
        let minY = (visibleBounds.lowerPoint.y + batchUpdateCompensatingOffset + proposedCompensatingOffset).rounded()
9x
889
        let interSectionSpacing = layoutRepresentation.settings.interSectionSpacing
9x
890
9x
891
        switch action {
9x
892
        case .insert:
9x
893
            let sectionsAfterUpdate = layout(at: .afterUpdate).sections
2x
894
            guard isLayoutBiggerThanVisibleBounds(at: .afterUpdate, visibleBounds: visibleBounds),
2x
895
                  sectionIndex < sectionsAfterUpdate.count else {
2x
896
                return
!
897
            }
2x
898
            let section = sectionsAfterUpdate[sectionIndex]
2x
899
2x
900
            if section.offsetY.rounded() - interSectionSpacing <= minY {
2x
901
                proposedCompensatingOffset += section.height + interSectionSpacing
!
902
            }
9x
903
        case let .frameUpdate(previousFrame, newFrame):
9x
904
            guard sectionIndex < layout(at: .afterUpdate).sections.count,
5x
905
                  isLayoutBiggerThanVisibleBounds(at: .afterUpdate, withFullCompensation: true, visibleBounds: visibleBounds) else {
5x
906
                return
!
907
            }
5x
908
            if newFrame.minY.rounded() <= minY {
5x
909
                batchUpdateCompensatingOffset += newFrame.height - previousFrame.height
2x
910
            }
9x
911
        case .delete:
9x
912
            guard isLayoutBiggerThanVisibleBounds(at: .afterUpdate, visibleBounds: visibleBounds),
2x
913
                  sectionIndex < layout(at: .afterUpdate).sections.count else {
2x
914
                return
1x
915
            }
1x
916
            let section = layout(at: .beforeUpdate).sections[sectionIndex]
1x
917
            if section.locationHeight.rounded() <= minY {
1x
918
                // Changing content offset for deleted items using `invalidateLayout(with:) causes UI glitches.
!
919
                // So we are using targetContentOffset(forProposedContentOffset:) which is going to be called after.
!
920
                proposedCompensatingOffset -= (section.height + interSectionSpacing)
!
921
            }
9x
922
        }
9x
923
9x
924
    }
9x
925
926
    private func offsetByCompensation(frame: inout CGRect,
927
                                      at itemPath: ItemPath,
928
                                      for state: ModelState,
929
                                      backward: Bool = false) {
4310x
930
        guard layoutRepresentation.keepContentOffsetAtBottomOnBatchUpdates,
4310x
931
              state == .afterUpdate,
4310x
932
              isLayoutBiggerThanVisibleBounds(at: .afterUpdate) else {
4310x
933
            return
4310x
934
        }
4310x
935
        frame.offsettingBy(dx: 0, dy: proposedCompensatingOffset * (backward ? -1 : 1))
!
936
    }
!
937
938
}
939
940
extension RandomAccessCollection where Index == Int {
941
942
    func binarySearch(predicate: (Element) -> ComparisonResult) -> Index? {
1000000x
943
        var lowerBound = startIndex
1000000x
944
        var upperBound = endIndex
1000000x
945
1000000x
946
        while lowerBound < upperBound {
16000000x
947
            let midIndex = lowerBound &+ (upperBound &- lowerBound) / 2
16000000x
948
            if predicate(self[midIndex]) == .orderedSame {
16000000x
949
                return midIndex
1000000x
950
            } else if predicate(self[midIndex]) == .orderedAscending {
15000000x
951
                lowerBound = midIndex &+ 1
6000000x
952
            } else {
15000000x
953
                upperBound = midIndex
9000000x
954
            }
15000000x
955
        }
15000000x
956
        return nil
5x
957
    }
1000000x
958
959
    func binarySearchRange(predicate: (Element) -> ComparisonResult) -> [Element] {
1000000x
960
        func leftMostSearch(lowerBound: Index, upperBound: Index) -> Index? {
1000000x
961
            var lowerBound = lowerBound
1000000x
962
            var upperBound = upperBound
1000000x
963
1000000x
964
            while lowerBound < upperBound {
18000000x
965
                let midIndex = (lowerBound &+ upperBound) / 2
17000000x
966
                if predicate(self[midIndex]) == .orderedAscending {
17000000x
967
                    lowerBound = midIndex &+ 1
7000000x
968
                } else {
17000000x
969
                    upperBound = midIndex
10000000x
970
                }
17000000x
971
            }
17000000x
972
            if predicate(self[lowerBound]) == .orderedSame {
1000000x
973
                return lowerBound
1000000x
974
            } else {
1000000x
975
                return nil
1x
976
            }
1x
977
        }
!
978
1000000x
979
        func rightMostSearch(lowerBound: Index, upperBound: Index) -> Index? {
1000000x
980
            var lowerBound = lowerBound
1000000x
981
            var upperBound = upperBound
1000000x
982
1000000x
983
            while lowerBound < upperBound {
18000000x
984
                let midIndex = (lowerBound &+ upperBound &+ 1) / 2
17000000x
985
                if predicate(self[midIndex]) == .orderedDescending {
17000000x
986
                    upperBound = midIndex &- 1
12000000x
987
                } else {
17000000x
988
                    lowerBound = midIndex
5000000x
989
                }
17000000x
990
            }
17000000x
991
            if predicate(self[lowerBound]) == .orderedSame {
1000000x
992
                return lowerBound
1000000x
993
            } else {
1000000x
994
                return nil
!
995
            }
!
996
        }
!
997
        guard !isEmpty,
1000000x
998
              let lowerBound = leftMostSearch(lowerBound: startIndex, upperBound: endIndex - 1),
1000000x
999
              let upperBound = rightMostSearch(lowerBound: startIndex, upperBound: endIndex - 1) else {
1000000x
1000
            return []
2x
1001
        }
1000000x
1002
1000000x
1003
        return Array(self[lowerBound...upperBound])
1000000x
1004
    }
1000000x
1005
1006
}
1007
1008
// Helps to reduce the amount of looses in bridging calls to objc `UICollectionView` getter methods.
1009
struct AdditionalLayoutAttributes {
1010
1011
    let additionalInsets: UIEdgeInsets
1012
1013
    let viewSize: CGSize
1014
1015
    let adjustedContentInsets: UIEdgeInsets
1016
1017
    let visibleBounds: CGRect
1018
1019
    let layoutFrame: CGRect
1020
1021
    fileprivate init(_ layoutRepresentation: ChatLayoutRepresentation) {
1022
        viewSize = layoutRepresentation.viewSize
1023
        adjustedContentInsets = layoutRepresentation.adjustedContentInset
1024
        visibleBounds = layoutRepresentation.visibleBounds
1025
        layoutFrame = layoutRepresentation.layoutFrame
1026
        additionalInsets = layoutRepresentation.settings.additionalInsets
1027
    }
1028
}