From 20ebb83b47ecee1c8ef914895c6e3246929b362b Mon Sep 17 00:00:00 2001 From: Liu Bing Date: Wed, 31 Oct 2018 21:06:33 -0700 Subject: [PATCH 1/2] Fix a recycling bug that purges indirect subviews Only walk first level of subviews that are created by LayoutKit. If a subview isn't created by LayoutKit, then there is no need to walk subviews of it, as they must not be created by this layout. --- LayoutKitTests/ViewRecyclerTests.swift | 24 ++++++++++++++++++++++++ Sources/ViewRecycler.swift | 8 ++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/LayoutKitTests/ViewRecyclerTests.swift b/LayoutKitTests/ViewRecyclerTests.swift index 8fd291ad..994129d6 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. diff --git a/Sources/ViewRecycler.swift b/Sources/ViewRecycler.swift index c9df554c..81692311 100644 --- a/Sources/ViewRecycler.swift +++ b/Sources/ViewRecycler.swift @@ -28,15 +28,19 @@ 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 + let visitor: (View) -> Void = { view in if let viewReuseId = view.viewReuseId { self.viewsById[viewReuseId] = view } else { self.unidentifiedViews.insert(view) } } + rootView?.subviews.filter({$0.isLayoutKitView || $0.viewReuseId != nil}).forEach { (view) in + visitor(view) + view.walkSubviews(visitor: visitor) + } } /** From 15b510a0f01babc2f771e99206d5bf30a6db8a43 Mon Sep 17 00:00:00 2001 From: Liu Bing Date: Thu, 1 Nov 2018 20:07:50 -0700 Subject: [PATCH 2/2] Check LayoutKitView at every level. Fix test failures in ViewRecyclerTests. Add comments for the change. --- LayoutKitTests/ViewRecyclerTests.swift | 2 ++ Sources/ViewRecycler.swift | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/LayoutKitTests/ViewRecyclerTests.swift b/LayoutKitTests/ViewRecyclerTests.swift index 994129d6..311323b7 100644 --- a/LayoutKitTests/ViewRecyclerTests.swift +++ b/LayoutKitTests/ViewRecyclerTests.swift @@ -172,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 81692311..af27e456 100644 --- a/Sources/ViewRecycler.swift +++ b/Sources/ViewRecycler.swift @@ -30,17 +30,13 @@ class ViewRecycler { /// Retains all LayoutKit subviews of rootView for recycling. init(rootView: View?) { - let visitor: (View) -> Void = { view in + rootView?.walkSubviews { (view) in if let viewReuseId = view.viewReuseId { self.viewsById[viewReuseId] = view } else { self.unidentifiedViews.insert(view) } } - rootView?.subviews.filter({$0.isLayoutKitView || $0.viewReuseId != nil}).forEach { (view) in - visitor(view) - view.walkSubviews(visitor: visitor) - } } /** @@ -112,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) }