AsyncImage
New view that automatically downloads and displays images, also has placeholder, images can be customized via modifiers as usual, can have custom behaviour for error handling.
AsyncImage(url: ...) { image in
image
.resizable()
.aspectRation(contentMode: .fill)
}task(_:) concurrency view modifier
task(_:) lets you attach an async task to the lifetime of your view: it will be triggered when its view appears, and will be cancelled when this view disappears.
Text(displayValue)
.task {
var results = TextProcessResults()
for try await line in textURL.lines() {
results.accumulateResults(line: line)
}
displayValue = results.textSummary()
}Lists & Grids
Pull to refresh
Pull to refresh via refreshable(action:) concurrency view modifier, this modifier configures a refresh action (RefreshAction) and passes down through the environment.
Use an await expression inside the action. SwiftUI shows a refresh indicator, which stays visible for the duration of the awaited operation.
List(mailbox.conversations) {
ConversationCell($0)
}
.refreshable {
await mailbox.fetch()
}Binding
New List and ForEach initializers allowing us to get a binding per each element:
struct DirectionsList: View {
@Binding var directions: [Direction]
var body: some View {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
}
}This is back-ported all the way to iOS 13.
Separator Customization
listRowSeparatorTint(_:edges:)- custom row separator colorslistSectionSeparatorTint(_:edges:)- custom section separator colorslistRowSeparator(_:edges:)- can be used to hide the separators altogether
Swipe Actions
New swipeActions(edge:allowsFullSwipe:content:) view modifier to add swipe actions.
Define each action with Buttons, use the tint(_:) view modifier to customize the background color (or use the button’s role).
List(store.messages) { message in
MessageCell(message: message)
.swipeActions(edge: .leading) {
Button { store.toggleUnread(message) } label: {
if message.isUnread {
Label("Read", systemImage: "envelope.open")
} else {
Label("Unread", systemImage: "envelope.badge")
}
}
.tint(.yellow)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
store.delete(message)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.blue)
Button { store.flag(message) } label: {
Label("Flag", systemImage: "flag")
}
.tint(.green)
}
}
}Style Updates
All styles now come with a new enum-like syntax:
List {
...
}
.listStyle(.grouped)instead of:
List {
...
}
.listStyle(GroupedListStyle())New (macOS-only) style, which alternates the colors of the rows:
List {
...
}
.listStyle(bordered(alternatesRowBackgrounds: true))Table (macOS-only)
New Table view, supports selection, sorting, and more:
struct ContentView: View {
@State private var characters = StoryCharacter.previewData
var body: some View {
Table(characters) {
TableColumn("") { CharacterIcon($0) }
.width(20)
TableColumn("Villain") { Text($0.isVillain ? "Villain" : "Hero") }
.width(40)
TableColumn("Name", value: \.name)
TableColumn("Powers", value: \.powers)
}
}
}Search
New searchable(_:text:placement:) view modifiers, it adds a search field where more appropriate based on the context:
NavigationView {
List {
...
}
.searchable(...)
}Sharing data
onDragnow comes with apreviewViewparameter, letting us customize what view to show when dragging.new
importsItemProvidersview modifier makes a view a drop target that accepts item providersnew
exportsItemProvidersview modifier exposes our app data to external system services
SF Symbols
Two new rendering modes:
Hierarchical - like monochrome, but automatically adds multiple levels of opacity to really emphasize the key elements of the symbol
Palette - gives more fine-grained control over individual layers color with custom fills
SwiftUI automatically chooses the correct symbol variant to use based on the context, for example a symbol used in the tabbar will use the
.fillvariant.
Canvas
New view allowing immediate-mode drawing similar to drawRect from UIKit or AppKit:
Canvas { context, size in
let metrics = gridMetrics(in: size)
for (index, symbol) in symbols.enumerated() {
let rect = metrics[index]
let (sRect, opacity) = rect.fishEyeTransform(around: focalPoint)
context.opacity = opacity
let image = context.resolve(symbol.image)
context.draw(image, in: sRect.fit(image.size))
}
}We can use TimelineView to make our canvas update over time.
Displaying sensitive data
New modifiers that automatically redact sensitive data when the user is no longer authenticated (for when the phone is locked or similar)
Image(systemName: favoriteSymbol)
.font(.title2)
.privacySensitive(true)Material (blur)
New blur/vibrancy effects:
struct ColorList: View {
var body: some View {
ZStack {
...
materialOverlay
}
}
var materialOverlay: some View {
VStack {
Text("Symbol Browser")
.font(.largeTitle.bold())
Text("\(symbols.count) symbols 🎉")
.foregroundStyle(.secondary)
.font(.title2.bold())
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16.0))
}
}Preview
We can now preview screens in different orientations:
struct ColorList_Previews: PreviewProvider {
static var previews: some View {
ColorList()
.previewInterfaceOrientation(.portrait)
ColorList()
.previewInterfaceOrientation(.landscapeLeft)
}
}Text
markdown support
restrict dynamic type size of a text/view via new dynamicTypeSize(_:) view modifier
make text selectable or not via textSelection(_:) view modifier (macOS only)
new powerful formatters
TextFields
support for prompts, separate from its label, to let users know what kind of content a field is expecting. In macOS, the prompt will be used as the placeholder text.
onSubmit(_:)view modifier to detect when the user submits the text (this replaces the previousTextField’sonCommitparametersubmitLabel(_:)view modifier to customize the return key action, and to help give users a hint of what kind of action will occur when submitting a field
struct ContentView: View {
@State private var activity: Activity = .sample
@State private var newAttendee = PersonNameComponents()
var body: some View {
TextField("New Person", value: $newAttendee,
format: .name(style: .medium)
)
.onSubmit {
activity.append(Person(newAttendee))
newAttendee = PersonNameComponents()
}
.submitLabel(.done)
}
}keyboard toolbar support via the usual
toolbar(_:)view modifier with new.keyboardplacement
struct ContentView: View {
@State private var activity: Activity = .sample
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Name", text: $activity.name, prompt: Text("New Activity"))
TextField("Location", text: $activity.location)
DatePicker("Date", selection: $activity.date)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button(action: selectPreviousField) {
Label("Previous", systemImage: "chevron.up")
}
.disabled(!hasPreviousField)
Button(action: selectNextField) {
Label("Next", systemImage: "chevron.down")
}
.disabled(!hasNextField)
}
}
}
private func selectPreviousField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue - 1)!
}
}
private var hasPreviousField: Bool {
if let currentFocusedField = focusedField {
return currentFocusedField.rawValue > 0
} else {
return false
}
}
private func selectNextField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue + 1)!
}
}
private var hasNextField: Bool {
if let currentFocusedField = focusedField {
return currentFocusedField.rawValue < Field.allCases.count
} else {
return false
}
}
}textfield focus control via
@FocusStateproperty wrapper:
struct ContentView: View {
@State private var activity: Activity = .sample
@State private var newAttendee = PersonNameComponents()
@FocusState private var addAttendeeIsFocused: Bool = false
var body: some View {
VStack(alignment: .leading) {
TextField("New Person", value: $newAttendee, format: .name(style: .medium))
.focused($addAttendeeIsFocused)
ControlGroup {
Button {
addAttendeeIsFocused = true
} label: {
Label("Add Attendee", systemImage: "plus")
}
}
}
}
}Buttons
New bordered style (
Button("Add") {}.buttonStyle(.bordered)), which supports tinting via the.tintview modifiernew
controlSize(_:)view modifier for different buttons appearancesnew
controlProminence(_:)to highlight importance of each button
struct ContentView: View {
var body: some View {
VStack {
Button(action: addToJar) {
Text("Add to Jar").frame(maxWidth: 300)
}
.controlProminence(.increased)
.keyboardShortcut(.defaultAction)
Button(action: addToWatchlist) {
Text("Add to Watchlist").frame(maxWidth: 300)
}
.tint(.accentColor)
}
.buttonStyle(.bordered)
.controlSize(.large)
}
private func addToJar() {}
private func addToWatchlist() {}
}New Button roles to give each button additional semantics, which SwiftUI uses to display the button accordingly:
struct ContentView: View {
var entry: ButtonEntry = .sample
var body: some View {
ButtonEntryCell(entry)
.contextMenu {
Section {
Button("Open") {
// ...
}
// This button will have red tint as it's destructive
Button("Delete...", role: .destructive) {
// ...
}
}
}
}Buttons confirmation dialogs via
confirmationDialogview modifier:
struct ContentView: View {
var entry: ButtonEntry = .sample
@State private var showConfirmation: Bool = false
var body: some View {
ButtonEntryCell(entry)
.contextMenu {
Section {
Button("Open") {
// ...
}
Button("Delete...", role: .destructive) {
showConfirmation = true
// ...
}
}
}
.confirmationDialog(
"Are you sure you want to delete \(entry.name)?",
isPresented: $showConfirmation
) {
Button("Delete", role: .destructive) {
// delete the entry
}
} message: {
Text("Deleting \(entry.name) will remove it from all of your jars.")
}
}
}Menus
More flexibility and new modifiers to control primary and secondary actions:
struct ContentView: View {
var buttonEntry: ButtonEntry = .sample
@StateObject private var jarStore = JarStore()
var body: some View {
Menu("Add") {
ForEach(jarStore.allJars) { jar in
Button("Add to \(jar.name)") {
jarStore.add(buttonEntry, to: jar)
}
}
} primaryAction: {
jarStore.addToDefaultJar(buttonEntry)
}
.menuStyle(BorderedButtonMenuStyle())
.
}
}ControlGroup
New view used to gather controls together (the system will display the controls at the right place with correct spacing etc):
ControlGroup {
Button(action: archive) {
Label("Archive", systemImage: "archiveBox")
}
Button(action: delete) {
Label("Delete", systemName: "trash")
}
}