Slather logo

Coverage for "SectionModel.swift" : 84.76%

(89 of 105 relevant lines covered)

ChatLayout/Classes/Core/Model/SectionModel.swift

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