1 |
//
|
|
2 |
// ChatLayout
|
|
3 |
// SectionModel.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 |
struct SectionModel<Layout: ChatLayoutRepresentation> {
|
|
17 |
|
|
18 |
let id: UUID
|
|
19 |
|
|
20 |
private(set) var header: ItemModel?
|
|
21 |
|
|
22 |
private(set) var footer: ItemModel?
|
|
23 |
|
|
24 |
private(set) var items: ContiguousArray<ItemModel>
|
|
25 |
|
|
26 |
var offsetY: CGFloat = 0
|
|
27 |
|
|
28 |
private unowned var collectionLayout: Layout
|
|
29 |
|
|
30 |
var frame: CGRect {
|
10x |
31 |
let additionalInsets = collectionLayout.settings.additionalInsets
|
10x |
32 |
return CGRect(x: 0,
|
10x |
33 |
y: offsetY,
|
10x |
34 |
width: collectionLayout.visibleBounds.width - additionalInsets.left - additionalInsets.right,
|
10x |
35 |
height: height)
|
10x |
36 |
}
|
10x |
37 |
|
|
38 |
var height: CGFloat {
|
|
39 |
if let footer = footer {
|
|
40 |
return footer.frame.maxY
|
|
41 |
} else {
|
|
42 |
guard let lastItem = items.last else {
|
2x |
43 |
return header?.frame.maxY ?? .zero
|
! |
44 |
}
|
2x |
45 |
return lastItem.frame.maxY
|
2x |
46 |
}
|
2x |
47 |
}
|
! |
48 |
|
|
49 |
var locationHeight: CGFloat {
|
|
50 |
offsetY + height
|
|
51 |
}
|
|
52 |
|
|
53 |
init(id: UUID = UUID(),
|
|
54 |
header: ItemModel?,
|
|
55 |
footer: ItemModel?,
|
|
56 |
items: ContiguousArray<ItemModel> = [],
|
|
57 |
collectionLayout: Layout) {
|
60x |
58 |
self.id = id
|
60x |
59 |
self.items = items
|
60x |
60 |
self.collectionLayout = collectionLayout
|
60x |
61 |
self.header = header
|
60x |
62 |
self.footer = footer
|
60x |
63 |
}
|
60x |
64 |
|
|
65 |
mutating func assembleLayout() {
|
118x |
66 |
var offsetY: CGFloat = 0
|
118x |
67 |
|
118x |
68 |
if header != nil {
|
118x |
69 |
header?.offsetY = 0
|
115x |
70 |
offsetY += header?.frame.height ?? 0
|
115x |
71 |
}
|
118x |
72 |
|
118x |
73 |
items.withUnsafeMutableBufferPointer { directlyMutableItems in
|
118x |
74 |
for rowIndex in 0..<directlyMutableItems.count {
|
|
75 |
directlyMutableItems[rowIndex].offsetY = offsetY
|
|
76 |
offsetY += directlyMutableItems[rowIndex].size.height + collectionLayout.settings.interItemSpacing
|
|
77 |
}
|
|
78 |
}
|
118x |
79 |
|
118x |
80 |
if footer != nil {
|
118x |
81 |
footer?.offsetY = offsetY
|
117x |
82 |
}
|
118x |
83 |
}
|
118x |
84 |
|
|
85 |
// MARK: To use when its is important to make the correct insertion
|
|
86 |
|
|
87 |
mutating func setAndAssemble(header: ItemModel) {
|
3x |
88 |
guard let oldHeader = self.header else {
|
3x |
89 |
self.header = header
|
! |
90 |
offsetEverything(below: -1, by: header.size.height)
|
! |
91 |
return
|
! |
92 |
}
|
3x |
93 |
#if DEBUG
|
3x |
94 |
if header.id != oldHeader.id {
|
3x |
95 |
assertionFailure("Internal inconsistency.")
|
! |
96 |
}
|
3x |
97 |
#endif
|
3x |
98 |
self.header = header
|
3x |
99 |
let heightDiff = header.size.height - oldHeader.size.height
|
3x |
100 |
offsetEverything(below: -1, by: heightDiff)
|
3x |
101 |
}
|
3x |
102 |
|
|
103 |
mutating func setAndAssemble(item: ItemModel, at index: Int) {
|
10000x |
104 |
guard index < items.count else {
|
10000x |
105 |
assertionFailure("Incorrect item index.")
|
! |
106 |
return
|
! |
107 |
}
|
10000x |
108 |
let oldItem = items[index]
|
10000x |
109 |
#if DEBUG
|
10000x |
110 |
if item.id != oldItem.id {
|
10000x |
111 |
assertionFailure("Internal inconsistency.")
|
! |
112 |
}
|
10000x |
113 |
#endif
|
10000x |
114 |
items[index] = item
|
10000x |
115 |
|
10000x |
116 |
let heightDiff = item.size.height - oldItem.size.height
|
10000x |
117 |
offsetEverything(below: index, by: heightDiff)
|
10000x |
118 |
}
|
10000x |
119 |
|
|
120 |
mutating func setAndAssemble(footer: ItemModel) {
|
3x |
121 |
#if DEBUG
|
3x |
122 |
if let oldFooter = self.footer,
|
3x |
123 |
footer.id != oldFooter.id {
|
3x |
124 |
assertionFailure("Internal inconsistency.")
|
! |
125 |
}
|
3x |
126 |
#endif
|
3x |
127 |
self.footer = footer
|
3x |
128 |
}
|
3x |
129 |
|
|
130 |
// MARK: Just updaters
|
|
131 |
|
|
132 |
mutating func set(header: ItemModel?) {
|
5x |
133 |
self.header = header
|
5x |
134 |
}
|
5x |
135 |
|
|
136 |
mutating func set(items: ContiguousArray<ItemModel>) {
|
5x |
137 |
self.items = items
|
5x |
138 |
}
|
5x |
139 |
|
|
140 |
mutating func set(footer: ItemModel?) {
|
5x |
141 |
guard let _ = self.footer, let _ = footer else {
|
5x |
142 |
self.footer = footer
|
1x |
143 |
return
|
1x |
144 |
}
|
4x |
145 |
self.footer = footer
|
4x |
146 |
}
|
4x |
147 |
|
|
148 |
private mutating func offsetEverything(below index: Int, by heightDiff: CGFloat) {
|
10000x |
149 |
guard heightDiff != 0 else {
|
10000x |
150 |
return
|
9000x |
151 |
}
|
9000x |
152 |
if index < items.count &- 1 {
|
1000x |
153 |
let nextIndex = index &+ 1
|
1000x |
154 |
items.withUnsafeMutableBufferPointer { directlyMutableItems in
|
1000x |
155 |
DispatchQueue.concurrentPerform(iterations: directlyMutableItems.count &- nextIndex) { internalIndex in
|
|
156 |
directlyMutableItems[internalIndex &+ nextIndex].offsetY += heightDiff
|
|
157 |
}
|
|
158 |
}
|
1000x |
159 |
}
|
1000x |
160 |
footer?.offsetY += heightDiff
|
1000x |
161 |
}
|
1000x |
162 |
|
|
163 |
// MARK: To use only withing process(updateItems:)
|
|
164 |
|
|
165 |
mutating func insert(_ item: ItemModel, at index: Int) {
|
|
166 |
guard index <= items.count else {
|
|
167 |
assertionFailure("Incorrect item index.")
|
! |
168 |
return
|
! |
169 |
}
|
|
170 |
items.insert(item, at: index)
|
|
171 |
}
|
|
172 |
|
|
173 |
mutating func replace(_ item: ItemModel, at index: Int) {
|
10300x |
174 |
guard index <= items.count else {
|
10300x |
175 |
assertionFailure("Incorrect item index.")
|
! |
176 |
return
|
! |
177 |
}
|
10300x |
178 |
items[index] = item
|
10300x |
179 |
}
|
10300x |
180 |
|
|
181 |
mutating func remove(at index: Int) {
|
|
182 |
guard index < items.count else {
|
|
183 |
assertionFailure("Incorrect item index.")
|
! |
184 |
return
|
! |
185 |
}
|
|
186 |
items.remove(at: index)
|
|
187 |
}
|
|
188 |
|
|
189 |
}
|
|