diff --git a/LayoutKitTests/ViewRecyclerTests.swift b/LayoutKitTests/ViewRecyclerTests.swift index 8fd291ad..311323b7 100644 --- a/LayoutKitTests/ViewRecyclerTests.swift +++ b/LayoutKitTests/ViewRecyclerTests.swift @@ -91,6 +91,30 @@ class ViewRecyclerTests: XCTestCase { XCTAssertNotNil(one.superview) } + func testRecycledViewWithoutPurgingIndirectSubviews() { + let root = View() + let headerView = View() + headerView.isLayoutKitView = true + root.addSubview(headerView) + let titleView = View(viewReuseId: "1") + root.addSubview(titleView) + let collectionView = View() + let cellView = View() + cellView.isLayoutKitView = true + root.addSubview(collectionView) + collectionView.addSubview(cellView) + + let recycler = ViewRecycler(rootView: root) + let _ = recycler.makeOrRecycleView(havingViewReuseId: nil, viewProvider: { + return View() + }) + + recycler.purgeViews() + XCTAssertNil(headerView.superview) + XCTAssertNil(titleView.superview) + XCTAssertNotNil(cellView.superview) + } + #if os(iOS) || os(tvOS) /// Test that a reused view's frame shouldn't change if its transform and layer anchor point /// get set to the default values. @@ -148,5 +172,7 @@ extension View { convenience init(viewReuseId: String) { self.init(frame: .zero) self.viewReuseId = viewReuseId + // All views created in the ViewRecycler should be marked as LayoutKitView. + self.isLayoutKitView = true } } diff --git a/Sources/ViewRecycler.swift b/Sources/ViewRecycler.swift index c9df554c..af27e456 100644 --- a/Sources/ViewRecycler.swift +++ b/Sources/ViewRecycler.swift @@ -28,7 +28,7 @@ class ViewRecycler { private let defaultTransform = CGAffineTransform.identity #endif - /// Retains all subviews of rootView for recycling. + /// Retains all LayoutKit subviews of rootView for recycling. init(rootView: View?) { rootView?.walkSubviews { (view) in if let viewReuseId = view.viewReuseId { @@ -108,9 +108,14 @@ private var isLayoutKitViewKey: UInt8 = 0 extension View { - /// Calls visitor for each transitive subview. + /// Calls visitor for each transitive LayoutKit subview. func walkSubviews(visitor: (View) -> Void) { - for subview in subviews { + /* + Fix a recycling bug that purges indirect subviews by only walking subviews that are created by LayoutKit. + If a subview isn't a LayoutKit subview, then there is no need to walk subviews of it, + as they must not be created by this layout. + */ + for subview in subviews where subview.isLayoutKitView { visitor(subview) subview.walkSubviews(visitor: visitor) }