diff --git a/BlueprintUILists/Sources/BlueprintItemContent.swift b/BlueprintUILists/Sources/BlueprintItemContent.swift index 76fdc6a32..abd982183 100644 --- a/BlueprintUILists/Sources/BlueprintItemContent.swift +++ b/BlueprintUILists/Sources/BlueprintItemContent.swift @@ -147,4 +147,8 @@ public extension BlueprintItemContent return view } + + static func createReusableSelectedBackgroundView(frame: CGRect) -> SelectedBackgroundView { + self.createReusableBackgroundView(frame: frame) + } } diff --git a/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift b/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift index bebef2dc3..2144ae966 100644 --- a/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift +++ b/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift @@ -109,7 +109,7 @@ public protocol HeaderFooterContent /// You do not need to provide this `typealias` unless you would like /// to draw a selected background view. /// - associatedtype PressedBackgroundView:UIView = BackgroundView + associatedtype PressedBackgroundView:UIView = UIView /// Create and return a new background view used to render the content's pressed background. /// @@ -171,10 +171,10 @@ public extension HeaderFooterContent where Self.BackgroundView == UIView } } -public extension HeaderFooterContent where Self.PressedBackgroundView == BackgroundView +public extension HeaderFooterContent where Self.PressedBackgroundView == UIView { static func createReusablePressedBackgroundView(frame : CGRect) -> PressedBackgroundView { - self.createReusableBackgroundView(frame: frame) + PressedBackgroundView(frame: frame) } } diff --git a/ListableUI/Sources/Internal/HeaderFooterContentView.swift b/ListableUI/Sources/Internal/HeaderFooterContentView.swift index 42b6fb305..13b9b428a 100644 --- a/ListableUI/Sources/Internal/HeaderFooterContentView.swift +++ b/ListableUI/Sources/Internal/HeaderFooterContentView.swift @@ -64,6 +64,22 @@ final class HeaderFooterContentView : UIView self.content.sizeThatFits(size) } + override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize { + self.content.systemLayoutSizeFitting(targetSize) + } + + override func systemLayoutSizeFitting( + _ targetSize: CGSize, + withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, + verticalFittingPriority: UILayoutPriority + ) -> CGSize { + self.content.systemLayoutSizeFitting( + targetSize, + withHorizontalFittingPriority: horizontalFittingPriority, + verticalFittingPriority: verticalFittingPriority + ) + } + override func layoutSubviews() { super.layoutSubviews() diff --git a/ListableUI/Sources/Internal/ItemCell.swift b/ListableUI/Sources/Internal/ItemCell.swift index ee59262b4..ff1a2b6dc 100644 --- a/ListableUI/Sources/Internal/ItemCell.swift +++ b/ListableUI/Sources/Internal/ItemCell.swift @@ -11,6 +11,8 @@ import UIKit protocol AnyItemCell : UICollectionViewCell { func closeSwipeActions() + + func wasDequeued(with liveCells : LiveCells) } /// @@ -122,13 +124,25 @@ final class ItemCell : UICollectionViewCell, AnyItemCell func closeSwipeActions() { self.contentContainer.performAnimatedClose() } + + private var hasBeenDequeued = false + + func wasDequeued(with liveCells : LiveCells) { + guard hasBeenDequeued == false else { + return + } + + self.hasBeenDequeued = true + + liveCells.add(self) + } } final class LiveCells { func add(_ cell : AnyItemCell) { - self.cells.insert(.init(cell)) + self.cells.append(.init(cell: cell)) self.cells = self.cells.filter { $0.cell != nil } } @@ -141,25 +155,9 @@ final class LiveCells { } } - private var cells : Set = [] + private(set) var cells : [LiveCell] = [] - private struct LiveCell : Hashable { - - private let identifier : ObjectIdentifier - + struct LiveCell { weak var cell : AnyItemCell? - - init(_ cell : AnyItemCell) { - self.identifier = ObjectIdentifier(cell) - self.cell = cell - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.identifier) - } - - static func == (lhs : LiveCell, rhs : LiveCell) -> Bool { - lhs.identifier == rhs.identifier - } } } diff --git a/ListableUI/Sources/Internal/UIView+Additions.swift b/ListableUI/Sources/Internal/UIView.swift similarity index 94% rename from ListableUI/Sources/Internal/UIView+Additions.swift rename to ListableUI/Sources/Internal/UIView.swift index 54e36abc5..e1a0dc708 100644 --- a/ListableUI/Sources/Internal/UIView+Additions.swift +++ b/ListableUI/Sources/Internal/UIView.swift @@ -1,5 +1,5 @@ // -// UIView+Additions.swift +// UIView.swift // ListableUI // // Created by Kyle Van Essen on 10/28/20. diff --git a/ListableUI/Sources/Internal/UIViewPropertyAnimator+System.swift b/ListableUI/Sources/Internal/UIViewPropertyAnimator.swift similarity index 95% rename from ListableUI/Sources/Internal/UIViewPropertyAnimator+System.swift rename to ListableUI/Sources/Internal/UIViewPropertyAnimator.swift index fcb6d872c..0e05f23b7 100644 --- a/ListableUI/Sources/Internal/UIViewPropertyAnimator+System.swift +++ b/ListableUI/Sources/Internal/UIViewPropertyAnimator.swift @@ -1,5 +1,5 @@ // -// UIViewPropertyAnimator+System.swift +// UIViewPropertyAnimator.swift // ListableUI // // Created by Kyle Bashour on 4/17/20. diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 4b8c1dd36..499fa3eff 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -178,7 +178,7 @@ public protocol ItemContent where Coordinator.ItemContentType == Self /// You do not need to provide this `typealias` unless you would like /// to draw a selected background view. /// - associatedtype SelectedBackgroundView:UIView = BackgroundView + associatedtype SelectedBackgroundView:UIView = UIView /// Create and return a new background view used to render the content's selected background. @@ -295,11 +295,11 @@ public extension ItemContent where BackgroundView == UIView /// Provide a UIView when no special selected background view is specified. -public extension ItemContent where BackgroundView == SelectedBackgroundView +public extension ItemContent where BackgroundView == UIView { - static func createReusableSelectedBackgroundView(frame : CGRect) -> BackgroundView + static func createReusableSelectedBackgroundView(frame : CGRect) -> SelectedBackgroundView { - self.createReusableBackgroundView(frame: frame) + SelectedBackgroundView(frame: frame) } } diff --git a/ListableUI/Sources/ListView/ListView.DataSource.swift b/ListableUI/Sources/ListView/ListView.DataSource.swift index a3c487a71..754a37b1f 100644 --- a/ListableUI/Sources/ListView/ListView.DataSource.swift +++ b/ListableUI/Sources/ListView/ListView.DataSource.swift @@ -41,7 +41,7 @@ internal extension ListView environment: environment ) - self.liveCells.add(cell) + cell.wasDequeued(with: self.liveCells) return cell } diff --git a/ListableUI/Tests/Internal/ItemCellTests.swift b/ListableUI/Tests/Internal/ItemCellTests.swift index 1ed54fbb4..67a2bf9f8 100644 --- a/ListableUI/Tests/Internal/ItemCellTests.swift +++ b/ListableUI/Tests/Internal/ItemCellTests.swift @@ -30,22 +30,29 @@ class ItemElementCellTests : XCTestCase func test_sizeThatFits() { - // The default implementation of size that fits on UIView returns the existing size of the view. - // Make sure that value is returned from the cell. - - let cell1 = ItemCell(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))) - XCTAssertEqual(cell1.sizeThatFits(.zero), CGSize(width: 100.0, height: 100.0)) - - let cell2 = ItemCell(frame: CGRect(origin: .zero, size: CGSize(width: 150.0, height: 150.0))) - XCTAssertEqual(cell2.sizeThatFits(.zero), CGSize(width: 150.0, height: 150.0)) + let cell = ItemCell(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))) + XCTAssertEqual(cell.sizeThatFits(.zero), CGSize(width: 40.0, height: 50.0)) } - func test_systemLayoutSizeFitting() { - XCTFail() + func test_systemLayoutSizeFitting() + { + let cell = ItemCell(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))) + XCTAssertEqual(cell.systemLayoutSizeFitting(.zero), CGSize(width: 41.0, height: 51.0)) } - func test_systemLayoutSizeFitting_withHorizontalFittingPriority_verticalFittingPriority() { - XCTFail() + func test_systemLayoutSizeFitting_withHorizontalFittingPriority_verticalFittingPriority() + { + let cell = ItemCell(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))) + + XCTAssertEqual( + cell.systemLayoutSizeFitting( + .zero, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ), + + CGSize(width: 42.0, height: 52.0) + ) } } @@ -62,7 +69,25 @@ fileprivate struct TestItemContent : ItemContent, Equatable typealias ContentView = UIView static func createReusableContentView(frame: CGRect) -> UIView { - return UIView(frame: frame) + return View(frame: frame) + } + + private final class View : UIView { + override func sizeThatFits(_ size: CGSize) -> CGSize { + CGSize(width: 40, height: 50) + } + + override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize { + CGSize(width: 41, height: 51) + } + + override func systemLayoutSizeFitting( + _ targetSize: CGSize, + withHorizontalFittingPriority horizontalFittingPriority: + UILayoutPriority, verticalFittingPriority: UILayoutPriority + ) -> CGSize { + CGSize(width: 42, height: 52) + } } } @@ -70,10 +95,48 @@ fileprivate struct TestItemContent : ItemContent, Equatable class ItemElementCell_LiveCells_Tests : XCTestCase { func test_add() { + let liveCells = LiveCells() + + var cell1 : AnyItemCell? = ItemCell(frame: .zero) + + liveCells.add(cell1!) + // Should only add the cell once. + + XCTAssertEqual(liveCells.cells.count, 1) + + // Nil out the first cell + + weak var weakCell1 = cell1 + + cell1 = nil + + self.waitFor { + weakCell1 == nil + } + + // Register a second cell, should remove the first + + let cell2 = ItemCell(frame: .zero) + + liveCells.add(cell2) + + XCTAssertEqual(liveCells.cells.count, 1) + XCTAssertTrue(liveCells.cells.first?.cell === cell2) } - - func test_perform() { + + private struct TestContent : ItemContent, Equatable { + + var identifier: Identifier { + .init() + } + + static func createReusableContentView(frame: CGRect) -> UIView { + UIView(frame: frame) + } + func apply(to views: ItemContentViews, for reason: ApplyReason, with info: ApplyItemContentInfo) { + // Nothing needed + } } } diff --git a/ListableUI/Tests/Internal/SupplementaryItemViewTests.swift b/ListableUI/Tests/Internal/SupplementaryContainerViewTests.swift similarity index 65% rename from ListableUI/Tests/Internal/SupplementaryItemViewTests.swift rename to ListableUI/Tests/Internal/SupplementaryContainerViewTests.swift index 95475d29d..483f540ef 100644 --- a/ListableUI/Tests/Internal/SupplementaryItemViewTests.swift +++ b/ListableUI/Tests/Internal/SupplementaryContainerViewTests.swift @@ -29,7 +29,7 @@ class SupplementaryContainerViewTests: XCTestCase func test_sizeThatFits() { let cache = ReusableViewCache() - let view = SupplementaryContainerView(frame: CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))) + let view = SupplementaryContainerView(frame:.zero) view.reuseCache = cache view.environment = .empty @@ -38,15 +38,43 @@ class SupplementaryContainerViewTests: XCTestCase view.headerFooter = self.newHeaderFooter() - XCTAssertEqual(view.sizeThatFits(.zero), CGSize(width: 100, height: 100)) + XCTAssertEqual(view.sizeThatFits(.zero), CGSize(width: 50, height: 40)) } func test_systemLayoutSizeFitting() { - XCTFail() + let cache = ReusableViewCache() + let view = SupplementaryContainerView(frame:.zero) + + view.reuseCache = cache + view.environment = .empty + + XCTAssertEqual(view.sizeThatFits(.zero), .zero) + + view.headerFooter = self.newHeaderFooter() + + XCTAssertEqual(view.systemLayoutSizeFitting(.zero), CGSize(width: 51, height: 41)) } func test_systemLayoutSizeFitting_withHorizontalFittingPriority_verticalFittingPriority() { - XCTFail() + let cache = ReusableViewCache() + let view = SupplementaryContainerView(frame:.zero) + + view.reuseCache = cache + view.environment = .empty + + XCTAssertEqual(view.sizeThatFits(.zero), .zero) + + view.headerFooter = self.newHeaderFooter() + + XCTAssertEqual( + view.systemLayoutSizeFitting( + .zero, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .defaultLow + ), + + CGSize(width: 52, height: 42) + ) } func test_headerFooter() @@ -71,7 +99,7 @@ class SupplementaryContainerViewTests: XCTestCase let content = view.content! XCTAssertTrue(type(of: content) === HeaderFooterContentView.self) - XCTAssertEqual(view.frame.size, CGSize(width: 100, height: 100)) + XCTAssertEqual(view.frame.size, CGSize(width: 50, height: 40)) // Unset the header footer, make sure the view is pushed back into the cache. @@ -125,9 +153,20 @@ fileprivate struct TestHeaderFooterContent : HeaderFooterContent, Equatable final class View : UIView { - override func sizeThatFits(_ size: CGSize) -> CGSize - { - return CGSize(width: 100, height: 100) + override func sizeThatFits(_ size: CGSize) -> CGSize { + CGSize(width: 50, height: 40) + } + + override func systemLayoutSizeFitting(_ targetSize: CGSize) -> CGSize { + CGSize(width: 51, height: 41) + } + + override func systemLayoutSizeFitting( + _ targetSize: CGSize, + withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, + verticalFittingPriority: UILayoutPriority + ) -> CGSize { + CGSize(width: 52, height: 42) } } } diff --git a/ListableUI/Tests/Internal/UIViewTests.swift b/ListableUI/Tests/Internal/UIViewTests.swift new file mode 100644 index 000000000..73df08a13 --- /dev/null +++ b/ListableUI/Tests/Internal/UIViewTests.swift @@ -0,0 +1,40 @@ +// +// UIView+AdditionsTests.swift +// ListableUI-Unit-Tests +// +// Created by Kyle Van Essen on 12/10/20. +// + +import XCTest +@testable import ListableUI + + +class UIViewTests : XCTestCase { + + func test_firstSuperview() { + + let view1 = View1() + let view2 = View2() + let view3 = View3() + + view1.addSubview(view2) + view2.addSubview(view3) + + XCTAssertEqual(view3.firstSuperview(ofType: UIView.self), view2) + XCTAssertEqual(view3.firstSuperview(ofType: View3.self), nil) + + XCTAssertEqual(view3.firstSuperview(ofType: View2.self), view2) + XCTAssertEqual(view3.firstSuperview(ofType: View1.self), view1) + + XCTAssertEqual(view3.firstSuperview(ofType: View4.self), nil) + } +} + + +fileprivate final class View1 : UIView {} + +fileprivate final class View2 : UIView {} + +fileprivate final class View3 : UIView {} + +fileprivate final class View4 : UIView {}