Slather logo

Coverage for "StateController.swift" : 38.71%

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