WWDC Quick Look 💓 By SwiftGGTeam
Stacks, Grids, and Outlines in SwiftUI

Stacks, Grids, and Outlines in SwiftUI

Watch original video

Highlight

SwiftUI 2020 adds LazyVStack, LazyHStack, LazyVGrid, LazyHGrid, List(children:), OutlineGroup and DisclosureGroup, 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:

  • ScrollView is still responsible for scrolling; lazy stack itself does not provide scrolling capabilities.
  • LazyVStack(spacing: 0) replaces the original VStack(spacing: 0), which is the entry point for performance changes.
  • ForEach(sandwiches) maintains data-driven writing, and each model corresponds to a HeroView.
  • session clearly recommends: use VStack or HStack first 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.
  • ForEach and HeroView remain unchanged, the layout container takes over the change from list to grid.
  • transcript specifically mentions that .adaptive is 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:

  • graphics is the root node array of the tree.
  • children: \.children tells List where the child nodes of each Graphic are.
  • GraphicRow(graphic) is still only responsible for single-row display, and hierarchical expansion is handled by List.
  • 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 List continues 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:

  • Form is 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 to areFillControlsShowing.
  • Toggle and ColorRow are 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 ScrollView and LazyVStack for the first version. At the beginning, keep the existing cell view, replace only the outer VStack, 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 LazyVGrid plus GridItem(.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 use List(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, Section and OutlineGroup. 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 with isExpanded binding, and other groups remain collapsed, suitable for property panels, settings pages and editor sidebars.

Comments

GitHub Issues · utterances