contributors |
---|
DaemonLoki, MortenGregersen, multitudes |
1:05 - SwiftUI in more places
10:21 - Simplified data flow
18:46 - Extraordinary animations
27:18 - Enhanced interactions
- SwiftUI in more places
- Simplified data flow
- Extraordinary animations
- Enhanced interactions
Spatial computing brings SwiftUI into a bold new future with all-new 3D capabilities like volumes; rich experiences with immersive spaces; new 3D gestures, effects, and layout; and deep integration with RealityKit. From core pieces like the Home View in Control Center to familiar apps like TV, Safari, and Freeform; to all-new environments like immersive rehearsals in Keynote; SwiftUI is at the heart of these user experiences
Scenes for spatial computing uses WindowGroup
(render as 2D windows with 3D controls).
Add .windowStyle(.volumetric)
to get a volume.
NavigationSplitView {
SidebarList($selection)
} detail: {
Detail(for: selection)
}
TabView {
DogsTab ()
.tabItem { ... }
CatsTab()
.tabItem { ... }
BirdsTab ()
.tabItem { ... }
}
Fill a volume with a static model using Model3D
.
For dynamic, interactive models with lighting effects and more, use the new RealityView
.
// Volume content
import SwiftUI
import RealityKit
struct Globe: View {
var body: some View {
RealityView { content in
if let earth = try? await ModelEntity(named: "Earth") {
earth.addImageBasedLighting()
content.add(earth)
}
}
}
}
Use an ImmersiveSpace with the mixed immersion style to connect your app to the real world, combining your content with people's surroundings. Anchor elements of your app to tables and surfaces, and augment and enrich the real world with virtual objects and effects. Go further with the full immersion style. Your app takes complete control. Build these connected and immersive experiences using the same Model3D and RealityView that work in volumes.
- full immersion style
- mixed immersion style
See more in the session:
Meet SwiftUI for spatial computing - WWDC23
- Newly empowered with new transitions:
- NavigationSplitView
- TabView gets a new
.verticalPagingStyle
driven by the Digital Crown - NavigationStack
.containerBackground
modifier lets you configure the background of the container which animate when you push and pop content.
// Container backgrounds
CityDetails(city: city)
.containerBackground(for: navigation) { ... }
- Multiplatform toolbar placements (
.topBarLeading
and.topBarTrailing
, along with the existing.bottomBar
) let you perfectly place small detail views in your Apple Watch apps.
// Toolbar placements
.toolbar {
ToolbarItem(placement: .topBarTrailing) { ... }
ToolbarItem(placement: .bottomBar) { ...}
}
- New additions
DatePicker
- Selection in
List
s
// Newly available
DatePicker(
"Time (h:m:s)",
selection: $date,
displayedComponents: hourMinuteAndSecond
)
NavigationSplitView {
List(locales, selection: $selectedLocale) { locale in
...
}
.containerBackground(...)
} detail: {
if let selectedLocale { ... }
}
See more in the sessions:
�Design and build apps for watchOS 10 - WWDC23
and
Update your app for watchOS 10
Widgets for the Smart Stack on watchOS 10 let the people using your app see their information on the go.
SwiftUI is core to widgets wherever they appear, like these other new places. Widgets on the Lock Screen on iPadOS 17 are a great complement to widgets on the Home Screen.
Big, bold widgets shine on the iPhone Always-On display with Standby Mode.
And desktop widgets on macOS Sonoma...
Widgets now support interactive controls. Toggle and Button in Widgets can now activate code defined in your own app bundle using App Intents. And you can animate your widgets using SwiftUI transition and animation modifiers.
- Widgets for Smart Stack
- Appear when scrolling down
- Also available on iPadOS 17
- Standby Mode for iPhone
- Desktop Widgets on macOS
- Interactive controls for Widgets
- Toggle and Button
- Ase App Intents
- animate with transitions and animations
- Previews leverage macros to allow you to show the widget states with a timeline
See more in the sessions:
Bring widgets to new places - WWDC23
and
Bring widgets to life - WWDC23
#Preview(as: .systemSmall) {
CaffeineTrackerWidget()
} timeline: {
CaffeineLogEntry.log1
CaffeineLogEntry.log2
CaffeineLogEntry.log3
CaffeineLogEntry.log4
}
Declare and configure a Preview, add a widget type, and define a timeline for testing. Xcode Previews shows the current widget state and a timeline that lets you see the animations between states. Of course, the new previews work with regular SwiftUI views and apps as well.
import SwiftUI
import AppIntents
struct CaffeineTrackerwidgetView : View {
var entry: CaffeineLogEntry
var body: some View {
VStack (alignment: leading) {
TotalCaffeineView(totalCaffeine:
entry.totalCaffeine)
Spacer ()
if let log = entry.log {
LastDrinkView(log: log)
}
Spacer ()
HStack {
Spacer ()
LoaDrinkView()
Spacer ()
}
}
.fontDesign (.rounded)
.containerBackground(for: .widget) {
Color.cosmicLatte
}
}
}
New Previews syntax:
#Preview("good dog") {
ZStack(alignment: .bottom) {
Rectangle()
.fill(Color.blue.gradient)
Text("Riley")
.font(.largeTitle)
.padding()
.background(.thinMaterial, in: .capsule)
.padding()
}
.ignoresSafeArea()
}
You can now interact with previews of Mac apps right inside Xcode.
See more in the sessions:
Build programmatic UI with Xcode Previews - WWDC23
and
Several frameworks bring new or improved support:
- Authentication Services / MapKit
- HealthKit Swift Charts Continuity device picker
- Apple Pay Later / StoreKit / In-app purchasing / CareKit
MapKit delivers a massive update:
// MapKit
import SwiftUI
import MapKit
Map {
Marker(item: destination)
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
UserAnnotation()
}
.mapControls {
MapUserLocationButton( )
MapCompass()
}
NEW: Use maps in your view. NEW: Add markers, polylines and the user's location.
import SwiftUI
import MapKit
struct Maps_Snippet: View {
private let location = CLLocationCoordinate2D(
latitude: CLLocationDegrees(floatLiteral: 37.3353),
longitude: CLLocationDegrees(floatLiteral: -122.0097))
var body: some View {
Map {
Marker("Pond", coordinate: location)
UserAnnotation()
}
.mapControls {
MapUserLocationButton()
MapCompass()
}
}
}
#Preview {
Maps_Snippet()
}
See more in the session:
Meet MapKit for SwiftUI - WWDC23
// Swift Charts
Chart {
ForEach(SalesData.last365Days, id: \.day) {
BarMark(
x: .value("Day", SO.day, unit: .day),
y: .value("Sales", $0.sales)
)
}
.foregroundStyle(.blue)
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 30)
.chartScrollPosition(x: $scrollPosition)
NEW: Scrolling and selection in charts.
import SwiftUI
import Charts
struct ScrollingChart_Snippet: View {
@State private var scrollPosition = SalesData.last365Days.first!
@State private var selection: SalesData?
var body: some View {
Chart {
...
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 30)
.chartScrollPosition(x: $scrollPosition)
.chartXSelection(value: $selection)
}
}
NEW: Donut and pie charts with the new SectorMark
.
import SwiftUI
import Charts
struct DonutChart_Snippet: View {
var sales = Bagel.salesData
var body: some View {
Chart(sales, id: \.name) { element in
SectorMark(
angle: .value("Sales", element.sales),
innerRadius: .ratio(0.6),
angularInset: 1.5)
.cornerRadius(5)
.foregroundStyle(by: .value("Name", element.name))
}
}
}
See more in the session:
Explore pie charts and interactivity in Swift Charts - WWDC23
NEW: Cross platform view, for presenting available subscriptions. The view can be customized.
// StoreKit
import SwiftUI
import StoreKit
SubscriptionStoreView(groupID: passGroupID) {
PassMarketingContent()
.lightMarketingContentStyle()
.containerBackground(
for: .subscriptionStoreFullHeight
) {
SkyBackground()
}
}
.backgroundStyle(.clear)
.subscriptionStoreButtonLabel(.multiline)
.subscriptionStorePickerItemBackground(.thinMaterial)
.storeButton(.visible, for: .redeemCode)
import SwiftUI
import StoreKit
struct SubscriptionStore_Snippet {
var body: some View {
SubscriptionStoreView(groupID: passGroupID) {
MyMarketingContent()
.lightMarketingContentStyle()
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
}
.backgroundStyle(.clear)
.subscriptionStoreButtonLabel(.multiline)
.subscriptionStorePickerItemBackground(.thinMaterial)
.storeButton(.visible, for: .redeemCode)
}
}
See more in the session:
Meet StoreKit for SwiftUI - WWDC23
@Observable
macro- familiar patterns while keeping code precise and performant
- simply add
@Observable
to a class - no need for
@State
or similar things in the view - only read values trigger view updates
- get rid of
@
ObservableObject,@StateObject
and others, only@State
and@Environment
are left
// Observable model
import Foundation
@Observable
class Dog: Identifiable {
var id = UUID()
var name = ""
var age = 1
var breed = DogBreed.mutt
var owner: Person?
}
@Observable
macro- And this view is reading the isFavorite property, so when that changes, it will get reevaluated. Invalidation only happens for properties which are read, so you can pass your model through intermediate views without triggering any unnecessary updates.
struct DogCard: View {
var dog: Dog
var body: some View {
DogImage(dog: dog)
.overlay(alignment: bottom) {
HStack {
Text(dog.name)
Spacer()
Image(systemName: "heart")
.symbolVariant(
dog.isFavorite ? .fill : .none)
}
.background(.thinMaterial)
}
}
}
@State
can be passed into the environment- can then be read either via the type (e.g.
User.self
) or with a custom key
- can then be read either via the type (e.g.
SwiftUI includes several tools for defining your state and its relationship to your views, several of which are designed for use with ObservableObject:
- @State
- @StateObject
- @ObservedObject
- @Environment
- @EnvironmentObject
When using Observable, this is becomes even simpler since it's designed to work directly with the State and Environment dynamic properties:
- @State
- @Environment
Observables are also a natural fit to represent mutable state, like on this form for a new dog sighting. The model is defined using the State dynamic property, and I'm passing bindings to its properties to the form elements responsible for editing that property.
@State private var model = DogDetails ()
Form {
Section {
TextField( "Name"', text: $model.dogName)
DogBreedPicker(selection: $model.dogBreed)
}
Section {
StarRating(selection: $model.rating)
TextField("Location", text: $model.location)
}
Section {
PhotosPicker(
"Add photo...", selection: $model.photo,
matching: .images)
}
}
}
Lastly, Observable types integrate seamlessly into the environment.
@main
struct WhatsNew2023: App {
@State private var currentUser: User?
var body: some Scene {
WindowGroup {
ContentView()
.environment(currentUser)
}
}
}
@Observable final class User { ... }
Since views throughout our app want a way to fetch the current user, I've added it to the environment of my root view. The user profile view then reads the value using the Environment dynamic property. I'm using the type as the environment key here, but custom keys are also supported.
struct ProfileView: View {
@Environment (User.self) private var currentUser: User?
var body: some View {
if let currentUser {
UserDetails(user: currentUser)
} else {
LoginScreen ()
}
}
}
Be sure to watch:
Discover Observation in SwiftUI - WWDC23
To set up my Dog model type for SwiftData, I'll switch from using Observable to the Model macro. This is the only change I need to make. In addition to the persistence provided by SwiftData, models also receive all the benefits of using Observable.
// SwiftData model
import Foundation
import SwiftData
@Model
class Dog {
var name = ""
var age = 1
var breed = DogBreed.mutt
var owner: Person?
}
- add a modifier for
.modelContainer
with the definition of the model type (.modelContainer(for: Dog.self)
) added to theWindowGroup
of the app - inside of the view, switch
@State
to@Query
which allows e.g. sorting (@Query(sort: \.dateSpotted)
) and efficient data loading even for large datasets- also stores document data
import SwiftUI
import SwiftData
@main
struct WhatsNew2023: App {
var body: some Scene {
WindowGroup {
ContentView()
}
modelContainer (for: Dog.self)
}
}
Then in my view code, I'll switch my array of dogs to use the new Query dynamic property instead of @State. Using Query will tell SwiftData to fetch the model values from the underlying database.
import SwiftUI
import SwiftData
struct RecentDogs: View {
@Query private var dogs: [Dog] = ...
var body: some View {
ScrollView {
LazyVStack {
ForEach(dogs) { dog in
DogCard (dog: dog)
}
}
}
}
}
Query is incredibly efficient for large data sets and allows for customization in how the data is returned, such as changing the sort order to use the date I spotted the dog:
struct RecentDogs: View {
@Query(sort: \.dateSpotted) private var dogs: [Dog]
var body: some View {
ScrollView {
LazyVStack {
ForEach(dogs) { dog in
DogCard (dog: dog)
}
}
}
}
}
- DocumentContainer gets updates
- SwiftData
- Sharing options
- Undo/Redo support
- Inspector (presenting differently on the respective layouts)
// DocumentGroup with SwiftData model
import SwiftUI
import SwiftData
@main
struct WhatsNew2023: App {
var body: some Scene {
DocumentGroup(editing: DogTag.self, contentType: •dogTag) {
ContentView()
}
}
}
extension UTType {
static var dogTag: UTType {
...
}
}
Please see:
and
Build an app with SwiftData - WWDC23
DocumentGroup also gains a number of new platform affordances when running on iOS 17 or iPadOS 17, such as automatic sharing and document renaming support, as well as undo controls in the toolbar.
Inspector is a new modifier for displaying details about the current selection or context. It's presented as a distinct section in your interface. On macOS, Inspector presents as a trailing sidebar. as well as on iPadOS in a regular size class. In compact size classes, it will present itself as a sheet.
// Inspector
struct ContentView: View {
@State private var inspectorPresented = true
var body: some View {
DogTagEditor()
.inspector(isPresented: $inspectorPresented) {
DogTagInspector()
}
}
}
To uncover all the details on Inspector, watch:
Inspectors in SwiftUI: Discover the details - WWDC23
New modifiers to give my image export dialog some useful information, like adjusting the confirmation button's label.
ContentView()
.fileExporter(isPresented: $isExporterPresented,
item: selectedItem,
contentType: .png,
defaultFilename: selectedItem?.name)
{
handleDataExport($0)
}
.fileExporterFilenameLabel("Export Image")
.fileDialogConfirmationLabel("Export Image")
- Dialogs get new powers
- e.g. severity, suppression toggle
- HelpLinks
// Dialog customizations
DogTagEditor()
.confirmationDialog(
"Are you sure you want to delete the selected dog tag?",
isPresented: $showDeleteDialog)
{
Button("Delete dog tag", role: .destructive) {
deleteSelectedTag()
}
HelpLink {
..
}
}
.dialogSeverity(.critical)
.dialogSuppressionToggle(isSuppressed: $suppressed)
- Lists and Tables
- customization of data toggling
- programmatically expand sections
- new stylings for tables
Lists and tables are a key part of most apps, and SwiftUI has brought some new features and APIs for fine-tuning them in iOS 17 and macOS Sonoma. Tables support customization of their column ordering and visibility. When coupled with the SceneStorage dynamic property, these preferences can be persisted across runs of your app. You provide the table with a value representing the customization state and give each column a unique stable identifier.
// Table customization support
struct DogSightingsTable: View {
@SceneStorage ("TableConfiguration")
private var columnCustomization: TableColumnCustomization<DogSighting>
var body: some View {
Table(dogSightings, selection: $selectedSighting,
columnCustomization: $columnCustomization) {
TableColumn ("Dog Name", value: \.name)
.customizationID("name")
TableColumn("Dog Breed", value: \.breed.name)
.customizationID("breed")
...
}
}
}
Tables now also have all the power of OutlineGroup built in. This is great for large data sets that lend themselves to a hierarchical structure, like this one which groups some of my favorite dogs with their proud parents. Simply use the new DisclosureTableRow to represent rows that contain other rows, and build the rest of your table as you would normally.
Table (of: DogGenealogy.self) {
TableColumn ("Dog Name", value: \.name)
TableColumn ("Dog Breed", value: \.breed.name)
TableColumn( "Age (Dog Years)") {
Text ($0.age, format: .number)
}
TableColumn ("Favorite Toy", value: \.favoriteToy)
} rows: {
ForEach(dogs) { dog in
DisclosureTableRow(dog) {
ForEach(dog.children) { child in
TableRow(child)
}
}
}
}
Sections within a list or table have gained support for programmatic expansion. I've used it here in my app's sidebar to show the location section as collapsed initially, but while still allowing for expansion. The new initializer takes a binding to a value which reflects the current expansion state of the section.
// Programmatic Section expansion
Section("Dog Breeds", isExpanded: $isBreedSectionExpanded) {
DogBreeds()
}
Section ("Locations", isExpanded: SisLocationSectionExpanded) {
SightingLocations()
}
For smaller data sets, tables have also gained a few new styling affordances, such as how row backgrounds and column headers are displayed.
Table(dogSightings, selection: $selection) {
TableColumn("Breed" ) {
Text(SO.name)
}
TableColumn("Sightings") {
Text($0.sightings, format: .number)
}
TableColumn("Rating") {
StarRating($0.rating)
}
}
.alternatingRowBackgrounds(.disabled)
.tableColumnHeaders(.hidden)
Custom controls like my star rating will also benefit from the new background prominence environment property. Using a less prominent foreground style when the background is prominent lets my custom control feel right at home in a list.
StarRating(sighting.rating)
.foregroundStyle(.starRatingForeground)
struct StarRatingForegroundStyle: ShapeStyle {
func resolve(in environment: EnvironmentValues)
-> some ShapeStyle
{
if environment.backgroundProminence == .increased {
return AnyShapeStyle(.secondary)
} else {
return AnyShapeStyle(.yellow)
}
}
}
We've also made big improvements to the performance, particularly when dealing with large data sets. To learn more about this and the ways you can optimize your own SwiftUI views, check out:
Demystify SwiftUI performance - WWDC23
- Keyframe Animator API
- animate multiple values in parallel
- give the animator a value of animatable values
KeyframeAnimator
defines a view, then a list ofKeyframeTrack
s with different keyframe animations.
Changes to the state trigger my animation. In the first closure, I build a view, modified by my animatable properties, like the vertical offset of my logo. In the second closure, I define how these properties change over time. For example, the first track defines the animation of my verticalTranslation property. I pull my logo down 30 points over the first quarter second using a spring animation. Then I make my Beagle leap and land using a cubic curve. Finally, I bring this dog home with a natural spring animation. I define additional tracks for my other animated properties.
All these tracks run in parallel to create this cool animation. To learn how to leverage keyframe animators in your apps, check out:
�Wind your way through advanced animations in SwiftUI - WWDC23
KeyframeAnimator(
initialValue: LogoAnimationValues(), trigger: runPlan
) { values in
LogoField(color: color, isFocused: isFocused)
.scaleEffect(values.scale)
.rotationEffect(values.rotation, anchor: .bottom)
.offset(y: values.verticalTranslation)
} keyframes: { _ in
KeyframeTrack(\.verticalTranslation) {
SpringKeyframe(30, duration: 0.25, spring: .smooth)
CubicKeyframe(-120, duration: 0.3)
CubicKeyframe(-120, duration: 0.3)
SpringKeyframe(0, spring: .bouncy)
}
KeyframeTrack(\.scale) { ... }
KeyframeTrack(\.rotation) { ... }
- difference to keyframe: sequentially go through animation steps, while keyframe goes through multiple ones in parallel
- start one animation when the previous one has finished
Ex. working on an Apple Watch app to record dog sightings. It's pretty simple so far, just our happy icon and a button to register a sighting. I'd like to animate this icon when I tap the button. This is a good place for a phase animator. A phase animator is simpler than a keyframe animator. Instead of parallel tracks, it steps through a single sequence of phases. This lets me start one animation when the previous animation finishes. I give the animator a sequence of phases and tell it to run my animation whenever my sightingCount changes. Then in this first closure, I set the rotation and scale of my happy dog based on the current phase. The second closure tells SwiftUI how to animate into each phase.
They're now the default animation for apps built on or after iOS 17 and aligned releases.
Haptic feedback is easy with the new sensory feedback API. To play haptic feedback, I just attach the sensoryFeedback modifier, specify what sort of feedback I want and when it should happen.
HappyDog()
.phaseAnimator(
SightingPhases.allCases, trigger: sightingCount
) { content, phase in
content
.rotationEffect(phase.rotation)
.scaleEffect(phase.scale)
} animation: { phase in
switch phase {
case .shrink: .snappy(duration: 0.1)
case .spin: .bouncy
case .grow: .spring(
duration: 0.2, bounce: 0.1, blendDuration: 0.1)
case .reset: .linear(duration: 0.0)
}
}
.sensoryFeedback(.increase, trigger: sightingCount)
- define multiple phases and hand them to the
.phaseAnimator
modifier, together with a trigger that triggers the animation whenever a value changes - define the content and how the elements of the phases change it (e.g. rotation, scale)
- provide the types of animations for the different phases, e.g.
.snappy
,.bouncy
, or newspring
options - Haptic Feedback
.sensoryFeedback(.increase, trigger: sightingCount)
Check out the Human Interface Guidelines to learn what sorts of feedback will be best in your apps:
Playing haptics - Human Interface Guidelines
To learn about the fundamentals of animation in SwiftUI, check out:
Explore SwiftUI animation - WWDC23
and
- works with coordinate spaces
- and geometry reader somehow
- sure fun to play around with
The visual effects modifier lets me update these dog photos based on their position. And I don't need a GeometryReader to do it. I've got a little simulation that moves a focal point around the screen. This red dot shows what I mean by focal point. I associate a coordinate space with this grid that shows all the dogs.
// Coordinate spaces and visual effects
ScrollView {
LazyVGrid(columns: columns) {
ForEach(dogs) { dog in
DogCircle(dog: dog, focalPoint: simulation.point)
}
}
}
.coordinateSpace(.dogGrid)
Then inside my DogCircle view, I add a visual effect. The closure gets my content to modify and a geometry proxy. I'm passing the geometry proxy to a helper method to compute the scale.
// Coordinate spaces and visual effects
struct DogCircle: View {
var dog: Dog
var focalPoint: CGPoint
var body: some View {
DogImage (dog: dog)
.visualEffect { content, geometry in
content
.scale(contentScale(in: geometry))
.grayscale(contentGrayscale(in: geometry))
.saturation(contentSaturation (in: geometry))
}
}
}
}
I can use the geometry proxy to get the size of my grid view and the frame of a single dog circle relative to my grid view. That lets me compute how far any dog is from the focal point of the simulation, so I can scale up the focused doggos. With visual effects, I can do all of this without using a GeometryReader. And it automatically adapts to different sizes.
// Coordinate spaces and visual effects
func contentScale(in geometry: GeometryProxy) -> Double {
guard let gridSize = geometry.bounds (of: .dogGrid)?.size else { return 0 }
let frame = geometry.frame(in: .dogGrid)
...
}
- Text can now be styles with a
foregroundStyle
inside another Text Text("\(Text(dog.name).foregroundStyle(stripes)) is a good dog")
- It works with a Shader
- Can bring Metal shaders into SwiftUI with ShaderLibrary
I'm passing my stripeSpacing and angle, along with a color from my asset catalog, to a custom Metal shader.
var stripes: Shader {
ShaderLibrary.angledFill(
.float(stripeSpacing),
.float(stripleAngle),
.color(Color(.stripes))
)
}
var stripes: Shader {
ShaderLibrary.angledFill(
.float(stripeSpacing),
. .float(stripeAngle),
.color(Color(.stripes))
)
}
Using SwiftUI's new ShaderLibrary, I can turn Metal shader functions directly into SwiftUI shape styles, like this one that renders the stripes in Furdinand's name.
// Metal shaders
[[ stitchable ]] half4
angledFill(float2 position, float width, float angle, half4 color)
{
float pMagnitude = sqrt(position.x * position.x + position.y * position.y);
float pAngle = angle +
(position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x));
float rotatedX = pMagnitude * cos(pAngle);
float rotatedY = pMagnitude * sin(pAngle);
return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2;
}
If you'd like to take Metal shaders out for a spin, just add a new Metal file to your project and call your shader function using ShaderLibrary in SwiftUI.
Symbols get a .symbolEffect
modifier with multiple options:
.pulse
.variableColor
.scale
.appear/disappear
.replace
- event notifications with
bounce
- new
.textScale(.secondary)
modifier automatically scaling
Image(systemName: "..").symbolEffect(...)
Check also:
Animate symbols in your app - WWDC23
I want to point out one last feature. Notice the units on the text here. In the past I might have used small caps for this effect, but now I can get this appearance by applying the new textScale modifier to my units.
And, if Jeff and I bring our app to the Chinese market, the units will be sized correctly, even though the concept of small caps isn't part of the typography in Chinese.
// Text style
Text("\(space) \(Text("PX").textScale(.secondary))")
Text ("\(angle) \(Text("RAD").textScale(.secondary))")
We have another tool to help apps work great in multiple locales. Some languages, like Thai, use taller letter forms. When text from one of these languages is embedded in text localized in a language with shorter letter forms, like English, the taller text can be crowded or clipped. When we know that this might be an issue -- for example, if our dog names were globally crowd-sourced -- we can apply the typesettingLanguage modifier. This lets SwiftUI know that the text might need more space.
// Typesetting language
struct Dog {
var name: String
var language: Locale.Language
...
}
var dog = Dog(
name: "lala",
language: .init(languageCode: .thai))
Text(
"""
Who's a good dog, \
\(Text(dog.name).typesettingLanguage(dog.language))?
""")
- the
.scrollTransition
modifier can be applied to elements inside of the ScrollView - gets a
content
and aphase
, let’s apply effects to thecontent
with the help of thephase
properties, e.g. the.isIdentity
.containerRelativeFrame
allows to split the view into parts of the frame (count
) and define how much each element should span (span
)- can use a
.safeAreaInset
modifier with an edge to position this .scrollTargetLayout
can be aded to theLazyHStack
and then theScrollView
can get ascrollTargetBehavior(.viewAligned)
modifier- paging behavior also possible, or something custom using the
ScrollTargetBehavior
protocol .scrollPosition
shows the top most item
I'd like to add some visual effects to my dog cards as they transition in and out of the visible area of my scroll view. The scroll transition modifier is very similar to the visual effect modifier Curt used earlier for the welcome screen. It lets you apply effects to items in your scroll view.
// Scroll transition effects
ScrollView {
LazyVStack {
ForEach(dogs) { dog in
DogCard (dog: dog)
.scrollTransition { content, phase in
content
.scaleEffect(phase.isIdentity ? 1 : 0.6)
.opacity (phase.isIdentity ? 1: 0)
}
}
}
}
.safeAreaPadding(.horizontal, 16.0)
I'd also like to add a side-scrolling list of my favorite dog parks to this screen. Above my vertical stack of dogs, I'll drop in a horizontal stack for the park cards. I'm using the new containerRelativeFrame modifier to size these park cards relative to the visible size of the horizontal scroll view. The count specifies how many chunks to divide the screen into. The span says how many of those chunks each view should take.This is pretty great, but I'd like my park cards to snap into place. The new scrollTargetLayout modifier makes that easy. I'll add it to the LazyHStack and modify the scroll view to align to views in the targeted layout.
// ScrollView layout enhancements
ScrollView {
...
}
.safeAreaInset(edge: .top) {
ScrollView(.horizontal) {
LazyHStack {
ForEach(parks) { park in
ParkCard(park: park)
.containerRelativeFrame(
.horizontal, count: 5, span: 2, spacing: 8)
}
}
.scrollTargetLayout ()
}
.scrollTargetBehavior(.viewAligned)
}
Scroll views can also be defined to use a paging behavior. And you can define your own behavior using the scrollTargetBehavior protocol. The new scrollPosition modifier takes a binding to the topmost item's ID, and it's updated as I scroll.
// Scroll position
ScrollView {
...
}
safeAreaInset(edge: .top) {
...
}
.scrollPosition(id: $scrolledID)
To learn more about all these and the other great improvements to Scroll View, be sure to watch:
- Images support HDR with
.allowDynamicRange(.high)
- use sparingly
Image now supports rendering content with high dynamic range. By applying the allowedDynamicRange modifier, the beautiful images in our app's gallery screen can be shown with their full fidelity. It's best to use this sparingly, though, and usually when the image stands alone.
// HDR images
struct GalleryDetail: View {
var dog: Dog
var body: some View {
VStack {
CloseButton ()
Spacer ()
AsyncImage (url: dog.photoURL)
Spacer()
ImageActions()
}
}
}
I'm also going to add the new accessibilityZoomAction modifier to my view. This allows assistive technologies like VoiceOver to access the same functionality without using the gesture. I'll just update the zoom level depending on the action's direction, and I can see what mischief she's been up to now. VoiceOver: Zooming image view. Image.
// Accessibility zoom
DogImage(dog: dog)
.scaleEffect (zoomLevel)
.gesture (magnification)
.accessibilityZoomAction { action in
switch action.direction {
case .ZoomIn:
zoomLevel += 0.5
case .zoomOut:
zoomLevel -= 0.5
}
}
For more, be sure to check out:
Build accessible apps with SwiftUI and UIKit - WWDC23
- use static imports of colors defined in the asset catalog with
Color(.tennisBallYellow)
Color now supports using static member syntax to look up custom colors defined in your app's asset catalog. This gives compile-time safety when using them, so you'll never lose time to a typo. For the document app I showed earlier, I've added a menu containing several useful actions to the toolbar. The top section of the menu is a ControlGroup with the new compactMenu style, which shows its items as icons in a horizontal stack. The tag color selector is defined as a picker with the new palette style. Using this style in concert with symbol images gives a great visual representation in menus, especially one like this where I can use the label's tint to differentiate them.
// Static member syntax for custom colors
Picker("Tag Color", selection: $selection) {
Label ("Tennis Ball Yellow")
.tint (Color(.tennisBallYellow))
.tag(.yellow)
Label("Rawhide Brown")
.tint(Color(.rawhideBrown))
.tag (.brown)
...
}
Lastly, the paletteSelectionEffect modifier lets me use a symbol variant to represent the selected item in the picker. With my menu in place, Buddy's dog tag can now be his favorite color, tennis-ball yellow.
Picker("Tag Color", selection: $selectedTagColor) {
ForEach(tagColors) { tagColor in
Label(tagColor.title, systemImage: "tag")
.tint(tagColor.color)
.tag(tagColor)
}
}
.pickerStyle(.palette)
.paletteSelectionEffect(.symbolVariant(.fill))
- Control group with new compactMenu style (
.controlGroupStyle(.compactMenu)
) - Pickerstyle with a
.pickerStyle(.palette)
and the.paletteSelectionEffect(.symbolVariant(.fill))
The top section of the menu is a ControlGroup with the new compactMenu style, which shows its items as icons in a horizontal stack. The tag color selector is defined as a picker with the new palette style. Using this style in concert with symbol images gives a great visual representation in menus, especially one like this where I can use the label's tint to differentiate them. Lastly, the paletteSelectionEffect modifier lets me use a symbol variant to represent the selected item in the picker.
ControlGroup {
Button {
cutSelection()
} label: {
Label("Cut", systemImage: "scissors")
}
Button {
copySelection ()
} label: {
Label ("Copy", systemImage: "doc.on.doc")
}
}
.controlGroupStyle (.compactMenu)
The tag color selector is defined as a picker with the new palette style. Using this style in concert with symbol images gives a great visual representation in menus, especially one like this where I can use the label's tint to differentiate them. Lastly, the paletteSelectionEffect modifier lets me use a symbol variant to represent the selected item in the picker.
Picker ("Tag Color", selection: $selectedTagColor) {
ForEach(tagColors) { tagColor in
Label(tagColor.title, systemImage: "tag")
.tint(tagColor.color)
.tag (tagColor)
}
}
.pickerStyle(.palette)
.paletteSelectionEffect(.symbolVariant(.fill))
- New Button styles (coming after
.buttonStyle(.bordered)
).buttonBorderShape(.roundedRectangle)
buttonBorderShape(.circle)
- Buttons can support drag actions / force-clicking (macOS) with the
.springLoadingBehavior(.enabled)
modifier
Bordered buttons can now be defined with new built-in shapes, such as circle and rounded rectangle. These new border shape styles work on iOS, watchOS, and macOS.
// Bordered button shapes
Button {
toggleGuides()
} label: {
Label("Toggle Guides", systemImage: "ruler")
}
.buttonStyle(.bordered)
.buttonBorderShape(. roundedRectangle)
...
Button {
showPopover.toggle()
} label: {
Label("Add", systemImage: "plus")
}
.buttonStyle(.bordered)
.buttonBorderShape (.circle)
Buttons on macOS and iOS can now react to drag actions, like this button in my editor that opens a popover. The new springLoadingBehavior modifier indicates that a button should trigger its action when a drag pauses over it, or when force-clicking it on macOS.
// Button spring-loading behavior
Button {
showPopover = true
} label: {
Label("Add", systemImage: "plus")
.buttonStyle(.bordered)
.buttonBorderShape(.circle)
.springLoadingBehavior(.enabled)
- New
.hoverEffect(.highlight)
(only available for tvOS?)
// Highlight hover effect
struct DogGalleryCard: View {
@FocusState private var isFocused: Bool
var dog: Dog
var body: some View {
Button {
openDogDetail(dog)
} label: {
DogGalleryImage(dog: dog)
.hoverEffect(.highlight)
Text(dog.name)
.opacity(isFocused ? 1 : 0)
}
.buttonStyle(.borderless)
.focused (SisFocused)
}
}
.onKeyPress
to allow for reaction to any keyboard input Focusable views on platforms with hardware keyboard support can use the onKeyPress modifier to directly react to any keyboard input. The modifier takes a set of keys to match against and an action to perform for the event.
// Handling key presses
DogTagEditor()
.focusable (true, interactions: •edit)
.focusEffectDisabled ()
.onKeyPress (characters: letters, phases: .down) { press in
...
}
To get your fill of focus-related recipes, be sure to watch
The SwiftUI cookbook for focus - WWDC23
- SwiftUl in more places
- Simplified data flow
- Extraordinary animations
- Enhanced interactions
Have a question? Ask with tag wwdc2023-10148
Search the forums for tag wwdc2023-10148
Playing haptics
Animate symbols in your app - WWDC23
Animate with springs - WWDC23
Beyond scroll views - WWDC23
Bring widgets to new places - WWDC23
Bring widgets to life - WWDC23
Build accessible apps with SwiftUI and UIKit - WWDC23
Build an app with SwiftData - WWDC23
Build programmatic UI with Xcode Previews - WWDC23
Design and build apps for watchOS 10 - WWDC23
Demystify SwiftUI performance - WWDC23
Discover Observation in SwiftUI - WWDC23
Explore pie charts and interactivity in Swift Charts - WWDC23
Explore SwiftUI animation - WWDC23
Inspectors in SwiftUI: Discover the details - WWDC23
Meet MapKit for SwiftUI - WWDC23
Meet StoreKit for SwiftUI - WWDC23
Meet SwiftData - WWDC23
Meet SwiftUI for spatial computing - WWDC23
The SwiftUI cookbook for focus - WWDC23
What’s new in Swift - WWDC23
Wind your way through advanced animations in SwiftUI - WWDC23
Update your app for watchOS 10