Skip to content

Commit 878aed8

Browse files
authored
Merge pull request #18 from composed-swift/improve-composed-section-provider-performance
2 parents 408b965 + 95ec49b commit 878aed8

File tree

3 files changed

+204
-57
lines changed

3 files changed

+204
-57
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/Composed.xcscheme

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
buildConfiguration = "Debug"
2727
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2828
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29-
shouldUseLaunchSchemeArgsEnv = "YES">
29+
shouldUseLaunchSchemeArgsEnv = "YES"
30+
codeCoverageEnabled = "YES">
3031
<Testables>
3132
<TestableReference
3233
skipped = "NO">

Sources/Composed/Providers/ComposedSectionProvider.swift

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,10 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
2929

3030
open weak var updateDelegate: SectionProviderUpdateDelegate?
3131

32-
/// Represents all of the children this provider contains
33-
private var children: [Child] = []
34-
3532
/// Returns all the sections this provider contains
36-
public var sections: [Section] {
37-
return children.flatMap { kind -> [Section] in
38-
switch kind {
39-
case let .section(section):
40-
return [section]
41-
case let .provider(provider):
42-
return provider.sections
43-
}
44-
}
45-
}
33+
public private(set) var sections: [Section] = []
34+
35+
public private(set) var numberOfSections: Int = 0
4636

4737
/// Returns all the providers this provider contains
4838
public var providers: [SectionProvider] {
@@ -55,14 +45,8 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
5545
}
5646
}
5747

58-
public var numberOfSections: Int {
59-
return children.reduce(into: 0, { result, kind in
60-
switch kind {
61-
case .section: result += 1
62-
case let .provider(provider): result += provider.numberOfSections
63-
}
64-
})
65-
}
48+
/// Represents all of the children this provider contains
49+
private var children: [Child] = []
6650

6751
public init() { }
6852

@@ -76,6 +60,15 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
7660
public func sectionOffset(for provider: SectionProvider) -> Int {
7761
guard provider !== self else { return 0 }
7862

63+
// A quick test for if this is the last child is a small optimisation, mainly
64+
// beneficial when the provider has just been appended.
65+
switch children.last {
66+
case .some(.provider(let lastProvider)) where lastProvider === provider:
67+
return numberOfSections - provider.numberOfSections
68+
default:
69+
break
70+
}
71+
7972
var offset: Int = 0
8073

8174
for child in children {
@@ -101,6 +94,15 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
10194
}
10295

10396
public func sectionOffset(for section: Section) -> Int {
97+
// A quick test for if this is the last child is a small optimisation, mainly
98+
// beneficial when the section has just been appended.
99+
switch children.last {
100+
case .some(.section(let lastSection)) where lastSection === section:
101+
return numberOfSections - 1
102+
default:
103+
break
104+
}
105+
104106
var offset: Int = 0
105107

106108
for child in children {
@@ -165,7 +167,9 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
165167

166168
updateDelegate?.willBeginUpdating(self)
167169
children.insert(.section(child), at: index)
170+
numberOfSections += 1
168171
let sectionOffset = self.sectionOffset(for: child)
172+
sections.insert(child, at: sectionOffset)
169173
updateDelegate?.provider(self, didInsertSections: [child], at: IndexSet(integer: sectionOffset))
170174
updateDelegate?.didEndUpdating(self)
171175
}
@@ -181,8 +185,10 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
181185

182186
updateDelegate?.willBeginUpdating(self)
183187
children.insert(.provider(child), at: index)
188+
numberOfSections += child.sections.count
184189
let firstIndex = sectionOffset(for: child)
185190
let endIndex = firstIndex + child.sections.count
191+
sections.insert(contentsOf: child.sections, at: firstIndex)
186192
updateDelegate?.provider(self, didInsertSections: child.sections, at: IndexSet(integersIn: firstIndex..<endIndex))
187193
updateDelegate?.didEndUpdating(self)
188194
}
@@ -227,10 +233,42 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
227233

228234
updateDelegate?.willBeginUpdating(self)
229235
children.remove(at: index)
236+
numberOfSections -= sections.count
237+
self.sections.removeSubrange(firstIndex ..< endIndex)
230238
updateDelegate?.provider(self, didRemoveSections: sections, at: IndexSet(integersIn: firstIndex..<endIndex))
231239
updateDelegate?.didEndUpdating(self)
232240
}
233241

242+
public func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) {
243+
assert(sections.count == indexes.count, "Number of indexes must equal number of sections inserted")
244+
245+
numberOfSections += sections.count
246+
247+
let sectionOffset = self.sectionOffset(for: provider)
248+
249+
indexes
250+
.enumerated()
251+
.map { element in
252+
return (sections[element.offset], element.element + sectionOffset)
253+
}
254+
.forEach { element in
255+
self.sections.insert(element.0, at: element.1)
256+
}
257+
258+
updateDelegate?.provider(provider, didInsertSections: sections, at: indexes)
259+
}
260+
261+
public func provider(_ provider: SectionProvider, didRemoveSections sections: [Section], at indexes: IndexSet) {
262+
assert(sections.count == indexes.count, "Number of indexes must equal number of sections removed")
263+
264+
numberOfSections -= sections.count
265+
266+
let sectionOffset = self.sectionOffset(for: provider)
267+
indexes.map { $0 + sectionOffset }.reversed().forEach { self.sections.remove(at: $0) }
268+
269+
updateDelegate?.provider(provider, didRemoveSections: sections, at: indexes)
270+
}
271+
234272
}
235273

236274
// MARK:- Convenience Functions

Tests/ComposedTests/ComposedSectionProvider.swift

Lines changed: 143 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,54 @@ final class ComposedSectionProvider_Spec: QuickSpec {
88

99
override func spec() {
1010
describe("ComposedSectionProvider") {
11-
let global = ComposedSectionProvider()
12-
13-
let child1 = ComposedSectionProvider()
14-
let child1a = ArraySection<String>()
15-
let child1b = ArraySection<String>()
16-
let child2 = ComposedSectionProvider()
17-
let child2a = ComposedSectionProvider()
18-
let child2b = ArraySection<String>()
19-
let child2c = ArraySection<String>()
20-
let child2z = ComposedSectionProvider()
21-
let child2d = ArraySection<String>()
22-
let child2e = ComposedSectionProvider()
23-
let child2f = ArraySection<String>()
24-
25-
child1.append(child1a)
26-
child1.insert(child1b, after: child1a)
27-
28-
child2.append(child2a)
29-
child2a.append(child2c)
30-
child2a.insert(child2b, before: child2c)
31-
32-
child2.insert(child2z, after: child2a)
33-
child2.append(child2d)
34-
child2e.append(child2f)
35-
child2.append(child2e)
36-
global.append(child1)
37-
global.append(child2)
38-
39-
it("should contain 2 global sections") {
40-
expect(global.numberOfSections) == 6
11+
var global: ComposedSectionProvider!
12+
var child1: ComposedSectionProvider!
13+
var child1a: ArraySection<String>!
14+
var child1b: ArraySection<String>!
15+
var child2: ComposedSectionProvider!
16+
var child2a: ComposedSectionProvider!
17+
var child2b: ArraySection<String>!
18+
var child2c: ArraySection<String>!
19+
var child2d: ArraySection<String>!
20+
var child2e: ComposedSectionProvider!
21+
var child2f: ArraySection<String>!
22+
var child2g: ComposedSectionProvider!
23+
var child2h: ArraySection<String>!
24+
25+
beforeEach {
26+
global = ComposedSectionProvider()
27+
28+
child1 = ComposedSectionProvider()
29+
child1a = ArraySection<String>()
30+
child1b = ArraySection<String>()
31+
child2 = ComposedSectionProvider()
32+
child2a = ComposedSectionProvider()
33+
child2b = ArraySection<String>()
34+
child2c = ArraySection<String>()
35+
child2d = ArraySection<String>()
36+
child2e = ComposedSectionProvider()
37+
child2f = ArraySection<String>()
38+
child2g = ComposedSectionProvider()
39+
child2h = ArraySection<String>()
40+
41+
child1.append(child1a)
42+
child1.insert(child1b, after: child1a)
43+
44+
child2.append(child2a)
45+
child2a.append(child2c)
46+
child2a.insert(child2b, before: child2c)
47+
child2a.insert(child2d, after: child2c)
48+
49+
child2.insert(child2e, after: child2a)
50+
child2.append(child2f)
51+
child2g.append(child2h)
52+
child2.append(child2g)
53+
global.append(child1)
54+
global.append(child2)
55+
}
56+
57+
it("should contain 7 global sections") {
58+
expect(global.numberOfSections) == 7
4159
}
4260

4361
it("cache should contain 2 providers") {
@@ -46,25 +64,37 @@ final class ComposedSectionProvider_Spec: QuickSpec {
4664

4765
it("should return the right offsets") {
4866
expect(global.sectionOffset(for: child1)) == 0
67+
expect(global.sectionOffset(for: child1a)) == 0
68+
expect(global.sectionOffset(for: child1b)) == 1
4969
expect(global.sectionOffset(for: child2)) == 2
5070
expect(global.sectionOffset(for: child2a)) == 2
51-
expect(global.sectionOffset(for: child2z)) == 4
71+
expect(global.sectionOffset(for: child2b)) == 2
72+
expect(global.sectionOffset(for: child2c)) == 3
73+
expect(global.sectionOffset(for: child2d)) == 4
5274
expect(global.sectionOffset(for: child2e)) == 5
75+
expect(global.sectionOffset(for: child2f)) == 5
76+
expect(global.sectionOffset(for: child2g)) == 6
77+
expect(global.sectionOffset(for: child2h)) == 6
5378

5479
expect(child2.sectionOffset(for: child2a)) == 0
55-
expect(child2.sectionOffset(for: child2z)) == 2
5680
expect(child2.sectionOffset(for: child2e)) == 3
81+
expect(child2.sectionOffset(for: child2g)) == 4
82+
83+
expect(child2a.sectionOffset(for: child2b)) == 0
84+
expect(child2a.sectionOffset(for: child2c)) == 1
85+
expect(child2a.sectionOffset(for: child2d)) == 2
5786
}
5887

5988
context("when a section is inserted after a section provider with multiple sections") {
6089
var mockDelegate: MockSectionProviderUpdateDelegate!
6190
var countBefore: Int!
91+
var newSection: ArraySection<String>!
6292

6393
beforeEach {
6494
mockDelegate = MockSectionProviderUpdateDelegate()
6595
global.updateDelegate = mockDelegate
6696

67-
let newSection = ArraySection<String>()
97+
newSection = ArraySection<String>()
6898
countBefore = global.numberOfSections
6999

70100
global.append(newSection)
@@ -73,19 +103,38 @@ final class ComposedSectionProvider_Spec: QuickSpec {
73103
it("should pass the correct indexes to the delegate") {
74104
expect(mockDelegate.didInsertSectionsCalls.last!.2) == IndexSet(integer: countBefore)
75105
}
106+
107+
it("should update the sections count") {
108+
expect(global.numberOfSections) == 8
109+
}
110+
111+
it("should contain the correct sections") {
112+
expect(global.sections[0]) === child1a
113+
expect(global.sections[1]) === child1b
114+
expect(global.sections[2]) === child2b
115+
expect(global.sections[3]) === child2c
116+
expect(global.sections[4]) === child2d
117+
expect(global.sections[5]) === child2f
118+
expect(global.sections[6]) === child2h
119+
expect(global.sections[7]) === newSection
120+
}
76121
}
77122

78123
context("when a section provider is inserted after a section provider with multiple sections") {
79124
var mockDelegate: MockSectionProviderUpdateDelegate!
80125
var countBefore: Int!
81126
var sectionProvider: ComposedSectionProvider!
127+
var newSection1: ArraySection<String>!
128+
var newSection2: ArraySection<String>!
82129

83130
beforeEach {
84131
mockDelegate = MockSectionProviderUpdateDelegate()
85132
global.updateDelegate = mockDelegate
86133
sectionProvider = ComposedSectionProvider()
87-
sectionProvider.append(ArraySection<String>())
88-
sectionProvider.append(ArraySection<String>())
134+
newSection1 = ArraySection<String>()
135+
newSection2 = ArraySection<String>()
136+
sectionProvider.append(newSection1)
137+
sectionProvider.append(newSection2)
89138

90139
countBefore = global.numberOfSections
91140

@@ -95,6 +144,22 @@ final class ComposedSectionProvider_Spec: QuickSpec {
95144
it("should pass the correct indexes to the delegate") {
96145
expect(mockDelegate.didInsertSectionsCalls.last!.2) == IndexSet(integersIn: countBefore..<(countBefore + sectionProvider.numberOfSections))
97146
}
147+
148+
it("should update the sections count") {
149+
expect(global.numberOfSections) == 9
150+
}
151+
152+
it("should contain the correct sections") {
153+
expect(global.sections[0]) === child1a
154+
expect(global.sections[1]) === child1b
155+
expect(global.sections[2]) === child2b
156+
expect(global.sections[3]) === child2c
157+
expect(global.sections[4]) === child2d
158+
expect(global.sections[5]) === child2f
159+
expect(global.sections[6]) === child2h
160+
expect(global.sections[7]) === newSection1
161+
expect(global.sections[8]) === newSection2
162+
}
98163
}
99164

100165
context("when a section located after a section provider with multiple sections is removed") {
@@ -116,6 +181,49 @@ final class ComposedSectionProvider_Spec: QuickSpec {
116181
it("should pass the correct indexes to the delegate") {
117182
expect(mockDelegate.didRemoveSectionsCalls.last!.2) == IndexSet(integer: countBefore - 1)
118183
}
184+
185+
it("should contain the correct sections") {
186+
expect(global.sections[0]) === child1a
187+
expect(global.sections[1]) === child1b
188+
expect(global.sections[2]) === child2b
189+
expect(global.sections[3]) === child2c
190+
expect(global.sections[4]) === child2d
191+
expect(global.sections[5]) === child2f
192+
expect(global.sections[6]) === child2h
193+
}
194+
}
195+
196+
context("when multiple sections are removed") {
197+
var mockDelegate: MockSectionProviderUpdateDelegate!
198+
var countBefore: Int!
199+
200+
beforeEach {
201+
mockDelegate = MockSectionProviderUpdateDelegate()
202+
global.updateDelegate = mockDelegate
203+
204+
countBefore = global.numberOfSections
205+
206+
child2.remove(child2a)
207+
}
208+
209+
it("should pass through the removed indexes to the delegate") {
210+
expect(mockDelegate.didRemoveSectionsCalls.last!.2) == IndexSet([0, 1, 2])
211+
}
212+
213+
it("should update the number of sections") {
214+
expect(global.numberOfSections) == countBefore - 3
215+
}
216+
217+
it("should pass itself to the delegate") {
218+
expect(mockDelegate.didRemoveSectionsCalls.last!.0) === child2
219+
}
220+
221+
it("should contain the correct sections") {
222+
expect(global.sections[0]) === child1a
223+
expect(global.sections[1]) === child1b
224+
expect(global.sections[2]) === child2f
225+
expect(global.sections[3]) === child2h
226+
}
119227
}
120228
}
121229
}

0 commit comments

Comments
 (0)