WWDC 21
What's new in SwiftUI
Async images
Built-in support for loading images asynchronously:
AsyncImage(url: photo.url)
AsyncImage
provides a default placeholder, but it can also be customized:
AsyncImage(url: photo.url) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Color.blue }
Custom animations and error handling:
AsyncImage( url: photo.url, transaction: .init(animation: .spring()) ) { phase in switch phase { case .empty: ... case .success(let image): ... case .failure(let error): ... } }
Improvements to lists
Pull to refresh:
Support for pull-to-refresh in lists on iOS:
List { ... } .refreshable { await items.reload() }
Tasks:
Asynchronous task associated with a view:
.task { await items.load() }
The task is run when the view appears and is automatically cancelled when the view is removed
This can be also used to load data from an async sequence that produces items continuously for the whole duration of the view being visible
See more about new concurrency features in "Discover concurrency in SwiftUI"
Bindings to list items:
A List
can now accept a binding to an array property and will pass a binding to each specific item to the closure
This lets you use controls that require a binding (like TextField
) within list items:
List($items) { $item in Text(item.title) TextField("Item", text: $item.text) }
The previous solution for cases like this was to iterate over list indexes and bind to e.g. $list[index].text
, but this causes SwiftUI to reload the list after every change anywhere
This is actually a Swift language change, so it works in all previous versions of SwiftUI too
Also works in ForEach
and some other places
Customizing lists:
.listRowSeparatorTint(Color.blue)
β customizing the color of separators
β can be defined on the item to give each item a different separator
.listRowSeparator(.hidden)
β hide separators completely
Same for customizing separators between sections: .listSectionSeparator
, .listSectionSeparatorTint
Swipe actions:
You can now define custom swipe actions on list items:
CharacterProfile(character) .swipeActions { Button { character.isPinned.toggle() } label: { Label("Pin", systemImage: "pin") } .tint(.yellow) }
By default the action is on the right edge β use .swipeActions(edge: .leading)
to put the action on the left edge
Add multiple .swipeActions
modifiers to include swipe actions on both sides
Improvements on the Mac:
List style with alternating backgrounds, like in standard NSTableView
:
.listStyle(.inset(alternatesRowBackgrounds: true)
)
New control for full-featured multi-column tables:
Table(characters) { TableColumn("<>") { CharacterIcon($0) } .width(20) TableColumn("Villain") { Text($0.isVillain ? "Villain" : "Hero") } .width(40) TableColumn("Name", value: \.name) TableColumn("Powers", value: \.powers) }
Like a List
, a Table
presents a list of rows rendered from the given content, except a Table
has a defined list of columns
Table
supports single- and multi-row selection:
@State private var singleSelection: StoryCharacter.ID? @State private var multiSelection = Set<StoryCharacter.ID>() Table(characters, selection: $multiSelection) { ... }
You can sort the table by selected column β to support sorting in a column, provide a key-path to a model value which should be used for sorting:
@State private var sortOrder = [KeyPathComparator(\StoryCharacter.name)] Table(characters, selection: $selection, sortOrder: $sortOrder) { TableColumn("Name", value: \.name) ... }
Tables also support various visual styles and allow you to customize the appearance of each column
Integration with Core Data fetch requests:
Fetch requests now provide a binding to their sort descriptors, which lets you build a sortable multi-column table backed by a Core Data request:
@FetchRequest(sortDescriptors: [SortDescriptor(\.name)]) private var characters: FetchedResults<StoryCharacter> Table(characters, selection: $selection, sortOrder: $characters.sortDescriptors) { ... }
New @SectionedFetchRequest
which lets you build a list divided into sections from a Core Data request:
@SectionedFetchRequest( sectionIdentifier: \.isPinned, sortDescriptors: [ SortDescriptor(\.isPinned, order: .reverse), SortDescriptor(\.lastModified) ], animation: .default ) private var characterSections: SectionedFetchResults<...>
The value of the property is a list of sections, each containing a list of items:
List { ForEach(characterSections) { section in Section(section.id) ForEach(section) { character in CharacterRowView(character) } } }
Support for search in lists:
.searchable(text: $characters.filterText)
SwiftUI automatically adds a search field in the appropriate place depending on the platform, and may automatically provide suggestions based on the context
The modifier takes a binding to the entered filter text, which you should use to filter the results
See more in "Craft search experiences in SwiftUI"
Sharing data with other apps
Support for customized previews during drag & drop operation:
CharacterIcon(character) .onDrag { character.itemProvider } preview: { CharacterDragPreview(character) }
New ways to import and export data to/from your app using Item Providers
Importing items from outside:
.importsItemProviders(StoryCharacter.imageAttachmentTypes) { itemProviders in guard let firstItem = itemProviders.first else { return false } Task { selectedCharacter.imageAttachment = await StoryCharacter.loadImage(from: firstItem) } return true }
This lets you e.g. import an image taken with an iPhone's camera to a Mac app using the Continuity Camera feature
The new ImportFromDevicesCommands()
helper lets you add the necessary menu entries (File > Import from iPhone or iPad):
WindowGroup { ... } .commands { ImportFromDevicesCommands() } }
Exporting items to another app, e.g. to Shortcuts:
.exportsItemProviders(StoryCharacter.contentTypes) { if let selectedCharacter = selectedCharacter { return [selectedCharacter.itemProvider] } else { return [] } }
Graphics
SF Symbols:
Many new symbols
Two new rendering modes:
- hierarchical β uses one color like monochrome, but adds multiple levels of opacity to emphasize key elements of the symbol
- palette β gives you fine grained control over which parts of the symbol should be filled with what color
New SwiftUI colors: mint, teal, cyan, indigo, brown
Symbol variants like "filled" are now automatically chosen for you depending on the context
So if e.g. filled symbols should be used in tab bars, you can just use "person.circle"
instead of "person.circle.fill"
and the framework will choose the right variant automatically
This makes your code more reusable β e.g. you can use the same tab bar on macOS and it will use the outline versions of the same symbols there
More in "What's new in SF Symbols" and "SF Symbols in SwiftUI"
Canvas view:
A new view that gives you a rectangular canvas for free drawing, like custom NSViews/UIViews
with drawRect:
in AppKit/UIKit
You can use this to draw a specific custom element, or to draw multiple elements like a grid of small icons that don't need individual tracking and invalidation
Canvas { context, size in for symbol in symbols { let image = context.resolve(symbol.image) context.draw(image, in: symbol.rect) } }
To provide accessibility to a Canvas
element, you can use the new .accessibilityChildren
modifier:
Canvas { ... } .accessibilityChildren { List(symbols) { Text($0.name) } }
This provides a completely separate view hierarchy to accessibility interfaces which is more readable than the actual view
Timeline view:
TimelineView
is a new view that generates different content based on the time of rendering
Can be used to build e.g. an animated screen saver for tvOS
TimelineView(.animation) { let time = $0.date.timeIntervalSince1970 Canvas { context, size in // draw items differently based on the time parameter } }
The timeline takes a TimelineSchedule
parameter, which specifies when or how often it should be updated
Timeline view can also be used to provide a view for a Watch app which automatically updates when the app is in dimmed mode on always-on displays
Also in the dimmed mode on watchOS you can use the new .privacySensitive
modifier to blur out elements which can include private information and should be hidden when the Watch is not being used:
VStack(alignment: .center) { Text("Favorite Symbol") Image(systemName: favoriteSymbol) .privacySensitive(true) }
The .privacySensitive
modifier can also be used in widgets β in this case some content may be hidden (blurred) when the widget is shown on the lock screen while the device is locked
More info in "What's new in watchOS 8" and "Principles of great widgets"
Materials:
You can now create views with material backgrounds (the ones with a translucent blur effect):
VStack { Text("Symbol Browser") .font(.largeTitle) Text("\(symbols.count) symbols") .foregroundStyle(.secondary) .font(.title2) } .padding() .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16.0))
When you use semantic styles like .foregroundStyle(.secondary)
for the text, the content of a container with material background uses the appropriate "vibrancy" effect
The effect is applied to text, with the exception of emoji, which are excluded because this would render them an in incorrect way
On the Mac, system controls like sidebars and popups use the material background and now also apply the vibrancy effect to their content
More about canvas and materials in the "Add rich graphics to your SwiftUI app" talk
You can also read more about vibrancy and materials in my old blog post about the introduction of dark mode in macOS Mojave β "Dark Side of the Mac: Appearance & Materials"
Safe area inset:
There is a new view modifier .safeAreaInset
that allows you to position a view on top of a ScrollView
in a way that adjusts the scroll view's top/bottom insets so that all the content can be reached and the beginning/end isn't obscured by the overlay:
ScrollView { ... } .safeAreaInset(edge: .bottom, spacing: 0) { VStack { Text("\(symbols.count) symbols selected") } .padding() .background(.regularMaterial) }
See more in the "Add rich graphics to your SwiftUI app" talk
Preview enhancements:
New .previewInterfaceOrientation
modifier for choosing portrait/landscape orientation:
static var previews: some View { ColorList() .previewInterfaceOrientation(.vertical) ColorList() .previewInterfaceOrientation(.horizontal) }
The Attributes Inspector in the preview now includes a section for accessibility attributes, and there is a new inspector tab that shows the accessibility properties of all view elements at a glance
Text and keyboard
Markdown support:
Text
now automatically interprets Markdown formatting:
Text("**Hello**, [WWDC](https://developer.apple.com/wwdc21)!") Text("`print(helloText)`")
This is built on top of the new Swift-native AttributedString
in Foundation
It has a new rich, type-safe API for adding attributes, and even allows defining custom attributes that can be applied to text through the Markdown syntax
See more in "What's new in Foundation"
Localization:
Xcode 13 can now automatically extract strings for localization using the Swift compiler (see option in build settings)
See more in "Localize your SwiftUI app"
Dynamic Type:
You can now restrict some content to only a specified range of Dynamic Type text sizes β so that when the user picks one of the extra large sizes which would make your text too large to fit, it stays at the maximum allowed size:
Header("Today's Activities") .dynamicTypeSize(.large .. .extraExtraLarge)
Text selection:
You can enable selection of non-editable text:
Text(activity.description) .textSelection(.enabled)
This allows the user to select and copy any part of the text on macOS
On iOS this only allows copying the whole text though
New formatter APIs:
Foundation now includes new .formatted()
APIs for various values like dates that allow you to specify the format in a type-safe way:
Text(date.formatted()) Text(date.formatted(date: .omitted, time: .shortened)) Text(date.formatted( .dateTime.weekday(.wide).day().month().hour().minute()))
There's even a list formatter API like ListFormatter
:
people.map(\.nameComponents).formatted( .list(memberStyle: .name(style: .short), type: .and)) // => "Matt, Jacob, and Taylor"
Format styles can also be specified in TextField
:
@State private var newAttendee = PersonNameComponents() var body: some View { ... TextField("New Person", value: $newAttendee, format: .name(style: .medium)) ... }
The entered text is automatically validated and reformatted according to the specified format, and then parsed into a value of a correct type if possible
See more about the new format styles in "What's new in Foundation"
Text field prompts:
TextField
now includes a prompt:
parameter, which allows you to specify a placeholder value (an example text to enter) that is different from the label (which normally should specify the name or meaning of the field)
Text fields inside a Form
on macOS now display with labels outside on the left, right-aligned to the text field, with the prompt (if any) shown as the placeholder
β previously they were rendered like on iOS, with the label used as the placeholder inside the field
Text field submission:
New .onSubmit
modifier allows you to specify an action to execute when a text field is submitted by pressing a physical or virtual Return key:
TextField( "New Person", value: $newAttendee, format: .name(style: .medium) ) .onSubmit { people.append(Person(newAttendee)) }
This can also be applied to the entire form:
Form { TextField("Username", text: $viewModel.userName) SecureField("Password", text: $viewModel.password) } .onSubmit(of: .text) { guard viewModel.validate() else { return } viewModel.login() }
You can now also configure the title of the Return key on the iOS keyboard (which gives it a blue background):
TextField(...) .onSubmit { ... } .submitLabel(.done)
Keyboard accessory views:
Adding accessory views to the iOS keyboard β configure toolbar items with the placement of .keyboard
:
Form { ... } .toolbar { ToolbarItemGroup(placement: .keyboard) { Button(action: selectPreviousField) { Label("Previous", systemImage: "chevron.up") } .disabled(!hasPreviousField) Button(action: selectNextField) { Label("Next", systemImage: "chevron.down") } .disabled(!hasNextField) } }
β on macOS the buttons will be shown in the Touch Bar instead
Focus state:
New property wrapper @FocusState
that reflects the state of focus and provides precise control over it
A @FocusState
property is bound to a field's focus using .focused
The value of the property will then show if and which view is currently focused, and you can move the focus by modifying the value
Simple form: declare a boolean property, and it will be set to true when the view is focused:
@FocusState private var newPersonIsFocused: Bool var body: some View { TextField("New Person", value: $newAttendee, format: .name(style: .medium) ) .focused($newPersonIsFocused) Button { newPersonIsFocused = true } label: { Label("Add Attendee", systemImage: "plus") } }
In a more advanced version, the focus property can use any Hashable
as its type, e.g. an enum, and be used to show which of the possible fields is currently focused:
private enum Field: Int, Hashable { case name, location, date, addAttendee } @FocusState private var focusedField: Field? var body: some View { TextField("New Person", value: $newAttendee, format: .name(style: .medium) ) .focused(focusedField, equals: .addAttendee) Button { focusedField = .addAttendee } label: { Label("Add Attendee", systemImage: "plus") } }
This allows you to build any kind of complex screen with multiple focusable fields that you may want to move between programmatically
You can also use .focused
to dismiss the keyboard on iOS by unfocusing a focused field:
@FocusState private var newPersonIsFocused: Bool var body: some View { TextField(...).focused($newPersonIsFocused) } func endEditing() { newPersonIsFocused = false }
See more about the new focus API in "Direct and reflect focus in SwiftUI"
Buttons
Support for bordered buttons on iOS:
Button("Add") { ... } .buttonStyle(.bordered)
Like all modifiers, this can be added to a larger container and applies to all buttons inside
Choose a color for a bordered button by specifying a tint:
Button("Buy") { ... } .buttonStyle(.bordered) .tint(.green)
Make buttons smaller or larger by using the .controlSize
modifier, previously used only on macOS:
ForEach(entry.tag) { tag in Button(tag.name) { ... } .tint(tag.color) } .controlSize(.small)
β this only seems to work for buttons at the moment
You can also make "prominent" bordered buttons which use a style that stands out more (stronger background):
Button("Submit", action: submitForm) .buttonStyle(.borderedProminent)
βΉοΈ In the video, this is shown as .controlProminence(.increased)
. The .controlProminence
modifier was replaced in a later beta with a separate .borderedProminent
button style.
Buttons with a .large
size can be used to build a menu of action buttons at the bottom of a screen/form, with the default button marked as prominent:
VStack { Button(action: addtoJar) { Text("Add to Jar").frame(maxWidth: 300) } .buttonStyle(.borderedProminent) .keyboardShortcut(.defaultAction) Button(action: addToWatchlist) { Text("Add to Watchlist").frame(maxWidth: 300) } .tint(.accentColor) .buttonStyle(.bordered) } .controlSize(.large)
Note: do not add .borderedProminent
to every single button on the screen β this should be only used for single primary actions
β this is the equivalent of a blue-colored default button in a dialog on macOS, one that is bound to the Return key shortcut, of which by definition there can be only one within a dialog
Buttons using the new styles like .controlSize
, .borderedProminent
and .tint
automatically provide appropriate pressed states and automatically adapt to dark mode, Dynamic Type font sizes etc.
You can now specify a "role" for a button, currently .cancel
or .destructive
:
Button("Delete...", role: .destructive) { ... }
Marking a button as destructive adds a red foreground and/or background depending on the context, to emphasize that the action is potentially dangerous
Cancel and destructive buttons are often used in confirmation dialogs, which now have a new view modifier made specifically for them:
RowCell(entry) .contextMenu { Button("Delete...", role: .destructive) { showConfirmation = true } } .confirmationDialog("Are you sure you want to delete \(title)?", isPresented: $showConfirmation ) { Button("Delete", role: .destructive) { deleteEntry(entry) } } message: Text("Deleting \(title) will remove it from all of your jars.") }
A confirmation dialog is shown as an action sheet on iOS, as a popover on the iPad, and as an alert on macOS
Another type of button is a menu button (available before):
Menu("Add") { ForEach(jarStore.allJars) { jar in Button("Add to \(jar.name)") { jarStore.add(buttonEntry, to: jar) } } } .menuStyle(.borderedButton)
βΉοΈ In the video, this is shown as .menuStyle(.button)
. There is however no such menu style as .button
β use .borderedButton
on macOS and .borderlessButton
elsewhere (or just keep the default).
A bordered button menu is rendered as a button with a down arrow on the right on macOS (a pull-down menu button)
A new option for button menus (both macOS and iOS) is to provide a "primary action":
Menu("Add") { ForEach(jarStore.allJars) { jar in Button("Add to \(jar.name)") { jarStore.add(buttonEntry, to: jar) } } } primaryAction: { jarStore.add(buttonEntry) }
This makes the button act as both a normal button and a menu:
- on iOS, a normal tap runs the primary action, and a long press shows the menu
- on macOS, the button uses a slightly different style and lets you either click the main button part for the primary action, or click the arrow on the right to show the menu (without a primary action, clicking either of the parts shows the menu)
You can also choose to hide the arrow indicator of a menu button on macOS using .menuIndicator
:
Menu("Add") { ... } .menuStyle(.borderedButton) .menuIndicator(.hidden)
Such menu button looks like a completely plain button, but still acts as a menu button:
- without a primary action, it always shows the menu when pressed
- with a primary action, it runs the primary action when pressed, and to show the menu you need to do a long-press like on iOS
You can use such buttons with a hidden indicator to decrease the visual prominence of the button, e.g. if there are a lot of them in the same view
A Toggle
can now also be displayed as a toggle button:
Toggle(isOn: $showOnlyNewFilter) { Label("Show Only New", systemImage: "sparkles") } .toggleStyle(.button)
This works like a toggle button on macOS (NSButton.ButtonType.toggle
) β one that cycles between an "on" state (with a blue background) and an "off" state when pressed; it works the same way on iOS β in the "on" state it's rendered as a bordered button with a blue background
There is also a new control that groups buttons, mainly toolbar buttons β ControlGroup
:
ControlGroup { Button(action: archive) { Label("Archive", systemImage: "archiveBox") } Button(action: delete) { Label("Delete", systemImage: "trash") } }
Toolbar buttons in a ControlGroup
are displayed grouped together visually, as a kind of segmented control β as opposed to buttons in ToolbarItemGroup
, which are only arranged side by side, but not joined visually in any way
ControlGroup
with menu buttons can be used to create a pair of standard back/forward navigation buttons:
ControlGroup { Menu { ForEach(history) { ... } } label: { Label("Back", systemImage: "chevron.backward") } primaryAction: { goBack(to: history[0]) } .disabled(history.isEmpty) Menu { ForEach(forwardHistory) { ... } } label: { Label("Forward", systemImage: "chevron.forward") } primaryAction: { goForward(to: forwardHistory[0]) } .disabled(forwardHistory.isEmpty) }