2025 Privacy & SecuritySwiftUI & UI Frameworks
WWDC25 · 26 min · Privacy & Security / SwiftUI & UI Frameworks
What’s new in UIKit
Modernize your app with the latest APIs in UIKit, including enhanced menu bar support, automatic observation tracking, a new UI update method, and improvements to animations. We’ll also cover how you can include SwiftUI scenes in your UIKit app and explore SF Symbols, HDR color pickers, and more.
Watch at developer.apple.com ↗Chapters
- 0:00 — Introduction
- 0:59 — New design system
- 2:29 — Containers and adaptivity
- 3:21 — The menu bar
- 9:58 — Architectural improvements
- 10:21 — Automatic observation tracking
- 12:33 — New UI update method
- 15:45 — Improvements to animations
- 17:45 — Scene updates
- 18:55 — HDR Color support
- 20:38 — Swift notifications
- 21:20 — Migrate to a scene-based life cycle
- 22:40 — OpenURL support for file URLs
- 23:17 — SF Symbols 7
- 25:13 — Next steps
Code shown on screen · 18 snippets
Main menu system configuration
// Main menu system configuration
var config = UIMainMenuSystem.Configuration()
// Declare support for default commands, like printing
config.printingPreference = .included
// Opt out of default commands, like inspector
config.inspectorPreference = .removed
// Configure the Find commands to be a single "Search" element
config.findingConfiguration.style = .search Main menu system build configuration
// Main menu system configuration
// Have the main menu system build using this configuration, and make custom additions.
// Call this early, e.g. in application(_:didFinishLaunchingWithOptions:), and call it once
UIMainMenuSystem.shared.setBuildConfiguration(config) { builder in
builder.insertElements([...], afterCommand: #selector(copy(_:)))
let deleteKeyCommand = UIKeyCommand(...)
builder.replace(command: #selector(delete(_:)), withElements: [deleteKeyCommand])
} Keyboard shortcut repeatability
// Keyboard shortcut repeatability
let keyCommand = UIKeyCommand(...)
keyCommand.repeatBehavior = .nonRepeatable Focus-based deferred menu elements (App Delegate)
// Focus-based deferred menu elements
extension UIDeferredMenuElement.Identifier {
static let browserHistory: Self = .init(rawValue: "com.example.deferred-element.history")
}
// Create a focus-based deferred element that will display browser history
let historyDeferredElement = UIDeferredMenuElement.usingFocus(
identifier: .browserHistory,
shouldCacheItems: false
)
// Insert it into the app’s custom History menu when building the main menu
builder.insertElements([historyDeferredElement], atEndOfMenu: .history) Focus-based deferred menu elements (View Controller)
// Focus-based deferred menu elements
class BrowserViewController: UIViewController {
// ...
override func provider(
for deferredElement: UIDeferredMenuElement
) -> UIDeferredMenuElement.Provider? {
if deferredElement.identifier == .browserHistory {
return UIDeferredMenuElement.Provider { completion in
let browserHistoryMenuElements = profile.browserHistoryElements()
completion(browserHistoryMenuElements)
}
}
return nil
}
} Using an Observable object and automatic observation tracking
// Using an Observable object and automatic observation tracking
class UnreadMessagesModel {
var showStatus: Bool
var statusText: String
}
class MessageListViewController: UIViewController {
var unreadMessagesModel: UnreadMessagesModel
var statusLabel: UILabel
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
statusLabel.alpha = unreadMessagesModel.showStatus ? 1.0 : 0.0
statusLabel.text = unreadMessagesModel.statusText
}
} Configuring a UICollectionView cell with automatic observation tracking
// Configuring a UICollectionView cell with automatic observation tracking
class ListItemModel {
var icon: UIImage
var title: String
var subtitle: String
}
func collectionView(
_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
let listItemModel = listItemModel(for: indexPath)
cell.configurationUpdateHandler = { cell, state in
var content = UIListContentConfiguration.subtitleCell()
content.image = listItemModel.icon
content.text = listItemModel.title
content.secondaryText = listItemModel.subtitle
cell.contentConfiguration = content
}
return cell
} Using automatic observation tracking and updateProperties()
// Using automatic observation tracking and updateProperties()
class BadgeModel {
var badgeCount: Int?
}
class MyViewController: UIViewController {
var model: BadgeModel
let folderButton: UIBarButtonItem
override func updateProperties() {
super.updateProperties()
if let badgeCount = model.badgeCount {
folderButton.badge = .count(badgeCount)
} else {
folderButton.badge = nil
}
}
} Using the flushUpdates animation option to automatically animate updates
// Using the flushUpdates animation option to automatically animate updates
// Automatically animate changes with Observable objects
UIView.animate(options: .flushUpdates) {
model.badgeColor = .red
} Automatically animate changes to Auto Layout constraints with flushUpdates
// Automatically animate changes to Auto Layout constraints
UIView.animate(options: .flushUpdates) {
// Change the constant of a NSLayoutConstraint
topSpacingConstraint.constant = 20
// Change which constraints are active
leadingEdgeConstraint.isActive = false
trailingEdgeConstraint.isActive = true
} Setting up a UIHostingSceneDelegate
// Setting up a UIHostingSceneDelegate
import UIKit
import SwiftUI
class ZenGardenSceneDelegate: UIResponder, UIHostingSceneDelegate {
static var rootScene: some Scene {
WindowGroup(id: "zengarden") {
ZenGardenView()
}
#if os(visionOS)
ImmersiveSpace(id: "zengardenspace") {
ZenGardenSpace()
}
.immersionStyle(selection: .constant(.full),
in: .mixed, .progressive, .full)
#endif
}
} Using a UIHostingSceneDelegate
// Using a UIHostingSceneDelegate
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: "Zen Garden Scene",
sessionRole: connectingSceneSession.role)
configuration.delegateClass = ZenGardenSceneDelegate.self
return configuration
} Requesting a scene
// Requesting a scene
func openZenGardenSpace() {
let request = UISceneSessionActivationRequest(
hostingDelegateClass: ZenGardenSceneDelegate.self,
id: “zengardenspace")!
UIApplication.shared.activateSceneSession(for: request)
} HDR color support
// Create an HDR red relative to a 2.5x peak white
let hdrRed = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0, linearExposure: 2.5) HDR color picking
// Support picking HDR colors relative to a
// maximum peak white of 2x
colorPickerController.maximumLinearExposure = 2.0 Mixing SDR and HDR content
// Mixing SDR and HDR content
registerForTraitChanges([UITraitHDRHeadroomUsageLimit.self]) { traitEnvironment, previousTraitCollection in
let currentHeadroomLimit = traitEnvironment.traitCollection.hdrHeadroomUsageLimit
// Update HDR usage based on currentHeadroomLimit’s value
} Adopting Swift notifications
// Adopting Swift notifications
override func viewDidLoad() {
super.viewDidLoad()
let keyboardObserver = NotificationCenter.default.addObserver(
of: UIScreen.self
for: .keyboardWillShow
) { message in
UIView.animate(
withDuration: message.animationDuration, delay: 0, options: .flushUpdates
) {
// Use message.endFrame to animate the layout of views with the keyboard
let keyboardOverlap = view.bounds.maxY - message.endFrame.minY
bottomConstraint.constant = keyboardOverlap
}
}
} Using a symbol content transition to automatically animate symbol updates
// Using a symbol content transition to automatically animate symbol updates
var configuration = UIButton.Configuration.plain()
configuration.symbolContentTransition = UISymbolContentTransition(.replace) Resources
Related sessions
-
26 min -
18 min -
17 min -
15 min -
36 min -
34 min -
29 min -
23 min -
17 min