WWDC 2024, What’s new in SwiftUI

In this specific article we will focus on SwiftUI and new announcement by apple.

Serhan Khan
15 min readJun 25, 2024

Key Features:

  1. Fresh Apps: That aims to add new features to make apps feel like brand new.
  2. Harnessing the platform: Tools to refine our apps as developers so they feel at home on every platform.
  3. Framework Foundations: Widespread improvements to the foundational buildings blocks of the framework.
  4. Crafting experiences: Whole suite of the new tools for crafting immersive experiences.

In new SwiftUI improvements, developers can really freshen up their app from a new tab view, mesh gradients, and snappy controls.

We will be reviewing a Karaoke Planner app where it was developed by apple developers to understand new features that was introduced during WWDC 2024.

Fresh Apps

A) Fresh Tabbar view for with SwiftUI:

Figure 1 — Sidebar driven Karaoke Planner App

In iOS 18 side bar has become a lot more flexible, for example when we click on a button above (top left side corner).

Figure 2 — Button that changes the side bar to Tabview

The side bar place and UI changes to a new way (tab bar representation) . With that said please refer figure 3.

Figure 3 — Tabbar Representation

So, how do we achieve this new representation let’s dive into a example code block:

import SwiftUI

struct KaraokeTabView: View {
@State var customization = TabViewCustomization()

var body: some View {
TabView {
Tab("Parties", image: "party.popper") {
PartiesView(parties: Party.all)
}
.customizationID("karaoke.tab.parties")

Tab("Planning", image: "pencil.and.list.clipboard") {
PlanningView()
}
.customizationID("karaoke.tab.planning")

Tab("Attendance", image: "person.3") {
AttendanceView()
}
.customizationID("karaoke.tab.attendance")

Tab("Song List", image: "music.note.list") {
SongListView()
}
.customizationID("karaoke.tab.songlist")
}
.tabViewStyle(.sidebarAdaptable)
.tabViewCustomization($customization)
}
}

struct PartiesView: View {
var parties: [Party]
var body: some View { Text("PartiesView") }
}

struct PlanningView: View {
var body: some View { Text("PlanningView") }
}

struct AttendanceView: View {
var body: some View { Text("AttendanceView") }
}

struct SongListView: View {
var body: some View { Text("SongListView") }
}

struct Party {
static var all: [Party] = []
}

#Preview {
KaraokeTabView()
}

The customisation behaviour lets us to reordering or removing tabs from which is completely programatically controllable.

@State var customization = TabViewCustomization()
Figure 4 — Reorderable Side bar example

Refreshed side bar also can be applied to tvOS so that you can use it in multiple platforms, such as iPadOS, tvOS, macOS.

Figure 5 — tvOS Side Bar Representation

In macOS, we can style the tab bar to be used as a side bar or segmented control.

Figure 6 — macOS Segmented and Side bar representation

B) Sheet Presentation Sizing:

Sheet presentation sizing now unified and simplified across platforms.

We as developers can use presentationSizing modifier to adjust our sheets sizes accordingly, new SwiftUI has three alternatives for this, (.form), (.page) or (.customSizing)

struct AllPartiesView: View {
@State var showAddSheet: Bool = true
var parties: [Party] = []

var body: some View {
PartiesGridView(parties: parties, showAddSheet: $showAddSheet)
.sheet(isPresented: $showAddSheet) {
AddPartyView()
.presentationSizing(.form)
}
}
}
Figure 7 — New Sheet Presentation Sizing

In addition to this SwiftUI supports new Zoom navigation transition too. Where we can represent the DetailView by using new NavigationTransition(.zoom) modifier to bring the DetailView in a zoomed way.

Figure 8 — New navigation transition (Zoom)

And here is how it looks for you (expanded parties view):

Figure 9 — Zoomed Navigation Transition View ( For Parties)
import SwiftUI

struct PartyView: View {
var party: Party
@Namespace() var namespace

var body: some View {
NavigationLink {
PartyDetailView(party: party)
.navigationTransition(.zoom(
sourceID: party.id, in: namespace))
} label: {
Text("Party!")
}
.matchedTransitionSource(id: party.id, in: namespace)
}
}

struct PartyDetailView: View {
var party: Party

var body: some View {
Text("PartyDetailView")
}
}

struct Party: Identifiable {
var id = UUID()
static var all: [Party] = []
}

#Preview {
@Previewable var party: Party = Party()
NavigationStack {
PartyView(party: party)
}
}

Note: These new Tabview features available on iPadOs 18 and forward.

C) Controls API:

Developers now eligible to add their own resizable controls to control center such as toggles and buttons that live in control center or the lock screen.

The controls are even able to be activated by the action button. Controls are a new kind of Widget that are easy to build with App Intents!

Figure 10 — New Control Center Widget

Below code demonstrates how we can add the new widget to control center.

import WidgetKit
import SwiftUI

struct StartPartyControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.karaoke_start_party"
) {
ControlWidgetButton(action: StartPartyIntent()) {
Label("Start the Party!", systemImage: "music.mic")
Text(PartyManager.shared.nextParty.name)
}
}
}
}

// Model code

class PartyManager {
static let shared = PartyManager()
var nextParty: Party = Party(name: "WWDC Karaoke")
}

struct Party {
var name: String
}

// AppIntent

import AppIntents

struct StartPartyIntent: AppIntent {
static let title: LocalizedStringResource = "Start the Party"

func perform() async throws -> some IntentResult {
return .result()
}
}

D) Vectorised and function plots

Function plotting in Swift charts makes it easy for us to draw graphs

import SwiftUI
import Charts

struct AttendanceView: View {
var body: some View {
Chart {
LinePlot(x: "Parties", y: "Guests") { x in
pow(x, 2)
}
.foregroundStyle(.purple)
}
.chartXScale(domain: 1...10)
.chartYScale(domain: 1...100)
}
}

#Preview {
AttendanceView()
.padding(40)
}
Figure 11 — Function Plotting and Vectorised Charts

E) Dynamic Table Columns

Using “TableColumnForEach”, we can now have a dynamic number of table columns.

import SwiftUI

struct SongCountsTable: View {

var body: some View {
Table(Self.guestData) {
// A static column for the name
TableColumn("Name", value: \.name)

TableColumnForEach(Self.partyData) { party in
TableColumn(party.name) { guest in
Text(guest.songsSung[party.id] ?? 0, format: .number)
}
}
}
}

private static func randSongsSung(low: Bool = false) -> [Int : Int] {
var songs: [Int : Int] = [:]
for party in partyData {
songs[party.id] = low ? Int.random(in: 0...3) : Int.random(in: 3...12)
}
return songs
}

private static let guestData: [GuestData] = [
GuestData(name: "Sommer", songsSung: randSongsSung()),
GuestData(name: "Sam", songsSung: randSongsSung()),
GuestData(name: "Max", songsSung: randSongsSung()),
GuestData(name: "Kyle", songsSung: randSongsSung(low: true)),
GuestData(name: "Matt", songsSung: randSongsSung(low: true)),
GuestData(name: "Apollo", songsSung: randSongsSung()),
GuestData(name: "Anna", songsSung: randSongsSung()),
GuestData(name: "Raj", songsSung: randSongsSung()),
GuestData(name: "John", songsSung: randSongsSung(low: true)),
GuestData(name: "Harry", songsSung: randSongsSung()),
GuestData(name: "Luca", songsSung: randSongsSung()),
GuestData(name: "Curt", songsSung: randSongsSung()),
GuestData(name: "Betsy", songsSung: randSongsSung())
]

private static let partyData: [PartyData] = [
PartyData(partyNumber: 1, numberGuests: 5),
PartyData(partyNumber: 2, numberGuests: 6),
PartyData(partyNumber: 3, numberGuests: 7),
PartyData(partyNumber: 4, numberGuests: 9),
PartyData(partyNumber: 5, numberGuests: 9),
PartyData(partyNumber: 6, numberGuests: 10),
PartyData(partyNumber: 7, numberGuests: 11),
PartyData(partyNumber: 8, numberGuests: 12),
PartyData(partyNumber: 9, numberGuests: 11),
PartyData(partyNumber: 10, numberGuests: 13),
]

}

struct GuestData: Identifiable {
let name: String
let songsSung: [Int : Int]

let id = UUID()
}

struct PartyData: Identifiable {
let partyNumber: Int
let numberGuests: Int
let symbolSize = 100

var id: Int {
partyNumber
}

var name: String {
"\(partyNumber)"
}
}

#Preview {
SongCountsTable()
.padding(40)
}
Figure 12 — Table Columns

F) Mesh Gradients

SwiftUI has added first class support for colourful mesh gradients, by interpolating between points on a grid of colours.

For example with this specific new feature we can have gradient background colours for our views.

Figure 13 — Mesh Gradient Example View
import SwiftUI

struct MyMesh: View {
var body: some View {
MeshGradient(
width: 3,
height: 3,
points: [
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5), .init(0.3, 0.5), .init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
],
colors: [
.red, .purple, .indigo,
.orange, .cyan, .blue,
.yellow, .green, .mint
]
)
}
}

#Preview {
MyMesh()
.statusBarHidden()
}

G) Document launch scene (iPadOS)

Document based applications now can have new UI with implementing the DocumentGroup so here when we use the document group we will have the following ui:

Figure 14 — Document Group View

As you can see now we have “Playgrounds title” , “Learn to Code” button and “New App” button on the design.

In addition to this UI elements, we have Background Accessory, Foreground accessories for both hand sides (left & right) as shown in figure 15.

Figure 15 — Document Group Accessories

Example code for Document Groups:

DocumentGroupLaunchScene("Your Lyrics") {
NewDocumentButton()
Button("New Parody from Existing Song") {
// Do something!
}
} background: {
PinkPurpleGradient()
} backgroundAccessoryView: { geometry in
MusicNotesAccessoryView(geometry: geometry)
.symbolEffect(.wiggle(.rotational.continuous()))
} overlayAccessoryView: { geometry in
MicrophoneAccessoryView(geometry: geometry)
}

H) New symbol effects:

Now symbols have new abilities to accept some certaint effects on our applications example usage:

Figure 16 — New Symbol Effect Code Example

Some other new SF symbol effects as follows:

.wiggle effect: That oscillates a symbol in any direction or angle to draw attention

Figure 17 — Wiggle Effect

.breathe effect: Smoothly scales a symbol up and down to indicate the ongoing activity.

Figure 18 — Breathe Effect

.rotate effect: Spins some parts of a symbol around the designated anchor point.

Figure 19 — Rotate Effect

Some of the existing presets have also been enhanced with the new features.

For example: the default “Replace” animation now prefers a new MagicReplace behaviour.

With MagicReplace, symbols smoothly animate badges and slashes.

Figure 20 — Replace Effect

Harnessing the platform:

A) Windowing

New windowing SwiftUI modifiers helps us to add new stype and level to our windows in macOS for example.

Window("Lyric Preview", id: "lyricPreview") {
LyricPreview()
}
.windowStyle(.plain)
.windowLevel(.floating)
.defaultWindowPlacement { content, context in
let displayBounds = context.defaultDisplay.visibleRect
let contentSize = content.sizeThatFits(.unspecified)
return topPreviewPlacement(size: contentSize, bounds: displayBounds)
}
}
Figure 21 — Windowing Style Modifiers

Even now we can add drag gesture to a window:

Figure 22 — Window Drag Gesture
Text(currentLyric)
.background(.thinMaterial, in: .capsule)
.gesture(WindowDragGesture())

This new window modifiers work in visionOS too.

In addition to new window modifiers, we do have a push window action where we can use to open a window and hide the originated window.

Figure 23 — Push Window Example

For more information about windows please refer this link.

B) New SwiftUI Input Methods (visionOS)

SwiftUI gives us many new tools for taking advantage of the unique input methods each platform offers.

For example in visionOs, we can make the views react when people look at them, place a finger near them or move them pointer over them.

Figure 24 — Non-collapsed profile view in visionOS
Figure 25 — Expanded profile view in visionOS

So, when we take consideration above figure 24 and 25 we can see that the profile was non-collapsed and after taking action by user (ie. by looking at the view), the profile image has been expanded and we could see more information about the user.

Within the new hover effect we can control the state of the view and we can adjust the transitions between the active and the inactive.

struct ProfileButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(.thinMaterial)
.hoverEffect(.highlight)
.clipShape(.capsule)
.hoverEffect { effect, isActive, _ in
effect.scaleEffect(isActive ? 1.05 : 1.0)
}
}
}

C) Modifier Key Alternates

ipadOS, macOS and visionOS apps provide keyboard support (as shortcuts)

For example in macOS if we would like to add new shortcut modifier to our application we can do it with few lines of code:

Button("Preview Lyrics in Window") {
// show preview in window
}
.modifierKeyAlternate(.option) {
Button("Preview Lyrics in Full Screen") {
// show preview in full screen
}
}
.keyboardShortcut("p", modifiers: [.shift, .command])

In the main menu of the macOS, lets say we have an item to open a preview window, above code snippet is an example to this usage and we obtain the following functionality shown as below:

Figure 26 — Modifier Keys

D) Pointer interactions

The pointer interactions are another important form of input across many devices.

Figure 27 — Pointer Interactions

The pointer style API lets us customise the appearance and visibility of our system pointer.

Figure 28 — Pointer Style Modifier

With iPadOS 17.5 the SwiftUI support features of Apple Pencil and Apple Pencil Pro, like double tap and squeeze.

With Apple Pencil Squeeze Action now we can gather information from the gesture, and see what action is preferred.

Figure 29 — Apple Pencil Squeeze Gesture

E) Widgets and Live Activities

Now widgets and live activities will appear on Apple watch automatically.

We as developers we can use new .supplementalActivityFamilies modifier to be able to adjust the content on watchOS.

Figure 30 — Tailored Content for WatchOS

And to allow the users to enhance the advance the live activity we can apply double tap, by using .handGestureShortcut Modifier.

Framework Foundations

A) Custom Containers

A new api on ForEach, subviewOf lets us iterate over the subviews of a given view, like in given example below the where each subview is wrapped in it is own cardview.

We can use this to make custom containers that have the same capabilities as SwifUI’s built in containers like list and including mixing static and dynamic content supporting sections, and adding containers to specific modifiers.

Figure 31 — Foreach Subview of for given content
struct DisplayBoard<Content: View>: View {
@ViewBuilder var content: Content

var body: some View {
DisplayBoardCardLayout {
ForEach(subviewOf: content) { subview in
CardView {
subview
}
}
}
.background { BoardBackgroundView() }
}
}

DisplayBoard {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")

ForEach(songsFromSam) { song in
Text(song.title)
}
}

We can use this in order to support sections also such as:

Figure 32 — Sections on new Custom Containers API

B) New Ease of Use Improvement

Entry macro:

Instead of having to write out a full conformance to EnvironmentKey , and an extension on environment values, we can now just simple property with the Entry macro.

Before: We had to write an extension to enviromentKey for the wanted struct to have it available for the SwiftUI project as an environment.

After: Currently we can mark it as @Entry so that SwiftUI will automatically handle the declaration and have it ready to use as an environment variable within our SwiftUI project.

extension EnvironmentValues {
@Entry var karaokePartyColor: Color = .purple
}

Entry macros are not only limited with the Environment values, the entry macro also can be used focus values, transaction and container values.

extension FocusValues {
@Entry var lyricNote: String? = nil
}

extension Transaction {
@Entry var animatePartyIcons: Bool = false
}

extension ContainerValues {
@Entry var displayBoardCardStyle: DisplayBoardCardStyle = .bordered
}

Default accessibility label augmentation:

Currently we are eligible to add additional information to SwiftUI’s controls without overriding the framework provided label. For more information please refer the link.

Xcode previews has a new dynamic linking architecture

That allows us to switch between a preview and build-and-run without needing to rebuild your project, increasing your iteration speed.

And it is now easier to setup Previews too. We can now use state directly in previews using @Previewable macro, eliminating the boilerplate of wrapping your preview content in a view.

New way of working with text and manage selection:

SwiftUI now offers programatic access to, and control of text selection within text editing controls.

Now we can read the properties of the selection, such as the selected ranges.

Figure 33 — Reading Range from InspectorContent

We can use this to show suggested rhymes for selected words in the inspector.

Figure 34 — Showing rhymes for selected word

With new .searchFocused:

We can programatically, drive the focus state of a search field, meaning we can check if a search field is focused on, and programatically move focus to and from the search field.

Text Suggestions:

We can now add new text suggestions to any text field.

With a few lines of code we can provide a suggested texts for the user. The suggestions appear as a drop down menu, and when we pick up an option the textfield updates with the selected completion.

Figure 34 — Textfield suggestions

Example code as follows:

Graphic Capabilities:

Mixing Colors: As a developer we can use mix colours together. A new mix modifier on color blends it with another Colour by a given amount.

Custom Shader Feature:

Custom shaders feature with the ability to precompile shaders before their first use, so we can avoid frame drops by lazy shader compilation.

Scrolling Enhancements:

There are a bunch of new APIs to give us to developers that are fine grained control of our scroll views.

With that said currently we can have a deeper level of integration with the state of a ScrollView with onScrollGeometryChanged, which lets use performantly react to changes in thins like content offsets, contentsize and more. Like with this “back to invitation” button, which appears when we scroll past the top of the scroll view’s contents.

Figure 35 — Back to Invitation Button

We can now detect when a view’s visibility has changed due to scrolling!

Letting us create great experiences centred around content moving on or off screen, like auto-playing audio.

New Swift 6 Language Mode:

Enables compile-time data-race safety, and SwiftUI improved its APIs to make it easier to adopt the new language mode in our apps.

Views in SwiftUI have always been evaluated on the main actor, and the view protocol is now marked with the main actor annotation to reflect that.

That means all types conforming to View, are implicitly isolated to the main actor by default. So if you were explicitly marking your View as main actor, you can remove that annotation without any change in behaviour.

The new Swift 6 language mode is opt-in, so you can take advantage of it whenever you are ready. How to migrate your application to Swift6.

Improved Interoperability:

SwiftUI was designed not just for building brand new apps, but also building new feature in existing apps written with UIKit and AppKit.

Gesture Interoperability

You can now take the any built-in pr custom UIGestureRecognizer and use it in your Swift SwiftUI view hierarchy. It even works SwiftUI views that aren’t directly backed by UIKit.

SwiftUI animations in UIKit and AppKit

UIKit and AppKit can now take the advantage of the power of SwiftUI animations.

Crafting experiences:

New APIs for working with volumes, and immersive spaces, as well as text effects.

Custom Text Renderers:

We can use the following TextRenderer Protocol to highlight a word for example.

KaraokeRenderer makes copy of the text behind the original drawing, which is blurred and has a tint.

--

--