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