From 3946a415fd5cb8dbac85de26e039e15dfe792d17 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Tue, 22 Dec 2020 11:01:13 +0000 Subject: [PATCH 01/47] Add FlatSection --- Sources/Composed/Sections/FlatSection.swift | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Sources/Composed/Sections/FlatSection.swift diff --git a/Sources/Composed/Sections/FlatSection.swift b/Sources/Composed/Sections/FlatSection.swift new file mode 100644 index 0000000..1bf6aa3 --- /dev/null +++ b/Sources/Composed/Sections/FlatSection.swift @@ -0,0 +1,102 @@ +/// A section that flattens each of its children in to a single section. +open class FlatSection: Section { + open private(set) var children: [Section] = [] + + public var numberOfElements: Int { + children.map(\.numberOfElements).reduce(0, +) + } + + public var updateDelegate: SectionUpdateDelegate? + + open func append(_ section: Section) { + updateDelegate?.willBeginUpdating(self) + + let indexOfFirstChildElement = numberOfElements + children.append(section) + + (0.. Int? { + var offset = 0 + + for child in children { + if child === section { + return offset + } + + offset += child.numberOfElements + } + + return nil + } + + private func indexesRange(for section: Section) -> Range? { + guard let sectionOffset = offset(for: section) else { return nil } + return (sectionOffset.. [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 = offset(for: section) else { return } + updateDelegate?.section(self, select: sectionOffset + index) + } + + public func section(_ section: Section, deselect index: Int) { + guard let sectionOffset = offset(for: section) else { return } + updateDelegate?.section(self, deselect: sectionOffset + index) + } + + public func section(_ section: Section, move sourceIndex: Int, to destinationIndex: Int) { + guard let sectionOffset = offset(for: section) else { return } + updateDelegate?.section(self, move: sourceIndex + sectionOffset, to: destinationIndex + sectionOffset) + } +} From babdadfbc4f10052c11774e47650593cd1bb990f Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Tue, 22 Dec 2020 11:15:19 +0000 Subject: [PATCH 02/47] Mark `append(_:)` public --- Sources/Composed/Sections/FlatSection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Composed/Sections/FlatSection.swift b/Sources/Composed/Sections/FlatSection.swift index 1bf6aa3..98fc3f3 100644 --- a/Sources/Composed/Sections/FlatSection.swift +++ b/Sources/Composed/Sections/FlatSection.swift @@ -8,7 +8,7 @@ open class FlatSection: Section { public var updateDelegate: SectionUpdateDelegate? - open func append(_ section: Section) { + public func append(_ section: Section) { updateDelegate?.willBeginUpdating(self) let indexOfFirstChildElement = numberOfElements From 59daa6553f845e899e2ab4b33fcef3f3fd440613 Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Tue, 22 Dec 2020 11:15:26 +0000 Subject: [PATCH 03/47] Add `section(at:)` --- Sources/Composed/Sections/FlatSection.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/Composed/Sections/FlatSection.swift b/Sources/Composed/Sections/FlatSection.swift index 98fc3f3..8195148 100644 --- a/Sources/Composed/Sections/FlatSection.swift +++ b/Sources/Composed/Sections/FlatSection.swift @@ -23,6 +23,20 @@ open class FlatSection: Section { updateDelegate?.didEndUpdating(self) } + public func section(at index: Int) -> (section: Section, offset: Int)? { + var offset = 0 + + for child in children { + if index <= offset + child.numberOfElements { + return (child, offset) + } + + offset += child.numberOfElements + } + + return nil + } + private func offset(for section: Section) -> Int? { var offset = 0 From c4fe2251929d41a0049e55292d3e499328d478ef Mon Sep 17 00:00:00 2001 From: Joseph Duffy Date: Tue, 22 Dec 2020 13:48:35 +0000 Subject: [PATCH 04/47] Add support for flat UI sections, with mixed cells --- ...CollectionCompositionalLayoutHandler.swift | 2 +- .../CollectionFlowLayoutHandler.swift | 2 +- .../CollectionCoordinator.swift | 61 ++++---- .../CollectionView/CollectionElement.swift | 52 ++++++- .../CollectionView/CollectionSection.swift | 6 +- .../CollectionSectionProvider.swift | 22 --- ...ollectionViewSectionElementsProvider.swift | 138 ++++++++++++++++++ .../CollectionContextMenuHandler.swift | 2 +- .../Handlers/CollectionDragHandler.swift | 4 +- .../Handlers/CollectionDropHandler.swift | 2 +- .../Handlers/CollectionEditingHandler.swift | 2 +- .../Handlers/CollectionSelectionHandler.swift | 4 +- .../Handlers/CollectionUpdateHandler.swift | 2 +- .../UICollectionViewSection.swift | 35 +++++ .../TableView/TableCoordinator.swift | 2 +- 15 files changed, 270 insertions(+), 66 deletions(-) delete mode 100644 Sources/ComposedUI/CollectionView/CollectionSectionProvider.swift create mode 100644 Sources/ComposedUI/CollectionView/FlatUICollectionViewSectionElementsProvider.swift create mode 100644 Sources/ComposedUI/CollectionView/UICollectionViewSection.swift diff --git a/Sources/ComposedLayouts/CollectionCompositionalLayoutHandler.swift b/Sources/ComposedLayouts/CollectionCompositionalLayoutHandler.swift index 8b91d37..43f1337 100644 --- a/Sources/ComposedLayouts/CollectionCompositionalLayoutHandler.swift +++ b/Sources/ComposedLayouts/CollectionCompositionalLayoutHandler.swift @@ -3,7 +3,7 @@ import ComposedUI @available(iOS 13.0, *) /// Conform your section to this protocol to provide a layout section for a `UICollectionViewCompositionalLayout` -public protocol CompositionalLayoutHandler: CollectionSectionProvider { +public protocol CompositionalLayoutHandler: UICollectionViewSection { /// Return a layout section for this section /// - Parameter environment: The current environment for this layout diff --git a/Sources/ComposedLayouts/CollectionFlowLayoutHandler.swift b/Sources/ComposedLayouts/CollectionFlowLayoutHandler.swift index 1758453..3f06580 100644 --- a/Sources/ComposedLayouts/CollectionFlowLayoutHandler.swift +++ b/Sources/ComposedLayouts/CollectionFlowLayoutHandler.swift @@ -31,7 +31,7 @@ public struct CollectionFlowLayoutEnvironment { } /// Conform your section to this protocol to override sizing and metric values for a `UICollectionViewFlowLayout` -public protocol CollectionFlowLayoutHandler: CollectionSectionProvider { +public protocol CollectionFlowLayoutHandler: UICollectionViewSection { /// Return the size for the element at the specified index /// - Parameters: diff --git a/Sources/ComposedUI/CollectionView/CollectionCoordinator.swift b/Sources/ComposedUI/CollectionView/CollectionCoordinator.swift index f2ae35e..f3472d5 100644 --- a/Sources/ComposedUI/CollectionView/CollectionCoordinator.swift +++ b/Sources/ComposedUI/CollectionView/CollectionCoordinator.swift @@ -59,7 +59,7 @@ open class CollectionCoordinator: NSObject { private weak var originalDropDelegate: UICollectionViewDropDelegate? private var dropDelegateObserver: NSKeyValueObservation? - private var cachedProviders: [CollectionElementsProvider] = [] + private var cachedElementsProviders: [UICollectionViewSectionElementsProvider] = [] /// Make a new coordinator with the specified collectionView and sectionProvider /// - Parameters: @@ -147,31 +147,42 @@ 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..](), { cells, index in + let cell = elementsProvider.cell(for: index) + + guard !cells.contains(where: { $0.reuseIdentifier == cell.reuseIdentifier }) else { return } + + cells.append(cell) + }) + + for cell in cells { + switch cell.dequeueMethod { + case let .fromNib(type): + let nib = UINib(nibName: String(describing: type), bundle: Bundle(for: type)) + collectionView.register(nib, forCellWithReuseIdentifier: cell.reuseIdentifier) + case let .fromClass(type): + collectionView.register(type, forCellWithReuseIdentifier: cell.reuseIdentifier) + case .fromStoryboard: + break + } } - [section.header, section.footer].compactMap { $0 }.forEach { + [elementsProvider.header, elementsProvider.footer].compactMap { $0 }.forEach { switch $0.dequeueMethod { case let .fromNib(type): let nib = UINib(nibName: String(describing: type), bundle: Bundle(for: type)) @@ -183,7 +194,7 @@ open class CollectionCoordinator: NSObject { } } - cachedProviders.append(section) + cachedElementsProviders.append(elementsProvider) } collectionView.allowsMultipleSelection = true @@ -303,7 +314,7 @@ extension CollectionCoordinator: SectionProviderMappingDelegate { assert(Thread.isMainThread) changes.append { [weak self] in guard let self = self else { return } - + var indexPathsToReload: [IndexPath] = [] for indexPath in indexPaths { guard let section = self.sectionProvider.sections[indexPath.section] as? CollectionUpdateHandler, @@ -313,7 +324,7 @@ extension CollectionCoordinator: SectionProviderMappingDelegate { continue } - self.cachedProviders[indexPath.section].cell.configure(cell, indexPath.item, self.mapper.provider.sections[indexPath.section]) + self.cachedElementsProviders[indexPath.section].cell(for: indexPath.item).configure(cell, indexPath.item, self.mapper.provider.sections[indexPath.section]) } guard !indexPathsToReload.isEmpty else { return } @@ -380,7 +391,7 @@ extension CollectionCoordinator: UICollectionViewDataSource { let elements = elementsProvider(for: indexPath.section) let section = mapper.provider.sections[indexPath.section] - elements.cell.willAppear(cell, indexPath.item, section) + elements.cell(for: indexPath.item).willAppear?(cell, indexPath.item, section) } public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { @@ -392,13 +403,13 @@ extension CollectionCoordinator: UICollectionViewDataSource { guard indexPath.section < sectionProvider.numberOfSections else { return } let elements = elementsProvider(for: indexPath.section) let section = mapper.provider.sections[indexPath.section] - elements.cell.didDisappear(cell, indexPath.item, section) + elements.cell(for: indexPath.item).didDisappear?(cell, indexPath.item, section) } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { assert(Thread.isMainThread) let elements = elementsProvider(for: indexPath.section) - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: elements.cell.reuseIdentifier, for: indexPath) + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: elements.cell(for: indexPath.item).reuseIdentifier, for: indexPath) if let handler = sectionProvider.sections[indexPath.section] as? EditingHandler { if let handler = sectionProvider.sections[indexPath.section] as? CollectionEditingHandler { @@ -408,7 +419,7 @@ extension CollectionCoordinator: UICollectionViewDataSource { } } - 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]) return cell } @@ -474,13 +485,13 @@ 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] } - + } @available(iOS 13.0, *) @@ -691,7 +702,7 @@ extension CollectionCoordinator: UICollectionViewDropDelegate { return section.dragSession(previewParametersForElementAt: indexPath.item, cell: cell) } - + public func collectionView(_ collectionView: UICollectionView, dropPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? { guard let section = sectionProvider.sections[indexPath.section] as? CollectionDropHandler, let cell = collectionView.cellForItem(at: indexPath) else { diff --git a/Sources/ComposedUI/CollectionView/CollectionElement.swift b/Sources/ComposedUI/CollectionView/CollectionElement.swift index 7f25546..93e6bde 100644 --- a/Sources/ComposedUI/CollectionView/CollectionElement.swift +++ b/Sources/ComposedUI/CollectionView/CollectionElement.swift @@ -32,19 +32,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 where View: UICollectionViewCell { public let dequeueMethod: DequeueMethod 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: @@ -66,8 +77,8 @@ public final class CollectionCellElement: CollectionElement where View: UI configure(view as! View, index, section as! Section) } - willAppear = { _, _, _ in } - didDisappear = { _, _, _ in } + willAppear = nil + didDisappear = nil } /// Makes a new element for representing a cell @@ -103,6 +114,37 @@ public final class CollectionCellElement: CollectionElement where View: UI } } + /// 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 + internal init( + dequeueMethod: DequeueMethod, + 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, _ in + willAppear?(view as! View, index) + } + + 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 diff --git a/Sources/ComposedUI/CollectionView/CollectionSection.swift b/Sources/ComposedUI/CollectionView/CollectionSection.swift index 02261ba..6923bae 100644 --- a/Sources/ComposedUI/CollectionView/CollectionSection.swift +++ b/Sources/ComposedUI/CollectionView/CollectionSection.swift @@ -3,7 +3,7 @@ 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 @@ -36,7 +36,7 @@ open class CollectionSection: CollectionElementsProvider { self.section = section // The code below copies the relevent elements to erase type-safety - + let dequeueMethod: DequeueMethod switch cell.dequeueMethod { case .fromClass: dequeueMethod = .fromClass(Cell.self) @@ -89,7 +89,7 @@ open class CollectionSection: CollectionElementsProvider { } else { kind = footer.kind } - + self.footer = CollectionSupplementaryElement(section: section, dequeueMethod: dequeueMethod, reuseIdentifier: footer.reuseIdentifier, diff --git a/Sources/ComposedUI/CollectionView/CollectionSectionProvider.swift b/Sources/ComposedUI/CollectionView/CollectionSectionProvider.swift deleted file mode 100644 index 3b31fc7..0000000 --- a/Sources/ComposedUI/CollectionView/CollectionSectionProvider.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit -import Composed - -/// Provides a section to a collection view. Conform to this protool to use your section with a `UICollectionView` -public protocol CollectionSectionProvider: Section { - - /// Return a section cofiguration for the collection view. - /// - Parameter traitCollection: The trait collection being applied to the view - func section(with traitCollection: UITraitCollection) -> CollectionSection - -} - -internal protocol CollectionElementsProvider { - var cell: CollectionCellElement { get } - var header: CollectionSupplementaryElement? { get } - var footer: CollectionSupplementaryElement? { get } - var numberOfElements: Int { get } -} - -extension CollectionElementsProvider { - var isEmpty: Bool { return numberOfElements == 0 } -} diff --git a/Sources/ComposedUI/CollectionView/FlatUICollectionViewSectionElementsProvider.swift b/Sources/ComposedUI/CollectionView/FlatUICollectionViewSectionElementsProvider.swift new file mode 100644 index 0000000..c94b0cd --- /dev/null +++ b/Sources/ComposedUI/CollectionView/FlatUICollectionViewSectionElementsProvider.swift @@ -0,0 +1,138 @@ +import UIKit +import Composed + +open class FlatUICollectionViewSectionElementsProvider: UICollectionViewSectionElementsProvider { + /// The header configuration element + public let header: CollectionSupplementaryElement? + + /// The footer configuration element + public let footer: CollectionSupplementaryElement? + + /// The number of elements in this section + open var numberOfElements: Int { + return flatSection?.numberOfElements ?? 0 + } + + // The underlying section associated with this section + private weak var flatSection: FlatSection? + + private let traitCollection: UITraitCollection + + /// Makes a new configuration with the specified cell, header and/or footer elements + /// - Parameters: + /// - section: The section this will be associated with + /// - cell: The cell configuration element + /// - header: The header configuration element + /// - footer: The footer configuration element + public init( + section: FlatSection, + traitCollection: UITraitCollection, + header: CollectionSupplementaryElement
? = nil, + footer: CollectionSupplementaryElement