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