diff --git a/Sources/Composed/Core/Section.swift b/Sources/Composed/Core/Section.swift index d240971..2524b29 100644 --- a/Sources/Composed/Core/Section.swift +++ b/Sources/Composed/Core/Section.swift @@ -82,4 +82,13 @@ public protocol SectionUpdateDelegate: class { /// - destinationIndex: The final index where the element will be moved to func section(_ section: Section, move sourceIndex: Int, to destinationIndex: Int) + /// Notifies the delegate that the section invalidated its header. + /// - Parameters: + /// - section: The section that invalidated its header. + func sectionDidInvalidateHeader(_ section: Section) + + /// Notifies the delegate that the section invalidated its footer. + /// - Parameters: + /// - section: The section that invalidated its footer. + func sectionDidInvalidateFooter(_ section: Section) } diff --git a/Sources/Composed/Core/SectionProviderMapping.swift b/Sources/Composed/Core/SectionProviderMapping.swift index 303fab4..d3eaa11 100644 --- a/Sources/Composed/Core/SectionProviderMapping.swift +++ b/Sources/Composed/Core/SectionProviderMapping.swift @@ -82,6 +82,16 @@ public protocol SectionProviderMappingDelegate: class { /// - destinationIndexPath: The final indexPath func mapping(_ mapping: SectionProviderMapping, move sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) + /// Notifies the delegate that the section invalidated its header. + /// - Parameters: + /// - sectionIndex: The index of the section that invalidated its header. + func mappingDidInvalidateHeader(at sectionIndex: Int) + + /// Notifies the delegate that the section invalidated its footer. + /// - Parameters: + /// - sectionIndex: The index of the section that invalidated its footer. + func mappingDidInvalidateFooter(at sectionIndex: Int) + } /// An object that encapsulates the logic required to map `SectionProvider`s to a global context, @@ -229,6 +239,16 @@ public final class SectionProviderMapping: SectionProviderUpdateDelegate, Sectio delegate?.mapping(self, didMoveElementsAt: [(source, destination)]) } + public func sectionDidInvalidateHeader(_ section: Section) { + guard let sectionOffset = self.sectionOffset(of: section) else { return } + delegate?.mappingDidInvalidateHeader(at: sectionOffset) + } + + public func sectionDidInvalidateFooter(_ section: Section) { + guard let sectionOffset = self.sectionOffset(of: section) else { return } + delegate?.mappingDidInvalidateFooter(at: sectionOffset) + } + // Rebuilds the cached providers to improve lookup performance. // This is generally only required when a sections are either inserted or removed, so it should be fairly efficient. private func rebuildSectionOffsets() { diff --git a/Sources/Composed/Sections/FlatSection+SelectionHandler.swift b/Sources/Composed/Sections/FlatSection+SelectionHandler.swift new file mode 100644 index 0000000..44502a6 --- /dev/null +++ b/Sources/Composed/Sections/FlatSection+SelectionHandler.swift @@ -0,0 +1,105 @@ +extension FlatSection: SelectionHandler { + public final var selectionHandlingSections: [SelectionHandler] { + sections.compactMap { $0 as? SelectionHandler } + } + + open var allowsMultipleSelection: Bool { + let selectionHandlingSection = sections.compactMap { $0 as? SelectionHandler } + if selectionHandlingSection.isEmpty { + return false + } else if selectionHandlingSection.count == 1 { + return selectionHandlingSection.first!.allowsMultipleSelection + } else { + return true + } + } + + /// Returns all element indexes that are currently selected + open var selectedIndexes: [Int] { + return sections.flatMap { section -> [Int] in + guard let section = section as? SelectionHandler else { return [] } + let offset = self.indexForFirstElement(of: section)! + return section.selectedIndexes.map { $0 + offset } + } + } + + /// When a highlight is attempted, this method will be called giving the caller a chance to prevent it + /// - Parameter index: The element index + open func shouldHighlight(at index: Int) -> Bool { + guard let sectionMeta = self.sectionForElementIndex(index) else { return false } + guard let section = sectionMeta.section as? SelectionHandler else { return false } + + let sectionIndex = index - sectionMeta.offset + return section.shouldHighlight(at: sectionIndex) + } + + /// When a selection is attempted, this method will be called giving the caller a chance to prevent it + /// - Parameter index: The element index + open func shouldSelect(at index: Int) -> Bool { + guard let sectionMeta = self.sectionForElementIndex(index) else { return false } + guard let section = sectionMeta.section as? SelectionHandler else { return false } + + let sectionIndex = index - sectionMeta.offset + return section.shouldSelect(at: sectionIndex) + } + + /// When a selection occurs, this method will be called to notify the section + /// - Parameter index: The element index + open func didSelect(at index: Int) { + guard let sectionMeta = self.sectionForElementIndex(index) else { return } + guard let section = sectionMeta.section as? SelectionHandler else { return } + + let sectionIndex = index - sectionMeta.offset + section.didSelect(at: sectionIndex) + } + + /// When a deselection is attempted, this method will be called giving the caller a chance to prevent it + /// - Parameter index: The element index + open func shouldDeselect(at index: Int) -> Bool { + guard let sectionMeta = self.sectionForElementIndex(index) else { return false } + guard let section = sectionMeta.section as? SelectionHandler else { return false } + + let sectionIndex = index - sectionMeta.offset + return section.shouldDeselect(at: sectionIndex) + } + + /// When a deselection occurs, this method will be called to notify the section + /// - Parameter index: The element index + open func didDeselect(at index: Int) { + guard let sectionMeta = self.sectionForElementIndex(index) else { return } + guard let section = sectionMeta.section as? SelectionHandler else { return } + + let sectionIndex = index - sectionMeta.offset + return section.didDeselect(at: sectionIndex) + } + + /// Selects the element at the specified index + /// - Parameter index: The element index + open func select(index: Int) { + guard let sectionMeta = self.sectionForElementIndex(index) else { return } + guard let section = sectionMeta.section as? SelectionHandler else { return } + + let sectionIndex = index - sectionMeta.offset + section.select(index: sectionIndex) + } + + /// Deselects the element at the specified index + /// - Parameter index: The element index + open func deselect(index: Int) { + guard let sectionMeta = self.sectionForElementIndex(index) else { return } + guard let section = sectionMeta.section as? SelectionHandler else { return } + + let sectionIndex = index - sectionMeta.offset + section.deselect(index: sectionIndex) + } + + /// Selects all elements in this section + open func selectAll() { + selectionHandlingSections.forEach { $0.selectAll() } + } + + /// Deselects all elements in this section + open func deselectAll() { + selectionHandlingSections.forEach { $0.deselectAll() } + } +} diff --git a/Sources/Composed/Sections/FlatSection.swift b/Sources/Composed/Sections/FlatSection.swift new file mode 100644 index 0000000..e51b5f8 --- /dev/null +++ b/Sources/Composed/Sections/FlatSection.swift @@ -0,0 +1,433 @@ +import Foundation + +/// A section that flattens each of its children in to a single section. +open class FlatSection: Section, CustomReflectable { + private enum Child { + /// A single section. + case section(Section) + + /// An object that provides 0 or more sections. + case sectionProvider(SectionProvider) + } + + public private(set) var sections: ContiguousArray
= [] + + public var numberOfElements: Int { + sections.map(\.numberOfElements).reduce(0, +) + } + + public weak var updateDelegate: SectionUpdateDelegate? + + public var customMirror: Mirror { + Mirror( + self, + children: [ + "children": children, + ] + ) + } + + private var children: [Child] = [] + + public init() {} + + public func append(_ section: Section) { + updateDelegate?.willBeginUpdating(self) + + let indexOfFirstChildElement = numberOfElements + children.append(.section(section)) + sections.append(section) + section.updateDelegate = self + + (0.. (section: Section, offset: Int)? { + var offset = 0 + + for child in children { + switch child { + case .section(let section): + if !section.isEmpty, offset == index { + return (section, offset) + } else if index < offset + section.numberOfElements { + return (section, offset) + } + + offset += section.numberOfElements + case .sectionProvider(let sectionProvider): + for section in sectionProvider.sections { + if !section.isEmpty, offset == index { + return (section, offset) + } else if index < offset + section.numberOfElements { + return (section, offset) + } + + offset += section.numberOfElements + } + } + } + + return nil + } + + public final func indexForFirstElement(of section: Section) -> Int? { + var offset = 0 + + for childSection in sections { + if childSection === section { + return offset + } + + offset += childSection.numberOfElements + } + + return nil + } + + public final func indexForFirstElement(of sectionProvider: SectionProvider) -> Int? { + var offset = 0 + + for child in children { + switch child { + case .section(let section): + offset += section.numberOfElements + case .sectionProvider(let childSectionProvider): + if childSectionProvider === sectionProvider { + return offset + } else if let aggregate = childSectionProvider as? AggregateSectionProvider, let sectionOffset = aggregate.sectionOffset(for: sectionProvider) { + return childSectionProvider.sections[0.. Range? { + guard let sectionOffset = indexForFirstElement(of: section) else { return nil } + return (sectionOffset.. Int? { + var index = 0 + + for child in children { + switch child { + case .section(let childSection): + if childSection === section { + return index + } + case .sectionProvider: + break + } + + index += 1 + } + + return nil + } + + public final func childIndex(of sectionProvider: SectionProvider) -> Int? { + var index = 0 + + for child in children { + switch child { + case .section: + break + case .sectionProvider(let childSectionProvider): + if childSectionProvider === sectionProvider { + return index + } + } + + index += 1 + } + + return nil + } + + private func indexForFirstElement(of child: Child) -> Int? { + switch child { + case .section(let childSection): + return indexForFirstElement(of: childSection) + case .sectionProvider(let sectionProvider): + return indexForFirstElement(of: sectionProvider) + } + } + + /// The index of the first section of `sectionProvider` in the `sections` array. + /// + /// - parameter sectionProvider: The section provider to calculate the section index of. + /// - returns: The index of the first section of `sectionProvider` in the `sections` array, or `nil` if it is not in the hierarchy. + private func sectionIndex(of sectionProvider: SectionProvider) -> Int? { + var index = 0 + + for child in children { + switch child { + case .section: + index += 1 + case .sectionProvider(let childSectionProvider): + if childSectionProvider === sectionProvider { + return index + } else if let aggregate = childSectionProvider as? AggregateSectionProvider, let offset = aggregate.sectionOffset(for: sectionProvider) { + return index + offset + } + + index += childSectionProvider.numberOfSections + } + } + + return nil + } +} + +extension FlatSection: SectionUpdateDelegate { + public func willBeginUpdating(_ section: Section) { + updateDelegate?.willBeginUpdating(self) + } + + public func didEndUpdating(_ section: Section) { + updateDelegate?.didEndUpdating(self) + } + + public func invalidateAll(_ section: Section) { + updateDelegate?.invalidateAll(self) + } + + public func section(_ section: Section, didInsertElementAt index: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, didInsertElementAt: sectionOffset + index) + } + + public func section(_ section: Section, didRemoveElementAt index: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, didRemoveElementAt: sectionOffset + index) + } + + public func section(_ section: Section, didUpdateElementAt index: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, didUpdateElementAt: sectionOffset + index) + } + + public func section(_ section: Section, didMoveElementAt index: Int, to newIndex: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, didMoveElementAt: sectionOffset + index, to: newIndex + index) + } + + public func selectedIndexes(in section: Section) -> [Int] { + guard let allSelectedIndexes = updateDelegate?.selectedIndexes(in: self) else { return [] } + guard let sectionIndexes = indexesRange(for: section) else { return [] } + + return allSelectedIndexes + .filter(sectionIndexes.contains(_:)) + .map { $0 - sectionIndexes.startIndex } + } + + public func section(_ section: Section, select index: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, select: sectionOffset + index) + } + + public func section(_ section: Section, deselect index: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, deselect: sectionOffset + index) + } + + public func section(_ section: Section, move sourceIndex: Int, to destinationIndex: Int) { + guard let sectionOffset = indexForFirstElement(of: section) else { return } + updateDelegate?.section(self, move: sourceIndex + sectionOffset, to: destinationIndex + sectionOffset) + } + + public func sectionDidInvalidateHeader(_ section: Section) { + // Headers of children are currently ignored. + } + + public func sectionDidInvalidateFooter(_ section: Section) { + // Footers of children are currently ignored. + } +} + +extension FlatSection: SectionProviderUpdateDelegate { + public func willBeginUpdating(_ provider: SectionProvider) { + updateDelegate?.willBeginUpdating(self) + } + + public func didEndUpdating(_ provider: SectionProvider) { + updateDelegate?.didEndUpdating(self) + } + + public func invalidateAll(_ provider: SectionProvider) { + sections = ContiguousArray(children.flatMap { child -> [Section] in + switch child { + case .section(let section): + return [section] + case .sectionProvider(let sectionProvider): + return sectionProvider.sections + } + }) + updateDelegate?.invalidateAll(self) + } + + public func provider(_ provider: SectionProvider, didInsertSections sections: [Section], at indexes: IndexSet) { + guard let providerSectionIndex = sectionIndex(of: provider) else { + assertionFailure(#function + " has been called for a provider that is not a child") + return + } + + updateDelegate?.willBeginUpdating(self) + + for (section, index) in zip(sections, indexes) { + section.updateDelegate = self + + let sectionIndex = index + providerSectionIndex + self.sections.insert(section, at: sectionIndex) + let firstSectionIndex = self.indexForFirstElement(of: section)! + + (firstSectionIndex..() /// Make a new coordinator with the specified collectionView and sectionProvider @@ -153,44 +154,55 @@ open class CollectionCoordinator: NSObject { open func invalidateVisibleCells() { for (indexPath, cell) in zip(collectionView.indexPathsForVisibleItems, collectionView.visibleCells) { let elements = elementsProvider(for: indexPath.section) - elements.cell.configure(cell, indexPath.item, mapper.provider.sections[indexPath.section]) + elements.cell(for: indexPath.item).configure(cell, indexPath.item, mapper.provider.sections[indexPath.section]) } } // Prepares and caches the section to improve performance private func prepareSections() { - cachedProviders.removeAll() + cachedElementsProviders.removeAll() mapper.delegate = self for index in 0.. UICollectionViewCell { assert(Thread.isMainThread) let elements = elementsProvider(for: indexPath.section) - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: elements.cell.reuseIdentifier, for: indexPath) + let cellElement = elements.cell(for: indexPath.item) + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellElement.reuseIdentifier, for: indexPath) if let handler = sectionProvider.sections[indexPath.section] as? EditingHandler { if let handler = sectionProvider.sections[indexPath.section] as? CollectionEditingHandler { @@ -426,7 +494,9 @@ extension CollectionCoordinator: UICollectionViewDataSource { } } - elements.cell.configure(cell, indexPath.item, mapper.provider.sections[indexPath.section]) + let section = mapper.provider.sections[indexPath.section] + cellSectionMap[cell] = (cellElement, section) + cellElement.configure(cell, indexPath.item, section) return cell } @@ -436,14 +506,17 @@ extension CollectionCoordinator: UICollectionViewDataSource { originalDelegate?.collectionView?(collectionView, willDisplaySupplementaryView: view, forElementKind: elementKind, at: indexPath) } - guard indexPath.section > sectionProvider.numberOfSections else { return } + guard indexPath.section < sectionProvider.numberOfSections else { return } + let elements = elementsProvider(for: indexPath.section) let section = mapper.provider.sections[indexPath.section] if let header = elements.header, header.kind.rawValue == elementKind { - elements.header?.willAppear?(view, indexPath.section, section) + header.willAppear?(view, indexPath.section, section) + header.configure(view, indexPath.section, section) } else if let footer = elements.footer, footer.kind.rawValue == elementKind { - elements.footer?.willAppear?(view, indexPath.section, section) + footer.willAppear?(view, indexPath.section, section) + footer.configure(view, indexPath.section, section) } else { // the original delegate can handle this } @@ -465,7 +538,7 @@ extension CollectionCoordinator: UICollectionViewDataSource { } else { guard let view = originalDataSource?.collectionView?(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath) else { // when in production its better to return 'something' to prevent crashing - assertionFailure("Unsupported supplementary kind: \(kind) at indexPath: \(indexPath). Did you forget to register your header or footer?") + assertionFailure("Unsupported supplementary kind: \(kind) at indexPath: \(indexPath). Check if your layout it returning attributes for the supplementary element at \(indexPath)") return collectionView.dequeue(supplementary: PlaceholderSupplementaryView.self, ofKind: PlaceholderSupplementaryView.kind, for: indexPath) } @@ -492,11 +565,11 @@ extension CollectionCoordinator: UICollectionViewDataSource { } } - private func elementsProvider(for section: Int) -> CollectionElementsProvider { - guard cachedProviders.indices.contains(section) else { + private func elementsProvider(for section: Int) -> UICollectionViewSectionElementsProvider { + guard cachedElementsProviders.indices.contains(section) else { fatalError("No UI configuration available for section \(section)") } - return cachedProviders[section] + return cachedElementsProviders[section] } } diff --git a/Sources/ComposedUI/CollectionView/CollectionElement.swift b/Sources/ComposedUI/CollectionView/CollectionElement.swift index 7f25546..fbcd116 100644 --- a/Sources/ComposedUI/CollectionView/CollectionElement.swift +++ b/Sources/ComposedUI/CollectionView/CollectionElement.swift @@ -20,11 +20,8 @@ public enum CollectionElementKind { /// Defines an element used by a `CollectionSection` to provide configurations for a cell, header and/or footer. public protocol CollectionElement { - /// A typealias for representing a `UICollectionReusableView` - associatedtype View: UICollectionReusableView - /// The method to use for registering and dequeueing a view for this element - var dequeueMethod: DequeueMethod { get } + var dequeueMethod: AnyDequeueMethod { get } /// A closure that will be called whenever the elements view needs to be configured var configure: (UICollectionReusableView, Int, Section) -> Void { get } @@ -32,19 +29,30 @@ public protocol CollectionElement { /// The reuseIdentifier to use for this element var reuseIdentifier: String { get } + /// A closure that will be called before the elements view is appeared + var willAppear: ((UICollectionReusableView, Int, Section) -> Void)? { get } + + /// A closure that will be called after the elements view has disappeared + var didDisappear: ((UICollectionReusableView, Int, Section) -> Void)? { get } + +} + +extension CollectionElement { + public var willAppear: ((UICollectionReusableView, Int, Section) -> Void)? { nil } + public var didDisappear: ((UICollectionReusableView, Int, Section) -> Void)? { nil } } /// Defines a cell element to be used by a `CollectionSection` to provide a configuration for a cell -public final class CollectionCellElement: CollectionElement where View: UICollectionViewCell { +open class CollectionCellElement: CollectionElement { - public let dequeueMethod: DequeueMethod + public let dequeueMethod: AnyDequeueMethod public let configure: (UICollectionReusableView, Int, Section) -> Void public let reuseIdentifier: String /// The closure that will be called before the elements view appears - public let willAppear: (UICollectionReusableView, Int, Section) -> Void + public let willAppear: ((UICollectionReusableView, Int, Section) -> Void)? /// The closure that will be called after the elements view disappears - public let didDisappear: (UICollectionReusableView, Int, Section) -> Void + public let didDisappear: ((UICollectionReusableView, Int, Section) -> Void)? /// Makes a new element for representing a cell /// - Parameters: @@ -52,22 +60,46 @@ public final class CollectionCellElement: CollectionElement where View: UI /// - dequeueMethod: The method to use for registering and dequeueing a cell for this element /// - reuseIdentifier: The reuseIdentifier to use for this element /// - configure: A closure that will be called whenever the elements view needs to be configured - public init
(section: Section, + public init(section: Section, dequeueMethod: DequeueMethod, reuseIdentifier: String? = nil, configure: @escaping (View, Int, Section) -> Void) - where Section: Composed.Section { - self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier - self.dequeueMethod = dequeueMethod + where Section: Composed.Section { + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod.erasedAsAnyDequeueMethod - // swiftlint:disable force_cast + // swiftlint:disable force_cast + + self.configure = { view, index, section in + configure(view as! View, index, section as! Section) + } - self.configure = { view, index, section in - configure(view as! View, index, section as! Section) - } + willAppear = nil + didDisappear = nil + } + + /// Makes a new element for representing a cell + /// - Parameters: + /// - section: The section where this element's cell will be shown in + /// - dequeueMethod: The method to use for registering and dequeueing a cell for this element + /// - reuseIdentifier: The reuseIdentifier to use for this element + /// - configure: A closure that will be called whenever the elements view needs to be configured + public init(section: Section, + dequeueMethod: AnyDequeueMethod, + reuseIdentifier: String? = nil, + configure: @escaping (View, Int, Section) -> Void) + where Section: Composed.Section { + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod + + // swiftlint:disable force_cast + + self.configure = { view, index, section in + configure(view as! View, index, section as! Section) + } - willAppear = { _, _, _ in } - didDisappear = { _, _, _ in } + willAppear = nil + didDisappear = nil } /// Makes a new element for representing a cell @@ -78,37 +110,101 @@ public final class CollectionCellElement: CollectionElement where View: UI /// - configure: A closure that will be called whenever the elements view needs to be configured /// - willAppear: A closure that will be called before the elements view appears /// - didDisappear: A closure that will be called after the elements view disappears - public init
(section: Section, + public init(section: Section, dequeueMethod: DequeueMethod, reuseIdentifier: String? = nil, configure: @escaping (View, Int, Section) -> Void, willAppear: ((View, Int, Section) -> Void)? = nil, didDisappear: ((View, Int, Section) -> Void)? = nil) - where Section: Composed.Section { - self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier - self.dequeueMethod = dequeueMethod + where Section: Composed.Section { + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod.erasedAsAnyDequeueMethod - // swiftlint:disable force_cast + // swiftlint:disable force_cast + + self.configure = { view, index, section in + configure(view as! View, index, section as! Section) + } - self.configure = { view, index, section in - configure(view as! View, index, section as! Section) - } + self.willAppear = { view, index, section in + willAppear?(view as! View, index, section as! Section) + } + + self.didDisappear = { view, index, section in + didDisappear?(view as! View, index, section as! Section) + } + } + + /// Makes a new element for representing a cell + /// - Parameters: + /// - section: The section where this element's cell will be shown in + /// - dequeueMethod: The method to use for registering and dequeueing a cell for this element + /// - reuseIdentifier: The reuseIdentifier to use for this element + /// - configure: A closure that will be called whenever the elements view needs to be configured + /// - willAppear: A closure that will be called before the elements view appears + /// - didDisappear: A closure that will be called after the elements view disappears + public init(section: Section, + dequeueMethod: AnyDequeueMethod, + reuseIdentifier: String? = nil, + configure: @escaping (View, Int, Section) -> Void, + willAppear: ((View, Int, Section) -> Void)? = nil, + didDisappear: ((View, Int, Section) -> Void)? = nil) + where Section: Composed.Section { + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod + + // swiftlint:disable force_cast + + self.configure = { view, index, section in + configure(view as! View, index, section as! Section) + } + + self.willAppear = { view, index, section in + willAppear?(view as! View, index, section as! Section) + } + + self.didDisappear = { view, index, section in + didDisappear?(view as! View, index, section as! Section) + } + } + + /// Makes a new element for representing a cell + /// - Parameters: + /// - dequeueMethod: The method to use for registering and dequeueing a cell for this element + /// - reuseIdentifier: The reuseIdentifier to use for this element + /// - configure: A closure that will be called whenever the elements view needs to be configured + /// - willAppear: A closure that will be called before the elements view appears + /// - didDisappear: A closure that will be called after the elements view disappears + public init( + dequeueMethod: AnyDequeueMethod, + reuseIdentifier: String? = nil, + configure: @escaping (View, Int) -> Void, + willAppear: ((View, Int) -> Void)? = nil, + didDisappear: ((View, Int) -> Void)? = nil) { + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod + + // swiftlint:disable force_cast + + self.configure = { view, index, _ in + configure(view as! View, index) + } - self.willAppear = { view, index, section in - willAppear?(view as! View, index, section as! Section) - } + self.willAppear = { view, index, _ in + willAppear?(view as! View, index) + } - self.didDisappear = { view, index, section in - didDisappear?(view as! View, index, section as! Section) - } + self.didDisappear = { view, index, _ in + didDisappear?(view as! View, index) + } } } /// Defines a supplementary element to be used by a `CollectionSection` to provide a configuration for a supplementary view -public final class CollectionSupplementaryElement: CollectionElement where View: UICollectionReusableView { +public final class CollectionSupplementaryElement: CollectionElement { - public let dequeueMethod: DequeueMethod + public let dequeueMethod: AnyDequeueMethod public let configure: (UICollectionReusableView, Int, Section) -> Void public let reuseIdentifier: String @@ -127,23 +223,51 @@ public final class CollectionSupplementaryElement: CollectionElement where /// - reuseIdentifier: The reuseIdentifier to use for this element /// - kind: The `elementKind` this element represents /// - configure: A closure that will be called whenever the elements view needs to be configured - public init
(section: Section, - dequeueMethod: DequeueMethod, - reuseIdentifier: String? = nil, - kind: CollectionElementKind = .automatic, - configure: @escaping (View, Int, Section) -> Void) - where Section: Composed.Section { - self.kind = kind - self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier - self.dequeueMethod = dequeueMethod - - self.configure = { view, index, section in - // swiftlint:disable force_cast - configure(view as! View, index, section as! Section) - } - - willAppear = nil - didDisappear = nil + public init( + section: Section, + dequeueMethod: DequeueMethod, + reuseIdentifier: String? = nil, + kind: CollectionElementKind = .automatic, + configure: @escaping (_ view: View, _ sectionIndex: Int, _ section: Section) -> Void + ) where Section: Composed.Section { + self.kind = kind + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod.erasedAsAnyDequeueMethod + + self.configure = { view, index, section in + // swiftlint:disable force_cast + configure(view as! View, index, section as! Section) + } + + willAppear = nil + didDisappear = nil + } + + /// Makes a new element for representing a supplementary view + /// - Parameters: + /// - section: The section where this element's view will be shown in + /// - dequeueMethod: The method to use for registering and dequeueing a view for this element + /// - reuseIdentifier: The reuseIdentifier to use for this element + /// - kind: The `elementKind` this element represents + /// - configure: A closure that will be called whenever the elements view needs to be configured + public init( + section: Section, + dequeueMethod: AnyDequeueMethod, + reuseIdentifier: String? = nil, + kind: CollectionElementKind = .automatic, + configure: @escaping (_ view: View, _ sectionIndex: Int, _ section: Section) -> Void + ) where Section: Composed.Section { + self.kind = kind + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod + + self.configure = { view, index, section in + // swiftlint:disable force_cast + configure(view as! View, index, section as! Section) + } + + willAppear = nil + didDisappear = nil } /// Makes a new element for representing a supplementary view @@ -155,31 +279,68 @@ public final class CollectionSupplementaryElement: CollectionElement where /// - configure: A closure that will be called whenever the elements view needs to be configured /// - willAppear: A closure that will be called before the elements view appears /// - didDisappear: A closure that will be called after the elements view disappears - public init
(section: Section, + public init( + section: Section, dequeueMethod: DequeueMethod, reuseIdentifier: String? = nil, kind: CollectionElementKind = .automatic, configure: @escaping (View, Int, Section) -> Void, willAppear: ((View, Int, Section) -> Void)? = nil, - didDisappear: ((View, Int, Section) -> Void)? = nil) - where Section: Composed.Section { - self.kind = kind - self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier - self.dequeueMethod = dequeueMethod + didDisappear: ((View, Int, Section) -> Void)? = nil + ) where Section: Composed.Section { + self.kind = kind + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod.erasedAsAnyDequeueMethod - // swiftlint:disable force_cast + // swiftlint:disable force_cast + + self.configure = { view, index, section in + configure(view as! View, index, section as! Section) + } - self.configure = { view, index, section in - configure(view as! View, index, section as! Section) - } + self.willAppear = { view, index, section in + willAppear?(view as! View, index, section as! Section) + } - self.willAppear = { view, index, section in - willAppear?(view as! View, index, section as! Section) - } + self.didDisappear = { view, index, section in + didDisappear?(view as! View, index, section as! Section) + } + } - self.didDisappear = { view, index, section in - didDisappear?(view as! View, index, section as! Section) - } + /// Makes a new element for representing a supplementary view + /// - Parameters: + /// - section: The section where this element's view will be shown in + /// - dequeueMethod: The method to use for registering and dequeueing a view for this element + /// - reuseIdentifier: The reuseIdentifier to use for this element + /// - kind: The `elementKind` this element represents + /// - configure: A closure that will be called whenever the elements view needs to be configured + /// - willAppear: A closure that will be called before the elements view appears + /// - didDisappear: A closure that will be called after the elements view disappears + public init( + dequeueMethod: DequeueMethod, + reuseIdentifier: String? = nil, + kind: CollectionElementKind = .automatic, + configure: @escaping (View, Int) -> Void, + willAppear: ((View, Int) -> Void)? = nil, + didDisappear: ((View, Int) -> Void)? = nil + ) { + self.kind = kind + self.reuseIdentifier = reuseIdentifier ?? View.reuseIdentifier + self.dequeueMethod = dequeueMethod.erasedAsAnyDequeueMethod + + // swiftlint:disable force_cast + + self.configure = { view, index, _ in + configure(view as! View, index) + } + + self.willAppear = { view, index, _ in + willAppear?(view as! View, index) + } + + self.didDisappear = { view, index, _ in + didDisappear?(view as! View, index) + } } } diff --git a/Sources/ComposedUI/CollectionView/CollectionSection.swift b/Sources/ComposedUI/CollectionView/CollectionSection.swift index 02261ba..9718f5c 100644 --- a/Sources/ComposedUI/CollectionView/CollectionSection.swift +++ b/Sources/ComposedUI/CollectionView/CollectionSection.swift @@ -3,16 +3,16 @@ import Composed /// Defines a configuration for a section in a `UICollectionView`. /// The section must contain a cell element, but can also optionally include a header and/or footer element. -open class CollectionSection: CollectionElementsProvider { +open class CollectionSection: SingleUICollectionViewSectionElementsProvider { /// The cell configuration element - public let cell: CollectionCellElement + public let cell: CollectionCellElement /// The header configuration element - public let header: CollectionSupplementaryElement? + public let header: CollectionSupplementaryElement? /// The footer configuration element - public let footer: CollectionSupplementaryElement? + public let footer: CollectionSupplementaryElement? /// The number of elements in this section open var numberOfElements: Int { @@ -28,76 +28,56 @@ open class CollectionSection: CollectionElementsProvider { /// - cell: The cell configuration element /// - header: The header configuration element /// - footer: The footer configuration element - public init(section: Section, - cell: CollectionCellElement, - header: CollectionSupplementaryElement
? = nil, - footer: CollectionSupplementaryElement