Slather logo

Coverage for "LayoutModel.swift" : 75.38%

(98 of 130 relevant lines covered)

ChatLayout/Classes/Core/Model/LayoutModel.swift

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