Slather logo

Coverage for "StateController.swift" : 78.90%

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