From 60c21e26a7bb2e741d3d64fb0a88bbd202a5210c Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Fri, 24 May 2019 16:04:28 +0100 Subject: [PATCH 1/5] Add SectionProviderMapper --- Composed.xcodeproj/project.pbxproj | 42 +++- .../DataSources/SectionedDataSource.swift | 190 ------------------ Composed/Core/Sections/ArraySection.swift | 23 +++ .../Sections/ComposedSectionProvider.swift | 96 +++++++++ Composed/Core/Sections/Section.swift | 24 +++ Composed/Core/Sections/SectionProvider.swift | 70 +++++++ .../Core/Sections/SectionProviderMapper.swift | 104 ++++++++++ ...ed.swift => ComposedSectionProvider.swift} | 34 ++-- .../SectionProviderMapper+Spec.swift | 125 ++++++++++++ 9 files changed, 494 insertions(+), 214 deletions(-) create mode 100644 Composed/Core/Sections/ArraySection.swift create mode 100644 Composed/Core/Sections/ComposedSectionProvider.swift create mode 100644 Composed/Core/Sections/Section.swift create mode 100644 Composed/Core/Sections/SectionProvider.swift create mode 100644 Composed/Core/Sections/SectionProviderMapper.swift rename ComposedTests/{AllNewAndImproved.swift => ComposedSectionProvider.swift} (62%) create mode 100644 ComposedTests/SectionProviderMapper+Spec.swift diff --git a/Composed.xcodeproj/project.pbxproj b/Composed.xcodeproj/project.pbxproj index fa19a39..b1c2174 100644 --- a/Composed.xcodeproj/project.pbxproj +++ b/Composed.xcodeproj/project.pbxproj @@ -15,8 +15,7 @@ 5402214B2195C4F800F0E173 /* Composed.h in Headers */ = {isa = PBXBuildFile; fileRef = 5402213B2195C4F700F0E173 /* Composed.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5402214E2195C4F800F0E173 /* Composed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 540221392195C4F700F0E173 /* Composed.framework */; }; 5402214F2195C4F800F0E173 /* Composed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 540221392195C4F700F0E173 /* Composed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 545667902296EFFE001246DC /* AllNewAndImproved.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5456678F2296EFFE001246DC /* AllNewAndImproved.swift */; }; - 545667922297061A001246DC /* SectionProviderDelegate+Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545667912297061A001246DC /* SectionProviderDelegate+Spec.swift */; }; + 545667902296EFFE001246DC /* ComposedSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5456678F2296EFFE001246DC /* ComposedSectionProvider.swift */; }; 54645B5D2281A3AE00E53245 /* ColumnSizingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54645B5C2281A3AE00E53245 /* ColumnSizingStrategy.swift */; }; 54645B5F2281A3C100E53245 /* CarouselSizingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54645B5E2281A3C100E53245 /* CarouselSizingStrategy.swift */; }; 54645B612281A3FD00E53245 /* NoSizingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54645B602281A3FD00E53245 /* NoSizingStrategy.swift */; }; @@ -84,6 +83,12 @@ 54E5CA3F228327AC00CCA628 /* Wrapper+Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E5CA3E228327AC00CCA628 /* Wrapper+Spec.swift */; }; D937381D22609C05001F29EB /* ArrayDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D937381C22609C05001F29EB /* ArrayDataSource.swift */; }; D93738212260C641001F29EB /* SingleElementDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93738202260C641001F29EB /* SingleElementDataSource.swift */; }; + D94114092297F29900077F90 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94114082297F29900077F90 /* Section.swift */; }; + D941140B2297F30D00077F90 /* SectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140A2297F30D00077F90 /* SectionProvider.swift */; }; + D941140D2297F40500077F90 /* ComposedSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140C2297F40500077F90 /* ComposedSectionProvider.swift */; }; + D941140F2297F4FA00077F90 /* ArraySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140E2297F4FA00077F90 /* ArraySection.swift */; }; + D94114112297F86100077F90 /* SectionProviderMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94114102297F86100077F90 /* SectionProviderMapper.swift */; }; + D9D7DCBD22981094001DA4D9 /* SectionProviderMapper+Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -140,7 +145,7 @@ 540221392195C4F700F0E173 /* Composed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Composed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5402213B2195C4F700F0E173 /* Composed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Composed.h; sourceTree = ""; }; 5402213C2195C4F700F0E173 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5456678F2296EFFE001246DC /* AllNewAndImproved.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllNewAndImproved.swift; sourceTree = ""; }; + 5456678F2296EFFE001246DC /* ComposedSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposedSectionProvider.swift; sourceTree = ""; }; 545667912297061A001246DC /* SectionProviderDelegate+Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SectionProviderDelegate+Spec.swift"; sourceTree = ""; }; 54645B5C2281A3AE00E53245 /* ColumnSizingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnSizingStrategy.swift; sourceTree = ""; }; 54645B5E2281A3C100E53245 /* CarouselSizingStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselSizingStrategy.swift; sourceTree = ""; }; @@ -208,6 +213,12 @@ 54E5CA3E228327AC00CCA628 /* Wrapper+Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Wrapper+Spec.swift"; sourceTree = ""; }; D937381C22609C05001F29EB /* ArrayDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayDataSource.swift; sourceTree = ""; }; D93738202260C641001F29EB /* SingleElementDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleElementDataSource.swift; sourceTree = ""; }; + D94114082297F29900077F90 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; + D941140A2297F30D00077F90 /* SectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProvider.swift; sourceTree = ""; }; + D941140C2297F40500077F90 /* ComposedSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposedSectionProvider.swift; sourceTree = ""; }; + D941140E2297F4FA00077F90 /* ArraySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArraySection.swift; sourceTree = ""; }; + D94114102297F86100077F90 /* SectionProviderMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProviderMapper.swift; sourceTree = ""; }; + D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SectionProviderMapper+Spec.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -308,7 +319,8 @@ 54E5CA3A2283276B00CCA628 /* Complex+Spec.swift */, 54E5CA3C2283277600CCA628 /* Lifecycle+Spec.swift */, 54E5CA3E228327AC00CCA628 /* Wrapper+Spec.swift */, - 5456678F2296EFFE001246DC /* AllNewAndImproved.swift */, + 5456678F2296EFFE001246DC /* ComposedSectionProvider.swift */, + D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */, 545667912297061A001246DC /* SectionProviderDelegate+Spec.swift */, ); path = ComposedTests; @@ -343,6 +355,7 @@ 54A172F7225CC71A00A7D7FD /* Core */ = { isa = PBXGroup; children = ( + D94114072297F28C00077F90 /* Sections */, 54E5CA282282FF7D00CCA628 /* Deprecations.swift */, 54A172F8225CC71A00A7D7FD /* DataSources */, 54A172FE225CC71A00A7D7FD /* DataStores */, @@ -419,6 +432,18 @@ path = Views; sourceTree = ""; }; + D94114072297F28C00077F90 /* Sections */ = { + isa = PBXGroup; + children = ( + D94114082297F29900077F90 /* Section.swift */, + D941140A2297F30D00077F90 /* SectionProvider.swift */, + D941140C2297F40500077F90 /* ComposedSectionProvider.swift */, + D941140E2297F4FA00077F90 /* ArraySection.swift */, + D94114102297F86100077F90 /* SectionProviderMapper.swift */, + ); + path = Sections; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -581,6 +606,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D941140D2297F40500077F90 /* ComposedSectionProvider.swift in Sources */, + D941140B2297F30D00077F90 /* SectionProvider.swift in Sources */, 54A17323225CC72900A7D7FD /* PhotosDataStore.swift in Sources */, 54A17328225CC72900A7D7FD /* GlobalViewsProvidingDataSource.swift in Sources */, 54645B692282E8BC00E53245 /* MutableDataStore.swift in Sources */, @@ -590,6 +617,7 @@ 54A1733D225CC72900A7D7FD /* DataSourceInvalidationContext.swift in Sources */, 54A1731B225CC72900A7D7FD /* SectionedDataSource.swift in Sources */, 54A1733A225CC72900A7D7FD /* EmbeddedDataSourceCell.swift in Sources */, + D94114112297F86100077F90 /* SectionProviderMapper.swift in Sources */, 54645B612281A3FD00E53245 /* NoSizingStrategy.swift in Sources */, 54E5CA272282FA3400CCA628 /* ComposedChangeDetails.swift in Sources */, 54A1733C225CC72900A7D7FD /* DataSourceCoordinator.swift in Sources */, @@ -606,6 +634,7 @@ 54A1732F225CC72900A7D7FD /* NibLoadable.swift in Sources */, 54A17338225CC72900A7D7FD /* Snap.swift in Sources */, 54A1731C225CC72900A7D7FD /* SegmentedDataSource.swift in Sources */, + D941140F2297F4FA00077F90 /* ArraySection.swift in Sources */, 54A17339225CC72900A7D7FD /* DataSourceViewController.swift in Sources */, 54A1732D225CC72900A7D7FD /* ComposedMappings.swift in Sources */, 54A17327225CC72900A7D7FD /* LifecycleObservingDataSource.swift in Sources */, @@ -621,6 +650,7 @@ 54A1732E225CC72900A7D7FD /* CollectionUISizingStrategy.swift in Sources */, 54A1732B225CC72900A7D7FD /* DataSourceChangeSet.swift in Sources */, 54A17320225CC72900A7D7FD /* ArrayDataStore.swift in Sources */, + D94114092297F29900077F90 /* Section.swift in Sources */, 54645B5D2281A3AE00E53245 /* ColumnSizingStrategy.swift in Sources */, 54A17326225CC72900A7D7FD /* DataSource+Handlers.swift in Sources */, ); @@ -632,15 +662,15 @@ files = ( 54E5CA31228326E200CCA628 /* Empty+Spec.swift in Sources */, 54E5CA3D2283277600CCA628 /* Lifecycle+Spec.swift in Sources */, - 545667922297061A001246DC /* SectionProviderDelegate+Spec.swift in Sources */, 54E5CA352283270C00CCA628 /* Basic+Spec.swift in Sources */, 54E5CA2D228326BB00CCA628 /* Composed+Spec.swift in Sources */, 54E5CA3B2283276B00CCA628 /* Complex+Spec.swift in Sources */, + D9D7DCBD22981094001DA4D9 /* SectionProviderMapper+Spec.swift in Sources */, 54E5CA33228326F200CCA628 /* Sectioned+Spec.swift in Sources */, 54E5CA3F228327AC00CCA628 /* Wrapper+Spec.swift in Sources */, 54E5CA372283272800CCA628 /* Managed+Spec.swift in Sources */, 54645B952282F5D900E53245 /* Array+Spec.swift in Sources */, - 545667902296EFFE001246DC /* AllNewAndImproved.swift in Sources */, + 545667902296EFFE001246DC /* ComposedSectionProvider.swift in Sources */, 54E5CA2F228326CF00CCA628 /* Segmented+Spec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Composed/Core/DataSources/SectionedDataSource.swift b/Composed/Core/DataSources/SectionedDataSource.swift index d3778cd..5e13507 100644 --- a/Composed/Core/DataSources/SectionedDataSource.swift +++ b/Composed/Core/DataSources/SectionedDataSource.swift @@ -1,195 +1,5 @@ import Foundation -protocol UpdateDelegate: class { - func section(_ section: Section, didInsertElementAt index: Int) - func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) -} - -protocol Section: class { - var numberOfElements: Int { get } - var updateDelegate: UpdateDelegate? { get set } -} - -extension Section { - var isEmpty: Bool { return numberOfElements == 0 } -} - -protocol MutableSection: Section { } - -enum Kind { - case provider(SectionProvider) - case section(Section) -} - -protocol SectionProvider: class { - var updateDelegate: UpdateDelegate? { get set } - - var sections: [Section] { get } - - var numberOfSections: Int { get } - func numberOfElements(in section: Int) -> Int -} - -protocol AggregateSectionProvider: SectionProvider { - var providers: [SectionProvider] { get } - var cachedProviderSections: [HashableProvider: Int] { get } -} - -struct HashableProvider: Hashable { - let provider: SectionProvider - - init(_ provider: SectionProvider) { - self.provider = provider - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(provider)) - } - - static func == (lhs: HashableProvider, rhs: HashableProvider) -> Bool { - return lhs.provider === rhs.provider - } -} - -extension SectionProvider { - var isEmpty: Bool { - return sections.allSatisfy { $0.isEmpty } - } - - var numberOfSections: Int { return sections.count } - - func numberOfElements(in section: Int) -> Int { - return sections[section].numberOfElements - } -} - -final class ArraySection: MutableSection { - - weak var updateDelegate: UpdateDelegate? - var elements: [Element] = [] - - func element(at index: Int) -> Element { - return elements[index] - } - - var numberOfElements: Int { - return elements.count - } - - func append(element: Element) { - let index = elements.count - elements.append(element) - updateDelegate?.section(self, didInsertElementAt: index) - } - -} - -final class ComposedSectionProvider: AggregateSectionProvider { - - var updateDelegate: UpdateDelegate? { - didSet { - providers.forEach { $0.updateDelegate = updateDelegate } - } - } - - var cachedProviderSections: [HashableProvider: Int] { - var offset: Int = 0 - var result = [HashableProvider: Int]() - result[HashableProvider(self)] = offset - - return children.reduce(into: result) { result, store in - switch store { - case .section: - offset += 1 - case .provider(let provider): - if let provider = provider as? AggregateSectionProvider { - provider.cachedProviderSections.forEach { - result[$0.key] = $0.value + offset - } - } else { - result[HashableProvider(provider)] = offset - } - - offset += provider.numberOfSections - } - } - } - - - private var children: [Kind] = [] - - var sections: [Section] { - return children.flatMap { kind -> [Section] in - switch kind { - case let .section(section): - return [section] - case let .provider(provider): - return provider.sections - } - } - } - - var providers: [SectionProvider] { - return children.compactMap { kind in - switch kind { - case .section: return nil - case let .provider(provider): - return provider - } - } - } - - var numberOfSections: Int { - return children.reduce(into: 0, { result, kind in - switch kind { - case .section: result += 1 - case let .provider(provider): result += provider.numberOfSections - } - }) - } - - func numberOfElements(in section: Int) -> Int { - return sections[section].numberOfElements - } - - func append(_ child: SectionProvider) { - child.updateDelegate = updateDelegate - - let firstIndex = sections.count - let endIndex = firstIndex + child.sections.count - - children.append(.provider(child)) - updateDelegate?.provider(self, didInsertSections: child.sections, at: IndexSet(integersIn: firstIndex..: CollectionDataSource { public typealias Store = ArrayDataStore diff --git a/Composed/Core/Sections/ArraySection.swift b/Composed/Core/Sections/ArraySection.swift new file mode 100644 index 0000000..30dec5c --- /dev/null +++ b/Composed/Core/Sections/ArraySection.swift @@ -0,0 +1,23 @@ +import Foundation + +public final class ArraySection: MutableSection { + + public weak var updateDelegate: SectionUpdateDelegate? + + public var elements: [Element] = [] + + public func element(at index: Int) -> Element { + return elements[index] + } + + public var numberOfElements: Int { + return elements.count + } + + public func append(element: Element) { + let index = elements.count + elements.append(element) + updateDelegate?.section(self, didInsertElementAt: index) + } + +} diff --git a/Composed/Core/Sections/ComposedSectionProvider.swift b/Composed/Core/Sections/ComposedSectionProvider.swift new file mode 100644 index 0000000..bc45afb --- /dev/null +++ b/Composed/Core/Sections/ComposedSectionProvider.swift @@ -0,0 +1,96 @@ +import Foundation + +public final class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpdateDelegate { + + private enum Child { + case provider(SectionProvider) + case section(Section) + } + + public var updateDelegate: SectionProviderUpdateDelegate? + + private var children: [Child] = [] + + public var sections: [Section] { + return children.flatMap { kind -> [Section] in + switch kind { + case let .section(section): + return [section] + case let .provider(provider): + return provider.sections + } + } + } + + public var providers: [SectionProvider] { + return children.compactMap { kind in + switch kind { + case .section: return nil + case let .provider(provider): + return provider + } + } + } + + public var numberOfSections: Int { + return children.reduce(into: 0, { result, kind in + switch kind { + case .section: result += 1 + case let .provider(provider): result += provider.numberOfSections + } + }) + } + + public func numberOfElements(in section: Int) -> Int { + return sections[section].numberOfElements + } + + public func sectionOffset(for provider: SectionProvider) -> Int { + guard provider !== self else { return 0 } + + var offset: Int = 0 + + for child in children { + switch child { + case .section: + offset += 1 + case .provider(let childProvider): + if childProvider === provider { + return offset + } else if let childProvider = childProvider as? AggregateSectionProvider { + let sectionOffset = childProvider.sectionOffset(for: provider) + if sectionOffset != -1 { + return offset + sectionOffset + } + } + + offset += childProvider.numberOfSections + } + } + + // Provider is not in the hierachy + return -1 + } + + func append(_ child: SectionProvider) { + child.updateDelegate = self + + let firstIndex = sections.count + let endIndex = firstIndex + child.sections.count + + children.append(.provider(child)) + updateDelegate?.provider(self, didInsertSections: child.sections, at: IndexSet(integersIn: firstIndex.. Int + +} + +extension SectionProvider { + + public var isEmpty: Bool { + return sections.isEmpty || sections.allSatisfy { $0.isEmpty } + } + + public var numberOfSections: Int { + return sections.count + } + + public func numberOfElements(in section: Int) -> Int { + return sections[section].numberOfElements + } + +} + +public protocol AggregateSectionProvider: SectionProvider { + + var providers: [SectionProvider] { get } + + /** + Calculates the section offset for the provided section provider in the + context of the callee + + - parameter provider: The provider to calculate the section offset of + - returns: The section offset of the provided section provider, or -1 if + the section provider is not in the hierachy + */ + func sectionOffset(for provider: SectionProvider) -> Int + +} + +public struct HashableProvider: Hashable { + + public static func == (lhs: HashableProvider, rhs: HashableProvider) -> Bool { + return lhs.provider === rhs.provider + } + + private let provider: SectionProvider + + public init(_ provider: SectionProvider) { + self.provider = provider + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(provider)) + } + +} + + +public protocol SectionProviderUpdateDelegate: class { + func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) +} diff --git a/Composed/Core/Sections/SectionProviderMapper.swift b/Composed/Core/Sections/SectionProviderMapper.swift new file mode 100644 index 0000000..d2e13c6 --- /dev/null +++ b/Composed/Core/Sections/SectionProviderMapper.swift @@ -0,0 +1,104 @@ +import Foundation + +/** + An object that encapsulates the logic required to map `SectionProvider`s to + a global context, allowing elements in a `Section` to be referenced via an + `IndexPath` + */ +public final class SectionProviderMapper: SectionProviderUpdateDelegate, SectionUpdateDelegate { + + public weak var delegate: SectionProviderMapperDelegate? + + public let globalProvider: SectionProvider + + public var numberOfSections: Int { + return globalProvider.numberOfSections + } + + private var cachedProviderSections: [HashableProvider: Int] = [:] + + public init(globalProvider: SectionProvider) { + self.globalProvider = globalProvider + + globalProvider.updateDelegate = self + } + + public func sectionOffset(of sectionProdiver: SectionProvider) -> Int? { + return cachedProviderSections[HashableProvider(sectionProdiver)] + } + + public func sectionOffset(of section: Section) -> Int? { + return globalProvider.sections.firstIndex(where: { $0 === section }) + } + + public func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) { + if provider is AggregateSectionProvider { + // The inserted section couldn've been due to a new section provider + // being inserted in to the hierachy; rebuild the offsets cache + buildProviderSectionOffsets() + } + + guard let offset = sectionOffset(of: provider) else { + assertionFailure("Cannot call \(#function) with a provider not in the hierachy") + return + } + + let globalIndexes = IndexSet(indexes.map { $0 + offset }) + delegate?.sectionProviderMapper(self, didInsertSections: globalIndexes) + } + + public func section(_ section: Section, didInsertElementAt index: Int) { + guard let offset = sectionOffset(of: section) else { + assertionFailure("Cannot call \(#function) with a section not in the hierachy") + return + } + let indexPath = IndexPath(item: index, section: offset) + delegate?.sectionProviderMapper(self, didInsertElementsAt: [indexPath]) + } + + public func section(_ section: Section, didRemoveElementAt index: Int) { + + } + + public func section(_ section: Section, didMoveElementAt index: Int, to newIndex: Int) { + + } + + private func buildProviderSectionOffsets() { + var providerSections: [HashableProvider: Int] = [ + HashableProvider(globalProvider): 0, + ] + + defer { + cachedProviderSections = providerSections + } + + guard let aggregate = globalProvider as? AggregateSectionProvider else { return } + + func addOffsets(forChildrenOf aggregate: AggregateSectionProvider, offset: Int = 0) { + for child in aggregate.providers { + let aggregateSectionOffset = aggregate.sectionOffset(for: child) + guard aggregateSectionOffset > -1 else { + assertionFailure("AggregateSectionProvider shoudl return a value greater than -1 for section offset of child \(child)") + continue + } + providerSections[HashableProvider(child)] = offset + aggregateSectionOffset + + if let aggregate = child as? AggregateSectionProvider { + addOffsets(forChildrenOf: aggregate, offset: offset + aggregateSectionOffset) + } + } + } + + addOffsets(forChildrenOf: aggregate) + } + +} + +public protocol SectionProviderMapperDelegate: class { + + func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertSections sections: IndexSet) + + func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertElementsAt indexPaths: [IndexPath]) + +} diff --git a/ComposedTests/AllNewAndImproved.swift b/ComposedTests/ComposedSectionProvider.swift similarity index 62% rename from ComposedTests/AllNewAndImproved.swift rename to ComposedTests/ComposedSectionProvider.swift index 93999f7..d92ac89 100644 --- a/ComposedTests/AllNewAndImproved.swift +++ b/ComposedTests/ComposedSectionProvider.swift @@ -3,12 +3,10 @@ import Nimble @testable import Composed -final class AllNewAndImproved_Spec: QuickSpec { +final class ComposedSectionProvider_Spec: QuickSpec { override func spec() { - super.spec() - - describe("") { + describe("ComposedSectionProvider") { let global = ComposedSectionProvider() let child1 = ComposedSectionProvider() @@ -18,8 +16,8 @@ final class AllNewAndImproved_Spec: QuickSpec { let child2a = ComposedSectionProvider() let child2b = ArraySection() let child2c = ArraySection() - let child2d = ArraySection() let child2z = ComposedSectionProvider() + let child2d = ArraySection() let child2e = ComposedSectionProvider() let child2f = ArraySection() @@ -28,31 +26,31 @@ final class AllNewAndImproved_Spec: QuickSpec { child2.append(child2a) child2a.append(child2b) child2a.append(child2c) + child2.append(child2z) child2.append(child2d) child2e.append(child2f) - child2.append(child2z) child2.append(child2e) global.append(child1) global.append(child2) - let cache = global.cachedProviderSections - it("should contain 2 global sections") { expect(global.numberOfSections) == 6 } - it("cache should contain 5 providers") { - expect(cache.count) == 6 + it("cache should contain 2 providers") { + expect(global.providers.count) == 2 } - it("section offset should be 2") { - expect(cache[HashableProvider(child2)]) == 2 - expect(cache[HashableProvider(child2a)]) == 2 - expect(cache[HashableProvider(child2e)]) == 5 - - expect(child2.cachedProviderSections[HashableProvider(child2a)]) == 0 - expect(child2.cachedProviderSections[HashableProvider(child2z)]) == 3 - expect(child2.cachedProviderSections[HashableProvider(child2e)]) == 3 + 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 } } } diff --git a/ComposedTests/SectionProviderMapper+Spec.swift b/ComposedTests/SectionProviderMapper+Spec.swift new file mode 100644 index 0000000..a333d26 --- /dev/null +++ b/ComposedTests/SectionProviderMapper+Spec.swift @@ -0,0 +1,125 @@ +import Quick +import Nimble + +@testable import Composed + +final class SectionProviderMapper_Spec: QuickSpec { + + override func spec() { + describe("SectionProviderMapper") { + context("with a composed section provider") { + var global: ComposedSectionProvider! + var mapper: SectionProviderMapper! + var delegate: MockSectionProviderMapperDelegate! + + beforeEach { + global = ComposedSectionProvider() + mapper = SectionProviderMapper(globalProvider: global) + delegate = MockSectionProviderMapperDelegate() + mapper.delegate = delegate + } + + it("should become the delegate of the section provider") { + expect(global.updateDelegate) === mapper + } + + context("when a child section has been added to the global provider") { + var child: Section! + + beforeEach { + child = ArraySection() + global.append(child) + } + + it("should call the sectionProviderMapper(_:didInsertSections:) delegate function") { + expect(delegate.didInsertSections).toNot(beNil()) + } + + it("should notify the delegate of the inserted section") { + expect(delegate.didInsertSections!.sections) == IndexSet(integer: 0) + } + } + + context("with a composed section provider that contains 2 child sections") { + var level1EmbeddedSectionProvider: ComposedSectionProvider! + var level1Section1: Section! + var level1Section2: Section! + + beforeEach { + level1EmbeddedSectionProvider = ComposedSectionProvider() + level1Section1 = ArraySection() + level1Section2 = ArraySection() + level1EmbeddedSectionProvider.append(level1Section1) + level1EmbeddedSectionProvider.append(level1Section2) + + global.append(level1EmbeddedSectionProvider) + } + + it("should return 2 sections") { + expect(mapper.numberOfSections) == 2 + } + + context("and a composed section provider with 3 sections") { + var level2EmbeddedSectionProvider: ComposedSectionProvider! + var level2Section1: Section! + var level2Section2: Section! + var level2Section3: Section! + + beforeEach { + level2EmbeddedSectionProvider = ComposedSectionProvider() + level2Section1 = ArraySection() + level2Section2 = ArraySection() + level2Section3 = ArraySection() + level2EmbeddedSectionProvider.append(level2Section1) + level2EmbeddedSectionProvider.append(level2Section2) + level2EmbeddedSectionProvider.append(level2Section3) + + level1EmbeddedSectionProvider.append(level2EmbeddedSectionProvider) + } + + it("should return 5 sections") { + expect(mapper.numberOfSections) == 5 + } + + it("should return a section offset of 2 for the composed section provider") { + expect(mapper.sectionOffset(of: level2EmbeddedSectionProvider)) == 2 + } + + it("should notify the delegate of the inserted sections") { + expect(delegate.didInsertSections!.sections) == IndexSet(2...4) + } + } + } + + context("with embedded composed providers") { + var level1EmbeddedSectionProvider: ComposedSectionProvider! + var level2EmbeddedSectionProvider: ComposedSectionProvider! + + beforeEach { + level1EmbeddedSectionProvider = ComposedSectionProvider() + level2EmbeddedSectionProvider = ComposedSectionProvider() + level1EmbeddedSectionProvider.append(level2EmbeddedSectionProvider) + global.append(level1EmbeddedSectionProvider) + } + } + } + } + } + +} + +final class MockSectionProviderMapperDelegate: SectionProviderMapperDelegate { + + var didInsertSections: (sectionProviderMapper: SectionProviderMapper, sections: IndexSet)? + + var didInsertElements: (section: SectionProviderMapper, indexPaths: [IndexPath])? + + func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertSections sections: IndexSet) { + didInsertSections = (sectionProviderMapper, sections) + } + + func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertElementsAt indexPaths: [IndexPath]) { + didInsertElements = (sectionProviderMapper, indexPaths) + } + +} From 43dca6198f76a14fe4b8d6f041c438aa58cda0b8 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Fri, 24 May 2019 19:04:18 +0100 Subject: [PATCH 2/5] Add basic support for UICollectionView No layout, registration is done every dequeue, many other issues :) --- Composed.xcodeproj/project.pbxproj | 24 +++++++ Composed/Core/Sections/ArraySection.swift | 8 ++- ...ectionViewSectionProviderCoordinator.swift | 60 ++++++++++++++++++ .../Sections/ComposedSectionProvider.swift | 6 +- .../UI/CollectionUIConfiguration.swift | 24 +++++++ .../UI/SectionCollectionUIConfiguration.swift | 58 +++++++++++++++++ Playground/Base.lproj/Main.storyboard | 62 ++++++++++++++++++- Playground/SectionsViewController.swift | 35 +++++++++++ 8 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift create mode 100644 Composed/Core/Sections/UI/CollectionUIConfiguration.swift create mode 100644 Composed/Core/Sections/UI/SectionCollectionUIConfiguration.swift create mode 100644 Playground/SectionsViewController.swift diff --git a/Composed.xcodeproj/project.pbxproj b/Composed.xcodeproj/project.pbxproj index b1c2174..dc3d7eb 100644 --- a/Composed.xcodeproj/project.pbxproj +++ b/Composed.xcodeproj/project.pbxproj @@ -89,6 +89,10 @@ D941140F2297F4FA00077F90 /* ArraySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140E2297F4FA00077F90 /* ArraySection.swift */; }; D94114112297F86100077F90 /* SectionProviderMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94114102297F86100077F90 /* SectionProviderMapper.swift */; }; D9D7DCBD22981094001DA4D9 /* SectionProviderMapper+Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */; }; + D9D7DCBF22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCBE22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift */; }; + D9D7DCC122985F04001DA4D9 /* SectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCC022985F04001DA4D9 /* SectionsViewController.swift */; }; + D9D7DCC422986949001DA4D9 /* SectionCollectionUIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCC322986949001DA4D9 /* SectionCollectionUIConfiguration.swift */; }; + D9D7DCC62298695E001DA4D9 /* CollectionUIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCC52298695E001DA4D9 /* CollectionUIConfiguration.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -219,6 +223,10 @@ D941140E2297F4FA00077F90 /* ArraySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArraySection.swift; sourceTree = ""; }; D94114102297F86100077F90 /* SectionProviderMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProviderMapper.swift; sourceTree = ""; }; D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SectionProviderMapper+Spec.swift"; sourceTree = ""; }; + D9D7DCBE22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSectionProviderCoordinator.swift; sourceTree = ""; }; + D9D7DCC022985F04001DA4D9 /* SectionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionsViewController.swift; sourceTree = ""; }; + D9D7DCC322986949001DA4D9 /* SectionCollectionUIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionCollectionUIConfiguration.swift; sourceTree = ""; }; + D9D7DCC52298695E001DA4D9 /* CollectionUIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionUIConfiguration.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -275,6 +283,7 @@ isa = PBXGroup; children = ( 540221222195C44F00F0E173 /* AppDelegate.swift */, + D9D7DCC022985F04001DA4D9 /* SectionsViewController.swift */, 54BAB15D221497FA0064CE51 /* GlobalHeaderView.swift */, 54BAB15F221498000064CE51 /* GlobalHeaderView.xib */, 54BAB1632214D05C0064CE51 /* GlobalFooterView.swift */, @@ -440,10 +449,21 @@ D941140C2297F40500077F90 /* ComposedSectionProvider.swift */, D941140E2297F4FA00077F90 /* ArraySection.swift */, D94114102297F86100077F90 /* SectionProviderMapper.swift */, + D9D7DCBE22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift */, + D9D7DCC222986942001DA4D9 /* UI */, ); path = Sections; sourceTree = ""; }; + D9D7DCC222986942001DA4D9 /* UI */ = { + isa = PBXGroup; + children = ( + D9D7DCC52298695E001DA4D9 /* CollectionUIConfiguration.swift */, + D9D7DCC322986949001DA4D9 /* SectionCollectionUIConfiguration.swift */, + ); + path = UI; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -598,6 +618,7 @@ 54BAB15E221497FA0064CE51 /* GlobalHeaderView.swift in Sources */, 54BAB1642214D05C0064CE51 /* GlobalFooterView.swift in Sources */, 548A8B702214FA2800C0A276 /* BackgroundView.swift in Sources */, + D9D7DCC122985F04001DA4D9 /* SectionsViewController.swift in Sources */, 540221232195C45000F0E173 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -626,6 +647,7 @@ 54A17324225CC72900A7D7FD /* AggregateDataSource.swift in Sources */, 54A17334225CC72900A7D7FD /* FlowLayout+Metrics.swift in Sources */, 54A17330225CC72900A7D7FD /* FlowLayoutInvalidationContext.swift in Sources */, + D9D7DCBF22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift in Sources */, 54A17325225CC72900A7D7FD /* CollectionDataSource.swift in Sources */, 54A17331225CC72900A7D7FD /* FlowLayout+Helpers.swift in Sources */, 54A17322225CC72900A7D7FD /* DataStore.swift in Sources */, @@ -633,6 +655,7 @@ 54A1732A225CC72900A7D7FD /* CollectionUIProvidingDataSource.swift in Sources */, 54A1732F225CC72900A7D7FD /* NibLoadable.swift in Sources */, 54A17338225CC72900A7D7FD /* Snap.swift in Sources */, + D9D7DCC62298695E001DA4D9 /* CollectionUIConfiguration.swift in Sources */, 54A1731C225CC72900A7D7FD /* SegmentedDataSource.swift in Sources */, D941140F2297F4FA00077F90 /* ArraySection.swift in Sources */, 54A17339225CC72900A7D7FD /* DataSourceViewController.swift in Sources */, @@ -647,6 +670,7 @@ 54A17337225CC72900A7D7FD /* DataSourceHeaderFooterView.swift in Sources */, 54A1731F225CC72900A7D7FD /* ComposedDataSource.swift in Sources */, 54A1733B225CC72900A7D7FD /* CollectionUIViewProvider.swift in Sources */, + D9D7DCC422986949001DA4D9 /* SectionCollectionUIConfiguration.swift in Sources */, 54A1732E225CC72900A7D7FD /* CollectionUISizingStrategy.swift in Sources */, 54A1732B225CC72900A7D7FD /* DataSourceChangeSet.swift in Sources */, 54A17320225CC72900A7D7FD /* ArrayDataStore.swift in Sources */, diff --git a/Composed/Core/Sections/ArraySection.swift b/Composed/Core/Sections/ArraySection.swift index 30dec5c..c2ba0cb 100644 --- a/Composed/Core/Sections/ArraySection.swift +++ b/Composed/Core/Sections/ArraySection.swift @@ -1,10 +1,14 @@ import Foundation -public final class ArraySection: MutableSection { +open class ArraySection: MutableSection { public weak var updateDelegate: SectionUpdateDelegate? - public var elements: [Element] = [] + public var elements: [Element] + + public init(elements: [Element] = []) { + self.elements = elements + } public func element(at index: Int) -> Element { return elements[index] diff --git a/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift b/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift new file mode 100644 index 0000000..1cf720a --- /dev/null +++ b/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift @@ -0,0 +1,60 @@ +import UIKit + +public final class CollectionViewSectionProviderCoordinator: NSObject, UICollectionViewDataSource, SectionProviderMapperDelegate { + + private let mapper: SectionProviderMapper + private let collectionView: UICollectionView + + public init(collectionView: UICollectionView, sectionProvider: SectionProvider) { + self.collectionView = collectionView + mapper = SectionProviderMapper(globalProvider: sectionProvider) + + super.init() + + collectionView.dataSource = self + } + + // MARK: - SectionProviderMapperDelegate + + public func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertSections sections: IndexSet) { + collectionView.insertSections(sections) + } + + public func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertElementsAt indexPaths: [IndexPath]) { + collectionView.insertItems(at: indexPaths) + } + + // MARK: - UICollectionViewDataSource + + public func numberOfSections(in collectionView: UICollectionView) -> Int { + return mapper.numberOfSections + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return collectionUIConfigurationProvider(for: section)?.numberOfElements ?? 0 + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let configuration = collectionUIConfigurationProvider(for: indexPath.section) else { + fatalError("No UI configuration available for section \(indexPath.section)") + } + + let type = Swift.type(of: configuration.prototype) + switch configuration.dequeueMethod { + case .nib: + let nib = UINib(nibName: String(describing: type), bundle: Bundle(for: type)) + collectionView.register(nib, forCellWithReuseIdentifier: configuration.reuseIdentifier) + case .class: + collectionView.register(type, forCellWithReuseIdentifier: configuration.reuseIdentifier) + } + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: configuration.reuseIdentifier, for: indexPath) + configuration.configure(cell: cell, at: indexPath.row) + return cell + } + + private func collectionUIConfigurationProvider(for section: Int) -> CollectionUIConfiguration? { + return (mapper.globalProvider.sections[section] as? CollectionUIConfigurationProvider)?.collectionUIConfiguration + } + +} diff --git a/Composed/Core/Sections/ComposedSectionProvider.swift b/Composed/Core/Sections/ComposedSectionProvider.swift index bc45afb..86d809f 100644 --- a/Composed/Core/Sections/ComposedSectionProvider.swift +++ b/Composed/Core/Sections/ComposedSectionProvider.swift @@ -41,6 +41,8 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro }) } + public init() { } + public func numberOfElements(in section: Int) -> Int { return sections[section].numberOfElements } @@ -72,7 +74,7 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro return -1 } - func append(_ child: SectionProvider) { + public func append(_ child: SectionProvider) { child.updateDelegate = self let firstIndex = sections.count @@ -82,7 +84,7 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro updateDelegate?.provider(self, didInsertSections: child.sections, at: IndexSet(integersIn: firstIndex.. UICollectionReusableView + private var _prototypeView: UICollectionReusableView? + + public var prototype: UICollectionReusableView { + if let view = _prototypeView { return view } + let view = prototypeProvider() + _prototypeView = view + return view + } + + private weak var section: Section? + private let configureCell: (UICollectionViewCell, Int) -> Void + + public init(section: Section, prototype: @escaping @autoclosure () -> Cell, cellDequeueMethod: DequeueMethod, cellReuseIdentifier: String? = nil, cellConfigurator: @escaping (Cell, Int, Section) -> Void, headerConfiguration: CollectionUIViewProvider? = nil, footerConfiguration: CollectionUIViewProvider? = nil, backgroundViewClass: UICollectionReusableView.Type? = nil) { + self.section = section + self.prototypeProvider = prototype + self.dequeueMethod = cellDequeueMethod + self.configureCell = { [weak section] c, index in + guard let cell = c as? Cell else { + assertionFailure("Got an unknown cell. Expecting cell of type \(Cell.self), got \(c)") + return + } + guard let section = section else { + assertionFailure("Asked to configure cell after section has been deallocated") + return + } + cellConfigurator(cell, index, section) + } + self.headerConfiguration = headerConfiguration + self.footerConfiguration = footerConfiguration + self.backgroundViewClass = backgroundViewClass + } + + public func configure(cell: UICollectionViewCell, at index: Int) { + configureCell(cell, index) + } + +} diff --git a/Playground/Base.lproj/Main.storyboard b/Playground/Base.lproj/Main.storyboard index cdd6929..9dc1717 100644 --- a/Playground/Base.lproj/Main.storyboard +++ b/Playground/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -23,7 +23,7 @@ - + @@ -31,16 +31,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Playground/SectionsViewController.swift b/Playground/SectionsViewController.swift new file mode 100644 index 0000000..caecc3b --- /dev/null +++ b/Playground/SectionsViewController.swift @@ -0,0 +1,35 @@ +import UIKit +import Composed + +final class SectionsViewController: UICollectionViewController { + + private var coordinator: CollectionViewSectionProviderCoordinator! + + override func viewDidLoad() { + super.viewDidLoad() + + let personSection = PersonSection(elements: [ + Person(name: "Joseph Duffy", age: 25), + Person(name: "Joseph Duffy", age: 26), + Person(name: "Joseph Duffy", age: 27), + Person(name: "Joseph Duffy", age: 28), + Person(name: "Joseph Duffy", age: 29), + Person(name: "Joseph Duffy", age: 30), + ]) + let sectionProvider = ComposedSectionProvider() + sectionProvider.append(personSection) + coordinator = CollectionViewSectionProviderCoordinator(collectionView: collectionView, sectionProvider: sectionProvider) + } + +} + +final class PersonSection: ArraySection, CollectionUIConfigurationProvider { + + private(set) lazy var collectionUIConfiguration: CollectionUIConfiguration = { + return SectionCollectionUIConfiguration(section: self, prototype: PersonCell.fromNib, cellDequeueMethod: .nib, cellConfigurator: { cell, index, section in + let person = section.element(at: index) + cell.prepare(person: person) + }) + }() + +} From de5dcf9ea184d2ae2784da8042c71529d80c8071 Mon Sep 17 00:00:00 2001 From: Shaps Benkau Date: Thu, 4 Jul 2019 11:01:34 +0100 Subject: [PATCH 3/5] Added missing update methods (untested) and also some light renaming/refactoring --- Composed.xcodeproj/project.pbxproj | 8 +- Composed/Core/Sections/ArraySection.swift | 16 +- ...ectionViewSectionProviderCoordinator.swift | 42 ++- .../Sections/ComposedSectionProvider.swift | 47 ++- Composed/Core/Sections/Section.swift | 9 +- Composed/Core/Sections/SectionProvider.swift | 38 +- .../Core/Sections/SectionProviderMapper.swift | 104 ------ .../Sections/SectionProviderMapping.swift | 352 ++++++++++++++++++ .../SectionProviderMapper+Spec.swift | 28 +- Playground/Base.lproj/Main.storyboard | 15 +- Playground/PersonCell.xib | 18 +- Playground/SectionsViewController.swift | 1 + 12 files changed, 471 insertions(+), 207 deletions(-) delete mode 100644 Composed/Core/Sections/SectionProviderMapper.swift create mode 100644 Composed/Core/Sections/SectionProviderMapping.swift diff --git a/Composed.xcodeproj/project.pbxproj b/Composed.xcodeproj/project.pbxproj index dc3d7eb..9f2916a 100644 --- a/Composed.xcodeproj/project.pbxproj +++ b/Composed.xcodeproj/project.pbxproj @@ -87,7 +87,7 @@ D941140B2297F30D00077F90 /* SectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140A2297F30D00077F90 /* SectionProvider.swift */; }; D941140D2297F40500077F90 /* ComposedSectionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140C2297F40500077F90 /* ComposedSectionProvider.swift */; }; D941140F2297F4FA00077F90 /* ArraySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D941140E2297F4FA00077F90 /* ArraySection.swift */; }; - D94114112297F86100077F90 /* SectionProviderMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94114102297F86100077F90 /* SectionProviderMapper.swift */; }; + D94114112297F86100077F90 /* SectionProviderMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94114102297F86100077F90 /* SectionProviderMapping.swift */; }; D9D7DCBD22981094001DA4D9 /* SectionProviderMapper+Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */; }; D9D7DCBF22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCBE22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift */; }; D9D7DCC122985F04001DA4D9 /* SectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D7DCC022985F04001DA4D9 /* SectionsViewController.swift */; }; @@ -221,7 +221,7 @@ D941140A2297F30D00077F90 /* SectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProvider.swift; sourceTree = ""; }; D941140C2297F40500077F90 /* ComposedSectionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposedSectionProvider.swift; sourceTree = ""; }; D941140E2297F4FA00077F90 /* ArraySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArraySection.swift; sourceTree = ""; }; - D94114102297F86100077F90 /* SectionProviderMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProviderMapper.swift; sourceTree = ""; }; + D94114102297F86100077F90 /* SectionProviderMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProviderMapping.swift; sourceTree = ""; }; D9D7DCBC22981094001DA4D9 /* SectionProviderMapper+Spec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SectionProviderMapper+Spec.swift"; sourceTree = ""; }; D9D7DCBE22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSectionProviderCoordinator.swift; sourceTree = ""; }; D9D7DCC022985F04001DA4D9 /* SectionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionsViewController.swift; sourceTree = ""; }; @@ -448,7 +448,7 @@ D941140A2297F30D00077F90 /* SectionProvider.swift */, D941140C2297F40500077F90 /* ComposedSectionProvider.swift */, D941140E2297F4FA00077F90 /* ArraySection.swift */, - D94114102297F86100077F90 /* SectionProviderMapper.swift */, + D94114102297F86100077F90 /* SectionProviderMapping.swift */, D9D7DCBE22985C44001DA4D9 /* CollectionViewSectionProviderCoordinator.swift */, D9D7DCC222986942001DA4D9 /* UI */, ); @@ -638,7 +638,7 @@ 54A1733D225CC72900A7D7FD /* DataSourceInvalidationContext.swift in Sources */, 54A1731B225CC72900A7D7FD /* SectionedDataSource.swift in Sources */, 54A1733A225CC72900A7D7FD /* EmbeddedDataSourceCell.swift in Sources */, - D94114112297F86100077F90 /* SectionProviderMapper.swift in Sources */, + D94114112297F86100077F90 /* SectionProviderMapping.swift in Sources */, 54645B612281A3FD00E53245 /* NoSizingStrategy.swift in Sources */, 54E5CA272282FA3400CCA628 /* ComposedChangeDetails.swift in Sources */, 54A1733C225CC72900A7D7FD /* DataSourceCoordinator.swift in Sources */, diff --git a/Composed/Core/Sections/ArraySection.swift b/Composed/Core/Sections/ArraySection.swift index c2ba0cb..c2179e9 100644 --- a/Composed/Core/Sections/ArraySection.swift +++ b/Composed/Core/Sections/ArraySection.swift @@ -1,27 +1,27 @@ import Foundation -open class ArraySection: MutableSection { - +open class ArraySection: Section { + public weak var updateDelegate: SectionUpdateDelegate? - + public var elements: [Element] - + public init(elements: [Element] = []) { self.elements = elements } - + public func element(at index: Int) -> Element { return elements[index] } - + public var numberOfElements: Int { return elements.count } - + public func append(element: Element) { let index = elements.count elements.append(element) updateDelegate?.section(self, didInsertElementAt: index) } - + } diff --git a/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift b/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift index 1cf720a..4ce6449 100644 --- a/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift +++ b/Composed/Core/Sections/CollectionViewSectionProviderCoordinator.swift @@ -1,29 +1,51 @@ import UIKit -public final class CollectionViewSectionProviderCoordinator: NSObject, UICollectionViewDataSource, SectionProviderMapperDelegate { +public final class CollectionViewSectionProviderCoordinator: NSObject, UICollectionViewDataSource, SectionProviderMappingDelegate { - private let mapper: SectionProviderMapper + private let mapper: SectionProviderMapping private let collectionView: UICollectionView public init(collectionView: UICollectionView, sectionProvider: SectionProvider) { self.collectionView = collectionView - mapper = SectionProviderMapper(globalProvider: sectionProvider) + mapper = SectionProviderMapping(provider: sectionProvider) super.init() collectionView.dataSource = self } - // MARK: - SectionProviderMapperDelegate - - public func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertSections sections: IndexSet) { + // MARK: - SectionProviderMappingDelegate + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { collectionView.insertSections(sections) } - - public func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertElementsAt indexPaths: [IndexPath]) { + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { collectionView.insertItems(at: indexPaths) } - + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + collectionView.deleteSections(sections) + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + collectionView.deleteItems(at: indexPaths) + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + collectionView.reloadSections(sections) + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + collectionView.reloadItems(at: indexPaths) + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + moves.forEach { + collectionView.moveItem(at: $0.0, to: $0.1) + } + } + // MARK: - UICollectionViewDataSource public func numberOfSections(in collectionView: UICollectionView) -> Int { @@ -54,7 +76,7 @@ public final class CollectionViewSectionProviderCoordinator: NSObject, UICollect } private func collectionUIConfigurationProvider(for section: Int) -> CollectionUIConfiguration? { - return (mapper.globalProvider.sections[section] as? CollectionUIConfigurationProvider)?.collectionUIConfiguration + return (mapper.provider.sections[section] as? CollectionUIConfigurationProvider)?.collectionUIConfiguration } } diff --git a/Composed/Core/Sections/ComposedSectionProvider.swift b/Composed/Core/Sections/ComposedSectionProvider.swift index 86d809f..6306350 100644 --- a/Composed/Core/Sections/ComposedSectionProvider.swift +++ b/Composed/Core/Sections/ComposedSectionProvider.swift @@ -1,16 +1,16 @@ import Foundation public final class ComposedSectionProvider: AggregateSectionProvider, SectionProviderUpdateDelegate { - + private enum Child { case provider(SectionProvider) case section(Section) } - + public var updateDelegate: SectionProviderUpdateDelegate? - + private var children: [Child] = [] - + public var sections: [Section] { return children.flatMap { kind -> [Section] in switch kind { @@ -21,7 +21,7 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro } } } - + public var providers: [SectionProvider] { return children.compactMap { kind in switch kind { @@ -31,7 +31,7 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro } } } - + public var numberOfSections: Int { return children.reduce(into: 0, { result, kind in switch kind { @@ -40,18 +40,18 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro } }) } - + public init() { } - + public func numberOfElements(in section: Int) -> Int { return sections[section].numberOfElements } - + public func sectionOffset(for provider: SectionProvider) -> Int { guard provider !== self else { return 0 } - + var offset: Int = 0 - + for child in children { switch child { case .section: @@ -65,34 +65,41 @@ public final class ComposedSectionProvider: AggregateSectionProvider, SectionPro return offset + sectionOffset } } - + offset += childProvider.numberOfSections } } - + // Provider is not in the hierachy return -1 } - + public func append(_ child: SectionProvider) { child.updateDelegate = self - + let firstIndex = sections.count let endIndex = firstIndex + child.sections.count - + children.append(.provider(child)) updateDelegate?.provider(self, didInsertSections: child.sections, at: IndexSet(integersIn: firstIndex.. Int - + } extension SectionProvider { - + public var isEmpty: Bool { return sections.isEmpty || sections.allSatisfy { $0.isEmpty } } - + public var numberOfSections: Int { return sections.count } - + public func numberOfElements(in section: Int) -> Int { return sections[section].numberOfElements } - + } public protocol AggregateSectionProvider: SectionProvider { var providers: [SectionProvider] { get } - + /** Calculates the section offset for the provided section provider in the context of the callee - + - parameter provider: The provider to calculate the section offset of - returns: The section offset of the provided section provider, or -1 if - the section provider is not in the hierachy + the section provider is not in the hierachy */ func sectionOffset(for provider: SectionProvider) -> Int - + } public struct HashableProvider: Hashable { - + public static func == (lhs: HashableProvider, rhs: HashableProvider) -> Bool { return lhs.provider === rhs.provider } - + private let provider: SectionProvider - + public init(_ provider: SectionProvider) { self.provider = provider } - + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(provider)) } @@ -67,4 +67,6 @@ public struct HashableProvider: Hashable { public protocol SectionProviderUpdateDelegate: class { func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) + func provider(_ provider: SectionProvider, didRemoveSections sections: [Section], at indexes: IndexSet) + func provider(_ provider: SectionProvider, didUpdateSections sections: [Section], at indexes: IndexSet) } diff --git a/Composed/Core/Sections/SectionProviderMapper.swift b/Composed/Core/Sections/SectionProviderMapper.swift deleted file mode 100644 index d2e13c6..0000000 --- a/Composed/Core/Sections/SectionProviderMapper.swift +++ /dev/null @@ -1,104 +0,0 @@ -import Foundation - -/** - An object that encapsulates the logic required to map `SectionProvider`s to - a global context, allowing elements in a `Section` to be referenced via an - `IndexPath` - */ -public final class SectionProviderMapper: SectionProviderUpdateDelegate, SectionUpdateDelegate { - - public weak var delegate: SectionProviderMapperDelegate? - - public let globalProvider: SectionProvider - - public var numberOfSections: Int { - return globalProvider.numberOfSections - } - - private var cachedProviderSections: [HashableProvider: Int] = [:] - - public init(globalProvider: SectionProvider) { - self.globalProvider = globalProvider - - globalProvider.updateDelegate = self - } - - public func sectionOffset(of sectionProdiver: SectionProvider) -> Int? { - return cachedProviderSections[HashableProvider(sectionProdiver)] - } - - public func sectionOffset(of section: Section) -> Int? { - return globalProvider.sections.firstIndex(where: { $0 === section }) - } - - public func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) { - if provider is AggregateSectionProvider { - // The inserted section couldn've been due to a new section provider - // being inserted in to the hierachy; rebuild the offsets cache - buildProviderSectionOffsets() - } - - guard let offset = sectionOffset(of: provider) else { - assertionFailure("Cannot call \(#function) with a provider not in the hierachy") - return - } - - let globalIndexes = IndexSet(indexes.map { $0 + offset }) - delegate?.sectionProviderMapper(self, didInsertSections: globalIndexes) - } - - public func section(_ section: Section, didInsertElementAt index: Int) { - guard let offset = sectionOffset(of: section) else { - assertionFailure("Cannot call \(#function) with a section not in the hierachy") - return - } - let indexPath = IndexPath(item: index, section: offset) - delegate?.sectionProviderMapper(self, didInsertElementsAt: [indexPath]) - } - - public func section(_ section: Section, didRemoveElementAt index: Int) { - - } - - public func section(_ section: Section, didMoveElementAt index: Int, to newIndex: Int) { - - } - - private func buildProviderSectionOffsets() { - var providerSections: [HashableProvider: Int] = [ - HashableProvider(globalProvider): 0, - ] - - defer { - cachedProviderSections = providerSections - } - - guard let aggregate = globalProvider as? AggregateSectionProvider else { return } - - func addOffsets(forChildrenOf aggregate: AggregateSectionProvider, offset: Int = 0) { - for child in aggregate.providers { - let aggregateSectionOffset = aggregate.sectionOffset(for: child) - guard aggregateSectionOffset > -1 else { - assertionFailure("AggregateSectionProvider shoudl return a value greater than -1 for section offset of child \(child)") - continue - } - providerSections[HashableProvider(child)] = offset + aggregateSectionOffset - - if let aggregate = child as? AggregateSectionProvider { - addOffsets(forChildrenOf: aggregate, offset: offset + aggregateSectionOffset) - } - } - } - - addOffsets(forChildrenOf: aggregate) - } - -} - -public protocol SectionProviderMapperDelegate: class { - - func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertSections sections: IndexSet) - - func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertElementsAt indexPaths: [IndexPath]) - -} diff --git a/Composed/Core/Sections/SectionProviderMapping.swift b/Composed/Core/Sections/SectionProviderMapping.swift new file mode 100644 index 0000000..982b2e5 --- /dev/null +++ b/Composed/Core/Sections/SectionProviderMapping.swift @@ -0,0 +1,352 @@ +// Do we really need to import UIKit here in order to access the collection views IndexPath(item:section:)? +import Foundation + +/** + An object tha + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) { + <#code#> + } + + public func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) { + <#code#> + } + t encapsulates the logic required to map `SectionProvider`s to + a global context, allowing elements in a `Section` to be referenced via an + `IndexPath` + */ +public final class SectionProviderMapping: SectionProviderUpdateDelegate, SectionUpdateDelegate { + + public weak var delegate: SectionProviderMappingDelegate? + + public let provider: SectionProvider + + public var numberOfSections: Int { + return provider.numberOfSections + } + + private var cachedProviderSections: [HashableProvider: Int] = [:] + + public init(provider: SectionProvider) { + self.provider = provider + provider.updateDelegate = self + } + + public func sectionOffset(of provider: SectionProvider) -> Int? { + return cachedProviderSections[HashableProvider(provider)] + } + + public func sectionOffset(of section: Section) -> Int? { + return provider.sections.firstIndex(where: { $0 === section }) + } + + private func globalIndexes(for provider: SectionProvider, with indexes: IndexSet) -> IndexSet { + if provider is AggregateSectionProvider { + // The inserted section couldn've been due to a new section provider + // being inserted in to the hierachy; rebuild the offsets cache + rebuildSectionOffsets() + } + + guard let offset = sectionOffset(of: provider) else { + assertionFailure("Cannot call \(#function) with a provider not in the hierachy") + return [] + } + + return IndexSet(indexes.map { $0 + offset }) + } + + public func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) { + let indexes = globalIndexes(for: provider, with: indexes) + delegate?.mapping(self, didInsertSections: indexes) + // add sections.count to all sections >= sectionOffset(for: provider) + } + + public func provider(_ provider: SectionProvider, didRemoveSections sections: [Section], at indexes: IndexSet) { + let indexes = globalIndexes(for: provider, with: indexes) + delegate?.mapping(self, didRemoveSections: indexes) + // minus sections.count to all sections >= sectionOffset(for: provider) + } + + public func provider(_ provider: SectionProvider, didUpdateSections sections: [Section], at indexes: IndexSet) { + guard let offset = sectionOffset(of: provider) else { + assertionFailure("Cannot call \(#function) with a provider not in the hierachy") + return + } + let indexes = IndexSet(indexes.map { $0 + offset }) + delegate?.mapping(self, didUpdateSections: indexes) + } + + private func indexPath(for index: Int, in section: Section) -> IndexPath? { + guard let offset = sectionOffset(of: section) else { + assertionFailure("Cannot call \(#function) with a section not in the hierachy") + return nil + } + return IndexPath(item: index, section: offset) + } + + public func section(_ section: Section, didInsertElementAt index: Int) { + guard let indexPath = self.indexPath(for: index, in: section) else { return } + delegate?.mapping(self, didInsertElementsAt: [indexPath]) + } + + public func section(_ section: Section, didRemoveElementAt index: Int) { + guard let indexPath = self.indexPath(for: index, in: section) else { return } + delegate?.mapping(self, didRemoveElementsAt: [indexPath]) + } + + public func section(_ section: Section, didUpdateElementAt index: Int) { + guard let indexPath = self.indexPath(for: index, in: section) else { return } + delegate?.mapping(self, didUpdateElementsAt: [indexPath]) + } + + public func section(_ section: Section, didMoveElementAt index: Int, to newIndex: Int) { + guard let source = self.indexPath(for: index, in: section) else { return } + guard let destination = self.indexPath(for: newIndex, in: section) else { return } + delegate?.mapping(self, didMoveElementsAt: [(source, destination)]) + } + + private func rebuildSectionOffsets() { + var providerSections: [HashableProvider: Int] = [HashableProvider(provider): 0] + + defer { + cachedProviderSections = providerSections + } + + // Joseph: can we actually just return here? + guard let aggregate = provider as? AggregateSectionProvider else { return } + + func addOffsets(forChildrenOf aggregate: AggregateSectionProvider, offset: Int = 0) { + for child in aggregate.providers { + let aggregateSectionOffset = aggregate.sectionOffset(for: child) + guard aggregateSectionOffset > -1 else { + assertionFailure("AggregateSectionProvider should return a value > -1 for section offset of child \(child)") + continue + } + providerSections[HashableProvider(child)] = offset + aggregateSectionOffset + + if let aggregate = child as? AggregateSectionProvider { + addOffsets(forChildrenOf: aggregate, offset: offset + aggregateSectionOffset) + } + } + } + + addOffsets(forChildrenOf: aggregate) + } + +} + +public protocol SectionProviderMappingDelegate: class { + func mapping(_ mapping: SectionProviderMapping, didInsertSections sections: IndexSet) + func mapping(_ mapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) + + func mapping(_ mapping: SectionProviderMapping, didRemoveSections sections: IndexSet) + func mapping(_ mapping: SectionProviderMapping, didRemoveElementsAt indexPaths: [IndexPath]) + + func mapping(_ mapping: SectionProviderMapping, didUpdateSections sections: IndexSet) + func mapping(_ mapping: SectionProviderMapping, didUpdateElementsAt indexPaths: [IndexPath]) + + func mapping(_ mapping: SectionProviderMapping, didMoveElementsAt moves: [(IndexPath, IndexPath)]) +} + +// We don't want to import UIKit here so we create the same IndexPath init. +// This will 'just work' on the consumer's end. +private extension IndexPath { + var section: Int { + return self[0] + } + + var item: Int { + return self[1] + } + + init(item: Int, section: Int) { + self.init(indexes: [section, item]) + } +} diff --git a/ComposedTests/SectionProviderMapper+Spec.swift b/ComposedTests/SectionProviderMapper+Spec.swift index a333d26..6da7ed8 100644 --- a/ComposedTests/SectionProviderMapper+Spec.swift +++ b/ComposedTests/SectionProviderMapper+Spec.swift @@ -3,19 +3,19 @@ import Nimble @testable import Composed -final class SectionProviderMapper_Spec: QuickSpec { +final class SectionProviderMapping_Spec: QuickSpec { override func spec() { - describe("SectionProviderMapper") { + describe("SectionProviderMapping") { context("with a composed section provider") { var global: ComposedSectionProvider! - var mapper: SectionProviderMapper! - var delegate: MockSectionProviderMapperDelegate! + var mapper: SectionProviderMapping! + var delegate: MockSectionProviderMappingDelegate! beforeEach { global = ComposedSectionProvider() - mapper = SectionProviderMapper(globalProvider: global) - delegate = MockSectionProviderMapperDelegate() + mapper = SectionProviderMapping(globalProvider: global) + delegate = MockSectionProviderMappingDelegate() mapper.delegate = delegate } @@ -31,7 +31,7 @@ final class SectionProviderMapper_Spec: QuickSpec { global.append(child) } - it("should call the sectionProviderMapper(_:didInsertSections:) delegate function") { + it("should call the SectionProviderMapping(_:didInsertSections:) delegate function") { expect(delegate.didInsertSections).toNot(beNil()) } @@ -108,18 +108,18 @@ final class SectionProviderMapper_Spec: QuickSpec { } -final class MockSectionProviderMapperDelegate: SectionProviderMapperDelegate { +final class MockSectionProviderMappingDelegate: SectionProviderMappingDelegate { - var didInsertSections: (sectionProviderMapper: SectionProviderMapper, sections: IndexSet)? + var didInsertSections: (SectionProviderMapping: SectionProviderMapping, sections: IndexSet)? - var didInsertElements: (section: SectionProviderMapper, indexPaths: [IndexPath])? + var didInsertElements: (section: SectionProviderMapping, indexPaths: [IndexPath])? - func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertSections sections: IndexSet) { - didInsertSections = (sectionProviderMapper, sections) + func SectionProviderMapping(_ SectionProviderMapping: SectionProviderMapping, didInsertSections sections: IndexSet) { + didInsertSections = (SectionProviderMapping, sections) } - func sectionProviderMapper(_ sectionProviderMapper: SectionProviderMapper, didInsertElementsAt indexPaths: [IndexPath]) { - didInsertElements = (sectionProviderMapper, indexPaths) + func SectionProviderMapping(_ SectionProviderMapping: SectionProviderMapping, didInsertElementsAt indexPaths: [IndexPath]) { + didInsertElements = (SectionProviderMapping, indexPaths) } } diff --git a/Playground/Base.lproj/Main.storyboard b/Playground/Base.lproj/Main.storyboard index 9dc1717..055c023 100644 --- a/Playground/Base.lproj/Main.storyboard +++ b/Playground/Base.lproj/Main.storyboard @@ -72,21 +72,12 @@ - + - - - - - - - - - - + @@ -96,7 +87,7 @@ - + diff --git a/Playground/PersonCell.xib b/Playground/PersonCell.xib index b2cd1fb..3a903ed 100644 --- a/Playground/PersonCell.xib +++ b/Playground/PersonCell.xib @@ -13,20 +13,20 @@ - + - + - - + + - + - + - + diff --git a/Playground/SectionsViewController.swift b/Playground/SectionsViewController.swift index caecc3b..0973881 100644 --- a/Playground/SectionsViewController.swift +++ b/Playground/SectionsViewController.swift @@ -19,6 +19,7 @@ final class SectionsViewController: UICollectionViewController { let sectionProvider = ComposedSectionProvider() sectionProvider.append(personSection) coordinator = CollectionViewSectionProviderCoordinator(collectionView: collectionView, sectionProvider: sectionProvider) + collectionView.alwaysBounceVertical = true } } From 2d5684889ccae21debf26af2dc0a87fc21c6e0ba Mon Sep 17 00:00:00 2001 From: Shaps Benkau Date: Mon, 22 Jul 2019 17:37:37 +0100 Subject: [PATCH 4/5] Minor updates to the sample --- Playground/Base.lproj/Main.storyboard | 12 +++++------- Playground/SectionsViewController.swift | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Playground/Base.lproj/Main.storyboard b/Playground/Base.lproj/Main.storyboard index 055c023..ae8f6be 100644 --- a/Playground/Base.lproj/Main.storyboard +++ b/Playground/Base.lproj/Main.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -16,7 +14,7 @@ - + @@ -51,7 +49,7 @@ - + diff --git a/Playground/SectionsViewController.swift b/Playground/SectionsViewController.swift index 0973881..dced66f 100644 --- a/Playground/SectionsViewController.swift +++ b/Playground/SectionsViewController.swift @@ -1,7 +1,7 @@ import UIKit import Composed -final class SectionsViewController: UICollectionViewController { +final class SectionsViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { private var coordinator: CollectionViewSectionProviderCoordinator! @@ -16,10 +16,28 @@ final class SectionsViewController: UICollectionViewController { Person(name: "Joseph Duffy", age: 29), Person(name: "Joseph Duffy", age: 30), ]) + let sectionProvider = ComposedSectionProvider() sectionProvider.append(personSection) - coordinator = CollectionViewSectionProviderCoordinator(collectionView: collectionView, sectionProvider: sectionProvider) + sectionProvider.append(personSection) + collectionView.alwaysBounceVertical = true + + let layout = FlowLayout() + layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + collectionView.collectionViewLayout = layout + + coordinator = CollectionViewSectionProviderCoordinator(collectionView: collectionView, sectionProvider: sectionProvider) + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + personSection.append(element: Person(name: "Shaps", age: 39)) + } + } + + let cell = PersonCell.fromNib + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let target = CGSize(width: collectionView.bounds.width - 40, height: 0) + return cell.contentView.systemLayoutSizeFitting(target, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) } } From 4e0f8e7e72d4b082ad60de58d4d3e44a1990dbcb Mon Sep 17 00:00:00 2001 From: Shaps Benkau Date: Mon, 22 Jul 2019 19:15:42 +0100 Subject: [PATCH 5/5] Updated gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e7d1857..60e8271 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ ## Project Specific -API.swift +.build/ ## Build generated build/