-
Notifications
You must be signed in to change notification settings - Fork 45
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
Change how we cache sizing across sizeThatFits and autolayout #277
Conversation
6d085fb
to
9e22cfd
Compare
@@ -226,18 +294,20 @@ public final class BlueprintView: UIView { | |||
setNeedsViewHierarchyUpdate() | |||
} | |||
|
|||
private func performUpdate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was called in exactly one place and only did this, so I removed it.
9e22cfd
to
af9e981
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this automatically solve our perf issues with preferredContentSize?
Ish. Kinda. But not enough. In my testing, it seems to help with about 50% of the excess perf, since we now cache some calls to |
if bounds.width == 0 { | ||
return .unconstrained | ||
} else { | ||
return .init(width: bounds.width) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also be doing this for height?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd have to switch on the content hugging priority I think; since without a passed in constraint or layout priority, we'd have to base it on those.
I'm not sure I follow—what is the functionality you'd be missing out on by omitting this mode? Couldn't you still get this same behavior by attempting a layout with a 0-width frame with the changes you have in this PR? I'm not sure I understand the value of the current state over what's here. |
I think (unintentionally or not), this previously allowed Swift previews to always display the natural size of an embedded blueprint view. @bencochran added the existing behaviour and could speak more to it |
…anges * origin/main: Fix size assertion grammar. Prep 0.32.0 Update BlueprintUI/Sources/Layout/Builder.swift Add support for for loop and available checks in result builders. Update changelog Break a retain cycle in posts demo Break retain cycle in FocusState storage chore(ios): update changelog chore(ios): update tests fix(ios): adjust keyboard correctly Improve assertion messages for unconstrained values. Fix test warnings by adding .self references Go with 0.31.0 instead Prep 0.30.1 Comment in new methods on result builders Update CHANGELOG Improve error messages for result builders with optionals
This should be good for a re-review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little concerned that the interactions between various functions, caches, and cache invalidations are intertwined and hard to reason about. Perhaps it was just as bad before and I just got used to it.
@@ -70,6 +70,11 @@ public struct Environment { | |||
} | |||
} | |||
|
|||
/// If the `Environment` contains any values. | |||
public var isEmpty: Bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be public. An Environment is never really "empty", because every key has a default. The presence of overrides is intentionally opaque.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, true; though in that case there's no values stored; so in the cases where this is used the default values would always be the same anyway. I guess this probably isn't useful outside of Blueprint though so I can make it internal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
private func setNeedsViewHierarchyUpdate() { | ||
guard !needsViewHierarchyUpdate else { return } | ||
|
||
invalidateIntrinsicContentSize() | ||
sizesThatFit.removeAll() | ||
|
||
if needsViewHierarchyUpdate { return } | ||
|
||
needsViewHierarchyUpdate = true | ||
|
||
/// We currently rely on CA's layout pass to actually perform a hierarchy update. | ||
/// We use `UIView`'s layout pass to actually perform a hierarchy update. | ||
/// If a manual update is required, call `layoutIfNeeded()`. | ||
setNeedsLayout() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like an unexpected set of responsibilities to me. Why would setNeedsViewHierarchyUpdate
have a cache invalidation side effect, rather than just setting a flag? It could get called a lot before the render pass actually runs. Some docs, maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had them separated before, but Kyle rightly pointed out that we called the two methods in all the same places – so not unifying to one method was less error-prone for future callsites. Maybe worth renaming this method instead? Or something...
public override func invalidateIntrinsicContentSize() { | ||
cachedIntrinsicContentSize = nil | ||
super.invalidateIntrinsicContentSize() | ||
return sizeThatFits(constraint.maximum) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the idea of intentionally round-tripping SizeConstraint.unconstrained
through .greatestFiniteMagnitude
back to SizeConstraint.unconstrained
. Can you extract out the part of measurement that takes in a constraint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure; will do; though note that sizeThatFits
already handles this case because people call sizeThatFits
with tons of weird "unconstrained" values.
@@ -36,7 +36,7 @@ public final class BlueprintView: UIView { | |||
|
|||
private let rootController: NativeViewController | |||
|
|||
private var cachedIntrinsicContentSize: CGSize? = nil | |||
private var sizesThatFit: [SizeConstraint: CGSize] = [:] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use the CacheTree
for this? Caching multiple measurements/layouts on the same elements was one of the follow-up goals from #209, and this seems like it does exactly that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could; but those are currently all short-lived because they depend on the exact element state at call time (eg, the subtrees are stored by element index); so it seemed to make more sense to introduce a top-level cache that holds "inert" measurement values. Eg, we'd still need to invalidate the CacheTree
in all the same places we do here, and keeping one around seems to potentially invite mis-use when all we really care about here is the top-level size; not digging into child sizes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'd still need to invalidate the CacheTree in all the same places we do here
Yep, that's exactly what I had in mind. The CacheTree
has an advantage in that a top-level miss can still have nested cache hits.
This is fine as-is though; I'll explore converting to CacheTree
later.
|
||
/// Measures the size needed to display the view within the given `SizeConstraint`. | ||
/// by measuring the current `element` of the `BlueprintView`. | ||
public func sizeThatFits(_ constraint: SizeConstraint) -> CGSize { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It felt reasonable to expose publicly; since the SizeConstraint
through its types (eg, Axis.unconstrained
) is clearer about how to use the provided SizeConstraint
vs the regular CGSize
magic numbers for unconstrained measurements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I like this for the same reason as the original suggestion to make it explicit. That is, for consumers who know they have a blueprint view, they can do unconstrained measurement without jumping through greatest finite magnitude weirdness.
@n8chur recently added support for caching intrinsic content size, which made me wonder: Why don't we do this everywhere? This will provide benefits for any existing impl which calls
sizeThatFits
or relies on the other autolayout methods, since we now only do an expensive re-measure when content has actually changed.Changes
sizeThatFits
now also caches results.intrinsicContentSize
is now backed bysizeThatFits
.systemLayoutSizeFitting...
overrides to call through tosizeThatFits
.One more thing!
The old impl of
intrinsicContentSize
would use an unconstrained width if we had a new element, but had not yet laid out. This seems really wrong to me, since it was very inconsistent and hard to predict, to the point of non-determinism: Had you laid out yet? Great! Your constraint is the view width! Had you not? Uhh, you have no width constraint. This is a behaviour change, and there was value to the old behaviour, so it might make sense to add an optional/mode for this. Thoughts?