WWDC Quick Look 💓 By SwiftGGTeam
The craft of SwiftUI API design: Progressive disclosure

The craft of SwiftUI API design: Progressive disclosure

Watch original video

Highlight

Sam Lazarus from the SwiftUI team comes at it from a clever angle: the macOS save dialog. Simple mode only displays file names and a few commonly used locations, and expands to become a complete file browser. Requirements of different levels of complexity see UIs of different levels of complexity - this is Progressive Disclosure.


Core Content

This session talks about how the SwiftUI team designs APIs.

The definition given by Sam Lazarus is very straightforward: Progressive Disclosure is to let the complexity of the call site increase with the complexity of the usage scenario (02:19). For simple scenarios, only write necessary information. Complex scenes open up more capabilities.

This is similar to the macOS save dialog. The default state has given the save location and common locations; when needing to browse the file system, the user can expand the complete interface (01:14). SwiftUI wants to put this kind of experience into the API: it’s quick to use for the first time, and there are no restrictions on subsequent expansion.

Sam emphasized that any reusable component or abstraction is an API. People who write components usually look at the declaration site, but the user experience occurs at the call site (02:00). This is also the main thread of this field: deducing the API shape from the call point.

Detailed Content

Start with common use cases

(04:18) SwiftUIButtonis the first example. Buttons must have labels. Most button labels are just text, so the most common way to write them is short.

Button("Next Page") {
    currentPage += 1
}

Key points:

  • Button("Next Page")Put the most common text labels into the first-level API.
  • Only button actions are retained in the closure, which here is to increase the current page by one.
  • The call point does not appearTextHStackor extra parameters since this scenario doesn’t require them.

When the label needs to be customized, SwiftUI will provide a full version (04:36).

Button {
    currentPage += 1
} label: {
    Text("Next Page")
}

Key points:

  • The first closure is still the button action. -labelThe closure begins to expose the label’s view structure. -Text("Next Page")Allows the caller to continue to use normal SwiftUI view capabilities.

More complex labels continue to use the same entry (04:43).

Button {
    currentPage += 1
} label: {
    HStack {
        Text("Next Page")
        NextPagePreview()
    }
}

Key points:

  • HStackCombine the text and preview view into a label. -Text("Next Page")Still assumes a readable name. -NextPagePreview()is additional custom content that only appears when needed.

This is the first strategy of progressive disclosure: identify the most common use cases first, and then provide a concise entry point for them. Complex capabilities remain in the next-level API.

Set smart defaults for common scenarios

05:30Textis an example of smart defaults.

Text("Hello WWDC22!")

Key points:

  • The call site writes only what is to be displayed.
  • SwiftUI will use the locale in the environment to find the localized string in the app bundle.
  • Text will automatically adapt to the current color scheme, supporting dark mode.
  • Text automatically scales based on the current accessibility Dynamic Type dimensions.

These behaviors can be specified manually. But in ordinary scenarios, they should not occupy the call point.

TextThere is one more detail. twoTextWhen placed on the stack, the spacing between text will automatically adjust to the correct line spacing for the current context (06:12).

VStack {
    Text("Hello WWDC22!")
    Text("Call to Code.")
}

Key points:

  • VStackOnly expresses the vertical arrangement of two texts.
  • eachTextStill just declare the content.
  • Typography details like line spacing are handled by SwiftUI defaults.

Toolbar also uses the same idea. When no position is specified, SwiftUI will place the button according to platform conventions: macOS places the button on the toolbar leading edge, iOS places it on the navigation bar trailing edge, and watchOS only displays the first item and fixes it below the navigation bar (06:43). When you need to control the position, use placementToolbarItemGroup07:20)。

Optimization call point: four contractions of Table

08:09TableThis is the most complete example in this field. Multi-column tables can support capabilities such as sorting, rich content cells, grouping rows, etc. Complete writing is very powerful, but ordinary forms should not write the complete structure every time.

The complex version looks like this:

@State var sortOrder = [KeyPathComparator(\Book.title)]

var body: some View {
    Table(sortOrder: $sortOrder) {
        TableColumn("Title", value: \Book.title) { book in
            Text(book.title).bold()
        }
        TableColumn("Author", value: \Book.author) { book in
            Text(book.author).italic()
        }
    } rows: {
        Section("Favorites") {
            ForEach(favorites) { book in
                TableRow(book)
            }
        }
        Section("Currently Reading") {
            ForEach(currentlyReading) { book in
                TableRow(book)
            }
        }
    }
    .onChange(of: sortOrder) { newValue in
        favorites.sort(using: newValue)
        currentlyReading.sort(using: newValue)
    }
}

Key points:

  • sortOrderSave the current sorting rules. -Table(sortOrder:)Allows the table to change sorting state based on column header clicks. -TableColumnDeclare column names, value key paths and cell views. -SectionDivide rows into different regions. -ForEachConvert each book in the collection toTableRow
  • .onChange(of: sortOrder)Reorder data sources when sorting changes.

This version is suitable for complex forms. Next, SwiftUI gradually removes information that is not needed in ordinary scenarios.

In the first step, ordinary rows are often just done on collectionsForEach, and then generate for each elementTableRow. SwiftUI allows collections to be passed directly toTable09:58)。

@State var sortOrder = [KeyPathComparator(\Book.title)]

var body: some View {
    Table(currentlyReading, sortOrder: $sortOrder) {
        TableColumn("Title", value: \.title) { book in
            Text(book.title)
        }
        TableColumn("Author", value: \.author) { book in
            Text(book.author)
        }
    }
    .onChange(of: sortOrder) { newValue in
        currentlyReading.sort(using: newValue)
    }
}

Key points:

  • Table(currentlyReading, sortOrder:)Hand the data collection to the table.
  • Call points are no longer handwrittenForEachandTableRow
  • \.title\.authorUse a relative key path because the collection element type is already represented bycurrentlyReadingroll out.
  • Sorting logic is still retained as this version also supports sorting.

In the second step, if the value key path of the column points to a string, the most common display method isText. SwiftUI allows cell views to be omitted (10:23).

@State var sortOrder = [KeyPathComparator(\Book.title)]

var body: some View {
    Table(currentlyReading, sortOrder: $sortOrder) {
        TableColumn("Title", value: \.title)
        TableColumn("Author", value: \.author)
    }
    .onChange(of: sortOrder) { newValue in
        currentlyReading.sort(using: newValue)
    }
}

Key points:

  • TableColumn("Title", value: \.title)It is enough to generate the title column. -TableColumn("Author", value: \.author)The author column is generated in the same way.
  • in the cellText(book.title)andText(book.author)Overridden by default behavior.
  • Sort bindings and sort callbacks still appear at the call site.

In the third step, the simplest table does not care about sorting. SwiftUI now provides a version without sort order (10:51).

var body: some View {
    Table(currentlyReading) {
        TableColumn("Title", value: \.title)
        TableColumn("Author", value: \.author)
    }
}

Key points:

  • Table(currentlyReading)Represents the data source.
  • twoTableColumnIndicates columns that must be displayed.
  • NosortOrder, since sorting is not required for this use case.
  • There are no rows closure and cell closure, because the default behavior is enough to express this table.

Sam summarized two inspection questions: which common use cases are worthy of convenience; which information is essential information that must always be provided (11:03). The final form of Table is the result of these two questions.

Use combination instead of enumerating all possibilities

(11:37) The last strategy is Compose, don’t enumerate.

There are two types of core information in HStack: what the content is, and how the content is arranged. The content has been expressed by the view builder. If the arrangement is designed with enum, it will quickly get out of control: after leading, center, and trailing, there will also be even distribution, only leaving space between elements, only leaving space before the last element, etc. (12:08).

Instead of adding enum cases for each permutation, SwiftUI providesSpacer, letting the caller assemble the permutation.

struct StackExample: View {
    var body: some View {
        HStack { // centered
            Spacer()
            Box().tint(.red)
            Box().tint(.green)
            Box().tint(.blue)
            Spacer()
        }
    }
}

Key points:

  • HStackStill only responsible for arranging content horizontally.
  • firstSpacer()Push the entire box toward the middle.
  • threeBox().tint(...)is the actual content.
  • finalSpacer()Combined with the first spacer, it creates a centered effect.

Uniform distribution does not require new APIs (13:42).

struct StackExample: View {
    var body: some View {
        HStack { // evenly spaced
            Spacer()
            Box().tint(.red)
            Spacer()
            Box().tint(.green)
            Spacer()
            Box().tint(.blue)
            Spacer()
        }
    }
}

Key points:

  • eachSpacer()They are all composable layout elements.
  • When spacers are inserted between content, whitespace is spread across multiple locations.
  • The API has not grown, but the expressive capabilities have expanded.

The same goes for just leaving space between elements (13:43).

struct StackExample: View {
    var body: some View {
        HStack { // space only between elements
            Box().tint(.red)
            Spacer()
            Box().tint(.green)
            Spacer()
            Box().tint(.blue)
        }
    }
}

Key points:

  • Box().tint(.red)Box().tint(.green)Box().tint(.blue)are three content views.
  • twoSpacer()Appears only between content.
  • There are no spacers at the beginning and end, so whitespace will not be placed at either end.

This part of the reminder is very practical: don’t misunderstand “common situations” as “write all common situations as enumerations”. If the possibilities continue to grow, it should be broken into small composable units (13:20).


Core Takeaways

  • What to do: Write a shortest call entry for the SwiftUI component within the team. Why it’s worth doing: Session repeatedly emphasizes that the complexity of the call point must match the usage scenario. Common scenarios should be run first. How ​​to start: Find out how 80% of the components are called, add an initializer to it that contains only the necessary parameters, and leave the advanced configuration to the full initializer.

  • What: Set smart default values ​​for custom view parameters. Why it’s worth doing:Textand toolbar examples illustrate that defaults can move details like localization, color schemes, and platform conventions away from ordinary call points. How ​​to start: Check whether each parameter must be written every time; for parameters that can be omitted, change them to default parameters or fromEnvironmentread in.

  • What: Override the call site of a table or list component. Why it’s worth doing: The Table example shows the shrinking process from full capabilities to simple entry. How ​​to start: Keep the complete version first, and then add convenience APIs for collection input, string columns, and unsorted scenarios.

  • What: Split the enum configuration into composable views or modifiers. Why it’s worth doing: HStack does not enumerate all arrangements, but usesSpacerLet developers compose layouts. How ​​to start: If an enum keeps adding cases, first ask whether these cases can be combined from smaller structures.

  • What to do: Write examples of calls before writing the API. Why it’s worth doing: The core perspective of this field is the call site, and the priority is to check whether the API is smooth from the point of use. How ​​to start: First write three pieces of target calling code: the most common, the slightly more complex, and the most advanced. The type, initializer, and default value are then deduced based on these call points.


Comments

GitHub Issues · utterances