diff --git a/Sources/UIComponent/Core/Model/RenderNode/AnyRenderNode.swift b/Sources/UIComponent/Core/Model/RenderNode/AnyRenderNode.swift index 99d9609e..9af3c7c2 100644 --- a/Sources/UIComponent/Core/Model/RenderNode/AnyRenderNode.swift +++ b/Sources/UIComponent/Core/Model/RenderNode/AnyRenderNode.swift @@ -25,6 +25,9 @@ public struct AnyRenderNode: RenderNode { public var reuseStrategy: ReuseStrategy { erasing.reuseStrategy } + public var defaultReuseKey: String { + "AnyRenderNode<\(erasing.defaultReuseKey)>" + } public var size: CGSize { erasing.size } @@ -71,6 +74,9 @@ public struct AnyRenderNodeOfView: RenderNode { public var reuseStrategy: ReuseStrategy { erasing.reuseStrategy } + public var defaultReuseKey: String { + "AnyRenderNodeOfView<\(erasing.defaultReuseKey)>" + } public var size: CGSize { erasing.size } diff --git a/Sources/UIComponent/Core/Model/RenderNode/RenderNode.swift b/Sources/UIComponent/Core/Model/RenderNode/RenderNode.swift index 74743c60..0d96830d 100644 --- a/Sources/UIComponent/Core/Model/RenderNode/RenderNode.swift +++ b/Sources/UIComponent/Core/Model/RenderNode/RenderNode.swift @@ -20,6 +20,10 @@ public protocol RenderNode { /// The strategy to use when reusing views. var reuseStrategy: ReuseStrategy { get } + /// The default reuse key for the render node. This key will be used when reuseStrategy is set to .automatic. + /// This will also be used as fallbackId for structured identity when id is not set. + var defaultReuseKey: String { get } + /// The size of the render node. var size: CGSize { get } @@ -108,6 +112,7 @@ extension RenderNode { public var id: String? { nil } public var animator: Animator? { nil } public var reuseStrategy: ReuseStrategy { .automatic } + public var defaultReuseKey: String { "\(type(of: self))" } public var shouldRenderView: Bool { children.isEmpty } public func makeView() -> View { @@ -132,7 +137,7 @@ extension RenderNode { public func defaultVisibleRenderablesImplementation(in frame: CGRect) -> [Renderable] { var result = [Renderable]() if shouldRenderView, frame.intersects(CGRect(origin: .zero, size: size)) { - result.append(Renderable(frame: CGRect(origin: .zero, size: size), renderNode: self, fallbackId: "\(type(of: self))")) + result.append(Renderable(frame: CGRect(origin: .zero, size: size), renderNode: self, fallbackId: defaultReuseKey)) } let indexes = visibleIndexes(in: frame) for i in indexes { @@ -155,7 +160,7 @@ extension RenderNode { internal func _makeView() -> UIView { switch reuseStrategy { case .automatic: - return ReuseManager.shared.dequeue(identifier: "\(type(of: self))", makeView()) + return ReuseManager.shared.dequeue(identifier: defaultReuseKey, makeView()) case .noReuse: return makeView() case .key(let key): diff --git a/Tests/UIComponentTests/ReuseTest.swift b/Tests/UIComponentTests/ReuseTest.swift index 225661f0..1c30dff4 100644 --- a/Tests/UIComponentTests/ReuseTest.swift +++ b/Tests/UIComponentTests/ReuseTest.swift @@ -3,6 +3,7 @@ import XCTest @testable import UIComponent +import UIKit final class ReuseTests: XCTestCase { var componentView: ComponentView! @@ -151,6 +152,25 @@ final class ReuseTests: XCTestCase { XCTAssertNotEqual(existingLabel, newLabel) } + func testNoReuseWithDifferentAttributesAndAnyComponentOfView() { + componentView.component = Text("1").textColor(.red).eraseToAnyComponentOfView() + componentView.reloadData() + XCTAssertEqual(componentView.subviews.count, 1) + let existingLabel = componentView.subviews.first as? UILabel + XCTAssertNotNil(existingLabel) + XCTAssertEqual(existingLabel?.text, "1") + XCTAssertEqual(existingLabel?.textColor, .red) + componentView.component = Text("2").eraseToAnyComponentOfView() + componentView.reloadData() + XCTAssertEqual(componentView.subviews.count, 1) + let newLabel = componentView.subviews.first as? UILabel + XCTAssertNotNil(newLabel) + XCTAssertEqual(newLabel?.text, "2") + + // the UILabel should not be reused + XCTAssertNotEqual(existingLabel, newLabel) + } + func testNoReuseWithSameAttributes() { componentView.component = Text("1").reuseStrategy(.noReuse).textColor(.red).id("1") componentView.reloadData()