WWDC Quick Look 💓 By SwiftGGTeam
What’s new in UIKit

What’s new in UIKit

Watch original video

Highlight

UIKit on iOS 26 wires Swift Observation into update methods like layoutSubviews and updateProperties. Read an @Observable property and UIKit records the dependency and invalidates the view for you. No more manual setNeedsLayout calls.


Core content

UIKit veterans know the dance: change a model, call setNeedsLayout, then read the model again in layoutSubviews to refresh the view. For animations you also have to call layoutIfNeeded() inside the closure or constraint changes won’t animate. The whole dependency graph is hand-maintained. Miss one wire and you get stale or extra updates. UICollectionView cell configuration is a classic landmine.

UIKit on iOS 26 welds Swift Observation directly into the core update pipeline. Methods like layoutSubviews, updateProperties, and a cell’s configurationUpdateHandler will automatically track any @Observable properties they read, and UIKit invalidates the right view when those properties change. The new updateProperties method splits “content and style updates” from “layout math” — it runs before layoutSubviews and is independent of it, so unrelated events no longer trigger a full layout pass. Animations get a new flushUpdates option that flushes pending updates before the animation starts, so you can drop layoutIfNeeded(). Together these moves push UIKit closer to SwiftUI’s declarative model while keeping the existing view tree and performance profile.

Beyond that, iPadOS 26 brings the macOS menu bar to iPad — swipe down from the top to summon it. UISplitViewController now supports an inspector column natively. SwiftUI scenes can be embedded into a UIKit app via UIHostingSceneDelegate. UIColor supports HDR. The notification system gets strongly-typed NotificationCenter.Message. SF Symbols 7 adds draw animations. And one hard requirement: apps built against the latest SDK on versions after iOS 26 must migrate to the UIScene life cycle to launch at all.


Details

Automatic observation tracking: @Observable is enough

UIKit now tracks the @Observable properties you read inside methods like layoutSubviews and updateProperties (10:54).

// Using an Observable object and automatic observation tracking

@Observable 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
    }
}

Key points:

  • UnreadMessagesModel is marked @Observable, so both properties become observable.
  • viewWillLayoutSubviews reads showStatus and statusText; UIKit records the dependency.
  • When either property changes, UIKit invalidates the view and runs viewWillLayoutSubviews again.
  • No KVO, no Combine, no hand-rolled setNeedsLayout.
  • You can back-deploy to iOS 18 by adding UIObservationTrackingEnabled to Info.plist.

updateProperties: separate content updates from layout

The new updateProperties method runs before layoutSubviews and is meant for configuring content, style, and behavior (13:27).

// Using automatic observation tracking and updateProperties()

@Observable 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
        }
   }
}

Key points:

  • updateProperties runs after trait updates and before layoutSubviews.
  • This example only configures the badge content. When badgeCount changes, only this block reruns — there’s no full layout pass.
  • You can read the trait collection (already updated) and you can invalidate layout from inside; UIKit will run layout right after.
  • Trigger it manually with setNeedsUpdateProperties.
  • It complements layoutSubviews rather than replacing it: content and style go here, geometry goes in layoutSubviews.

flushUpdates: animations flush updates for you

UIView animations on iOS 26 get a .flushUpdates option that applies pending invalidations before and after the animation (16:57).

// Using the flushUpdates animation option to automatically animate updates

// Automatically animate changes with Observable objects
UIView.animate(options: .flushUpdates) {
    model.badgeColor = .red
}

Key points:

  • The closure only mutates @Observable properties — no layoutIfNeeded().
  • UIKit flushes pending updates once before the animation starts and once when it ends.
  • Views that depend on the property are pulled into the animation automatically.
  • The same trick works for Auto Layout: change a constant or isActive inside the closure and dependent views animate to their new positions.

iPadOS 26 introduces the menu bar. Alongside UIMenuBuilder, the new UIMainMenuSystem.Configuration lets you trim default commands declaratively (4:56).

// 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

Key points:

  • printingPreference = .included opts the app into the system print command set.
  • inspectorPreference = .removed turns off the new inspector toggle command.
  • findingConfiguration.style = .search collapses the Find command family into a single Search — better for image and music apps than Find Next/Previous.
  • Call this once in application(_:didFinishLaunchingWithOptions:). Setting the configuration triggers a menu rebuild.
  • Heads-up: starting in iOS 26, menus from storyboards no longer load — you must build them in code.

Focus-based deferred menu element

Menu content like History depends on the current focus (which browsing profile, for example) and can’t be baked into the main menu build (8:06).

// 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
    }
}

Key points:

  • In the App Delegate, create a placeholder with UIDeferredMenuElement.usingFocus(identifier:shouldCacheItems:) and insert it into the main menu.
  • The view controller overrides provider(for:) and returns a UIDeferredMenuElement.Provider for the matching identifier. UIKit walks up the responder chain until it finds one that can supply content.
  • The win: menus fill on demand, so a profile switch doesn’t rebuild the entire main menu.

Embedding SwiftUI scenes in a UIKit app

UIHostingSceneDelegate lets a UIKit app host SwiftUI’s WindowGroup and visionOS’s ImmersiveSpace (18:07).

// 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 
    }
}

Key points:

  • rootScene is a SwiftUI Scene and can hold multiple WindowGroups or ImmersiveSpaces.
  • Set delegateClass to this class inside application(_:configurationForConnecting:options:) to wire it up.
  • Use UISceneSessionActivationRequest(hostingDelegateClass:id:) to request a specific scene programmatically — for example, to open a visionOS immersive space.
  • Good fit for UIKit apps adopting SwiftUI step by step, and for any app that needs visionOS immersive spaces.

Strongly-typed NotificationCenter.Message

NotificationCenter.default.addObserver(of:for:) now returns a strongly-typed message — no more userInfo dictionaries (20:54).

// 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
        }
    }
}

Key points:

  • for: .keyboardWillShow is a strongly-typed NotificationCenter.Message name.
  • message.animationDuration and message.endFrame give you the keyboard animation duration and end frame directly — no userInfo[UIResponder.keyboardFrameEndUserInfoKey] lookup-and-cast.
  • Combine it with .flushUpdates and your constraint changes ride the keyboard animation.

Takeaways

1. Refactor cell configuration onto configurationUpdateHandler + @Observable

Why it pays off: cell configuration is usually scattered across cellForItemAt, prepareForReuse, and KVO callbacks, and keeping state in sync is easy to break. configurationUpdateHandler now supports automatic observation tracking, so you define it once and it updates itself (11:48).

How to start: turn the cell’s model into an @Observable class. After dequeue in cellForItemAt, assign configurationUpdateHandler. Inside the handler, read the model’s properties to build a UIListContentConfiguration. Property changes will reconfigure the cell automatically — no setNeedsConfigurationUpdate needed.

2. Move “content and style” code from layoutSubviews to updateProperties

Why it pays off: layoutSubviews runs on every resize, scroll, and subview change. Mixing badge counts, text, and colors into it does a lot of pointless work. The new updateProperties runs only when content invalidates, so the work is cheaper and the responsibilities are cleaner.

How to start: walk through your existing viewWillLayoutSubviews and layoutSubviews code. Move every “read model, set property” line into override func updateProperties(). Leave layout math (frames, constraint constants) where it is. Call setNeedsUpdateProperties() when you need to trigger it by hand.

3. Start the UIScene migration now

Why it pays off: apps built against the latest SDK on versions after iOS 26 will not launch unless they have moved to the UIScene life cycle. Many older UIApplicationDelegate callbacks and UIWindow initializers are already deprecated (21:19).

How to start: read Apple’s tech note Migrating to the UIKit scene-based life cycle. Begin by adding UIApplicationSceneManifest to Info.plist. Even single-window apps need to migrate. Move UI setup out of application(_:didFinishLaunchingWithOptions:) and into UISceneDelegate.scene(_:willConnectTo:options:). Switch UIWindow to init(windowScene:).

4. Move keyboard and notification code to strongly-typed NotificationCenter.Message

Why it pays off: the old keyboardWillShowNotification plus userInfo dictionary lookups read poorly, the keys are easy to misspell, and you have to cast values out by hand. The strongly-typed message hands you animationDuration, endFrame, and friends as real properties, checked at compile time.

How to start: replace addObserver(forName:object:queue:using:) with addObserver(of:for:). Change the callback to (message) in and read message.xxx directly. Add .flushUpdates to the animation closure and your constraint changes ride the keyboard animation.


Comments

GitHub Issues · utterances