Slather logo

Coverage for "LayoutModel.swift" : 74.19%

(92 of 124 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
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
}