Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Caching] Caching Into Main Comparison #398

Closed
wants to merge 73 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c3480c2
Adds changes to Environment and EnvironmentKey needed for cross-rende…
kdaker Nov 3, 2022
721496c
Add ElementState and ComparableElement types
kyleve Nov 3, 2022
22b094a
Complete ElementState docs
kyleve Nov 3, 2022
1898904
ElementState and ComparableElement tests
kyleve Nov 4, 2022
c810f46
Add initial ElementState init test
kyleve Nov 4, 2022
cf5872b
Fix: Don't set wasVisited to true first
kyleve Nov 4, 2022
861519a
Add caching for ElementIdentifier
kdaker Nov 4, 2022
0164a99
Remove depth from ElementState
kdaker Nov 4, 2022
f8947af
Add test fixtures for ElementState
kyleve Nov 4, 2022
e445e45
Add IdentifiedNode for comparing the tree shape
kyleve Nov 4, 2022
fed8227
Merge remote-tracking branch 'origin/kareem/elementidentifier-changes…
kyleve Nov 4, 2022
bf70a85
Update ElementContent to use the ElementStateTree
kyleve Nov 5, 2022
3f0debd
Update BlueprintView to use ElementStateTree
kyleve Nov 5, 2022
f5cae50
Fix tests
kyleve Nov 5, 2022
3c7c90d
Tests pass
kyleve Nov 5, 2022
62fa56d
Remove CacheTree
kyleve Nov 5, 2022
f40c2ee
Add some tests for ElementState
kdaker Nov 5, 2022
ee8b386
Merge pull request #390 from square/kareem/add-elementstate-tests
kdaker Nov 5, 2022
3318286
Cleanup LayoutResultNode
kyleve Nov 7, 2022
a7bc706
states -> state
kyleve Nov 7, 2022
297b2c9
Properly pass through name
kyleve Nov 7, 2022
7d9a2be
Clean up manual element id creation
kyleve Nov 7, 2022
b92205e
Cleanup
kyleve Nov 8, 2022
7f8fe90
Code review cleanup 1
kyleve Nov 8, 2022
fd54137
ElementIdentifier test updates per code review
kyleve Nov 8, 2022
57cef4b
Code review: Rename ElementSnapshot, remove context from isEquivalent.
kyleve Nov 8, 2022
bdf5d98
Code review: Environment tracking API updates
kyleve Nov 8, 2022
ca05086
Code review: Update ComparableElement documentation
kyleve Nov 8, 2022
ea5e63a
Add debugDescription tests
kyleve Nov 8, 2022
cdb385f
Merge pull request #386 from square/kareem/elementidentifier-changes
kyleve Nov 8, 2022
6c963ca
Merge remote-tracking branch 'origin/feature/caching' into kareem/add…
kyleve Nov 8, 2022
6ebab2c
Cleanup: URLHandler isEquivalent
kyleve Nov 9, 2022
ad35d3c
Simplify observing read keys
kyleve Nov 11, 2022
973484c
Merge pull request #382 from square/kareem/add-environment-equatability
kyleve Nov 11, 2022
7fdcc1f
Merge remote-tracking branch 'origin/feature/caching' into kve/add-el…
kyleve Nov 11, 2022
f802fef
Fix logical merge conflicts
kyleve Nov 11, 2022
4337e27
Remove wasUpdateEquivalent
kyleve Nov 11, 2022
ce30b03
Improve debug description
kyleve Nov 11, 2022
d02bc12
Merge branch 'kve/add-elementstate' into kve/add-elementstate-tests
kyleve Nov 11, 2022
98c24a2
Rename perform to ifDebug
kyleve Nov 11, 2022
3b01e8a
Merge remote-tracking branch 'origin/kve/add-elementstate-tests' into…
kyleve Nov 14, 2022
dece8d3
Merge pull request #383 from square/kve/add-elementstate
kyleve Nov 14, 2022
2be8fba
Add ComparableElement conformance to Label and AttributedLabel
kdaker Nov 15, 2022
7e5f7a0
Merge pull request #393 from square/kareem/label-comparable-element-c…
kdaker Nov 15, 2022
9403bfa
Make layout setup and completion methods private
kyleve Nov 15, 2022
8c7f162
Cleanup before review
kyleve Nov 15, 2022
5b2e0dd
Simplify how storage and layout works for singular child elements
kyleve Nov 15, 2022
0736591
Pull in updated performance tests for caching testing
kyleve Nov 16, 2022
db5c748
Merge remote-tracking branch 'origin/main' into feature/caching
kyleve Nov 16, 2022
d54dfc3
Merge branch 'feature/caching' into kve/add-elementstate-tests
kyleve Nov 16, 2022
5cd07ff
Merge branch 'kve/add-elementstate-tests' into kve/caching-element-co…
kyleve Nov 16, 2022
fa6abc3
Merge branch 'kve/caching-element-content-updates' into kve/single-ch…
kyleve Nov 16, 2022
c18d918
Merge branch 'kve/caching-element-content-updates' into kve/add-perf-…
kyleve Nov 16, 2022
5a4625d
Merge branch 'feature/caching' into kve/add-elementstate-tests
kyleve Nov 16, 2022
447ca5b
Merge branch 'kve/add-elementstate-tests' into kve/caching-element-co…
kyleve Nov 16, 2022
daa657a
Add ElementContent(byMeasuring:) and the new MeasureNestedElementStor…
kdaker Nov 16, 2022
f2127d1
linter fixes
kdaker Nov 16, 2022
9f03e97
remove default from new init
kdaker Nov 16, 2022
9d6ad9f
more linter fixes
kdaker Nov 16, 2022
c9cadd1
Introduce fixes found when fully integrating new ElementState APIs. T…
kyleve Nov 16, 2022
fe699e6
Merge remote-tracking branch 'origin/kve/caching-element-content-upda…
kyleve Nov 28, 2022
2766acd
Merge pull request #402 from square/kve/caching-full-integration-fixes
kyleve Nov 28, 2022
2fe3f38
Merge pull request #388 from square/kve/add-elementstate-tests
kyleve Nov 28, 2022
f517d1a
Fix formatting
kyleve Nov 28, 2022
668f04d
Fix pixel rounding passthrough
kyleve Nov 28, 2022
7a0f993
Merge pull request #389 from square/kve/caching-element-content-updates
kyleve Nov 30, 2022
3874800
Merge branch 'feature/caching' into kve/single-child-storage
kyleve Dec 1, 2022
750d34a
Add forEachElement to SingleChildStorage
kyleve Dec 1, 2022
4925883
Merge pull request #395 from square/kve/single-child-storage
kyleve Dec 1, 2022
0e275df
Merge remote-tracking branch 'origin/feature/caching' into kareem/ele…
kyleve Dec 2, 2022
1713907
Fix: Logical merge conflict, missing method
kyleve Dec 2, 2022
b46ba20
Merge pull request #397 from square/kareem/element-content-bymeasuring
kyleve Dec 2, 2022
f9c6120
Begin backfilling state tree tests (#404)
kyleve Dec 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions BlueprintUI/Sources/BlueprintView/BlueprintView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public final class BlueprintView: UIView {

private var sizesThatFit: [SizeConstraint: CGSize] = [:]

/// The live, tracked state for each element in the element tree.
internal let rootState: ElementStateTree

/// A base environment used when laying out and rendering the element tree.
///
/// Some keys will be overridden with the traits from the view itself. Eg, `windowSize`, `safeAreaInsets`, etc.
Expand All @@ -48,8 +51,7 @@ public final class BlueprintView: UIView {
/// environment, the values from this environment will take priority over the inherited environment.
public var environment: Environment {
didSet {
// Shortcut: If both environments were empty, nothing changed.
if oldValue.isEmpty && environment.isEmpty { return }
guard oldValue != environment else { return }

setNeedsViewHierarchyUpdate()
}
Expand Down Expand Up @@ -100,8 +102,23 @@ public final class BlueprintView: UIView {
}
}

/// An optional name to help identify this view
public var name: String?
/// An optional name to help identify this view. Will be
/// present in signpost logs, for example.
public var name: String? {
didSet {
guard oldValue != name else { return }

didUpdateName()
}
}

private func didUpdateName() {
if let name = name, name.isEmpty == false {
rootState.name = "BlueprintView: \(name)"
} else {
rootState.name = "BlueprintView"
}
}

/// Provides performance metrics about the duration of layouts, updates, etc.
public weak var metricsDelegate: BlueprintViewMetricsDelegate? = nil
Expand Down Expand Up @@ -132,18 +149,22 @@ public final class BlueprintView: UIView {
content: UIView.describe { _ in },
// Because no layout update occurs here, passing an empty environment is fine;
// the correct environment will be passed during update.
environment: .empty,
environment: environment,
layoutAttributes: LayoutAttributes(),
children: []
)
)

rootState = .init(name: "")

super.init(frame: CGRect.zero)

backgroundColor = .white
addSubview(rootController.view)
setContentHuggingPriority(.defaultHigh, for: .horizontal)
setContentHuggingPriority(.defaultHigh, for: .vertical)

didUpdateName()
}

public convenience override init(frame: CGRect) {
Expand Down Expand Up @@ -213,8 +234,7 @@ public final class BlueprintView: UIView {

let measurement = element.content.measure(
in: constraint,
environment: makeEnvironment(),
cache: CacheFactory.makeCache(name: cacheName)
environment: makeEnvironment()
)

sizesThatFit[constraint] = measurement
Expand Down Expand Up @@ -340,9 +360,10 @@ public final class BlueprintView: UIView {
)

/// Grab view descriptions
let viewNodes = element?
.layout(layoutAttributes: LayoutAttributes(frame: rootFrame), environment: environment)
.resolve() ?? []
let viewNodes = calculateNativeViewNodes(
in: environment,
rootFrame: rootFrame
)

let measurementEndDate = Date()
Logger.logLayoutEnd(view: self)
Expand Down Expand Up @@ -391,6 +412,31 @@ public final class BlueprintView: UIView {
)
}

/// Performs a full measurement and layout pass of all contained elements, and then collapses the nodes down
/// into `NativeViewNode`s, which represent only the view-backed elements. These view nodes
/// are then pushed into a `NativeViewController` to update the on-screen view hierarchy.
private func calculateNativeViewNodes(
in environment: Environment,
rootFrame: CGRect
) -> [(path: ElementPath, node: NativeViewNode)] {

if let element = self.element {
let (_, node) = rootState.performUpdate(with: element, in: environment) { state in
LayoutResultNode(
identifier: .identifierFor(singleChild: element),
layoutAttributes: .init(frame: rootFrame),
environment: environment,
state: state
)
}

return node.resolve()
} else {
_ = rootState.teardownRootElement()
return []
}
}

var currentNativeViewControllers: [(path: ElementPath, node: NativeViewController)] {

/// Perform an update if needed so that the node hierarchy is fully populated.
Expand Down Expand Up @@ -688,3 +734,4 @@ extension BlueprintView.NativeViewController {
}
}


65 changes: 65 additions & 0 deletions BlueprintUI/Sources/Element/ComparableElement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// ComparableElement.swift
// BlueprintUI
//
// Created by Kyle Van Essen on 7/3/21.
//

import Foundation
import UIKit


/// An `ComparableElement` is an element which can cache its
/// measurements and layouts across layout passes in order to dramatically improve
/// performance of repeated layouts.
///
/// Some element types within Blueprint already conform to `ComparableElement`.
/// If your element can be easily compared for equality, consider making your
/// it conform to `ComparableElement` for improved performance across layouts.
public protocol ComparableElement: AnyComparableElement {

///
/// Indicates if the element is equivalent to the other provided element.
/// Return `true` if the elements are the same, or return `false` if something about
/// the element changed that will affect measurement or layout.
///
/// ## Note
/// Even if this method returns true, the `ViewDescription`
/// backing the element will still be re-applied to the on-screen view.
///
/// ## Equatable
/// If your element conforms to `Equatable`, this method is synthesized automatically.
///
func isEquivalent(to other: Self) -> Bool
}


/// Provides a default `ComparableElement` implementation for `Equatable` elements.
extension ComparableElement where Self: Equatable {

public func isEquivalent(to other: Self) -> Bool {
self == other
}
}


/// A type-erased version of `ComparableElement`, allowing the comparison
/// of two arbitrary elements, and allowing direct access to methods, without self or associated type constraints.
///
/// ## Note
/// You do not need to implement this protocol yourself. It is implemented by Blueprint on `ComparableElement`.
public protocol AnyComparableElement: Element {

/// Returns true if the two elements are the same type, and are equivalent.
func anyIsEquivalent(to other: AnyComparableElement) -> Bool
}


extension ComparableElement {

public func anyIsEquivalent(to other: AnyComparableElement) -> Bool {
guard let other = other as? Self else { return false }

return isEquivalent(to: other)
}
}
Loading