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