-
Notifications
You must be signed in to change notification settings - Fork 47
feat: flexible SwiftUI preferredContentSize calc [UI-7665] #320
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
Conversation
// deduce the natural size of content that scrolls in either direction, or both, or | ||
// neither. | ||
|
||
let fixedResult = view.sizeThatFits(fixedSize) |
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.
Any idea what the perf cost is for these repeated measurements? I wonder if we could introduce our own sizing options that would allow us to do fewer calls based on more specific sizing configurations.
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.
also, is the new logic specifically for scroll views? if so, do we want to do some of these extra calculations conditionally? or does it have to account for them being embedded as descendant views?
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.
Any idea what the perf cost is for these repeated measurements
Unknown, but my suspicion is it's fine due to SwiftUI layout being much better than what we're used to.
also, is the new logic specifically for scroll views? if so, do we want to do some of these extra calculations conditionally? or does it have to account for them being embedded as descendant views?
We do need to calculate PCS regardless. The multiple measurements are to account for scroll views. Unfortunately we have to account for there being a scroll view anywhere in the content, and there's just no way to know.
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.
Unfortunately we have to account for there being a scroll view anywhere in the content, and there's just no way to know.
It'd be a science experiment, but I wonder how feasible it'd be to push the measurements onto the SwiftUI side with a custom Layout and then bubble up context like 'hasScrollView'/'hasMultilineText' via preferences so we could be slightly smarter with our measurements.
I think I'd rather spend the time submitting feedback to Apple to hopefully make this better directly out of the box with the hosting controller though!
@@ -146,21 +147,31 @@ private final class ModeledHostingController<Model, Content: View>: UIHostingCon | |||
|
|||
defer { hasLaidOutOnce = true } | |||
|
|||
if #available(iOS 16.0, *) { |
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.
was the availability change intentional?
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.
Yep. Previously we were letting UIHostingController
handle PCS on iOS 16+, except for the first layout. This if
handled the first layout case, and the else
was for ≤15. Now we're handling PCS in all cases.
let size = CGSize( | ||
width: min(fixedResult.width, unboundedHorizontalResult.width), | ||
height: min(fixedResult.height, unboundedVerticalResult.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.
any chance we need to (or should) handle weird/unexpected values here? is sizeThatFits
guaranteed to return sensical results? i guess the concern would be if we end up assigning a preferred content size that doesn't 'make sense' – would that matter?
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.
any chance we need to (or should) handle weird/unexpected values here? is
sizeThatFits
guaranteed to return sensical results? i guess the concern would be if we end up assigning a preferred content size that doesn't 'make sense' – would that matter?
It's not guaranteed to do anything by API contract, but based on observation it seems like SwiftUI will return sensible values.
} | ||
} else if swiftUIScreenSizingOptions.contains(.preferredContentSize) { | ||
let size = view.sizeThatFits(view.frame.size) | ||
if swiftUIScreenSizingOptions.contains(.preferredContentSize) { |
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.
if this is the path we want to go, shall we extract the duplicate calculation logic?
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 following the rule of 3 here, since these two don't currently share any common place to extract to. Plus we'll probably deprecate SwiftUIScreen
once ObservableScreen
is more established.
// deduce the natural size of content that scrolls in either direction, or both, or | ||
// neither. | ||
|
||
let fixedResult = view.sizeThatFits(fixedSize) |
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.
also, is the new logic specifically for scroll views? if so, do we want to do some of these extra calculations conditionally? or does it have to account for them being embedded as descendant views?
6fc3869
to
7ee2295
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.
After trying out these changes in the Market Catalog app, SwiftUI Screens size themselves much better than before 🎉. I also didn't experience performance hiccups on a physical device with dialogs and sheets. I think this is a good solution to merge in!
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.
changes all look reasonable to me!
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.
Thanks for working through this!
@@ -133,7 +134,7 @@ private final class ModeledHostingController<Model, Content: View>: UIHostingCon | |||
super.viewDidLoad() | |||
|
|||
// `UIHostingController`'s provides a system background color by default. In order to | |||
// support `ObervableModelScreen`s being composed in contexts where it is composed within another | |||
// support `SwiftUIScreen`s being composed in contexts where it is composed within another |
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.
Revert this line?
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.
Whoops. I feel like this line is self-explanatory but apparently not.
private struct TestView: View { | ||
var store: Store<StateAccessor<State>> | ||
|
||
var body: some View { | ||
WithPerceptionTracking { | ||
if store.axes.isEmpty { | ||
box | ||
} else { | ||
ScrollView(store.axes) { | ||
box | ||
} | ||
} | ||
} | ||
} |
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'd love if multiline text was also included in the test cases.
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.
Would be hard to do without making the test fragile.
// deduce the natural size of content that scrolls in either direction, or both, or | ||
// neither. | ||
|
||
let fixedResult = view.sizeThatFits(fixedSize) |
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.
Unfortunately we have to account for there being a scroll view anywhere in the content, and there's just no way to know.
It'd be a science experiment, but I wonder how feasible it'd be to push the measurements onto the SwiftUI side with a custom Layout and then bubble up context like 'hasScrollView'/'hasMultilineText' via preferences so we could be slightly smarter with our measurements.
I think I'd rather spend the time submitting feedback to Apple to hopefully make this better directly out of the box with the hosting controller though!
This is a possible solution for preferredContentSize of SwiftUI content, for discussion.
A summary of the problem:
This solution finds an appropriate PCS for the general case by measuring in a few different constraints and combining the results. This accommodates content with no scroll view, as well as content with scroll views in either axis or both.
If we like this I'll follow up with tests that validate PCS in various permutations of size and scrolling axes.tests added!Checklist