diff --git a/Example/Source/Bricks/Custom/SegmentHeaderBrick.swift b/Example/Source/Bricks/Custom/SegmentHeaderBrick.swift index 8e66707..763ed96 100644 --- a/Example/Source/Bricks/Custom/SegmentHeaderBrick.swift +++ b/Example/Source/Bricks/Custom/SegmentHeaderBrick.swift @@ -29,6 +29,8 @@ class SegmentHeaderBrickCell: BrickCell, Bricklike { return } + dataSource.configure(cell: self) + self.segmentControl.removeAllSegments() for (index, title) in dataSource.titles.enumerated() { self.segmentControl.insertSegment(withTitle: title, at: index, animated: false) @@ -40,6 +42,12 @@ class SegmentHeaderBrickCell: BrickCell, Bricklike { protocol SegmentHeaderBrickDataSource: class { var titles: [String] { get } var selectedSegmentIndex: Int { get } + + func configure(cell: SegmentHeaderBrickCell) +} + +extension SegmentHeaderBrickDataSource { + func configure(cell: SegmentHeaderBrickCell) {/*Optional*/} } protocol SegmentHeaderBrickDelegate: class { diff --git a/Example/Source/Examples/Interactive/BrickCollectionInteractiveViewController.swift b/Example/Source/Examples/Interactive/BrickCollectionInteractiveViewController.swift index 2285d5f..36c5c79 100644 --- a/Example/Source/Examples/Interactive/BrickCollectionInteractiveViewController.swift +++ b/Example/Source/Examples/Interactive/BrickCollectionInteractiveViewController.swift @@ -134,7 +134,7 @@ class BrickCollectionInteractiveViewController: BrickViewController, HasTitle { extension BrickCollectionInteractiveViewController { - func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let brickInfo = brickCollectionView.brickInfo(at:indexPath) if brickInfo.brick.identifier == BrickIdentifiers.titleLabel { even = !even @@ -192,7 +192,7 @@ extension BrickCollectionInteractiveViewController: CollectionBrickCellDataSourc layout.hideBehaviorDataSource = self } - func dataSourceForCollectionBrickCell(cell: CollectionBrickCell) -> BrickCollectionViewDataSource { + func dataSourceForCollectionBrickCell(_ cell: CollectionBrickCell) -> BrickCollectionViewDataSource { return dataSources[cell.index] } } diff --git a/Example/Source/Examples/Interactive/DynamicContentViewController.swift b/Example/Source/Examples/Interactive/DynamicContentViewController.swift index 29ecae3..15c4528 100644 --- a/Example/Source/Examples/Interactive/DynamicContentViewController.swift +++ b/Example/Source/Examples/Interactive/DynamicContentViewController.swift @@ -30,7 +30,7 @@ class DynamicContentViewController: BrickViewController, HasTitle { var hidden: Bool = false var reload: Bool = false - var imageURLs: [NSURL]? + var imageURLs: [URL]? let placeholderCount = 5 let overrideContentSource = ActivityIndicatorOverrideSource() @@ -59,7 +59,7 @@ class DynamicContentViewController: BrickViewController, HasTitle { self.overrideContentSource.shouldOverride = false imageURLs = [] for _ in 1...5 { - self.imageURLs?.append(NSURL(string:"https://secure.img2.wfrcdn.com/lf/8/hash/2664/10628031/1/custom_image.jpg")!) + self.imageURLs?.append(URL(string:"https://secure.img2.wfrcdn.com/lf/8/hash/2664/10628031/1/custom_image.jpg")!) } self.brickCollectionView.reloadBricksWithIdentifiers([DynamicContentViewController.Identifiers.HideableSectionContentImage]) } @@ -116,11 +116,11 @@ extension DynamicContentViewController: BrickRepeatCountDataSource { extension DynamicContentViewController: ImageBrickDataSource { - func imageURLForImageBrickCell(imageBrickCell: ImageBrickCell) -> NSURL? { + func imageURLForImageBrickCell(_ imageBrickCell: ImageBrickCell) -> URL? { return imageURLs?[imageBrickCell.index] } - func contentModeForImageBrickCell(imageBrickCell: ImageBrickCell) -> UIViewContentMode { + func contentModeForImageBrickCell(_ imageBrickCell: ImageBrickCell) -> UIViewContentMode { return .scaleAspectFit } } diff --git a/Example/Source/Examples/Interactive/InteractiveAlignViewController.swift b/Example/Source/Examples/Interactive/InteractiveAlignViewController.swift index 31ecd31..25d0be8 100644 --- a/Example/Source/Examples/Interactive/InteractiveAlignViewController.swift +++ b/Example/Source/Examples/Interactive/InteractiveAlignViewController.swift @@ -49,10 +49,13 @@ class InteractiveAlignViewController: BrickViewController, HasTitle { self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(InteractiveAlignViewController.add)) - self.registerBrickClass(LabelBrick.self) self.brickCollectionView.layout.appearBehavior = ScaleAppearBehavior(scale: 0.5) - let labelBrick = LabelBrick("Label", width: .ratio(ratio: 1/3), height: .fixed(size: 100), backgroundColor: UIColor.lightGray.withAlphaComponent(0.3), dataSource: self) + let labelBrick = GenericBrick("Label", width: .ratio(ratio: 1), /*height: .fixed(size: 100),*/ backgroundColor: .brickGray1) { label, cell in + label.text = "BRICK \(cell.index)" + label.configure(textColor: UIColor.brickGray1.complemetaryColor) + cell.edgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + } let section = BrickSection(bricks: [ labelBrick, diff --git a/Source/Cells/BrickCell.swift b/Source/Cells/BrickCell.swift index 64284b9..600a7f0 100644 --- a/Source/Cells/BrickCell.swift +++ b/Source/Cells/BrickCell.swift @@ -206,8 +206,16 @@ open class BrickCell: BaseBrickCell { let preferred = layoutAttributes.copy() as! UICollectionViewLayoutAttributes - let size = CGSize(width: layoutAttributes.frame.width, height: self.heightForBrickView(withWidth: layoutAttributes.frame.width)) - preferred.frame.size = size + // We're inverting the frame because the given frame is already transformed + var invertedFrame = layoutAttributes.frame.applying(layoutAttributes.transform.inverted()) + let size = CGSize(width: layoutAttributes.frame.width, height: self.heightForBrickView(withWidth: invertedFrame.width)) + + // Setting the size of the frame will return the "transformed" size + invertedFrame.size = size + + // We need to invert the frame again, because UILayoutAttributes will transform the frame again + preferred.frame = invertedFrame.applying(layoutAttributes.transform.inverted()) + return preferred } diff --git a/Source/Layout/BrickFlowLayout.swift b/Source/Layout/BrickFlowLayout.swift index 58d9972..df4dd8b 100644 --- a/Source/Layout/BrickFlowLayout.swift +++ b/Source/Layout/BrickFlowLayout.swift @@ -109,9 +109,6 @@ open class BrickFlowLayout: UICollectionViewLayout, BrickLayout { /// Sections internal fileprivate(set) var sections: [Int: BrickLayoutSection]? - /// Flag to indicate that an update cycle is happening - var isUpdating: Bool = false - /// IndexPaths being added var insertedIndexPaths: [IndexPath] = [] @@ -140,7 +137,7 @@ open class BrickFlowLayout: UICollectionViewLayout, BrickLayout { if let sections = sections { //Only continue calculating if the new frame of interest is further than the old frame - let shouldContinueCalculating = scrollDirection == .vertical ? oldRect.maxY <= frameOfInterest.maxY : oldRect.maxX <= frameOfInterest.maxX + let shouldContinueCalculating = scrollDirection == .vertical ? oldRect.maxY < frameOfInterest.maxY : oldRect.maxX < frameOfInterest.maxX if shouldContinueCalculating { let currentSections = sections.values @@ -222,12 +219,6 @@ open class BrickFlowLayout: UICollectionViewLayout, BrickLayout { sections?[sectionIndex] = section } - internal func updateNumberOfItems(_ brickSection: BrickLayoutSection, numberOfItems: Int? = nil) { - brickSection.setNumberOfItems(numberOfItems ?? _collectionView.numberOfItems(inSection: brickSection.sectionIndex), addedAttributes: { (attributes, oldFrame) in - }, removedAttributes: { (attributes, oldFrame) in - }) - } - internal func updateNumberOfItemsInSection(_ section: Int, numberOfItems: Int, updatedAttributes: @escaping OnAttributesUpdatedHandler) { guard let brickSection = sections?[section] else { return @@ -238,7 +229,6 @@ open class BrickFlowLayout: UICollectionViewLayout, BrickLayout { } let height = brickSection.frame.height - self.updateNumberOfItems(brickSection, numberOfItems: numberOfItems) guard let indexPath = dataSource?.brickLayout(self, indexPathFor: section) else { return @@ -309,33 +299,13 @@ extension BrickFlowLayout { case .updateHeight(let indexPath, _): delegate?.brickLayout(self, didUpdateHeightForItemAtIndexPath: indexPath) default: break } - } else if context.invalidateDataSourceCounts { - invalidateDataCounts(context) - } else { - return + } else if !context.invalidateDataSourceCounts { + invalidateLayout(with: BrickLayoutInvalidationContext(type: .invalidate)) } super.invalidateLayout(with: context) } - func invalidateDataCounts(_ context: UICollectionViewLayoutInvalidationContext) { - zIndexer.reset(for: self) - - var changedSections = [Int: Int]() - for section in 0..<_collectionView.numberOfSections { - if let brickSection = sections?[section] { - let numberOfItems = _collectionView.numberOfItems(inSection: section) - if brickSection.numberOfItems != numberOfItems { - changedSections[section] = numberOfItems - } - } - } - if !changedSections.isEmpty { - _ = BrickLayoutInvalidationContext(type: .invalidateDataSourceCounts(sections: changedSections)).invalidateWithLayout(self, context: context) - } - - } - open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { if !isCalculating { _ = calculateSectionsIfNeeded(rect) @@ -346,8 +316,8 @@ extension BrickFlowLayout { } var attributes: [UICollectionViewLayoutAttributes] = [] - for (_, section) in sections { - attributes += section.layoutAttributesForElementsInRect(rect, with: zIndexer) + for (sectionIndex, section) in sections { + attributes += section.layoutAttributesForElementsInRect(rect, with: zIndexer, maxIndex: _collectionView.numberOfItems(inSection: sectionIndex) - 1) } return attributes @@ -447,9 +417,10 @@ extension BrickFlowLayout: BrickLayoutSectionDataSource { let type = _dataSource.brickLayout(self, brickLayoutTypeForItemAt: indexPath) switch type { case .brick: break - case .section(let section): - if let brickSection = sections?[section] { - updateNumberOfItems(brickSection) + case .section(let sectionIndex): + if let brickSection = sections?[sectionIndex] { + brickSection.sectionAttributes = attributes + if brickSection.sectionWidth != width { brickSection.setSectionWidth(width, updatedAttributes: updatedAttributes) } else if brickSection.origin.x != origin.x { @@ -460,7 +431,7 @@ extension BrickFlowLayout: BrickLayoutSectionDataSource { brickSection.invalidateAttributes(updatedAttributes) } } else { - calculateSection(for: section, with: attributes, containedInWidth: width, at: origin) + calculateSection(for: sectionIndex, with: attributes, containedInWidth: width, at: origin) } } } @@ -599,7 +570,6 @@ extension BrickFlowLayout: BrickLayoutInvalidationProvider { switch type { case .section(let section): if let brickSection = self.sections?[section] { - updateNumberOfItems(brickSection) brickSection.setOrigin(attributes.frame.origin, fromBehaviors: fromBehaviors, updatedAttributes: { attributes, oldFrame in updatedAttributes(attributes, oldFrame) self.attributesWereUpdated(attributes, oldFrame: oldFrame, fromBehaviors: fromBehaviors, updatedAttributes: updatedAttributes) @@ -693,9 +663,23 @@ extension BrickFlowLayout { } } - isUpdating = true + if (insertedIndexPaths.count + deletedIndexPaths.count) > 0 { + zIndexer.reset(for: self) + for (sectionIndex, section) in sections! { // OK to force unwrap sections. Without them being initialized, there wouldn't be any insert/delete possible + let inserted = insertedIndexPaths.filter({ (indexPath) -> Bool in + return indexPath.section == sectionIndex + }) + + let deleted = deletedIndexPaths.filter({ (indexPath) -> Bool in + return indexPath.section == sectionIndex + }) + + section.updateNumberOfItems(inserted: inserted.map({$0.item}), deleted: deleted.map({$0.item})) + } + } reloadItems(at: reloadIndexPaths) + invalidateLayout(with: BrickLayoutInvalidationContext(type: .invalidate)) } fileprivate func reloadItems(at indexPaths: [IndexPath]) { @@ -706,7 +690,7 @@ extension BrickFlowLayout { switch _dataSource.brickLayout(self, brickLayoutTypeForItemAt: indexPath) { case .brick: - invalidateLayout(with: BrickLayoutInvalidationContext(type: .invalidateHeight(indexPath: indexPath))) + BrickLayoutInvalidationContext(type: .invalidateHeight(indexPath: indexPath)).invalidateWithLayout(self) default: break } } @@ -717,14 +701,13 @@ extension BrickFlowLayout { insertedIndexPaths = [] deletedIndexPaths = [] reloadIndexPaths = [] - isUpdating = false } override open func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { var attributes: BrickLayoutAttributes? - + if insertedIndexPaths.contains(itemIndexPath) { if let copy = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)?.copy() as? BrickLayoutAttributes { appearBehavior?.configureAttributesForAppearing(copy, in: _collectionView) diff --git a/Source/Layout/BrickLayoutInvalidationContext.swift b/Source/Layout/BrickLayoutInvalidationContext.swift index 48df495..368a2e2 100644 --- a/Source/Layout/BrickLayoutInvalidationContext.swift +++ b/Source/Layout/BrickLayoutInvalidationContext.swift @@ -15,7 +15,6 @@ enum BrickLayoutInvalidationContextType { case scrolling case rotation case behaviorsChanged - case invalidateDataSourceCounts(sections: [Int: Int]) // Key: section Value: numberOfItems case invalidate case updateVisibility @@ -26,7 +25,7 @@ enum BrickLayoutInvalidationContextType { */ var shouldInvalidateAllAttributes: Bool { switch self { - case .rotation, .invalidate, .creation, .updateVisibility, .invalidateDataSourceCounts(_), .updateHeight(_): return true + case .rotation, .invalidate, .creation, .updateVisibility, .updateHeight(_): return true default: return false } } @@ -43,7 +42,6 @@ protocol BrickLayoutInvalidationProvider: class { func recalculateContentSize() -> CGSize func invalidateContent(_ updatedAttributes: @escaping OnAttributesUpdatedHandler) func registerUpdatedAttributes(_ attributes: BrickLayoutAttributes, oldFrame: CGRect?, fromBehaviors: Bool, updatedAttributes: @escaping OnAttributesUpdatedHandler) - func updateNumberOfItemsInSection(_ section: Int, numberOfItems: Int, updatedAttributes: @escaping OnAttributesUpdatedHandler) func applyHideBehavior(updatedAttributes: @escaping OnAttributesUpdatedHandler) func updateContentSize(_ contentSize: CGSize) } @@ -64,10 +62,12 @@ class BrickLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext self.type = type } + @discardableResult func invalidateWithLayout(_ layout: UICollectionViewLayout) -> Bool { return self.invalidateWithLayout(layout, context: self) } + @discardableResult func invalidateWithLayout(_ layout: UICollectionViewLayout, context: UICollectionViewLayoutInvalidationContext) -> Bool { guard let provider = layout as? BrickLayoutInvalidationProvider @@ -105,14 +105,6 @@ class BrickLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext self.invalidateSections(provider, layout: layout) case .updateVisibility: self.applyHideBehaviors(provider, updatedAttributes: updateAttributes) - case .invalidateDataSourceCounts(let sections): - let keys = Array(sections.keys).sorted(by: <) // We need to sort the keys first so the updates are done in the correct order - for section in keys { - let numberOfItems = sections[section]! - provider.updateNumberOfItemsInSection(section, numberOfItems: numberOfItems, updatedAttributes: { attributes, olfFrame in - }) - } - applyHideBehaviors(provider, updatedAttributes: updateAttributes) default: break } diff --git a/Source/Layout/BrickLayoutSection.swift b/Source/Layout/BrickLayoutSection.swift index a55876f..3d2529b 100644 --- a/Source/Layout/BrickLayoutSection.swift +++ b/Source/Layout/BrickLayoutSection.swift @@ -137,36 +137,49 @@ internal class BrickLayoutSection { frame.size.width = sectionWidth } - /// Set the number of items for this BrickLayoutSection - /// - /// - Parameters: - /// - addedAttributes: callback for the added attributes - /// - removedAttributes: callback for the removed attributes - func setNumberOfItems(_ numberOfItems: Int, addedAttributes: OnAttributesUpdatedHandler?, removedAttributes: OnAttributesUpdatedHandler?) { - guard numberOfItems != self.numberOfItems else { + /// Update the number of items for this BrickLayoutSection + func updateNumberOfItems(inserted: [Int], deleted: [Int]) { + guard inserted.count + deleted.count > 0 else { return } - let difference = numberOfItems - self.numberOfItems + var startIndexToUpdate: Int = numberOfItems - 1 - if difference > 0 { - self.numberOfItems = numberOfItems - var startIndex = attributes.count - updateAttributeIdentifiers(targetStartIndex: &startIndex) - createOrUpdateCells(from: startIndex, invalidate: true, updatedAttributes: addedAttributes) - } else { - self.numberOfItems = numberOfItems - while attributes.count > numberOfItems { - let lastIndex = attributes.keys.max()! // Max Element will always be available based on this logic (difference is smaller than 0, so there are values available) - let last = attributes[lastIndex]! - removedAttributes?(last, last.frame) - - attributes.removeValue(forKey: lastIndex) + let sortedDeleted = deleted.sorted(by: >) + + for index in sortedDeleted { + attributes[index] = nil + + // Move every index with 1 + for i in (index+1).. [UICollectionViewLayoutAttributes] { + func layoutAttributesForElementsInRect(_ rect: CGRect, with zIndexer: BrickZIndexer, maxIndex: Int? = nil) -> [UICollectionViewLayoutAttributes] { var attributes = [UICollectionViewLayoutAttributes]() + let actualMaxIndex = maxIndex ?? (numberOfItems - 1) if self.attributes.isEmpty { return attributes @@ -737,7 +751,7 @@ extension BrickLayoutSection { // Closure that checks if an attribute is within the rect and adds it to the attributes to return // Returns true if the frame is within the rect let frameCheck: (_ index: Int) -> Bool = { index in - guard let brickAttributes = self.attributes[index] else { + guard let brickAttributes = self.attributes[index], index <= actualMaxIndex else { return false } if rect.intersects(brickAttributes.frame) { diff --git a/Tests/Layout/BrickLayoutSectionTests.swift b/Tests/Layout/BrickLayoutSectionTests.swift index 3335e87..975f466 100644 --- a/Tests/Layout/BrickLayoutSectionTests.swift +++ b/Tests/Layout/BrickLayoutSectionTests.swift @@ -231,41 +231,11 @@ class BrickLayoutSectionTests: XCTestCase { dataSource.widthRatios = [1, 1, 1, 1, 1, 1] dataSource.heights = [50, 50, 50, 50, 50, 50] - var addedAttributes = [BrickLayoutAttributes]() - section.setNumberOfItems(6, addedAttributes: { (attributes, oldFrame) in - addedAttributes.append(attributes) - }, removedAttributes: nil) - XCTAssertEqual(addedAttributes.count, 6) + section.updateNumberOfItems(inserted: [0,1,2,3,4,5], deleted: []) - let frames = section.orderedAttributeFrames - XCTAssertEqual(frames.count, 6) - XCTAssertEqual(frames[0], CGRect(x: 5, y: 10, width: 310, height: 50)) - XCTAssertEqual(frames[1], CGRect(x: 5, y: 65, width: 310, height: 50)) - XCTAssertEqual(frames[2], CGRect(x: 5, y: 120, width: 310, height: 50)) - XCTAssertEqual(frames[3], CGRect(x: 5, y: 175, width: 310, height: 50)) - XCTAssertEqual(frames[4], CGRect(x: 5, y: 230, width: 310, height: 50)) - XCTAssertEqual(frames[5], CGRect(x: 5, y: 285, width: 310, height: 50)) - - XCTAssertEqual(section.frame, CGRect(x: 0, y: 0, width: 320, height: 345)) - } - func testNoItems() { - let dataSource = FixedBrickLayoutSectionDataSource(widthRatios: [1, 1, 1, 1, 1, 1], heights: [50, 50, 50, 50, 50, 50], edgeInsets: UIEdgeInsets(top: 10, left: 5, bottom: 10, right: 5), inset: 5) - let section = BrickLayoutSection( - sectionIndex: 0, - sectionAttributes: nil, - numberOfItems: 6, - origin: CGPoint.zero, - sectionWidth: 320, - dataSource: dataSource) section.invalidateAttributes(nil) - var addedAttributes = [BrickLayoutAttributes]() - section.setNumberOfItems(6, addedAttributes: { (attributes, oldFrame) in - addedAttributes.append(attributes) - }, removedAttributes: nil) - XCTAssertEqual(addedAttributes.count, 0) - let frames = section.orderedAttributeFrames XCTAssertEqual(frames.count, 6) XCTAssertEqual(frames[0], CGRect(x: 5, y: 10, width: 310, height: 50)) @@ -325,11 +295,8 @@ class BrickLayoutSectionTests: XCTestCase { dataSource.heights.removeLast() dataSource.heights.removeLast() - var deletedAttributes = [BrickLayoutAttributes]() - section.setNumberOfItems(3, addedAttributes: nil, removedAttributes: { (attributes, oldFrame) in - deletedAttributes.append(attributes) - }) - XCTAssertEqual(deletedAttributes.count, 2) + section.updateNumberOfItems(inserted: [], deleted: [3, 4]) + section.invalidateAttributes(nil) let frames = section.orderedAttributeFrames XCTAssertEqual(frames.count, 3)