`DisclosureGroup` should build its content view lazily

Originator:darren.mo
Number:rdar://FB13406169 Date Originated:2023-11-23
Status:Open Resolved:
Product:SwiftUI Product Version:iOS 17.2 Seed 3 (21C5046c)
Classification:Suggestion Reproducible:
 
`DisclosureGroup` currently builds its content view immediately. However, the content view may perform expensive operations (e.g. fetching data via SwiftData’s `@Query` macro). If there are many `DisclosureGroup` instances in a given `List`, SwiftUI would wait for all of those expensive operations to complete before showing the `List`, even though all of the `DisclosureGroup` instances are initially in a collapsed state.

Instead, `DisclosureGroup` could build its content view during the first expansion. That way, a `List` of `DisclosureGroup` instances could be loaded in a scalable manner.

Comments

Apple

We need a sample project or code snippet illustrating what you’re trying to do.

Me

I have attached a sample project that demonstrates the issue, with the help of two SwiftData models called Parent and Child. Parent has a one-to-many relationship with Child. The app is initialized with 100 parents that have 1000 children each. Pressing the Open Expensive View button causes the app to hang for a few seconds. I have also attached a Time Profiler trace to help you investigate.

ExpensiveView is a List of DisclosureGroup views representing each Parent model. The DisclosureGroup content is a ForEach of the Child models that are related to the Parent. Since the DisclosureGroup builds its content view eagerly, the children of every parent are fetched all at once when ExpensiveView appears. This approach does not scale, as demonstrated by the hang.

Instead, DisclosureGroup should build its content view lazily upon the first expansion of the group.

By darren.mo at Jan. 4, 2024, 1:29 a.m. (reply...)

Apple

Thank you for filing the feedback report. If you reduce this down to something like:

`struct ContentView: View { var body: some View { List { DisclosureGroup("Row") { ForEach(0..<1000) { r in let _ = print("Building row \(r)") Text("Row \(r)") } } } } } `

You can see that only the visible rows are requested, and no rows at all until the parent row is expanded.

A few tips here:

  1. Don’t do sorting in ForEach inline, that will be expensive.
  2. Avoid side effects in view construction, SwiftUI may need to initialize multiple views.

Does this help with your use case?

By darren.mo at Jan. 4, 2024, 1:29 a.m. (reply...)

Me

Happy New Year! Thanks for the response.

The example you gave is flawed. ForEach is indeed lazy, but the issue we are discussing is DisclosureGroup. Try the following and notice that the message is printed even when the disclosure group has not yet been expanded:

`struct ContentView: View { var body: some View { List { DisclosureGroup("Row") { let _ = print("Building DisclosureGroup content view") ForEach(0..<1000) { r in Text("Row \(r)") } } } } } `

Regarding your tips, the sorting isn’t the main cost; the main cost is accessing the children property, which causes SwiftData to synchronously load the relationship. Do you have a workaround for this SwiftData use case?

By darren.mo at Jan. 4, 2024, 1:29 a.m. (reply...)

Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!