64 min read
What's New in UIKit
Learn what's new in UIKit in the iOS 26 SDK.

WWDC25 is long since over and, as per recent years, UIKit is far from the star of the show. What was once the new and shiny thing is no longer, but that’s not to say it didn't receive plenty of new and shiny additions. In fact, that couldn't be further from the truth!

As always, Apple has done a good job of documenting the big changes to UIKit. This year’s What’s New in UIKit session is well worth tuning in to! However, there's only so much you can fit into an easily digestible dub dub session and, at the time of writing, even Apple's UIKit updates article barely scratches the surface of this year's changes!

So, for my fellow UIKit-enjoyers out there, here's my first (and maybe annual?) comprehensive list of changes to UIKit in iOS 19 26...

UIAction

Identifier

UIAction, specifically UIAction.Identifier, has received a small quality of life change with a new constant, newFromPasteboard.

static let newFromPasteboard: UIAction.Identifier

This joins the small handful of predefined action identifiers available for common system tasks: paste, pasteAndGo, pasteAndMatchStyle and pasteAndSearch... As creating a new document from the current contents of the pasteboard is an increasingly popular UX paradigm, it's a welcome addition.

UIApplicationDelegate

In the run-up to WWDC, there were rumours circulating online about a potential UIApplicationDelegate deprecation, stemming from a blog post highlighting some interesting code committed to Apple's open-source WebKit repo.

The assumption was that developers would have to move towards the SwiftUI app lifecycle, backed by a class called SwiftUI.AppDelegate that conformed to the UIApplicationDelegate protocol.

Given a moment of critical thinking, you might have arrived at the more sensible conclusion that Apple might be deprecating a small handful of UIApplicationDelegate's methods in favour of their modern UISceneDelegate and UIWindowSceneDelegate equivalents...

Anyway, Apple have deprecated a small handful of UIApplicationDelegate's method in favour of their modern UISceneDelegate and UIWindowSceneDelegate equivalents!

UIApplicationDelegate DeprecationUISceneDelegate Equivalent
applicationDidBecomeActive(_:)sceneDidBecomeActive(_:)
applicationWillResignActive(_:)sceneWillResignActive(_:)
applicationDidEnterBackground(_:)sceneDidEnterBackground(_:)
applicationWillEnterForeground(_:)sceneWillEnterForeground(_:)
application(_:willContinueUserActivityWithType:)scene(_:willContinueUserActivityWithType:)
application(_:continue:restorationHandler:)scene(_:continue:)
application(_:didUpdate:)scene(_:didUpdate:)
application(_:didFailToContinueUserActivityWithType:error:)scene(_:didFailToContinueUserActivityWithType:error:)
application(_:open:options:)scene(_:openURLContexts:)
UIApplicationDelegate DeprecationUIWindowSceneDelegate Equivalent
application(_:performActionFor:completionHandler:)windowScene(_:performActionFor:completionHandler:)
application(_:userDidAcceptCloudKitShareWith:)windowScene(_:userDidAcceptCloudKitShareWith:)

These deprecations shouldn't come as too much of a surprise as Apple have been pushing UISceneDelegate and UIWindowSceneDelegate for a while. With a bit of luck, you've already migrated and can focus your efforts on Liquid Glass-ifying your app instead!

UIBackgroundExtensionView

UIBackgroundExtensionView is a new UIView subclass used for extending your app's rich content, like header images, into a parent view's unsafe areas, such as that below a UISplitViewController' sidebar.

class UIBackgroundExtensionView: UIView {
    var contentView: UIView? { get set }
    var automaticallyPlacesContentView: Bool { get set }
}

You provide the background extension view with a contentView, such as a UIImageView, and the background extension view seamlessly extends the content into the unsafe areas without clipping the content itself. It does this with a handful of clever visual effects, such as tiling and flipping your content view and applying a number of subtle blurs.

let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(resource: .iOS26Wallpaper)
let backgroundExtensionView = UIBackgroundExtensionView()
backgroundExtensionView.contentView = imageView
backgroundExtensionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(backgroundExtensionView)
NSLayoutConstraint.activate([
    backgroundExtensionView.topAnchor.constraint(equalTo: view.topAnchor),
    backgroundExtensionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    backgroundExtensionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    backgroundExtensionView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.4)
])

The background extension view also gives you the option to layout your content view manually by setting its automaticallyPlacesContentView property to false. You can set the content view's position using Auto Layout or by setting its frame.

backgroundExtensionView.automaticallyPlacesContentView = false

See the example below where the top of the image view is constrained to the top of the background extension view rather than to the top of the background extension view's safe area layout guide.

UIBarButtonItem

UIBarButtonItem has received a number of nice changes to support their new Liquid Glass appearance, the first being a new property, sharesBackground.

var sharesBackground: Bool { get set }

In iOS 26, adjacent bar buttons are grouped with a shared Liquid Glass background. Using this property, you can opt out of this behaviour on a per-button basis. See the examples below where, on the left, the bar buttons share a background and, on the right, they don't.

let plusBarButtonItem = UIBarButtonItem()
plusBarButtonItem.sharesBackground = false
plusBarButtonItem.image = UIImage(systemName: "plus")
let minusBarButtonItem = UIBarButtonItem()
minusBarButtonItem.image = UIImage(systemName: "minus")
navigationItem.rightBarButtonItems = [plusBarButtonItem, minusBarButtonItem]

It's worth noting that, as per the example above, only one bar button item needs to opt out of the sharesBackground property for the effect to take place.

Alternatively, for the same effect, you can use the new static fixedSpace() convenience method to initialise an empty bar button item with a fixed width of zero.

class func fixedSpace() -> UIBarButtonItem

Just sandwich one of these between your bar button items that you don't want to share a background, and you're good to go!

On the topic of backgrounds, if you have a more custom design system in your app, you might want to opt out of the Liquid Glass background altogether. In which case, the new hidesSharedBackground property is your friend!

var hidesSharedBackground: Bool { get set }
let closeBarButtonItem = UIBarButtonItem()
closeBarButtonItem.hidesSharedBackground = true
closeBarButtonItem.image = UIImage(systemName: "xmark")
navigationItem.rightBarButtonItem = closeBarButtonItem

UIBarButtonItem also has a new identifier property that we can customise to influence the new shared transition of bar buttons between navigation bars and toolbars.

var identifier: String? { get set }

Under the hood, UIKit uses various heuristics to determine if two instances of UIBarButtonItem are "the same" across two screens and, therefore, whether or not they should animate between states. However, it can't always get this right. One such example is when you're using a customView for your bar button. See the example below where two visually identical bar buttons animate across navigation controller transitions.

Setting both bar button items' identifier property to a mutual string fixes this.

let label = UILabel()
label.text = "Label"
let barButtonItem = UIBarButtonItem(customView: label)
barButtonItem.identifier = "com.example.label-custom-view"
navigationItem.rightBarButtonItem = barButtonItem

Finally, UIBarButtonItem has a new badge property.

var badge: UIBarButtonItem.Badge? { get set }

This property serves to provide similar functionality to UITabBarItem's badgeValue property, but is far more customisable, allowing us to customise not only the badge value, but also the font, foreground colour, and background colour!

It's worth noting that, at the time of writing, bar button badges are only supported in navigation bars - not tool bars.

Badge

The new UIBarButtonItem.Badge structure is a really nice new API for describing the appearance of a bar button item's badge value. It's incredibly flexible, far more so than UITabBarItem's equivalent API.

extension UIBarButtonItem {
    struct Badge: Equatable {
        var foregroundColor: UIColor? { get set }
        var backgroundColor: UIColor? { get set }
        var stringValue: String? { get }
        var font: UIFont? { get set }
        static func count(_ count: Int) -> UIBarButtonItem.Badge
        static func string(_ string: String) -> UIBarButtonItem.Badge
        static func indicator() -> UIBarButtonItem.Badge
    }
}

You don't initialise an instance of the new Badge struct yourself. Instead, you can use one of the convenient static methods such as count(_:), string(_:) or indicator().

Once created, you can customise the font, foregroundColor and backgroundColor properties as needed.

Style

UIBarButtonItem.Style has been updated with a new prominent case, resulting in a bar button with a more eye-catching appearance, tinted with your app's accent colour by default.

case prominent

This serves as a replacement to the old done case which has been deprecated in iOS 26.

let doneBarButtonItem = UIBarButtonItem()
doneBarButtonItem.style = .prominent
doneBarButtonItem.image = UIImage(systemName: "checkmark")
navigationItem.rightBarButtonItem = doneBarButtonItem

SystemItem

The UIBarButtonItem.SystemItem enumeration has received a new writingTools case.

case writingTools

This new system item allows us to add a Writing Tools bar button in navigation bars or tools bars, without having to use private or custom SF Symbols!

UIBarButtonItemGroup

UIBarButtonItemGroup has been updated with a convenient way of initialising a new group with a single, empty bar button item of a fixed width of zero, via the new fixedSpace() class function.

class func fixedSpace() -> UIBarButtonItemGroup

This serves the same purpose as the method under the same of UIBarButtonItem, to visually separate bar buttons. There's no special going on under the hood. In fact, it's just shorthand for...

let barButtonItems: [UIBarButtonItem] = [.fixedSpace()]
let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil)

UIButton

Configuration

UIButton.Configuration has received several small but important additions this year. The first being several new, static methods for creating Liquid Glass buttons: glass(), clearGlass() prominentGlass() and prominentClearGlass().

extension UIButton {
    struct Configuration: Hashable {
        static func glass() -> UIButton.Configuration
        static func clearGlass() -> UIButton.Configuration
        static func prominentGlass() -> UIButton.Configuration
        static func prominentClearGlass() -> UIButton.Configuration
    }
}
let button = UIButton()
button.configuration = .glass()
button.configuration?.title = "Glass"

The second update to UIButton.Configuration will have you wondering, "Why wasn't this added years ago?" Apple have finally added a symbolContentTransition property!

var symbolContentTransition: UISymbolContentTransition? { get set }

The value of this property should be an instance of the new UISymbolContentTransition class.

let button = UIButton()
button.configuration = .prominentGlass()
button.configuration?.symbolContentTransition = UISymbolContentTransition(.replace)

The symbol content transition will take place automatically whenever you set the button configuration's image property.

UIColor

It's been a long time coming, what with Apple's displays supporting HDR for many years, but UIColor has finally been updated with initialisers to construct HDR colours!

Firstly, init(red:green:blue:alpha:exposure:).

init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat, exposure: CGFloat)

Simply call this initialiser, passing in the red, green, blue, and alpha values as normal, but also provide an exposure value. UIKit multiplies this value to the power of 2, meaning an exposure value of 1 makes the colour twice as bright as the SDR's peak white.

Secondly, init(red:green:blue:alpha:linearExposure:).

init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat, linearExposure: CGFloat)

Much like its (non-linear) exposure counterpart, call this initialiser as you would normally, with RGBA values, this time passing in a linearExposure value. UIKit doesn't do anything under the hood to compute the brightness here, so a value of 1 would result in a colour one times the brightness of the SDR's peak white, and a value of 2 would result in a colour two times as bright.

To get the exposure of an HDR colour after the fact, use the new linearExposure property.

var linearExposure: CGFloat { get }

A new property of UIColor, standardDynamicRange, will return an instance of the same UIColor but in the SDR colour space.

var standardDynamicRange: UIColor { get }

Finally, on the UIColor-front, there's a new applyingContentHeadroom(_:) method. This method changes how bright the colour is perceived in HDR by applying a new content headroom (how much "extra brightness" the colour has above the SDR's peak white).

func applyingContentHeadroom(_ contentHeadroom: CGFloat) -> UIColor

It's worth noting that the colour returned by this method maintains the original colour's RGBA components.

UIColorPickerViewController

On the topic of colours, UIColorPickerViewController has been updated with a new maximumLinearExposure property to support HDR colours.

var maximumLinearExposure: CGFloat { get set }

Setting this property to a value of anything greater than 1 will surface a new HDR Boost slider to the user for them to customise the exposure of the selected colour.

There's also a new-ish property of UIColorPickerViewController that's been added this year, supportsEyedropper. I say "new-ish" as this was added in iOS 18 as private API but, in iOS 26, is exposed to us mere mortals!

var supportsEyedropper: Bool { get set }

This is another one of those "you see what you get" APIs. Setting this property to false disables the colour picker's eyedropper tool, removing the button from the top, leading edge of the UI.

The default value of this property is true.

UIColorWell

UIColorWell has also received the same treatment as UIColorPickerViewController. Firstly, with a new maximumLinearExposure property.

var maximumLinearExposure: CGFloat { get set }

And secondly, with a supportsEyedropper property.

var supportsEyedropper: Bool { get set }

Because UIColorWell presents a UIColorPickerViewController, the result of these APIs is identical to that of colour picker view controller's equivalents, documented above.

UICornerConfiguration

This is one I'm incredibly excited about - UICornerConfiguration is a new structure, added to support UIView's new cornerConfiguration API, representing the corner radii of a view.

struct UICornerConfiguration: Equatable, Hashable, CustomStringConvertible {
    static func corners(radius: UICornerRadius) -> UICornerConfiguration
    static func corners(topLeftRadius: UICornerRadius?, topRightRadius: UICornerRadius?, bottomLeftRadius: UICornerRadius?, bottomRightRadius: UICornerRadius?) -> UICornerConfiguration
    static func capsule(maximumRadius: Double? = nil) -> UICornerConfiguration
    static func uniformCorners(radius: UICornerRadius) -> UICornerConfiguration
    static func uniformEdges(topRadius: UICornerRadius, bottomRadius: UICornerRadius) -> UICornerConfiguration
    static func uniformEdges(leftRadius: UICornerRadius, rightRadius: UICornerRadius) -> UICornerConfiguration
    static func uniformTopRadius(_ topRadius: UICornerRadius, bottomLeftRadius: UICornerRadius? = nil, bottomRightRadius: UICornerRadius? = nil) -> UICornerConfiguration
    static func uniformBottomRadius(_ bottomRadius: UICornerRadius, topLeftRadius: UICornerRadius? = nil, topRightRadius: UICornerRadius? = nil) -> UICornerConfiguration
    static func uniformLeftRadius(_ leftRadius: UICornerRadius, topRightRadius: UICornerRadius? = nil, bottomRightRadius: UICornerRadius? = nil) -> UICornerConfiguration
    static func uniformRightRadius(_ rightRadius: UICornerRadius, topLeftRadius: UICornerRadius? = nil, bottomLeftRadius: UICornerRadius? = nil) -> UICornerConfiguration
}

It might not sound exciting at first, but it unlocks a host of possibilities that were previously a bit of a ball-ache with UIKit! Let's go over a few of these configurations...

First, corners(radius:). This is the simplest of the static methods defined on UICornerConfiguration, applying the specified radius to all corners.

let rectangleView = UIView()
rectangleView.backgroundColor = .systemBlue
rectangleView.cornerConfiguration = .corners(radius: .fixed(26))

In this instance, we passed in a fixed(_:) corner radius of 26 points - check out UICornerRadius for more possibilities!

UICornerConfiguration also has a static corners(topLeftRadius:topRightRadius:bottomLeftRadius:bottomRightRadius:) method which allows you to specify independent radii for each corner.

rectangleView.cornerConfiguration = .corners(
    topLeftRadius: .fixed(26),
    topRightRadius: .fixed(10),
    bottomLeftRadius: .fixed(10),
    bottomRightRadius: .fixed(26)
)

The final configuration we'll look at is capsule(maximumRadius:)! Note the maximumRadius parameter has a default value of nil, so you can omit it entirely if you're not using it.

rectangleView.cornerConfiguration = .capsule()

No more overriding layoutSubviews() to create a capsule-like view!

UICornerRadius

Above, I mentioned that I was pretty excited about UIKit's new corner configuration API - let's take a look at why!

The new UICornerRadius struct represents a radius of a rounded corner. As you've seen, this can be used to round the corners of views uniformly or independently.

rectangleView.cornerConfiguration = .corners(radius: .fixed(26))
rectangleView.cornerConfiguration = .corners(topLeftRadius: .fixed(26), topRightRadius: .fixed(52), bottomLeftRadius: .fixed(52), bottomRightRadius: .fixed(26))

However, there's more to this API!

struct UICornerRadius: Equatable, Hashable, ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, CustomStringConvertible {
    static func fixed(_ radius: Double) -> UICornerRadius
    static func containerConcentric(minimum: CGFloat? = nil) -> UICornerRadius
}

As you can see, there is a static containerConcentric(minimum:) method, alongside fixed(_:). This is where it gets good.

Using a containerConcentric(minimum:) radius for your view's corner configuration will automatically calculate a corner radius concentric to the view's superview, in a similar fashion to SwiftUI's ContainerRelativeShape.

let containerView = UIView()
containerView.backgroundColor = .systemBlue
containerView.cornerConfiguration = .corners(radius: .fixed(26))
let concentricView = UIView()
concentricView.backgroundColor = .systemCyan
concentricView.cornerConfiguration = .corners(radius: .containerConcentric())
view.addSubview(containerView)
containerView.addSubview(concentricView)

But that's not all! By far the best feature is corners that adapt to the shape of the window scene and, by extension, the shape of the display itself!

let concentricView = UIView()
concentricView.backgroundColor = .systemBlue
concentricView.cornerConfiguration = .corners(radius: .containerConcentric())

This has, of course, been possible for some time, albeit a little manually, with UIScreen's private _displayCornerRadius property. However, this is a fantastic example of Apple taking an API, that was previously behind closed doors, and transforming it into something that is worth more than the sum of its parts. They could have dropped the underscore, exposed _displayCornerRadius, and called it a day. Instead, what we got is an incredibly flexible API with deep integration throughout system components such as sheets, popovers and window scenes!

UIContextMenuSystem

UIContextMenuSystem is a new class that's been added in iOS 26 that does...nothing? OK, not quite nothing, but nothing new!

UIContextMenuSystem inherits from UIMenuSystem so you can access the usual class properties like main and context, and methods like setNeedsRebuild() and setNeedsRevalidate().

The only member of UIContextMenuSystem itself is its shared property for accessing the singleton instance.

class var shared: UIContextMenuSystem { get }

In fact, calling UIContextMenuSystem.shared is exactly the same as calling UIMenuSystem.context.

I believe this class has been added to mirror the new UIMainMenuSystem class which does offer a subset of APIs for managing the main menu.

UIDeferredMenuElement

As part of UIKit's slew of changes to support iPadOS's new macOS-like menu bar, UIDeferredMenuElement has been updated to support focus-based menus.

There's a new static usingFocus(identifier:shouldCacheItems:) method which, when inserted into the main menu system, populates its elements from the UIResponder chain.

let historyIdentifier = UIDeferredMenuElement.Identifier(rawValue:"com.example.deferred-history-element")
let historyMenuElement = UIDeferredMenuElement.usingFocus(identifier: historyIdentifier, shouldCacheItems: false)
builder.insertElements([historyMenuElement], atEndOfMenu: .view)

You can populate this menu by overriding the new provider(for:) method in your UIResponder subclass and returning an instance of UIDeferredMenuElement.Provider.

To support this behaviour, UIDeferredMenuElement has a new read-only identifier property of type UIDeferredMenuElement.Identifier.

open var identifier: UIDeferredMenuElement.Identifier { get }

Identifier

UIDeferredMenuElement.Identifier is a simple structure representing a unique identifier for a deferred menu element.

extension UIDeferredMenuElement {
    struct Identifier: Hashable, Equatable, RawRepresentable {
        init(_ rawValue: String)
        init(rawValue: String)
    }
}

Simply create an instance of this using either the init(_:) or init(rawValue:) initialisers and pass this into the static usingFocus(identifier:shouldCacheItems:) method.

Provider

Finally, on the UIDeferredMenuElement front, is a new class, Provider.

extension UIDeferredMenuElement {
    class Provider: NSObject {
        convenience init(_ elementProvider: @escaping (@escaping ([UIMenuElement]) -> Void) -> Void)
    }
}

This class doesn't have any public members, other than its convenience init(_:) initialiser.

let provider = UIDeferredMenuElement.Provider { completion in
    let elements: [UIMenuElement] = // ...
    completion(elements)
}

You'll return an instance of this class when overriding provider(for:) in your UIResponder subclass.

UIDocumentBrowserAction

UIDocumentBrowserAction is an API that allows you to provide custom actions in a UIDocumentBrowserViewController. A custom action can appear in the document browser's navigation bar when the user enters into the Select mode, or context menus when the user long presses on a document or folder.

You're able to specify a title, image, and where you want this action to be available, i.e. the navigation bar and/or context menu. If you provide both a title and an image, only your image is shown in the navigation bar button, meaning the result of your action may not be entirely clear to users.

To remedy this, Apple have introduced a new imageOnlyForContextMenu property to UIDocumentBrowserAction.

var imageOnlyForContextMenu: UIImage? { get set }

This image will only be displayed alongside your custom action's title in context menus, falling back to your title in the navigation bar.

UIGlassEffect

In iOS 26, Liquid Glass really is the star of the show. UIGlassEffect is one of the main APIs that allow developers to take advantage of these new effects.

class UIGlassEffect: UIVisualEffect {
    var isInteractive: Bool { get set }
    var tintColor: UIColor? { get set }
}

UIGlassEffect is a UIVisualEffect subclass, much like UIBlurEffect and UIVibrancyEffect. This means you'll (almost) always be using it alongside UIVisualEffectView.

You create an instance of UIGlassEffect using the init(style:) initialiser. This style parameter should be one of a couple of constants defined in the UIGlassEffect.Style class.

let glassEffect = UIGlassEffect(style: .regular)
let visualEffectView = UIVisualEffectView(effect: glassEffect)
let glassEffect = UIGlassEffect(style: .clear)
let visualEffectView = UIVisualEffectView(effect: glassEffect)

From here, you can customise the tint colour of the glass and whether or not the glass is interactive.

let glassEffect = UIGlassEffect(style: .regular)
glassEffect.tintColor = .tintColor
glassEffect.isInteractive = true

The tint colour does exactly what you'd expect, it gives you a sort of stained glass effect. Whether or not the effect is interactive is little more subtle. If true, the glass expands and various highlights are applied when the user taps the glass.

Style

UIGlassEffect.Style is a new enumeration, supporting iOS 26's UIGlassEffect API, defining a set of glass styles.

extension UIGlassEffect {
    public enum Style: Int {
        case regular = 0
        case clear = 1
    }
}

As of now, there are two styles available: regular and clear.

UIHDRHeadroomUsageLimit

UIHDRHeadroomUsageLimit is a simple enumeration supporting UITraitCollection's new hdrHeadroomUsageLimit property and the new UITraitHDRHeadroomUsageLimit trait definition.

enum UIHDRHeadroomUsageLimit: Int {
    case unspecified = -1
    case active = 0
    case inactive = 1
}

These enum cases describes the current HDR headroom limitations of a view hierarchy. Specifically, whether or not a view's content should fall back to SDR content to better emphasize overlaid foreground content, such as a popover.

The unspecified case conveys a lack of HDR headroom limitation information, you'll see this case before your UIView subclass moves to its superview. The active case indicates that headroom usage limits are in effect and that your content should fall back to SDR. Finally, the inactive indicates that headroom usage limits are disabled and that your content can be displayed in HDR.

UIImage

SymbolConfiguration

UIImage.SymbolConfiguration has been updated with two new initialisers to support new symbol effects added in SF Symbols 7.

The first of the two is init(colorRenderingMode:) that takes a single argument of the new type UIImage.SymbolColorRenderingMode.

convenience init(colorRenderingMode mode: UIImage.SymbolColorRenderingMode)

This has been added to support SF Symbols' new gradient rendering mode. To get up and running, call init(colorRenderingMode:) with the gradient symbol colour rendering mode case.

let image = UIImage(systemName: "bolt.house.fill")?.withConfiguration(
    UIImage.SymbolConfiguration(colorRenderingMode: .gradient)
        .applying(
            UIImage.SymbolConfiguration(paletteColors: [.systemOrange])
        )
)

The second new initialiser is init(variableValueMode:). This takes a single argument of the new type UIImage.SymbolVariableValueMode.

convenience init(variableValueMode: UIImage.SymbolVariableValueMode)

This has been added to support the new draw effect added in SF Symbols 7. To use it, construct a symbol configuration with the init(variableValueMode:) initialiser, passing in the draw variable value mode case.

let image = UIImage(systemName: "command")?.withConfiguration(
    UIImage.SymbolConfiguration(variableValueMode: .draw)
)

SymbolColorRenderingMode

UIImage.SymbolColorRenderingMode is a new enumeration supporting SF Symbols 7's new gradient rendering mode - specifically, UIImage.SymbolConfiguration's new init(colorRenderingMode:) initialiser.

extension UIImage {
    enum SymbolColorRenderingMode: Int {
        case automatic = 0
        case flat = 1
        case gradient = 2
    }
}

This enum allows you to specify a symbol with a normal flat colour rendering mode, a gradient rendering mode, or the symbol's automatic rendering mode - either flat or gradient.

SymbolVariableValueMode

UIImage.SymbolVariableValueMode is a new enumeration supporting SF Symbols 7's new draw effect. You can use one of these cases to pass in to UIImage.SymbolConfiguration's new init(variableValueMode:) initialiser.

extension UIImage {
    enum SymbolVariableValueMode: Int {
        case automatic = 0
        case color = 1
        case draw = 2
    }
}

The color case will give you the existing variable colour effect which animates the symbol layer by layer depending on a given threshold (think Wi-Fi bars), draw will animate the symbol along its components paths as if it were actually being drawn by hand, and automatic will default to the symbol's preferred animation.

UIMainMenuSystem

UIMainMenuSystem is a new UIMenuSystem subclass in iOS 26 to support iPadOS' new macOS-like menu bar.

Like its superclass, you don't create an instance of UIMainMenuSystem yourself. Instead, you access a singleton instance through the class shared property.

class var shared: UIMainMenuSystem { get }

UIMainMenuSystem has a single member, setBuildConfiguration(_:buildHandler:).

func setBuildConfiguration(_ configuration: UIMainMenuSystem.Configuration, buildHandler: ((any UIMenuBuilder) -> Void)? = nil)

Calling this method allows you to apply a new UIMainMenuSystem.Configuration and, optionally, provide a closure, called by the system, to customise an instance of UIMenuBuilder in a similar fashion to UIResponder's buildMenu(with:) method.

let configuration = UIMainMenuSystem.Configuration()
UIMainMenuSystem.shared.setBuildConfiguration(configuration) { menuBuilder in
    menuBuilder.remove(menu: .file)
}

In the example below, the system ran the provided buildHandler closure, removing the default File menu from the main menu.

Configuration

UIMainMenuSystem.Configuration is a new class that groups a bunch of properties for customising iPadOS' new main menu.

extension UIMainMenuSystem {
    class Configuration: NSObject {
        var newScenePreference: UIMenuSystem.ElementGroupPreference { get set }
        var documentPreference: UIMenuSystem.ElementGroupPreference { get set }
        var printingPreference: UIMenuSystem.ElementGroupPreference { get set }
        var findingPreference: UIMenuSystem.ElementGroupPreference { get set }
        var findingConfiguration: UIMenuSystem.FindElementGroupConfiguration { get }
        var toolbarPreference: UIMenuSystem.ElementGroupPreference { get set }
        var sidebarPreference: UIMenuSystem.ElementGroupPreference { get set }
        var inspectorPreference: UIMenuSystem.ElementGroupPreference { get set }
        var textFormattingPreference: UIMenuSystem.ElementGroupPreference { get set }
    }
}

The new configuration class provides several properties, of type UIMenuSystem.ElementGroupPreference, for easily including or removing groups of menu elements.

let configuration = UIMainMenuSystem.Configuration()
configuration.newScenePreference = .removed
UIMainMenuSystem.shared.setBuildConfiguration(configuration)

Functionally, this is very similar to using UIMainMenuSystem's setBuildConfiguration(_:buildHandler:) method and manually calling remove(menu: .newItem) on the menu builder passed into the closure - but it's a nice shorthand nonetheless.

Also new in this configuration class is the findingConfiguration property, of type UIMenuSystem.FindElementGroupConfiguration. This class contains a single property, style, of type UIMenuSystem.FindElementGroupConfiguration.Style - an enumeration with a few cases describing the subset of controls displayed in the system Find menu.

The first case we'll go over, is search. This replaces the Find and Replace menu with a single Search menu element, ideal for photo or music apps where the focus is on searching content rather than text.

let configuration = UIMainMenuSystem.Configuration()
configuration.findingConfiguration.style = .search
UIMainMenuSystem.shared.setBuildConfiguration(configuration)

Up next is the nonEditableText case which displays a Find (no replace) menu.

There's also an editableText case for displaying a Find and Replace menu!

And, finally, an automatic case which allows the system to decide based on..."platform and system behaviours" according to the documentation!

UIMenu

Configuration

The UIMenu.Identifier structure provides a set of constants to identify system menu items.

In iOS 26, the newScene identifier has been deprecated in favour of a more generic newItem identifier.

static let newItem: UIMenu.Identifier

And a new findPanel identifier has been added for menus containing children such as Find, Find and Replace, Find Next, and Find Previous.

static let findPanel: UIMenu.Identifier

UIMenuBuilder

With the introduction of UIMainMenuSystem, supporting iPadOS' new macOS-like menu bar, the UIMenuBuilder protocol is going to become essential to a whole lot more apps!

Because of this, Apple have added a bunch of new methods to the protocol that make configuring menus a hell of a lot easier! For the sake of brevity, I won't go over them all individually but they're pretty self-explanatory.

func replace(menu replacedIdentifier: UIMenu.Identifier, with replacementElements: [UIMenuElement])
func replace(action replacedIdentifier: UIAction.Identifier, with replacementElements: [UIMenuElement])
func replace(command replacedAction: Selector, propertyList replacedPropertyList: Any? = nil, with replacementElements: [UIMenuElement])
func insertElements(_ insertedElements: [UIMenuElement], beforeMenu siblingIdentifier: UIMenu.Identifier)
func insertElements(_ insertedElements: [UIMenuElement], afterMenu siblingIdentifier: UIMenu.Identifier)
func insertElements(_ insertedElements: [UIMenuElement], beforeAction siblingIdentifier: UIAction.Identifier)
func insertElements(_ insertedElements: [UIMenuElement], afterAction siblingIdentifier: UIAction.Identifier)
func insertElements(_ childElements: [UIMenuElement], atStartOfMenu parentIdentifier: UIMenu.Identifier)
func insertElements(_ childElements: [UIMenuElement], atEndOfMenu parentIdentifier: UIMenu.Identifier)
func insertElements(_ insertedElements: [UIMenuElement], beforeCommand siblingAction: Selector, propertyList siblingPropertyList: Any? = nil)
func insertElements(_ insertedElements: [UIMenuElement], afterCommand siblingAction: Selector, propertyList siblingPropertyList: Any? = nil)
func remove(action removedIdentifier: UIAction.Identifier)
func remove(command removedAction: Selector, propertyList removedPropertyList: Any? = nil)

UIMenuElement

RepeatBehavior

UIMenuElement has been updated with a new, nested type, RepeatBehavior. This is an enumeration that describes how a menu element should respond to repeated interaction, such as the user holding down their associated keyboard shortcut keys.

extension UIMenuElement {
    enum RepeatBehavior: Int {
        case automatic = 0
        case repeatable = 1
        case nonRepeatable = 2
    }
}

You can assign any of the above cases to the new repeatBehavior property of any UIMenuLeaf subclass, such as UIAction or UIKeyCommand.

UIMenuLeaf

If you're reading this article chronologically, you'll have already have come across the new UIMenuElement.RepeatBehavior enum.

Well, UIMenuLeaf's repeatBehavior is the corresponding property!

var repeatBehavior: UIMenuElement.RepeatBehavior { get set }

This property controls whether or not a menu element's action can be performed repeatedly in quick succession, such as when the user holds down the element's corresponding keyboard shortcuts keys. This is particularly handy for destructive actions, such as a Delete menu element in an email app, where you don't want the action to be triggered in quick succession by accident.

Simply set the repeatBehavior property of your UIMenuLeaf-conforming object to one of the three RepeatBehavior cases: repeatable, nonRepeatable, or automatic to let the system decide.

let action = UIAction(title: "Delete") { action in
    // ...
}
action.repeatBehavior = .nonRepeatable

UIMenuSystem

To support UIMainMenuSystem's new Configuration object, UIMenuSystem has been updated with a few new type declarations.

ElementGroupPreference

UIMenuSystem.ElementGroupPreference is a new enumeration which provides cases describing whether or not a group of menu elements should be included in iPadOS' new main menu.

extension UIMenuSystem {
    enum ElementGroupPreference: Int {
        case automatic = 0
        case removed = 1
        case included = 2
    }
}

These cases can be assigned to a bunch of UIMainMenuSystem.Configuration's properties to conditionally show or hide menu element groups. Check out UIMainMenuSystem.Configuration to see these properties in action!

FindElementGroupConfiguration

Also new in UIMenuSystem is the FindElementGroupConfiguration class which provides a set of options specifically for customising the Find menu element group.

extension UIMenuSystem {
    class FindElementGroupConfiguration: NSObject {
        var style: UIMenuSystem.FindElementGroupConfiguration.Style
    }
}

As of now, there's only one property, style, which can be set to one of the four cases in the new UIMenuSystem.FindElementGroupConfiguration.Style enumeration.

Style

UIMenuSystem.FindElementGroupConfiguration.Style is a new enumeration supporting UIMenuSystem's new FindElementGroupConfiguration class. It several cases that describe which children of the Find and Replace menu element group should be displayed.

extension UIMenuSystem.FindElementGroupConfiguration {
    enum Style: Int {
        case automatic = 0
        case search = 1
        case nonEditableText = 2
        case editableText = 3
    }
}

Assigning the automatic case to a find menu element group configuration's style property will allow the system to decide how best to display the Find and Replace menu. The search case will remove Find and Replace options, substituting them with a simple Search element. The nonEditableText will give you just Find options, where Replace is not appropriate. And editableText will give you the full Find and Replace functionality.

Again, check out UIMainMenuSystem.Configuration to learn more!

UIModalPresentationStyle

I bet an update to UIModalPresentationStyle wasn't on your WWDC 2025 bingo card, yet here we are! It's hardly a belter, but the formSheet presentation style in now available on tvOS!

Somewhere, someone out there just punched the air...

UIMutableTraits

This year, Apple have added several new traits to UITraitCollection relating to tab bar accessories, split view controller layout environments, and more!

Because UIMutableTraits is somewhat auxiliary to UITraitCollection, I've covered the bulk of the changes in that section, further down this page.

To support these changes, UIMutableTraits has been updated with several new properties.

extension UIMutableTraits {
    var headroomUsageLimit: UITraitHDRHeadroomUsageLimit.Value { get set }
    var resolvesNaturalAlignmentWithBaseWritingDirection: Bool { get set }
    var splitViewControllerLayoutEnvironment: UISplitViewController.LayoutEnvironment { get set }
    var tabAccessoryEnvironment: UITabAccessory.Environment { get set }
}

Check out the section on UITraitCollection to learn more!

UINavigationBarAppearance

UINavigationBarAppearance has been updated with a number of new properties to support the exciting additions to UINavigationItem.

There's now a new property for customising the default appearance of navigation bar subtitles with subtitleTextAttributes.

var subtitleTextAttributes: [NSAttributedString.Key: Any] { get set }

And a new property for customising the default appearance of large subtitles too with largeSubtitleTextAttributes.

var largeSubtitleTextAttributes: [NSAttributedString.Key: Any] { get set }

Finally, inline with the changes made to UIBarButtonItem.Style and UIToolbarAppearance, the doneButtonAppearance property has been deprecate in favour of the new prominentButtonAppearance property.

var prominentButtonAppearance: UIBarButtonItemAppearance { get set }

UINavigationController

In iOS 26, it's now possible to interactively dismiss a view controller embedded in a navigation controller by performing a leading to trailing swipe anywhere on the screen. Previously, this was only possible when initiating the swipe from the screen's leading edge.

There was, however, a catch that almost every seasoned iOS developer has run into at some point in their career. If you hide the navigation bar's back button, likely in favour of your own custom back button, the underlying pan gesture recogniser would silently fail.

The workaround? Set the delegate of the navigation controller's interactivePopGestureRecognizer to an object of your choosing, adding the required UIGestureRecognizerDelegate protocol conformance, and returning true from gestureRecognizerShouldBegin(_:).

In iOS 26, there's a new interactiveContentPopGestureRecognizer property of UINavigationController.

var interactiveContentPopGestureRecognizer: UIGestureRecognizer? { get }

Now, we can force that gesture recogniser to begin even when that pesky back button is hidden!

UINavigationItem

If you follow me on Twitter, you'll know that I have an unhealthy obsession with navigation bars on iOS. So, you can imagine my delight when I saw a whopping 81 additions to UINavigationItem.h when diffing iOS 26's UIKit headers.

Before you get too excited, the _bottomPalette API is still not...an API - but there are a lot of goodies for us navigation bar fans to sink our teeth into!

First of all, a new attributedTitle property!

var attributedTitle: AttributedString? { get set }

This, for me, has been long awaited as UINavigationBar's titleTextAttributes and largeTitleTextAttributes properties always felt a little limited, only supporting attributes applied to the entire string, rather than portions of the string defined by NSRanges.

var attributedSting = AttributedString("Title")
attributedSting.foregroundColor = UIColor.systemBlue
navigationItem.attributedTitle = attributedSting

A new subtitle property has been added that, when set, is displayed below the navigation bar's title view.

var subtitle: String? { get set }
navigationItem.subtitle = "Subtitle"

There's also a corresponding attributedSubtitle property to match the new attributedTitle.

var attributedSubtitle: AttributedString? { get set }
var attributedSting = AttributedString("Subtitle")
attributedSting.foregroundColor = UIColor.systemBlue
navigationItem.attributedSubtitle = attributedSting

On top of that, there's even a new subtitleView property to go alongside UINavigationItem's titleView which has been available since iOS 2!

var subtitleView: UIView? { get set }
let button = UIButton()
button.configuration = .borderedProminent()
button.configuration?.title = "Subtitle View"
navigationItem.subtitleView = button

Also new is a separate largeTitle property which allows you to have a large title that differs from the navigation bar's compact title.

var largeTitle: String? { get set }
navigationItem.title = "Title"
navigationItem.largeTitle = "Large Title"

A corresponding largeSubtitle property has also been added which will display a subtitle label underneath the large title, on the leading edge.

var largeSubtitle: String? { get set }
navigationItem.largeSubtitle = "Large Subtitle"

And, of course, there is a matching largeAttributedSubtitle property.

var largeAttributedSubtitle: AttributedString? { get set }
var attributedSting = AttributedString("Large Attributed Subtitle")
attributedSting.foregroundColor = UIColor.systemBlue
navigationItem.largeAttributedSubtitle = attributedSting

And, lastly, on the navigation bar title-subtitle customisation front, is largeSubtitleView.

var largeSubtitleView: UIView? { get set }
let button = UIButton()
button.configuration = .borderedProminent()
button.configuration?.title = "Large Subtitle View"
navigationItem.largeSubtitleView = button

That's not all that's new with UINavigationItem, however. We also have several new properties for customising the the position of the navigation bar's search bar.

First of all is a new property named searchBarPlacementAllowsExternalIntegration.

var searchBarPlacementAllowsExternalIntegration: Bool { get set }

This property, which is false by default, allows an ancestor view controller to take the search bar and place it somewhere other than the navigation bar. If set to true, a UISplitViewController might place the search bar in the trailing-most column's navigation bar on iPadOS.

let sidebarViewController = SidebarViewController()
sidebarViewController.navigationItem.searchController = UISearchController()
sidebarViewController.navigationItem.searchBarPlacementAllowsExternalIntegration = true
viewControllers = [
    sidebarViewController, // ...
]

In the example below, settingsearchBarPlacementAllowsExternalIntegration to false (on the left) places sidebarViewController's search bar within its own navigation bar. Setting the property to true (on the right) allows the containing split view controller to the place the search bar in its trailing-most column.

Similarly, on iOS, the system will sometimes place the search bar in a view controller's toolbar. This may or may not be what you want, so, to support this change, a new searchBarPlacementAllowsToolbarIntegration property has been added.

var searchBarPlacementAllowsToolbarIntegration: Bool { get set }

The default value is true so if you want to opt out of this behaviour, just flip this property to false.

If you already have a toolbar, you can integrate the search bar amongst your existing bar button items, positioning it exactly where you want with the new searchBarPlacementBarButtonItem property.

var searchBarPlacementBarButtonItem: UIBarButtonItem { get }
let addBarButtonItem = UIBarButtonItem(systemItem: .add)
toolbarItems = [navigationItem.searchBarPlacementBarButtonItem, .flexibleSpace(), addBarButtonItem]

SearchBarPlacement

To support the various new search bar placements, Apple have updated the UINavigationItem.SearchBarPlacement - the cases of which can be assigned to UINavigationItem's preferredSearchBarPlacement property.

Before we take a look at the new cases, Apple have deprecated the existing inline case - the documentation states that this case has been renamed to integrated.

This new integrated case places the search within toolbars or the navigation bar, either as a search field or a button depending on the available space.

case integrated = 1

The new integratedButton case also positions the search bar in the toolbar or navigation bar, however always as a button, regardless of the available space.

case integratedButton = 4

There's also a new integratedCentered case which behaves the same as integrated except, when the search bar's owning view controller is a descendant of a [UITabBarController] or has a navigation item style of browser or editor, centers the search bar in the navigation bar.

case integratedCentered = 3

UIResponder

UIResponder has been updated with a new provider(for:) method to support UIDeferredMenuElement's new focus-based menu elements.

func provider(for deferredElement: UIDeferredMenuElement) -> UIDeferredMenuElement.Provider?

When overriding this method, you can check the deferredElement's identifier property and conditionally return a UIDeferredMenuElement.Provider if needed.

override func provider(for deferredElement: UIDeferredMenuElement) -> UIDeferredMenuElement.Provider? {
    let historyIdentifier = UIDeferredMenuElement.Identifier(rawValue: "com.example.deferred-history-element")
    if deferredElement.identifier == historyIdentifier {
        return UIDeferredMenuElement.Provider { completion in
            let elements = browserProfile.historyElements()
            completion(elements)
        }
    }
    return nil
}

UIResponderStandardEditActions

The UIResponderStandardEditActions protocol has been updated with a handful of new methods for your UIResponder subclass to implement to support iPadOS's new main menu system. These include...

optional func alignCenter(_ sender: Any?)
optional func alignJustified(_ sender: Any?)
optional func alignLeft(_ sender: Any?)
optional func alignRight(_ sender: Any?)
optional func newFromPasteboard(_ sender: Any?)
optional func performClose(_ sender: Any?)
optional func showWritingTools(_ sender: Any?)
optional func toggleInspector(_ sender: Any?)
optional func toggleSidebar(_ sender: Any?)

Override any of these new methods in your UIResponder subclass to enable their respective menu elements in the main menu.

class TextEditorViewController: UIViewController {
    var textView: UITextView!
    override func alignCenter(_ sender: Any?) {
        textView.textAlignment = .center
    }
}

Interestingly, as of the iOS 26 beta 4 SDK, toggleSidebar(_:) back-deploys all the way back to iOS 15! Do with that what you will...

UIScene

In visionOS 26, users can now persist windows, volumes, and widgets in physical space. These persisted user interface elements are tied to the rooms they were used in, meaning that, if the user leaves the room and comes back a later time, their locked windows are right where they left them!

This behaviour is enabled by default but may not be appropriate for all windows, like welcome screens or login prompts. To opt out, you can use a new property of UIScene called destructionConditions.

var destructionConditions: Set<UIScene.DestructionCondition> { get set }

Simply set the value of this property to a Set generic over the new UIScene.DestructionCondition type, including the systemDisconnection condition.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let windowScene = scene as! UIWindowScene
        windowScene.destructionConditions = [.systemDisconnection]
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = ViewController()
        window.makeKeyAndVisible()
        self.window = window
    }
}

Furthermore, in the case that a secondary window, such as a toolbar, was the last active scene before your app was terminated by the user, UIKit might try to relaunch this scene the next time a user opens your app from the home view.

To avoid this, add the userInitiatedDismissal option to the secondary scene's destruction conditions.

DestructionCondition

UIScene.DestructionCondition is a new structure defining some constants that describe how a scene should behave with visionOS' new automatic scene restoration behaviour.

extension UIScene {
    struct DestructionCondition: Hashable, Equatable, Sendable, CustomStringConvertible {
        static var systemDisconnection: UIScene.DestructionCondition { get }
        static var userInitiatedDismissal: UIScene.DestructionCondition { get }
    }
}

There are currently two constants defined.

Firstly, systemDisconnection. Adding this to your window scene's destructionConditions means this scene will never be restored when the system closes your app as a result of the user leaving the physical space your app's window was placed.

And secondly, userInitiatedDismissal. UIKit will disregard any scene whose destruction conditions contains this property for restoration when the user opens your app from a closed state.

UISceneSession

Role

This year, UISceneSession.Role has been updated with a new static constant windowAssistiveAccessApplication.

static let windowAssistiveAccessApplication: UISceneSession.Role

Assistive Access is a system-wide experience that simplifies the user interface to make navigation iOS and iPadOS easier for people with cognitive disabilities.

It does so by reducing the number of elements on screen at any one time, streamlining interactions, adhering to consistent design practices, and providing clear pathways to success.

Up until now, we've had to rely on AccessibilitySettings's static isAssistiveAccessEnabled property to determine whether or not to adapt our UI for assistive access. While helpful, this encouraged developers to pepper these checks throughout their codebases which led to a lot of additional code that was harder to reason about.

This new windowAssistiveAccessApplication role allows us to create entirely separate scenes, specifically for Assistive Access, encouraging purpose-written code that is, as a result, much easier to reason about.

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let role = connectingSceneSession.role
        let configuration = UISceneConfiguration(name: nil, sessionRole: role)
        if connectingSceneSession.role == .windowAssistiveAccessApplication {
            configuration.delegateClass = AssistiveAccessSceneDelegate.self
        }
        return configuration
    }
}

UIScrollEdgeEffect

UIScrollEdgeEffect is a new class in iOS 26 that contains properties of the new effect applied to a particular edge of a scroll view.

class UIScrollEdgeEffect: NSObject {
    var style: UIScrollEdgeEffect.Style { get set }
    var isHidden: Bool { get set }
}

Adjusting the style property with one of the cases defined in the new UIScrollEdgeEffect.Style enumeration changes how the effect looks, check out the difference between these styles a little further down.

Setting the isHidden property to true completely disables the effect. The default value is false.

You don't initialise an instance of this class yourself. Instead, you can customise these values through several new properties of UIScrollView.

Style

UIScrollEdgeEffect.Style is a new class supporting UIScrollEdgeEffect. This class defines three constants for customising the style of the scroll edge effect.

extension UIScrollEdgeEffect {
    class Style: NSObject {
        class var automatic: UIScrollEdgeEffect.Style { get }
        class var soft: UIScrollEdgeEffect.Style { get }
        class var hard: UIScrollEdgeEffect.Style { get }
    }
}

Below is an example of the soft and hard styles. The difference is subtle, so you may have to pixel peep, but it largely boils down to the variable blur effect radius, and the opacity of the colour overlay.

The automatic style allows the system to switch between the soft and hard effects depending on the underlying content.

UIScrollEdgeElementContainerInteraction

On iOS 26, scroll views underneath navigation bars or toolbars automatically apply a variable blur and gradient colour overlay to their edges, ensuring legibility of overlapping content.

This effect isn't limited to system bars, however - you can use it with any custom container view that overlays an edge of a scroll view. How? With a new UIInteraction, UIScrollEdgeElementContainerInteraction, of course!

class UIScrollEdgeElementContainerInteraction: NSObject, UIInteraction {
    weak var scrollView: UIScrollView? { get set }
    var edge: UIRectEdge { get set }
}

Simply create an instace of UIScrollEdgeElementContainerInteraction using the init() initialiser, set its scrollView property to your content scroll view, set your preferred edge, and call addInteraction(_:) on your container view.

let interaction = UIScrollEdgeElementContainerInteraction()
interaction.scrollView = textView
interaction.edge = .bottom
let buttonsContainerView = UIView()
buttonsContainerView.addInteraction(interaction)

Notably abscent is the option to set the edge effect's style, as per UIScrollEdgeEffect.

UIScrollView

Unfortunately, in iOS 26, we did not get a variable blur effect API. -[UIBlurEffect effectWithVariableBlurRadius:imageMask:] exists, and Apple make liberal use of this throughout their own apps, but it remains behind closed doors.

With that moaning out of the way, we did get a couple of APIs that allow us to create a similar effect under certain circumstances...

...with limits.

Among these APIs are UIScrollView's new edge effect properties.

var topEdgeEffect: UIScrollEdgeEffect { get }
var leftEdgeEffect: UIScrollEdgeEffect { get }
var bottomEdgeEffect: UIScrollEdgeEffect { get }
var rightEdgeEffect: UIScrollEdgeEffect { get }

These new properties, all of type UIScrollEdgeEffect, control the subtle blur effect and colour overlay shown beneath system chrome, like navigation or tab bars.

You can customise the intensity of this effect by adjusting the scroll edge effect's style property, which can be one of the two UIScrollEdgeEffect.Style enum cases. You can also disable the effect entirely by setting the isHidden property to true, but that's where the customisation ends, I'm afraid.

scrollView.topEdgeEffect.style = .soft
scrollView.topEdgeEffect.isHidden = false

Of course, these properties are accessible to all UIScrollView subclasses like UITableView, UICollectionView and UITextView.

UIScrollView also has a new received a new property supporting visionOS' new Look to Scroll feature, lookToScrollAxes.

var lookToScrollAxes: UIAxis { get set }

When enabled, the scroll view will automatically begin scrolling in the relevant direction when the user's gaze moves towards the edge of the scroll view.

This behaviour is opt-in, so, to get this up and running, simply set the value of the lookToScrollAxes property to one of the options defined in the UIAxis option set: horizontal, vertical or both.

let scrollView = UIScrollView()
scrollView.lookToScrollAxes = .vertical

UISearchBarDelegate

To support Apple's new Natural Selection feature, UISearchBarDelegate has been updated with one deprecation and one new method. The deprecated method in question is searchBar(_:shouldChangeTextIn:replacementText:) which has been replaced with searchBar(_:shouldChangeTextInRanges:replacementText:).

optional func searchBar(_ searchBar: UISearchBar, shouldChangeTextInRanges ranges: [NSValue], replacementText: String) -> Bool

UISearchController

There are no changes to UISearchController's interface, just a slight change in the behaviour of hidesNavigationBarDuringPresentation.

Previously, the default value of this property was true. However, on iOS 26, the default value is "determined by context". So, unless you explicitly set this property to true or false via the API, the behaviour may not be consistent.

UISearchTab

UISearchTab has a new automaticallyActivatesSearch property that, when set to true, automatically activates the search field when the tab becomes active. For this to work, the navigationItem of the search tab's view controller should have a non-nil searchController.

tabs = [
    UISearchTab { tab in
        let searchTab = tab as! UISearchTab
        searchTab.automaticallyActivatesSearch = true
        let searchViewController = SearchViewController()
        searchViewController.navigationItem.searchController = UISearchController()
        return UINavigationController(rootViewController: searchViewController)
    }
]

It's also worth noting that, when setting automaticallyActivatesSearch to true, the previously selected tab will become active when the user cancels the search.

UISlider

In iOS 17, a new, private _sliderStyle property was added to UISlider to support the new fluid sliders seen across apps like Music and Podcasts. This year, the underscore has been dropped and sliderStyle is available to us all!

var sliderStyle: UISlider.Style { get set }

Simply set the value of this property to one of the new UISlider.SliderStyle enumeration cases, default or thumbless.

let slider = UISlider()
slider.sliderStyle = .default
let slider = UISlider()
slider.sliderStyle = .thumbless

Next on the UISlider front is a new trackConfiguration property!

var trackConfiguration: UISlider.TrackConfiguration? { get set }

This is a fantastic new API for adding tick marks to a slider, but it's far from just visual, with options to control the starting point (or neutral value) of the slider, whether or not the slider snaps to a tick value, and more!

let slider = UISlider()
slider.trackConfiguration = UISlider.TrackConfiguration(numberOfTicks: 5)

Check out more details about UISlider.TrackConfiguration a little further down!

SliderStyle

UISlider.SliderStyle is a new enumeration that's been added to support UISlider's new sliderStyle property. It has two cases, default and thumbless.

extension UISlider {
    enum SliderStyle: Int {
        case `default`
        case thumbless
    }
}

Assign one of these enum cases to the sliderStyle property of a UISlider to customise the appearance and behaviour of the slider.

TrackConfiguration

UISlider.TrackConfiguration is a new structure supporting UISlider's trackConfiguration property, added in iOS 26.

extension UISlider {
    struct TrackConfiguration: Hashable {
        var allowsTickValuesOnly: Bool { get set }
        var neutralValue: Float { get set }
        var enabledRange: ClosedRange<Float> { get set }
        let ticks: [UISlider.TrackConfiguration.Tick]
        init(allowsTickValuesOnly: Bool = true, neutralValue: Float = 0, enabledRange: ClosedRange&lt:Float> = 0...1, ticks: [UISlider.TrackConfiguration.Tick])
        init(allowsTickValuesOnly: Bool = true, neutralValue: Float = 0, enabledRange: ClosedRange<Float> = 0...1, numberOfTicks: Int)
    }
}

You can create an instace of TrackConfiguration using either the init(allowsTickValuesOnly:neutralValue:enabledRange:ticks:) or init(allowsTickValuesOnly:neutralValue:enabledRange:numberOfTicks:) initialisers.

Helpfully, most of these initialisers' parameters have default values, so you can just construct a track configuration with init(ticks:) or init(numberOfTicks:).

Let's start by looking at the allowsTickValuesOnly property, which ensures the slider snaps to its tick values.

var trackConfiguration = UISlider.TrackConfiguration(numberOfTicks: 5)
trackConfiguration.allowsTickValuesOnly = true
slider.trackConfiguration = trackConfiguration

It's worth noting that when adding a UIControl.Event.valueChanged action, the slider only reports changes to the slider's value when it's equal to the float value of one of the ticks.

Next, we have the neutralValue property. This controls the starting point of the slider and, as a result, where the slider's fill colour is anchored to.

trackConfiguration.neutralValue = 0.5

Finally, the enabledRange property allows you to clamp the slider's value between two given floating point numbers without having to change the minimum and maximum value.

trackConfiguration.enabledRange = 0...0.75

Tick

UISlider.TrackConfiguration.Tick is a new structure, supporting UISlider.TrackConfiguration, representing a tick mark on a slider.

extension UISlider.TrackConfiguration {
    struct Tick: Hashable {
        let position: Float
        var title: String? { get set }
        var image: UIImage? { get set }
        init(position: Float, title: String? = nil, image: UIImage? = nil)
    }
}

You can customise the position of the tick, which should be a value between 0 and 1, and (supposedly) the title and the image displayed as the tick mark itself. I say "supportedly" because, as of beta 4, these properties don't seem to do anything... I'll be keeping an eyes on this during the beta cycle and update this article if that changes!

An array of these Tick objects make up the ticks property of a UISlider's trackConfiguration.

UISplitViewController

In iPadOS 26, UIKit has added support for an additional, trailing split view controller column, known as an inspector column.

With this comes a slew of new properties for sizing the secondary and inspectors columns.

First of all, minimumSecondaryColumnWidth. Now that the secondary column can be displayed alongside an inspector, it makes sense that, like the primary column, a minimum width can be specified. Setting this property sets the minimum width, in points, of that secondary column.

var minimumSecondaryColumnWidth: CGFloat { get set }

Up next is preferredSecondaryColumnWidth. This is the ideal width, in points, that your secondary column should be. This property may or may not be respected by the system depending on the available space.

var preferredSecondaryColumnWidth: CGFloat { get set }

Finally, for the secondary column, is preferredSecondaryColumnWidthFraction. Much like preferredSecondaryColumnWidth, this property allows you to specify your desired width of the secondary column, but, instead, with a fractional value, between 0 and 1. Specifying a value of 0.5 will result in a secondary column with a width of half of the available space.

var preferredSecondaryColumnWidthFraction: CGFloat { get set }

First up on the inspector column front is maximumInspectorColumnWidth. As you might expect, this property sets the maximum width of your inspector column, in points.

var maximumInspectorColumnWidth: CGFloat { get set }

Similarly, minimumInspectorColumnWidth sets the minimum inspector column width.

var minimumInspectorColumnWidth: CGFloat { get set }

The preferredInspectorColumnWidth sets the ideal width of the inspector column, in points, which, like preferredSecondaryColumnWidth, may or may not be respected depending on the available space.

var preferredInspectorColumnWidth: CGFloat { get set }

And, finally, preferredInspectorColumnWidthFraction sets your preferred inspector column width as a fraction of the total available space.

var preferredInspectorColumnWidthFraction: CGFloat { get set }

Like on macOS, inspector panes can be manually hidden and shown. In fact, UIResponderStandardEditActions has been updated with a new toggleInspector(_:) method to support just this. As such, a new method has been added to UISplitViewController for checking the visibility of a column, isShowing(_:).

func isShowing(_ column: UISplitViewController.Column) -> Bool

To support this, a new inspector case has been added to UISplitViewController.Column.

Column

To support iPadOS' new inspector columns, the UISplitViewController.Column enumeration has been updated with a new case, inspector.

case inspector = 4

You can use this new case when calling split view controller methods like viewController(for:) or setViewController(for:).

let inspectorViewController = InspectorViewController()
setViewController(inspectorViewController, for: .inspector)

LayoutEnvironment

UISplitViewController.LayoutEnvironment is a new enumeration supporting UITraitCollection's new splitViewControllerLayoutEnvironment trait.

extension UISplitViewController {
    enum LayoutEnvironment: Int {
        case none = 0
        case expanded = 1
        case collapsed = 2
    }
}

This enum provides three cases, representing the current display mode of an ancestor split view controller, if one is present.

The expanded case represents the split view controller's side-by-side column display mode, shown in a regular width environment. The compact case represents the split view controller's navigation stack display mode, used in a compact width environment. And the none case represents a lack of an ancestor split view controller altogether.

To see an example of how you might use this enumeration, check out the example further up this page!

UISplitViewControllerDelegate

The UISplitViewControllerDelegate protocol has been updated with a couple of handy methods that wil be called when one of a split view controller's columns is shown or hidden.

Let's start with splitViewController(_:didShow:). This method is called when one of the split view controller's columns appears.

optional func splitViewController(_ svc: UISplitViewController, didShow column: UISplitViewController.Column)

And finally, on the split view controller delegate front, splitViewController(_:didHide:). This method is called when one of the split view controller's columns is hidden.

optional func splitViewController(_ svc: UISplitViewController, didHide column: UISplitViewController.Column)

These methods complement the splitViewController(_:willShow:) and splitViewController(_:willHide:) methods of old.

UISymbolContentTransition

UISymbolContentTransition is a new class supporting UIButton.Configuration's new symbolContentTransition property.

class UISymbolContentTransition: NSObject {
    var contentTransition: any ContentTransitionSymbolEffect & SymbolEffect { get }
    var options: SymbolEffectOptions { get }
    convenience init(_ contentTransition: any ContentTransitionSymbolEffect & SymbolEffect, options: SymbolEffectOptions = .default)
}

You create an instance of this class using the init(_:options:) initialiser, passing in an object conforming to the Symbols framework's ContentTransitionSymbolEffect and SymbolEffect protocols.

Optionally, you can provide a set of SymbolEffectOptions, allowing you to control the speed of the transition, and whether or not it's repeating.

UITabAccessory

For some time, a common pattern in apps, especially those featuring some sort of media playback, has been to add a floating toolbar above the tab bar that remains in place across tabs. This design pattern was perhaps popularised by Apple themselves in apps like Music and Podcasts.

There was never an official API to support this, but there wasn't much need? Internal and external clients alike added subviews to a UITabBarController's view manually and updated each child view controller's additionalSafeAreaInsets to allow scrolling content to come to rest above the toolbar.

Well, no longer! In iOS 26 Apple have added a new bottomAccessory property to UITabBarController and, to support this, UITabAccessory has been introduced.

class UITabAccessory: NSObject {
    var contentView: UIView { get }
    init(contentView: UIView)
}

Creating a tab accessory is as simple as calling init(contentView) and assigning the initialised object to your tab bar controller's bottomAccessory property - learn more below!

Environment

In iOS 26, UITabBarController's tab bar is able to collapse and expand, based on the user's scrolling. As a result, the new bottomAccessory becomes minimised and sandwiched between the app's main and search tabs.

Apple have given us a way to determine when our UITabAccessory is in this collapsed or expanded state with a new UITabAccessory.Environment enumeration which describes the current appearance of the accessory view.

extension UITabAccessory {
    enum Environment: Int {
        case unspecified = 0
        case none = 1
        case regular = 2
        case inline = 3
    }
}

The regular and inline cases are self-explanatory, relating to the expanded or compact state, respectively. The unspecified case is returned if there is no information about the tab accessory environment, such as when your tab accessory's content view has yet to move to its superview. The none case indicates that the view is not in a tab accessory environment at all, so any view that isn't your tab accessory's content view or a descendant of it.

You can get the current tab accessory environment through UITraitCollection's new tabAccessoryEnvironment property.

UITabBarController

In iOS 26, UITabBarController has been updated with several new properties to support the new UITabAccessory. Perhaps most importantly is the new bottomAccessory property.

var bottomAccessory: UITabAccessory? { get set }

Adding a bottom accessory is as simple as initialising an instance of the UITabAccessory class with a content view, and assigning it to this new bottomAccessory property.

let tabAccessory = UITabAccessory(contentView: contentView)
bottomAccessory = tabAccessory

Of course, this property is optional, so you can set the value to nil to remove the bottom accessory.

There's also a new setBottomAccessory(_:animated) method which allows you to add or remove a bottom accessory with a system-defined animation.

func setBottomAccessory(_ bottomAccessory: UITabAccessory?, animated: Bool)
let tabAccessory = UITabAccessory(contentView: contentView)
setBottomAccessory(tabAccessory, animated: true)

UITabBarController has also received a new tabBarMinimizeBehavior property, of type UITabBarController.MinimizeBehavior.

var tabBarMinimizeBehavior: UITabBarController.MinimizeBehavior { get set }

This property controls whether or not the tab bar minimises into its compact appearance, and what triggers it to do so. Learn more about these enum cases further down the page.

tabBarMinimizeBehavior = .onScrollDown

Finally, on the tab bar controller front, a new property has been added to help determine the available space in the tab bar controller's view, accounting for the tab bar and tab accessory.

var contentLayoutGuide: UILayoutGuide { get }

In the example below, I've constrained a subview, with a background colour of UIColor.tertiarySystemFill, to the top, bottom, leading and trailing anchors of the content layout guide.

And, of course, this layout guide adapts as the tab bar collapses or expands, and as the accessory view is shown or hidden.

MinimizeBehavior

UITabBarController.MinimizeBehavior is a new enumeration that provides a set of cases describing how the new iOS 26 tab bar collapses.

extension UITabBarController {
    enum MinimizeBehavior: Int {
        case automatic = 0
        case never = 1
        case onScrollDown = 2
        case onScrollUp = 3
    }
}

Setting the value of a UITabBarController's tabBarMinimizeBehavior property to never will disable the behaviour entirely. Setting the value of this property to onScrollDown will collapse the tab bar when the user scrolls down, and will expand the tab bar when the user scrolls back up. Setting the value of this property to onScrollUp will do the opposite, which is recommended if a scroll view's content is aligned to the bottom. The default case, automatic, lets the system decide this behaviour.

UITextFieldDelegate

UITextFieldDelegate has seen a couple of small updates to support Apple's new Natural Selection feature. Namely, two deprecations, and two new methods.

Firstly, textField(_:shouldChangeCharactersIn:replacementString:) has been deprecated in favour of a new, optional method, textField(_:shouldChangeCharactersInRanges:replacementString:).

optional func textField(_ textField: UITextField, shouldChangeCharactersInRanges ranges: [NSValue], replacementString string: String) -> Bool

And, secondly, textField(_:editMenuForCharactersIn:suggestedActions:) has been deprecated in favour of another new, optional method, textField(_:editMenuForCharactersInRanges:suggestedActions:).

optional func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu?

UITextView

In iOS 26, UITextView has seen one small but pretty significant change. The selectedRange property has been deprecated, in favour of the new selectedRanges property.

var selectedRanges: [NSRange] { get set }

This is to better support selecting text that contains a combination of left-to-right and right-to-left languages; a feature that Apple is calling Natural Selection.

For both left-to-right and right-to-left languages, the underlying text storage object stores characters in the order that they are written in. When displaying a left-to-right or right-to-left language in isolation, this isn't a problem. The issue arises when they are displayed together, in a single document, as the next character in the text storage sequence may not be the next character displayed on screen.

Historically, this has led to some pretty funky text selection behaviour. In iOS 26, the text selection experience is seamless, even when combining left-to-right and right-to-left languages.

In the video above, the final selection will result in an array of two NSRanges being returned from the selectedRanges property. The first with a location of 0 and length of 6, and the second with a location of 8 and length of 2.

UITextViewDelegate

Much like UITextFieldDelegate, UITextViewDelegate has seen a couple of deprecations and new methods to support Apple's new Natural Selection feature.

First up, textView(_:shouldChangeTextIn:replacementText:) has been deprecated in favour of the new, optional textView(_:shouldChangeTextInRanges:replacementText:) method.

optional func textView(_ textView: UITextView, shouldChangeTextInRanges ranges: [NSValue], replacementText text: String) -> Bool

And textView(_:editMenuForTextIn:suggestedActions:) has been deprecated in favour of the new, optional textView(_:editMenuForTextInRanges:suggestedActions:) method too.

optional func textView(_ textView: UITextView, editMenuForTextInRanges ranges: [NSValue], suggestedActions: [UIMenuElement]) -> UIMenu?

UIToolbarAppearance

Much like UINavigationBarAppearance, UIToolbarAppearance has been updated to support the new bar button item style, prominent. The old doneButtonAppearance property has been deprecated, with a new prominentButtonAppearance property taking its place.

var prominentButtonAppearance: UIBarButtonItemAppearance { get set }

You can use this prominent button appearance proxy to customise the default look of your prominent toolbar items, across your app.

UITraitCollection

In iOS 26, UITraitCollection has been updated with several new properties to support new UIKit features.

First of all, a new property, tabAccessoryEnvironment. This will return one of the cases in the new UITabAccessory.Environment enumeration which you can use for determining layout information of a view participating in a tab accessory's view hierarchy.

var tabAccessoryEnvironment: UITabAccessory.Environment { get }

Secondly, another new property, splitViewControllerLayoutEnvironment. This will return one of the cases from the new UISplitViewController.LayoutEnvironment which you can use to determine if an ancestor split view controller is in its expanded or collapsed state.

var splitViewControllerLayoutEnvironment: UISplitViewController.LayoutEnvironment { get }

In the example below, a UICollectionViewCell subclass queries this trait to conditionally show a disclosure indicator. If the split view controller is in its expanded state, the disclosure indicator is hidden...

...and if the split view controller is in its collapsed state, the disclosure indicator is shown...

As of iOS 18, UIImageViews displaying an HDR image would intelligently fall back to SDR to ensure that the focused content of your app stands out. One such example of this is when a view controller displaying an HDR image presents another view controller.

In iOS 26, developers can add support for this to their custom components using the new hdrHeadroomUsageLimit property, which will return a case from the new UIHDRHeadroomUsageLimit enumeration.

var hdrHeadroomUsageLimit: UIHDRHeadroomUsageLimit { get }

Finally, resolvesNaturalAlignmentWithBaseWritingDirection. This property will return a Boolean value indicating whether or not text in a given language should be displayed in its native writing direction, or instead follow the writing direction of the user's default language.

var resolvesNaturalAlignmentWithBaseWritingDirection: Bool { get }

For example, if the value of this property is true and the user's default language is English (a left to right language), text written in Arabic (a right to left language) will be displayed from right to left. If this property is set to false, the Arabic text will be displayed from left to right.

traitOverrides.resolvesNaturalAlignmentWithBaseWritingDirection = true
traitOverrides.resolvesNaturalAlignmentWithBaseWritingDirection = false

New initialisers of UITraitCollection have also been added to construct trait collections with the desired values of these new properties.

init(hdrHeadroomUsageLimit: UIHDRHeadroomUsageLimit)
init(tabAccessoryEnvironment: UITabAccessory.Environment)
convenience init(resolvesNaturalAlignmentWithBaseWritingDirection: Bool)

UITraitHDRHeadroomUsageLimit

UITraitHDRHeadroomUsageLimit is a new structure conforming to the UITraitDefinition protocol that's been added to support UITraitCollection's trait observation APIs.

struct UITraitHDRHeadroomUsageLimit: UITraitDefinition {
    static let defaultValue: UIHDRHeadroomUsageLimit
    static let name: String
    static let identifier: String
    static let affectsColorAppearance: Bool
}

Its associated type is the new UIHDRHeadroomUsageLimit enumeration.

You can use this type alongside APIs such as registerForTraitChanges(_:handler:) to observe change to the HDR headroom usage limit.

registerForTraitChanges([UITraitHDRHeadroomUsageLimit.self]) { (traitEnvironment: Self, previousTraitCollection: UITraitCollection) in
    let limit = traitEnvironment.traitCollection.hdrHeadroomUsageLimit
}

To learn more, check out UITraitCollection's new hdrHeadroomUsageLimit property.

UITraitResolvesNaturalAlignmentWithBaseWritingDirection

Take a deep breath! UITraitResolvesNaturalAlignmentWithBaseWritingDirection is a new structure conforming to the UITraitDefinition protocol that's also been added to support UITraitCollection's trait observation APIs.

struct UITraitResolvesNaturalAlignmentWithBaseWritingDirection: UITraitDefinition {
    static let defaultValue: Bool?
    static let name: String
    static let identifier: String
    static let affectsColorAppearance: Bool
}

You can also use this type alongside trait registration APIs like registerForTraitChanges(_:handler:). To learn more about what this does, check out UITraitCollection's new resolvesNaturalAlignmentWithBaseWritingDirection property.

UITraitSplitViewControllerLayoutEnvironment

UITraitSplitViewControllerLayoutEnvironment is another new structure conforming to the UITraitDefinition protocol, also added to support UITraitCollection's trait observation APIs.

struct UITraitSplitViewControllerLayoutEnvironment: UITraitDefinition {
    static let defaultValue: UISplitViewController.LayoutEnvironment
    static let name: String
    static let identifier: String
    static let affectsColorAppearance: Bool
}

Like the trait definitions listed above, you can use this with the all the trait registration APIs. And to learn more, check out the associated type, UISplitViewController.LayoutEnvironment.

UITraitTabAccessoryEnvironment

The last new trait definition in the iOS 26 SDK is UITraitTabAccessoryEnvironment. Like all of the above, this is a simple structure conforming to the UITraitDefinition protocol, added to support trait observation APIs.

struct UITraitTabAccessoryEnvironment: UITraitDefinition {
    static let defaultValue: UITabAccessory.Environment
    static let name: String
    static let identifier: String
    static let affectsColorAppearance: Bool
}

Have a read of the updates to UITabAccessory and UITabAccessory.Environment to learn more!

UIView

In iOS 26, Apple have added support for the Swift-based Observation framework's Observable objects. UIKit automatically tracks references to Observable objects in methods like layoutSubviews(), determines which properties of the Observable that the method relies on, and, when those properties change, reruns the appropriate method.

@Observable class ObservableModel {
    var isLoading: Bool = false
}
class ObservableView: UIView {
    var model: ObservableModel
    var label: UILabel
    override func layoutSubviews() {
        super.layoutSubviews()
        label.text = model.isLoading ? "Loading" : "Done"
    }
}

In the example above, when the model's isLoading property changes, layoutSubviews() will be called automatically and label's text property will be updated.

However, you might not want your layoutSubviews() code to be rerun when some state in an Observable object changes as it may contain some computationally expensive layout code. Apple's solution to this is a new method of UIView, updateProperties().

func updateProperties()

This method, which fully supports observation tracking, runs before layoutSubviews() and should be used to populate content or apply styling to your views.

Like other lifecycle methods, you can trigger it manually with the new setNeedsUpdateProperties() method, although it's worth noting that multiple calls may be coalesced into a single update during the next layout pass.

func setNeedsUpdateProperties()

To force an immediate properties update, call the new updatePropertiesIfNeeded() method. This will invalidate this view, including any descendant subviews.

func updatePropertiesIfNeeded()

UIView has also received a few new methods that work in conjunction with its new LayoutRegion API - a struct that describes an area of a view, with optional horizontal or vertical insets to account for corner curvature or window controls.

First of all, layoutGuide(for:). This method will return an instance of UILayoutGuide for the given layout region. Perfect for use with Auto Layout constraints.

func layoutGuide(for region: UIView.LayoutRegion) -> UILayoutGuide

Up next, edgeInsets(for:), which will return UIEdgeInsets for the given layout region. You can, of course, use these insets with manual layout code, or even set a scroll view's contentInset property, without insetting the frame of the scroll view itself.

func edgeInsets(for region: UIView.LayoutRegion) -> UIEdgeInsets

And, finally, directionalEdgeInsets(for:). This will return an instance of NSDirectionalEdgeInsets for the desired layout region.

func directionalEdgeInsets(for region: UIView.LayoutRegion) -> NSDirectionalEdgeInsets

For an example of UIView.LayoutRegion in action, read on...

This year, the UIKit team have added an all-new way of configuring a view's corner radii with the new cornerConfiguration property!

var cornerConfiguration: UICornerConfiguration { get set }

These corner configurations allow you to set uniform, independent, and container-concentric corner radii incredibly easily!

let rectangleView = UIView()
rectangleView.backgroundColor = .systemBlue
rectangleView.cornerConfiguration = .uniformEdges(topRadius: 52, bottomRadius: 26)

This API is incredibly versatile and has been integrated with system components such as sheets, popovers and even window scenes! Check out UICornerConfiguration and UICornerRadius to learn more!

To support this, a nice new convenience method has been added to UIView, effectiveRadius(corner:).

func effectiveRadius(corner: UIRectCorner) -> CGFloat

When called, this method will return the radius of the specified corner, as a CGFloat, based on the view's current corner configuration.

let radius = rectangleView.effectiveRadius(corner: .topLeft)
print(radius) // 52

AnimationOptions

UIView's AnimationOptions option set has received a new option, flushUpdates.

static var flushUpdates: UIView.AnimationOptions { get }

When using this new animation option, UIKit applies any pending trait, property, or layout changes before entering and exiting an animation context.

What does this mean in practicality? You no longer have to call layoutIfNeeded() inside a UIView.animate(withDuration:animations:completion:) block when animating changes to constraints, for example.

UIView.animate(withDuration: 1) {
    heightConstraint.constant = 100
    layoutIfNeeded()
}
UIView.animate(withDuration: 1, options: .flushUpdates) {
    heightConstraint.constant = 100
}
 

LayoutRegion

UIView's new LayoutRegion API is a structure that describes an area of a view, like the safe area, but with features like corner curve avoidance built in.

You don't initialise an instance of this struct directly. Instead, use one of the predefined several static methods.

extension UIView {
    struct LayoutRegion: Hashable, Equatable {
        static func safeArea(cornerAdaptation: UIView.LayoutRegion.AdaptivityAxis? = nil) -> UIView.LayoutRegion
        static func margins(cornerAdaptation: UIView.LayoutRegion.AdaptivityAxis? = nil) -> UIView.LayoutRegion
        static func readableContent(cornerAdaptation: UIView.LayoutRegion.AdaptivityAxis? = nil) -> UIView.LayoutRegion
    }
}

The above will return a layout region representing the safe area, layout margins, or readable content guide, respectively.

Each static method accepts a case from the new UIView.LayoutRegion.AdaptivityAxis enumeration as an argument. This will return a layout region avoiding corner curves by insetting the region from the horizontal or vertical edge if needed.

You can use the return value of these functions to request a layout guide, edge insets, or directional edge insets for the given layout region using UIView's new layoutMargins(for:), edgeInsets(for:) or directionalEdgeInsets(for:) methods, respectively.

In the example below, a layout guide is requested with a vertical corner adaptation to avoid the new window controls on iPadOS.

let label = UILabel()
label.text = "Hello, World!"
label.font = .preferredFont(forTextStyle: .extraLargeTitle)
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
let contentGuide = view.layoutGuide(
    for: .margins(cornerAdaptation: .vertical)
)
NSLayoutConstraint.activate([
    label.topAnchor.constraint(equalTo: contentGuide.topAnchor),
    label.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor)
])

AdaptivityAxis

UIView.LayoutRegion.AdaptivityAxis is new enumeration that's been added to support the new UIView.LayoutRegion structure.

Specifically, it provides two cases that can be passed into LayoutRegion's static safeArea(cornerAdaptation:), margins(cornerAdaptation:) and readableContent(cornerAdaptation:) methods.

extension UIView.LayoutRegion {
    enum AdaptivityAxis: Equatable {
        case horizontal
        case vertical
    }
}

While similar in purpose to UIAxis, this is not an OptionSet which means you can only request a layout guide, etc., for a single axis at a time. Perhaps the UIKit team don't foresee the need for insetting content both vertically and horizontally with corner adaptations at the same time?

UIViewController

In iOS 26, UIViewController has also been updated with first class support for observation tracking. Methods such as viewWillLayoutSubviews(), viewDidLayoutSubviews() and updateViewConstraints() all support automatic observation tracking.

Much like UIView, there are three new methods supporting observation tracking outside of layout lifecycle methods: updateProperties(), setNeedsUpdateProperties() and updatePropertiesIfNeeded().

func updateProperties()
func setNeedsUpdateProperties()
func updatePropertiesIfNeeded()

Also new in iOS 26 is UIViewController's new prefersInterfaceOrientationLocked property.

var prefersInterfaceOrientationLocked: Bool { get }

This differs from the existing supportedInterfaceOrientations property as your view controller's interface may be agnostic to interface orientation, supporting both portrait and landscape, but may want to lock the orientation to whatever the current interface orientation is.

One such example of this is a driving game with tilt controls, which may be playable in both portrait and landscape. When the user starts the game, it would make sense to temporarily lock the scene's orientation to prevent unwanted changes caused by the user rotating their devices to steer.

This property is read-only, so you'll have to override it in your UIViewController subclass. To alert the system of any changes made to the underlying value of this property, call the new setNeedsUpdateOfPrefersInterfaceOrientationLocked() method.

func setNeedsUpdateOfPrefersInterfaceOrientationLocked()

Container view controllers, such as UINavigationController or UITabBarController, can specify a child view controller from which the prefersInterfaceOrientationLocked property should be derived.

class NavigationController: UINavigationController {
    override var childViewControllerForInterfaceOrientationLock: UIViewController? {
        return topViewController
    }
}

You can query the current state of the interface orientation lock through UIWindowScene.Geometry's new isInterfaceOrientationLocked property.

Transition

In iOS 18, Apple added a new zoom transition API, accessible through a new property of UIViewController, preferredTransition. This property accepts an instance of a new type, UIViewController.Transition. This new type introduced a number of transitions; most notably, zoom(options:sourceViewProvider:).

This year, Apple have refined this API with a new zoom(options:sourceBarButtonItemProvider:) method that allows you to return an instance of UIBarButtonItem in the trailing closure, instead of a UIView.

let viewController = DetailViewController()
viewController.preferredTransition = .zoom { context in
    return navigationItem.rightBarButtonItem
}
present(viewController, animated: true)

UIViewPropertyAnimator

Much like UIView.AnimationOptions, UIViewPropertyAnimators now supporting applying pending updates to traits, properties, and layout before entering or exiting an animation context.

This is an opt-in behaviour with a new property of UIViewPropertyAnimator, flushUpdates.

var flushUpdates: Bool { get set }

As per UIView.AnimationOptions, this essentially means you no longer have to call layoutIfNeeded() in your animation block.

let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut)
animator.flushUpdates = true
animator.addAnimations {
    heightConstraint.constant = 100
}
animator.startAnimation()

The default value is false.

UIWindowScene

There's not much to note about UIWindowScene itself in iOS 26, just a couple of deprecations. The coordinateSpace and interfaceOrientation properties have been deprecated, in favour of properties of the same names in the window scene's effectiveGeometry object.

Geometry

As mentioned above, UIWindowScene's coordinateSpace and interfaceOrientation properties have been deprecated. Their functionality, however, lives on in the form of two properties of UIWindowScene.Geometry, under the same names.

Added in iOS 26, coordinateSpace, and added in iOS 16, interfaceOrientation.

var coordinateSpace: any UICoordinateSpace { get }
var interfaceOrientation: UIInterfaceOrientation { get }

UIWindowScene's Geometry object has also seen the addition of a new isInterfaceOrientationLocked property, supporting the new property added to UIViewController, prefersInterfaceOrientationLocked. This property will tell you if any visible view controller in the relevant scene has requested the interface orientation be locked to the current state.

var isInterfaceOrientationLocked: Bool { get }

The final addition to UIWindowScene.Geometry is the new isInteractivelyResizing property.

var isInteractivelyResizing: Bool { get }

This property will return true if the window scene is being resized by the user in iPadOS' new windowing system. You might use the result of this property to defer any particularly graphically-intensive work until the user has finished interactively resizing the window.

UIWindowSceneDelegate

The UIWindowSceneDelegate protocol has been updated with a couple of new, optional methods to support iPadOS' new windowing system. Firstly, windowScene(_:didUpdateEffectiveGeometry:).

optional func windowScene(_ windowScene: UIWindowScene, didUpdateEffectiveGeometry previousEffectiveGeometry: UIWindowScene.Geometry)

This method is called by the system whenever the frame, coordinate space or interface orientation of a scene changes. It will also be called when a view controller's preference for an interface orientation lock changes.

Secondly, preferredWindowingControlStyle(for:).

optional func preferredWindowingControlStyle(for windowScene: UIWindowScene) -> UIWindowScene.WindowingControlStyle

This method is called by the system to determine the style of the horizontal traffic light window controls. As of now, there are three styles: automatic, minimal and unified, accessible as class variables of UIWindowScene.WindowingControlStyle.

extension UIWindowScene {
    class WindowingControlStyle: NSObject {
        class var automatic: UIWindowScene.WindowingControlStyle { get }
        class var minimal: UIWindowScene.WindowingControlStyle { get }
        class var unified: UIWindowScene.WindowingControlStyle { get }
    }
}

The automatic style will default to the system's preferred style which, at the time of writing, is unified. Below, you can see the subtle difference between the unified and minimal control styles.

UIWritingToolsCoordinator

The UIWritingToolsCoordinator class has been updated with a new property, includesTextListMarkers.

var includesTextListMarkers: Bool { get set }

Put simply, this controls whether or not the result of a writing tools operation includes prefixes, such as bullets or numbers, alongside list items.

In the example below, on the left, the writing tools coordinator's includesTextListMarkers property is set to false by default. On the right, the property is set to true - note the duplicated list markers.

You might want to set this property to true if you're still using TextKit, which expects list markers in the contents of an NSAttributedString for paragraphs associated with an NSTextList, versus TextKit 2, which does not.

UIWritingToolsResultOptions

The UIWritingToolsResultOptions option set has received a new presentationIntent option.

static var presentationIntent: UIWritingToolsResultOptions { get }

Prior to iOS 26, the writing tools result options allowed you to specify an output containing plain text, rich text, lists, tables, or any combination of the aforementioned formats. These are great, but offer limited scope for customisation, returning attributed strings with concrete stylistic formatting like font sizes.

The new presentationIntent option serves to eliminate this limitation. So, in the case of a section header, rather than Writings Tools returning an attributed string with attributes of a fixed font size and weight...

let attributes: [NSAttributedString.Key: Any] = [
    .font: UIFont.systemFont(ofSize: 24, weight: .semibold)
]

...it will return an attributed string with presentation intent attributes for you to render as you wish...

let attributes: [NSAttributedString.Key: Any] = [
    .presentationIntent: PresentationIntent(.header(level: 1), identity: 1)
]

And that's all! Not just on the UIWritingToolsResultOptions front, but for UIKit 26 altogether. I think you'll agree it's been a pretty big year for this soon to be deprecated framework!

If you've made it this far, thank you for reading! This article was a real labour of love, the culmination or many weeks worth of work alongside my full-time job. As a result, I've been coerced into adding a Buy Me a Coffe link so if you enjoyed this article feel free to do that. If you didn't enjoy this article, shout at me on Twitter!

Until next time...