Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions Sources/Composed/Providers/ComposedSectionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
})
}

/// A flag used by `performBatchUpdates(_:)` to prevent called functions
/// calling `willBeginUpdating(_:)` and `didEndUpdating(_:)`.
private var isPerformingBatchUpdates = false

public init() { }

/// Returns the number of elements in the specified section
Expand Down Expand Up @@ -156,18 +160,45 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
insert(child, at: children.count)
}

/// Perform multiple updates in a batch, only notifying the delegate that the section provider ended updating
/// when all updates have been applied.
///
/// It is safe to call this function within a batch updates closure.
///
/// - parameter updates: The closure that performs the updates.
public func performBatchUpdates(_ updates: (_ sectionProvider: ComposedSectionProvider) -> Void) {
if !isPerformingBatchUpdates {
isPerformingBatchUpdates = true
updateDelegate?.willBeginUpdating(self)

updates(self)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if passing the section provider here is really useful? It might be more confusing than helpful.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I don't think its a good idea tbh. Also definitely not required (for now anyway) so the minimum API would be better IMHO.


isPerformingBatchUpdates = false
updateDelegate?.didEndUpdating(self)
} else {
// When `isPerformingBatchUpdates` flag is set this has been called within an `updates` closure
updates(self)
}
}

/// Inserts the specified `Section` at the given index
/// - Parameters:
/// - child: The `Section` to insert
/// - index: The index where the `Section` should be inserted
public func insert(_ child: Section, at index: Int) {
guard (0...children.count).contains(index) else { fatalError("Index out of bounds: \(index)") }

updateDelegate?.willBeginUpdating(self)
if !isPerformingBatchUpdates {
updateDelegate?.willBeginUpdating(self)
}

children.insert(.section(child), at: index)
let sectionOffset = self.sectionOffset(for: child)
updateDelegate?.provider(self, didInsertSections: [child], at: IndexSet(integer: sectionOffset))
updateDelegate?.didEndUpdating(self)

if !isPerformingBatchUpdates {
updateDelegate?.didEndUpdating(self)
}
}

/// Inserts the specified `SectionProvider` at the given index
Expand All @@ -179,12 +210,18 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd

child.updateDelegate = self

updateDelegate?.willBeginUpdating(self)
if !isPerformingBatchUpdates {
updateDelegate?.willBeginUpdating(self)
}

children.insert(.provider(child), at: index)
let firstIndex = sectionOffset(for: child)
let endIndex = firstIndex + child.sections.count
updateDelegate?.provider(self, didInsertSections: child.sections, at: IndexSet(integersIn: firstIndex..<endIndex))
updateDelegate?.didEndUpdating(self)

if !isPerformingBatchUpdates {
updateDelegate?.didEndUpdating(self)
}
}

/// Removes the specified `Section`
Expand Down Expand Up @@ -225,10 +262,16 @@ open class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpd
let firstIndex = sectionOffset
let endIndex = sectionOffset + sections.count

updateDelegate?.willBeginUpdating(self)
if !isPerformingBatchUpdates {
updateDelegate?.willBeginUpdating(self)
}

children.remove(at: index)
updateDelegate?.provider(self, didRemoveSections: sections, at: IndexSet(integersIn: firstIndex..<endIndex))
updateDelegate?.didEndUpdating(self)

if !isPerformingBatchUpdates {
updateDelegate?.didEndUpdating(self)
}
}

}
Expand Down
75 changes: 75 additions & 0 deletions Tests/ComposedTests/ComposedSectionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,81 @@ final class ComposedSectionProvider_Spec: QuickSpec {
}
}
}

describe("ComposedSectionProvider.performBatchUpdates(_:)") {
var mockDelegate: MockSectionProviderUpdateDelegate!
var global: ComposedSectionProvider!
var child1: ComposedSectionProvider!
var child1a: ArraySection<String>!
var child1b: ArraySection<String>!
var child2: ComposedSectionProvider!
var child2a: ComposedSectionProvider!
var child2b: ArraySection<String>!
var child2c: ArraySection<String>!
var child2z: ComposedSectionProvider!
var child2d: ArraySection<String>!
var child2e: ComposedSectionProvider!
var child2f: ArraySection<String>!

beforeEach {
global = ComposedSectionProvider()
mockDelegate = MockSectionProviderUpdateDelegate()
global.updateDelegate = mockDelegate

child1 = ComposedSectionProvider()
child1a = ArraySection<String>()
child1b = ArraySection<String>()
child2 = ComposedSectionProvider()
child2a = ComposedSectionProvider()
child2b = ArraySection<String>()
child2c = ArraySection<String>()
child2z = ComposedSectionProvider()
child2d = ArraySection<String>()
child2e = ComposedSectionProvider()
child2f = ArraySection<String>()

global.performBatchUpdates { _ in
child1.append(child1a)
child1.insert(child1b, after: child1a)

child2.append(child2a)
child2a.append(child2c)
child2a.insert(child2b, before: child2c)

child2.insert(child2z, after: child2a)
child2.append(child2d)
child2e.append(child2f)
child2.append(child2e)
global.append(child1)
global.append(child2)
}
}

it("should contain 2 global sections") {
expect(global.numberOfSections) == 6
}

it("cache should contain 2 providers") {
expect(global.providers.count) == 2
}

it("should return the right offsets") {
expect(global.sectionOffset(for: child1)) == 0
expect(global.sectionOffset(for: child2)) == 2
expect(global.sectionOffset(for: child2a)) == 2
expect(global.sectionOffset(for: child2z)) == 4
expect(global.sectionOffset(for: child2e)) == 5

expect(child2.sectionOffset(for: child2a)) == 0
expect(child2.sectionOffset(for: child2z)) == 2
expect(child2.sectionOffset(for: child2e)) == 3
}

it("should call the delegate will begin/did end methods once") {
expect(mockDelegate.willBeginUpdatingCalls.count) == 1
expect(mockDelegate.didEndUpdatingCalls.count) == 1
}
}
}

}
Expand Down