Highlight
SwiftUI 2020 adds
LazyVStack,LazyHStack,LazyVGrid,LazyHGrid,List(children:),OutlineGroupandDisclosureGroup, so that long lists, grid layouts, hierarchical data and inspector controls can be directly implemented using composite views.
Core Content
Cody started by talking about a sandwich gallery. The initial implementation is very natural: put HeroView into ForEach, cover it with a layer of VStack, and then use ScrollView to be responsible for scrolling. There is no problem when the data is small; when the number of photos increases, all pictures must be created and measured first when the page is opened, the first screen response becomes slower, and the memory also increases.
The answer for SwiftUI 2020 is lazy stack. After the outer scroll container is replaced with LazyVStack, only the visible sandwich card will be rendered on the first screen, and the subsequent content will be created after the user scrolls nearby. The internal scoring HStack and image overlay do not need to be changed to lazy, because when a HeroView appears on the screen, the content inside is originally displayed at the same time.
Next, the same gallery was moved to the iPad. Single-column images waste space on large screens, so session introduces LazyVGrid and LazyHGrid. Developers use GridItem to describe the size of columns or rows, either by fixing three columns or by using .adaptive(minimum:) to let the number of columns vary with the available width.
In the second half, I switched to ShapeEdit, a document-based app that runs on macOS, iPadOS, and iOS. It only needs to display a flat list of graphics on the canvas at first; after adding “graphic groups can contain graphic groups”, the data becomes a tree of arbitrary depth. List(children:) can expand the tree directly along the children key path, and OutlineGroup puts the same capability into a custom list structure.
Finally there is the Inspector panel. Fill, shadow, text and other controls are too crowded when they are all spread out. DisclosureGroup allows each group of controls to have a title, an expanded state, and a content area. With Label and Boolean binding, you can also set the default expanded group. This extends SwiftUI’s layout story from “arranging views” to “progressively displaying information”.
Detailed Content
Lazy stack: scrolling content is only created when visible
(03:27) Normal VStack will enumerate all sandwiches and create corresponding views when ScrollView appears. The question in the transcript is very specific: the more photos there are, the longer the page will take to respond. Apple has added LazyVStack and LazyHStack to allow content in the scroll direction to be rendered on demand.
// Fetch sandwiches from the sandwich store
let sandwiches: [Sandwich] = …
ScrollView {
LazyVStack(spacing: 0) {
ForEach(sandwiches) { sandwich in
HeroView(sandwich: sandwich)
}
}
}
Key points:
ScrollViewis still responsible for scrolling; lazy stack itself does not provide scrolling capabilities.LazyVStack(spacing: 0)replaces the originalVStack(spacing: 0), which is the entry point for performance changes.ForEach(sandwiches)maintains data-driven writing, and each model corresponds to aHeroView.- session clearly recommends: use
VStackorHStackfirst when in doubt, use Instruments to find the performance bottleneck, and then use lazy stack.
Lazy grid: Increase content density on large screens
(05:48) After the sandwich gallery is placed on the iPad, the single-column layout only makes the images larger, without increasing the amount of content on the screen. LazyVGrid and LazyHGrid use GridItem to describe the grid size, so that the same batch of data can be arranged in columns or rows.
// Fetch sandwiches from the sandwich store
let sandwiches: [Sandwich] = …
// Define grid columns
var columns = [
GridItem(.adaptive(minimum: 300), spacing: 0)
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(sandwiches) { sandwich in
HeroView(sandwich: sandwich)
}
}
}
Key points:
GridItem(.adaptive(minimum: 300), spacing: 0)requires each column to be at least 300 points wide.LazyVGrid(columns: columns, spacing: 0)reads the column configuration and generates as many equal-width columns as available.ForEachandHeroViewremain unchanged, the layout container takes over the change from list to grid.- transcript specifically mentions that
.adaptiveis suitable for landscape iPads and macOS with resizable windows.
List children: Turn a flat list into a hierarchical list
(08:41) ShapeEdit’s sidebar was originally just a list of canvas graphics. The graphics group can continue to contain subgraphics after the list has to express the tree structure. SwiftUI adds children key path to List, using a parameter to tell the list how to traverse the data tree.
struct GraphicsList: View {
var graphics: [Graphic]
var body: some View {
List(
graphics,
children: \.children
) { graphic in
GraphicRow(graphic)
}
.listStyle(SidebarListStyle())
}
}
Key points:
graphicsis the root node array of the tree.children: \.childrentellsListwhere the child nodes of eachGraphicare.GraphicRow(graphic)is still only responsible for single-row display, and hierarchical expansion is handled byList.SidebarListStyle()brings sidebar style; the transcript mentions that bold headers in iOS 14 also come from list style improvements in this direction.
OutlineGroup: Reuse tree traversal in custom structures
(09:52) List(children:) is suitable for situations where the entire list is a tree. ShapeEdit requires one section for each canvas, and a graphics tree inside each section, so OutlineGroup is used instead.
// Customizing your outlines
List {
ForEach(canvases) { canvas in
Section(header: Text(canvas.name)) {
OutlineGroup(canvas.graphics, children: \.children)
{ graphic in
GraphicRow(graphic)
}
}
}
}
Key points:
- The outer
Listcontinues to be responsible for selection, scrolling and list interaction. ForEach(canvases)creates a partition for each canvas.Section(header: Text(canvas.name))turns the canvas name into the section title.OutlineGroup(canvas.graphics, children: \.children)traverses the tree graphics data inside each partition.
DisclosureGroup: Controls the expansion of non-tree information
(13:10) The fill, shadow, and text controls in the inspector do not belong to the rule tree structure, but they also need to be collapsed. DisclosureGroup provides expansion indicator, label and content; if boolean binding is passed in, the default expansion can also be controlled by state.
// Progressive display of information
Form {
DisclosureGroup(isExpanded: $areFillControlsShowing) {
Toggle("Fill shape?", isOn: isFilled)
ColorRow("Fill color", color: fillColor)
} label: {
Label("Fill", …)
}
…
}
Key points:
Formis suitable for hosting settings and a set of controls. The session also mentioned that it can be used for Settings Scenes of macOS.DisclosureGroup(isExpanded:)hands expanded state toareFillControlsShowing.ToggleandColorRoware the actual controls that are collapsed or expanded.Label("Fill", …)combines the title and icon. The SF Symbol icon is used in transcript.
Core Takeaways
-
Long content gallery: To make a photo, menu or product gallery, you can first use
ScrollViewandLazyVStackfor the first version. At the beginning, keep the existing cell view, replace only the outerVStack, and then use Instruments to check the first screen response and memory. -
Adaptive iPad Grid: When moving the single column content on the phone to the iPad or macOS window, use
LazyVGridplusGridItem(.adaptive(minimum: 300)). Start with a minimum column width to avoid writing multiple layouts for horizontal, vertical, and window widths. -
Document sidebar tree: If the App has folders, layers, canvas objects or task groups, you can let the model expose
children, and then useList(items, children: \.children)to make an expandable sidebar. The single row view is kept simple and tree traversal is left to SwiftUI. -
Partitioned Outline Editor: When the data has multiple root collections, such as multiple canvases, multiple projects, or multiple accounts, use a combination of
List,ForEach,SectionandOutlineGroup. This way each partition has its own title, and the tree can still be expanded inside each partition. -
Compact Inspector Panel: Put less commonly used form controls into
DisclosureGroup. Commonly used groups are expanded by default withisExpandedbinding, and other groups remain collapsed, suitable for property panels, settings pages and editor sidebars.
Related Sessions
- What’s new in SwiftUI — SwiftUI 2020 overview, including outlines, grids and controls expanded by this session.
- App essentials in SwiftUI — transcript The follow-up session named at the end, continuing to talk about app, scene and Settings Scenes.
- Data Essentials in SwiftUI — transcript A follow-up session named at the end, talking about how model data drives view updates.
- Build document-based apps in SwiftUI — ShapeEdit is a document-based App scenario. This session complements the DocumentGroup and document management foundation.
Comments
GitHub Issues · utterances