Presenters:
- Nick Teissler, SwiftUI Engineer
- Franck Ndame Mpouli, SwiftUI Engineer
- Introduce you to Swift Charts
- Show off SwiftUI's data-driven, strongly-typed model for navigation and new window techniques
- Suite of new controls and deeper customization of existing controls
- Transferable protocol
- New graphics APIs and advanced layout APIs
Declarative framework for building beautiful, state-driven charts.
- Different types of charts, as easy as changing from BarMark to LineMark
- Can add modifiers to do things like assign colors, add points
- Can use SwiftUI Views within a chart
- Swift Charts handles localization, Dark Mode, and Dynamic Type automatically
- Works across all platforms
SwiftUI already supports the most common navigation patterns, such as:
- Push-and-pop navigation stacks
- Expansive, detail-rich split views
- Powerful multi-window experiences
Updates for all three patterns this year
New container view this year: NavigationStack
- supports push-and-pop style navigation
NavigationStack {
List(foodItems) { item in
NavigationLink {
FoodDetailView(item: item)
} label: {
FoodRow(food: item)
}
}
.navigationTitle("Party Food")
}
Data-driven NavigationStack:
NavigationStack {
List(foodItems) { item in
NavigationLink(value: item) {
Label(item.title, image: item.iconName)
}
}
.navigationTitle("Party Food")
.navigationDestination(for: FoodItem.self) { item in
FoodDetailView(item: item)
}
}
With data-driven NavigationStack
you can use state to track navigation:
// NavigationPath
var foodItems: [FoodItem]
@State private var selectedFoodItems: [FoodItem] = []
var body: some View {
NavigationStack(path: $selectedFodItems) {
List(foodItems) { item in
NavigationLink(value: item) {
Label(item.title, image: item.iconName)
}
}
.navigationTitle("Party Food")
.navigationDestination(for: FoodItem.self) { item in
FoodDetailView(item: item, path: $selectedFoodItems)
}
}
}
With this, we can easily add a button to return to the first item:
// FoodDetailView.body
Button("Back to First Item") {
// Remove all but first element
selectedFodItems.removeSubrange(1...)
}
NavigationSplitView is for multicolumn navigation. NavigationSplitView
can declare two- and three-column layouts
NavigationSplitView {
List(PartyTask.allCases, selection: $selectedTask) { task in
NavigationLink(value: $0) {
TaskLabel(task: $0)
}
.listItemTint(task.color)
}
} detail: {
switch selectedTask {
case .food:
FoodOverview()
case .music:
MusicOverview()
}
}
NavigationSplitView {
List(PartyTask.allCases, selection: $selectedTask) { task in
NavigationLink(value: task) {
TaskLabel(task: task)
}
.listItemTint(task.color)
}
} detail: {
selectedTask.flatMap { $0.color } ?? .white
}
New Window API - can open from menu or add keyboard shortcuts to open
@main
struct PartyPlanner: App {
var body: some Scene {
WindowGroup("Party Planner") {
PartyPlannerHome()
}
Window("Party Budget", id: "budget") {
Text("Budget View")
}
.keyboardShortcut("0")
}
}
Toolbar button with action that shows the window:
struct DetailView: View {
@Environment(\.openWindow) var openWindow
var body: some View {
Text("Detail View")
.toolbar {
Button {
openWindow(id: "budget")
} label: {
Image(systemName: "dollarsign")
}
}
}
}
Can programmatically open new windows, and there is a whole suite of new window customization options.
For a multi-platform app, can use resizable sheets:
struct PartyPlannerHome: View {
@State private var selectedTask: PartyTask?
@State private var presented: Bool = false
var body: some View {
NavigationSplitView {
List(PartyTask.allCases, selection: $selectedTask) { task in
NavigationLink(value: task) {
TaskLabel(task: task)
}
.listItemTint(task.color)
}
} detail: {
if case .food = selectedTask {
FoodsListView()
} else {
selectedTask.flatMap { $0.color } ?? .white
}
}
.sheet(isPresented: $presented) {
Text("Budget View")
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
}
}
@main
struct PartyPlanner: App {
var body: some Scene {
Window("Party Budget", id: "budget") {
Text("Budget View")
}
MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
BulletinBoard()
}
.menuBarExtraStyle(.window)
}
}
Settings interfaces are control heavy. New settings designed to present controls in a consistent and well-organized fashion. Different types of controls grouped into sections. .grouped
formStyle.
Form {
Section {
LabeledContent("Location", value: address)
DatePicker("Date", selection: $date)
}
Section("Vibe") {
Picker("Accent color", selection: $accent) { ... }
Picker("Color scheme", selection: $scheme) { ... }
Toggle(isOn: $extraGuests) {
Text("Allow extra guests")
Text("The more the merrier!")
}
}
}
.formStyle(.grouped)
Controls consistently align labels, content grouped under headings.
Can display content, and allow selecting the text. Can also wrap any kind of view.
Section {
LabeledContent("Location") {
AddressView(location)
}
DatePicker("Date", selection: $date)
}
Other new control features:
- New
.axis
parameter for TextFields to expand them vertically .lineLimit
modifier supports more advanced behaviorsMultiDatePicker
control - supports non-contiguous date selection- Mixed-state controls
- Disclosure group with toggles; each inner toggle has an individual binding, while the aggregate toggle takes a collection
- Displays a mixed state if the values don't all match
DisclosureGroup {
Toggle("Balloons", isOn: $includeBalloons)
Toggle("Confetti", isOn: $includeConfetti)
Toggle("Inflatables", isOn: $includeInflatables)
Toggle("Party Horns", isOn: $includePartyHorns)
} label: {
Toggle("All Decorations", isOn: [
$includeBalloons,
$includeConfetti,
$includeInflatables,
$includePartyHorns
])
}
- Button-style toggles
- Steppers
- Accessibility quick actions
Tables now supported on iPad OS. Uses same Tables API introduced last year.
@State private var attendees: [Attendee]
var body: some View {
Table(attendees) {
TableColumn("Name") { attendee in
AttendeeRow(attendee)
}
TableColumn("City", value: \.city)
TableColumn("Status") { attendee in
StatusRow(attendee)
}
}
}
There is a new selection-based contentMenu
modifier:
@State private var attendees: [Attendee]
@State private var selection: Set<Attendee.ID>
var body: some View {
Table(attendees, selection: $selection) {
...
}
.contextMenu(forSelectionType: Attendee.ID.self) { selection in
if selection.isEmpty {
Button("New Invitation") { addInvitation() }
} else if selection.count = 1 {
Button("Mark as VIP") { markVIPs(selection) }
} else {
Button("Mark as VIPs") { markVIPs(selection) }
}
}
}
Users can customize and reorder toolbars:
Table(attendees, selection: $selection) {
...
}
.toolbar(id: "invitations") {
ToolbarItem(id: "new", placement: .secondaryAction) {
Button(action: sendNewInvitation) {
Label("New Invitation", systemImage: "envelope")
}
}
}
New this year, .searchable
can support tokenized inputs and suggestions:
@State private var queryText: String
@State private var queryTokens: [InvitationToken]
var body: some View {
InvitationsContentView()
.searchable(text: $queryText, tokens: $queryTokens) { token in
Label(token.displayName, systemImage: token.systemImage)
}
}
SwiftUI now supports search scopes, which appear in a scope bar:
@State private var queryText: String
@State private var queryTokens: [InvitationToken]
@State private var scope: AttendanceScope
var body: some View {
InvitationsContentView()
.searchable(text: $queryText, tokens: $queryTokens, scope: $scope) { token in
Label(token.displayName, systemImage: token.systemImage)
} scopes: {
Text("In Person").tag(AttendanceScope.inPerson)
Text("Online").tag(AttendanceScope.online)
}
}
New PhotosPicker
view:
@State private var selection: PhotosPickerItem?
var body: some View {
Gallery(...)
.toolbar {
PhotosPicker(
selection: $selection, matching: .images
) {
Label("Pick a photo", systemImage: "plus.app")
}
}
}
New ShareLink API:
Gallery(...)
.toolbar {
ShareLink(
item: image,
preview: SharePreview("Birthday Effects")
)
}
New protocol. A Swift-first declarative way to describe how types are transferred across applications. This powers SwiftUI features, like drag-and-drop.
New DropDestination API, which accepts a payload type - in this case, just an image:
@State private var image: Image?
var body: some View {
Gallery(..)
.dropDestination(
payloadType: Image.self
) { receivedImages, location in
image = receivedImages.first
return !receivedImages.isEmpty
}
}
Many standard types, such as string and image, already conform to Transferrable:
- String
- Data
- Attributed String
- Image
- ...
You can implement it in your own custom types.
struct BirthdayFilter: Codable {
...
}
extension BirthdayFilter: Transferable {
static var representation: some TransferRepresentation {
CodableRepresentation(contentType: .birthdayFilter)
}
}
- Color has a new style that adds a subtle gradient:
.backgroundStyle(.blue.gradient)
- New Shadow modifier:
.foregroundStyle(
.white.shadow(.drop(radius: 1, y: 1.5))
)
- Grid is a new container view that arranges views in a 2D grid
- Enable automatic alignments across rows and columns
struct VipDetailView: View {
var body: some View {
Grid {
GridRow {
NameHeadline()
.gridCellColumns(2)
}
GridRow {
CalendarIcon()
SymbolGrid()
}
}
}
}
New Layout protocol lets you customize Layout abstractions. Using the new AnyLayout
type to switch between Grid and custom layout:
var body: some View {
let grid = Grid(
alignment: .center,
horizontalSpacing: 0,
veritcalSpacing: 0
)
let layout = model.usesGridLayout ? AnyLayout(grid) : AnyLayout(scattered)
layout {
ForEach(rows) { row in
GridRow {
ForEach(row.columns) {
CellView()
}
}
}
}
}
- Preview variants for multiple appearances, type sizes, and orientations without writing any configuration code
- Previews runs in Live mode by default
- Hello Swift Charts
- Raise the bar
- The SwiftUI cookbook for navigation
- What's new in Xcode
- Use Xcode to develop a multiplatform app
- Bring multiple windows to your SwiftUI app
- Meet Transferable
- Compose custom layouts with SwiftUI