1 |
//
|
|
2 |
// ChatLayout
|
|
3 |
// LayoutModel.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 LayoutModel {
|
|
17 |
|
|
18 |
private struct ItemUUIDKey: Hashable {
|
|
19 |
|
|
20 |
let kind: ItemKind
|
|
21 |
|
|
22 |
let id: UUID
|
|
23 |
|
|
24 |
}
|
|
25 |
|
|
26 |
private(set) var sections: [SectionModel]
|
|
27 |
|
|
28 |
private unowned var collectionLayout: ChatLayoutRepresentation
|
|
29 |
|
|
30 |
private var sectionIndexByIdentifierCache: [UUID: Int]?
|
|
31 |
|
|
32 |
private var itemPathByIdentifierCache: [ItemUUIDKey: ItemPath]?
|
|
33 |
|
|
34 |
init(sections: [SectionModel], collectionLayout: ChatLayoutRepresentation) {
|
88x |
35 |
self.sections = sections
|
88x |
36 |
self.collectionLayout = collectionLayout
|
88x |
37 |
}
|
88x |
38 |
|
|
39 |
mutating func assembleLayout() {
|
65x |
40 |
var offset: CGFloat = collectionLayout.settings.additionalInsets.top
|
65x |
41 |
|
65x |
42 |
var sectionIndexByIdentifierCache = [UUID: Int](minimumCapacity: sections.count)
|
65x |
43 |
var itemPathByIdentifierCache = [ItemUUIDKey: ItemPath](minimumCapacity: sections.reduce(into: 0) { $0 += $1.items.count })
|
65x |
44 |
|
65x |
45 |
sections.withUnsafeMutableBufferPointer { directlyMutableSections in
|
115x |
46 |
for sectionIndex in 0..<directlyMutableSections.count {
|
115x |
47 |
sectionIndexByIdentifierCache[directlyMutableSections[sectionIndex].id] = sectionIndex
|
115x |
48 |
directlyMutableSections[sectionIndex].offsetY = offset
|
115x |
49 |
offset += directlyMutableSections[sectionIndex].height + collectionLayout.settings.interSectionSpacing
|
115x |
50 |
if let header = directlyMutableSections[sectionIndex].header {
|
112x |
51 |
itemPathByIdentifierCache[ItemUUIDKey(kind: .header, id: header.id)] = ItemPath(item: 0, section: sectionIndex)
|
115x |
52 |
}
|
|
53 |
for itemIndex in 0..<directlyMutableSections[sectionIndex].items.count {
|
|
54 |
itemPathByIdentifierCache[ItemUUIDKey(kind: .cell, id: directlyMutableSections[sectionIndex].items[itemIndex].id)] = ItemPath(item: itemIndex, section: sectionIndex)
|
|
55 |
}
|
115x |
56 |
if let footer = directlyMutableSections[sectionIndex].footer {
|
114x |
57 |
itemPathByIdentifierCache[ItemUUIDKey(kind: .footer, id: footer.id)] = ItemPath(item: 0, section: sectionIndex)
|
115x |
58 |
}
|
115x |
59 |
}
|
65x |
60 |
}
|
65x |
61 |
|
65x |
62 |
self.itemPathByIdentifierCache = itemPathByIdentifierCache
|
|
63 |
self.sectionIndexByIdentifierCache = sectionIndexByIdentifierCache
|
|
64 |
}
|
|
65 |
|
3x |
66 |
// MARK: To use when its is important to make the correct insertion
|
3x |
67 |
|
! |
68 |
mutating func setAndAssemble(header: ItemModel, sectionIndex: Int) {
|
! |
69 |
guard sectionIndex < sections.count else {
|
3x |
70 |
assertionFailure("Incorrect section index.")
|
3x |
71 |
return
|
3x |
72 |
}
|
3x |
73 |
|
3x |
74 |
let oldSection = sections[sectionIndex]
|
3x |
75 |
sections[sectionIndex].setAndAssemble(header: header)
|
3x |
76 |
let heightDiff = sections[sectionIndex].height - oldSection.height
|
|
77 |
offsetEverything(below: sectionIndex, by: heightDiff)
|
10000x |
78 |
}
|
10000x |
79 |
|
! |
80 |
mutating func setAndAssemble(item: ItemModel, sectionIndex: Int, itemIndex: Int) {
|
! |
81 |
guard sectionIndex < sections.count else {
|
10000x |
82 |
assertionFailure("Incorrect section index.")
|
10000x |
83 |
return
|
10000x |
84 |
}
|
10000x |
85 |
let oldSection = sections[sectionIndex]
|
10000x |
86 |
sections[sectionIndex].setAndAssemble(item: item, at: itemIndex)
|
10000x |
87 |
let heightDiff = sections[sectionIndex].height - oldSection.height
|
|
88 |
offsetEverything(below: sectionIndex, by: heightDiff)
|
3x |
89 |
}
|
3x |
90 |
|
! |
91 |
mutating func setAndAssemble(footer: ItemModel, sectionIndex: Int) {
|
! |
92 |
guard sectionIndex < sections.count else {
|
3x |
93 |
assertionFailure("Incorrect section index.")
|
3x |
94 |
return
|
3x |
95 |
}
|
3x |
96 |
let oldSection = sections[sectionIndex]
|
3x |
97 |
sections[sectionIndex].setAndAssemble(footer: footer)
|
3x |
98 |
let heightDiff = sections[sectionIndex].height - oldSection.height
|
|
99 |
offsetEverything(below: sectionIndex, by: heightDiff)
|
5x |
100 |
}
|
5x |
101 |
|
! |
102 |
func sectionIndex(by sectionId: UUID) -> Int? {
|
! |
103 |
guard let sectionIndexByIdentifierCache = sectionIndexByIdentifierCache else {
|
5x |
104 |
assertionFailure("Internal inconsistency. Cache is not prepared.")
|
5x |
105 |
return sections.firstIndex(where: { $0.id == sectionId })
|
5x |
106 |
}
|
|
107 |
return sectionIndexByIdentifierCache[sectionId]
|
10300x |
108 |
}
|
10300x |
109 |
|
! |
110 |
func itemPath(by itemId: UUID, kind: ItemKind) -> ItemPath? {
|
! |
111 |
guard let itemPathByIdentifierCache = itemPathByIdentifierCache else {
|
! |
112 |
assertionFailure("Internal inconsistency. Cache is not prepared.")
|
! |
113 |
for (sectionIndex, section) in sections.enumerated() {
|
! |
114 |
switch kind {
|
! |
115 |
case .header:
|
! |
116 |
if itemId == section.header?.id {
|
! |
117 |
return ItemPath(item: 0, section: sectionIndex)
|
! |
118 |
}
|
! |
119 |
case .footer:
|
! |
120 |
if itemId == section.footer?.id {
|
! |
121 |
return ItemPath(item: 0, section: sectionIndex)
|
! |
122 |
}
|
! |
123 |
case .cell:
|
! |
124 |
if let itemIndex = section.items.firstIndex(where: { $0.id == itemId }) {
|
! |
125 |
return ItemPath(item: itemIndex, section: sectionIndex)
|
! |
126 |
}
|
! |
127 |
}
|
10300x |
128 |
}
|
10300x |
129 |
return nil
|
10300x |
130 |
}
|
|
131 |
return itemPathByIdentifierCache[ItemUUIDKey(kind: kind, id: itemId)]
|
10000x |
132 |
}
|
10000x |
133 |
|
9000x |
134 |
private mutating func offsetEverything(below index: Int, by heightDiff: CGFloat) {
|
9000x |
135 |
guard heightDiff != 0 else {
|
1000x |
136 |
return
|
16x |
137 |
}
|
16x |
138 |
if index < sections.count &- 1 {
|
16x |
139 |
let nextIndex = index &+ 1
|
1000x |
140 |
sections.withUnsafeMutableBufferPointer { directlyMutableSections in
|
1000x |
141 |
DispatchQueue.concurrentPerform(iterations: directlyMutableSections.count &- nextIndex) { internalIndex in
|
|
142 |
directlyMutableSections[internalIndex &+ nextIndex].offsetY += heightDiff
|
|
143 |
}
|
|
144 |
}
|
9x |
145 |
}
|
9x |
146 |
}
|
9x |
147 |
|
! |
148 |
// MARK: To use only withing process(updateItems:)
|
! |
149 |
|
9x |
150 |
mutating func insertSection(_ section: SectionModel, at sectionIndex: Int) {
|
9x |
151 |
var sections = sections
|
9x |
152 |
guard sectionIndex <= sections.count else {
|
9x |
153 |
assertionFailure("Incorrect section index.")
|
9x |
154 |
return
|
9x |
155 |
}
|
|
156 |
|
4x |
157 |
sections.insert(section, at: sectionIndex)
|
7x |
158 |
self.sections = sections
|
! |
159 |
resetCache()
|
! |
160 |
}
|
4x |
161 |
|
4x |
162 |
mutating func removeSection(by sectionIdentifier: UUID) {
|
4x |
163 |
guard let sectionIndex = sections.firstIndex(where: { $0.id == sectionIdentifier }) else {
|
4x |
164 |
assertionFailure("Incorrect section identifier.")
|
|
165 |
return
|
5x |
166 |
}
|
5x |
167 |
sections.remove(at: sectionIndex)
|
5x |
168 |
resetCache()
|
5x |
169 |
}
|
|
170 |
|
|
171 |
mutating func removeSection(for sectionIndex: Int) {
|
|
172 |
sections.remove(at: sectionIndex)
|
|
173 |
resetCache()
|
|
174 |
}
|
|
175 |
|
10300x |
176 |
mutating func insertItem(_ item: ItemModel, at indexPath: IndexPath) {
|
10300x |
177 |
sections[indexPath.section].insert(item, at: indexPath.item)
|
10300x |
178 |
resetCache()
|
10300x |
179 |
}
|
|
180 |
|
|
181 |
mutating func replaceItem(_ item: ItemModel, at indexPath: IndexPath) {
|
|
182 |
sections[indexPath.section].replace(item, at: indexPath.item)
|
|
183 |
resetCache()
|
|
184 |
}
|
|
185 |
|
|
186 |
mutating func removeItem(by itemId: UUID) {
|
|
187 |
var itemPath: ItemPath?
|
|
188 |
for (sectionIndex, section) in sections.enumerated() {
|
|
189 |
if let itemIndex = section.items.firstIndex(where: { $0.id == itemId }) {
|
! |
190 |
itemPath = ItemPath(item: itemIndex, section: sectionIndex)
|
! |
191 |
break
|
|
192 |
}
|
|
193 |
}
|
|
194 |
guard let path = itemPath else {
|
|
195 |
assertionFailure("Incorrect item identifier.")
|
|
196 |
return
|
|
197 |
}
|
|
198 |
sections[path.section].remove(at: path.item)
|
|
199 |
resetCache()
|
|
200 |
}
|
|
201 |
|
|
202 |
private mutating func resetCache() {
|
|
203 |
itemPathByIdentifierCache = nil
|
|
204 |
sectionIndexByIdentifierCache = nil
|
|
205 |
}
|
|
206 |
|
|
207 |
}
|
|