From 73f6360d7863d3d035a3ad7c477c306308764258 Mon Sep 17 00:00:00 2001 From: Anders Ha Date: Tue, 9 Apr 2019 21:29:44 +0100 Subject: [PATCH] Change the symbol generation algorithm. Rename the struct. --- Bento.xcodeproj/project.pbxproj | 16 +-- Bento/Renderable/AnyRenderable.swift | 10 +- Bento/Renderable/Renderable.swift | 23 ++-- Bento/Renderable/ReusabilityHint.swift | 60 +++++++++ .../Renderable/ReusabilityHintCombiner.swift | 27 ---- Bento/Views/BentoReusableView.swift | 2 +- BentoTests/ReusabilityHintCombinerTests.swift | 115 ---------------- BentoTests/ReusabilityHintTests.swift | 125 ++++++++++++++++++ 8 files changed, 211 insertions(+), 167 deletions(-) create mode 100644 Bento/Renderable/ReusabilityHint.swift delete mode 100644 Bento/Renderable/ReusabilityHintCombiner.swift delete mode 100644 BentoTests/ReusabilityHintCombinerTests.swift create mode 100644 BentoTests/ReusabilityHintTests.swift diff --git a/Bento.xcodeproj/project.pbxproj b/Bento.xcodeproj/project.pbxproj index 126fb70..cc107df 100644 --- a/Bento.xcodeproj/project.pbxproj +++ b/Bento.xcodeproj/project.pbxproj @@ -82,8 +82,8 @@ 65E3ECAC2113591500869DF3 /* FocusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E3ECAB2113591500869DF3 /* FocusableView.swift */; }; 65E3ECAE2113594600869DF3 /* BentoCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E3ECAD2113594600869DF3 /* BentoCollectionView.swift */; }; 65E3ECB02113598700869DF3 /* UIKit+BentoCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E3ECAF2113598700869DF3 /* UIKit+BentoCollectionView.swift */; }; - 65E4D8B8225D0AC100CA7CB3 /* ReusabilityHintCombiner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E4D8B7225D0AC100CA7CB3 /* ReusabilityHintCombiner.swift */; }; - 65E4D8BA225D11C700CA7CB3 /* ReusabilityHintCombinerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E4D8B9225D11C700CA7CB3 /* ReusabilityHintCombinerTests.swift */; }; + 65E4D8B8225D0AC100CA7CB3 /* ReusabilityHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E4D8B7225D0AC100CA7CB3 /* ReusabilityHint.swift */; }; + 65E4D8BA225D11C700CA7CB3 /* ReusabilityHintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E4D8B9225D11C700CA7CB3 /* ReusabilityHintTests.swift */; }; 740921B620ACDDDA00B59F5C /* IfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740921B520ACDDDA00B59F5C /* IfTests.swift */; }; 740921B820ACE5EC00B59F5C /* ConcatenationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740921B720ACE5EC00B59F5C /* ConcatenationTests.swift */; }; 74208FA12083B1F00062CC8D /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74208FA22083B1F00062CC8D /* Nimble.framework */; }; @@ -211,8 +211,8 @@ 65E3ECAB2113591500869DF3 /* FocusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableView.swift; sourceTree = ""; }; 65E3ECAD2113594600869DF3 /* BentoCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BentoCollectionView.swift; sourceTree = ""; }; 65E3ECAF2113598700869DF3 /* UIKit+BentoCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+BentoCollectionView.swift"; sourceTree = ""; }; - 65E4D8B7225D0AC100CA7CB3 /* ReusabilityHintCombiner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusabilityHintCombiner.swift; sourceTree = ""; }; - 65E4D8B9225D11C700CA7CB3 /* ReusabilityHintCombinerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusabilityHintCombinerTests.swift; sourceTree = ""; }; + 65E4D8B7225D0AC100CA7CB3 /* ReusabilityHint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusabilityHint.swift; sourceTree = ""; }; + 65E4D8B9225D11C700CA7CB3 /* ReusabilityHintTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusabilityHintTests.swift; sourceTree = ""; }; 740921B520ACDDDA00B59F5C /* IfTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfTests.swift; sourceTree = ""; }; 740921B720ACE5EC00B59F5C /* ConcatenationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcatenationTests.swift; sourceTree = ""; }; 74208FA22083B1F00062CC8D /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -435,7 +435,7 @@ 65E3ECA521133C9400869DF3 /* UIKit+CollectionViewFocus.swift */, 582D9986217F87B100C67B0D /* ComponentLifecycleAware.swift */, 65020C312203186400DC8F42 /* NativeView.swift */, - 65E4D8B7225D0AC100CA7CB3 /* ReusabilityHintCombiner.swift */, + 65E4D8B7225D0AC100CA7CB3 /* ReusabilityHint.swift */, ); path = Renderable; sourceTree = ""; @@ -453,7 +453,7 @@ 5830C5E621F22DDC0029044B /* ComponentLifecycleAware.swift */, 651E75BF221005E300130866 /* UIKitContainerDiffApplicationTests.swift */, 58FC4421207CF29F00DA3614 /* Info.plist */, - 65E4D8B9225D11C700CA7CB3 /* ReusabilityHintCombinerTests.swift */, + 65E4D8B9225D11C700CA7CB3 /* ReusabilityHintTests.swift */, ); path = BentoTests; sourceTree = ""; @@ -741,7 +741,7 @@ 9AF4786C2120CA7500F87E21 /* BentoReusableView.swift in Sources */, 61B64BB6208745730092082C /* CollectionViewContainerCell.swift in Sources */, 65E3ECA2211317EA00869DF3 /* FocusCoordinator.swift in Sources */, - 65E4D8B8225D0AC100CA7CB3 /* ReusabilityHintCombiner.swift in Sources */, + 65E4D8B8225D0AC100CA7CB3 /* ReusabilityHint.swift in Sources */, 65E3ECAE2113594600869DF3 /* BentoCollectionView.swift in Sources */, 65E3ECAC2113591500869DF3 /* FocusableView.swift in Sources */, A9509880661501C40B50E453 /* TableViewHeaderFooterView.swift in Sources */, @@ -760,7 +760,7 @@ 58D27BA721B83B2700DC9600 /* DeletableTests.swift in Sources */, 653D460B2256665000CF3E4C /* AdapterStoreTests.swift in Sources */, 58FC4429207CF2BB00DA3614 /* TestRenderable.swift in Sources */, - 65E4D8BA225D11C700CA7CB3 /* ReusabilityHintCombinerTests.swift in Sources */, + 65E4D8BA225D11C700CA7CB3 /* ReusabilityHintTests.swift in Sources */, 5830C5E721F22DDC0029044B /* ComponentLifecycleAware.swift in Sources */, 740921B620ACDDDA00B59F5C /* IfTests.swift in Sources */, 740921B820ACE5EC00B59F5C /* ConcatenationTests.swift in Sources */, diff --git a/Bento/Renderable/AnyRenderable.swift b/Bento/Renderable/AnyRenderable.swift index ccc7413..7ab4b22 100644 --- a/Bento/Renderable/AnyRenderable.swift +++ b/Bento/Renderable/AnyRenderable.swift @@ -25,8 +25,8 @@ public struct AnyRenderable: Renderable { base.render(in: view) } - public func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) { - base.makeReusabilityHint(using: &combiner) + public func makeReusabilityHint(_ hint: inout ReusabilityHint) { + base.makeReusabilityHint(&hint) } func cast(to type: T.Type) -> T? { @@ -86,8 +86,8 @@ class AnyRenderableBox: AnyRenderableBoxBase { base.render(in: view as! Base.View) } - override func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) { - base.makeReusabilityHint(using: &combiner) + override func makeReusabilityHint(_ hint: inout ReusabilityHint) { + base.makeReusabilityHint(&hint) } override func cast(to type: T.Type) -> T? { @@ -108,6 +108,6 @@ class AnyRenderableBoxBase { return AnyRenderable(self) } func render(in view: UIView) { fatalError() } - func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) { fatalError() } + func makeReusabilityHint(_ hint: inout ReusabilityHint) { fatalError() } func cast(to type: T.Type) -> T? { fatalError() } } diff --git a/Bento/Renderable/Renderable.swift b/Bento/Renderable/Renderable.swift index 7fd5418..94ad7f1 100644 --- a/Bento/Renderable/Renderable.swift +++ b/Bento/Renderable/Renderable.swift @@ -26,8 +26,8 @@ public protocol Renderable { /// // Logic to recreate the view hierarchy if types and orders of components do not match /// } /// - /// func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) { - /// children.forEach { combiner.combine($0) } + /// func makeReusabilityHint(_ hint: inout ReusabilityHint) { + /// children.forEach { hint.combine($0) } /// } /// } /// @@ -51,7 +51,7 @@ public protocol Renderable { /// CompositeComponent(children: [C(), B(), A()]) /// ``` /// - /// - important: The order of `combiner.combine(_:)` matters. + /// - important: The order of `ReusabilityHint.combine(_:)` matters. /// /// - important: Bento always considers the component type for view reusability. So components need not combine /// its own type again. @@ -60,12 +60,13 @@ public protocol Renderable { /// or dynamic view hierarchies. /// /// - parameters: - /// - combiner: The combiner to concatenate all relevant information that affects reusability. - func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) + /// - hint: An opaque structure which may be fed with all relevant information of which Bento should take account + /// when considering view reusability. + func makeReusabilityHint(_ hint: inout ReusabilityHint) } public extension Renderable { - func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) {} + func makeReusabilityHint(_ hint: inout ReusabilityHint) {} func asAnyRenderable() -> AnyRenderable { return AnyRenderable(self) @@ -99,13 +100,13 @@ internal extension Renderable { ?? type(of: self) } - var reusabilityHintCombiner: ReusabilityHintCombiner { - var combiner = ReusabilityHintCombiner(root: self) - makeReusabilityHint(using: &combiner) - return combiner + var reusabilityHint: ReusabilityHint { + var hint = ReusabilityHint(root: self) + makeReusabilityHint(&hint) + return hint } var reuseIdentifier: String { - return reusabilityHintCombiner.generate() + return reusabilityHint.generate() } } diff --git a/Bento/Renderable/ReusabilityHint.swift b/Bento/Renderable/ReusabilityHint.swift new file mode 100644 index 0000000..730cc1c --- /dev/null +++ b/Bento/Renderable/ReusabilityHint.swift @@ -0,0 +1,60 @@ +public struct ReusabilityHint { + internal enum Mark: Equatable { + case node(Any.Type) + case begin + case end + + static func == (lhs: Mark, rhs: Mark) -> Bool { + switch (lhs, rhs) { + case let (.node(lhs), .node(rhs)): + return lhs == rhs + case (.begin, .begin), (.end, end): + return true + default: + return false + } + } + } + + internal var marks: [Mark] + + internal init(root: R) { + marks = [.node(root.componentType), .begin] + } + + public mutating func combine(_ component: R) { + marks.append(.node(component.componentType)) + marks.append(.begin) + component.makeReusabilityHint(&self) + marks.append(.end) + } + + func generate() -> String { + // NOTE: It isn't quite important that we generate a very clean symbol. It only needs to be consistently + // reproduced. + var symbol = marks.reduce(into: "") { buffer, mark in + switch mark { + case let .node(type): + buffer += fullyQualifiedTypeName(of: type) + case .begin: + buffer += "[" + case .end: + buffer += "]" + } + } + + // NOTE: Since the root node opens with a `begin` mark, we should balance it with an `end` mark. + symbol += "]" + return symbol + } + + func isCompatible(with other: ReusabilityHint) -> Bool { + return marks == other.marks + } +} + +func fullyQualifiedTypeName(of type: Any.Type) -> String { + /// NOTE: `String.init(reflecting:)` gives the fully qualified type name. + // Tests would catch unexpeced type name printing behavior due to Swift runtime changes. + return String(reflecting: type) +} diff --git a/Bento/Renderable/ReusabilityHintCombiner.swift b/Bento/Renderable/ReusabilityHintCombiner.swift deleted file mode 100644 index 399088d..0000000 --- a/Bento/Renderable/ReusabilityHintCombiner.swift +++ /dev/null @@ -1,27 +0,0 @@ -public struct ReusabilityHintCombiner { - internal var types: [Any.Type] - - internal init(root: R) { - types = [root.componentType] - } - - public mutating func combine(_ component: R) { - types.append(component.componentType) - component.makeReusabilityHint(using: &self) - } - - __consuming func generate() -> String { - return types.map(fullyQualifiedTypeName(of:)).joined(separator: ",") - } - - __consuming func isCompatible(with other: __owned ReusabilityHintCombiner) -> Bool { - return types.elementsEqual(other.types, by: ==) - } -} - - -func fullyQualifiedTypeName(of type: Any.Type) -> String { - /// NOTE: `String.init(reflecting:)` gives the fully qualified type name. - // Tests would catch unexpeced type name printing behavior due to Swift runtime changes. - return String(reflecting: type) -} diff --git a/Bento/Views/BentoReusableView.swift b/Bento/Views/BentoReusableView.swift index 2a1043d..f263aab 100644 --- a/Bento/Views/BentoReusableView.swift +++ b/Bento/Views/BentoReusableView.swift @@ -14,7 +14,7 @@ extension BentoReusableView { if let oldComponent = oldComponent, let view = containedView, - oldComponent.reusabilityHintCombiner.isCompatible(with: component.reusabilityHintCombiner) { + oldComponent.reusabilityHint.isCompatible(with: component.reusabilityHint) { renderingView = view } else { renderingView = component.viewType.generate() diff --git a/BentoTests/ReusabilityHintCombinerTests.swift b/BentoTests/ReusabilityHintCombinerTests.swift deleted file mode 100644 index f6183aa..0000000 --- a/BentoTests/ReusabilityHintCombinerTests.swift +++ /dev/null @@ -1,115 +0,0 @@ -import XCTest -import UIKit -import Nimble -@testable import Bento - -final class ReusabilityHintCombinerTests: XCTestCase { - func test_combine_A() { - let expectedSymbol = "BentoTests.ComponentA" - - let combiner = ReusabilityHintCombiner(root: ComponentA()) - let symbol = combiner.generate() - - expect(symbol) == String(reflecting: ComponentA.self) - expect(symbol) == expectedSymbol - } - - func test_combine_AB() { - let expectedSymbol = "BentoTests.ComponentA,BentoTests.ComponentB" - - var combiner = ReusabilityHintCombiner(root: ComponentA()) - combiner.combine(ComponentB()) - let symbol = combiner.generate() - - expect(symbol) == [ - String(reflecting: ComponentA.self), - String(reflecting: ComponentB.self) - ].joined(separator: ",") - expect(symbol) == expectedSymbol - } - - func test_combine_BA() { - let expectedSymbol = "BentoTests.ComponentB,BentoTests.ComponentA" - - var combiner = ReusabilityHintCombiner(root: ComponentB()) - combiner.combine(ComponentA()) - let symbol = combiner.generate() - - expect(symbol) == [ - String(reflecting: ComponentB.self), - String(reflecting: ComponentA.self) - ].joined(separator: ",") - expect(symbol) == expectedSymbol - } - - func test_combine_ABC() { - let expectedSymbol = "BentoTests.ComponentA,BentoTests.ComponentB,BentoTests.ComponentC" - - var combiner = ReusabilityHintCombiner(root: ComponentA()) - combiner.combine(ComponentB()) - combiner.combine(ComponentC()) - let symbol = combiner.generate() - - expect(symbol) == [ - String(reflecting: ComponentA.self), - String(reflecting: ComponentB.self), - String(reflecting: ComponentC.self) - ].joined(separator: ",") - expect(symbol) == expectedSymbol - } - - func test_reuseIdentifier_single() { - let expectedSymbol = "BentoTests.ComponentA" - let symbol = ComponentA().reuseIdentifier - - expect(symbol) == String(reflecting: ComponentA.self) - expect(symbol) == expectedSymbol - } - - func test_reuseIdentifier_containerOfAB() { - let expectedSymbol = "BentoTests.ContainerOfAB,BentoTests.ComponentA,BentoTests.ComponentB" - let symbol = ContainerOfAB().reuseIdentifier - - expect(symbol) == [ - String(reflecting: ContainerOfAB.self), - String(reflecting: ComponentA.self), - String(reflecting: ComponentB.self) - ].joined(separator: ",") - expect(symbol) == expectedSymbol - } - - func test_reuseIdentifier_containerOfContainerOfAB() { - let expectedSymbol = "BentoTests.ContainerOfContainerOfAB,BentoTests.ContainerOfAB,BentoTests.ComponentA,BentoTests.ComponentB" - let symbol = ContainerOfContainerOfAB().reuseIdentifier - - expect(symbol) == [ - String(reflecting: ContainerOfContainerOfAB.self), - String(reflecting: ContainerOfAB.self), - String(reflecting: ComponentA.self), - String(reflecting: ComponentB.self) - ].joined(separator: ",") - expect(symbol) == expectedSymbol - } -} - -private protocol ReusabilityHintRenderable: Renderable {} -extension ReusabilityHintRenderable { - func render(in view: UIView) {} -} - -// NOTE: Marked as internal so that the fully qualified type name (needed by a test assertion) does not depend on the -// source location. -internal struct ComponentA: ReusabilityHintRenderable {} -internal struct ComponentB: ReusabilityHintRenderable {} -internal struct ComponentC: ReusabilityHintRenderable {} -internal struct ContainerOfAB: ReusabilityHintRenderable { - internal func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) { - combiner.combine(ComponentA()) - combiner.combine(ComponentB()) - } -} -internal struct ContainerOfContainerOfAB: ReusabilityHintRenderable { - func makeReusabilityHint(using combiner: inout ReusabilityHintCombiner) { - combiner.combine(ContainerOfAB()) - } -} diff --git a/BentoTests/ReusabilityHintTests.swift b/BentoTests/ReusabilityHintTests.swift new file mode 100644 index 0000000..4be9866 --- /dev/null +++ b/BentoTests/ReusabilityHintTests.swift @@ -0,0 +1,125 @@ +import XCTest +import UIKit +import Nimble +@testable import Bento + +final class ReusabilityHintTests: XCTestCase { + func test_combine_A() { + let expectedSymbol = "BentoTests.ComponentA[]" + + let combiner = ReusabilityHint(root: ComponentA()) + let symbol = combiner.generate() + + expect(symbol) == expectedSymbol + } + + func test_combine_AB() { + let expectedSymbol = "BentoTests.ComponentA[BentoTests.ComponentB[]]" + + var combiner = ReusabilityHint(root: ComponentA()) + combiner.combine(ComponentB()) + + let symbol = combiner.generate() + expect(symbol) == expectedSymbol + } + + func test_combine_BA() { + let expectedSymbol = "BentoTests.ComponentB[BentoTests.ComponentA[]]" + + var combiner = ReusabilityHint(root: ComponentB()) + combiner.combine(ComponentA()) + + let symbol = combiner.generate() + expect(symbol) == expectedSymbol + } + + func test_combine_ABC() { + let expectedSymbol = "BentoTests.ComponentA[BentoTests.ComponentB[]BentoTests.ComponentC[]]" + + var combiner = ReusabilityHint(root: ComponentA()) + combiner.combine(ComponentB()) + combiner.combine(ComponentC()) + + let symbol = combiner.generate() + expect(symbol) == expectedSymbol + } + + func test_should_be_compatible_to_itself() { + let hint = Container([]).reusabilityHint + + expect(hint.isCompatible(with: hint)) == true + } + + func test_should_be_compatible_to_itself_with_children() { + let hint = Container([ + ComponentA().asAnyRenderable(), + ComponentB().asAnyRenderable() + ]).reusabilityHint + + expect(hint.isCompatible(with: hint)) == true + } + + func test_trees_that_are_equal_after_flattening_should_not_be_considered_compatible() { + let lhs = Container([ + Container([ + Container([]).asAnyRenderable() + ]).asAnyRenderable() + ]).reusabilityHint + let rhs = Container([ + Container([]).asAnyRenderable(), + Container([]).asAnyRenderable() + ]).reusabilityHint + + expect(lhs.isCompatible(with: rhs)) == false + expect(lhs.generate()) != rhs.generate() + } + + func test_reuseIdentifier_single() { + let expectedSymbol = "BentoTests.ComponentA[]" + let symbol = ComponentA().reuseIdentifier + + expect(symbol) == expectedSymbol + } + + func test_reuseIdentifier_containerOfAB() { + let expectedSymbol = "BentoTests.Container[BentoTests.ComponentA[]BentoTests.ComponentB[]]" + let symbol = Container([ + ComponentA().asAnyRenderable(), + ComponentB().asAnyRenderable() + ]).reuseIdentifier + expect(symbol) == expectedSymbol + } + + func test_reuseIdentifier_containerOfContainerOfAB() { + let expectedSymbol = "BentoTests.Container[BentoTests.Container[BentoTests.ComponentA[]BentoTests.ComponentB[]]]" + let symbol = Container([ + Container([ + ComponentA().asAnyRenderable(), + ComponentB().asAnyRenderable() + ]).asAnyRenderable() + ]).reuseIdentifier + expect(symbol) == expectedSymbol + } +} + +private protocol ReusabilityHintRenderable: Renderable {} +extension ReusabilityHintRenderable { + func render(in view: UIView) {} +} + +// NOTE: Marked as internal so that the fully qualified type name (needed by a test assertion) does not depend on the +// source location. +internal struct ComponentA: ReusabilityHintRenderable {} +internal struct ComponentB: ReusabilityHintRenderable {} +internal struct ComponentC: ReusabilityHintRenderable {} +internal struct Container: ReusabilityHintRenderable { + let children: [AnyRenderable] + + init(_ children: [AnyRenderable]) { + self.children = children + } + + internal func makeReusabilityHint(_ hint: inout ReusabilityHint) { + children.forEach { hint.combine($0) } + } +}